Recently during some personal research I came across a very interesting avenue for remote code execution (RCE). The program let you provide one argument to the command line before appending some flags after. Now it wasn’t “one” escapable argument, but was actually only one argument much to my disappointment.
Due to this, we are unlikely to LOLBIN our way out of the situation. So instead, let’s talk about a tale of bypassing theoretical file validation within some file upload and how to convince Linux to then run arbitrary code via said file when the rough following command is typed into a shell!!
<file name> --program --provided --flags
This won’t cover things like least significant bit or more complicated steganography. It’s more so about straight files, bypassing magic byte checking and some unix nuances.
Further to this, the payloads shown will be executed in a bash context as this is what most servers run. You could modify the payloads shown to work with other shells I’m sure. The content in this blog has been tested on the following:
bash
- Worksdash
- Workssh
- Workszsh
- Does not work
Disclaimers:
- For the purposes of this blog post and chosen ’exploit path’, we assume all files are written to disk with executable permissions. In the real world you’d need something like unzip on upload or another vulnerability to chain with this for execution in our context. Uploading a file with the execution bit set is also included in this blog as a later step.
- The techniques listed here bypass file renaming defense in depth measures as the exploit we are targeting is within the file & extension, not the file name itself.
Extension faking Link to heading
Let’s imagine the server is checking the extension to ensure it ends with .png
in an attempt to limit content being uploaded. So knowing that, how do we ‘fake’ a file in the eyes of a server, yet still have enough remaining context for execution?
Well as it turns out, when attempting to execute a file unix gives preference to shebangs in files. This is quite the functionality when it comes to exploiting file upload functionality, sadly it’s also easy to pick up.
Imagine we have a file called cat.png
with the following content:
#!/usr/bin/python3
print("Hello world")
When one calls the file with ./cat.png --program --provided --flags
(as per our execution context from earlier), unix will happily go and ship the file out to the Python executable to run our program. How good!
Using this, we now have an arbitrary code file at our disposal including all the preinstalled Python packages. Establishing a persistent presence should be simple from here.
But oh no! The server does magic byte checking Link to heading
Well let’s see, the server is now checking the following criteria are met:
- That the file ends with
.png
- That the magic bytes of the file is a PNG using something like python-magic
Well dayum our original file magic bytes as a Python file! That’s not going to cut it.
Well, luckily for us another fun quirk of unix is that we can simply embed valid bash commands within a file and if a file is executed without a program prepended to the command then the file is executed within the context of the current shell. What that means for us is that given we get to provide one word to execute; the file in this case; our shell will assume that we are simply asking it to run the program. Essentially changing ./cat.png
to sh ./cat.png
in the background for us.
This means we can embed arbitrary commands within a ‘valid image’ and have them execute while bypassing file checks. For example, imagine we have the following file titled rev.png
. In this file, we have the following content:
Or as hex:
Or within nano:
Note the raw file can be downloaded here.
Now this file contains some basic code to construct a reverse shell with the ncat server running at localhost:3456
. Further to this, the file continues to maintain its innocence to the server by meeting the following criteria:
- The file name of
rev.png
is plausibly within the allow list for an image upload site - The mime type of our file is returned as a PNG still despite the malicious code. This is because it contains the relevant magic bytes expected of PNG files. This can also be seen in the image below:
A quick seqway, why does this work? Link to heading
As mentioned at the start, this method of execution has been tested on the following shells:
bash
- Worksdash
- Workssh
- Workszsh
- Does not work
To me, it appears the reason this works is that bash
ignores syntax errors. Essentially it goes “Huh that’s odd, this line is wrong. That’s okay though, maybe I can still run the next line!”.
Meanwhile, a shell such as zsh
goes “Huh that’s odd, this line isn’t valid. I reckon I ought to stop and tell the user!”.
Getting past metadata stripping / file re-encoding Link to heading
Sometimes file servers may do some of the following on top of basic checks:
- Check the file to ensure the file is well-formed without syntax warnings in the expected structure
- Convert the file to a format such as bmp, before back to png
We can effectively bypass the first scenario, I haven’t managed to find a way to bypass the second yet.
For example, if we review the exif data of our earlier file; rev.png
; we can see a warning about corruption within the file:
So to counter this, we actually need to embed our payload within a valid PNG image. Like fully valid, fully formed PNG. We can find the spec here but things like a CRC (Cyclic Redundancy Check) for chunks looks remarkably complicated. Let’s just use a tool instead :D
So instead of doing a lot of the hard work, I simply found a Python tool made for Python 2 and ported it to Python 3 (and added some features like newline payload support). It goes by PCRT (PNG Check & Repair Tool) and can be found here.
Using a command such as the following we are able to inject a valid bash command into a PNG file without failing the aforementioned check. I’d like to note here that test.png
is just a simple, correct PNG with no modifications.
python PCRT.py -i test.png -o output.png -p "\necho 'Hello world'\n"
Using the same method as prior, we can see it now passes further PNG checks.
Now, when we execute the file we still receive command injection (hidden amongst the bash errors).
If you wanted to use this for malicious purposes, you could modify the payload to follow in the lines of the reverse shell from the earlier section.
Getting a file onto a server with the executable bit set Link to heading
A key part of getting execution also relies on your ability to get the executable bit set on your file. We need to do this as ./<file>
will not execute otherwise. So how can we achieve this? Well the answer is via file types such as zip
.
Note if your command execution channel does sh <user input> --program --flags
or similar you can ignore this section.
When uploading a zip file, permissions may be retained for the files within the zip. Now not all libraries maintain permissions, but an example of the vulnerable path can be seen in the flask app below.
import os
import secrets
import subprocess
from pathlib import Path
from flask import Flask, request, flash, redirect, url_for
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = "./uploads"
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "zip"}
app = Flask(__name__)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/", methods=["GET", "POST"])
def upload_file():
if request.method == "POST":
if "file" not in request.files:
flash("No file part")
return redirect(request.url)
file = request.files["file"]
if file.filename == "":
flash("No selected file")
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secrets.token_urlsafe(4) + "." + file.filename.rsplit(".", 1)[1]
zip_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
file.save(zip_path)
if filename.endswith(".zip"):
# Unzip the files and delete the zip itself
subprocess.run(["unzip", zip_path, "-d", UPLOAD_FOLDER])
Path(zip_path).unlink()
return redirect(url_for("upload_file", name=filename))
return """
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
"""
if __name__ == "__main__":
app.run()
Note that this code ignores other best practices in order to remain short for the sake of this blog
With the output file maintaining the required permissions:
So if the application supports compressed uploads, there is a decent chance you can smuggle bits onto the servers files.
Further to this, even if the application conducts the relevant checks against each file extracted, this method will still bypass those checks.
End result Link to heading
Combining all of this together, we can now create files which do the following:
- Bypass server side file extension checks by embedding payloads in arbitrary file types
- Bypass server side magic byte checking by embedding payloads in otherwise ‘valid’ files
- Bypass server side file structure checks
- Upload files with the execution bit set on to the application server
And on top of this, the files may contain arbitrary payloads.
✨ Woo much success ✨
You can then use these payloads on the underlying server or for further distribution if publicly hosted etc. The possibilities of a bypass like this may be nearly limitless given a specific use case for the files.
Further to this, as we have arbitrary bash payloads we are able to obfuscate the payloads using the various utilities available to the shell on the system to further hide our activities from pesky processes which attempt to conduct security checks.
A short side track to OWASP Link to heading
So let’s discuss what we have achieved in the context of the OWASP File Upload Cheat Sheet in a PNG mindset, as well as the recommendations they provide. Specifically, let’s look at the “File Upload Protection” section which lists the following high level items:
- Extension Validation:
- Ensure only allowed extensions can be uploaded.
- Content-Type Validation:
- While they discuss how spoofable this is, they also mention it can be a useful ‘quick check’.
- File Signature Validation:
- While also easily spoofable, this is another quick check to do.
- Filename Sanitisation:
- TLDR rename file names to random strings.
- File Content Validation:
- For images, apply image rewriting techniques.
Now there are a few others, but they aren’t really relevant to our discussions. Now, I’ve tested a few websites in my time and I don’t think most of them do all of this. Do yours?
Anyway, let’s think. We can bypass all bar one of these requirements. Shesh, now that’s impressive. Maybe one day I’ll find a way around image rewriting techniques as well!
Regardless, this has been a fun bunch of research and I hope you’ve learnt a thing or two. Happy hacking and good luck with your next file validation bypass!
Kudos Link to heading
Cheers to Jim for his input regarding execution bits and the context required. Other kudos goes to the HLA crew for all their input into the research throughout its evolution.