一文搞懂前端加密和js逆向
学习的必要性
在如今科技飞速发展的时代,传统的明文传输已经慢慢退出历史的舞台。更多的是对用户传入的参数进行加密。
甚至有些时候,对用户的响应内容也是加密的。
那么,当我们测试越权、弱口令和 sql 注入的时候,就必须要对传入的参数进行加密。否则大多时都是无功而返。同时针对于前端加密还有一个天然的优势。由于内容都是经过加密的。 自然不会被 WAF 所拦截。
常见的 JS 加密
在线解密工具:https://www.ssleye.com/ssltool/
对称加密
比如 AES、DES 使用的加密和解密的密钥都是同一个。所以叫做对称加密。
非对称加密
比如 RSA 和 ECBSA 使用非对称加密,也就是说,加密和解密使用的是两个密钥,公钥一般在前端,私钥放在后端用于解密。
JS 逆向
快速定位到加密处代码
示例靶场地址:https://github.com/outlaws-bai/GalaxyDemo
通过字段关键字
虽然这里传入的值是经过加密处理的,但是它的 key 是明文的。
因此我们可以直接进行搜索 data:
或 data=
以及 data =
。进行定位。
当然这里我更推荐在 network 中使用 Ctrl + F 键进行搜索:
定位到相关位置只会可以发现这里使用的是 DES 加密。
- key 为 12345678。
- 偏移量 IV 为 12345678。
- 模式为 CBC
- 填充为 Pkcs7
对加密的内容进行解密:
通过 JS 关键字
可以全局搜索一些关键字如 CryptoJS
、encrypt
、decrypt
、key
、iv
等。
通过路由
点击查询或登录的时候,我们来到 network 查看请求,可以发现请求的接口为 getUserInfo
看其启动器即可定位到请求时执行的方法。
定位到相应方法只会,查看对接口传入的参数。本案例传入的是 {data : encrypted}
接着往上追踪即可。
通过事件监听
这里的加密字段是通过点击 query 按钮的时候进行加密的,因此我们可以查看该元素的监听事件。重点关注一些 click 事件和 submit 事件。最后点击定位即可。
配合断点调试
实际业务系统中,可能会添加一些混淆、或代码量比较大,方法比较多。那么此时我们就需要配合断点调试。
通过以上几种方法定位到大概位置,然后多下几个断点进行调试:
一步一步查看断点,此时我们的 username 还为明文。
继续跟进发现这里对我们传入的用户名作为对象中的值,其属性名称为 username 值为我们传入的内容,即 user1,最后对该对象进行 DES 加密之后,作为 getUserInfo
接口的 body 部分中的 {data : "此处为加密内容"}
内容发送给后端。
因此前端实际发送的内容为:
这里需要注意的是,断点不要下在函数定义上。
靶场实践
这里使用的靶场为:https://github.com/SwagXz/encrypt-labs
AES 固定 KEY
发送给后端的内容:
由于这里传入的参数名为 encryptedData
因此我们在前端进行搜索:
可以发现该接口发送的内容跟我们抓包获取的发送内容一致。都是以 encryptedData=
开头。并且该 JS 代码是做了混淆处理的。
接着我们向上追踪。定位对加密字段的处理片段,可以发现是 _0x1375d7
这个变量。在这里发现 iv 的变量为 _0x2d9cd5
,那么上一个则是 AES 的 KEY 值。最后发现 sendDataAes()
函数的形参为 api 的 URL 地址。
上述之后我们发现:
- 加密方式:AES
- 模式:CBC
- 填充:未知
- IV:1234567890123456
- KEY:1234567890123456
这个填充字段由于混淆比较严重,于是我们这里尝试断点调试。
接着在 console 控制台中输出 padding 属性对应的值获取方法,但是这里为 undefined。
但是这个偏移量填充字段随便输入也能解密。https://www.toolhelper.cn/SymmetricEncryption/AES
最后想知道哪里调用了 sendDataAes()
方法搜索 sendDataAes(
即可。最终发现是我们点击登录的按钮绑定了该方法。传入的 api 地址为 encrypt/aes.php
至此已经分析完成。
AES 服务端获取 KEY
输入用户名和密码点击登录进行抓包,发现这里先请求了 server_generate_key.php
获取 KYE 和 IV。
因此现在已经拿到了 KEY 和 IV。但是直接解密是无法解密的。猜测可能进行了其他处理。
接着在前端进行定位。这里我们采用查看登录按钮的事件进行定位。
定位到 fetchAndSendDataAes()
函数的定义,搜索 fetchAndSendDataAes(
。发现这里先请求了 server_generate_key.php
文件。
接着 debug 发现这里经过一系列的处理,将 key 和 iv 赋值给这两个变量。
当 debug 经过这里之后,我们在控制台进行打印。实际上最终的 KEY 和 IV 是十六进制的。
后面的代码就是获取用户名和密码。我将其转为 JSON 格式:例如:
1 | { |
接着调用加密方法进行加密。
从上图中我们可以知道填充为:Pkcs7,但是加密方式以及模式还不知道。这里我们就需要 debug 到以上代码的下方。然后在 console 控制台打印。
最后得到:
- 加密方式:AES
- 密钥:87eeb363f722d4730ffdce736f8f19ff
- 偏移量:aa29401e5892b0820cefa2f31bb4e141
- 模式:CBC
- 填充:Pkcs7
这里使用 autoDecoder 插件对其进行爆破,首先配置:
我们再来看请求,这里就会对内容进行解密。
接着我们需要使用 Intruder 模块对此处进行爆破(直接在上图中右键发送即可)
虽然这里看到的传输内容是明文,但是实际上是加密只会发送到服务端的。这是 autoDecoder 自动会对请求体进行加密。
RSA 加密
抓取登录的数据包:
这里我使用的还是通过按钮的事件定位到具体的方法。
进入到这里只会,我们在这里打断点。这里很明显是一个公钥。
当将公钥赋值给变量执行之后,我们在 console 控制台打印这个公钥。由于存在 \n
换行故而这里通过 console.log()
进行打印。会解析换行符号。
但是由于该环境 RSA 只有公钥泄漏没有私钥,因此只能用于加密,无法对内容进行解密。因此进行密码的爆破还是可以的。
这里有个坑点就是需要 URL 编码一下。
注意红框部分不要填,不然会解密失败。
对密码进行爆破,autodecoder 插件会自动对 body 进行加密。
DES 规律 KEY
定位到相关函数之后,我们这里直接找到 KEY 和 IV 所在的位置。
- 红色箭头指向的为 IV
- 绿色箭头指向的伪 KEY
- 蓝色箭头指向的为 Padding
这里我们 debug 到以上代码的下方,然后在控制台打印。
可以得到信息如下:
- KEY:61646d696e363636
- IV:3939393961646d69
- Padding:Pkcs7
其中 KEY 和 IV 是经过 HEX 编码之后的。解码如下:
这里我们也可以在控制台将他们进行打印。
这里蓝色箭头所指向的是用户名,红色箭头所指向的是对密码进行 DES 加密之后传给后端的。
因此用户名为明文,密码为密文传输。
代码中很明显将 666 和 9999 写死了,而动态的是用户名。如果传入的用户名为 test,则 KEY 就为 test666,IV 为 9999test。所以 KEY 和 IV 的生成规则为:
- KEY:用户名+666
- IV:9999+用户名前四位
但是经过实际测试,如果用户名 8 位,则 KEY 的后面不填充 6,反则将空余的位置填充 6。IV 则是在前面填充 9999 后面取用户名的前四位。
在上方我们就发现,这里将 KEY 和 IV 采用了十六进制编码。如果我们想要爆破用户名 admin,则 KEY 为 admin666 的十六进制。IV 为 9999admi。将其转为十六进制即可。
接着代码中将密码直接获取并进行加密。因此密文就是密码的直接加密形式。
在 autoDecoder 插件中配置
Burp 解密效果如下,将其发送到爆破模块。
核对一下地址,有时候会自动转为 https 并且添加枚举处。
简单添加一下字典。
明文加签
通过事件监听定位到该按钮的点击事件。
- 红色框标注的分别获取了用户名和密码的值。
- 绿色框标注了通过生成随机数字将其转为 32 进制变成 0-9 a-z 范围的数据。
- 蓝色框标注的为当前时间戳(JS 获取的有毫秒时间戳,最后除了 1000 变为秒级的)。
- 黄色框标注的为固定 KEY。
- 紫色框标注的是将用户名+密码+绿色框随机字符+蓝色框随机字符串拼接形成新的字符串。
- 黑色框标注的是通过 HmacSHA256 算法将紫色框的内容使用黄色框的 KEY 进行加密。
nonce 为上图绿色框标注的内容,timestamp 为蓝色框标注的内容,signature 为紫色框标注的内容(黑色框进行加密的,最后返回十六进制)。
由于 autoDecoder 没有这个算法,故而需要我们编写加密接口的脚本:
1 | import json |
在 autoDecoder 插件中配置:
选择接口加解密
配置加解密的接口
接着 burpsuite 枚举密码字段即可:
虽然这里的 nonce 、timestamp、signature 没有改变。
但是实际上发给服务器的实际通过加密接口进行替换了,这里可以看下 wireshark 抓的数据包:
先请求加密接口替换数据获取加密的数据:
然后向接口发送校验请求:
可以看到相关的字段已经被加密了。
禁止重放
当我们抓取登录的数据包时,第一次发送正常:
第二次发送就提示错误:
通过事件监听定位到相应的代码并进行断点调试,发现这里调用了 generateRequestData()
方法获取了响应体。接着直接发送了。
跟到创建请求体的方法:
- 第一个绿色框标注的为获取用户名和密码的值
- 第二个绿色框标注的为获取当前时间戳(毫秒级)
- 红色框标注的为获取公钥
继续跟进分析:
这里将毫秒级的时间戳和公钥带入到了 _0x5b0e97() 函数,该函数返回的值,就是最终 random 的值。
跟进到该方法分析,就是将当前的毫秒级时间戳进行 RSA 加密只会,作为 random 的值来防止重放。
编写脚本:
1 | import json |
在 autoDecoder 中配置使用即可,具体参考上一关《明文加签》
加签 KEY 在服务端
先看 burpsuite 抓的登录请求:
首先会访问后端接口将用户名和密码发送给加签的接口,获取一个签名。
接着将签名发送给校验用户名和密码的接口进行校验。如果修改了用户名或密码字段则需要重新获取签名。
前端代码跟我们在 burpsuite 看到的效果是一样的,先从服务端获取签名,再去校验。
虽说签名加密和解密都在后端,但是我们可以编写脚本进行密码枚举。
1 | import json |
运行脚本,配置 autoDecoder 爆破即可:
经过大量的枚举测试,把线程调低一点(便于有足够的时间去获取签名),还是可行的。
在实战场景下,如果有验证码的存在,可能会稍微复杂一些。
JS 逆向的高级应用
虽说这个系统本是一个靶场,不过这个靶场比较贴合实际场景,因此将其记录。
靶场地址:https://github.com/0ctDay/encrypt-decrypt-vuls
尝试登录系统发现传入的用户名和密码都进行了加密(属于不显示参数的那种),而响应包也是加密了的。
同时在请求头中也发现了随机的 requestId 和 sign 值来防止重放:
作者文章中提到“很多新手会认为一定得找到加解密函数。其实不然, 老手都会告诉你无论是APP还是JS中可以先找到明文点”。那么什么是明文点呢?
在前端进行复杂的请求操作时,肯定会经过一系列从A函数–>B函数–>C函数–>D函数–>E函数之类的流程, 那么在这个流程中,假设D函数是加密函数,那么ABC函数中原始请求参数均是明文的,这就是明文点,找到明文点后再一步步调试, 其实就能顺腾摸瓜找到加密函数了。
寻找明文点
方法一: v_jstools
作者项目地址:https://github.com/cilame/v_jstools
新版可能有点问题,不行的话可以去链接: https://pan.baidu.com/s/14ZDJ4roEfa1Q1k2hbzdy3w 提取码: 15vc 进行如下配置:
打开如下按钮
接着控制台会出现如下 inject start!
表示开启成功。
由于控制台有一些干扰信息清空之后,进行登录:
这里就发现了明文的位置,我们点击打印的 URL 地址跳转到相应为止。
在此处进行断掉调试,此时的 t.data
还是明文。接着经过函数 v 处理之后依旧是明文,因此加密点不在这里。
看后续代码我们可以发现:
- 请求头中的 timestamp 为变量 r 的值,即当前时间戳(毫秒级,转为秒级需除 1000)
- 请求头中的 requestId 为变量 i 的值,i 的值由 p 函数生成并返回。
- 请求头中的 sign 值为 n+i+r 的 MD5 值。n 为用户名密码验证码的 JSON。
最后将 n 带入到了函数 l 进行了处理。我们执行到该处。可以发现经过函数 l 处理之后就变成了加密内容。
因此我们在函数 l(n)
处下一个断点,进入到该函数:
按照 AES 的加密方法使用,可得出:
- t 为要加密的数据
- f 为加密所使用的 KEY
- h 为加密所使用的偏移量
- 加密模式为 CBC
- 填充为 Pkcs7
因此使用在线网站解密:
解密请求包:
解密响应包:
方法二:JS Hook
修改当前数据包
断点调试修改
这种方式不推荐,比较麻烦。
JS-forward
该系统存在 requestId,timestamp,和 sign 因此可以借助 JS-Forward 进行发送(即使知道了 KEY 和 IV, 也不能在 Burpsuite 中进行重发),后续可能通过 JSRPC 远程调用或 Python 编写加密脚本解决这一问题。
首先我们了解一下 JS-forward 运行原理,简单来说就是在明文点处插入一段 JS 代码,这段代码先通过 AJAX 请求将现有的请求内容发送给 burpsuite,burpsuite 拦截并修改内容后,返回到原始变量中,优点是操作比较统一,如果明文点正确,后续所有的改包操作都可以在 burpsuite 中进行
第一步确认明文点:
刚刚通过 v_jstools 工具已经确认了明文点,也就是 t.data
。
JS-Forward 项目地址:https://github.com/G-Security-Team/JS-Forward
启动 JS-Forward:
第二步插入 JS 代码:
JS-Forward 的使用尽量在明文点函数的第一行插入 JS-Forward 生成的代码,因为不知道后续代码做了什么操作。
这里以 Chrome 浏览器插入 JS 为例:
b1:找到F12–源代码–替换(覆盖)–点击选择文件夹–选择我们硬盘中一个空文件夹
如果有提问,点击编辑文件
b2: 在 网页–明文点JS文件处–右键–替换内容
在函数的第一行插入 JS-Forward 生成的 JS 代码,然后 CTRL + S 保存。
关闭调试功能或关闭 F12,打开 burpsuite 进行拦截(浏览器无需设置代理到 burpsuite):
该方式是不需要浏览器设置代理为 Burpsuite 的。
工具的原理如下:在明文代码处插入 JS 代码,JS-Forward 会将明文数据的变量值发给 Burpsuite(通过代理的模式),将 Burpsuite 的返回的 JSON 字段,赋值给 t.data
。
主动发包的加密和解密
以上《修改当前数据包》只适合浏览器进行提交操作后的数据修改,而实际场景下,可能有一些自动请求的接口也加密了。
- 优点:是简单易上手,就算是复杂的加密环境,只要找到明文点,后续工作不太复杂。
- 缺点:是无法应对主动发包的情况,比如要使用被动扫描工具,暴力破解,重放测试等需求的时候,无法自动化完成。
JsRpc 远程 JS 调用
项目地址:https://github.com/jxhczhl/JsRpc
启动 JsRpc 并注入代码
运行程序:
接着复制 JsRpc 项目目录下的 resource 目录的 JsEnv_Dev.js 文件内容到 console 控制台:
接着输入如下代码连接客户端:
1 | var example = new Hlclient("ws://127.0.0.1:12080/ws?group=example"); |
此时的服务端状态:
记录加密函数
根据上诉对系统的分析,我们可以得知,加密函数为 l
。因此我们需要记录加密函数到 window。但是由于函数 l
可能是局部的函数,因此我们需要断点到其使用处再进行记录。
1 | window.enc = l |
向 JsRpc 注册加密函数
控制台执行:
1 | // 这个example要和客户端连接变量名一致 |
此时就可以把断点执行掉了(不然会提示超时)。接着访问:
1 | http://127.0.0.1:12080/go?group=example&action=enc¶m=admin123 |
此时该接口就调用了 JS 中的加密函数返回加密之后的值。但是此时还不足以进行爆破。因为不只是对请求体的内容进行加密,而请求头部信息中还有 requestId、sign、timestamp 等随机值。
JSRPC-配合 AutoDecoder
方案介绍和请求分析
目前比较流行的一个解决方案, 通过 mitm 将原始请求发送到 JS-RPC 中进行加密后修改原始数据包内容, 再进行发包 。
这里关于网站的加密分析在本文的“寻找明文点->方法一:v-jstools”已经分析了。下面直接引用:
请求头中的随机字段:
- timestamp
- requestId
- sign
请求体的加密字段:
- 用户名、密码、验证码的 JSON 数据
后续代码调试我们可以发现:
- 请求头中的 timestamp 为变量 r 的值,即当前时间戳(毫秒级,转为秒级需除 1000)
- 请求头中的 requestId 为变量 i 的值,i 的值由 p 函数生成并返回。
- 请求头中的 sign 值为 n+i+r 的 MD5 值。n 为用户名密码验证码的 JSON。
最后将 n 带入到了函数 l
进行了处理,函数 l
就是请求体的加密函数。
针对数据包的修改我们需要在请求头中添加
- timestamp
- requestId
- sign
然后对请求体的内容调用页面的函数 l
进行加密。
启动 JsRpc 并注入代码
复制 resouces/JsEnv_Dev.js 的内容注入到网页并进行连接:
1 | var example = new Hlclient("ws://127.0.0.1:12080/ws?group=example"); |
断点调试记录函数并注册
断点到执行加密函数的函数内:
记录函数:
1 | //时间戳 |
注册:
1 | //md5函数 |
测试 JSRPC
由于我们注册的时候使用的是 req,因此这里的 action 为 req。
这样我们就可以一次性获取所有请求的需求了。
密码破解
这里需要说明的是,该靶场的验证码在前端生成,也就是说验证码在后端是没有校验的,因此随便填即可。
编写脚本 用于调用 JSRPC 将请求体进行加密,并返回 timestamp,requestId,sign 以及 body 加密的值。
1 | import json |
接着 autoDecoder 配置:
添加 payload 之后进行枚举(这里建议不要使用多线程,不限请求加密接口可能不及时,导致一些漏处理):
失败的数据包:
Yakit 热加载
实战项目
JS 源码地图
对某系统进行渗透测试,发现在获取一些当前用户信息、用户列表等接口的时候,这里传入了 token。
尝试对其进行 base64 解码,发现也是乱码,那么可能就没有那么简单。
于是在进行翻阅 JS 文件的时候,意外发现该网站是通过 webpack 进行打包的,存在 .js.map
文件直接可以还原混淆的打包之前的 vue 源码。这里直接查看在工具类中发现 crypto.js 文件,文件中发现了 KEY 以及模式。
通过上诉信息,解密我们传入的 Token,发现 id 为 23。
尝试将 1 进行加密之后查看返回信息:
返回了其他信息,证明此处是存在越权的。
对影响内容按照上面的方法进行解密,成功越权。