Python的10个未知安全漏洞陷阱

Python的10个未知安全漏洞陷阱

由于使用标准库和通用框架,Python 开发人员相信他们的应用程序具有可靠的安全状态。但是,在 Python 中,就像在任何其他编程语言中一样,某些功能可能会被开发人员误导或误用。通常,只有非常细微的差别或细节才能让开发人员疏忽并向代码库添加严重的安全漏洞。

在这篇博文中,我们分享了我们在实际 Python 项目中遇到的 10 个安全陷阱。我们选择了我们认为在开发者社区中鲜为人知的陷阱。通过解释每个问题及其影响,我们希望提高您的安全意识。如果您正在使用这些功能中的任何一个,请务必检查您的 Python 代码!

1. 优化Asserts

Python 提供了以优化方式执行代码的能力。这允许代码以更少的内存运行得更快。当应用程序被大规模使用或可用资源很少时,它尤其有效。一些预先打包的 Python 应用程序提供了优化的字节码。但是,在优化代码时,会忽略所有Asserts语句。这些有时被开发人员用来评估代码中的某些条件。例如,如果使用Asserts作为身份验证检查的一部分,这可能会导致安全绕过。

Python的10个未知安全漏洞陷阱

view.py

1    def superuser_action(request, user):
2        assert user.is_super_user
3        # execute action as super user

在这个例子中,第 2 行中的 assert 语句将被忽略,每个非超级用户都可以到达下一行代码。不建议将 assert 语句用于与安全相关的检查,但我们确实在实际应用程序中看到了它们。

2. MakeDirs 权限

函数os.makedirs在文件系统中创建一个或多个文件夹。它的第二个参数mode用于指定创建的文件夹的默认权限。在以下代码片段的第 2 行中,文件夹 A/B/C 是使用rwx—— (00700) 权限创建的。这意味着只有当前用户(所有者)对这些文件夹具有读取、写入和执行权限。

view.py

1    def init_directories(request):
2        os.makedirs("A/B/C", mode=0o700)
3        return HttpResponse("Done!")

在 Python < 3.6 中,文件夹 A、B 和 C 的创建权限均为 700。但是,在 Python > 3.6 中,只有最后一个文件夹 C 的权限为 700,其他文件夹 A 和 B 的创建权限均为默认 755。所以,对于 Python > 3.6,函数os.makedirs具有与 Linux 命令相同的属性:mkdir -m 700 -p A/B/C。一些开发人员不知道版本之间的差异,这已经导致 Django 中的权限升级漏洞 ( CVE-2020-24583 ),并以非常相似的方式导致 WordPress 中强化绕过

3. 绝对路径连接

所述os.path.join(path, *paths)函数用于多个文件路径组件连接成一个组合文件路径。第一个参数通常包含基本路径,而每个进一步的参数作为一个组件附加到基本路径。但是,该功能具有一些开发人员不知道的特殊性。如果附加组件之一以/开头,则包括基本路径在内的所有先前组件都将被删除,并且该组件被视为绝对路径。以下示例向开发人员展示了这种可能的陷阱。

view.py

1    def read_file(request):
2        filename = request.POST['filename']
3        file_path = os.path.join("var", "lib", filename)
4        if file_path.find(".") != -1:
5            return HttpResponse("Failed!")
6        with open(file_path) as f:
7        return HttpResponse(f.read(), content_type='text/plain')

在第 3 行,结果路径是使用os.path.join函数从用户控制的输入文件名构建的。在第 4 行中,检查生成的路径以查看它是否包含. 以防止路径遍历漏洞。但是,如果攻击者传递了文件名参数/a/b/c.txt,那么第3 行中的结果变量file_path就是一个绝对文件路径。在VAR / lib目录组件,包括基本路径现在被忽略os.path.join和攻击者可以读取任何文件,而无需使用单一特点。这种行为在os.path.join 中有描述文档说明它在过去导致了许多漏洞(Cuckoo Sandbox EvasionCVE-2020-35736)。

4. 任意临时文件

该tempfile.NamedTemporaryFile函数用于创建具有特定名称的临时文件。但是,前缀和后缀参数容易受到路径遍历攻击(问题 35278)。如果攻击者控制了这些参数之一,他就可以在文件系统中的任意位置创建一个临时文件。以下示例显示了开发人员可能遇到的陷阱。

view.py

1    def touch_tmp_file(request):
2        id = request.GET['id']
3        tmp_file = tempfile.NamedTemporaryFile(prefix=id)
4        return HttpResponse(f"tmp file: {tmp_file} created!", content_type='text/plain')

在第 3 行中,用户输入的id用作临时文件的前缀。如果攻击者将有效负载/../var/www/test作为id参数传递,则会创建以下tmp文件:/var/www/test_zdllj17。乍一看这听起来无害,但它为攻击者提供了利用更复杂漏洞的基础。

5. Extended Zip Slip

提取上传的文件档案是 Web 应用程序中的一个常见功能。在 Python 中,已知函数TarFile.extractall和TarFile.extract容易受到Zip Slip攻击。那时攻击者会篡改存档中的文件名,使其包含路径遍历 ( ../ ) 字符。这就是为什么存档条目应始终被视为不受信任的来源。zipfile.extractall和zipfile.extract功能可以消毒Extended Zip Slip,从而防止这样的路径遍历漏洞。但是,这并不意味着 ZipFile 库中不会出现路径遍历漏洞。以下示例显示了用于提取 zip 文件的代码。

view.py

4    def extract_html(request):
5        filename = request.FILES['filename']
6        zf = zipfile.ZipFile(filename.temporary_file_path(), "r")
7        for entry in zf.namelist():
8            if entry.endswith(".html"):
9                file_content = zf.read(entry)
10              with open(entry, "wb") as fp:
11                  fp.write(file_content)
12      zf.close()
13      return HttpResponse("HTML files extracted!")

在第 6 行,从上传的用户文件的临时路径创建了一个ZipFile处理程序。在第 7 – 11 行中,提取了所有以.html结尾的 zip 条目。第7 行中的函数zf.namelist包含 zip 文件中条目的名称。请注意,只有zipfile.extract和zipfile.extractall函数会清理条目,而不是任何其他函数。在这种情况下,攻击者可以创建具有任意内容的文件名,例如../../../var/www/html。恶意文件的内容在第 9 行读取,并在第 10-11 行写入攻击者的控制路径。结果,允许攻击者在整个服务器上创建任意 HTML 文件。

Python的10个未知安全漏洞陷阱

如上所述,档案中的条目应被视为不受信任。如果您不使用zipfile.extractall或zipfile.extract,则应始终清理 zip 条目的名称,例如使用os.path.basename。否则,它可能会导致像 NLTK Downloader ( CVE-2019-14751 ) 中发现的那样的严重安全漏洞。

6. 不完整的正则匹配

正则表达式 ( regex ) 是大多数 Web 应用程序的组成部分。我们经常看到自定义 Web 应用程序防火墙 (WAF) 将它们用于输入验证,例如检测恶意字符串。在 Python 中,re.match和re.search之间存在细微差别,我们希望在以下代码片段中进行演示。

view.py

3    def is_sql_injection(request):
4        pattern = re.compile(r".*(union)|(select).*")
5        name_to_test = request.GET['name']
6        if re.search(pattern, name_to_test):
7            return True
8        return False

在第 4 行,定义了一个模式,该模式匹配联合或选择以检测可能的 SQL 注入。这是一个糟糕的主意,因为您通常可以绕过这些黑名单,但我们已经在实际应用程序中看到了它。在第 6 行中,函数re.match与先前定义的模式一起使用,以检查第 5 行中的用户输入名称是否包含任何这些恶意值。但是,与re.search函数不同,re.match函数在新行上不匹配。例如,如果攻击者提交值aaaaaa \n union select,用户输入将与正则表达式不匹配。因此,可以绕过检查并且不提供任何保护。总的来说,我们不建议使用正则表达式拒绝列表进行任何安全检查。

7. Unicode Sanitizer 绕过

Unicode 允许在多种表示中使用字符并将这些字符映射到代码点。在 Unicode 标准中,为不同的 unicode 字符定义了四种规范化。应用程序可以使用这些规范化以独立于人类语言的统一方式存储数据,例如用户名。但是,攻击者可以利用这些规范化,这已经导致 Python 的urllib ( CVE-2019-9636 ) 中存在漏洞。以下代码片段演示了基于 NFKC 规范化的跨站点脚本 ( XSS ) 漏洞。

view.py

1    import unicodedata
2    from django.shortcuts import render
3    from django.utils.html import escape
4    
5    def render_input(request):
6        user_input = escape(request.GET['p'])
7        normalized_user_input = unicodedata.normalize("NFKC", user_input)
8        context = {'my_input': normalized_user_input}
9        return render(request, 'test.html', context)

在第 6 行中,用户输入由 Django 的转义函数清理以防止 XSS 漏洞。在第 7 行,经过过滤的输入通过 NFKC 算法进行标准化,以便通过test.html模板在第 8-9 行中正确呈现。

模板/test.html

1    <!DOCTYPE html>
2    <html lang="en">
3    <body>
4    {{ my_input | safe}}
5    </body>
6    </html>

在模板test.html 中,第4 行中的变量my_input被标记为安全 因为开发人员需要特殊字符并假设变量已经被转义函数清理过。通过使用关键字safe变量不会被 Django 额外清理。但是,由于第 7 行 ( view.py ) 中的规范化,字符%EF%B9%A4被转换为<并且%EF%B9%A5被转换为>。这允许攻击者注入任意 HTML 标签并触发 XSS 漏洞。为防止此漏洞,用户输入应始终在规范化后的最后一步进行清理。

8. Unicode 大小写冲突

