1 登录安全认证流程
首先,进入百度贴吧首页,点击右上角进入登录/注册界面。
登陆框图默认为百度贴吧App扫码登录,左下角为注册按钮,右上角则是切换为用户名密码登录。切换为用户名密码登录后,可以看到忘记密码按钮。进一步探索总结,其登录安全认证流程归纳如下:
- 注册:点击注册会跳转到百度的账号注册界面,因此我们可以推测百度贴吧使用的实际上和百度是同一个账号。注册需要提供用户名、手机号、密码以及验证码。其中,用户名使用中英文都可,但无法重复,且一旦注册,无法修改。一个手机号只能注册一个账户。密码要求为:①长度为8~14个字符;②字母/数字以及标点符号至少包含2种;③不允许有空格、中文。按要求正确输入上述四个表项,点击阅读并接受协议及声明即可成功注册。
- 扫码登录:按照提示使用最新版百度App扫码,必须已经在手机上登录百度贴吧账号,扫码后需要确认登录,点击取消则二维码失效。实测使用百度App扫码也可以成功登录。同时,在没有扫码的情况下,太长时间无操作,二维码也会失效。
- 用户名密码登录:用户名处可填写手机号/用户名/邮箱,正确输入该项与密码后,点击登录完成安全验证(将图片旋转到正确位置)即可成功登录。
- 忘记密码:首先输入手机号,完成安全验证(将图片旋转到正确位置)后,请求验证码并输入,即可进入修改密码界面。新密码填写要求与注册时相同,需要两次输入,且不可与旧密码相同。若更换手机(无法收到验证码),可点击手机不再使用,这时,可以使用百度旗下任何App扫码申诉,百度会将结果反馈到新的手机号(需要验证码确认)上。
2 密码加密流程
我们着重分析用户名密码登录过程中的加密流程。
2.1 信息收集
-
打开浏览器检查功能,选择网络项,输入账号,点击密码输入框,发现浏览器发送了getpublickey请求,多次测试,发现只要点击输入框,浏览器都会发送这个请求,并且每次收到的反馈都不一样,根据行为及请求名推测这是后面对密码加密要使用的公钥。
其中连续两次的请求反馈信息如下:
//第一次 bd__cbs__57hpuc({ "errno": '0', "msg": '', "pubkey": '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAPIrts2wEf8+Palf0mtPuTsQ+\nO6vNOCqf+2oSMtTSrlwlCV2jScqPdhcyyzkB8m4siuKpY1MbnBNqu0wtWwW6dyV+\nDaeSKqASvpzIDhBuua6y4qKd981vDLrugZ\/QiAoRwxvSCIofJEKqyzP6vvGxgFos\naOcO+GqtNtnmZNUMdwIDAQAB\n-----END PUBLIC KEY-----\n', "key": 'yyyNfqoUl1Wu89XNCg8yG99FQ7VfnQaA', "traceid": "" }) //第二次 bd__cbs__i98412({ "errno": '0', "msg": '', "pubkey": '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD1KK0XMEsRi3euQlOsPzk7IvJ5\nw1pteqB5hZwhJhhymz6DEOdQ2oqoldopD4lfuk\/RcvAOvVF5rHJNfN1lrT9\/xN48\nspi2bszoTpGMW070bJu4YrzV\/ZJr0noMM3yYQeZwE0PSjSDwYvPXLDubQBjWAZEl\nYM0AwlK9eS98FSbQkwIDAQAB\n-----END PUBLIC KEY-----\n', "key": 'sLI9Ptz4hw9sK2Y82bxin94BONMbXRn5', "traceid": "CFF7E601" })
-
输入一个不正确的密码(1234),点击登录,发现浏览器发送了api/?login请求,其负载部分信息如下:
可以看到,其中包含了username、password以及rsakey,并且rsakey的值与之前getpublickey请求中返回的key值相同,可能是用户凭证。通过之后的分析可以看到,加密并没有用到rsakey。很明显,这里的password已经被加密过了。由此,可以猜测这里的password采用的是RSA加密算法。
2.2 JS分析
-
在所有js文件中搜索“password” ,结果如下:
一共有12个文件中出现过“password”,根据文件名以及匹配条数,首先怀疑loginv4_tangram_6f2170e.js文件。
-
在该文件中进一步检索,发现涉及password赋值操作的只有两行代码,包含上下文分别是:
var a = baidu.form.json(n.getElement("smsForm")); a.password = n._SBCtoDBC(a.password), a.username = n._SBCtoDBC(a.username), a.FP_UID = n._getCookie("FP_UID") || "", a.FP_INFO = window.PP_FP_INFO || "",
以及
if (e.RSA && e.rsakey) { var o = i; o.length < 128 && !e.config.safeFlag && (s.password = baidu.url.escapeSymbol(e.RSA.encrypt(o)), s.rsakey = e.rsakey, s.crypttype = 12) }
很明显,前者将来自
smsForm
表单的数据转换为 JSON 格式并将其存储在变量a中,而后者包含**e.RSA.encrypt()**函数,应该就是我们要找的加密函数。为了验证猜想,我们在后者的第三行打上断点,再次点击登录按钮,逐步调试:
-
执行e.RSA.encrypt()前:
-
执行e.RSA.encrypt()后:
可以发现,o和i的值都是**“1234“,并且在执行e.RSA.encrypt()前后s.password的值分别为”1234“和”a6t%2BzVbhIKPwZv5ydME2OfI6yWCmnjS2srGFEA6zzDG3FwO1mKAcG1x5W0Fga7vJkJyfe8R4V%2BX%2B9shLIkWvBAXaJwNR9cCucovIYmsDzepGYGcFRvqYyY0CtudJJi0RdnceJTzjVgGYbO9e0SQQeAmvrGJ8wqCnr8wrJr00AQ%3D%3D“。后者应为一串URL编码的字符串,使用js对其
进行解码,以下是解码代码及结果**:退出调试模式,查看新一轮api/?login请求负载,结果如下:
发现这与我们解码得到的字符串完全相同,说明加密函数正是e.RSA.encrypt()。
-
-
下面我们探索RSA加密实例化的过程,亦即**e.RSA.encrypt()**函数的具体内容。
经过之前的分析可知,公钥不同,加密结果也不同,既然RSA加密与公钥相关,那么我们不妨就在文件中搜索**“pubkey”(这个参数是之前getpublickey**中返回的),结果就只有一个地方出现了这个变量:
在这里设置断点,执行调试,发现运行过程并没有在这里停止,而是直接发送了api/?login请求。结合之前发现在每次点击密码输入框时都会发送getpublickey请求,推测该公钥设置函数是在点击输入框之后执行的。点击输入框进行验证,函数在断点处停止,并且此时的公钥值与getpublickey请求返回相同,t中存储的就是getpublickey的返回,猜想得到验证:
截取相关初始化代码并给出注释:
var n = new passport.lib.RSA; //初始化加密函数 //设置公钥 //在提取代码时,可以只保留n,不需要用到e,因为加密只用到了e.RSA,而e.RSA实际上就是n n.setKey(t.pubkey), e && e({ RSA: n, rsakey: t.key })
-
继续查找passport.lib.RSA的定义:
根据上图定位到f(t)函数,截取与passport有关上下文(function(t)内部为具体RSA加密有关细节,不做展示):
-
查找**baidu.url.escapeSymbol()**函数的定义,将其保存至本地:
分析可知,baidu.url.escapeSymbol()函数是对加密结果的一个重新编码,用于将字符串中的特殊字符转义为URL安全的形式。
2.3 验证
结合加密函数初始化、加密部分的代码,编写加密js脚本password.js,解决一些诸如window、navigator未定义的问题,成功运行,输出为172位密文。但是,每次运行的结果都不同,有两种可能得结果,其一是前述分析出错,其二是在加密过程中用到了随机数操作。
再次进入登录页面,发现每次清除缓存后,即使没有再次发送公钥请求,新一轮的密文也会发生变化。因此可以判断之前的分析没有出错,推测应该是RSA算法中使用了随机填充,而直接点击登录按钮时,只有第一次执行js代码,后面的提交使用的是浏览器的缓存。
2.4 总结
以用户以及浏览器行为来总结百度贴吧的加密流程:
- 用户点击用户名输入框,输入用户名。
- 用户点击密码输入框(此时浏览器发送请求获取公钥),输入密码。
- 用户点击登录按钮,浏览器使用公钥对密码进行RSA加密并发送登录请求,请求包含用户名、密文、用户凭证、浏览器指纹等等。
3 代码应用
了解了网站的登录加密算法,我们可以编写脚本或程序来模拟用户的登录操作,这可以为后续的爬虫或者其他自动化数据采集方法做铺垫。在经过比较长时间的尝试后,发现除了密文以及密钥信息,登录还需要dv、fuid等信息,而这些信息都是动态变化的。经过初步分析,这写信息与浏览器指纹等有关,其分析与获取难度不亚于密文加密分析。再加上输入密码后还有人机验证,这对于爬虫等操作来说是不划算的,远不如直接使用cookie登录来得简单。所以这里仅仅对自动化获取密钥并加密做尝试。
简单的思路为:
- 向服务器发送获取公钥请求。
- 将返回解析为JSON格式并提取出密钥。
- 调用JS脚本对明文加密并输出。
以下是运行结果(相关代码见附录):
输出结果包含获取的公钥、密文以及密文长度。
pubkey:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNpwG1oZlm1o9worIguGRBDc2R
E2sMRYFEdCqvYTwiVtEy2XycScDnsgQMpH9p2JPonK27AcedstGxk5eyPTuUR4zC
CxlhfsXffr0PSgxY5oo4aCTX8WYJDXcStNhlJdx5i5ZaRcv160nRpE/baZXpXTAq
xKMmIA9Ty6SMM/U76QIDAQAB
-----END PUBLIC KEY-----
密文:
DivUrGUZTVW1PPRzUKWqVlkdi5MdVqpmDMzEin2dgvyPWUb9L4jJL8Atz5RqA80tOZsTVOUqtDFXp37yS0hBkLQcK5TizSfemnNKipEjCcM9pg2uJCBx+aWiLNQM/lB3kf7DhLyADerE1R3uBLl/E4MMtnPU2VctgZ6mPk8vMIk=
密文长度:
172
附录
import subprocess
import requests
import sys
import json
# 设置标准输出编码为UTF-8,否则提取JS输出会解码出错
sys.stdout = open(1, 'w', encoding='utf-8', closefd=False)
# 创建会话对象
session = requests.Session()
# 发送GET请求获取登录页
login_url = "https://passport.baidu.com/v2/getpublickey?token=9a4286a899d8903381c1a5e289a09a7c&tpl=tb&subpro=&apiver=v3&tt=1699406747230&gid=2341881-3220-4D98-BA80-05D2371B960E&loginversion=v4&traceid=13696501&time=1699406747&alg=v3&sig=VEhSUHE4eWtLTjRRZE1VWG9kTXExSXZOOHEybXlSeUxyM2ErVjRJbnM2UGlrbll4bmd2dkZ5dXBuU1lxM2FJeA%3D%3D&elapsed=9&shaOne=00c97d3bd2b1a4d30cbca01cd015078d236b1c20&rinfo=%7B%22fuid%22%3A%226b76bffbaa9fc6850ad92648b8494615%22%7D&callback=bd__cbs__vmtq5a"
response = session.get(login_url)
# 提取返回中的JSON部分
json_text = response.text.split('(', 1)[1].rsplit(')', 1)[0]
json_text = json_text.replace("'", "\"")
# 解析JSON数据
data = json.loads(json_text)
# 提取pubkey和key
pubkey = data.get("pubkey")
key = data.get("key")
print("pubkey:\n", pubkey)
# 密码明文
password = "1234"
# 执行外部JavaScript文件并传递参数
result = subprocess.run(["node", "password.js", password, pubkey], capture_output=True, text=True)
password = result.stdout.strip() # 去除字符串两端的空白字符
print("密文:\n",password)
print("密文长度:\n",len(password))