抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

在网上无意间看见一个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-i5-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系列有时间会去做做。

评论