加解密网关

凡所有相,皆是虚妄.

Posted by taomujian on May 2, 2024

简述

四年前,我当时写的开源漏扫系统linbing前后端数据传输采用了RSA加解密,当时所在公司的领导,是入行多年的技术大牛,当时给我说所有的前后端数据传输进行加密都是画蛇添足,多此一举,应该直接面对问题,比如说为什么登陆时要对数据包进行加密,我当时回答加密是为了防止被抓包进行爆破,领导说那么真正要做的是设置复杂密码这种措施,而不是进行加密,因为这样对于解决问题毫无作用,所有的加密措施都会被破解.

当时由于技术视野较低,并不能完全理解这些话所要表达的深意,经过这些年的技术沉淀,对前领导所说的话有了更深的理解.

在早期的时候,前端传给后端的数据在应用层上是明文的,可以用Burpsuit这种抓包工具抓取明文数据包,然后通过对数据包进行修改、重放、删除等操作发现了许多漏洞.

不管采用什么样的加解密方案,无论是采用单纯的对称加密算法或者是非对称加密算法还是对称加密算法和非对称加密算法混合的方案,都会被破解,加解密的这个壳被打碎后,所谓的加解密其实就是在裸奔.如果之前代码对于安全漏洞有预防措施还好,如果一点预防措施都没有,只靠加解密来预防安全漏洞的出现,那么系统就是千疮百孔,不堪一击.

破解

对于怎么破解前端和后端加密数据传输方案的方式有2种.

第1种方式比较麻烦,就是直接在浏览器中根据关键字符串打断点,然后把数据的加解密算法逆向出来,并把加密之前的数据通过打断点获取到,就可以用python脚本的方式模拟加解密过程,对数据进行加密进行发包,然后把收到的密文返回包解密即可.

第2种方式是在浏览器中根据关键字符串打断点,把数据的加解密算法逆向出来之后,采用中间人攻击的方法,启动2个加解密网关(就是2个服务)对数据自动进行加解密,再结合Burpsuit,就可以实现在Burpsuit看到明文请求包、明文返回包,并可以在Burpsuit中随意修改数据并进行请求,这种所谓的加解密就失去了原先的作用,看着就是透明的一样.

接下来就探讨下怎么实现这个加解密网关,让加解密方案彻底形同虚设.

加解密网关

流程

如下图所示:

流程.jpg

1
2
3
4
5
6
7
1. 首先在客户端(浏览器)上设置代理,把数据发送给上游代理网关
2. 上游代理网关收到客户端发来的密文会根据加解密算法把密文还原为明文,然后把明文发送给Burpsuit
3. 在Burpsuit上设置代理,代理地址为下游代理网关,Burpsuit收到明文数据后,把明文数据发送给下游代理网关
4. 下游代理网关收到明文数据后,会根据加密算法,把明文数据加密成密文数据,然后发给服务端(网站服务)
5. 下游代理网关收到服务端发来的密文数据后,会根据解密算法,把密文数据解密成明文数据,然后发给Burpsuit
6. Burpsuit收到下游代理网关发来的明文数据后,把收到的明文数据发给上游代理网关
7. 上游代理网关收到Burpsuit发来的明文数据后,会根据加密算法,把明文数据加密成密文数据,然后发给客户端

从上面的流程中可以看到,Burpsuit看到的都的请求包和返回包都是明文了.

示例

假如一个网站采用的是AES加解密算,请求包和返回包都是密文,通过浏览器打断点发现采用的是AES的CBC模式,用的key和iv分别为294d317438m440ed、8044ccb1qd92n5f5

用python来实现这个上游代理网关和下游代理网关,主要使用mitmproxy这个框架来实现

代码示例

aes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#!/usr/bin/env python3

import base64
from Crypto.Cipher import AES

class Aes_Crypto:
    def __init__(self, key, iv):
        self.key = key.encode()
        self.iv = iv if iv else (chr(0) * 16).encode() 
       
    def pkcs7padding(self, text):

        """
        明文使用PKCS7填充
        最终调用AES加密方法时,传入的是一个byte数组,要求是16的整数倍,因此需要对明文进行处理

        :param str text: 待加密内容(明文)

        :return:
        """

        bs = AES.block_size  # 16
        length = len(text)
        bytes_length = len(bytes(text, encoding='utf-8'))
        # tips:utf-8编码时,英文占1个byte,而中文占3个byte
        padding_size = length if(bytes_length == length) else bytes_length
        padding = bs - padding_size % bs
        # tips:chr(padding)看与其它语言的约定,有的会使用'\0'
        padding_text = chr(padding) * padding
        return text + padding_text

    def pkcs7unpadding(self, text):

        """
        处理使用PKCS7填充过的数据

        :param str text: 解密后的字符串

        :return:
        """

        length = len(text)
        unpadding = ord(text[length-1])
        return text[0:length-unpadding]

    def encrypt(self, content):

        """
        AES加密
        key,iv使用同一个
        模式cbc
        填充pkcs7

        :param str content: 待加密内容

        :return:
        """
    
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        # 处理明文
        content_padding = self.pkcs7padding(content)
        # 加密
        encrypt_bytes = cipher.encrypt(bytes(content_padding, encoding='utf-8'))
        # 重新编码
        result = str(base64.b64encode(encrypt_bytes), encoding='utf-8')
        return result

    def decrypt(self, content):

        """
        AES解密
        key,iv使用同一个
        模式cbc
        去填充pkcs7
        
        :param str content: 待解密的内容

        :return:
        """

        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        # base64解码
        encrypt_bytes = base64.b64decode(content)
        # 解密
        decrypt_bytes = cipher.decrypt(encrypt_bytes)
        # 重新编码
        result = str(decrypt_bytes, encoding='utf-8')
        # 去除填充内容
        result = self.pkcs7unpadding(result)
        return result

