预期解

通过浏览器的动态调试或者关键词查找功能,找到核心的登录函数,进而找到题目,最后动态调试静态分析出密码的加密方式,解出flag。

WriteUp

事前提示:

  • Firefox相比于Chrome,Firefox对于网页调试更加专业,因为Firefox直接就把老牌调试插件Firebug给内嵌成调试工具了,我自己测试也发现Firefox对于DOM事件的识别比Chrome更精确,Chrome还得自己手动跟半天,Firefox直接一步到位了。(这种情况我只是针对本题啊,其他大部分情况是不是也这个样子我没法测试,我之前都是硬着头皮用Chrome调试的)
  • 调试时记得使用小号窗口隐私浏览模式,以实现一键禁用所有浏览器插件/扩展。如果不禁用的话,可能调试时跟着跟着跟到插件的JS里去了。
  • 以下所有行为描述都基于Firefox的调试功能。

打开题目网站,进入登录页面,在开发者工具[F12]中使用选择器[Ctrl+Shift+C]将html节点定位到登录按钮,点击event,发现挂接的事件挺少的,就俩个,其中一个还是空函数,直接排除,那么click事件就是另一个函数了。
image.png

点击↗️三(大概长这个样子)[在调试器打开]按钮,进入调试器,首先格式化js,不然跟着太难受了。

输入好账号密码之后,在事件处下断点。(记得别把焦点放输入框上,不然下完断点会无尽截断)
image.png

F10步过几次,发现了如图的onStartLogin函数,猜测这里应该是登录函数的第一层入口。
image.png

F11步入,发现确实存在正在登录登录成功等字样,说明我们的判断没错。(Chrome浏览器在这里并不会显示中文,而是显示被编码的Unicode原文,因为这些字在源文件里就是写成Unicode形式的,只不过Firefox自动解码了而已)(所以实际上也可以在Firefox里通过查找关键词的方式一步到位定位到这个地方,但Chrome就需要查找Unicode才行)
image.png

尝试分析这个函数的内容,试图找到更加核心的登录入口(因为这里的代码都是去处理登录结果的,并未写如何登录的逻辑)。我们可以发现这个函数的主要骨架是for (;;)死循环里面套着switch...case结构,而i.next的值控制着分支的重复跳转,直到return出死循环。

观察case 0的情况,发现先将t.loginInfo.userAccount变量执行了(0, B.trim)函数,进行去除头尾空格,然后比较2 != t.remember来决定是否break本次switch以进入case 8,这里我们鼠标移到t.remember变量上看一下就能发现值为2,所以百度啥不成立,所以继续执行return语句,其中有两次函数调用,一个是j.default.awrap函数,一个是(0, G.UserLogin)函数,这里看名字就能发现,(0, G.UserLogin)函数应当就是更深层的登录函数入口,将鼠标移到这个函数的传参t.loginInfo也能发现,这个参数里有我们输入的账号密码等数据。
image.png

将整个return语句下断,因为对于这种分行语句,有时候下断会出现断点滞后的毛病,直接整句下断保险一点。
image.png

F8运行,来到断点处,F11步进一次,发现不是登录函数(因为鼠标移到G.UserLogin上可以看真实的函数定义,可知函数应当是r(e, t)),那就Shift+F11步出,然后再F11重新步入,发现终于来到了r(e, t)函数。
image.png

发现一堆区块注释,内容如下:
gongXiNiZhaoDaoLeDengLuHanShu,tiMuShi:ZmxhZ+aYr+W9ouWmgm55bnVjdGZ7NWMyNzVkZmEtODJiNy00Zjg4LTgyYzUtYTc2N2E1MTg4ZDU3feeahOS4gOS4suWtl+espuS4su+8jOeOsOW3suefpWZsYWfpg6jliIblrZfnrKbkuLpueW51Y3Rme2Y2Yz8/PzBhLTVjMTYtNGIzNS05OThkLWI1OTA3Pz8/MGM2OX3vvIznjrDlnKjlsI/mmI7lsIZmbGFn5aGr5YWl5a+G56CB5qGG5Lit6L+b6KGM55m75b2V77yM5oiq5Y+W5Yiw5o+Q5Lqk55qE5pWw5o2u5Lit77yM5a+G56CB6KKr5Yqg5a+G5Li6OGE2Yz8/Pz9iMjIyMjI/Pz8/ODg4MmY0Pz8/Pzg1OWbvvIzor7fmsYLlh7pmbGFn44CC77yI5pys6aKY5Lit55qEP+eahueUqOadpeaMh+S7o+WNleS4quWtl+espu+8iQ==

将拼音和Base64翻译成人话就是:恭喜你找到了登录函数,题目是:flag是形如nynuctf{5c275dfa-82b7-4f88-82c5-a767a5188d57}的一串字符串,现已知flag部分字符为nynuctf{f6c???0a-5c16-4b35-998d-b5907???0c69},现在小明将flag填入密码框中进行登录,截取到提交的数据中,密码被加密为8a6c????b22222????8882f4????859f,请求出flag。(本题中的?皆用来指代单个字符)

那么咱就得去分析这个登录函数是咋加密密码的了。整个函数如下:

            function r(e, t) {
              var n = void 0;
              if ((0, I.getKeyPsw) ()) n = E({
              }, e),
              n.userPassword = (0, I.getKeyPsw) ();
               else {
                n = E({
                }, e);
                var a = (0, x.default) ((0, x.default) (n.userPassword)), r = a.substr(0, 16), o = a.substr(16), l = r.split('').reverse().join('') + o.split('').reverse().join(''), u = l + l.substr(0, 3), s = (0, x.default) (u).toLowerCase(); n.userPassword = s, t ? (0, I.setKeyPsw) (s)  : (0, I.clearKeyPsw) ()
              }
              var c = (0, x.default) ((0, x.default) (n.userAccount)), f = c.substr(0, 16), d = c.substr(16), p = f.split('').reverse().join('') + d.split('').reverse().join(''), m = p + p.substr(3, 6), g = (0, x.default) (m); return n.token = g, new Promise(function (e, t) {
                i(n).then(function (t) {
                  1 === t.status && null != t.data && ((0, I.setLoginToken) (t.data.token || ''), (0, I.setLoginName) (t.data.userAccount), (0, I.setNickName) (t.data.nickName || ''), (0, I.setFullName) (t.data.userName || ''), (0, I.setVipName) (t.data.memberLevelVIPName || ''), (0, I.setIsFillBankCard) (t.data.bankCardCount > 0 ? 1 : 0), (0, I.setWithdrawPwdStatus) (t.data.withdrawPassword || 0)),
                  e(t)
                }).catch (function (e) {
                  t(e)
                })
              })
            }

对于第一个if...else语句,我们可以在控制台跑一下,发现执行了else分支。
image.png

那咱就看else分支写了啥。先是一个n = E({ }, e);(这不是和上面if分支一样吗,为啥不写外面去...),然后再是如下语句:

var a = (0, x.default) ((0, x.default) (n.userPassword)),
    r = a.substr(0, 16),
    o = a.substr(16),
    l = r.split('').reverse().join('') + o.split('').reverse().join(''),
    u = l + l.substr(0, 3),
    s = (0, x.default) (u).toLowerCase();
n.userPassword = s,
    t ? (0, I.setKeyPsw) (s) : (0, I.clearKeyPsw) ()

而紧接着if...else语句块的,就是如下语句:

var c = (0, x.default) ((0, x.default) (n.userAccount)),
    f = c.substr(0, 16),
    d = c.substr(16),
    p = f.split('').reverse().join('') + d.split('').reverse().join(''),
    m = p + p.substr(3, 6),
    g = (0, x.default) (m);

我们可以发现这两段代码就像复制的一样,只是部分地方变了一下,最显眼的就是n.userPasswordn.userAccount,那么题目既然说要用加密密码的方式来求flag,那只需要上面这段n.userPassword相关的就行了。

分析代码可得,首先密码经历了两次(0, x.default)函数,并赋值给a,再将a的前16位和16位之后的文本分别赋值给ro,接着将ro都进行倒置,再拼接成l,再将l的前3位拼到l自己的尾部形成u,最后再将u转为小写,执行一次(0, x.default)函数,赋值给s,最终得到新的n.userPassword就是s,也就是加密后的密码。

而其中最重要的是(0, x.default)函数,我们下断进行跟踪,发现这其实就是个md5函数。
image.png

那么根据代码就能写出py脚本进行遍历flag,看看哪个情况可以满足题目给的加密后的部分文本了。写出脚本如下:

import hashlib
def fuc():
    for c1 in "0123456789abcdef":
        for c2 in "0123456789abcdef":
            for c3 in "0123456789abcdef":
                for c4 in "0123456789abcdef":
                    for c5 in "0123456789abcdef":
                        for c6 in "0123456789abcdef":
                            flag = "nynuctf{f6c"+c1+c2+c3+"0a-5c16-4b35-998d-b5907"+c4+c5+c6+"0c69}"
                            pwd = hashlib.md5(flag.encode("UTF-8")).hexdigest()
                            pwd = hashlib.md5(pwd.encode("UTF-8")).hexdigest()
                            pwd = pwd[0:16][::-1] + pwd[16:32][::-1] + pwd[13:16][::-1]
                            pwd = hashlib.md5(pwd.encode("UTF-8")).hexdigest()
                            if pwd[0:4] == "8a6c" and pwd[8:14] == "b22222" and pwd[18:24] == "8882f4" and pwd[28:32] == "859f":
                                print(flag)
                                return

fuc()

flag

nynuctf{f6c0170a-5c16-4b35-998d-b59072a50c69}