简介

什么是yakit热加载???

广义上来说,热加载是一种允许在不停止或重启应用程序的情况下,动态加载或更新特定组件或模块的功能。这种技术常用于开发过程中,提高开发效率和用户体验。在YakitWeb Fuzzer中,热加载是一种高级技术,让 Yak 成为 Web Fuzzer 和用户自定义代码中的桥梁,它允许我们编写一段 Yak 函数,在Web Fuzzer过程中使用,从而实现自定义fuzztag或更多功能。

个人理解,热加载本质也是中间人劫持,类似于在渗透过程中使用burp或者yakit抓包->修改->放包的过程。只不过常规来说的中间人劫持是在客户端和服务器之间的。热加载就是在客户端与yakityakit与服务器之间的劫持。在原本的某个环节上面增加一段代码,从而实现渗透测试简便化。

热加载里面会涉及到Yaklang的编程,这块可以参考对应的官方文档地址:

https://www.yaklang.com/docs/yak-basic/intro

热加载使用方法介绍

此处以对请求包中的密码实现base64来进行介绍。我们直接来看案例,通过案例来理解热加载的意义。

1、对数据包里面的密码进行base64编码,我们先看没有热加载的情况:
image-20250528154035943

选择对应的标签

image-20250528154057001

image-20250528154110994

2、接下来使用热加载来实现加密:
点击热加载按钮:

image-20250528154148640

image-20250528154210756

接下来先解释一下这两个东西:插入热加载和热加载函数

调用热加载 fuzztag来插入热加载

实际上,我们调用热加载中编写的函数也使用 fuzztag 。我们也可以理解为热加载是一种特殊的 fuzztag ,它的格式为:

{{yak(funcname)}}

当我们需要传入参数时,则格式为

{{yak(funcname|param)}}

需要注意的是,我们传入的参数可以是 fuzztag ,也就是可以编写形如:

{{yak(funcname|{{x(pass_top25)}})}}

热加载函数定义来提供功能

我们可以在热加载页面中编写热加载函数,其函数定义格式如下:

// 函数名为funcname,参数只有一个,为param,类型是字符串
funcname = func(param) {
// 返回值可以是字符串或数组
return param
}

我们在热加载页面里面可以随意的去写相关的函数。比如:

image-20250528165404659

具体函数里面的一些即可和关键词还需要了解yaklang的知识。

写完函数怎么使用呢?相当简单,直接在模版内容里面进行插入编辑器位置即可。

image-20250528170009683

image-20250528170137701

接下来再加入字典作为参数:

image-20250528170222762

然后点击发送请求,看看效果:

image-20250528170253171

热加载中的”魔术方法”

在热加载代码区中,实际上存在几个特殊的魔术方法:beforeRequestafterRequest,它函数的定义如下:

// beforeRequest 允许在每次发送数据包前对请求做最后的处理,定义为 func(https bool, originReq []byte, req []byte) []byte
// https 请求是否为https请求
// originReq 原始请求
// req 请求
beforeRequest = func(https, originReq, req) {
// 我们可以将请求进行一定的修改
/*
一个替换请求参数a的例子
poc.ReplaceHTTPPacketQueryParam(req, "a", "bbb")
*/
// 将修改后的请求返回
return []byte(req)
}

// afterRequest 允许在返回响应前对响应做最后的处理,定义为 func(https bool, originReq []byte, req []byte, originRsp []byte, rsp []byte) []byte
// https 请求是否为https请求
// originReq 原始请求
// req 请求
// originRsp 原始响应
// rsp 响应
afterRequest = func(https, originReq, req, originRsp, rsp) {
// 我们可以将响应进行一定的修改,例如解密响应
/*
一个替换响应的例子
body = poc.GetHTTPPacketBody(rsp)
data = json.loads(body)~
if "result" in data {
data["result"] = string(codec.DecodeBase64(data["result"])~)
}
*/

return []byte(rsp)
}

image-20250528170627694

这两个魔术方法分别在每次请求之前和每次请求拿到响应之后调用,它们可以用于修改我们 Web Fuzzer 的请求与响应。通过这两个魔术方法配合 Yak代码,我们实际上可以实现许多有用的功能。

比如我们这里可以简单演示一个测试功能,把请求体里面的macOS替换成windows:

image-20250528171131867

image-20250630092559184

点击保存,发包测试:

image-20250528171212285

