0X00 前言
在数据安全越来越重视的情况下,请求包、响应包中有明文字段的应用越来越少了,不管是app还是web,金融、通信,民生以及其他比较重要的行业,都用到了加解密。so,之前没有接触到这方面的Yu9打算学习一下。然后注意到了yakit中的高级前端加解密与验签实战
靶场,想着通过Yakit靶场一块来看看目前比较流行的前端加解密与验签以及其存在的安全问题!
本来已经写完了整个靶场的通关教程,还发现了其中一个靶场存在的bug。
其中编写脚本使用的都是python
,怪我没有看官方之前发的打靶教程,写完之后才看到了官方的教程,一对比,发现使用yakit和它的热加载来解决这个问题更加方便(也可能是咱python学的太差)!所以之前计划发的文章就不发公众号了,通过篇文章一块来学习一下yakit的热加载功能。
yakit语法官方教程:https://yaklang.io/docs/intro/
yakit热加载教程:https://yaklang.io/products/Web%20Fuzzer/fuzz-hotpatch
Yu9之前写的靶场通关笔记:https://wsyu9a.github.io/FrontEndSec/
0X01 破解验签防篡改
之前Yu9写过的一篇文章,数据完整性与数据身份校验https://mp.weixin.qq.com/s/FgIRltZd5ehs-3x8SccUxQ,其中就涉及了一些数据防篡改的方案,大家可以通过这篇文章对签名技术有一个初步的了解!
签名验证(又叫验签或签名)是验证请求参数是否被篡改的一种常见安全手段,验证签名方法主流的有两种:
1)一种是 KEY+哈希算法,例如 HMAC-MD5 / HMAC-SHA256 等,比如要发送的数据是:
username="admin"&password="123456"
。在提交数据前把数据通过KEY+哈希算法
生成一个哈希值,发送数据是携带这个哈希值。例如:
正常数据:
{ "username": "admin", "password": "123456" }
签名数据:
{ "signature": "7d113a1544cd53ff6c527c865511be4f18d4372a7fa571dbc035f0fc12b2b092", "key": "31323334313233343132333431323334", "username": "admin", "password": "123456" }
2)另一种是使用非对称加密加密比如RSA把生成的哈希值(signature)在进行一层加密。
注:这段内容借鉴yakit前端验证签名(验签)表单:HMAC-SHA256靶场!
通过靶场(前端验证签名(验签)表单:HMAC-SHA256
)来具体看看:
在提交数据后,会先验证签名。签名验证通过后在验证用户名、密码
那签名真的可以防止数据被篡改吗?我们抓包修改个数据试试(把密码改成123123)
可以看到只要数据被篡改,签名就会验证失败。爆破的话讲究不用说了,只有我们最开始的那个数据包可以验证通过。
那我们如何才可以绕过签名篡改数据呢?
1)首先我们应该明确一下签名签证过程的原理和步骤
原理
前端:把数据按照一定格式
使用哈希算法进行秘钥哈希
运算生成哈希值
,发送数据包携带这个哈希值和key
后端:此时后端拿到数据有(以上边靶场为例):signature、key、username、password。把username和password按照一定格式在进行秘钥哈希运算生成的值与signature对比,若一样验证通过!
步骤
绕过签名的方式就是,伪造signature值:改变数据的同时生成对应的signature值,修改数据的时候,连带签名一起修改掉就好了。
那我们伪造signature的前提就是需要知道前端生成它的规则。
2)一半这个步骤就是通过javascript实现,那我们看看其中到底做了哪些处理!
首先就是找一下发包是调用了那个js来做的处理。
找到之后,我们就具体分析一下具体做了哪些处理
CryptoJS是一个流行的JavaScript密码学库,被广泛用于前端开发中处理加密、解密、哈希和编码等密码学操作
CryptoJS 的 key 在没有明确指定编码方式的情况下,默认的
toString
方法将输出十六进制 (Hex) 格式的字符串。
01生成一个 KEY,默认为 16 位数 1234123412341234,签名时使用的是UTF-8 编码字符串
02签名前的格式,例如:username=admin&password=123456
03签名使用的哈希算法是HmacSha256
,输出结果十六进制
3)明白这个逻辑后,我们可以使用 Yaklang 中codec模块的HmacSha256
函数来模拟这个过程!
热加载是一种高级技术,让 Yak 成为 Web Fuzzer 和用户自定义代码中的桥梁,它允许我们编写一段 Yak 函数,在 Web Fuzzer 过程中使用,从而实现自定义 fuzztag 或更多功能。
sign = func(p) {
key = `1234123412341234`
usernameDict = ["admin"]
passwordDict = x"{{payload(pass_top25)}}" // 我们可以使用x前缀字符串来通过fuzztag语法获取pass_top25字典中的值
// passwordDict = ["admin", "123456", "admin123", "88888888", "666666"] // 也可以直接使用手写的list
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
}
0X02 渗透前端JS加密表单
当我们学会测试带验签的接口的基本技能之后,我们会自我反省这个保护措施其实只是增加了操作的复杂度和难度,并不是真正的能解决“防篡改防重放的问题”。
当然,我们的密码仍然在通信过程中**“明文传输”**;
“明文密码传输”的不合规项一直是一个备受争议的选项,甚至前些年大家觉得这就是用来“凑数”的安服报告内容。
但是戏剧性的是,随着一些甲方单位真的全 API 通信上了加密之后,普通测试手段失效了,大家不再轻视这个问题,开始广泛讨论“如何绕过前端加密进行安全测试”这类话题。
注:这段话来源yakit公众号**渗透测试高级技巧:分析验签与前端加密(一)**https://mp.weixin.qq.com/s/ni3sVp0Gh-CwyMPuwk__Cw
在前端加密中AES-CBC/ECB的组合可能是我们最常遇到的两种,在正式开始之前我们先对AES加密算法进行扫盲,然后在通过CryptoJS.AES(CBC) 前端加密登陆表单
这个靶场来了解前端加密的渗透。
AES加密概述
秘钥
AES是一种对称加密
算法,用于加密和解密数据。它是目前应用最广泛的加密算法之一。
AES加密算法采用分组密码
(block cipher)的方式,将明文分成固定长度的数据块,然后对每个数据块进行相同的加密操作,以达到加密整个数据的目的。加密和解密都使用相同的密钥,因此属于对称加密算法。
AES加密算法的密钥长度可以是128位、192位或256位。其中,128位密钥被广泛使用,256位密钥的安全性最高。AES算法还有多种模式,如ECB、CBC、CFB、OFB等,其中CBC模式被广泛使用。
填充
上边我们提到了AES加密算法采用分组加密的方式。那什么是分组加密呢?
AES算法在对明文加密的时候,并不是把整个明文一股脑加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit
。这些明文块经过AES加密器的复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密结果。
若一个明文的长度是200bit,按照每个明文快128bit来拆分的话,第二个明文块只有72bit。不足128bit。这时候就需要对不足128bit的明文块进行填充(Padding)。
一个字节是4bit,英文1字节,中文utf-8编码3字节、gbk编码2字节。128比特=16字节
01、NoPadding:
不做任何填充,但是要求明文必须是16字节的整数倍。
02、PKCS7Padding(默认):
若明文块不足16个字节,在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。
例:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则补全为{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}
03、ISO10126Padding:
若明文块不足16个字节,在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。
比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为{1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}
模式
AES算法的模式是指在进行分组密码加密时,对数据块的处理方式。常见的AES加密算法的模式包括:ECB、CBC、CFB、OFB等,其中CBC模式被广泛使用。下边我们主要看一下ECB模式与CBC模式
01、ECB
电码本模式,是最简单的块密码加密模式,加密前根据加密块大小(如AES为128位)分成若干块,之后将每块使用相同的密钥单独加密,解密同理。
ECB相同的数据块密钥一样的话输出的密文也一样,容易被攻击。
02、CBC
密码分组链接模式,先将明文切分为块,每一块与上一块的密文块进行异或运算后,再使用密钥进行加密。第一个块需要使用初始化向量(IV),一般来说,每次加密时都会随机产生一个不同的比特序列来作为初始化向量。
递归加密带来的问题是没有办法随机访问各个区块了,访问某个区块必须解出前一个区块数据。
这种方式相对ECB提高了安全性,是常用的工作模式,用于SSL。缺点是加密过程是串行的,无法被并行化
除了ECB无须设置初始化向量IV而不安全之外,其它AES工作模式都必须设置向量IV
CBC模式下的加密登录表单
首先我们需要做的就是先了解一下其加密的逻辑:
01生成一个初始iv,长度为 16 字节的随机字节数组,不过我们使用固定的也OK
02生成一个 KEY,默认为 16 位数 1234123412341234
02加密前的格式为json,例如:{"username":"admin","password"="123456"}
03签名使用的哈希算法是AES/CBC
,输出结果十六进制
热加载模块中编写代码:
dncryptAesCbc = func(p) {
key = codec.DecodeHex("31323334313233343132333431323334")~
iv = codec.DecodeHex("880b2eaf1d19c0ae5816d3acd382775f")~
usernameDict = ["admin"]
passwordDict = x"{{payload(pass_top25)}}" // 我们可以使用x前缀字符串来通过fuzztag语法获取pass_top25字典中的值
// passwordDict = ["admin", "123456", "admin123", "88888888", "666666"] // 也可以直接使用手写的list
resultList = []
for username in usernameDict {
for password in passwordDict {
m = {"username":username,"password":password}
jsonInput = json.dumps(m)
result = codec.AESCBCEncryptWithPKCS7Padding(key, jsonInput, iv)~
base64Result = codec.EncodeBase64(result)
resultList.Append(base64Result)
}
}
return resultList
}
RSA加密AES秘钥
选择AES加密数据的原因是因为AES加密速度快,自然是我们的第一选择,但是缺点也明显。因为使用
同一个密钥
,如果有一方密钥泄露,那么数据也就不安全了。所以我们可以结合RSA互补二者的缺点,用AES来加密数据,使用RSA来加密传递AES密钥。
下边我们通过前端RSA加密AES密钥,服务器传输
靶场具体学习一下,在这个靶场中,我们有几个难题需要解决:
RSA的秘钥是通过服务器传输
响应体也加密无法直接判断是否成功
当然,万能的yakit针对这几个问题都有对应的解决方案,通过匹配器、数据提取器、Web Fuzzer序列
这些功能就可以完美解决!
首先第一步,还是分析具体的一个思路,然后在构造热加载代码:
1)通过/crypto/js/rsa/generator
接口获取RSA公钥私钥进行处理
2)使用AES-GCM加密数据,可以随机16位、初始iv随机12位。
AES-GCM 是一种常见的对称加密算法,它结合了 AES 和 GCM(Galois/Counter Mode)两种算法。其中,AES 用于加密数据,GCM 用于计算认证标签。
3)RSA-OAEP再对AES的key和iv加密
4)最后输出的是base64编码后的
知道他大概得逻辑后,就可以着手编写代码了
aesKey = "aaaaaaaaaaaaaaaa"
iv = "aaaaaaaaaaaa"
aesGCM = data => {
return codec.EncodeBase64(codec.AESGCMEncryptWithNonceSize12(aesKey, data, iv)~)
}
rsaOAEP_key = (pem) => {
return codec.EncodeBase64(codec.RSAEncryptWithOAEP(pem, aesKey)~)
}
rsaOAEP_iv = (pem) => {
return codec.EncodeBase64(codec.RSAEncryptWithOAEP(pem, iv)~)
}
之后还要解决的几个问题。
01、服务器获取公钥
使用数据提取器获取服务器传输的秘钥。
把提取字段名设置为publicKey
,这样我们在后续的Web Fuzzer序列
模块中就可以当做变量使用了!
02、继承获取来的秘钥
使用Web Fuzzer序列
模块,按图中顺序依次进行。
03、解密响应内容
我们可以看到这块响应的内容也是加密过的,so我们就没有办法直接判断我们的请求是否成功!
所以就要解决这个办法,所以我得想法是:
第一个请求中也提取出私钥,设置成变量。
第二个请求中提取去加密的响应结果,设置成变量!
新建第三个请求,热加载编写代码解密数据
1)提取出私钥
2)提取响应的加密数据字段
3)vps起一个web服务接受解密数据并响应便于判断
添加个一个匹配器
4)热加载中编写rsa、aes解密代码
oaepDec = (pem,data) => {
return string(codec.RSADecryptWithOAEP(pem, codec.DecodeBase64(data)~)~)
}
dec = (key,data,iv) => {
dump(key, data, iv)
return string(codec.AESGCMDecryptWithNonceSize12(key, codec.DecodeBase64(data)~, iv)~)
}
//解密后的iv
{{yak(oaepDec|{{p(privateKey)}}|{{p(encryptedIV)}})}}
//解密后的key
{{yak(oaepDec|{{p(privateKey)}}|{{p(encryptedKey)}})}}
//解密后的data
{{yak(dec|{{yak(oaepDec|{{p(privateKey)}}|{{p(encryptedKey)}})}}|{{p(data)}}|{{yak(oaepDec|{{p(privateKey)}}|{{p(encryptedIV)}})}})}}
04、插入字段爆破
0X03 总结
本文中出现的加密算法只是个例,实际应用中可能会有更多种类的算法和接口。但是,本文提供的靶场案例可以帮助大家掌握基本的加密和解密流程。只要能熟练的掌握这些内容,以后碰到别的场景咱只要稍微拓展一下,那也是手拿把掐!
0x04 声明
遵纪守法
请严格遵守网络安全法相关条例!
此分享主要用于交流学习,请勿用于非法用途,一切后果自付。
一切未经授权的网络攻击均为违法行为,互联网非法外之地。
0X05 参考
https://mp.weixin.qq.com/s/ni3sVp0Gh-CwyMPuwk__Cw
https://mp.weixin.qq.com/s/gMbbEV62XR5_QCACQwZnOw
https://zhongce.sina.com.cn/article/view/158689