开局一个登录框,漏洞全靠捡。
随便输入一个工号,点击密码登录查看数据包,看看数据包的情况。
可以查看到请求包中有两个参数:requestData和encrypted,响应包存在encrypted和responseData。通过名字分析可以猜测:encrypted中的内容就是加密当前请求包的密钥,和请求内容一同发送给服务端进行解密。响应包同理。
判断加密思路后可以开始追前端加密逻辑了,常见的方法有通过文件流程断点、代码标签断点、XHR提交断点。不过我一般喜欢简单粗暴的直接搜索关键字,如:encrypt/encode。
如上图所示一般不会存在很多包含此关键字的匹配行。刚开始学习可以每条都点进去打上断点,然后输入工号点击断点,通过是否能够断点断住来判断此处代码是否参与了加密。熟悉之后就可以根据代码来判断此处是否为加密点,个人的经验一般为:进入后查看上下文代码,观察是否存在key、iv、mode、Pkcs7、Pkcs5、RSA、AES、SM4、ECB、CBC等关键字,存在再观察一下逻辑,是否将key、iv传入了encrypt加密函数中,基本可以很顺利的找到大部分的加密点。
断点的位置一般都是断在明文请求体进入加密函数后,出来变成密文的位置。
从上图可以获得的信息,首先是代码方面
加密的方式:AES
加密的模式:ECB
加密的填充方式:Pkcs7
从作用域来看,可获得的信息有:
明文请求包
AES的加密密钥
加密后的结果
现在就来验证我们的猜测是否正确
问题一:如何判断返回值即为加密后的请求体数据?
将此数据包发送到Burpsuite,查看responseData是否与返回值一致即可判断。
问题二:如何判断推测的加密信息是否正确? 使用Tscan中的加解密模块,将我们获取到的所有信息放入其中再次进行加密,查看加密结果是否与作用域中的返回值是否一致。
确认加密模式没错之后就可以去寻找加密点了,此处只是调用了加密函数进行加密,还没有看到具体的加密逻辑,寻找具体的加密点可通过步入,继续执行、或者翻找上下文找到具体加密点,此处通过继续执行JS代码,发现具体加密点。
1 2 3 4 5 6 7 8 9 10 11 12 encodeData (e, t ) { var n = this .randomString (16 ) , r = this .Encrypt (e, n) , a = new JSEncrypt ; a.setPublicKey (t); var o = a.encrypt (n) , i = { requestData : r, encrypted : o }; return i }
根据上图可获得的信息有
AES 密钥的生成方式,随机生成的16位字符串
RSA 的公钥
encrypted的生成逻辑
到这里也就捋清楚了整体的加密解密逻辑
用户端请求操作: 用户输入–> AES 使用随机密钥加密用户输入 –> requestData –> 使用 RSA 加密随机生成的AES密钥 –> encrypted
服务器端处理用户请求逻辑: 服务器收到用户传来的 requestData 和 encrypted –> 使用 RSA 私钥解密 encrypted 获取随机密钥 –> 解密 requestData 数据 –> 后端处理
服务器响应用户请求逻辑: 后端处理完成 –> AES 使用随机密钥加密服务器响应包 –> responseData –> 使用 RSA 加密随机生成的AES密钥 –> encrypted
用户接收到响应操作: 接收到服务器响应数据 –> 浏览器使用 RSA 私钥解密 encrypted 获取随机密钥 –> 使用解密出的密钥解密 responseData 数据 –> 展示给用户
现在又到了验证环节:
问题一: AES生成方式是否为随机生成的16位字符串
1 2 3 4 5 6 7 8 n = this .randomString (16 ) randomString : e => { e = e || 32 ; for (var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678" , n = t.length , r = "" , a = 0 ; a < e; a++) r += t.charAt (Math .floor (Math .random () * n)); return r }
问题二:AES密钥是否为 RSA 加密后的结果
1 2 3 4 n = this .randomString (16 ) a = new JSEncrypt a.setPublicKey (t) a.encrypt (n)
根据setPublicKey,就能知道公钥的值,RSA 的公私钥通常以 —–BEGIN xxx KEY—– 和 —–END xxx KEY—– 包裹的格式(PEM 格式)存储,这是 PEM (Privacy-Enhanced Mail) 标准的标记方式。而本次RSA使用的加密 JSEncrypt 必须使用 PEM 格式。
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 from cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import padding as asym_paddingimport base64public_key_pem = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9baE8HsiYyy1IL35kjEsNTzfCGu9cgEG1aAQh3BUSsUQIVtwDeHcKzhjqMcPnPM1h 92HZAlt2UOyXj1YdkT/b7IL9mFTanNIQFR8jgqDH2l0PvRVyfjoEuRDCdBgmt8X03QI/JK4Gcg6hD42Zr2HCTH0emygdKaRKHM8kTGXQfwIDAQAB -----END PUBLIC KEY-----""" plaintext = "ABpR7ExGX2A7p2jH" def rsa_encrypt (public_key_pem, message ): public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8' )) encrypted = public_key.encrypt( message.encode('utf-8' ), asym_padding.PKCS1v15() ) return base64.b64encode(encrypted).decode('utf-8' ) print ("加密后的结果 (Base64):" )print (rsa_encrypt(public_key_pem,plaintext))
因为 PKCS1v15 填充方案在加密时引入了随机数,所以每次对同一个AES密钥加密都会出现不同的结果。但只要用正确的私钥解密,结果一定是原始明文。
验证上述的分析逻辑是否通顺,此处为了方便观察结果,提前将响应包解密了。
可以从上图看到我们的思路没有问题,下一步就是实现自动化。此处选择使用 mitmproxy 来作为burpsuite下游代理。整体思路如下:
burpsuite发送明文数据 –> mitmproxy加密成密文 –> 服务器响应密文 –> mitmproxy解密成明文 –> burpsuite响应体明文
此处无法实现下述逻辑:
浏览器密文 –> burpsuite解密成明文 –> mitmproxy加密成密文 –> 服务器响应密文 – mitmproxy解密成明文 –> burpsuite响应体明文
经个人实验得出的结论为: 浏览器和服务器端一共存在两套rsa公私钥。 浏览器掌握着:浏览器公钥+服务器私钥 服务器掌握着:服务器公钥+浏览器私钥
将浏览器加密后的requestData和encrypted用浏览器的公钥和服务器的私钥是无法解密的。
最终的mitmproxy脚本
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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 from mitmproxy import httpimport jsonimport base64from cryptography.hazmat.primitives.asymmetric import padding as asym_paddingfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesfrom cryptography.hazmat.primitives import paddingimport randomimport stringpublic_key_pem = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9baE8HsiYyy1IL35kjEsNTzfCGu9cgEG1aAQh3BUSsUQIVtwDeHcKzhjqMcPnPM1h 92HZAlt2UOyXj1YdkT/b7IL9mFTanNIQFR8jgqDH2l0PvRVyfjoEuRDCdBgmt8X03QI/JK4Gcg6hD42Zr2HCTH0emygdKaRKHM8kTGXQfwIDAQAB -----END PUBLIC KEY-----""" private_key_pem = """-----BEGIN RSA PRIVATE KEY----- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIE3Ctx/ZRvjTuAPnjtvquyEEDazVJNgAo909M9BX2umwBOwVXE+hGyH EXPDjZ+h4qJ2lTUc+B3UiVz0TnBlvGGuOQdvImWwfylHtkG+pzrTusE45AEy+AsQpfPDKrypIhk0bhQLGkEhoRWg04rcqrXXpzIoC0P8a+vlScz8LQ4NAgMBAAECgYAX74ZHiiHEpLq7rqj1AZ576YrHVzjXg/V1dYjTy5xNaLoz63ooXBhTskF9XEAjze0ZgzXofNFJVVGMsoTFNVNL0YKKBWTrtw58iXFDEn0JwD1LxYwZQsc+V1da/OzaL+0gDcuMY4YmLqOUGTmQer1LLt1oNobI3ZYga7Q1iPz40QJBALh/346m0J6R8tafrJgnmNLfNh8DXGq0CEChoP3tIeOf2tbcX9efcqEW+igTlm+8BtDHzHo2Yjf+bJEpp1e+mM8CQQCzSmpC2zYCRowOyIx3uX3KY1hue4i3uPrUSG2sBgmBdBLHmsSAA+wtKlAsS1kh0voCjvKLL2bHO4UxyDqeuipjAkBcWMzqFv8Gz6CP4p4+DlvE+KqbPVBtrC0RRJVTY/T5fRLJRsbGI235yYlus9cxmBiFOexUI5Jn2nY29nVnSuQrAkEAmFgH+K0ZtE9LnRgtu2GjEEDgGGjhn/MPNyggAIbUtunxNyg8BebPXQVSQID5yLLjex8J2ti5RVs+7zELFmprrwJAPe8jVXfSq+5nvHMiUO4m8/MR4YmMsvG2G/bVN74+v4SthceLFmLNPjTGuBvcuFgX4B5HP3B2M90Kr0KWmET99w== -----END RSA PRIVATE KEY-----""" def random_string (length=16 ): chars = string.ascii_letters + string.digits return '' .join(random.choices(chars, k=length)) def encrypt (plain_text, key ): key_bytes = key.encode('utf-8' ) plain_bytes = plain_text.encode('utf-8' ) padder = padding.PKCS7(128 ).padder() padded_data = padder.update(plain_bytes) + padder.finalize() cipher = Cipher(algorithms.AES(key_bytes), modes.ECB()) encryptor = cipher.encryptor() cipher_text = encryptor.update(padded_data) + encryptor.finalize() return base64.b64encode(cipher_text).decode('utf-8' ) def rsa_encrypt (public_key_pem, message ): public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8' )) encrypted = public_key.encrypt( message.encode('utf-8' ), asym_padding.PKCS1v15() ) return base64.b64encode(encrypted).decode('utf-8' ) def decrypt (cipher_text, key ): key_bytes = key.encode('utf-8' ) cipher_bytes = base64.b64decode(cipher_text) cipher = Cipher(algorithms.AES(key_bytes), modes.ECB()) decryptor = cipher.decryptor() decrypted_padded = decryptor.update(cipher_bytes) + decryptor.finalize() unpadder = padding.PKCS7(128 ).unpadder() decrypted = unpadder.update(decrypted_padded) + unpadder.finalize() return decrypted.decode('utf-8' ) def rsa_decrypt (private_key_pem, encrypted_message ): private_key = serialization.load_pem_private_key( private_key_pem.encode('utf-8' ), password=None ) decrypted = private_key.decrypt( base64.b64decode(encrypted_message), asym_padding.PKCS1v15() ) return decrypted.decode('utf-8' ) class CryptoProxy : def request (self, flow: http.HTTPFlow ): if flow.request.method in ["POST" , "PUT" , "PATCH" ]: try : data = json.loads(flow.request.content.decode('utf-8' )) print ("[DEBUG] 原始请求内容:" , data) if 'requestData' in data and 'encrypted' in data: print ("[INFO] 请求已包含加密字段,跳过处理" ) return n = random_string(16 ) encrypted_data = encrypt(json.dumps(data), n) rsa_encrypted_key = rsa_encrypt(public_key_pem, n) new_request_body = { 'requestData' : encrypted_data, 'encrypted' : rsa_encrypted_key } flow.request.content = json.dumps(new_request_body).encode('utf-8' ) flow.request.headers['Content-Length' ] = str (len (flow.request.content)) flow.request.headers['Content-Type' ] = 'application/json' except json.JSONDecodeError: print ("[WARNING] 请求不是有效的JSON格式" ) except Exception as e: print (f"[ERROR] 请求处理失败: {str (e)} " ) def response (self, flow: http.HTTPFlow ): try : raw_content = flow.response.content.decode('utf-8' , errors='ignore' ) response_data = json.loads(raw_content) encrypted_key = response_data.get('encrypted' ) or response_data.get('encryptedKey' ) encrypted_data = response_data.get('responseData' ) or response_data.get('data' ) if encrypted_key and encrypted_data: decrypted_key = rsa_decrypt(private_key_pem, encrypted_key) decrypted_data = decrypt(encrypted_data, decrypted_key) print ("[SUCCESS] 解密后的数据:" , decrypted_data) flow.response.content = decrypted_data.encode('utf-8' ) flow.response.headers['Content-Length' ] = str (len (flow.response.content)) flow.response.headers['Content-Type' ] = 'application/json' else : print ("[WARNING] 响应中缺少加密字段" ) except json.JSONDecodeError: print ("[ERROR] 响应不是有效的JSON格式" ) except Exception as e: print (f"[FATAL ERROR] 响应处理失败: {str (e)} " ) addons = [CryptoProxy()]
启动 mitmproxy
1 mitmdump -s encrypt.py --listen-port 8484
最终实现的效果