预期解
通过浏览器的动态调试
或者关键词查找
功能,找到核心的登录函数,进而找到题目,最后动态调试
或静态分析
出密码的加密方式,解出flag。
WriteUp
事前提示:
- Firefox相比于Chrome,Firefox对于网页调试更加专业,因为Firefox直接就把老牌调试插件Firebug给内嵌成调试工具了,我自己测试也发现Firefox对于DOM事件的识别比Chrome更精确,Chrome还得自己手动跟半天,Firefox直接一步到位了。(这种情况我只是针对本题啊,其他大部分情况是不是也这个样子我没法测试,我之前都是硬着头皮用Chrome调试的)
- 调试时记得使用
小号窗口
或隐私浏览
模式,以实现一键禁用所有浏览器插件/扩展。如果不禁用的话,可能调试时跟着跟着跟到插件的JS里去了。- 以下所有行为描述都基于Firefox的调试功能。
打开题目网站,进入登录页面,在开发者工具[F12]
中使用选择器[Ctrl+Shift+C]
将html节点定位到登录按钮,点击event
,发现挂接的事件挺少的,就俩个,其中一个还是空函数,直接排除,那么click
事件就是另一个函数了。
点击↗️三
(大概长这个样子)[在调试器打开]按钮,进入调试器,首先格式化js,不然跟着太难受了。
输入好账号密码之后,在事件处下断点。(记得别把焦点放输入框上,不然下完断点会无尽截断)
F10
步过几次,发现了如图的onStartLogin
函数,猜测这里应该是登录函数的第一层入口。
F11
步入,发现确实存在正在登录
、登录成功
等字样,说明我们的判断没错。(Chrome浏览器在这里并不会显示中文,而是显示被编码的Unicode原文,因为这些字在源文件里就是写成Unicode形式的,只不过Firefox自动解码了而已)(所以实际上也可以在Firefox里通过查找关键词的方式一步到位定位到这个地方,但Chrome就需要查找Unicode才行)
尝试分析这个函数的内容,试图找到更加核心的登录入口(因为这里的代码都是去处理登录结果的,并未写如何登录的逻辑)。我们可以发现这个函数的主要骨架是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
也能发现,这个参数里有我们输入的账号密码等数据。
将整个return
语句下断,因为对于这种分行语句,有时候下断会出现断点滞后的毛病,直接整句下断保险一点。
F8
运行,来到断点处,F11
步进一次,发现不是登录函数(因为鼠标移到G.UserLogin
上可以看真实的函数定义,可知函数应当是r(e, t)
),那就Shift+F11
步出,然后再F11
重新步入,发现终于来到了r(e, t)
函数。
发现一堆区块注释,内容如下:
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
分支。
那咱就看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.userPassword
和n.userAccount
,那么题目既然说要用加密密码的方式来求flag,那只需要上面这段n.userPassword
相关的就行了。
分析代码可得,首先密码
经历了两次(0, x.default)
函数,并赋值给a
,再将a
的前16位和16位之后的文本分别赋值给r
和o
,接着将r
和o
都进行倒置,再拼接成l
,再将l
的前3位拼到l
自己的尾部形成u
,最后再将u
转为小写,执行一次(0, x.default)
函数,赋值给s
,最终得到新的n.userPassword
就是s
,也就是加密后的密码。
而其中最重要的是(0, x.default)
函数,我们下断进行跟踪,发现这其实就是个md5
函数。
那么根据代码就能写出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}