if __name__ == '__main__':
    aes_crypto = Aes_Crypto('294d317438m440ed', '8044ccb1qd92n5f5')
    data = aes_crypto.encrypt('''12''')
    print(data)
    data = aes_crypto.decrypt(data)
    print(data)
upstream.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import json
from aes import Aes_Crypto
from mitmproxy import ctx
from mitmproxy.http import HTTPFlow

class FilterFlow:
    def request(self, flow):
        if flow.request.url.startswith("https://test.net")  and flow.request.method == 'POST':
            req = flow.request.get_text()
            try:
                req_json = json.loads(req)
                aes_crypto = Aes_Crypto('294d317438m440ed', '8044ccb1qd92n5f5')
                ctx.log.info("发送的加密数据为:")
                ctx.log.info(req_json)
                data = json.loads(aes_crypto.decrypt(req_json['encData']))
                req_json['encData'] = data
                ctx.log.info("发送的数据解密后为:")
                ctx.log.info(req_json)
                flow.request.set_text(json.dumps(req_json))
            except Exception as e:
                print(e)
                pass
                
    def response(self, flow:HTTPFlow):
        if flow.request.url.startswith("https://test.net") and flow.request.method == 'POST':
            ctx.log.info("当前请求url:" + flow.request.url)
            req = flow.response.get_text()
            try:
                resp_json = json.loads(req)
                ctx.log.info("接收到服务端传来的解密数据:")
                ctx.log.info(req)
                if type(resp_json['result']) == str:
                    aes_crypto = Aes_Crypto('294d317438m440ed', '8044ccb1qd92n5f5')
                    resp_json['result'] = aes_crypto.encrypt(json.dumps(resp_json['result']))
                    ctx.log.info(resp_json)
                    flow.response.set_text(json.dumps(resp_json))
            except Exception as e:
                print(e)
                pass
        
addons = [FilterFlow(),]
downstream.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import json
from aes import Aes_Crypto
from mitmproxy import ctx
from mitmproxy.http import HTTPFlow

class FilterFlow:
    def request(self, flow):
        if flow.request.url.startswith("https://test.net") and flow.request.method == 'POST':
            req = flow.request.get_text()
            try:
                req_json = json.loads(req)
                if 'encData' in req_json.keys():
                    aes_crypto = Aes_Crypto('294d317438m440ed', '8044ccb1qd92n5f5')
                    ctx.log.info("发送的明文数据为:")
                    ctx.log.info(req_json)
                    data = aes_crypto.encrypt(json.dumps(req_json['encData']).replace(' ', ''))
                    req_json['encData'] = data
                    ctx.log.info(req_json)
                    flow.request.set_text(json.dumps(req_json))
            except Exception as e:
                pass
        
    def response(self, flow:HTTPFlow):
        if flow.request.url.startswith("https://test.net") and flow.request.method == 'POST':
            ctx.log.info("当前请求url:" + flow.request.url)
            req = flow.response.get_text()
            try:
                resp_json = json.loads(req)
                ctx.log.info(req)
                if type(resp_json['result']) == str:
                    ctx.log.info("接收到服务端传来的解密数据:")
                    ctx.log.info(req)
                    if resp_json['result']:
                        aes_crypto = Aes_Crypto('294d317438m440ed', '8044ccb1qd92n5f5')
                        resp_json['result'] = json.loads(aes_crypto.decrypt(resp_json['result']))
                        ctx.log.info("数据解密完成")
                        ctx.log.info(resp_json)
                        flow.response.set_text(json.dumps(resp_json))
            except Exception as e:
                pass
        
addons = [FilterFlow(),]

安装依赖

1、安装python依赖(python版本越高越好,建议python3.10以上)

pip3 install mitmproxy pip3 install cryptography pip3 install pycryptodome

2、运行mitmproxy证书

下文中$PYTHON_HOME为python安装目录

首先到$PYTHON_HOME/Scripts目录下运行一下mitmdump.exe,完成之后在当前用户根目录(如C:\Users\Administrator)下的.mitmproxy文件夹下即会生成证书

Windows安装证书:双击mitmproxy-ca-cert.p12—-全部默认直接点”下一步”直到安装完成。

Android安装证书: 把mitmproxy-ca-cert.cer通过usb复制到手机上—-点击使用证书安装器安装证书

运行mitmdump.exe

burpsuite的监听地址为: http://127.0.0.1:8080

D:\前端加解密\upstream.py为upstream.py文件所在目录,根据实际修改

在$PYTHON_HOME/Scripts目录下运行mitmdump.exe -s D:\JS加解密\mitm\upstream.py -p 8081 –mode upstream:http://127.0.0.1:8080 –ssl-insecure

在$PYTHON_HOME/Scripts目录下运行mitmdump.exe -s D:\JS加解密\mitm\downstream.py -p 8082 –ssl-insecure

在burpsuite中设置代理

设置代理.jpg

触发流量

通过浏览器把请求的流量发送到http://127.0.0.1:8081

这时就可以在burpsuite中看到相关数据包请求时的明文参数和返回时的明文数据

总结

凡所有相,皆是虚妄.攻防总是处于不断的变化之中,每当有新的防御措施出来,就会有新的攻击手段出现,二者即是矛盾的,又相互促进发展,不断的移形.