在网上无意间看见一个CTF 解题平台,感觉里面的分类还挺有意思的。于是挑了一个javascript challenge来做一下。最近做题时经常感慨js的很多弱类型特性远比php丰富的多了,所以借此机会简单接触下js的魔鬼代码
0x01
首先js类型的题目必然是在静态页面就可以找到源码的。有时会加混淆,但混淆的机理其实基本上都挺一般的。这里直接查看页面源码
// 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>");
}
});
发现一个用户密码的判断。成功登陆即可完成事件。那么我们把密码扔进console转换得到JavaScriptIsSecure
.登陆拿flag.
0x02
// 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>");
}
});
这里直接用明确的算法对密码进行加密然后与hash值进行比较。不过注意的是php里常见的0与0e的弱类型比较在js中是不会出现的(但是可能会出现其他弱类型问题)
所以直接去网上找得到adminz
弱密码。
0x03
一个典型的混淆。同样也是典型的解法。
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]);} ;} );
我个人习惯是用这个网站先去一层混淆。然后再来看源码
var _0xc360 = ["val", "#cpass", "alk3", "02l1", "?p=", "indexOf", "href", "location", "<div class=\'error\'>Wrong password sorry.</div>", "html", "#cresponse", "click", ".c_submit"];
$(_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]);
};
});
其实还可以进一步去混淆。但是我并没有找到太好的网站。所以选择直接看。不难发现if语句及前面一行的内容是var _0xf382x2 = 'alk3'; if(_0xf382x1 == '02l1' + _0xf382x2 )
很清晰的表明了验证方式及密码02l1alk3
0x04
$(".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;
}
}
});
这里就开始使用了算法进行处理了。不过我们可以清晰的看到。user只做了一个16进制的简单显示避免直观。而pass在iv已知的情况下进行aes算法的密文比较。所以我们可以直接解出pass.
写个node脚本转换下密码
const cryptojs = require("crypto-js");
let k = cryptojs.SHA256("\x93\x39\x02\x49\x83\x02\x82\xf3\x23\xf8\xd3\x13\x37");
let key = cryptojs.enc.Hex.parse(k.toString().substring(0,32));
let iv = cryptojs.enc.Hex.parse(k.toString().substring(32,64));
let encrypted = "ob1xQz5ms9hRkPTx+ZHbVg==";
let p =cryptojs.AES.decrypt(encrypted,key,{iv:iv});
(function hex_to_ascii(str1) {
var hex = str1.toString();
var str = '';
for (var n = 0; n < hex.length; n += 2) {
str += String.fromCharCode(parseInt(hex.substr(n, 2), 16));
}
console.log(str);
} )(p);
运行得到
加上用户名扔进console其实就是h4x0r。所以即可登录拿flag.
0x05
function curry( orig_func ) {
var ap = Array.prototype, args = arguments;
function fn() {
ap.push.apply( fn.args, arguments );
return fn.args.length < orig_func.length ? fn : orig_func.apply( this, fn.args );
}
return function() {
fn.args = ap.slice.call( args, 1 );
return fn.apply( this, arguments );
};
}
function callback(x,y,i,a) {
return !y.call(x, a[a["length"]-1-i].toString().slice(19,21)) ? x : {};
}
var ref = {T : "BG8",J : "jep",j : "M2L",K : "L23",H : "r1A"};
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>");
}
}
function equal(o,o1)
{
var keys1 = Object.keys(o1);
var keys = Object.keys(o);
if(keys1.length != keys.length)
return false;
for(var i=0;i<keys.length;i++)
if(keys[i] != keys1[i] || o[keys[i]] != o1[keys1[i]])
return false;
return true;
}
function hook(f1,f2,f3) {
return function(x) { return f2(f1(x),f3(x));};
}
var h = curry(hook);
var fn = h(function(x) {return x >= 48;},new Function("a","b","return a && b;"));
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] + "'"));
}
这题开始难度就上升了。我们先慢慢审计代码。
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>");
}
}
这里取了页面中key值(即输入)进行一系列判断。只有e为false时我们才能进入document.location = document.location.href + "?p=" + _strKey;
注意到其中主要要求是var a = _strKey.split("-"); if(a.length !== 5)......
所以我们需要一个格式为xxx-xxx-xxx-xxx-xxx
的key.
接下来看其他函数。这里的curry()
是一个很有意思的js柯里化的函数( 为什么不是库里233) 。常用于多参函数复用并且与callback相结合。不过我们不必深究,先来看逻辑。
var h = curry(hook);
var fn = h(function(x) {return x >= 48;},new Function("a","b","return a && b;"));
首先是这里。出现了一个x>=48
,但是我们不知道x参数从哪里来。所以继续向下看
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] + "'"));
(fn(function(y){return y<=57})(_part.charCodeAt(0)) ? _part[0] : "'"+ _part[0] + "'")
这一部分说明其返回的是函数输入的第一个字符。看到这个IIFE的调用以及前面我们fn函数的构造。可以得出,这里对我们输入的第1个字符进行了比较,其ascii值应该在48到57之间(即数字)就直接取数字,否则就加上''
简单的调用下我们就可以看出来
而这个整体是与_part.substring(1,3)
作为参数被送进构造函数的。发现他是在key的检查里被调用了var o=a.map(genFunc).reduceRight(callback, new (genFunc(a[4]))(Function));
我们回头看下callback函数的定义以及reduceRight函数的功能
function callback(x,y,i,a) {
return !y.call(x, a[a["length"]-1-i].toString().slice(19,21)) ? x : {};
}
reduceRight() 方法接受一个函数作为累加器(accumulator)和数组的每个值(从右到左)将其减少为单个值。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/ReduceRight
参考上面的文档我们知道,callback作为回调函数,用于操作数组中的每个元素,而我们传入的a[4]生成的函数是数组最右的起点。之后依次向左进行处理。
这里我尽量作完整的解释。首先callback中a["length"]-1-i
对应的是a.length-1-i
即5-1-i
=4-i
.然后callback函数的四个参数分别对应了空map,构造函数,叠加器的趟数,以及被处理过的输入a(从xxxx-xxxx-xxxx-xxxx-xxxx
变成一个5元素的数组)我们简单调用下,会发现最终返回的结果中每个数组元素的第二,三个会不变,并变成新的map中对应值的第一二位。然后值的最后一位会变成原来输入数组的逆序第一位的第一个字母。而整个返回的map键名是原输入数组逆序的最后一个字母。
多说无益。我们用一个四元素的数组来看看返回结果
可以看到,返回的map表中第一个元素键名来自原来数组最后一个元素mnop
的最后一个字母。值部分bc
保持不变,最后一个字母则是mnop
的第一个字母m
.
既然我们需要满足输入key处理后为{T : "BG8",J : "jep",j : "M2L",K : "L23",H : "r1A"};
写个脚本处理下即可
var src = 'abcd-efgh-ijkl-mnop-qrst';
var dst = 'tbcq-pfgm-ljki-hnoe-drsa';
var key = 'TBG8-Jjep-jM2L-KL23-Hr1A';
var input = key;
for (i=0; i<src.length; i++) {
p = dst.indexOf(src[i]);
tmp = input.split('');
tmp[p] = key[i];
input = tmp.join('');
}
console.log(input);
得到keyABGH-3jeK-LM2j-pL2J-8r1T
0x06
// 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;
}
}
});
相比上一题算是小菜了。用户输入密码的ascii会与用户名administrator进行加法运算并与已知数组值进行比较。那么逆写即可
k = [176,214,205,246,264,255,227,237,242,244,265,270,283]
u = [97,100,109,105,110,105,115,116,114,97,116,111,114]
p = {}
s = "administrator"
list1=[]
for i in range(0, len(s)):
p[i] = k[i] - u[i] - i*10
list1.append(p[i])
print(''.join(chr(i) for i in list1))
得到OhLord4309111
0x07
// 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();
}
这里代码流程也比较复杂。简单概括就是去username作为输入然后每9个字符与所给的func进行异或。最后结果会转hex.
虽然我们不知道需要的username明文,但是可以利用a^b=c => a^c=b
这点,猜测密文中可能会存在跟前面的出flag一致的
if(document.location.href.indexOf("?p=") == -1) {
document.location = document.location.href + "?p=" + p;
}
所以我们简单爆破下username并进行内容筛选,找出有document出现的结果
这里借用另一位师傅的脚本https://github.com/NotSurprised/RingZer0-CTF-Writeup/blob/master/JavaScript/07.%20Why%20not%20be%20more%20secure/Why%20not%20be%20more%20secure.md
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";
function xor(ori_chr, dst_chr)
{
return String.fromCharCode(ori_chr.charCodeAt() ^ dst_chr.charCodeAt());
}
function decode(key)
{
var buffer = ''
for (var i = 0; i < func.length; i++)
{
buffer += xor(key[i % 9], func[i])
}
return buffer;
}
function guess(i, guesskey)
{
key = []
for (var j = 0; j < guesskey.length; j++)
{
key[(i+j) % 9] = xor(guesskey[j], func[i + j])
}
return key.join('');
}
for (var i = 0; i < func.length - 9; i++)
{
key = guess(i, 'document.');
finalbuffer = decode(key);
if (finalbuffer.indexOf('document.location') != -1)
{
console.log(i, key, finalbuffer);
}
}
拿到用户名密码
小结
简单小结下,js的trick还是挺多的,不过这几道题接触到trick的层面很浅。真正CTF比赛中运用到弱类型之类的倒是比较有意思。而这些challenge更多的就是利用js来混淆视听之类的,所以老实说比较考验代码审计的耐心跟debug水平。
这个平台的jail系列有时间会去做做。