前端无秘密:看我如何策反JS为我所用(下)

  • A+
所属分类:安全文章

前端无秘密:看我如何策反JS为我所用(下)

近日,参加金融行业某私测项目,随意选择某个业务办理,需要向客户发送短信验证码:


前端无秘密:看我如何策反JS为我所用(下)


响应报文中包含大段加密数据:


前端无秘密:看我如何策反JS为我所用(下)


全站并非全参数加密,加密必可疑!尝试篡改密文页面提示“实名认证异常”:


前端无秘密:看我如何策反JS为我所用(下)

猜测该密文涉及用户信息,且通过前端 JS 解密,验证之。


前端无秘密:看我如何策反 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 后放行:


前端无秘密:看我如何策反JS为我所用(下)


报错“参数签名异常”,说明存在参数防篡改的限制。


前端无秘密:看我如何策反JS为我所用(下)



刷新页面,我重新在页面上输入手机号 13988888840,点击“获取验证码”按钮,用 burp 的 proxy 抓包拦截请求(不放),将该请求转至 burp 的 repeater,对报文不作任何修改,第一次发送,响应 200,可获取 Data,第二次,响应 412,无法获取 Data:


前端无秘密:看我如何策反JS为我所用(下)


报错“条件不达标”(Precondition Failed),说明存在请求防重放的限制。

服务端是如何晓得我在篡改参数、重放请求呢?肯定离不开客户端的配合。于是我仔细审查请求报文中的 headers首部 authorization 引起了我的注意:


前端无秘密:看我如何策反JS为我所用(下)


怀疑是 sign 在作祟。

我重新从页面输入 13988888840,点击“获取验证码”按钮,抓包拦截请求,首部 authorization 如下:

authorization: origin=2|appkey=200000056|token=null|ts=1605169433400|noncestr=K2FZpfbe|sign=40ca525898eba6df88bca451342515c1

这次不对 PHONE_NO 参数值作任何变更,只把 sign 最末尾的字符从 1 改为 2(即 40ca525898eba6df88bca451342515c2)同样报“参数签名异常”的错:


前端无秘密:看我如何策反JS为我所用(下)


基本上验证了我的猜测,业务系统的防重放和防篡改能力依赖 sign 参数。我得想法绕过防御机制。

业务系统的防御,我大致了解(谁还没点在蓝队背景 <(▰˘◡˘▰)>)。客户端对所有请求参数进行哈希计算,得到参数签名(sign),将签名放入首部 authorization 中提交至服务端,服务端基于相关信息生成签名,与客户端提交的签名进行比较,若不同,说明参数被篡改,则不响应该请求,若相同则响应。签名用后即废,若重复,说明请求被重放,则不响应该请求,若不重复则响应。

刺探出 sign 的重要性,只要我能控制随意生成 sign,那么服务端防御的问题也就迎刃而解啦。


2.1.2 分析参数签名逻辑

虽然该站点前端有代码混淆、反调试等保护措施,但之前已加被我解决掉,找寻并提取实现签名逻辑的 JS 不会太困难。

全局搜索 sign 关键字,找到多个匹配项,只有 1875 行是生成首部 authorization 的值的逻辑:


前端无秘密:看我如何策反JS为我所用(下)



跳至 1875 行,进入函数 _e() 内部,找到了计算签名的逻辑(s),以及生成 authorization 值的逻辑(函数返回值);为查看 _e() 的调用实参,我在入口处设置断点,为查看生成的 authorization 值,在出口处设置断点:


前端无秘密:看我如何策反JS为我所用(下)


再回到页面上输入手机号 13988888840,点击“获取验证码”按钮,流程进入断点,了解传递实参的信息:


前端无秘密:看我如何策反JS为我所用(下)


切换至 console 中,参照调用方式,改用实参 13988888849 调用 _e():

_e("POST", "{"BODY":{"PHONE_NO":"13988

888849","GROUP_ID":"2","REGION_ID":"11"}}")

生成新签名:


前端无秘密:看我如何策反JS为我所用(下)


burp 开启拦截模式,放行前端断点,burp 中拦截到参数值 13988888840 及其 authorization 值的请求报文,将其改为 13988888849 及其新 authorization 值:


前端无秘密:看我如何策反JS为我所用(下)


服务端正常响应,返回 13988888849 加密后的用户信息 Data:


前端无秘密:看我如何策反JS为我所用(下)


现在,我可以绕过参数签名机制,具有随意更改参数的能力了。只要能控制生成签名,绕防重放也就易如反掌,每次提交请求时,我同步生成新签名即可。

为方便生成参数签名,我把 JS 的 _e() 转为 python 脚本 gen_authorization.py:


前端无秘密:看我如何策反JS为我所用(下)


整理下,现在我可以访问 /xx/api/xxxx/h5/xx/sChkBlPhone 接口获取加密后的用户信息 Data,可以调用 decrypt_data_by_nodejs.py 解密 Data,获取用户姓名、单位地址、家庭地址、身份证号码,拿到单个用户的敏感信息三要素;我可以调用 gen_authorization.py 绕过防重放和防篡改机制,批量获取多个用户的敏感信息。综合已有脚本编写 exp,实现武器化 get_users_info_by_requests.py:


前端无秘密:看我如何策反JS为我所用(下)


运行如下:


前端无秘密:看我如何策反JS为我所用(下)


2.2 复用页面方式

我计划用 webdriver 驱动 firefox 浏览器,模拟人工操作。在页面中输入手机号,点击“获取验证码”按钮,从解密后的 Data 中提取用户信息,以此往返,不断输入新手机号,获取多个用户的个人信息。

以我的判断,python 实现这个任务是件容易的事情,于是,我快速写了个 PoC 脚本 get_users_info_by_webdriver.py,用 webdriver 输入手机号、按下“获取验证码”按钮:


前端无秘密:看我如何策反JS为我所用(下)


一运行才知道,完全不是我预想的那样,挡在我前面的绊脚石还不止一颗。

解决非 DOM 弹窗问题。运行脚本,浏览器弹出提示框,询问是否允许该站点访问我的位置:


前端无秘密:看我如何策反JS为我所用(下)


每次点击按钮都会触发该弹窗,所以不可能手工点击,必须得让程序自行处理。要是这个弹框由 DOM 创建(类似 alert()),webdriver 可以轻松将其关掉,遗憾的是,它由浏览器创建。两个思路:一是,分析源码可知,前端通过 navigator.geolocation.getCurrentPosition() 获取位置,可拦截服务端下发的 JS,删除该函数调用后,放行至浏览器,让前端根本不执行获取位置的逻辑;二是,在 webdriver 中设置 firefox 的属性,让其总是允许获取位置信息,不再询问。后者较简单,选之,代码如下:


前端无秘密:看我如何策反JS为我所用(下)


解决等待 30 秒的问题。点击“获取验证码”后,按钮变为无效需等待 30 秒:


前端无秘密:看我如何策反JS为我所用(下)


webdriver 无法及时对新手机号触发“获取验证码”,直到等待时长结束。我不可能真的等个半分钟,那武器化有何意义!

一般来说,若通过 HTML 的 disabled 属性将元素设置为禁用状态,可让 webdriver 执行 JS 删除该属性即可:


前端无秘密:看我如何策反JS为我所用(下)


但从前端源码分析可知,该站点通过 div 实现的元素禁用:


前端无秘密:看我如何策反JS为我所用(下)


即便从元素审查删除 disabled-btn,一秒钟后按钮又被置为无效。

尝试从时长 30 秒入手。两个思路:一是,点击按钮后,刷新页面,自然不用再等待;二是,篡改服务端下发的 JS,将原本等待 30 秒调整为 0 秒。前者实现快、后者效率高,择优后者。

我开始找寻相关逻辑。JS 常以毫秒为单位设置定时器,搜索 30000,找到一个匹配项(其他为 300000):


前端无秘密:看我如何策反JS为我所用(下)


跟进下断点,点击按钮,发现并未中断流程,说明并非该语句;30000 也常用科学计数法表示,搜索 30e3、3e4 均无果:


前端无秘密:看我如何策反JS为我所用(下)


或许是以秒为单位呢,那就搜索 30,(⊙o⊙)…不能直接搜它,匹配项太多,得配合正则整词搜索,但 firefox 的全局搜索不支持正则,单个 JS 文件中的局部搜索才支持正则,所以,凭感觉针对先前关注的 businessReservation.js,正则搜索整词 30,匹配一项:


前端无秘密:看我如何策反JS为我所用(下)


代码逻辑清晰,每秒钟轮询验证是否到达 30,若是则更新按钮状态及内容。大概率是它!为验证猜测,我强制刷新页面,抓包拦截关键字 30-parseInt(n,10),将其改为 0:


前端无秘密:看我如何策反JS为我所用(下)


放行修改后的 JS 至浏览器,等待时长果然变为 0 秒,也就达到无需等待的目的了。


虽然上述方式可行,但必须借助 burp 手工删除等待逻辑,而 webdriver 无法修改 JS,怎么办呢?两个思路:一是,让 webdriver 流量过 burp,burp 先开拦截模式,将 30-parseInt(n,10) 改为 0 后,再调整为透传模式;二是,借助 mitmproxy,对自己实施中间人攻击,将 30-parseInt(n,10) 改为 0。思路一开发便捷,思路二运行高效,权衡之下,我选思路一。


其实嘛,思路一可以再优化下,实现全自动化。你知道,burp 是款功能强大的 web 渗透工具,其中,可以设定关键字,自动将报文中匹配项替换为指定字符串。所以,我加了一条替换规则:在响应报文中,将关键字 i=30-parseInt(n,10) 改为 i=0,如下所示:


前端无秘密:看我如何策反JS为我所用(下)


现在只需让 webdriver 的流量过 burp 即可。这好办,既可以在 webdriver 中用代码实现,也可用透明代理工具 proxychains 控制,显然后者简单多了。proxychains 原理不复杂,就是劫持(hook)网络收发的系统函数,将流量转发给三方。先配置 proxychains,让指定流量转发至 burp 的监听端口,编辑 /etc/proxychains.conf 追加如下内容:


前端无秘密:看我如何策反JS为我所用(下)


后续用透明代理方式加载脚本:


前端无秘密:看我如何策反JS为我所用(下)


burp 中确认能收到 webdriver 的流量:


前端无秘密:看我如何策反JS为我所用(下)


我看了 webdriver 控制的页面,等待计时器直接归零:


前端无秘密:看我如何策反JS为我所用(下)


目的达到了!

决解密数据未显示的问题。现在我能让浏览器自动输入不同手机号码并提交,虽然业务的 JS 解密了含有用户信息的 Data,但并未将其输出。我得想法增加输出逻辑。

首先,找到解密数据。由先前分析可知,调用解密函数的代码为:

te.CBC(i.Data, 'Dxxxxxxx1')

全局搜索,找到两个匹配项:


前端无秘密:看我如何策反JS为我所用(下)


分别下断点,页面上点击按钮后,控制流进入 1847 行的断点,证明该行的确在调用解密函数。单步执行,在栈中查看存放解密数据的变量 i,可以看到解密后的用户敏感信息:


前端无秘密:看我如何策反JS为我所用(下)


然后,输出解密数据。我计划这样,让 webdriver 流量过 burp,在 burp 中设置替换规则,将业务原来的语句:

i = JSON.parse(te.CBC(i.Data, 'Dxxxxxxx1'))

替换为

i = JSON.parse(te.CBC(i.Data, 'Dxxxxxxx1')); console.log(i)

增加将结果输出至控制台的逻辑,实操时发现,流量中根本找不到 i = JSON.parse(te.CBC(i.Data, 'Dxxxxxxx1')),想了想,这条语句是经过浏览器美化的,原始语句可能没有中间的空格、可能是双引号,于是,我到 burp 的 proxy - http history 中搜索 i=JSON.parse(te.CBC(i.Data,'Dxxxxxxx1')) 无果、搜索 i=JSON.parse(te.CBC(i.Data,'Dxxxxxxx1')) 命中两项:


前端无秘密:看我如何策反JS为我所用(下)


我重新配置 burp 的替换规则,将未被浏览器美化的原始语句:

i=JSON.parse(te.CBC(i.Data,"Dxxxxxxx1"))

替换为:

i=JSON.parse(te.CBC(i.Data,"Dxxxxxxx1")); console.log(i)

这次,倒是替换成功了,但前端又报错,分析发现,由于有两个匹配项,两个均被替换,所以,我优化了下替换规则,将原始语句:

(i=JSON.parse(te.CBC(i.Data,"Dxxxxxxx1"))),t.sucCallback&&

替换为:

(i=JSON.parse(te.CBC(i.Data,"Dxxxxxxx1")), console.log(i)),t.sucCallback&&

这下运行正常了,我进入开发者工具,页面立即卡死,喔喔~~(>_<)~~,忘记页面有防调试逻辑,没事,在 burp 中增加替换规则,将防调试逻辑代码 setTimeout(e,100) 替换为空语句 ; 即可;重新运行,进入开发者工具的 console 中,可看到变量 i 的内容,为让输出结果更简洁,再次优化替换语句,只输出用户姓名、单位和家庭住址、身份证号等敏感信息:

(i=JSON.parse(te.CBC(i.Data,"Dxxxxxxx1")), console.log(i['ROOT']['OUT_DATA']['CUST_NAME'], i['ROOT']['OUT_DATA']['ID_ADDRESS'], i['ROOT']['OUT_DATA']['CUST_ADDRESS'], i['ROOT']['OUT_DATA']['ID_ICCID'])),t.sucCallback&&

完整替换规则如下:


前端无秘密:看我如何策反JS为我所用(下)


最后,综合已有信息,配合 proxychains、burp 再次运行 get_users_info_by_webdriver.py,可批量拖取用户信息:


前端无秘密:看我如何策反JS为我所用(下)

结语


你看,业务有数据加密、有反调试、有防篡改、有防重放、有流控等一系列限制,我仍然可以逐一绕过,获取大规模用户信息。

所以,业务厂商号称具备的某种防御机制,哪怕说得出个一二三,在被实际捣腾之前,我也很难相信真的有效。


当然啦,即便这样,前端通过签名实现的参数防篡改、请求防重放仍有价值 提高了入侵门槛、延缓了攻击进程。总的来说,该站点安全做得挺好,在此基础上,若能增加动态防御,攻击难度肯定得翻倍!动态防御,在防自动化、防机器人、防前端调试方面,效果显著(从我少得可怜的攻防经验来看,某数做得不错)!那么,有动态防御就一定能阻挡我进攻的步伐?不知道,毕竟,前端,无秘密!

前端无秘密:看我如何策反JS为我所用(下)

本文始发于微信公众号(疯猫网络):前端无秘密:看我如何策反JS为我所用(下)

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: