近日,参加金融行业某私测项目,随意选择某个业务办理,需要向客户发送短信验证码:
响应报文中包含大段加密数据:
全站并非全参数加密,加密必可疑!尝试篡改密文,页面提示“实名认证异常”:
猜测该密文涉及用户信息,且通过前端 JS 解密,验证之。
武器化利用
分析清楚漏洞详情,接下来一定是将手工利用转变为自动攻击,实现武器化,才能将战果最大化。
武器化,我有两个选择:一是复用报文,对 PHONE_NO 参数加载手机号码字典,借助 python 的 requests 库,访问 /xx/api/xxxx/h5/xx/sChkBlPhone 接口获取 Data,调用前面已实现的解密脚本,批量获取用户信息;二是复用页面,驱动 webdriver,模拟人工操作,输入手机号、点击“获取验证码”按钮、抓包获取 Data、解密脚本,批量获取用户信息。粗略分析,前者运行高效、后者实现简单。选择一还是二呢?~( ´•︵•` )~,我都要!
2.1 复用报文方式
我计划基于已有原始请求,用脚本不断填写新 PHONE_NO 参数后提交,获取不同用户的个人信息。要让这条路可行,必须具备两个前提,服务端未限制篡改参数、服务端未限制重放请求。
2.1.1 防篡改与防重放
我在页面上输入手机号 13988888840,点击“获取验证码”按钮,用 burp 的 proxy 抓包拦截请求(不放),将 PHONE_NO 参数值改为 13988888849 后放行:
报错“参数签名异常”,说明存在参数防篡改的限制。
刷新页面,我重新在页面上输入手机号 13988888840,点击“获取验证码”按钮,用 burp 的 proxy 抓包拦截请求(不放),将该请求转至 burp 的 repeater,对报文不作任何修改,第一次发送,响应 200,可获取 Data,第二次,响应 412,无法获取 Data:
报错“条件不达标”(Precondition Failed),说明存在请求防重放的限制。
服务端是如何晓得我在篡改参数、重放请求呢?肯定离不开客户端的配合。于是,我仔细审查请求报文中的 headers,首部 authorization 引起了我的注意:
怀疑是 sign 在作祟。
我重新从页面输入 13988888840,点击“获取验证码”按钮,抓包拦截请求,首部 authorization 如下:
authorization: origin=2|appkey=200000056|token=null|ts=1605169433400|noncestr=K2FZpfbe|sign=40ca525898eba6df88bca451342515c1
这次不对 PHONE_NO 参数值作任何变更,只把 sign 最末尾的字符从 1 改为 2(即 40ca525898eba6df88bca451342515c2),同样报“参数签名异常”的错:
基本上验证了我的猜测,业务系统的防重放和防篡改能力依赖 sign 参数。我得想法绕过防御机制。
业务系统的防御,我大致了解(谁还没点在蓝队背景 <(▰˘◡˘▰)>)。客户端对所有请求参数进行哈希计算,得到参数签名(sign),将签名放入首部 authorization 中提交至服务端,服务端基于相关信息生成签名,与客户端提交的签名进行比较,若不同,说明参数被篡改,则不响应该请求,若相同则响应。签名用后即废,若重复,说明请求被重放,则不响应该请求,若不重复则响应。
刺探出 sign 的重要性,只要我能控制随意生成 sign,那么服务端防御的问题也就迎刃而解啦。
2.1.2 分析参数签名逻辑
虽然该站点前端有代码混淆、反调试等保护措施,但之前已加被我解决掉,找寻并提取实现签名逻辑的 JS 不会太困难。
全局搜索 sign 关键字,找到多个匹配项,只有 1875 行是生成首部 authorization 的值的逻辑:
跳至 1875 行,进入函数 _e() 内部,找到了计算签名的逻辑(s),以及生成 authorization 值的逻辑(函数返回值);为查看 _e() 的调用实参,我在入口处设置断点,为查看生成的 authorization 值,在出口处设置断点:
再回到页面上输入手机号 13988888840,点击“获取验证码”按钮,流程进入断点,了解传递实参的信息:
切换至 console 中,参照调用方式,改用实参 13988888849 调用 _e():
_e("POST", "{\"BODY\":{\"PHONE_NO\":\"13988888849\",\"GROUP_ID\":\"2\",\"REGION_ID\":\"11\"}}")
生成新签名:
burp 开启拦截模式,放行前端断点,burp 中拦截到参数值 13988888840 及其 authorization 值的请求报文,将其改为 13988888849 及其新 authorization 值:
服务端正常响应,返回 13988888849 加密后的用户信息 Data:
现在,我可以绕过参数签名机制,具有随意更改参数的能力了。只要能控制生成签名,绕防重放也就易如反掌,每次提交请求时,我同步生成新签名即可。
为方便生成参数签名,我把 JS 的 _e() 转为 python 脚本 gen_authorization.py:
整理下,现在我可以访问 /xx/api/xxxx/h5/xx/sChkBlPhone 接口获取加密后的用户信息 Data,可以调用 decrypt_data_by_nodejs.py 解密 Data,获取用户姓名、单位地址、家庭地址、身份证号码,拿到单个用户的敏感信息三要素;我可以调用 gen_authorization.py 绕过防重放和防篡改机制,批量获取多个用户的敏感信息。综合已有脚本编写 exp,实现武器化 get_users_info_by_requests.py: