yakit热加载实现自动化加解密
简介
什么是yakit
的热加载
???
广义上来说,热加载是一种允许在不停止或重启应用程序的情况下,动态加载或更新特定组件或模块的功能。这种技术常用于开发过程中,提高开发效率和用户体验。在Yakit
的Web Fuzzer
中,热加载是一种高级技术,让 Yak 成为 Web Fuzzer
和用户自定义代码中的桥梁,它允许我们编写一段 Yak 函数,在Web Fuzzer
过程中使用,从而实现自定义fuzztag
或更多功能。
个人理解,热加载本质也是中间人劫持,类似于在渗透过程中使用burp
或者yakit
抓包->修改->放包的过程。只不过常规来说的中间人劫持是在客户端和服务器之间的。热加载
就是在客户端与yakit
、yakit与服务器
之间的劫持。在原本的某个环节上面增加一段代码,从而实现渗透测试简便化。
热加载里面会涉及到Yaklang
的编程,这块可以参考对应的官方文档地址:
https://www.yaklang.com/docs/yak-basic/intro
热加载使用方法介绍
此处以对请求包中的密码实现base64
来进行介绍。我们直接来看案例,通过案例来理解热加载的意义。
1、对数据包里面的密码进行base64编码
,我们先看没有热加载的情况:
选择对应的标签
2、接下来使用热加载来实现加密:
点击热加载按钮:
接下来先解释一下这两个东西:插入热加载和热加载函数
调用热加载 fuzztag来插入热加载
实际上,我们调用热加载中编写的函数也使用 fuzztag 。我们也可以理解为热加载是一种特殊的 fuzztag ,它的格式为:
{{yak(funcname)}} |
当我们需要传入参数时,则格式为
{{yak(funcname|param)}} |
需要注意的是,我们传入的参数可以是 fuzztag ,也就是可以编写形如:
{{yak(funcname|{{x(pass_top25)}})}} |
热加载函数定义来提供功能
我们可以在热加载页面中编写热加载函数,其函数定义格式如下:
// 函数名为funcname,参数只有一个,为param,类型是字符串 |
我们在热加载页面里面可以随意的去写相关的函数。比如:
具体函数里面的一些即可和关键词还需要了解yaklang
的知识。
写完函数怎么使用呢?相当简单,直接在模版内容里面进行插入编辑器位置即可。
接下来再加入字典作为参数:
然后点击发送请求,看看效果:
热加载中的”魔术方法”
在热加载代码区中,实际上存在几个特殊的魔术方法:beforeRequest
和afterRequest
,它函数的定义如下:
// beforeRequest 允许在每次发送数据包前对请求做最后的处理,定义为 func(https bool, originReq []byte, req []byte) []byte |
这两个魔术方法分别在每次请求之前和每次请求拿到响应之后调用,它们可以用于修改我们 Web Fuzzer
的请求与响应。通过这两个魔术方法配合 Yak代码,我们实际上可以实现许多有用的功能。
比如我们这里可以简单演示一个测试功能,把请求体里面的macOS
替换成windows
:
点击保存,发包测试:
案例没有什么实际意义,但是可以简单说明一下beforeRequest
这个功能。
使用热加载实现更灵活的 fuzztag
热加载函数可以返回一个数组,当我们返回一个数组时, Web Fuzzer 会将数组中的每一个元素都作为值去发包,所以当我们数组有5个元素时, Web Fuzzer 会发出5个包,这样我们就可以实现更灵活的fuzztag了。
以下是一个简单的例子,我们返回一个数组,其原始值为list = ["1", "2", "3"]
, 然后将数组中的每个元素进行base64编码后再url编码:
handle = func(param) { |
然后我们点击保存按钮,保存热加载内容,这时候我们就可以在 Web Fuzzer 中使用这个热加载 fuzztag 了,如图所示,可以看到数字1,2,3都被base64编码后再url编码了:
案例1: Csrf token保护下的爆破
此处引用yakit官方案例:https://yaklang.io/products/Web%20Fuzzer/fuzz-hotpatch-example1
我们将会介绍一个热加载的实际应用案例:csrf token保护下的爆破。我们以pikachu靶场为例,安装pikachu靶场后(记得要初始化),我们打开如下页面:
任意输入账号密码之后,使用 yakit 的 MITM 模块拦截登录请求,我们可以看到请求如下:
注意到除了用户和密码之外,还存在一个token的post参数,这是一个csrf token,它主要是用来防止CSRF攻击的,但是这也给我们的爆破增加了一定的难度,因为每次爆破都需要使用一个新的token。那么在这种情况下我们应该如何爆破呢?
首先我们应该来看看token是从哪里获取的,我们继续使用 yakit 的 MITM 模块拦截请求,这次我们拦截的是直接访问该页面的GET请求,当我们将其发送到 Web FUzzer 发送请求后,可以看到其响应中存在token:
接下来就是让热加载出手了。我们很容易想到热加载中的”魔术”方法:beforeRequest
。我们可以在请求发送前,再次发送一个GET请求来获取token值,再用这个值进行爆破,就可以绕过csrf token的保护了。
我们来看看如何实现,首先我们需要在热加载页面中编写热加载内容,代码如下:
beforeRequest = func(req) { |
这里我们一共做了以下几件事情:
- 通过GET请求,拿到响应
- 通过响应拿到Set-Cookie的值
- 通过xpath语法获取token的值
- 替换
__TOKEN__
为实际的token - 添加POST请求的Cookie为第二步中拿到的Set-Cookie,这样模拟了多个用户同时请求的情况
我们将token参数改为__TOKEN__
,然后点击发生请求按钮,如图所示,显示username or password is not exists~
而不是csrf token error
,这证明我们已经成功获取到token了:
接下来就很简单了,使用我们前面学到的 fuzztag 知识,使用 fuzztag 爆破密码,成功爆破出了admin的密码为123456:
需要注意的是,我们使用了并发的技术来请求该网站,这也是我们为什么需要先从GET请求的响应中拿到Set-Cookie然后再设置POST请求的Cookie的原因,因为我们需要模拟多个用户同时请求的情况,如果是单个用户请求的话,我们需要将并发改为1。
案例2: 前端验签-SHA256
JS代码分析
通过查看源代码可以看到key为
1234123412341234 |
可以看到是通过SHA256来进行签名的,他把请求体的username和password字段提取,然后进行加密。
username=admin&password=admin123 |
使用CyberChef
加密,最终得到加密值为:fc4b936199576dd7671db23b71100b739026ca9dcb3ae78660c4ba3445d0654d
可以看到自己计算和前端计算的一致:
修改密码,重新构造签名:
username=admin&password=666666 |
发送请求,可以看到验证签名成功,密码正确登录成功,自此签名绕过成功:
POST /crypto/sign/hmac/sha256/verify HTTP/1.1 |
热加载
通过beforeRequest
劫持请求包,使用encryptData
函数进行加密,最终实现热加载自动签名功能。
encryptData = (packet) => { |
调试结果如下:
把beforeRequest
取消注释,添加到Web Fuzzer模块的热加载中:
保存后发送请求,热加载成功实现自动签名功能。
案例3: 前端一次性sign签名的分析
https://mp.weixin.qq.com/s/-kjj3PXqsUOj8Hlh38r-ug
前言
在最近的渗透测试项目,经手了某机构的管理系统,其中,它对请求包进行了sign签名校验。
签名位置:请求头中,单独列出一项 X-Request-Sign
签名的效果:
- 请求包仅能发送一次,重放提示签名已校验
- 签名存在时间校验,超出一定时间,签名过期
- 签名会校验请求体中的内容是否被修改,被修改后签名无效
初步分析:
第一条,一般是后端X-Request-Sign的值进行了记录,判断签名是否已经存在,比如保存在Redis中。因为存在签名过期时间,所以只要设置X-Request-Sign值的过期时间,就能避免存储数据过大的问题。
第二条,在X-Request-Sign中分为两个部分,一部分是经过算法得到的hash值,还有一部分拼接在字符串后面,是一串时间,精确到毫秒。
第三条,签名中进行hash算法的一部分参数为请求体中的内容。
X-Request-Sign分析
要分析具体的签名算法,就要先在前端定位代码。
因为X-Request-Sign不常见,基本可以当做是自定义的。
我们在JS代码中全局搜索X-Request-Sign
,即可定位到位置在哪。
在给X-Request-Sign
请求头赋值处,打上断点,向上追栈。
最后定位到的代码格式为
对请求体中参数分割为字符串数组,比如
["username=admin","password=admin123","timexx=202505241145141"] |
然后对数组进行排序,采用的是array.sort。
排序完成后,通过&
将字符串数组拼接为一个字符串。
python代码演示:
lis1 = ["username=admin", "password=admin123", "timexx=202505241145141"] |
接着进行国密SM3运算,SM3是国密中用来取hash值的算法,类似与MD5。
最后的结果为
803f6958f180714b77c940e306f7c395bd4b799b3d6652ba797809f2c515abf3202505241145141 |
Yakit + flask 模拟签名过程
这块直接使用Python调用库来实现国密算法,Python的国密库gmssl
在这里使用Python主要是Python方便排序对请求头,要是没有排序这个过程的话,可以直接调用yakit
的codec
就行
pip install gmssl |
流程如下:
Yakit webfuzzer ——》热加载 beforeRequest ——》Python flask ——》 热加载 beforeRequest ——》后端
在beforeRequest中,我们要先从req中获取请求体,然后构造http请求,发送给flask。
flask接收到请求体,对其进行分割、排序、拼接,然后进行SM3算法,将结果返回给beforeRequest。
Yakit beforeRequest
beforeRequest = func(https, originReq, req) { |
flask
@app.route('/sign', methods=['POST']) |
sm3算法
from gmssl import sm3 |
结果
开启共用热加载代码
后续修改请求包发送到后端,Yakit就会自动替换sign,使修改对我们透明。正常进行渗透测试即可。
案例4: yakit解决AES+RSA加密
数据包&JS分析
现在大多数金融类的站点/小程序在进行数据传输的时候,都会使用数字信封(AES+RSA)
的方式来做参数加密,这种情况下,对于测试人员很不方便。下面介绍如何利用yakit中的热加载实现明文数据包抓取。
首先,抓取数据包,可以看到里面有key、iv、data字段,但对应的字段也都是密文的。
从前端代码中分析,确认为AES+RSA
加密,其中key和iv是浏览器随机生成的16位byte:
找到了加密方法:
JS中代码如下:
CryptoJS['AES']['encrypt'](_0x57740d, _0x4515a4, { |
AES
的key和密钥,十六进制
和base64
的结果
key ae6696f1599be559c7ed292f147f8c70 |
RSA setPublicKey
-----BEGIN PUBLIC KEY----- |
加密后的key
VcwVaxDyjNvk60fim6Oe/+nyXD7ND0oZ78xRHtyWCEeMRHxvQSHOXJXoHv0bXwc+K0ob2nfr8SLC3iVIkVHauoU/T9UmJCs2v1r7UJkzfh9Q7vZwrZimivE/xIMlxs0x8iUOEbpvkj6YQyHDoikUZL18Rsa0+FLp/tkgbE1qEB8= |
加密后的iv:
AxSRLrB/A5APbG38wo8TYHvcwcOy3GeR6dHwtV3GCvMS2uqPBW2vVAII13cqZ5kmQJajX8rZY/0+uAEPLUFsITOwtWgPrQi8ntT02uJ5VS8T1Mm0pwlCnm8LerCKITf/I2+cfP13zFLWSrAXaueqk0XZ2Hg4/WmuPAPbK02vI2M= |
可得出,参数的加密方式
encryptedData = AES(data) |
在做测试的时候,encryptedKey
和encryptedIv
可以保持不变,encryptedData
按照之前AES
加解密的文章做即可。
替换本地JS固定密钥
那么,如果遇到的是在渗透测试查看流量的时候遇到呢?
那时候,每个包的key和iv都是随机的,我们要怎么处理?
经过之前的分析,AES的key和iv都是前端随机生成的,也就是后端对这个生成的结果是什么并不清楚。只要满足一定的格式,后端机会接受。
因此,我们完全可以在前端将key固定。
如何让浏览器加载我们修改后的key呢?
方法1:Override
通过浏览器的 override content进行本地JS替换。
方法比较简单,针对于web类应用很方便,直接修改JS代码,保存,重新加载即可实现,具体方法直接网上查一下就行。
方法2:charles Map Local
通过Charles自带的替换JS功能
右键 –> Map Local
可参考:https://blog.csdn.net/BoBoLUI/article/details/122681085
方法3:yakit热加载替换
我们通过 yakit mitm模块的热加载替换。原理其实和Charles的类似,只不过匹配的URL地址由我们匹配。
1、首先将app.js文件下载到本地,然后修改下JS代码,将原本的random直接修改为固定密钥(自己要是不会直接交给AI就行):
_0x4515a4 = CryptoJS.enc.Hex.parse('ae6696f1599be559c7ed292f147f8c70') |
2、然后,我们在本地用python起一个http服务
python -m http.server 8000 |
3、热加载中,修改响应包,所以这块应该使用afterRequest
这个”魔术方法”
afterRequest = func(ishttps, oreq/*原始请求*/ ,req/*hiajck修改之后的请求*/ ,orsp/*原始响应*/ ,rsp/*hijack修改后的响应*/){ |
4、我们清除浏览器的缓存,重新访问网站,查看app.js,可以看到此时aes的key和iv被我们固定
此时请求包中的参数,我们就可以通过已知密钥直接进行查看了。
yakit实现明文传输
上面一步中已经固定好了密钥,也就是说RSA这块已经搞定了,下面就生成Data字段的AES加密,怎么利用yakit来实现明文呢?
codec保存加解密模块
因为是通用的加密方式,并未魔改,所以我们可以直接采用codec模块,寻找正确的加解密方式
解密
base64——》AES
我们将这个过程保存模块,命名为decode_base64_AES
加密
AES——》base64
保存模块 encode_AES_base64
在mitm热加载中写代码
未修改前,history中报文如下
我们想在history中显示明文的数据包,方便我们查看请求和响应,方便我们进行渗透。
此时,只需要修改MITM热加载模块的 hijackSaveHTTPFlow
dec_base64_aes = func(data){ |
重启热加载,结果如下图,在history中,成功显示明文包,而并不影响正常的登录功能。
如果需要修改响应包,将其变为明文,可以参考
hijackSaveHTTPFlow = func(flow /* *yakit.HTTPFlow */, modify /* func(modified *yakit.HTTPFlow) */, drop/* func() */) { |
这块使用到了hijackSaveHTTPFlow
,该方法的作用如下:
手动劫持 明文修改+密文发出
MITM热加载流程:
请求 -> hijackHTTPRequest -> 前端劫持 -> beforeRequest -> 服务器响应 -> hijackResponse -> 后端劫持 -> afterRequest -> 客户端看到的响应 -> hijackSaveHTTPFlow
- hijackHTTPRequest 浏览器抵达mitm模块前
- beforeRequest 从mitm模块发出后
- hijackResponse 后端抵达mitm模块前
- afterRequest 前端看到响应前
我们要实现在mitm中无感劫持修改,需要修改的是 hijackHTTPRequest
dec_base64_aes = func(data){ |
在前端点击,发送,如下图,可以看到在mitm中能看到已经变成明文了
但是发送到后端的,我们想要的是加密后的,后端可以读取的,所以需要在 beforeRequest
函数中添加加密
enc_base64_aes = func(data){ |
演示如下:
点击AES + RSA
此时,手动劫持中显示为明文
将password,从admin修改为123456,提交数据
将password,从admin修改为123456,提交数据
在history中查看原始请求,可以看到,发送到后端的是加密后的
这样就实现了手动劫持,明文修改,密文发出。
在yak runner中修改脚本
在热加载模块中,由于没有输出,所以我们很难排查问题出在了哪里,这时就可以再yak runner
中修改参数打印调试:
对req进行处理,给req参数赋值。出错有显示,可以及时调整。
参考
https://yaklang.com/articles/vulnerability_testing_after_being_encrypted_by_the_front-end/