案例没有什么实际意义,但是可以简单说明一下beforeRequest这个功能。

使用热加载实现更灵活的 fuzztag

热加载函数可以返回一个数组,当我们返回一个数组时, Web Fuzzer 会将数组中的每一个元素都作为值去发包,所以当我们数组有5个元素时, Web Fuzzer 会发出5个包,这样我们就可以实现更灵活的fuzztag了。

以下是一个简单的例子,我们返回一个数组,其原始值为list = ["1", "2", "3"], 然后将数组中的每个元素进行base64编码后再url编码:

handle = func(param) {
list = ["1", "2", "3"]
for i, v = range list {
list[i] = codec.EncodeBase64Url(v)
}
return list
}

然后我们点击保存按钮,保存热加载内容,这时候我们就可以在 Web Fuzzer 中使用这个热加载 fuzztag 了,如图所示,可以看到数字1,2,3都被base64编码后再url编码了:

image-20250528172121015

案例1: Csrf token保护下的爆破

此处引用yakit官方案例:https://yaklang.io/products/Web%20Fuzzer/fuzz-hotpatch-example1

我们将会介绍一个热加载的实际应用案例:csrf token保护下的爆破。我们以pikachu靶场为例,安装pikachu靶场后(记得要初始化),我们打开如下页面:

img

任意输入账号密码之后,使用 yakit 的 MITM 模块拦截登录请求,我们可以看到请求如下:

img

注意到除了用户和密码之外,还存在一个token的post参数,这是一个csrf token,它主要是用来防止CSRF攻击的,但是这也给我们的爆破增加了一定的难度,因为每次爆破都需要使用一个新的token。那么在这种情况下我们应该如何爆破呢?

首先我们应该来看看token是从哪里获取的,我们继续使用 yakit 的 MITM 模块拦截请求,这次我们拦截的是直接访问该页面的GET请求,当我们将其发送到 Web FUzzer 发送请求后,可以看到其响应中存在token:

img

接下来就是让热加载出手了。我们很容易想到热加载中的”魔术”方法:beforeRequest。我们可以在请求发送前,再次发送一个GET请求来获取token值,再用这个值进行爆破,就可以绕过csrf token的保护了。

我们来看看如何实现,首先我们需要在热加载页面中编写热加载内容,代码如下:

beforeRequest = func(req) {
// 发送GET请求,获取响应
//rsp:存储服务器返回的响应,包括响应头和响应体,方便后续从响应中提取信息。
//_:这是一个占位符,表示不使用该返回值。在 Yaklang 中,_ 通常用来忽略不需要的返回值。
//err:存储函数调用过程中发生的错误。如果函数调用成功,err 为 nil;如果函数调用失败,err 会包含具体的错误信息。用于错误处理,如果 err 不为 nil,函数会直接返回原始请求 req。
rsp, _, err = poc.HTTP(`GET /vul/burteforce/bf_token.php HTTP/1.1
Host: 7f893460-c83a-4bce-83c7-d325ff9b65ba.node4.buuoj.cn:81
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7
Cache-Control: max-age=0
Referer: http://7f893460-c83a-4bce-83c7-d325ff9b65ba.node4.buuoj.cn:81/vul/csrf/csrfget/csrf_get_login.php
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
X-Forwarded-For: 127.0.0.1
x-forwarded-for: 127.0.0.1
`)
//报错处理,如果发生错误时候,直接返回原始请求req,不做任何修改
if err != nil {
return req
}
// 获取GET响应的Set-Cookie
cookie = poc.GetHTTPPacketHeader(rsp, "Set-Cookie")
node, err = xpath.LoadHTMLDocument(rsp)
if err != nil {
return req
}
// 通过xpath语法获取token的值
tokenNode = xpath.FindOne(node, "//input[@name='token']")
if tokenNode == nil {
return req
}
token = xpath.SelectAttr(tokenNode, "value")
// 替换token
req = req.ReplaceAll("__TOKEN__", token)
// 替换cookie
req = poc.AppendHTTPPacketHeader(req, "Cookie", cookie)
return req
}

这里我们一共做了以下几件事情:

  1. 通过GET请求,拿到响应
  2. 通过响应拿到Set-Cookie的值
  3. 通过xpath语法获取token的值
  4. 替换__TOKEN__为实际的token
  5. 添加POST请求的Cookie为第二步中拿到的Set-Cookie,这样模拟了多个用户同时请求的情况

我们将token参数改为__TOKEN__,然后点击发生请求按钮,如图所示,显示username or password is not exists~而不是csrf token error,这证明我们已经成功获取到token了:

img

接下来就很简单了,使用我们前面学到的 fuzztag 知识,使用 fuzztag 爆破密码,成功爆破出了admin的密码为123456:

img

需要注意的是,我们使用了并发的技术来请求该网站,这也是我们为什么需要先从GET请求的响应中拿到Set-Cookie然后再设置POST请求的Cookie的原因,因为我们需要模拟多个用户同时请求的情况,如果是单个用户请求的话,我们需要将并发改为1。

案例2: 前端验签-SHA256

JS代码分析

img

通过查看源代码可以看到key为

1234123412341234

img

可以看到是通过SHA256来进行签名的,他把请求体的username和password字段提取,然后进行加密。

username=admin&password=admin123

img

使用CyberChef加密,最终得到加密值为:fc4b936199576dd7671db23b71100b739026ca9dcb3ae78660c4ba3445d0654d

img

可以看到自己计算和前端计算的一致:

img

修改密码,重新构造签名:

username=admin&password=666666
=>
26976ad249c29595c3e9e368d9c3bc772b5a27291515caddd023d69421b7ffee

img

发送请求,可以看到验证签名成功,密码正确登录成功,自此签名绕过成功:

POST /crypto/sign/hmac/sha256/verify HTTP/1.1
Host: 127.0.0.1:8787
Content-Type: application/json

{
"signature": "26976ad249c29595c3e9e368d9c3bc772b5a27291515caddd023d69421b7ffee",
"key": "31323334313233343132333431323334",
"username": "admin",
"password": "666666"
}

img

热加载

通过beforeRequest劫持请求包,使用encryptData函数进行加密,最终实现热加载自动签名功能。

encryptData = (packet) => {
body = poc.GetHTTPPacketBody(packet)
params = json.loads(body)
//获取账号和密码
name = params.username
pass = params.password
key = "31323334313233343132333431323334" //十六进制密钥

//HmacSha256加密
signText = f`username=${name}&password=${pass}`
sign = codec.EncodeToHex(codec.HmacSha256(f`${codec.DecodeHex(key)~}`, signText))

//构造请求体
result = f`{"username":"${name}","password":"${pass}","signature":"${sign}","key":"${key}"}`

return string(poc.ReplaceBody(packet, result, false))
}

//发送到服务端修改数据包
// beforeRequest = func(req){
// return encryptData(req)
// }

//调试用
packet = <<<TEXT
POST /crypto/sign/hmac/sha256/verify HTTP/1.1
Host: 127.0.0.1:8787
Content-Type: application/json
Content-Length: 179

{"username":"admin","password":"admin123"}
TEXT
result = (encryptData(packet))
print(result)

调试结果如下:
img

beforeRequest取消注释,添加到Web Fuzzer模块的热加载中:

img

保存后发送请求,热加载成功实现自动签名功能。

img

img

案例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"]
lis1.sort()
print("&".join(lis1))

接着进行国密SM3运算,SM3是国密中用来取hash值的算法,类似与MD5。

image-20250604174403707

最后的结果为

803f6958f180714b77c940e306f7c395bd4b799b3d6652ba797809f2c515abf3202505241145141

Yakit + flask 模拟签名过程

这块直接使用Python调用库来实现国密算法,Python的国密库gmssl

在这里使用Python主要是Python方便排序对请求头,要是没有排序这个过程的话,可以直接调用yakitcodec就行

pip install gmssl

流程如下:
Yakit webfuzzer ——》热加载 beforeRequest ——》Python flask ——》 热加载 beforeRequest ——》后端

在beforeRequest中,我们要先从req中获取请求体,然后构造http请求,发送给flask。
flask接收到请求体,对其进行分割、排序、拼接,然后进行SM3算法,将结果返回给beforeRequest。

Yakit beforeRequest

beforeRequest = func(https, originReq, req) {
    // 获取请求体
    body_post = poc.GetHTTPPacketBody(req)
    // 发送到flask的数据包
    flask_sign = `POST /sign HTTP/1.1
Host: 127.0.0.1:5000
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded

a=1`
    // 发送http请求到flask,将请求中的请求体替换为req中的请求体
    rsq_body, _, _ = poc.HTTP(
        flask_sign, 
        poc.https(false), 
        poc.replaceBody(body_post, false), 
    )
    // 得到签名
    sign = poc.GetHTTPPacketBody(rsq_body)
    // 替换到请求头中
    req = poc.ReplaceHTTPPacketHeader(req, "X-Request-SIGN", sign)
    /*
    一个替换请求参数a的例子
    poc.ReplaceHTTPPacketQueryParam(req, "a", "bbb")
    */
    // 将修改后的请求返回
    return []byte(req)
}

