相关文章推荐
光明磊落的豆腐  ·  配置 SQL Server·  2 月前    · 
近视的橙子  ·  db.collection.findOneA ...·  5 月前    · 
活泼的红烧肉  ·  QListWidgetItem — Qt ...·  6 月前    · 

虽然如此,但其实题目隐藏了很多线索。因此要解题,首先需要知道作者究竟真正想隐藏的是什么。

只有透过现象看本质,抽丝剥茧,方得真相。

源码梳理

开启挑战页面后,要求输入密码,反正不知道,随便输就好,一段法语提示说密码错误。

提取页面源码,发现是一段混淆加密过的 Javascript 代码 :

< meta http - equiv = "Content-Type" content = "text/html; charset=UTF-8" > But du jeu : trouver le bon mdp ! Il vous faudra de la patience et de l intuition ! Si vous avez des problèmes , contactez aaSSfxxx sur #root - me < script > var ð = "\x71\x11\x24\x59\x8d\x6d\x71\x11\x35\x16\x8c\x6d\x71\x0d\x39\x47\x1f\x36\xf1\x2f\x39\x36\x8e\x3c\x4b\x39\x35\x12\x87\x7c\xa3\x10\x74\x58\x16\xc7\x71\x56\x68\x51\x2c\x8c\x73\x45\x32\x5b\x8c\x2a\xf1\x2f\x3f\x57\x6e\x04\x3d\x16\x75\x67\x16\x4f\x6d\x1c\x6e\x40\x01\x36\x93\x59\x33\x56\x04\x3e\x7b\x3a\x70\x50\x16\x04\x3d\x18\x73\x37\xac\x24\xe1\x56\x62\x5b\x8c\x2a\xf1\x45\x7f\x86\x07\x3e\x63\x47" ; function _ ( x , y ) { return x ^ y ; function __ ( y ) { var z = 0 ; for ( var i = 0 ; i < y ; i ++ ) { z + = Math . pow ( 2 , i ) ; return z ; function ___ ( y ) { var z = 0 ; for ( var i = 8 - y ; i < 8 ; i ++ ) { z + = Math . pow ( 2 , i ) ; return z function ____ ( x , y ) { y = y % 8 ; Ï = __ ( y ) ; Ï = ( x & Ï ) < < ( 8 - y ) ; return ( Ï ) + ( x > > y ) ; function _____ ( x , y ) { y = y % 8 ; Ï = ___ ( y ) ; Ï = ( x & Ï ) > > ( 8 - y ) ; return ( ( Ï ) + ( x < < y ) ) & 0x00ff ; function ______ ( x , y ) { return _____ ( x , y ) function _______ ( _________ , key ) { ________ = "" ; ________2 = "" ; for ( var i = 0 ; i < _________ . length ; i ++ ) { c = _________ . charCodeAt ( i ) ; if ( i != 0 ) { t = ________ . charCodeAt ( i - 1 ) % 2 ; switch ( t ) { case 0 : cr = _ ( c , key . charCodeAt ( i % key . length ) ) ; break ; case 1 : cr = ______ ( c , key . charCodeAt ( i % key . length ) ) ; break ; } else { cr = _ ( c , key . charCodeAt ( i % key . length ) ) ; ________ + = String . fromCharCode ( cr ) ; return ________ ; function __________ ( þ ) { var ŋ = 0 ; for ( var i = 0 ; i < þ . length ; i ++ ) { ŋ + = þ [ "charCodeAt" ] ( i ) if ( ŋ == 8932 ) { var ç = window . open ( "" , "" , "\x77\x69\x64\x74\x68\x3d\x33\x30\x30\x2c\x68\x65\x69\x67\x68\x74\x3d\x32\x20\x30" ) ; ç . document . write ( þ ) } else { alert ( "Mauvais mot de passe!" ) __________ ( _______ ( ð , prompt ( "Mot de passe?" ) ) ) ; < / script > < / head > < link rel = 'stylesheet' property = 'stylesheet' id = 's' type = 'text/css' href = '/template/s.css' media = 'all' / > < iframe id = 'iframe' src = 'https://www.root-me.org/?page=externe_header' > < / iframe > < / body > < / html >