如上所述,Unicode 字符被映射到代码点。然而,有许多不同的人类语言,Unicode 试图将它们统一起来。这也意味着不同字符具有相同“布局”的可能性很高。例如,小写的土耳其语ı(不带点)字符是大写的I。在基于拉丁字母的字母表中,字符i也是大写的I。在 Unicode 术语中,两个不同的字符以大写形式映射到相同的代码点。这种行为是可利用的,并且已经导致 Django 中的一个严重漏洞 ( CVE-2019-19844 )。让我们看一下以下密码重置功能的代码示例。

view.py

1    from django.core.mail import send_mail
2    from django.http import HttpResponse
3    from vuln.models import User
4
5    def reset_pw(request):
6        email = request.GET['email']
7        result = User.objects.filter(email__exact=email.upper()).first()
8        if not result:
9            return HttpResponse("User not found!")
10      send_mail('Reset Password','Your new pw: 123456.', '[email protected]', [email], fail_silently=False)
11      return HttpResponse("Password reset email send!")

在第 6行中,提供了用户输入的电子邮件,在第 7-9 行中,检查了提供的电子邮件值以查看是否存在拥有此给定电子邮件的用户。如果用户存在,则使用第 6 行中用户提供的电子邮件地址向第 10 行中的用户发送电子邮件。

重要的是要提及第 7-9 行中的电子邮件地址检查是通过使用不区分大小写的首先是上层功能。对于攻击,我们假设数据库中存在电子邮件[email protected]的用户。攻击者现在可以简单地传递foo@mıx.com作为第 6 行中的电子邮件,其中i被替换为土耳其语ı. 在第 7 行中,电子邮件然后被转换为大写,这导致[email protected]。这意味着已找到用户并发送密码重置电子邮件。但是,电子邮件从第 6 行发送到未转换的电子邮件地址,因此仍包含土耳其语ı。换句话说,另一个用户的密码被发送到攻击者控制的电子邮件地址。为防止此漏洞,第 10 行可以替换为数据库中用户的电子邮件。即使发生冲突,在这种情况下攻击者也无法从中受益。

9. IP 地址规范化

在 Python < 3.8 中,IP 地址由ipaddress库规范化,以便删除前导零。这种行为乍一看似乎无害,但它已经导致 Django 中存在一个高危漏洞 ( CVE-2021-33571 )。攻击者可以利用规范化绕过服务器端请求伪造 ( SSRF ) 攻击的潜在验证器。以下代码片段显示了如何绕过此类验证器。

view.py

1    import requests
2    import ipaddress
3
4    def send_request(request):
5        ip = request.GET['ip']
6        try:
7            if ip in ["127.0.0.1", "0.0.0.0"]:
8                return HttpResponse("Not allowed!")
9            ip = str(ipaddress.IPv4Address(ip))
10      except ipaddress.AddressValueError:
11         return HttpResponse("Error at validation!")
12      requests.get('https://' + ip)
13      return HttpResponse("Request send!")

在第 5 行,IP 地址由用户提供,在第 7 行,拒绝列表用于检查 IP 是否为本地地址,以防止可能的 SSRF 漏洞。拒绝列表并不完整,仅用作示例。在第 9 行,代码检查提供的 IP 是否是 IPv4 地址,同时 IP 是否已规范化。在所有验证之后,对提供的 IP 的实际请求在第 12 行执行。但是,攻击者可以将127.0.00.1作为 IP 地址传递,这在第 7 行的拒绝列表中没有找到。然后,在第 9 行,使用ipaddress.IPv4Address将 IP 规范化为127.0.0.1。因此,攻击者能够绕过 SSRF 验证器,并向本地网络地址发送请求。

10. URL 查询解析

在 Python < 3.7 中,函数urllib.parse.parse_qsl允许使用; 和&字符作为 URL 查询变量的分隔符。这里有趣的是; 字符不被其他语言识别为分隔符。在下面的示例中,我们想说明为什么这种行为会导致漏洞。假设我们正在运行一个基础设施,其中前端是一个 PHP 应用程序,还有另一个内部 Python 应用程序。

攻击者向 PHP 前端发送以下 GET 请求:

GET https://victim.com/?a=1;b=2

PHP 前端仅识别一个查询变量:a的内容为1;b=2。PHP 不处理;字符作为查询变量的分隔符。现在前端使用查询变量a将攻击者的请求转发到内部 Python 应用程序:

GET https://internal.backend/?a=1;b=2

如果使用urllib.parse.parse_qsl,Python 应用程序会处理两个查询变量:a=1和b=2这种查询变量解析的差异会导致致命的安全漏洞,例如 Django 中的 Web 缓存中毒漏洞(CVE- 2021-23336)。

概括

在这篇博文中,我们介绍了 10 个 Python 安全陷阱,我们认为这些陷阱在开发人员中鲜为人知。每个微妙的陷阱都很容易被忽视,并且在过去的现实世界应用程序中导致了安全漏洞。我们已经看到,从处理文件、目录、档案、URL、IP 到简单的字符串,各种操作中都可能出现陷阱。一个常见的模式是使用可能有意外行为的库函数。这提醒我们始终升级到最新版本并仔细阅读文档。我们正在研究这些陷阱,以在未来不断改进我们的代码分析器。

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注