ringzer0team.com WP JavaScript

题解

前言

ringzer0team.com是一个在线的CTF挑战平台,其中包含了前后端、逆向、编程、隐写、查证等多方面的题目,此次所写的writeup针对其中JavaScript系列题所做。

题解

Client side validation is so secure?

预览

解题过程

映入眼帘的是一个登陆框,随意输入账号密码可看到Wrong password sorry.的提示,且未有流量产生,由此可知是通过js判断账号密码的,并未向服务器发送查询请求。
因此我们拦截鼠标点击事件,为了避免jQuery之类的js影响,我们将jQuery和其他不必要的js文件加入blackbox,然后重新测试登陆。
点击后程序断在了这一块。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Look's like weak JavaScript auth script :)
$(".c_submit").click(function(event) {
event.preventDefault()
var u = $("#cuser").val();
var p = $("#cpass").val();
if(u == "admin" && p == String.fromCharCode(74,97,118,97,83,99,114,105,112,116,73,115,83,101,99,117,114,101)) {
if(document.location.href.indexOf("?p=") == -1) {
document.location = document.location.href + "?p=" + p;
}
} else {
$("#cresponse").html("<div class='alert alert-danger'>Wrong password sorry.</div>");
}
});

我们可以看到,其中有一条判断语句,正是其分支产生了错误提示:

1
if(u == "admin" && p == String.fromCharCode(74,97,118,97,83,99,114,105,112,116,73,115,83,101,99,117,114,101))

其中u来自id为cuser的文本框,p来自id为cpass的文本框,我们运行一下,即可得到密码:

提交即可得到flag

Link

Is hashing more secure?

预览

解题过程

同样的,我们设置click事件断点,获取到相关验证代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// Look's like weak JavaScript auth script :)
$(".c_submit").click(function(event) {
event.preventDefault();
var p = $("#cpass").val();
if(Sha1.hash(p) == "b89356ff6151527e89c4f3e3d30c8e6586c63962") {
if(document.location.href.indexOf("?p=") == -1) {
document.location = document.location.href + "?p=" + p;
}
} else {
$("#cresponse").html("<div class='alert alert-danger'>Wrong password sorry.</div>");
}
});

很显然,我们只要让Sha1.hash(p) == "b89356ff6151527e89c4f3e3d30c8e6586c63962"一句成立即可,查相关解密站点可知其明文为adminz,提交即可得到flag

Link

Then obfuscation is more secure?

预览

解题过程

设置断点后,程序停在了第83行,js代码被压缩成一行,格式化后如下

1
2
3
4
5
6
7
8
9
10
11
12
var _0xc360 = ["\x76\x61\x6C", "\x23\x63\x70\x61\x73\x73", "\x61\x6C\x6B\x33", "\x30\x32\x6C\x31", "\x3F\x70\x3D", "\x69\x6E\x64\x65\x78\x4F\x66", "\x68\x72\x65\x66", "\x6C\x6F\x63\x61\x74\x69\x6F\x6E", "\x3C\x64\x69\x76\x20\x63\x6C\x61\x73\x73\x3D\x27\x65\x72\x72\x6F\x72\x27\x3E\x57\x72\x6F\x6E\x67\x20\x70\x61\x73\x73\x77\x6F\x72\x64\x20\x73\x6F\x72\x72\x79\x2E\x3C\x2F\x64\x69\x76\x3E", "\x68\x74\x6D\x6C", "\x23\x63\x72\x65\x73\x70\x6F\x6E\x73\x65", "\x63\x6C\x69\x63\x6B", "\x2E\x63\x5F\x73\x75\x62\x6D\x69\x74"];
$(_0xc360[12])[_0xc360[11]](function () {
var _0xf382x1 = $(_0xc360[1])[_0xc360[0]]();
var _0xf382x2 = _0xc360[2];
if (_0xf382x1 == _0xc360[3] + _0xf382x2) {
if (document[_0xc360[7]][_0xc360[6]][_0xc360[5]](_0xc360[4]) == -1) {
document[_0xc360[7]] = document[_0xc360[7]][_0xc360[6]] + _0xc360[4] + _0xf382x1;
};
} else {
$(_0xc360[10])[_0xc360[9]](_0xc360[8]);
};
});

程序使用了大量的编码进行混淆,不过与前两题相同,在第5行有一个明显的判断语句,我们在第五行的位置设置断点,运行查看一下变量。

可以看到,实际上就是判断输入的值与02l1alk3是否相同,将02l1alk3作为密码输入,提交即可得到flag

Link

Why not?

预览

解题过程

拦截到验证代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Look's like weak JavaScript auth script :)
$(".c_submit").click(function(event) {
event.preventDefault();
var k = new Array(176,214,205,246,264,255,227,237,242,244,265,270,283);
var u = $("#cuser").val();
var p = $("#cpass").val();
var t = true;

if(u == "administrator") {
for(i = 0; i < u.length; i++) {
if((u.charCodeAt(i) + p.charCodeAt(i) + i * 10) != k[i]) {
$("#cresponse").html("<div class='alert alert-danger'>Wrong password sorry.</div>");
t = false;
break;
}
}
} else {
$("#cresponse").html("<div class='alert alert-danger'>Wrong password sorry.</div>");
t = false;
}
if(t) {
if(document.location.href.indexOf("?p=") == -1) {
document.location = document.location.href + "?p=" + p;
}
}
});

显然,我们要使u == "administrator"为真且(u.charCodeAt(i) + p.charCodeAt(i) + i * 10) != k[i]为假,则用户名为administrator,至于密码,则是要求用户名的ascii和密码的ascii相加,再加上位权,与k对应值相等,根据判断代码,有解密代码如下

1
2
3
4
5
var u = "administrator", z = "", k = new Array(176,214,205,246,264,255,227,237,242,244,265,270,283);
for(i = 0; i < u.length; i++) {
z += String.fromCharCode(k[i] - i * 10 - u.charCodeAt(i));
}
console.log(z);

