LFR via SSRF in BookStack

Beware of insecure-by-default libraries!

Blog LFR via SSRF in BookStack

| 6 min read

Contact us

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?


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:

"Request when creating book and page"

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?

Get started with Fluid Attacks' Ethical Hacking solution right now

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.


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:


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:

"File 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.


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.

Subscribe to our blog

Sign up for Fluid Attacks' weekly newsletter.

Recommended blog posts

You might be interested in the following related posts.

Photo by Towfiqu barbhuiya on Unsplash

Ensuring compliance and security in the banking sector

Photo by Andre Taissin on Unsplash

With great convenience comes increased risk

Photo by FlyD on Unsplash

Software supply chain management in financial services

Photo by Robs on Unsplash

Consequential data breaches in the financial sector

Photo by Towfiqu barbhuiya on Unsplash

Data protection in the financial sector, tips and more

Photo by Christian Wiediger on Unsplash

The need to enhance security within the fintech sector

Photo by Claudio Schwarz on Unsplash

Is your financial service as secure as you think?

Start your 21-day free trial

Discover the benefits of our Continuous Hacking solution, which hundreds of organizations are already enjoying.

Start your 21-day free trial
Fluid Logo Footer

Hacking software for over 20 years

Fluid Attacks tests applications and other systems, covering all software development stages. Our team assists clients in quickly identifying and managing vulnerabilities to reduce the risk of incidents and deploy secure technology.

Copyright © 0 Fluid Attacks. We hack your software. All rights reserved.