A Series of Unfortunate Images: Drupal 1-click to RCE Exploit Chain Detailed

April 11, 2019 | Vincent Lee

Recently, Drupal released a pair of critical patches for supported 7.x and 8.x versions. Included in the update is a set of bugs were originally submitted as a contender to the our ongoing Targeted Incentive Program. Code execution through these bugs is possible, but an attacker must first upload three malicious “images” to the target server and entice an authenticated site Administrator to follow a crafted link to achieve code execution. Not exactly the smoothest exploitation path, making it ineligible for a TIP award. However, the bugs were good enough for a targeted attack, so we purchased these wonderful bugs through the normal ZDI process. Here’s a video of the bugs in action:

Two bugs were chained to achieve 1-click to code execution. They are ZDI-19-130, and ZDI-19-291 discovered by Sam Thomas (@_s_n_t). Image upload can be achieved easily by uploading a profile picture when registering for a user account or uploading an image in a comment. Naturally, Drupal sites that disabled user registration and user comments are not susceptible to these attack vectors, but we urge you to update your Drupal server to the latest version regardless. 

ZDI-19-130 is a PHP deserialization bug that gets us from site Admin to RCE, and ZDI-19-291 is a persistent cross-site scripting vulnerability that the attacker can exploit to force the administrator to make a malicious request to trigger ZDI-19-130

The mechanics of ZDI-19-130 is based on the works Thomas presented at Black Hat earlier this year (PDF presentation). You can find the accompanying white paper (PDF). You can watch the video archive of the same Black Hat talk he has given at BSidesMCR here. In the presentations, Thomas detailed his discovery of a new vector to triggering PHP deserialization vulnerabilities through Phar archives. In essence, the metadata of a PHP Phar archive is stored in form of a PHP serialized object. File operations on Phar archives can trigger unserialization() on the stored metadata which can eventually lead to code execution.

On the other hand, ZDI-19-291 is a Perl Compatible Regular Expression (PCRE)-related vulnerability in the handling of uploaded filenames. When a user uploads a file, Drupal uses PRCE to modify to file name to avoid name duplication. A well intentioned commit containing a subtle PCRE bug has been residing in the code base for the past 8 years and can cause Drupal to drop the file extension of the file name when uploaded multiple times, thus allowing an attacker to upload arbitrary HTML files.

A Brief History - PHP Object Injection: The Origin

Back in 2009, the same year the iPhone 3GS was released, Stefan Esser (@i0n1c) demonstrated PHP unserialization process is vulnerable to Object Injection and can be further exploited via code reuse technique similar to ROP (PDF presentation). He later coined the term Property Oriented Programming (PDF presentation). Prior to his demonstration, PHP object unserialization bugs were mostly denial of service or hard to exploit memory corruption vulnerabilities.  

Similar to the debut of ROP, POP chain building was manual and tedious. Not many tools or literature were available. The only literature I am aware of is the remarkable paper on automated POP Chain generation by Johannes Dahse, et al, released in 2014 (PDF). Sadly they never released their tool to the public.

A Brief History - POP Exploitation Commoditized

Enter PHP Generic Gadget Chains (PHPGGC), which was released in July 2017. The PHP Generic Gadget Chain library can be thought of as analogous to the ysoserial Java deserialization vulnerability payload library. With the rise in popularity of PHP frameworks and libraries, and the help of the PHP autoload feature, PHP unserialization vulnerabilities have finally became trivial to exploit.

The Exploit - First stage: ZDI-19-291

Consider the following PHP snippet written to test part of the Drupal source code. According to the comments in the source, the following snippet attempts to strip the filename of control ASCII characters with a value less than 0x20 and replace them with the underscore (‘_’) character. The “/u” pattern modifier causes the PHP engine to treat the PCRE pattern and subject strings as UTF-8 encoded. Presumably, this modifier was added to the PCRE pattern to ensure UTF-8 compatibility.

Side Bar: UTF-8

Contrary to popular misconception of UTF-8 characters being two-byte wide, a valid UTF-8 code point may be one to four bytes long. UTF-8 is designed to be backwards compatible with ASCII character set. Therefore, at the one-byte code point range, the ASCII (octet 0x00 to 0x7F) and UTF-8 definitions intersect. The 0x80 to 0xF4 octets are used for multi-byte UTF-8 code point encoding. According to RFC3629, octet values of C0, C1, F5 to FF never appear in a valid UTF-8 string.

The Exploit - First stage: Test results

With the \xFF byte being invalid, and \x80 byte present without a valid leading byte, PHP throws the PREG_BAD_UTF8_ERROR and the $basename variable is set to NULL per documentation.