flask

@app.route('/sign', methods=['POST'])
def sign():
    body = request.get_data().decode("utf-8")
    body_list = body.lower().split("&")
    body_list.sort()
    body_sort = "&".join(body_list)
    return sm3_hash(body_sort) + datetime.now().strftime("%Y%m%d%H%M%S%f")[:15]

sm3算法

from gmssl import sm3
import binascii

def sm3_hash(message: str) -> str:
    """
    使用 SM3 算法计算输入消息的哈希值

    参数:
        message: 待计算哈希的字符串

    返回:
        计算结果的十六进制字符串
    """
    # 将输入字符串转换为字节
    message_bytes = message.encode('utf-8')

    # 计算 SM3 哈希
    hash_bytes = sm3.sm3_hash(list(message_bytes))

    # 将字节结果转换为十六进制字符串
    return binascii.hexlify(bytes.fromhex(hash_bytes)).decode('ascii')

结果

开启共用热加载代码

image-20250604175117638

后续修改请求包发送到后端,Yakit就会自动替换sign,使修改对我们透明。正常进行渗透测试即可。

案例4: yakit解决AES+RSA加密

数据包&JS分析

现在大多数金融类的站点/小程序在进行数据传输的时候,都会使用数字信封(AES+RSA)的方式来做参数加密,这种情况下,对于测试人员很不方便。下面介绍如何利用yakit中的热加载实现明文数据包抓取。

首先,抓取数据包,可以看到里面有key、iv、data字段,但对应的字段也都是密文的。

image-20250529095749519

从前端代码中分析,确认为AES+RSA加密,其中key和iv是浏览器随机生成的16位byte:

image-20250529095904237

找到了加密方法:

image-20250529095930462

JS中代码如下:

CryptoJS['AES']['encrypt'](_0x57740d, _0x4515a4, {
        'iv': _0x5e9345,
        'mode': CryptoJS['mode']['CBC'],
        'padding': CryptoJS['pad']['Pkcs7']
    })['toString']()

AES的key和密钥,十六进制base64的结果

key ae6696f1599be559c7ed292f147f8c70
    base64 rmaW8Vmb5VnH7SkvFH+McA==
    
iv c5433e11dfe2bfde74d997011c19f830
    base64 xUM+Ed/iv9502ZcBHBn4MA==

RSA setPublicKey

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRvA7giwinEkaTYllDYCkzujvi
NH+up0XAKXQot8RixKGpB7nr8AdidEvuo+wVCxZwDK3hlcRGrrqt0Gxqwc11btlM
DSj92Mr3xSaJcshZU8kfj325L8DRh9jpruphHBfh955ihvbednGAvOHOrz3Qy3Cb
ocDbsNeCwNpRxwjIdQIDAQAB
-----END PUBLIC KEY-----

加密后的key

VcwVaxDyjNvk60fim6Oe/+nyXD7ND0oZ78xRHtyWCEeMRHxvQSHOXJXoHv0bXwc+K0ob2nfr8SLC3iVIkVHauoU/T9UmJCs2v1r7UJkzfh9Q7vZwrZimivE/xIMlxs0x8iUOEbpvkj6YQyHDoikUZL18Rsa0+FLp/tkgbE1qEB8=

加密后的iv:

AxSRLrB/A5APbG38wo8TYHvcwcOy3GeR6dHwtV3GCvMS2uqPBW2vVAII13cqZ5kmQJajX8rZY/0+uAEPLUFsITOwtWgPrQi8ntT02uJ5VS8T1Mm0pwlCnm8LerCKITf/I2+cfP13zFLWSrAXaueqk0XZ2Hg4/WmuPAPbK02vI2M=

可得出,参数的加密方式

encryptedData = AES(data)
encryptedKey = RSA(key)
encryptedIv = RSA(iv)

在做测试的时候,encryptedKeyencryptedIv可以保持不变,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

image-20250529101102283

方法3:yakit热加载替换

我们通过 yakit mitm模块的热加载替换。原理其实和Charles的类似,只不过匹配的URL地址由我们匹配。

1、首先将app.js文件下载到本地,然后修改下JS代码,将原本的random直接修改为固定密钥(自己要是不会直接交给AI就行):

_0x4515a4 = CryptoJS.enc.Hex.parse('ae6696f1599be559c7ed292f147f8c70')
, _0x5e9345 = CryptoJS.enc.Hex.parse('c5433e11dfe2bfde74d997011c19f830')

image-20250529101310099

2、然后,我们在本地用python起一个http服务

python -m http.server 8000

3、热加载中,修改响应包,所以这块应该使用afterRequest这个”魔术方法”

afterRequest = func(ishttps, oreq/*原始请求*/ ,req/*hiajck修改之后的请求*/ ,orsp/*原始响应*/ ,rsp/*hijack修改后的响应*/){
    // Example:
    // if str.Contains(string(rsp), "凝聚磅礴的中国文学力量") {
    //     modified = poc.FixHTTPRequest(str.ReplaceAll(rsp, "凝聚磅礴的中国文学力量", "AAAAAAAAAAAAAAAA"))
    //     return []byte(modified)
    // }
    url = poc.GetUrlFromHTTPRequest("http",oreq /*type: string*/)
    // 匹配包含 app.js的url
    if url.Contains("app.js") {
    // 获取本地修改后的app.js
        rsp1, temp1 = poc.Get("http://127.0.0.1:8000/app.js")~
        rsp1 = poc.GetHTTPPacketBody(rsp1["RawPacket"])
        // 替换响应体
        rsp = poc.ReplaceHTTPPacketBody(rsp /*type: []byte*/, rsp1 )
    }
    return rsp;
}

图片

4、我们清除浏览器的缓存,重新访问网站,查看app.js,可以看到此时aes的key和iv被我们固定

图片

此时请求包中的参数,我们就可以通过已知密钥直接进行查看了。
图片

yakit实现明文传输

上面一步中已经固定好了密钥,也就是说RSA这块已经搞定了,下面就生成Data字段的AES加密,怎么利用yakit来实现明文呢?

codec保存加解密模块

因为是通用的加密方式,并未魔改,所以我们可以直接采用codec模块,寻找正确的加解密方式

解密
base64——》AES

image-20250529101915759

我们将这个过程保存模块,命名为decode_base64_AES

加密
AES——》base64

image-20250529102030249

保存模块 encode_AES_base64

在mitm热加载中写代码

未修改前,history中报文如下

image-20250529102109112

我们想在history中显示明文的数据包,方便我们查看请求和响应,方便我们进行渗透。

此时,只需要修改MITM热加载模块的 hijackSaveHTTPFlow

dec_base64_aes = func(data){
    aa = "{{codecflow(decode_base64_AES|" + data + ")}}"
    return fuzz.Strings(aa)[0]
}

decrypt = func(req){
    req_method = poc.GetHTTPRequestMethod(req /*type: []byte*/)
    if req_method == "POST" && str.Contains(string(req), "encryptedData" /*type: string*/){
        postParams = poc.ExtractPostParams(req /*type: []byte*/)~
        enc_data = postParams["encryptedData"]
        dec_data = dec_base64_aes(enc_data)
        postParams["encryptedData"] = dec_data
        req = poc.ReplaceHTTPPacketJsonBody(req /*type: []byte*/, postParams /*type: map[string]any*/)

    }
    return req

}

hijackSaveHTTPFlow = func(flow /* *yakit.HTTPFlow */, modify /* func(modified *yakit.HTTPFlow) */, drop/* func() */) {
    req = codec.StrconvUnquote(flow.Request)~
    flow.Request = codec.StrconvQuote(decrypt(req))
    modify(flow)
}

重启热加载,结果如下图,在history中,成功显示明文包,而并不影响正常的登录功能。

图片

如果需要修改响应包,将其变为明文,可以参考

hijackSaveHTTPFlow = func(flow /* *yakit.HTTPFlow */, modify /* func(modified *yakit.HTTPFlow) */, drop/* func() */) {
    req = codec.StrconvUnquote(flow.Request)~
    flow.Request = codec.StrconvQuote(decrypt(req))
    rsp = codec.StrconvUnquote(flow.Response)~
    flow.Response = codec.StrconvQuote(decrypt(rsp))
    modify(flow)
}