运行代码得到密码,提交即可得到flag

Link

Valid key required

预览

解题过程

设置断点

点击提交后,程序断在了这里

跟进去之后方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function validatekey()
{
e = false;
var _strKey = "";
try {
_strKey = document.getElementById("key").value;
var a = _strKey.split("-");
if(a.length !== 5)
e = true;

var o=a.map(genFunc).reduceRight(callback, new (genFunc(a[4]))(Function));

if(!equal(o,ref))
e = true;

}catch(e){
e = true;
}

if(!e) {
if(document.location.href.indexOf("?p=") == -1) {
document.location = document.location.href + "?p=" + _strKey;
}
} else {
$("#cresponse").html("<div class='alert alert-danger'>Wrong password sorry.</div>");
}
}

输入的字串由-进行分割为数组,要求分割后的数组长度为5,随后使用genFunc方法映射。我们跟进genFunc方法

1
2
3
4
5
6
function genFunc(_part) {
if(!_part || !(_part.length) || _part.length !== 4)
return function() {};

return new Function(_part.substring(1,3), "this." + _part[3] + "=" + _part.slice(1,3) + "+" + (fn(function(y){return y<=57})(_part.charCodeAt(0)) ? _part[0] : "'"+ _part[0] + "'"));
}

genFunc方法要求满足传入参数不为空且长度为4,否则返回一个空的方法。
这表示,key为xxxx-xxxx-xxxx-xxxx-xxxx这样一串序列。
同时使用黑盒和白盒审计的方式往往能加快解决问题的效率,我们经过测试,发现最终只是将输入序列字串的位置进行了更改,那么我们就不再继续分析具体算法了。
输入abcd-efgh-ijkl-mnop-qrst,然后将断点设在if(!equal(o,ref))这里,重新点击提交,单步进入。
跟进equal方法之后,可以看到chrome打印出了传入的o和o1两个参数,

那么我们直接将被处理过后的字串复制出来,按位置替换后即可得到密码。

提交即可得到flag
Link

Most Secure Crypto Algo

预览

解题过程

设置click断点后,程序停在了这一块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$(".c_submit").click(function(event) {
event.preventDefault();
var k = CryptoJS.SHA256("\x93\x39\x02\x49\x83\x02\x82\xf3\x23\xf8\xd3\x13\x37");
var u = $("#cuser").val();
var p = $("#cpass").val();
var t = true;

if(u == "\x68\x34\x78\x30\x72") {
if(!CryptoJS.AES.encrypt(p, CryptoJS.enc.Hex.parse(k.toString().substring(0,32)), { iv: CryptoJS.enc.Hex.parse(k.toString().substring(32,64)) }) == "ob1xQz5ms9hRkPTx+ZHbVg==") {
t = false;
}
} else {
$("#cresponse").html("<div class='alert alert-danger'>Wrong password sorry.</div>");
t = false;
}
if(t) {
if(document.location.href.indexOf("?p=") == -1) {
document.location = document.location.href + "?p=" + p;
}
}
});

代码将指定字符串用sha256计算一遍,赋值给k,然后进入判断用户名。
我们将代码段选中运行一下

返回值为h4x0r,也就是说要求用户名为h4x0r
然后将密码部分使用AES进行加密,将运算结果与ob1xQz5ms9hRkPTx+ZHbVg==进行比较。
加密流程大致如下

p作为参数1(消息)
k转为字符串→取其前32位→使用CryptoJS.enc.Hex.parse方法处理,作为参数2(密钥)
k转为字符串→取第33-64位→使用CryptoJS.enc.Hex.parse方法处理,作为参数3(密钥向量)
使用以上参数进行AES加密

根据以上信息,我们得到了密文,密钥,密钥向量,四者得到三者,则可得到明文。
我们使用CryptoJS自带的解密方法进行解密

运行结果为HEX编码,解码提交即可得到flag

Link

Why not be more secure?

预览

解题过程

关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Look's like weak JavaScript auth script :)
$(".c_submit").click(function(event) {
event.preventDefault();
var u = $("#cpass").val();
var k = $("#cuser").val();
var func = "\x2B\x09\x4A\x03\x49\x0F\x0E\x14\x15\x1A\x00\x10\x3F\x1A\x71\x5C\x5B\x5B\x00\x1A\x16\x38\x06\x46\x66\x5A\x55\x30\x0A\x03\x1D\x08\x50\x5F\x51\x15\x6B\x4F\x19\x56\x00\x54\x1B\x50\x58\x21\x1A\x0F\x13\x07\x46\x1D\x58\x58\x21\x0E\x16\x1F\x06\x5C\x1D\x5C\x45\x27\x09\x4C\x1F\x07\x56\x56\x4C\x78\x24\x47\x40\x49\x19\x0F\x11\x1D\x17\x7F\x52\x42\x5B\x58\x1B\x13\x4F\x17\x26\x00\x01\x03\x04\x57\x5D\x40\x19\x2E\x00\x01\x17\x1D\x5B\x5C\x5A\x17\x7F\x4F\x06\x19\x0A\x47\x5E\x51\x59\x36\x41\x0E\x19\x0A\x53\x47\x5D\x58\x2C\x41\x0A\x04\x0C\x54\x13\x1F\x17\x60\x50\x12\x4B\x4B\x12\x18\x14\x42\x79\x4F\x1F\x56\x14\x12\x56\x58\x44\x27\x4F\x19\x56\x49\x16\x1B\x16\x14\x21\x1D\x07\x05\x19\x5D\x5D\x47\x52\x60\x46\x4C\x1E\x1D\x5F\x5F\x1C\x15\x7E\x0B\x0B\x00\x49\x51\x5F\x55\x44\x31\x52\x45\x13\x1B\x40\x5C\x46\x10\x7C\x38\x10\x19\x07\x55\x13\x44\x56\x31\x1C\x15\x19\x1B\x56\x13\x47\x58\x30\x1D\x1B\x58\x55\x1D\x57\x5D\x41\x7C\x4D\x4B\x4D\x49\x4F";
buf = "";
if(k.length == 9) {
for(i = 0, j = 0; i < func.length; i++) {
c = parseInt(func.charCodeAt(i));
c = c ^ k.charCodeAt(j);
if(++j == k.length) {
j = 0;
}
buf += eval('"' + a(x(c)) + '"');
}
eval(buf);
} else {
$("#cresponse").html("<div class='alert alert-danger'>Wrong password sorry.</div>");
}
});

