You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HTTPS响应体在MITM代理脚本中以字节日志记录的问题解决

关于MITM代理HTTPS响应日志可读性的问题

我用Python脚本构建了中间人(MITM)代理来拦截HTTPS流量,脚本会把请求和响应记录到日志文件里。HTTPS请求和响应的头信息能正常以明文读取,但响应体却以字节字符串形式记录,很难解读。

相关代码

def relay_data(self, s_ssl, conn_ssl, buffer_size, url):
    website_response = []
    while True:
        try:
            request = conn_ssl.recv(buffer_size)
            if not request:
                break
            s_ssl.sendall(request)
        except socket.error:
            pass
        try:
            response = s_ssl.recv(buffer_size)
            if not response:
                break
            conn_ssl.sendall(response)
            if response:
               website_response.append(response)
        except socket.error:
            pass
    if website_response:
       log_response_data(website_response, "https")
    print(f"Request completed (HTTPS) [{url}]")

def log_response_data(website_response, protocol):
    formatted_response, demarcation = "", "____________________________________________________________________________________________________"
    for response in website_response:
        try:
           formatted_response += response.decode('utf-8')
        except:
           formatted_response += response
    if formatted_response:
       with open(f"{protocol}_log_file", "a") as F:
            F.write(f"{demarcation}\n{formatted_response}\n{demarcation}\n\n")

日志示例(截断)

______________________________________________________________________________________________________________________
b'HTTP/1.1 200 OK
Vary: Accept-Encoding
Content-Encoding: br
Content-Type: text/css; charset=utf-8
...(省略部分头信息)
Content-Length: 10118

b'\xe2'b'\x1d\x96\x88\xa2>\x04(B\x86\xb9G\x7fi\xdf\x7f~\xbe,U\xa3\xfb\xc0\xe3\x1b\x1b\xa4j\xa7\x9d\xe9\xbc\xee\xf6\xda\xc9\x1e72\xd8$N\xcc\x11s\x84\x14q\xbd\x99\xf6\xa64\x00\xe4\xdc\x99\xec\xe7\xb2\x95+"\x00\xca\xf9L\xa9\xf1A\xc26\xef\xd5\xcd\xec\x02U\x0bW\x05\xba\xaa#\xbfs\xb4\x92\xef\xd7\xdd3\xdc}3\xa0\xb0\x86\x12\xb9\xcbc\x1d\x16<\xe3\xe9d\x9cM\x7f\x96\xc8\xd9TY|A\xa8R\x12)HB=\x86\xea\xcb\xdeo\x14\t"\xf2\xcc\xb6e\xb8l\xf4d\xe2\xf7H\xb0@\x91\x90\xf0\xbbp\xd3\xe8\xe9\xe0\xaf\x050\xbfz\xb6\x94\xdcd\xeb\xadq\x92\x80xx\x17\x86\xb6K\xd7\xa6`\xa11r\x01uU\xc5+Q%\xb3\xf4\xe9\x14\x81\xee*A\x93\xbc\xa3\x8c\x97+\xec\x8e}\xda\xbbg\x8bv\x16\x04?*E\x80v3\x90\xb7?!\xec\x0b}\x87"\xb0\x9d\x83t\xdb{\xb7\x11!\xe8\xc23c\x91^\xfc\xe1\xc1\xceC\xcc\xa3\x9d\xb4\xc3\x10\xc5-\xb5\xe0\xd5K\xaa\xcf\x03\xd9\xff<\xf0\xe1\xde}\xdat\x04%\x14\xc01L}\x1c`\xef\xc3\xe8b8\xf2\xcf\xc5\x02\t6J/ \x03\x9e9\t\x1e/<R\xb3(\xd8\x12\x12\xd4q\xf6\xbb'\x00\x16\xd6v6\xbc\x8d\xc5\xc2\x8a%Y\x9a\x83\xa5\x8ad\xfb\xd9(\x16r\xa6t\xc2^\xee\xa51\xde\xd8m\x05\x1d/\x84DA\xc4\xc0Z\xd6\xdb x\xff\x1e\xd6\xcd\x06\xe6\x1c\x8fb\xa1\xc2F\xcc\x90\xf90\xc3c\x15}\xbb\xb5_\xd8j0e\xd6\xd0)}\x81\x9c:\xf30\xac\x8f\x01\xe8\xea}Sx\xc8\x03\xb8\x86X<\xa3*)4\xac\x08\xce\xc2\xde\xa8\x87\x1d5I7\xd7\xdf\xde\xdc\x88\xf7\xdf\x03\xc0<>\x15\xb0\xd6e7\xd2\x98\xb9$\xfd\xdd\xfa\xc9\xbe_\x0f\xde\xech#p\xd9/\x15\x81o\xc1i\x1f\x01\xf9\x99]\x01\x0e\xf7\xd5FC\xec\xb2xt\x11\x88\xa5E'\xbe\xf4w\x8b\xc0\x83w\xcd\xe9U\x97\xbb\xfb\xdf\xf9\xa9m\x86\x08\xdc\xc2\xdd\xd3\xdb\xee\xaf|\xfc\xcb\xfe\xb2\xfb\xd1\xebp\xdb`\xf7\x8f\xe0\x87K\xdb\xd0\xdf\xf2vg\xf7\xad\x05\x7f{\x06\xbf[\xe1\xb2\xdb\xac\x0fe*\x016\xeen\x11\xbf\xe4v\x9b\x8d\x8d\x1b\xf0e,L\xfe\x9b\x97\xfe\xed7NQ\xcc+\x16\x81\xbf\xfd\x86\x91F1F\xd5+\x96!\xe93?'........
______________________________________________________________________________________________________________________