这块使用到了hijackSaveHTTPFlow,该方法的作用如下:

image-20250529103900344

手动劫持 明文修改+密文发出

MITM热加载流程:
请求 -> hijackHTTPRequest -> 前端劫持 -> beforeRequest -> 服务器响应 -> hijackResponse -> 后端劫持 -> afterRequest -> 客户端看到的响应 -> hijackSaveHTTPFlow

  • hijackHTTPRequest 浏览器抵达mitm模块前
  • beforeRequest 从mitm模块发出后
  • hijackResponse 后端抵达mitm模块前
  • afterRequest 前端看到响应前

我们要实现在mitm中无感劫持修改,需要修改的是 hijackHTTPRequest

dec_base64_aes = func(data){
    aa = "{{codecflow(decode_base64_AES|" + data + ")}}"
    return fuzz.Strings(aa)[0]
}

// hijackHTTPRequest 会在过滤后的请求到达Yakit MITM前被调用,可以通过该函数提前将请求修改或丢弃
// isHttps 请求是否为https请求
// url 网站URL
// req 请求
// forward(req) 提交修改后的请求,如果未被调用,则使用原始的请求
// drop() 丢弃请求
hijackHTTPRequest = func(isHttps, url, req, forward /*func(modifiedRequest []byte)*/, drop /*func()*/) {
    req_method = poc.GetHTTPRequestMethod(req /*type: []byte*/)
    if req_method == "POST" && str.Contains(string(req), "encryptedData" /*type: string*/){
        postParams = poc.ExtractPostParams(req /*type: []byte*/)~
        enc_data = postParams["encryptedData"]
        dec_data = dec_base64_aes(enc_data)
        postParams["encryptedData"] = dec_data
        req = poc.ReplaceHTTPPacketJsonBody(req /*type: []byte*/, postParams /*type: map[string]any*/)

    }
    forward(req)

}

在前端点击,发送,如下图,可以看到在mitm中能看到已经变成明文了

image-20250529104216372

但是发送到后端的,我们想要的是加密后的,后端可以读取的,所以需要在 beforeRequest 函数中添加加密

enc_base64_aes = func(data){
    aa = "{{codecflow(encode_AES_base64|" + data + ")}}"
    return fuzz.Strings(aa)[0]
}

// beforeRequest 会在请求到达服务器之前被调用,可以通过该函数对请求做最后一次修改
// isHttps 请求是否为https请求
// oreq 原始请求
// req hijackRequest修改后的请求
// 返回值: 修改后的请求,如果没有返回值则使用hijackRequest修改后的请求
beforeRequest = func(ishttps /*bool*/, oreq /*[]byte*/, req/*[]byte*/){
    // Example:
    // if str.Contains(string(req), "凝聚磅礴的中国文学力量") {
    //     modified = poc.FixHTTPRequest(str.ReplaceAll(req, "凝聚磅礴的中国文学力量", "AAAAAAAAAAAAAAAA"))
    //     return []byte(modified)
    // }
    req_method = poc.GetHTTPRequestMethod(req /*type: []byte*/)
    if req_method == "POST" && str.Contains(string(req), "encryptedData" /*type: string*/){
        postParams = poc.ExtractPostParams(req /*type: []byte*/)~
        dec_data = postParams["encryptedData"]
        enc_data = enc_base64_aes(dec_data)
        postParams["encryptedData"] = enc_data
        req = poc.ReplaceHTTPPacketJsonBody(req /*type: []byte*/, postParams /*type: map[string]any*/)

    }
    return req
}

演示如下:

image-20250529104302696

点击AES + RSA

图片

此时,手动劫持中显示为明文

图片

将password,从admin修改为123456,提交数据

图片

将password,从admin修改为123456,提交数据

图片

在history中查看原始请求,可以看到,发送到后端的是加密后的

图片

这样就实现了手动劫持,明文修改,密文发出。

在yak runner中修改脚本

在热加载模块中,由于没有输出,所以我们很难排查问题出在了哪里,这时就可以再yak runner中修改参数打印调试:

image-20250529105112695

图片

对req进行处理,给req参数赋值。出错有显示,可以及时调整。

参考

https://yaklang.com/articles/vulnerability_testing_after_being_encrypted_by_the_front-end/

https://mp.weixin.qq.com/s/F31eANkyOyraTwLsC_FoMQ

https://www.cnblogs.com/CVE-Lemon/p/18622394