pwn2win 2021 Ruthless Monster writeup
This is a co-work with my colleagues(a-z): Adam, Cao Wei and Tri
Challenge description
Please, help me! The bad guys are using this server to send secret evil files. Could you intercept one of these files to me?
Ps: The servers are rebooted every N minutes Ps2: The bad guys send the file every 15 seconds.
We were given a .tar.gz file, which contains server.zip which contains a Dockerfile and a php file.
Content of Dockerfile:
1 | FROM privatebin/nginx-fpm-alpine |
Content of html/exif/index.php:
1 | <!DOCTYPE html> |
RCE on exiftool
This line in Dockerfile caught my attention:
1 | RUN wget https://exiftool.org/Image-ExifTool-12.23.tar.gz && tar -xzf Image-ExifTool-12.23.tar.gz && rm Image-ExifTool-12.23.tar.gz &&\ |
exiftool version: 12.23, clearly is it referring to the recent exiftool rce, CVE-2021-22204. You can refer to the author’s blog post for the details.
The docker image is the prebuild privatebin, which is a zero-knowledge content sharing webapp, with additional feature provided by html/exif/index.php
examine html/exif/index.php reveals that it allows you upload .pdf files, with first five bytes of pdf magic header. Then it will call the vulnerable exiftool to parse it. we can’t control the filename on the server.
In the above blog post, the author mentioned that some other specific file types are also affected, which includes pdf filetype:
If a PDF uses the
DCTDecodeorJPXDecodefilters thenExtractInfowill be called on it.
1
2
3
4
5
6 if ($filter eq '/DCTDecode' or $filter eq '/JPXDecode') {
DecodeStream($et, $dict) or last;
# save the image itself
$et->FoundTag($tagInfo, \$$dict{_stream});
# extract information from embedded image
$result = $et->ExtractInfo(\$$dict{_stream}, { ReEntry => 1 });
So I write(ctrl+c/v) a script as follows:
1 | import com.itextpdf.io.image.ImageData; |
This will create a pdf file with embedded malicious jpeg file encoded with DCTDecode filter, however, I found that only when exiftool is supplied with extra flags -ee (which means to extract information from embedded files), the vulnerability will be triggered, default calling of exiftool won’t.

However, in the html/exif/index.php
1 | $target_file = $target_dir . "/". md5($_SERVER['REMOTE_ADDR'] .random_bytes (16)). ".pdf"; |
can’t inject additional flags, seems a dead end.
Yolo, I tried appendding the 5 magic bytes of the magic header in pdf to the head of the malicious jpeg file, and use exiftool to parse it, surprisingly, rce gets triggered:

seems exiftool is too smart to detect the actual file type, and decides to parse it as the actual type.
1 | # last ditch effort to scan past unknown header for JPEG/TIFF |
Until here, we can get a reverse shell on the server.
Try to get the post link
From the description, we know that there is probably a bot creating a post with the contents of flag using the privatebin webapp, according to its description:
PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data.
Data is encrypted and decrypted in the browser using 256bit AES in Galois Counter mode.
seems the content is encrypted on the client side, before sending to the server, and the key was generated on the client side as well, not being sent to the server. Even if we get a reverse shell on the server, we won’t able to recover the content.
However, there is another line in its description:
- A server admin might be forced to hand over access logs to the authorities. PrivateBin encrypts your text and the discussion contents, but who accessed a paste (first) might still be disclosed via access logs.
I misread it, and thought we might be able to find the full url to the post in the access log, so I went for access log. In /etc/nginx/http.d/access_log.conf:
1 | # Log access to this file |
the access_log is written to its standard output, not into a file.
We can get nginx pid in /run/nginx.pid first, then cat /proc/{pid}/fd/1, which will print out its stdout content:

we can only know that the bot’s user-agent is HeadlessChrome/91.0.4469.0
In the Dockerfile above, we can see that all the web files(except the posts newly created) are owned by root and not modifiable by the current user of reverse shell, so we would not be able to inject or modify the JavaScript file sent to the client.
Rouge Nginx/PHP-FPM
We later found that the process php-fpm are owned by nobody, which means we can kill it and start our own php-fpm process:

(actually we can also restart nginx as well, which seems to be easier)
The significance for us to restart the php-fpm is that we can specify our own php.ini in the newly created php-fpm process, where we can introduce a way of manipulating the content being sent to the client, the auto_prepend_file and the auto_append_file option. (at first, we were thinking about abusing the opcache.preload which will run the file once at server startup and persist through lifetime, quite similar concept, but I turned to auto_prepend_file and auto_append_file)
I tried to use auto_append_file option to include a php script which will echo out a javascript code to add event listener on the submit button of the privatebin webapp, so when the bot tries to post the flag, my javascript will retrieve the contents and send to my server. However, this was blocked by CSP policy
1 | Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-eval' resource:". Either the 'unsafe-inline' keyword, a hash ('sha256-VeOfkStrmuX0qULe4bluh5KaLDjso0zBKOOHWlrrM3g='), or a nonce ('nonce-...') is required to enable inline execution. |
My colleagues come up with an idea to call ob_start() to buffer the output in auto_prepend_file and modify the CSP header in the auto_append_file.
I choose the other way:
set up a fake privatebin webapp on my own server, inject the hijacking javascript code as follow:
1 | <script> |
and in the auto_prepend_file test.php, I wrote the following content:
1 |
|
Only when the bot(tell from the user-agent) visits the privatebin webapp, I will send a 302 response header to redirect it to my fake privatebin webapp, and when it enters the flag content, my javascript code will retrieve the content and send it to my collector.php