function a(h) {
if(h.length != 2) {
h = "\x30" + h;
}
return "\x5c\x78" + h;
}

function x(d) {
if(d < 0) {
d = 0xFFFFFFFF + d + 1;
}
return d.toString(16).toUpperCase();
}

可以看到,我们输入的密码,也就是变量u,实际上并没有被用上,目前真正参与计算的是用户名。
当用户名长度为9时,其将用户名与预定义的一串字符进行系列运算,从而得到一个新字符串,然后将得到的字符串看作js代码执行。
我们继续向下看。

1
2
3
4
5
6
7
8
for(i = 0, j = 0; i < func.length; i++) {
c = parseInt(func.charCodeAt(i));
c = c ^ k.charCodeAt(j);
if(++j == k.length) {
j = 0;
}
buf += eval('"' + a(x(c)) + '"');
}

以k为密码,将func字符串进行xor运算。
将运算得到的字符带入x方法中,将该返回值再带入到a方法中。
我们跟进看x方法。

1
2
3
4
5
6
function x(d) {
if(d < 0) {
d = 0xFFFFFFFF + d + 1;
}
return d.toString(16).toUpperCase();
}

显然d<0不太可能成立,x方法将传入值转为16进制,然后返回,我们再跟进a方法。

1
2
3
4
5
6
function a(h) {
if(h.length != 2) {
h = "\x30" + h;
}
return "\x5c\x78" + h;
}

也就是说,当传入参数小于两个字符时,将在其前方填充"\x30"即字符0,随后统一填充"\x5c\x78"\x,将该值返回,因此,实际上返回值也就是\x11 \x01 之类。
而buf为用户名k与预定义字符串func进行异或运算所产生的明文,实际上是一串js代码。由于js代码属于文本,可读性非常高,由此可大范围缩减密码的范围。
对于异或运算,令a ^ b = c,则有a ^ c = b
我们目前知道func,而不知道真实buf和k。
由于buf是js代码,因此我们可以里面的关键字,如function,对于这题,按照前几题的代码看来,那么或许存在如document.location之类用于跳转的代码。
基于这种想法,我们可以写个代码跑一下密码。
如果字符串document.location = document.location.href存在,我们应该可以跑出错序密码,然后可以手动进行调整。
测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var func = "\x2B\x09\x4A\x03\x49\x0F\x0E\x14\x15\x1A\x00\x10\x3F\x1A\x71\x5C\x5B\x5B\x00\x1A\x16\x38\x06\x46\x66\x5A\x55\x30\x0A\x03\x1D\x08\x50\x5F\x51\x15\x6B\x4F\x19\x56\x00\x54\x1B\x50\x58\x21\x1A\x0F\x13\x07\x46\x1D\x58\x58\x21\x0E\x16\x1F\x06\x5C\x1D\x5C\x45\x27\x09\x4C\x1F\x07\x56\x56\x4C\x78\x24\x47\x40\x49\x19\x0F\x11\x1D\x17\x7F\x52\x42\x5B\x58\x1B\x13\x4F\x17\x26\x00\x01\x03\x04\x57\x5D\x40\x19\x2E\x00\x01\x17\x1D\x5B\x5C\x5A\x17\x7F\x4F\x06\x19\x0A\x47\x5E\x51\x59\x36\x41\x0E\x19\x0A\x53\x47\x5D\x58\x2C\x41\x0A\x04\x0C\x54\x13\x1F\x17\x60\x50\x12\x4B\x4B\x12\x18\x14\x42\x79\x4F\x1F\x56\x14\x12\x56\x58\x44\x27\x4F\x19\x56\x49\x16\x1B\x16\x14\x21\x1D\x07\x05\x19\x5D\x5D\x47\x52\x60\x46\x4C\x1E\x1D\x5F\x5F\x1C\x15\x7E\x0B\x0B\x00\x49\x51\x5F\x55\x44\x31\x52\x45\x13\x1B\x40\x5C\x46\x10\x7C\x38\x10\x19\x07\x55\x13\x44\x56\x31\x1C\x15\x19\x1B\x56\x13\x47\x58\x30\x1D\x1B\x58\x55\x1D\x57\x5D\x41\x7C\x4D\x4B\x4D\x49\x4F";
var a = "document.";
var b = "location ";
var c = "= documen";
for(var n = 9; n <= 126;){
for(var l = 0; l < func.length; l++ ){
if (l + 18 < func.length) {
for(var t = 0; t < 9; t++){
var flag = 0;
if (String.fromCharCode(func[l].charCodeAt() ^ n ) === a[t]) {
if (String.fromCharCode(func[l + 9].charCodeAt() ^ n ) === b[t]
&& String.fromCharCode(func[l + 18].charCodeAt() ^ n ) === c[t]
) {
if (flag === 0) {
console.log(n + "---" + String.fromCharCode(n) + "||\\x" + func[l].charCodeAt().toString(16) + "---" + a[t] + "<==>" + b[t]);
}
}
}
}
}
}
if (n === 9) {
n = 32;
}else{
n++;
}
}

运行结果如下所示

运行结果