疑问

  1. 为何HTTPS响应体在日志中以字节形式呈现?
  2. 如何解码日志中的字节数据?
  3. 是否需要区分不同内容类型并分别处理?若需要,如何在脚本中实现?

解决方案

1. 响应体以字节形式呈现的原因

  • 你的代码尝试用utf-8解码响应字节,但多数HTTPS响应体并非纯UTF-8文本:
    • 示例中响应启用了Content-Encoding: br(Brotli压缩),压缩后的内容是二进制,无法直接用UTF-8解码。
    • 图片、视频、二进制文件等本身是非文本数据,UTF-8解码必然失败。
  • 解码抛出异常时,代码直接将原始字节对象拼接到字符串中,Python会自动把字节对象转为b'...'格式的字符串,导致日志中出现这类内容。

2. 解码日志中的字节数据

针对已生成的日志:

  1. 还原字节对象:将日志中的b'...'格式字符串转成字节,可使用eval()(仅安全用于自身生成的日志)或手动处理转义字符后用bytes.fromhex()。
  2. 处理压缩编码:如果响应头包含Content-Encoding(如br、gzip、deflate),先解压数据:
    import brotli
    decompressed_data = brotli.decompress(byte_data)
    
  3. 解码文本内容:根据Content-Type指定的charset解码解压后的数据,比如text/css; charset=utf-8就用utf-8解码。
  4. 二进制数据处理:对于图片、视频等二进制数据,直接保存为文件查看,不要强行转成文本。

3. 区分内容类型的脚本实现

必须区分内容类型处理才能提升日志可读性,核心思路是先解析响应头,分离头和体,再根据头信息处理响应体。

修改后的关键代码

import brotli
import gzip
from io import BytesIO

def parse_response_header(response_data):
    """分离响应头和响应体,解析头信息"""
    header_end = response_data.find(b'\r\n\r\n')
    if header_end == -1:
        header_end = response_data.find(b'\n\n')
    if header_end == -1:
        return {}, response_data
    header_bytes = response_data[:header_end]
    body_bytes = response_data[header_end+4:] if header_end+4 < len(response_data) else b''
    
    # 解析头信息为字典
    headers = {}
    for line in header_bytes.splitlines():
        if b':' not in line:
            continue
        key, value = line.split(b':', 1)
        headers[key.decode('ascii').strip().lower()] = value.strip()
    return headers, body_bytes

def decode_body(headers, body_bytes):
    """根据头信息解码响应体"""
    # 处理内容编码(解压)
    content_encoding = headers.get('content-encoding', b'').decode('ascii').lower()
    if content_encoding == 'br':
        body_bytes = brotli.decompress(body_bytes)
    elif content_encoding == 'gzip':
        body_bytes = gzip.GzipFile(fileobj=BytesIO(body_bytes)).read()
    elif content_encoding == 'deflate':
        body_bytes = gzip.decompress(body_bytes)
    
    # 处理内容类型
    content_type = headers.get('content-type', b'').decode('ascii').lower()
    if content_type.startswith('text/') or 'json' in content_type or 'javascript' in content_type or 'css' in content_type:
        # 提取字符编码,默认utf-8
        charset = 'utf-8'
        if 'charset=' in content_type:
            charset_part = content_type.split('charset=')[-1]
            charset = charset_part.split(';')[0].strip()
        try:
            return body_bytes.decode(charset)
        except:
            return f"[无法解码为文本,二进制长度:{len(body_bytes)}]"
    else:
        # 二进制数据,记录类型和长度
        return f"[二进制数据,类型:{content_type},长度:{len(body_bytes)}]"

def relay_data(self, s_ssl, conn_ssl, buffer_size, url):
    website_response = []
    while True:
        try:
            request = conn_ssl.recv(buffer_size)
            if not request:
                break
            s_ssl.sendall(request)
        except socket.error:
            pass
        try:
            response = s_ssl.recv(buffer_size)
            if not response:
                break
            conn_ssl.sendall(response)
            if response:
               website_response.append(response)
        except socket.error:
            pass
    if website_response:
        # 合并所有响应片段后处理
        full_response = b''.join(website_response)
        log_response_data(full_response, "https")
    print(f"Request completed (HTTPS) [{url}]")

def log_response_data(full_response, protocol):
    demarcation = "____________________________________________________________________________________________________"
    # 解析头和体
    headers, body_bytes = parse_response_header(full_response)
    
    # 格式化头信息
    formatted_header = '\n'.join([f"{k.title()}: {v.decode('ascii')}" for k, v in headers.items()])
    # 解码响应体
    formatted_body = decode_body(headers, body_bytes)
    
    # 拼接日志内容
    formatted_response = f"{formatted_header}\n\n{formatted_body}"
    
    with open(f"{protocol}_log_file", "a") as F:
        F.write(f"{demarcation}\n{formatted_response}\n{demarcation}\n\n")

核心改进点

  • 头体分离:从完整响应中拆分出头信息和响应体,避免混合处理导致的错误。
  • 自动解压:识别并处理br、gzip、deflate三种常见压缩编码的响应体。
  • 类型区分:对文本类内容解码为可读字符串,二进制内容记录类型和长度,避免乱码。
  • 合并处理:先合并所有响应片段再解析,避免分段处理导致的头体拆分失败。

内容的提问来源于stack exchange,提问作者Æmmy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 02:59:52