Python开发人员始终相信他们写的程序是安全的,因为使用的都是标准库和通用框架。但是,Python的某些功能可能会被误用,从而导致安全问题。这些问题隐蔽,并且危害性十足,像一颗定时炸弹,存在于程序中,下面将分享在实际的Python项目中遇到的Top 10鲜为人知的安全陷阱。
为了使代码内存占用更少,运行更快,Python提供了一种优化运行的机制。然而需要注意的是,当以优化模式运行代码的时候,所有的assert都会被忽略掉,如果开发者使用了assert进行身份验证,可能导致权限绕过漏洞。
示例
def superuser_action(request, user):
assert user.is_super_user
# execute action as super user
这里的assert将会被忽略掉,导致验证身份的代码无效,从而造成权限绕过。
os.makedirs一般用来创建一个/多个文件夹,它的第2个参数mode用来指定创建文件夹的默认权限。我们先来看一个例子
示例
def init_directories(request):
os.makedirs("A/B/C", mode=0o700)
return HttpResponse("Done!")
在 Python < 3.6 中,文件夹 A、B 和 C 的创建权限分别为 700。
但是,在 Python > 3.6 中,只有最后一个文件夹 C 的权限为 700,其他文件夹 A 和 B 的默认权限为 755。
P.S. Python > 3.6,函数os.makedirs与 Linux 中mkdir -m 700 -p A/B/C 保持一致。
一些开发人员不知道版本之间的差异,可能会导致权限提升漏洞,比如 Django 中的权限升级漏洞 ( CVE-2020-24583 )。
os.path.join(path, *paths)函数用于将多个文件路径连接成一个组合文件路径。第一个参数通常是基础路径,而每个其他参数都会附加到基础路径后。但是这个函数有个鲜为人知的特性,如果某一个附加路径以 / 开头,包括基本路径在内的,它之前的路径都会被忽略,并且把这个附加路径视为绝对路径。
示例
def read_file(request):
filename = request.POST['filename']
file_path = os.path.join("var", "lib", filename)
if file_path.find(".") != -1:
return HttpResponse("Failed!")
with open(file_path) as f:
return HttpResponse(f.read(), content_type='text/plain')
如果传入的filename参数是 a/b/c.txt,则读取的文件是 /var/lib/a/b/c.txt,如果传入的是 /a/b/c.txt,则读取的文件变成了 /a/b/c.txt。这种问题已经导致了很多安全漏洞,比如CVE-2020-35736。
tempfile.NamedTemporaryFile函数用于创建具有特定名称的临时文件,有prefix和suffix两个参数指定前后缀,如果前后缀参数可控,则会导致任意目录写文件漏洞。
示例
def touch_tmp_file(request):
id = request.GET['id']
tmp_file = tempfile.NamedTemporaryFile(prefix=id)
return HttpResponse(f"tmp file: {tmp_file} created!", content_type='text/plain')
用户输入id用作临时文件的前缀。如果攻击者传递/../var/www/test给参数id,则会创建以下tmp文件:/var/www/test_zdllj17,可以将文件写到Web目录。
压缩文件中打包的文件使用../作为文件名,从而进行目录穿越读写文件已经是老生常谈了,zipfile的extract和extractall函数都会对../进行清理,从而阻断此漏洞。但是,zipfile中的函数只有这两个有这个功能,一些开发人员认为自己使用了zipfile库就是安全无忧的。
示例
def extract_html(request):
filename = request.FILES['filename']
zf = zipfile.ZipFile(filename.temporary_file_path(), "r")
for entry in zf.namelist():
if entry.endswith(".html"):
file_content = zf.read(entry)
with open(entry, "wb") as fp:
fp.write(file_content)
zf.close()
return HttpResponse("HTML files extracted!")
上述代码使用了zipfile库,但是没使用extract和 extractall函数。如果传入的zip文件夹中有一个../../var/www/html,则会将恶意内容写入到Web目录。NLTK下载器出现过这类漏洞(CVE-2019-14751)。
正则表达式是代码中不可或缺的一部分,re.search会匹配多行内容,而re.match只匹配第一行,攻击者可能会使用 n 进行绕过。
示例
def is_sql_injection(request):
pattern = re.compile(r".*(union)|(select).*")
name_to_test = request.GET['name']
if re.search(pattern, name_to_test):
return True
return False
如果攻击者传入aaaa n union select,不匹配正则表达式,从而绕过权限控制。
P.S. 不建议使用正则进行安全检查。
unicode.normalize() 函数可以按照指定形式对字符串进行标准化,如NFKC。可能会导致标准化前的无害代码,在绕过了安全检查后进行了标准化,变成了恶意payload。来看下面的例子。
示例
import unicodedata
from django.shortcuts import render
from django.utils.html import escape
def render_input(request):
user_input = escape(request.GET['p'])
normalized_user_input = unicodedata.normalize("NFKC", user_input)
context = {'my_input': normalized_user_input}
return render(request, 'test.html', context)
用户的输入被escape函数转义,来防止XSS。假设输入的字符为%EF%B9%A4script%EF%B9%A5,在标准化后,就变成了<script>,从而引入了XSS漏洞。
Unicode试图统一世界上所有的语言,然而语言的种类太多了,这也就导致不同语言的字符有很大概率会使用同一个Unicode编码。例如,小写的土耳其字符ı(不带点)的大写字符是l;拉丁文字符i大写字符也是l,两个不同语言的小写字符的大写字符Unicode编码是一致的,这可能会导致漏洞利用。Django漏洞(CVE-2019-19844)产生的原理就是这个。
示例
from django.core.mail import send_mail
from django.http import HttpResponse
from vuln.models import User
def reset_pw(request):
email = request.GET['email']
result = User.objects.filter(email__exact=email.upper()).first()
if not result:
return HttpResponse("User not found!")
send_mail('Reset Password','Your new pw: 123456.', '[email protected]', [email], fail_silently=False)
return HttpResponse("Password reset email send!")
可以看到,检查电子邮件的时候,将email转换成大写,进行判断是否存在。攻击者现在可以简单地将foo@mıx.com作为email参数传递(其中i被土耳其语ı替换)。进行检查的时候,email参数被转换为大写,结果为[email protected]。这意味着用户存在,然后发送密码重置的电子邮件。但是密码重置邮件的收件地址仍然是土耳其语的email。换句话说,另一个用户的密码被发送到攻击者控制的电子邮件地址中。
Python<3.8中,ipaddress库在标准化ip地址的时候,会删除多余的0(见下面示例)。这种行为会导致绕过防御SSRF的黑名单IP列表,从而导致SSRF。
示例
import requests
import ipaddress
def send_request(request):
ip = request.GET['ip']
try:
if ip in ["127.0.0.1", "0.0.0.0"]:
return HttpResponse("Not allowed!")
ip = str(ipaddress.IPv4Address(ip))
except ipaddress.AddressValueError:
return HttpResponse("Error at validation!")
requests.get('https://' + ip)
return HttpResponse("Request send!")
如果传入的ip参数是127.0.00.1,则会被ipaddress标准化为127.0.0.1,而127.0.00.1不在SSRF黑名单中,从而绕过了检查。
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,传到后端,python解析到两个变量a和b,这种差异会导致很严重的安全漏洞,例如Djabgo著名的Web缓存中毒漏洞(CVE-2021-23336)。
https://blog.sonarsource.com/10-unknown-security-pitfalls-for-python
本公众号内的文章及工具仅提供学习用途,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本公众号及文章作者不为此承担任何责任。
原文始发于微信公众号(我不是Hacker):你写的Python程序安全吗?详解Python 10大鲜为人知的安全陷阱