因为这段混淆代码真的很难看,我稍微替换了变量名和方法名,整理成如下代码:

function xor(x, y) {
    return x ^ y;
function powsum1(y) {
    var sum = 0;
    for (var i = 0; i < y; i++) {
        sum += Math.pow(2, i);
    return sum;
function powsum2(y) {
    var sum = 0;
    for (var i = 8 - y; i < 8; i++) {
        sum += Math.pow(2, i);
    return sum;
function bit1(x, y) {
    y = y % 8;
    ps = powsum1(y);
    ps = (x & ps) << (8 - y);
    return (ps) + (x >> y);
function bit2(x, y) {
    y = y % 8;
    ps = powsum2(y);
    ps = (x & ps) >> (8 - y);
    return ((ps) + (x << y)) & 0x00ff;
function bit(x, y) {
    return bit2(x, y)
function encrypy(salt, key) {
    pwd = "";
    unuse = "";
    for (var i = 0; i < salt.length; i++) {
        c = salt.charCodeAt(i);
        if (i != 0) {
            switch (pwd.charCodeAt(i - 1) % 2) {
                case 0:
                    n = xor(c, key.charCodeAt(i % key.length));
                    break;
                case 1:
                    n = bit(c, key.charCodeAt(i % key.length));
                    break;
        } else {
            n = xor(c, key.charCodeAt(i % key.length));
        pwd += String.fromCharCode(n);
    return pwd;
function check(pwd) {
    var checksum = 0;
    for (var i = 0; i < pwd.length; i++) {
        checksum += pwd["charCodeAt"](i)
    if (checksum == 8932) {
        var win = window.open("", "", "width=300, height=20");    // 这里对原本的十六进制字符做了解码
        win.document.write(pwd)
    } else {
        alert("Mauvais mot de passe: " + checksum)
var salt = "\x71\x11\x24\x59\x8d\x6d\x71\x11\x35\x16\x8c\x6d\x71\x0d\x39\x47\x1f\x36\xf1\x2f\x39\x36\x8e\x3c\x4b\x39\x35\x12\x87\x7c\xa3\x10\x74\x58\x16\xc7\x71\x56\x68\x51\x2c\x8c\x73\x45\x32\x5b\x8c\x2a\xf1\x2f\x3f\x57\x6e\x04\x3d\x16\x75\x67\x16\x4f\x6d\x1c\x6e\x40\x01\x36\x93\x59\x33\x56\x04\x3e\x7b\x3a\x70\x50\x16\x04\x3d\x18\x73\x37\xac\x24\xe1\x56\x62\x5b\x8c\x2a\xf1\x45\x7f\x86\x07\x3e\x63\x47"
var key = prompt("Mot de passe?")
var pwd = encrypy(salt, key)
check(pwd);

在这里我对 window.open() 的十六进制串做了解码,还原出来就是 width=300, height=20

关于 window.open() 函数这里暂且先不管,后面会着重说明。

salt 的十六进制串因为解码出来还是乱码,所以这里就先保持原样不动。

整理后的代码已经大致可以了解逻辑功能了。

但为了方便调试,我又用 python 重写了一遍,并加了注释:

# 异或运算
# 存在逆运算:若 z = xor(x, y) ,则 y = xor(x, z)
def xor(x, y) :
     return x ^ y
# 指数求和(除0外结果总是奇数)
# 由于 y 的取值范围只能是 [0, 8), 因此返回值只有 8 种可能:
#   1 => 1
#   2 => 3
#   3 => 7
#   4 => 15
#   5 => 31
#   6 => 63
#   7 => 127  
def powsum1(y) :
    sum = 0
    for i in range(0, y) :
        sum += pow(2, i)
    return sum
# 指数求和(结果总是偶数)
# 由于 y 的取值范围只能是 [0, 8), 因此返回值只有 8 种可能:
#   0 => 0
#   1 => 128
#   2 => 192
#   3 => 224
#   4 => 240
#   5 => 248
#   6 => 252
#   7 => 254
def powsum2(y) :
    sum = 0
    for i in range(8 - y, 8) :
        sum += pow(2, i)
    return sum
# 执行一些位运算(此函数并没有被调用)
# 但因为 powsum1 的缘故,当 x 固定的时候,结果只会是某 8 个固定的 ASCII 码
# 导致输入与输出不是一一映射关系,因此 bit1 不可能存在逆运算
# 另外当 x 固定的时候,无论 y 为何值,均有 bit1(x, y) == bit2(x, 8 - y)
def bit1(x, y) :
    y = y % 8
    ps = powsum1(y)
    ps = (x & ps) << (8 - y)
    return ps + (x >> y)
# 执行一些位运算
# 但因为 powsum2 的缘故,当 x 固定的时候,结果只会是某 8 个固定的 ASCII 码
# 导致输入与输出不是一一映射关系,因此 bit2 不可能存在逆运算
# 另外当 x 固定的时候,无论 y 为何值,均有 bit2(x, y) == bit1(x, 8 - y)
def bit2(x, y) :
    y = y % 8
    ps = powsum2(y)
    ps = (x & ps) >> (8 - y)
    return (ps + (x << y)) & 0x00ff
# 执行一些位运算
def bit(x, y) :
    return bit2(x, y)   # bit1(x, y) == bit2(x, 8 - y)
# 使用输入串 key 对盐 salt 循环加密,得到加密串 pwd
def encrypt(salt, key) :
    pwd = ''
    key_len = len(key)
    if key_len > 0 :
        for i, c in enumerate(salt) :
            x = ord(c)
            y = ord(key[i % key_len])
            if i > 0 and ord(pwd[i - 1]) % 2 != 0 :
                y = y % 8
                z = bit(x, y)
            else :
                z = xor(x, y)
            pwd += chr(z)
    return pwd
# 检查加密串 pwd 的校验和
# 当校验和为 8932 时,把 pwd 的内容输出到弹出窗口
def check(key, pwd) :
    checksum = 0
    for c in pwd :
        checksum += ord(c)   # 校验和
    flag = (checksum == 8932)
    if flag :
        # 原 js 代码:
        # var win = window.open("", "", "width=300, height=20")
        # win.document.write(pwd)
    print('%s(%i): \n\tkey=%s\n\tpwd=%s' % ('Bingo' if flag else 'Fail', checksum, key, pwd))
    return flag
if __name__ == '__main__' : 
    salt = "\x71\x11\x24\x59\x8d\x6d\x71\x11\x35\x16\x8c\x6d\x71\x0d\x39\x47\x1f\x36\xf1\x2f\x39\x36\x8e\x3c\x4b\x39\x35\x12\x87\x7c\xa3\x10\x74\x58\x16\xc7\x71\x56\x68\x51\x2c\x8c\x73\x45\x32\x5b\x8c\x2a\xf1\x2f\x3f\x57\x6e\x04\x3d\x16\x75\x67\x16\x4f\x6d\x1c\x6e\x40\x01\x36\x93\x59\x33\x56\x04\x3e\x7b\x3a\x70\x50\x16\x04\x3d\x18\x73\x37\xac\x24\xe1\x56\x62\x5b\x8c\x2a\xf1\x45\x7f\x86\x07\x3e\x63\x47";
    key = ""   # 输入串
    pwd = encrypt(salt, key)
    check(key, pwd);

针对 python 的每个函数我做了一些单元测试,大致归纳出每个函数的作用(过程略,有兴趣的同学可以自己测试)。

为了方便后面结合分析,我对这三份处理过的代码的每个函数名做了个映射关系表格,并附带了说明:

混淆 JS 函数/变量 美化 JS 函数/变量 重构 Python 函数/变量 _(x, y) xor(x, y) xor(x, y) 异或运算( 存在逆运算
若 z = xor(x, y) , 则 y = xor(x, z) __(y) powsum1(y) powsum1(y) 指数求和(被 bit1 调用)
y 取值范围为 [0, 8)],对应输出固定为
0, 1, 3, 7, 15, 31, 63, 127 ___(y) powsum2(y) powsum2(y) 指数求和(被 bit2 调用)
y 取值范围为 [0, 8)],对应输出固定为
0, 128, 192, 224, 240, 248, 252, 254 ____(x, y) bit1(x, y) bit1(x, y) 执行一些位运算(此函数并没有被调用)
甚至从某种意义上看 bit1 与 bit2 是 等价
其存在只是混淆视听, 误认为 是 bit2 的逆运算 _____(x, y) bit2(x, y) bit2(x, y) 执行一些位运算( 不存在逆运算
当 x 固定的时候,无论 y 为何值
结果只会是某 8 个固定 ASCII 码的其中之一
且均有 bit2(x, y) == bit1(x, 8 - y) ______(x, y) bit(x, y) bit(x, y) 无任何作用
此函数只是简单调用 bit2 而已 _______(ð, key) encrypt(salt, key) encrypt(salt, key) 使用输入串 key 对盐 salt 循环加密
得到加密串 pwd __________(þ) check(pwd) check(key, pwd) 检查加密串 pwd 每个字符的 ASCII 码之和
是否等于 8932,若是在则 弹出式窗口
打印 pwd 的内容 盐(固定值,十六进制串) 输入字符串( 本题需要推导的最终 flag ) salt 与 key 加密得到的字符串
若其 ASCII 码的校验和等于 8932
则会触发弹窗事件并将其打印出来

背景分析

到这里为止,整个代码在做什么,以及出题人希望我们做什么, 似乎 已经是很清晰了。

简单来说,就是 给定一个盐 salt ,然后要求输入一个密钥 key , 通过某种算法使用 salt key 加密得到密码 pwd

但是因为某种原因,密钥 key 遗失了, 密码 pwd 也忘记了。期望可以通过某种途径找回 key

目前只知道的线索有:

  • 加密算法的逻辑
  • salt
  • 密码 pwd 的校验和
  • 初步分析

    首先需要明确的是,因为有无数种组合,所以要通过校验和反推密码 pwd 本身是不可能的。

    因此 校验和只能作为我们是否找到了 key 的判断依据之一

    另外就算已知加密算法的逻辑,也无法写出对应的解密算法,究其原因就是 bit 函数是不可逆的。

    密码爆破

    通过前面分析,我凭第一直觉想到的解题方法就是密码爆破: 不断枚举输入 key 值,然后测试其输出 pwd 值的校验和是否等于 8932 ,只要命中则认为找到了目标 key

    于是我选择了两种爆破方式:

  • 以 ASCII 表作为 key 的每个字符范围进行枚举,令 key 的长度依次为 1 个字符、2个字符、3个字符….
  • 枚举从 darkweb 统计出来的 Top 10000 的弱密码作为 key (密码表可在 SecList 下载)
  • 利用前面重构出来的 python 代码,可以很容易就实现爆破测试,这里就不贴爆破代码了,直接说结果。

    通过爆破我找到了两个符合条件的 key 值: +3 yasmin

    但是很遗憾,这两个 key 值虽然可以使得 pwd 的校验和为 8932 ,但提交后发现并不是真正的 pwd

    看来事情并没有想象中简单。

    我认真地分析 encrypt(salt, key) 函数,又发现一件事情:

    key 的每个字符一直在通过 取模运算,循环 地对 salt 的每个字符依次执行某个操作( xor bit

    亦即前面得到的两个 key 值,只要通过循环,就可以衍生出更多符合校验和条件的 key 值,例如:

    +3 +3+3 +3+3+3 yasmin yasminyasmin yasminyasminyasmin 、……

    显然不可能有这么多正确答案,换言之 爆破不是正确的解题思路

    进一步分析

    仔细观察 encrypt(salt, key) 函数的行为,与其说这是 salt key 加密成 pwd 的过程,

    倒不如说 这是通过 key 把密码 salt 还原成明文 pwd 的过程 encrypt 实际上应该是一个解密函数

    据此我调整了一下这部分代码:

  • pwd => plaintext
  • salt => pwd
  • encrypt(salt, key) => decrypt(pwd, key)
  • # 使用输入串 key 对密文 pwd 循环解密,得到明文 plaintext
    def decrypt(pwd, key) :
        plaintext = ''
        key_len = len(key)
        if key_len > 0 :
            for i, c in enumerate(pwd) :
                x = ord(c)
                y = ord(key[i % key_len])
                if i > 0 and ord(plaintext[i - 1]) % 2 != 0 :
                    y = y % 8
                    z = bit(x, y)
                else :
                    z = xor(x, y)
                plaintext += chr(z)
        return plaintext
    # 检查明文 plaintext 的校验和
    # 当校验和为 8932 时,把 plaintext 的内容输出到弹出窗口
    def check(key, plaintext) :
        checksum = 0
        for c in plaintext :
            checksum += ord(c)   # 校验和
        flag = (checksum == 8932)
        if flag :
            # 原 js 代码:
            # var win = window.open("", "", "width=300, height=20")
            # win.document.write(pwd)
        print('%s(%i): \n\tkey=%s\n\tplaintext=%s' % ('Bingo' if flag else 'Fail', checksum, key, plaintext))
        return flag
    if __name__ == '__main__' : 
        pwd = "\x71\x11\x24\x59\x8d\x6d\x71\x11\x35\x16\x8c\x6d\x71\x0d\x39\x47\x1f\x36\xf1\x2f\x39\x36\x8e\x3c\x4b\x39\x35\x12\x87\x7c\xa3\x10\x74\x58\x16\xc7\x71\x56\x68\x51\x2c\x8c\x73\x45\x32\x5b\x8c\x2a\xf1\x2f\x3f\x57\x6e\x04\x3d\x16\x75\x67\x16\x4f\x6d\x1c\x6e\x40\x01\x36\x93\x59\x33\x56\x04\x3e\x7b\x3a\x70\x50\x16\x04\x3d\x18\x73\x37\xac\x24\xe1\x56\x62\x5b\x8c\x2a\xf1\x45\x7f\x86\x07\x3e\x63\x47";
        key = ""   # 输入串
        plaintext = decrypt(pwd, key)
        check(key, plaintext);

    调整思路

    调整代码后,整段代码的功能就更切合正常思路了:

    **有一密文 pwd , 需要找到其加密密钥 key 解密成明文 plaintext ,若解密成功则把 plaintext 输出到一个弹窗 **。

    为什么是弹窗

    在这里,我注意到一个问题,而且整个挑战也只有这个唯一提示:

    NB : You will have to enable popups in order to solve this challenge!

    就是说我要开启浏览器弹窗功能才能解决这个挑战。

    为什么不用 alert ?为什么是弹窗?为什么弹窗会成为提示要点?

    我把代码重构成 python 时,直观上认为弹窗只不过是输出结果的方式而已,就一直忽略了弹窗函数的作用。

    但既然提示指出了弹窗的必要性,可能在其中会隐含了什么。

    if (checksum == 8932) {
        var win = window.open("", "", "width=300, height=20"); 
        win.document.write(pwd)
    } else {
        alert("Mauvais mot de passe: " + checksum)
    

    在这段弹窗代码中,用到了两个 js 函数,从 W3School 查到其 API 说明如下:

  • window.open(URL,name,features,replace):用于打开一个新的浏览器窗口或查找一个已命名的窗口
  • document.write(exp1,exp2,exp3,....)可向页面写入 HTML 表达式或 JavaScript 代码
  • 其中 window.open 的参数说明为 :

       ○ URL:可选,声明了要在新窗口中显示的页面的 URL。

           如果省略了这个参数,或者它的值是空字符串,那么新窗口就不会显示任何页面内容

       ○ name:可选,声明了新窗口的名称

       ○ features:可选,声明了新窗口要显示的标准浏览器的特征

       ○ replace:可选,本题代码中并没有使用这个参数

    为了测试弹窗的效果,我把前面爆破得到的两个 key+3yasmin 分别输入到挑战页面。

    因为这两个值校验和均为 8932 ,所以必定会触发弹窗。

    使用浏览器的开发者工具查看弹窗源码,我发现几件很有意思的事情:

  • 弹窗显示了意义不明的乱码内容,而且为了显示乱码内容,弹窗自动添加了 <html> 等标签(因为使用 python 运行时,并没有输出这些标签,而且 window.open 的 URL 为空,即页面本身不会有任何内容,所以我肯定这些标签是弹窗自动加的,与 window.opendocument.write 无关)
  • 不同的 key 值得到输出到弹窗的内容不同(意味着很可能有一个 key 会使得内容变成明文)
  • key 值为 +3 时,不难发现乱码中有一对闭合标签 <o€_Â=>
  • 尤其是最后一点,乱码的闭合标签 ,给了我很大的启示:

    大胆猜测

    既然 window.open 开启了一个完全空白的窗口, document.write 又往里面写 plaintext

    那会不会 plaintext 的内容就是页面源码?

    而且会不会 plaintext 就是包含完整 html 标签的页面源码?

    虽然还不知道 plaintext 的全部内容,但如果是包含完整 html 标签的页面源码,那第一个字符很有可能就是 <

    更甚者, plaintext 前几个字符很有可能就是 <html>

    验证猜测

    虽然是全凭直觉的猜测,但要验证它也并不是什么复杂的事情。

    验证的思路很简单,逐个字符验证就可以了:

    现在已知密文 pwd 的每一个字符和 decrypt 的代码逻辑,又知道 plaintext 的前几个字符(虽然只是猜测),

    那么只需要逆向计算 key 的每一个字符,只要都不是乱码字符,就说明猜测正确,同时也能推算出 key 的值。

    逆推

    decrypt 逻辑可知:

    plaintext[0] = xor( pwd[0], key[0] ) ,现已知 pwd[0] = '\x71'plaintext[0] = '<'

    因为 xor 的逆运算就是其自身,因此有 key[0] = xor( pwd[0], plaintext[0] ) = 'M'

    又从 decrypt 逻辑可知:

    第 i 个字符使用 xor 还是 bit 方法,取决于 plaintext[i-1] 的 ASCII 码是奇是偶,

    因为 plaintext[0] = '<' 其 ASCII 码是偶数, 所以 plaintext[1] 依旧使用 xor 方法逆推,

    已知 pwd[1] = '\x11'plaintext[1] = 'h',因此有 key[1] = xor( pwd[1], plaintext[1] ) = 'y'

    根据上述规律,整理出前几个字符的逆推表如下:

    字符索引 : i pwd[i] plaintext[i-1] plaintext[i] bit(无逆运算) key[i] ?(未知)

    不难注意到,pwd[0] == pwd[6] == '\x71' ,因此很可能也有 plaintext[0] == plaintext[6] == '<'

    反推得到 key[0] == key[6] == 'M' ,也就是说,key 的字符开始重复了。

    通过前面做爆破时的思路可以,这可能就是 key 的取模循环点,换言之 key 应该只有 6 个字符:MyP4?S

    Bingo

    现在的问题在于,第 4 个字符 ? 是什么。

    因为从代码可知 bit 是不存在逆运算的,亦即虽然我们已知 pwd[4] == '\x8d'

    plaintext[4] = bit( pwd[4], key[4] ) = 'l' ,但是从算术上无法逆推 key[4] 值。

    既然无法逆推,那么顺序爆破就好了:

    通过枚举 key[4] 的 ASCII 码值(也就256次),记录所有使得 bit( pwd[4], key[4] ) = 'l'key[4] 值,

    于是我得到了这 12 个字符均是满足条件的: # + 3 ; C K S [ c k s {

    逐个代入到 ? 在挑战页面校验,最终发现只有 MyP4sS 会使得弹窗内容完全变成明文,即是真正的 key,完成挑战。

    (其实 ? 不用爆破也能猜到不是 s 就是 S,因为很明显 MyP4sS 就是 MyPASS 的变体)

    在最后,我在 python 代码输入 key = MyP4sS ,还原出真正的 plaintext 内容为:

    <html><head><title>Victoire!</title></head><body>Vous pouvez entrer ce mot de passe!</body></html>

    说明前面的猜测是正确。

  • payload:
  • flag: 下载
  • flag 下载后的 flagzip 的文件需要手动更改后缀为 *.zip,然后解压即可(为了避免直接刷答案)