In the Drupal source code, no check for errors is performed after the preg_replace() call. When an image with a file name containing an invalid UTF-8 character is uploaded to Drupal twice, the function will run with the $basename variable being loosely treated as an empty string. At the end, the function returns $destination, which is effectively set to result of ’_’.$counter++.

With this primitive, an attacker can upload a GIF profile picture to the Drupal website through user registration and cause it to have its extension dropped. Drupal will now serve the image at:


instead of:


Although checks are performed on the uploaded profile picture, prepending the characters “GIF” on an HTML file with “.gif” extension is sufficient to satisfy the checks.

Another way to upload the malicious GIF file is through the comment editor. In this case, the image will be served at /sites/default/files/inline-images/_0. However, the attacker will need to register a user account prior to commenting on a post in a default Drupal installation.

When this behavior is combined with the fact that the images are normally served without any Content-Type header, an attacker can upload malicious GIF/HTML files onto the Drupal server and trick a browser into rendering the files as HTML webpages by linking them with a content type hint. Here’s an example:

All in all, an attacker can achieve persistent XSS on the targeted Drupal site. The attacker may exploit this vulnerability to force a user with Administrator privilege to make a malicious query for the second stage by enticing the logged in user to follow a malicious link. You can find an executable PoC here.  

The Exploit - Second Stage: ZDI-19-130

ZDI-19-130 is an unserialization bug triggered through the file_temporary_path request parameter located at the /admin/config/media/file-system endpoint. An attacker may specify the phar:// stream wrapper to point the file_temporary_path parameter to a malicious Phar archive uploaded to the Drupal server prior to the attack.

The system_check_directory() function below is the form callback function that handles the request. From Thomas’ presentation, the file operation !is_dir($directory) is sufficient to cause PHP to trigger unserialization of the metadata stored in the Phar archive. Through the POP chain exploit technique, an attacker may use a crafted Phar archive to execute arbitrary code in the context of the web server.

The Exploit - Second Stage: The Polyglot

Prior to exploiting ZDI-19-130, we must upload a Phar archive onto the target. This can be achieved by uploading a JPEG/Phar polyglot file as the user profile picture during user registration. Below is a PoC JPEG/Phar polyglot file that will execute the cat /etc/passwd command on the victim machine when used in conjunction with a ZDI-19-130 exploit.


Much like JAR files, Phar archives are a collection various components packaged into a single archive file. In the PHP specification, different archiving formats can be used to package up the components. In the context of the exploitation, the TAR-based Phar archive is used.

To create the polyglot file, the attacker must first select a JPEG image vector. The malicious TAR-based Phar archive is then stored entirely in a JPEG comment segment located close to the beginning of the JPEG file. The Start of Image JPEG segment markers and Comment segment markers will slightly corrupt the first file name when interpreted as a TAR archive. When the TAR file checksum is fixed up, this slight corruption is inconsequential to the functionality of the exploit as long as the first file stored in the TAR/Phar archive does not correspond to the Phar metadata component file containing the POP chain payload.

Putting it all together

To recap, the attacker must first upload the ZDI-19-130 JPEG/Phar polyglot image file to the target and determine the location of uploaded image. Then, the attacker must upload the ZDI-19-291 GIF/HTML image XSS twice to cause image file to be stored on the server without an extension. Finally, the attacker must entice the site administrator to navigated to the ZDI-19-291 GIF/HTML image hosted on the target server through a link with a proper content type hint to cause the browser to render the image as HTML page and fire off the second part of the exploit. If everything works out, the attacker will be able to achieve code execution as the web server and be greeted with a reverse shell as shown in the demo video above.


Thomas has demonstrated a novel attack vector that can open up many doors for attackers. Unless PHP decides to modify the Phar archive processing behavior, programmers will need to be extra vigilant when passing user-controlled data through file operators. As passing user-controlled data into innocent file operators such as is_dir() were never considered high risk, we expect to see more vulnerabilities utilizing this vector to emerge. With the advance in POP chain exploitation toolset, PHP unserialization vulnerabilities are becoming trivial to exploit. Software vendors should view this as the last nail for serialize() and start migrating towards the safer json_encode() alternative.

While not a TIP winner, these are great bugs and fascinating research. If you are interested in the TIP initiative, be sure to check our blog often as new targets will be added as others drop off. With over $1,000,000 USD available in total awards, you may find an area deserving of some extra attention.

Until then, you can find me on Twitter at @TrendyTofu, and follow the team for the latest in exploit techniques and security patches.