You’ve found a portal for a firmware upgrade service, responsible for the deployment and maintenance of rogue androids hunting humans outside the tractor city. The question is… what are you going to do about it?

Category: Web

Solver: rgw, nh1729, n1k0

Flag: HTB{i_slipped_my_way_to_rce}


In this web challenge, we get a docker template of a flask web server. The user can upload .tar.gz archives which are extracted into a temporary directory on the server. Then, the structure is moved to a static directory to be presented to the user. The flag can be found in a file on the system.

Our first ideas revolved around injecting absolute paths into archive files to hijack the process of copying the files to the static directory. The idea being to move flag to the static directory.

After refining our process, we used the following command to create a malicious archive:

tar --transform='flags=r;s|testFile|/app/challenge/flag|' -P -czf test.tar.gz app testFile

This should create an archive containing a file at the absolute path /app/challenge/flag, with the intention to move the flag into our readable directory. (Don’t be irritated by the wrong path. We also noticed this mistake at some point. But let’s revisit all of our mistakes in their order.)

This worked in the sense that we could access the contents of /app/challenge/flag, with the minor inconvenience of having overwritten it when our file was extracted to /app/challenge/flag.

After this setback, we tried simple directory traversing with curl, also not successful. Neither was using symlinks in the .tar.gz to the flag, as those are treated exactly like directories by the code moving the files. At this point, in an act of desperation, we hoped we could prevent the code to overwrite the flag by supplying an empty file with its absolute path in the archive. Also didn’t work.

Then, one of our team members had the brilliant idea to use our ability to write files by packing them into archives with absolute paths to overwrite the index.html template to serve us the flag. Therefore, we constructed the following command:

tar --transform='flags=r;s|index.html|/app/application/templates/index.html|' -P -czf test2.tar.gz app index.html

Despite being a pretty promising attempt, this also caused the template to be moved into another static directory, leaving us with a TemplateNotFoundError of the framework. Nothing restarting the challenge can’t fix.


After we did not found any way to exploit the tar files, we had a closer look at the lines before where the uploaded file is stored in a temporary directory.

tmp = tempfile.gettempdir()
path = os.path.join(tmp, file.filename)

As we planned to overwrite the index.html template, we modified the POST data to write to the template directory:

Content-Disposition: form-data; name="file"; filename="/app/application/templates/index.html"

To do that, we uploaded a file using Firefox and then edited and resent the form data in the developer tools’ network tab. We quickly confirmed that the overwrite succeeded by replacing the version string in index.html. Then we looked into template injection. On [1] we found an injection for Jinja2:

{{"/etc/passwd").read() }}

We set the path to point to the flag. We initially assumed that the flag is in /app/challenge/flag. Unfortunately, this was wrong and we crashed our server as index.html now responded with an error message that the file cannot be found. We shut down our container again to restart. After revisiting the Dockerfile, we saw that the flag is actually in /app/flag. So we finally inserted the following into the index.html template:

{{"/app/flag").read() }}

After reloading the main page, we get the flag printed.


Other resources

[1] Side Template Injection#jinja2—read-remote-file