由此,我们可以知道document.所对应的密钥为Bobvi2347。而xor加密是使用密钥循环进行加密的,因此无法直接判断密文开头所对应的密钥是否是B,我们需要进一步进行计算。
根据计算结果可知,document.中的d所对应的密文的编码为\x26,在密文中可找到对应的值

通过计算可知,\x26前面的字符长度为90,刚好是9的倍数,也就是说循环加密到d的时候刚好使用了密钥的第一个字符进行加密,则Bobvi2347这个顺序是对的。
将用户名改为该值,在eval(buf);处下断点,继续运行,可观察到buf如下

1
"if(u == "XorIsCoolButNotUnbreakable") { if(document.location.href.indexOf("?p=") == -1) { document.location = document.location.href + "?p=" + u; } } else {  $("#cresponse").html("<div class='error'>Wrong password sorry.</div>"); }"

由此拿到密码,提交拿到flag
Link

WTF Lol!

预览

解题过程

输入密码,拦截点击事件,程序先断在了这块

1
2
3
4
5
6
7
8
9
10
function btn_click(value) {
try {
if (check_password(document.getElementById('pwd').value)) {
alert('That\'s the flag !');
return;
}
} catch(e) {}

alert('Nope !');
}

跟进check_password方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function check_password(password) {
var stack = "qwertyuiopasdfghjklzxcvbnm".split("");
var tmp = {
"t" : 9, "h" : 6, "e" : 5,
"f" : 1, "l" : 2, "a" : 3, "g" : 4,
"i" : 7, "s" : 8,
"j" : 10, "u" : 11, "m" : 12, "p" : 13,
"b" : 14, "r" : 15, "o" : 16, "w" : 17, "n" : 18,
"c" : 19, "d" : 20, "j" : 21, "k" : 22, "q" : 23,
"v" : 24, "x" : 25, "z" : 26
};

var i = 2;

var a1 = Number.prototype.valueOf;
var a2 = Number.prototype.toString;
var a3 = Array.prototype.valueOf;
var a4 = Array.prototype.toString;
var a5 = Object.prototype.valueOf;
var a6 = Object.prototype.toString;

function f1() { return stack[ i++ % stack.length ].charCodeAt(0); }
function f2() { i += 3; return stack.pop(); }
function f3() {
for (k in this) {
if (this.hasOwnProperty(k)) {
i += stack.indexOf(this[k][0]);
stack.push(this[k]);
}
}
return String.fromCharCode(new Number(stack[ i % stack.length ].charCodeAt(0)));
}

Number.prototype.valueOf = Number.prototype.toString = f1;
Array.prototype.valueOf = Array.prototype.toString = f2;
Object.prototype.valueOf = Object.prototype.toString = f3;

var a = (tmp[ [] ] * tmp[ [] ] * 1337 + tmp[ "" + { "wtf": password[1] } ]) / (tmp[ "" + { "wtf": password[0] } ] - tmp[ [] ]);
var b = (tmp[ [] ] * tmp[ [] ] * 7331 + tmp[ "" + { "lol": "o" } ]) / (tmp[ "" + { "wtf": password[1] } ] - tmp[ [] ]);
var c = (tmp[ [] ] * tmp[ [] ] * 1111 + tmp[ "" + { "wtf": password[3] } ]) / (tmp[ "" + { "lol": password[2] } ] - tmp[ [] ]);
var d = (tmp[ [] ] * tmp[ [] ] * 3333 + tmp[ "" + { "wtf": "g" } ]) / (tmp[ "" + { "wtf": password[3] } ] - tmp[ [] ]);
var e = (tmp[ [] ] * tmp[ [] ] * 7777 + tmp[ "" + { "wtf": "a" } ]) / (tmp[ "" + { "wtf": password[7] } ] - tmp[ [] ]);
var f = (tmp[ [] ] * tmp[ [] ] * 2222 + tmp[ "" + { "wtf": password[7] } ]) / (tmp[ "" + { "lol": password[5] } ] - tmp[ [] ]);
var g = (tmp[ [] ] * tmp[ [] ] * 6666 + tmp[ "" + { "lol": password[4] } ]) / (tmp[ "" + { "wtf": password[6] } ] - tmp[ [] ]);
var h = (tmp[ [] ] * tmp[ [] ] * 1234 + tmp[ "" + { "wtf": "a" } ]) / (tmp[ "" + { "wtf": password[4] } ] - tmp[ [] ]);
var ii = (tmp[ [] ] * tmp[ [] ] * 2345 + tmp[ "" + { "wtf": "h" } ]) / (tmp[ "" + { "wtf": password[9] } ] - tmp[ [] ]);
var j = (tmp[ [] ] * tmp[ [] ] * 3456 + tmp[ "" + { "wtf": password[9] } ]) / (tmp[ "" + { "lol": password[8] } ] - tmp[ [] ]);
var kk = (tmp[ [] ] * tmp[ [] ] * 4567 + tmp[ "" + { "lol": password[11] } ]) / (tmp[ "" + { "wtf": password[10] } ] - tmp[ [] ]);
var l = (tmp[ [] ] * tmp[ [] ] * 9999 + tmp[ "" + { "wtf": "o" } ]) / (tmp[ "" + { "wtf": password[11] } ] - tmp[ [] ]);

Number.prototype.valueOf = a1;
Number.prototype.toString = a2;
Array.prototype.valueOf = a3;
Array.prototype.toString = a4;
Object.prototype.valueOf = a5;
Object.prototype.toString = a6;

var m = a === b && b === c && c === d && d === e && e === f && f === g && g === h && h === ii && ii === j && j === kk && kk === l;
var n = password[0] != password[1] && password[2] != password[3] && password[4] != password[5] && password[6] != password[7] && password[8] != password[9] && password[10] != password[11]

return m && n;
}

