CSCG: PDF Spy

This is my third writeup for the Cybersecurity Challenge Germany (CSCG) in 2022.

Category: Misc

Flag: CSCG{y0u_l34k3d_7h3_fl46_5ucc355fully}

Writeup

Welcome l33t haxx0rs!!!1 You were able to intercept an encrypted PDF with one of these precious CSCG flags. However, you don’t know the password and and your friend with some next level crypto analysis skillz told you, that bruteforcing the password is not possible.

Now the only thing left is to send the PDF to the CSCG admin via the Homepage down below, so the admins don’t notice, that one of their flags is missing…

Note: The admin knows the password for the file and will open the PDF to check the flag. As the admin is a bit lazy, he automated the flag checking with the following docker container: https://hub.docker.com/r/frankwolf/master-pdf-editor But, he is also very security-aware, so he disabled javascript in his masterpdf-editor when viewing the file, so no hacker can leak his secrets, or can he?

As the description tells us, we get an encrypted PDF file that contains the flag. Since we are told that the password is pretty strong, we need to interact with the online service in order to leak the flag, so I looked at how it works. When requesting the challenge website, we see a form that allows us to upload a PDF:

After the upload, we see a “success” page:

We are also given the source code for the web application. It contains a pretty standard Flask server (this is not a Web challenge after all). The most interesting part is how the server interacts with the Master PDF viewer:

def check_file(path):
    with lock:
        a = subprocess.Popen([
            "/opt/master-pdf-editor-4/masterpdfeditor4",
            str(path)
        ])
        time.sleep(1)
        subprocess.check_output(["xdotool", "type", PASSWORD])
        subprocess.check_output(["xdotool", "key", "Return"])
        time.sleep(3)
        a.kill()

It seems like the password is just typed blindly into the window. On encrypted PDFs, this is the “enter password” dialog:

Therefore, if we can upload a PDF that opens another dialog when opened, we get the password typed in. But how do we do that? As the challenge description says, JavaScript is disabled…

Enabling JavaScript

From the start, I knew that PDFs can contain forms. When playing around in Master PDF Editor, I noticed a few options to auto-submit forms at various events, such as page open. Also, there are a few interesting options where forms can be submitted (Document > Page Properties > Actions):

I tried HTTP first, but that only returns the PDF I submitted. Thinking about it some more, we can use Local File to overwrite arbitrary files with data from the PDF! After some time, I realized that we could overwrite the Master PDF Viewer configuration file at /root/.config/Code Industry/Master PDF Editor.conf. I found out that the PDF Editor does not crash when reading an invalid configuration, instead it ignores the failure. Therefore, by overwriting this file, we can enable JavaScript in the PDF Editor!

Leaking the password

Now that JavaScript is enabled, we can create a second PDF that executes some JavaScript on startup. This script should open a dialog where the password will be typed in. Afterwards, it has to send the password to us. I tried submitting a form via HTTP first, but I couldn’t get that method to work. Therefore, I submitted a form to a local file again, this time overwriting a Jinja2 template used by the Flask server, /app/templates/uploaded.html.

When trying this out, I noticed that when the target file already exists and is larger than the submitted form, Master PDF Editor does not overwrite the whole original file, buit only as many characters as are needed for the form. To get around Jinja2 rendering errors, I appended “Lorem Ipsum” to the password. I also needed to close a few angle brackets opened by the sunmitted form so the password is actually displayed in the browser. The final JavaScript looks like this (“password-field” is a form field in the PDF):

this.getField("password-field").value = ">>>>" + app.response() + "Lorem ipsum ...";

When opening the PDF file, the following steps are executed:

  1. The JavaScript is run and the “password-field” field is overwritten.
  2. The form is submitted to /app/templates/uploaded.html

When refreshing the uploaded page after a few seconds, we see the password AgJDLVK2KgjU6TMbBV8S45UZW87otmV7 in the template output:

We decrypt the PDF and get the flag CSCG{y0u_l34k3d_7h3_fl46_5ucc355fully} in a form field. Actually, we didn’t leak the flag, but the password :).

After solving the challenge, I noticed that many people modified the original encrypted PDF to auto-submit its form contents via HTTP. I didn’t go down this path since I didn’t feel like modifying the PDF by hand, I think both solutions have their strenghts. Overall, I found this challenge to be very fun since creative approaches as well as technical approaches could lead to a solution.

Mitigations