【技术分享】文件解压之过 Python中的代码执行

http://p4.qhimg.com/t013b93a1f679d75675.jpg

译者:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

一、前言

Python中负责解压压缩文件的代码实现上并不安全,存在目录遍历漏洞,攻击者可以利用该漏洞覆盖__init__.py文件,实现任意代码执行。

在PHP中,实现代码执行最为简单的一种方式就是利用PHP中不安全的文件上传处理逻辑。如果你可以欺骗文件上传逻辑,上传任意PHP文件,那么你就可以执行任意PHP代码。然而,如果我们面对的是使用Go、Node.js、Python、Ruby等编写的现代Web框架时,情况会有所变化。这种情况下,即使我们把.py或者.js文件成功上传到服务器上,通过URL请求这些文件通常并不会返回任何结果,因为目标应用程序并没有开放相应的路由或者URL渠道。即使我们可以通过URL来访问这些资源,也不会触发任何代码执行动作,因为服务器会把这些文件当作静态文件,以文本形式返回这些文件的源代码。在本文中,我们会介绍如何在Python构造的Web环境中实现代码执行,前提是我们可以将压缩文件上传到服务器。

简而言之,Web应用中的安全规则就是永远不要信任用户的输入,这个原则不仅仅局限于原始的HTTP请求对象范围(如查询参数、具体post的内容、文件、头部信息等)。精心构造的压缩文件虽然看起来人畜无害,但如果负责解压此类文件的代码本身并不安全,那么这种文件就会带来安全风险。本文介绍了这类漏洞的细节及利用方法,具体灵感源自于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文件,然后将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”实现代码执行。Python的官方文档中有这样一段话:

“如果某个目录想成为Python中的包,那么该目录中就需要包含__init__.py文件,这样就能避免模块搜索时把目录名为常用字符串(如string)的那些目录包含进来。在最简单的情况下,__init__.py可以是个空文件,也可以用来执行包中的初始化代码或者设置__all__变量,稍后会继续描述。”

根据这段表述,假设Web应用将某个目录当成Python包,如果我们使用任意Python代码覆盖该目录中的__init__.py文件,当目标应用导入这个包时,就会执行我们的代码。通常i情况下,为了顺利执行代码,我们需要重启服务器。在本文案例中,我们的实验目标为一个Flask服务器,并且启用了debug功能(debug设为True),这意味着只要Python文件发生改动,服务器就会重启。


三、构造Payload

存在漏洞的这个Web应用有个名为config的目录,该目录中包含__init__.py以及settings.py文件。主功能文件server.py会从config目录中导入settings.py文件,这意味着如果我们可以将代码写入到config/__init__.py,我们就可以实现代码执行。我们可以使用如下代码构造攻击载荷:

import zipfile
z_info = zipfile.ZipInfo(r"../config/__init__.py")
z_file = zipfile.ZipFile("/home/ajin/Desktop/bad.zip", mode="w")
z_file.writestr(z_info, "print 'test'")
z_info.external_attr = 0777 << 16L
z_file.close()

查看负责文件上传的代码时, 你会看到上传的文件被解压到uploads目录中。我们可以利用zipfile.ZipInfo()语句构造恶意文件名。这里我们需要将文件名设为“../config/__init__.py”,以覆盖config目录中的__init__.py文件。z_info.external_attr = 0777 << 16L这条语句会将文件权限设为所有人可读可写权限。现在,我们来创建一个zip文件,将其上传到目标应用中。

我们可以看到Flask应用开始重载,然后服务器上的控制台会打印出“test”字符串。看来我们已经实现代码执行目的。


四、攻击实际环境中的应用

前面这个案例中,由于Flask服务器运行在debug模式下,因此会立即执行任意代码。实际环境中情况可能不会完全一致。你可能需要等待一段时间,直至服务器重启为止。另一个问题是,我们并不能每次都知道目标应用使用的包的具体路径(本例中为config目录)。如果目标使用的是开源项目,通过阅读源代码我们很容易就能得到这些信息。对于闭源应用来说,我们可以猜测比较常见的包目录,如conf、config、settings、utils、urls、view、tests、scripts、controllers、modules、models、admin、login等。这些包目录经常出现在某些Python Web框架中,如Django、Flask、Pyramid、Tornado、CherryPy、web2py等。

换个思路,假设目标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,假设virtualenv的目录为venv,那么我们就可以使用类似“../venv/lib/python2.7/site-packages/pip/__init__.py”之类的文件名。这样处理后pip会受到影响,但下次服务器上的用户运行pip命令时,就会执行我们的代码。


五、演示视频


六、预防措施

为了防御这个漏洞,你需要使用ZipFile.extract()来解压文件。zipfile文档中有这样一段话:

“如果待处理文件使用的是绝对路径,那么路径中包含的驱动、UNC字符以及前缀(后缀)斜杠会被过滤掉,例如,在Unix上,///foo/bar经过处理后会变为foo/bar,在Windows上,C:foobar经过处理后会变为foobar。文件名中包含的所有“..”字符会被移除,例如,../../foo../../ba..r会变成foo../ba..r。在Windows上的非法字符(:、<、>、|、"、?、以及*)会被替换为下划线(_)”。

(完)