ringzer0team.com JavaScript writeup

Client side validation is so secure?

预览

解题过程

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

// 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>");
	}
});

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

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事件断点,获取到相关验证代码如下:

// 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代码被压缩成一行,格式化后如下

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?

预览

解题过程

拦截到验证代码如下

// 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对应值相等,根据判断代码,有解密代码如下

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

预览

解题过程

设置断点

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

跟进去之后方法如下

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方法

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断点后,程序停在了这一块

$(".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?

预览

解题过程

关键代码如下

// 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代码执行。
我们继续向下看。

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方法。

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

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

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存在,我们应该可以跑出错序密码,然后可以手动进行调整。
测试代码如下

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如下

"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!

预览

解题过程

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

function btn_click(value) {
	try {
		if (check_password(document.getElementById('pwd').value)) {
			alert('That\'s the flag !');
			return;
		}
	} catch(e) {}

	alert('Nope !');
}

跟进check_password方法

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,由此,我们可以进行逐位猜解,最大限度的减少算力浪费。
测试代码如下。

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。

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报错。

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禁用断点,重新运行,发现报错不同。

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定义

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的值。

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报错。
定位代码为

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

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

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

定位代码如下

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

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

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

再次运行,错误如下

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

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

Challenge.prototype.calculate = function() {

跳转到Challenge的定义处

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属性从而报错,这里经过了一个三元运算,未被选择的正是正确代码段,我们将其更正为方法。
再次运行,错误如下

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方法有问题,格式化后代码如下

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

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

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

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

调用处代码美化后如下

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

跟进查看dummy定义

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

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

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

我们再查找一下b64的定义

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

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

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

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

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

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

Challenge.prototype.checkFirst = function(O) {
    var p = "U2hvY2t3YXZlIEZsYXNoIDIwLjAgcjBWRmM1Tm1GWGVITlpVemd4VEdwQlowdEdaSEJpYlZKMlpETk5aMVJzVVdkT2FUUjRUM2xDV0ZReFl6Sk9RMnRuVVZoQ2QySkhWbGhhVjBwTVlWaFJkazVVVFROTWFrMHlTVU5vVEZOR1VrNVVRM2RuWWtkc2NscFRRa2hhVjA1eVlubHJaMUV5YUhsaU1qRnNUSHBSTTB4cVFYVk5hbFY1VG1rME5FMURRbFJaVjFwb1kyMXJkazVVVFROTWFrMHk="
      , N = "E5";
    if (c8i[N](O, p)) {
        return c8i.e8O;
    }
    return c8i.j7O;
}

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

Shockwave Flash 20.0 r0

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字串。

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");

代码向下执行

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]赋所需的值。

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提示我们上面是正确的分支。
试着调试运算开始处的代码:

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的赋值也进行修正,然后继续向下执行

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

1 个赞