这道题替换了Number Array Object这几个对象类型自带的valueOf toString方法,而开发者工具的调试正是使用了这些方法,因此在使用调试时查看变量值、设置断点、代码测试,都会导致其中关键值i的改变。
而这道题又要求计算出一个值,使得其中的a b c d e f g h ii j kk l完全相等。
但是在另一方面,我们注意到,在计算完毕之后,对象的方法又被还原回来了。因此我们可以将其计算部分的代码段看作黑盒,然后进行猜解,只要不动方法被改过的那部分代码就可以了。
由于算力问题,爆破12位字符是不现实的,但是由于判断的时候,是先判断a===b,如果通过,则判断b===c,否则直接返回false,由此,我们可以进行逐位猜解,最大限度的减少算力浪费。
测试代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
function check_password(password) {
var stack = "qwertyuiopasdfghjklzxcvbnm".split("");
var tmp = {
"t" : 9, "h" : 6, "e" : 5,
"f" : 1, "l" : 2, "a" : 3, "g" : 4,
"i" : 7, "s" : 8,
"j" : 10, "u" : 11, "m" : 12, "p" : 13,
"b" : 14, "r" : 15, "o" : 16, "w" : 17, "n" : 18,
"c" : 19, "d" : 20, "j" : 21, "k" : 22, "q" : 23,
"v" : 24, "x" : 25, "z" : 26
};

var i = 2;

var a1 = Number.prototype.valueOf;
var a2 = Number.prototype.toString;
var a3 = Array.prototype.valueOf;
var a4 = Array.prototype.toString;
var a5 = Object.prototype.valueOf;
var a6 = Object.prototype.toString;

function f1() {
return stack[ i++ % stack.length ].charCodeAt(0);
}
function f2() {
i += 3; return stack.pop();
}
function f3() {
for (k in this) {
if (this.hasOwnProperty(k)) {
i += stack.indexOf(this[k][0]);
stack.push(this[k]);
}
}
return String.fromCharCode(new Number(stack[ i % stack.length ].charCodeAt(0)));
}

Number.prototype.valueOf = Number.prototype.toString = f1;
Array.prototype.valueOf = Array.prototype.toString = f2;
Object.prototype.valueOf = Object.prototype.toString = f3;
var a = (tmp[ [] ] * tmp[ [] ] * 1337 + tmp[ "" + { "wtf": password[1] } ]) / (tmp[ "" + { "wtf": password[0] } ] - tmp[ [] ]);
// 确定第0位
var b = (tmp[ [] ] * tmp[ [] ] * 7331 + tmp[ "" + { "lol": "o" } ]) / (tmp[ "" + { "wtf": password[1] } ] - tmp[ [] ]);
// 确定第1位
var c = (tmp[ [] ] * tmp[ [] ] * 1111 + tmp[ "" + { "wtf": password[3] } ]) / (tmp[ "" + { "lol": password[2] } ] - tmp[ [] ]);
// 确定第2位
var d = (tmp[ [] ] * tmp[ [] ] * 3333 + tmp[ "" + { "wtf": "g" } ]) / (tmp[ "" + { "wtf": password[3] } ] - tmp[ [] ]);
// 确定第3位
var e = (tmp[ [] ] * tmp[ [] ] * 7777 + tmp[ "" + { "wtf": "a" } ]) / (tmp[ "" + { "wtf": password[7] } ] - tmp[ [] ]);
// 限制第7位
var f = (tmp[ [] ] * tmp[ [] ] * 2222 + tmp[ "" + { "wtf": password[7] } ]) / (tmp[ "" + { "lol": password[5] } ] - tmp[ [] ]);
// 确定第7位,确定第5位
var g = (tmp[ [] ] * tmp[ [] ] * 6666 + tmp[ "" + { "lol": password[4] } ]) / (tmp[ "" + { "wtf": password[6] } ] - tmp[ [] ]);
// 限制第4位,限制第6位
var h = (tmp[ [] ] * tmp[ [] ] * 1234 + tmp[ "" + { "wtf": "a" } ]) / (tmp[ "" + { "wtf": password[4] } ] - tmp[ [] ]);
// 确定第4位,从而确定第6位
var ii = (tmp[ [] ] * tmp[ [] ] * 2345 + tmp[ "" + { "wtf": "h" } ]) / (tmp[ "" + { "wtf": password[9] } ] - tmp[ [] ]);
// 限制第9位
var j = (tmp[ [] ] * tmp[ [] ] * 3456 + tmp[ "" + { "wtf": password[9] } ]) / (tmp[ "" + { "lol": password[8] } ] - tmp[ [] ]);
// 确定第8 9位
var kk = (tmp[ [] ] * tmp[ [] ] * 4567 + tmp[ "" + { "lol": password[11] } ]) / (tmp[ "" + { "wtf": password[10] } ] - tmp[ [] ]);
// 限制10 11位
var l = (tmp[ [] ] * tmp[ [] ] * 9999 + tmp[ "" + { "wtf": "o" } ]) / (tmp[ "" + { "wtf": password[11] } ] - tmp[ [] ]);
// 确定11位,从而确定10位
// 0 1 2 3 7 5 4 6 9 8 11 10

Number.prototype.valueOf = a1;
Number.prototype.toString = a2;
Array.prototype.valueOf = a3;
Array.prototype.toString = a4;
Object.prototype.valueOf = a5;
Object.prototype.toString = a6;

if (a !== b) {
return '0-a-b';
} else if (b !== c) {
return '1-b-c';
} else if (c !== d) {
return '2-c-d';
} else if (d !== e) {
return '3-d-e';
} else if (e !== f) {
return '4-e-f';
} else if (f !== g) {
return '5-f-g';
} else if (g !== h) {
return '6-g-h';
} else if (h !== ii) {
return '7-h-ii';
} else if (ii !== j) {
return '8-ii-j';
} else if (j !== kk) {
return '9-j-k';
} else if (kk !== l) {
return '10-kk-l';
} else {
console.log(password);
return true;
}
}

