| 6 min read
Switching from blind SSRF to local file read. Have you ever thought about it? Browsing the Internet, I found a third-party library called Intervention Image. It is a PHP image management and manipulation library that provides a simple interface for loading, storing and editing images.
The disturbing thing is that, while reading about this library, I noticed that it was referred to as "vulnerable by default." To understand it better, I wanted to exploit it, and what better way than in the real world?
Vulnerability
I searched on GitHub which sites used Intervention Image. I then found a repository that had a lot of interaction and good stars, so I decided to work with that one. The repository in question is BookStack. The vulnerability I’ll demonstrate is in version 23.10.2.
I will now show you the exploitation path from the source to the sink. This way you will understand the unexpected payload we will use to exploit this vulnerability.
From source to sink
On an account with writer permissions, you can create books and, within them, fill your pages. Such pages accept Markdown and HTML code. I show that process in this video:
This is how that request looks like:
When we upload HTML (in this case) through a page, that HTML is handled here:
class PageContent { [...] public function setNewHTML(string $html): void { $html = $this->extractBase64ImagesFromHtml($html); // HERE $this->page->html = $this->formatHtml($html); $this->page->text = $this->toPlainText(); $this->page->markdown = ''; } protected function extractBase64ImagesFromHtml(string $htmlText): string { [...] $doc = $this->loadDocumentFromHtml($htmlText); [...] $xPath = new DOMXPath($doc); // Get all img elements with image data blobs $imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]'); foreach ($imageNodes as $imageNode) { $imageSrc = $imageNode->getAttribute('src'); $newUrl = $this->base64ImageUriToUploadedImageUrl($imageSrc); // HERE $imageNode->setAttribute('src', $newUrl); } [...] } protected function base64ImageUriToUploadedImageUrl(string $uri): string { $imageRepo = app()->make(ImageRepo::class); $imageInfo = $this->parseBase64ImageUri($uri); // Decode Image Data [...] try { $image = $imageRepo->saveNewFromData($imageName, $imageInfo['data'], 'gallery', $this->page->id); // HERE } catch (ImageUploadException $exception) { return ''; } return $image->url; } protected function parseBase64ImageUri(string $uri): array { [$dataDefinition, $base64ImageData] = explode(',', $uri, 2); $extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? ''); return [ 'extension' => $extension, 'data' => base64_decode($base64ImageData) ?: '',
];
}
}
In the previous fragment, we see the methods involved in handling the HTML that we send to the application through a page of a book. Of all these methods, there is one that is of special interest, which is saveNewFromData
. Let's look at it:
class ImageRepo { public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image { $image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo); $this->imageResizer->loadGalleryThumbnailsForImage($image, true); // HERE
return $image;
}
}
From this code fragment, we are only interested in loadGalleryThumbnailsForImage
. Let's examine it now:
class ImageResizer { public function __construct( protected ImageManager $intervention, // Intervention HERE protected ImageStorage $storage, ) {} public function loadGalleryThumbnailsForImage(Image $image, bool $shouldCreate): void { $thumbs = ['gallery' => null, 'display' => null]; try { $thumbs['gallery'] = $this->resizeToThumbnailUrl($image, 150, 150, false, $shouldCreate); $thumbs['display'] = $this->resizeToThumbnailUrl($image, 1680, null, true, $shouldCreate); } catch (Exception $exception) { // Prevent thumbnail errors from stopping execution } $image->setAttribute('thumbs', $thumbs); } public function resizeToThumbnailUrl( Image $image, ?int $width, ?int $height, bool $keepRatio = false, bool $shouldCreate = false ): ?string { [...] // If not in cache and thumbnail does not exist, generate thumb and cache path $thumbData = $this->resizeImageData($imageData, $width, $height, $keepRatio); // HERE [...] } public function resizeImageData(string $imageData, ?int $width, ?int $height, bool $keepRatio): string { try { $thumb = $this->intervention->make($imageData); // Vulnerability Here
} catch (Exception $e) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
}
[...]
}
}
Finally, we have arrived at the sink, this is where we pass the base64 encoded data that we have sent in the src
of the img
tags in the HTML of the book page. We know that we have absolute control of the string that is passed to this method but... what can we do with that?
Exploit path
Remember I told you that intervention->make
is vulnerable by default? Well, I want you to see why with your own eyes:
class ImageManager { public $config = [ 'driver' => 'gd' ]; public function make($data) { return $this->createDriver()->init($data); } private function createDriver() { [...] if ($this->config['driver'] instanceof AbstractDriver) { return $this->config['driver'];
}
[...]
}
}
namespace Intervention\Image\Gd;
class Driver extends \Intervention\Image\AbstractDriver
{
[...]
}
abstract class AbstractDriver { /** * Decoder instance to init images from * * @var \Intervention\Image\AbstractDecoder */ public $decoder; public function init($data) { return $this->decoder->init($data);
}
}
abstract class AbstractDecoder { public function initFromUrl($url) { $options = [ 'http' => [ 'method'=>"GET", 'protocol_version'=>1.1, // force use HTTP 1.1 for service mesh environment with envoy 'header'=>"Accept-language: en\r\n". "User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36\r\n" ] ]; $context = stream_context_create($options); // Pwned Here if ($data = @file_get_contents($url, false, $context)) { return $this->initFromBinary($data); } throw new NotReadableException( "Unable to init from given url (".$url.")." ); } public function isUrl() { return (bool) filter_var($this->data, FILTER_VALIDATE_URL); } public function init($data) { $this->data = $data; switch (true) { [...] case $this->isUrl(): return $this->initFromUrl($this->data);
[...]
}
}
}
The make
method can receive various types of data. Fortunately, it accepts URLs, which indicates that our input is valid.
string - Path of the image in filesystem.
string - URL of an image (allow_url_fopen must be enabled).
string - Binary image data.
string - Data-URL encoded image data.
string - Base64 encoded image data.
resource - PHP resource of type gd. (when using GD driver)
object - Imagick instance (when using Imagick driver)
object - Intervention\Image\Image instance
object - SplFileInfo instance
Now that you have the exploit path, you can clearly see how we have gone from $this->intervention->make($imageData);
to @file_get_contents($url, false, $context)
. We have complete control of the URL. This means we can perform SSRF attacks to interact with internal resources, etc.
However, it would be great if we could escalate this. Fortunately, there is a technique to filter the contents of arbitrary files using the php://
wrapper even if the output of the file read is not given to the user. This technique is called Blind File Oracles and was first discovered in DownUnderCTF 2022.
Summarizing, with a simple modification of the script php_filter_chains_oracle_exploit we can use the technique to filter the content of any file on the server.
Exploitation
The above script works with urlencode-based requests. Our case is different, because the requests are sent in JSON format. So, I simply changed the encoding of the request to urlencode, and it worked. That is perfect because it simplifies a lot the work and time spent to modify the script.
It is important to note that the exploit can be sent to either of these two endpoints in the parameter html
:
/BookStack/public/ajax/page/7/save-draft
/BookStack/public/books/books/book/draft/7
In my case I preferred to do it with the first endpoint. The first endpoint is a temporary save, and the second endpoint is when we save the page as such. I recommend before using the script, that you verify from Burp Suite if the input actually reaches the sink, because it may not happen due to internal cache rules.
Finally, we only have to assign the necessary cookies to the script, the path, the HTTP verb, the file we want to read, and wait for the result. Watch me execute the script here:
In the following screenshot, you can see that I got the file /etc/passwd partially leaked:
We assigned this vulnerability the CVE ID CVE-2023-6199 and a CVSS score of 7.1. Read our advisory here.
No fix available yet but here’s a workaround
There is currently no patch for Intervention Image addressing its vulnerability. However, if you are using this library, the best way to ensure you are not vulnerable is by never passing user data directly into the constructor. If you want to turn an upload into an image, pass the file path to the uploaded tempfile instead.
Conclusion
As always, extreme curiosity leads me to delve into applications to such an extent that I discover new ways to exploit vulnerabilities or to find rather peculiar sinkholes. In this blog post, we have seen how being able to edit HTML or Markdown content can lead to more critical vulnerabilities such as an arbitrary file read (in this case), instead of the typical cross-site scripting (XSS).
Remember that at Fluid Attacks we offer a solution to search for security vulnerabilities in software continuously. Secure your applications in a 21-day free trial of our automated security testing. You can upgrade at any time to include assessments by our hacking team.
Recommended blog posts
You might be interested in the following related posts.
Be more secure by increasing trust in your software
How it works and how it improves your security posture
Sophisticated web-based attacks and proactive measures
The importance of API security in this app-driven world
Protecting your cloud-based apps from cyber threats
Details on this trend and related data privacy concerns
A lesson of this global IT crash is to shift left