For the complete documentation index, see llms.txt. This page is also available as Markdown.
LFILINUXSSRFWERKZEUG

Plant Photographer

Dig deeper and try to uncover the flag hidden behind the scenes. - by munra & h4sh3m00

The following post by 0xb0b is licensed under CC BY 4.0


Scenario

Your friend, a passionate botanist and aspiring photographer, recently launched a personal portfolio website to showcase his growing collection of rare plant photos:

http://MACHINE_IP/

Proud of building the site himself from scratch, he’s asked you to take a quick look and let him know if anything could be improved. Look closely at how the site works under the hood, and determine whether it was coded with best practices in mind. If you find anything questionable, dig deeper and try to uncover the flag hidden behind the scenes.

Summary

Summary

In Plant Photographer we assess a self-built Flask portfolio application. Initial enumeration reveals a Werkzeug debug console protected by a PIN and an /admin endpoint restricted to localhost. Analysis o the /download feature exposes a server-side request forgery primitive with verbose error disclosure, leaking sensitive internal details including API keys, filesystem paths, and application structure. By abusing the file:// protocol and bypassing forced path concatenation with a URL-encoded fragment, we achieve arbitrary local file read, extracting /etc/passwd, application source code, and private documents.

Using this access, we gathered the specific hardware and environment identifiers required to reverse-engineer the Werkzeug debug PIN. After accounting for an older MD5 hashing implementation, we successfully unlocked the console, gaining remote command exection on the target.

Recon

Based on the scenario, we were instructed to take a close look at the client's website. We'll skip the port scan for now. At first glance, the website appears static. However, using WappAlyzer, we can see that we are dealing with a Python Flask server here.

Next, we perform a directory scan using Feroxbuster and find a conssoleand admin page to be interesting.

When we visit the console page, we see the Werkzeug/ Flagsk Debug console available locked by a PIN. If we were able to determine the public and private bits, we could reconstruct the PIN and thereby achieve remote code execution.

The Admin interface is locked for us. It is only available from the localhost. Unfortunately, attempts to bypass the restriction using headers such as X-Forwarded-For: localhost were unsuccessful.

While browsing manually, we have BurpSuite running but interception is disabled. We monitor the traffic in the HTTP History, but don't find anything there. We enable interception. And download the customer's resume.

The following request is made:

The server parameter is used to specify another server and an ID. In this case, the server sends a request to an internal server. We might be able to exploit this to carry out a server-side request forgery. However, enumerating the internal ports yielded no results. Changing the server parameter to one of our web servers or to localhost didn't help either.

Verbose Error Disclosure Leaks Sensitive Information

We're approaching the download endpoint manually for now and changing the port. And we get a pycurl error message.

The error message is very verbose and reveals the API key needed to retrieve files from the secure storage service. This is also the first flag.

In addition to the API key, we also record the following information. We now know the location of the webroot:

Where ther public documents stored:

We also find out which version of Python is being used:

Path Traversal & Sensitive File Disclosure via SSRF

For now, we want to stick with the Werkzeug exploit to achieve remote code execution. As mentioned earlier, we need public and private bits to reconstruct the PIN. However, we can only obtain these if we can read internal files on the server or if further information is leaked.

We try to leverage the server request paramater to read local file like this using the file:// protocol:

We see that the sserver actually does accept the file:// protocol by the error message Couldn't open file /etc/passwd/....

But the backend script is forcefully appending the internal directory and a file extension to whatever you input in the server parameter.

Because /etc/passwd is a file, not a directory, pycurl fails when it tries to look "inside" it for that subdirectory. To fix this, we add a URL Fragment (#) or a Null Byte to trick the parser into ignoring everything that follows our desired path. We URL encode the # to %23 and are able to read the /etc/passwd file.

Next we include the app.py. We discovered the path previously from the verbose error message.

We are now able to examine the functionality of the site at the source code level. In addition to the API key, we now also see a private-docs folder next to the public-docs folder; this folder is accessed when /admin is called, and the flag.pdf file is retrieved.

Next we try to include the flag.pdf via the file inclusion vulnerabiltiy...

... and are able to read the second flag.

Werkzeug Debug PIN Reconstruction → Remote Code Execution

Public bits

We will now continue extracting the private and public bits so that we can finally generate the Werkzeug console PIN. For reference, we will use the article from HackTricks.

This is a sample excerpt from the script in the Hacktricks wiki entry, including its probably_public_bits and private_bits.

Of the public bits, the only one we're currently missing is the username. We know that Python 3.10 is being used. For now, we'll assume the rest.

We request the /proc/self/environ file to determine the current user running. From the HOME variable, we can determine the user root indicated by HOME=/root.

For the public bits we assume now the following:

Private bits

We continue with the private bits, those include the mac address and machine-id.

First, we need the decimal expression of the mac address of the system. We get the MAC at file:///sys/class/net/<device id>/address. To get the device id we query for the file /proc/net/arp, its eth0.

Next, we query for /sys/class/net/eth0/address to get the MAC address.

To convert the mac address, we use the following resource, and chose the EUI-48 representation:

Next, we query the machine-id at /etc/machine-id. But the file is not found.

The following explanation from HackTricks says that the function get_machine_id() for the PIN generation concatenats the data from /etc/machine-id or /proc/sys/kernel/random/boot_id with the first line of /proc/self/cgroup post the last slash (/).

So we query for the machine ID with the alternative approach /proc/sys/kernel/random/boot_id.

Next, we query for the of /proc/self/cgroup.

We should have everything ready now. We set the public and private bits in the script. We receive a PIN and realize that it doesn't work.

There is a slight hint in the article.

This script produces the PIN by hashing the concatenated bits, adding specific salts (cookiesalt and pinsalt), and formatting the output. It’s important to note that the actual values for probably_public_bits and private_bits need to be accurately obtained from the target system to ensure the generated PIN matches the one expected by the Werkzeug console. If you are on an old version of Werkzeug, try changing the hashing algorithm to md5 instead of sha1.

Just to be sure we query for the script on the machine used. And see md5 is used instead of sha1.

To summarize everything again, here are our public and private keys. And the script uses MD5 instead of SHA1. And we find that the cgroup is sufficient as a specification

Exploit

The script now looks like this.

We run the script and receive a PIN.

The PIN is vali and we can finally execute commands on the system to retrieve the final flag.

Last updated