var flag = new Array(12);
for (flag[0] = 32; result !== true && flag[0] <= 127; flag[0]++) {
for (flag[1] = 32; result !== true && flag[1] <= 127; flag[1]++) {
for (flag[2] = 32; flag[1] != flag[0] && result !== true && flag[2] <= 127; flag[2]++) {
for (flag[3] = 32; flag[2] != flag[1] && result !== true && flag[3] <= 127; flag[3]++) {
for (flag[7] = 32; flag[3] != flag[2] && result !== true && flag[7] <= 127 && flag[1] <= 127 && flag[3] <= 127 ; flag[7]++) {
for (flag[5] = 32; result !== true && flag[5] <= 127 && flag[1] <= 127 && flag[2] <= 127 && flag[3] <= 127 && flag[7] <= 127; flag[5]++) {
for (flag[6] = 32; result !== true && flag[6] <= 127 && flag[1] <= 127 && flag[2] <= 127 && flag[3] <= 127 && flag[7] <= 127; flag[6]++) {
for (flag[4] = 32; flag[7] != flag[6] && flag[5] != flag[6] && result !== true && flag[4] <= 127 && flag[1] <= 127 && flag[2] <= 127 && flag[3] <= 127 && flag[7] <= 127; flag[4]++) {
for (flag[9] = 32; flag[5] != flag[4] && flag[3] != flag[4] && result !== true && flag[9] <= 127 && flag[1] <= 127 && flag[2] <= 127 && flag[3] <= 127 && flag[7] <= 127; flag[9]++) {
for (flag[8] = 32; result !== true && flag[8] <= 127 && flag[1] <= 127 && flag[2] <= 127 && flag[3] <= 127 && flag[7] <= 127 && flag[9] <= 127; flag[8]++) {
for (flag[11] = 32; flag[9] != flag[8] && flag[8] != flag[7] && result !== true && flag[11] <= 127 && flag[1] <= 127 && flag[2] <= 127 && flag[3] <= 127 && flag[7] <= 127 && flag[9] <= 127; flag[11]++) {
for (flag[10] = 32; result !== true && flag[10] <= 127 && flag[4] <= 127 && flag[1] <= 127 && flag[2] <= 127 && flag[3] <= 127 && flag[7] <= 127 && flag[9] <= 127; flag[10]++) {
if (flag[10] != flag[11] && flag[10] != flag[9]) {
var result = check_password(String.fromCharCode(flag[0], flag[1], flag[2], flag[3], flag[4], flag[5], flag[6], flag[7], flag[8], flag[9], flag[10], flag[11]));
switch (result) {
case '0-a-b':flag[1]++;break;
case '1-b-c':flag[3]++;break;
case '2-c-d':flag[3]++;break;
case '3-d-e':flag[7]++;break;
case '4-e-f':flag[5]++;break;
case '5-f-g':flag[4]++;break;
case '6-g-h':flag[4]++;break;
case '7-h-ii':flag[9]++;break;
case '8-ii-j':flag[8]++;break;
case '9-j-k':flag[10]++;break;
default:
if (result === true) {
console.log(result);
console.log("find the flag!");
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}

运行后可得到密码,提交拿到flag

Link

Beauty and the beast

预览

解题过程

打开开发者工具,发现直接被debugger断下,无法跳出,因此我们通过源代码寻找js代码

在其他空白页面开启F12调试,新建代码片段,记为true_orgin,然后运行片段。

可以发现直接被debugger断下,可以直接查看调用栈,找到前两个debugger。

1
2
3
4
5
6
7
8
9
10
11
try {
(function R7(O) {
if ((i[2] + (O / O))[i[3]] !== 1 || O % 20 === 0) {
(function() {}
).constructor(i[37])(); //i[37]值为debugger
} else {
debugger ;
}
R7(++O);
}(0))
} catch (O) {}

可以看出这段代码就是为了调试时永远进入debugger的。注释掉相关语句重新运行,发现console报错。

1
2
3
true_orgin:402 Uncaught TypeError: Cannot read property 'b4O' of undefined
at Function.c8i.k4O (<anonymous>:402:24)
at <anonymous>:405:21

为了分析原因,我们先将原始代码再复制为另一段js片段,记为the_true_orgin,分析是否是原始代码有问题,并Ctrl+F8禁用断点,重新运行,发现报错不同。

1
2
3
the_true_orgin:1 Uncaught ReferenceError: module is not defined
at <anonymous>:1:26508
at <anonymous>:1:27781

前一个提示的是c8i.k4O方法不存在,在代码格式化后的第402行,后一个的报错点则是第634行,显然原始代码中c8i.k4O方法是存在的,由此基本确定是对代码的改动导致的报错。
跟踪查看c8i定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var c8i = (function B(t, n) {
var E = ''
, D = decodeURIComponent(/* 此处省略1k多密文字符 */);
for (var o = 0, Z = 0; o < D["length"]; o++,
Z++) {
if (Z === n["length"]) {
Z = 0;
}
E += String["fromCharCode"](D["charCodeAt"](o) ^ n["charCodeAt"](Z)); //xor解密
}
var i = E.split('<,>');
// 此处省略两百多行代码
}
)(decodeURIComponent(/* 此处省略1W多个密文字符 */), "htq8Ure6eWWrIzyfUZbwXF60zbDctikoSyNkrYoSSTj1EE6O");

程序先定义B方法,然后将密文和密码传入B方法,而c8i获取到的是其运行后的返回值。
持续跟进,可以发现代码里使用了大量的异常捕获来保证程序的继续运行。而B方法最终在199行有如下代码,决定了c8i的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
try {
var x = 0
, U = 23
, l = [];
l[x] = y[i[0]](Z7(y[i[1]] + i[2])) + i[2];
var i7 = l[x][i[3]];
for (var o = t[i[3]] - 1, Z = 0; o >= 0; o--,
Z++) {
if (Z === i7) {
Z = 0;
if (++x === U) {
x = 0;
}
if (l[i[3]] < U) {
l[x] = y[i[0]](l[x - 1], l[x - 1]) + i[2];
}
i7 = l[x][i[3]];
}
w = String[i[4]](t[i[5]](o) ^ l[x][i[5]](Z)) + w;
}
var V = (0,
eval)(w);
if (typeof V === i[6]) {
for (var h in V) {
if (V[i[7]](h) && typeof V[h] === i[8]) {
V[h][i[9]] = V[h][i[10]] = function() {
return i[2];
}
;
}
}
}
(function x7(O) {
if (typeof O === i[6]) {
for (var p in O) {
if (O[i[7]](p)) {
if (typeof O[p] === i[8]) {
O[p][i[9]] = O[p][i[10]] = function() {
return i[2];
}
;
} else if (typeof O[p] === i[6]) {
x7(O[p]);
}
}
}
}
}
)(V);
if (typeof V !== i[11])
V[i[9]] = V[i[10]] = function() {
return i[2];
}
;
return V;
} catch (O) {
return function() {}
;
}

我们将断点设在var V = (0,eval)(w);一句,在Watch区域监视w和c8i,然后重新运行代码,断在了第219行,可以看到w是一串乱码。

因此eval语句报错。
上面这段代码使用了大量的变量用以混淆,实际上其流程是:获取到自身方法B的代码,并参与运算,得到一个w,然后运行w并捕获异常,若无异常,程序向下执行,最终返回对象V,若发生异常,则会导致返回一个空方法,导致c8i为空,从而使得后续代码调用c8i相关方法失败,显然这就是上面console报错的原因了。
即,对原始代码的修改,如格式化、注释,都将导致c8i为空,但如果不修改,其代码片段开头的debugger将导致代码持续断在代码开头,无法向下运行,即使禁用断点,也会导致后面无法使用断点进行动态分析,从而增加分析的难度。
为了解决这个问题,我们可以在格式化后的代码中注入原始代码字符串,由于代码原本是获取到自身代码,并转为字符串,因此我们只要将其替换为原始代码字符串即可。
或者采用另外一种方法,即从开发者工具的代码虚拟机中调出c8i。
由于未分析完全,第一种方法由于不知名的原因导致失败,我们采用了第二种方式。
同样的,禁用断点,运行原始代码,esc调出console,查看c8i,点开其中任一方法,查看其定义。

进入定位B7的定义代码片段

成功在代码虚拟机里找到了c8i的完整定义。删除其中的debugger相关代码,并用该定义替换原先代码中c8i的定义。
运行后也报了module的错,说明成功了。
接下来修复module报错。
定位代码为

1
if (typeof module !== y && module[k]) {

根据上下文可判断其为flash相关代码,因此我在GitHub寻找了相关代码作为参考。
向上查找y的定义y = c8i.X4O("d8d8") ? "ShockwaveFlash.ShockwaveFlash" : 'undefined',修改为undefined
修改后运行,错误如下

1
2
3
4
5
true_orgin:515 Uncaught TypeError: p[A] is not a function
at v (<anonymous>:515:19)
at <anonymous>:519:13
at <anonymous>:521:6
at <anonymous>:597:2

定位代码如下

1
g[p[A](K)] = O;	//即g.toBase64URI.activeXDetectRules(0)

查找A的定义A = c8i.n4O("4a") ? "activeXDetectRules" : "charAt",修改为charAt

1
g[p.charAt(K)] = O;

再次运行,错误如下

1
2
true_orgin:1247 Uncaught TypeError: Cannot set property 'calculate' of undefined
at <anonymous>:1247:31

定位到1247行的31列,其设置了Challenge对象的方法,根据提示可以看出是Challenge的prototype不存在导致的

1
Challenge.prototype.calculate = function() {

跳转到Challenge的定义处

1
2
3
4
5
var Challenge = c8i.k4O("b6d3") ? function(O) {
this[c8i.X7O] = c8i.a4O("7a") ? O : "re_utob";
this[c8i.y8O] = c8i.r4O("73") ? '-' : Base64;
}
: "version";

显然由于字符串不存在prototype属性从而报错,这里经过了一个三元运算,未被选择的正是正确代码段,我们将其更正为方法。
再次运行,错误如下

1
2
3
true_orgin:1247 Uncaught TypeError: this[c8i.y8O][c8i.n8O] is not a function
at Challenge.calculate (<anonymous>:1247:43)
at <anonymous>:1312:15

定位后确定是calculate方法有问题,格式化后代码如下

1
2
3
4
Challenge.prototype.calculate = function() {
this["data"] = this["instance"]["encode"](this["data"]);
return this;
}

向上查找,依然是Challenge方法有问题,Challenge.instance.encode方法不存在,修正后如下

1
2
3
4
var Challenge = function(O) {
this["data"] = O;
this["instance"] = Base64;
};

回过头看calculate的代码,大意是将data属性编码用base64,不过这里Base64未定义,相关代码需要修正,因此我们在相关调用编码的代码部分,手动赋值编码后的字符串。
另外为了防止this["instance"] = Base64;由于Base64未定义导致的报错,将其改成"Base64"
由于calculate方法无法使用,因此将断点设在calculate。

调用处代码美化后如下

1
dummy["calculate"]()["secondRound"](versioncheck);

跟进查看dummy定义

1
2
var dummy = new Challenge(navigator["userAgent"])
, versioncheck = FlashDetect["installed"];

可以看到,其data属性即浏览器User-Agent,同时检测了Flash插件的安装情况。
此处我们替换navigator["userAgent"]为准确UA字符串,同时开启chrome默认关闭的flash。
至此,dummy对象的data属性保存了UA,并将data进行base64编码。
继续跟进secondRound方法。

1
2
3
4
5
Challenge.prototype.secondRound = function(O) {
var p = "b64";
this["data"] = O + this[p](this["data"]);
this["data"] = this[p](this["data"]);
}

我们再查找一下b64的定义

1
2
3
Challenge.prototype.b64 = function(O) {
return this["instance"]["data"](O);
}

可以知道,其大意是返回data属性编码后的字串。
则secondRound方法流程为:将data编码,再加入flash版本,再编码,替换data。
完成后代码向下执行。

1
var versioncheck = dummy["get"](), kj4kjhkj43w980 = "error.js";

其中get方法用于获取到data属性值。
此时versioncheck为编码后的Flash版本和UA,代码向下执行。

1
2
3
if (dummy["checkFirst"](versioncheck)) {
kj4kjhkj43w980 = CryptoJS["SHA1"](dummy["lkslkj5lkj"]());
}

其中lkslkj5lkj方法获取到的是浏览器UA。
跟进checkFirst

1
2
3
4
5
6
7
8
Challenge.prototype.checkFirst = function(O) {
var p = "U2hvY2t3YXZlIEZsYXNoIDIwLjAgcjBWRmM1Tm1GWGVITlpVemd4VEdwQlowdEdaSEJpYlZKMlpETk5aMVJzVVdkT2FUUjRUM2xDV0ZReFl6Sk9RMnRuVVZoQ2QySkhWbGhhVjBwTVlWaFJkazVVVFROTWFrMHlTVU5vVEZOR1VrNVVRM2RuWWtkc2NscFRRa2hhVjA1eVlubHJaMUV5YUhsaU1qRnNUSHBSTTB4cVFYVk5hbFY1VG1rME5FMURRbFJaVjFwb1kyMXJkazVVVFROTWFrMHk="
, N = "E5";
if (c8i[N](O, p)) {
return c8i.e8O;
}
return c8i.j7O;
}

其中c8i.E5方法起到判断相等的作用。
我们将p解码几次,即可得到要求的版本

1
Shockwave Flash 20.0 r0

1
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36

随后将UA用sha1计算,这里我们将其替换为所需的UA字串。

1
kj4kjhkj43w980 = CryptoJS["SHA1"]("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36");

代码向下执行

1
2
3
4
5
6
7
8
9
10
11
var suffix = new Array();
suffix[c8i.q3] = parseInt(dummy["doGet"](c8i.b3));
suffix[c8i.Q3] = parseInt(dummy["doGet"](c8i.h8O));
suffix[c8i.W3] = dummy["doGet"](c8i.S8O);
suffix[c8i.u3] = dummy["doGet"](c8i.X9);
suffix[c8i.E3] = dummy["doGet"](c8i.s3);
c8i[c8i.u9]();
suffix[5] = suffix[5]["toString"]();
if (c8i["U5"](suffix[5]["length"], 6) && c8i["p3"](CryptoJS["SHA1"](suffix[5]), "be084fcf0f18867dd613af99c8cff52bdfa6037f")) {
kj4kjhkj43w980 += suffix[5] + ".js";
}

方法U5和p3用于判断相等,doGet方法用于获取url中指定参数的值。
这里suffix[5]由suffix[c8i.q3] suffix[c8i.Q3] suffix[c8i.W3] suffix[c8i.u3] suffix[c8i.E3]计算得出,我们不再从url加入查询语句,直接为suffix[5]赋所需的值。

1
if (c8i["U5"](suffix[5]["length"], 6) && c8i["p3"](CryptoJS["SHA1"](suffix[5]), "be084fcf0f18867dd613af99c8cff52bdfa6037f"))

由该判断语句可知,suffix[5]长度理应为6,且SHA1计算值为be084fcf0f18867dd613af99c8cff52bdfa6037f,根据上方相应代码,我们还可得知,suffix[5]为数字,根据以上提示,我们可以写出爆破算法,寻找该六位的数字,或者到相关解密站点进行解密。
解得明文为124341。
判断成功后,引入了变量kj4kjhkj43w980所代表的js文件。

代码段先定义了一个r9i对象,随后定义了相关方法。
我们查看最后的判断分支。

!erroralert alert-success提示我们上面是正确的分支。
试着调试运算开始处的代码:

1
2
3
var flag = r9i.u4("25") ? r9i.D : '%20'
, data = r9i.L4("141d") ? r9i.d : "No FLAG for you"
, error = r9i.l4("58a3") ? "nKUp5vr4JC7zsxR3pI2dS7J" : r9i.f;

我们通过查看当前值可以发现,此时data赋值为"No FLAG for you"error赋值为"nKUp5vr4JC7zsxR3pI2dS7J",由此将导致无法进入正确分支,因此我们需要修改代码。
根据data和error的提示,我们将flag的赋值也进行修正,然后继续向下执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (r9i["g"](aslkddalkj3("klsdslk2"), "Beast")) {	//如果url中参数klsdslk2值为Beast的话,进入这条分支
var lkejtlkjw = r9i.J4("dd") ? "lkfejskl4kjlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkfejskl4kjlkjtrlkjtr" : sdflkdsflklkjfddddd(r9i.o);
data += r9i.I4("b71") ? r9i.S : "lkasdlkdsa";
} else {
var t = r9i.v4("bc") ? function(i) {
data = r9i.S4("c8dc") ? i : "flag";
}
: ""
, M = r9i.O4("13d") ? function(i) {
error = i;
}
: "lkasdlkdsa";
t(r9i.O);
M(r9i.Q);
}

当url中参数klsdslk2值为Beast时,可进入上方分支,考虑到标题为Beauty and the beast,因此上方可能是正确代码段。此处url相关参数不参与任何计算,因此我们直接将其判断语句修改为true,进入上方语句。
最后再根据报错和题目提示,改几处三元运算的赋值,即可拿到FLAG。

Link

Author: hundan
Link: https://hundan.org/2018/08/12/writeup-ringzer0team-js/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.