Le module pickle de Python est non sécurisé par conception pour les entrées non fiables : le format est un petit langage à pile que l'unpickler exécute, et un objet peut déclarer, via __reduce__, un callable et des arguments à exécuter au moment du chargement. Contrairement à PHP ou Java, il n'y a pas de chasse à la chaîne de gadgets ; pickle.loads() sur des octets de l'attaquant est une exécution de code directe et immédiate.
La primitive reduce#
__reduce__ renvoie un callable et un tuple d'arguments ; l'unpickler l'appelle pendant la reconstruction. Une classe de payload nomme os.system (ou subprocess, builtins.eval, builtins.exec) comme callable :
import pickle, os, base64
class RCE:
def __reduce__(self):
return (os.system, ('id',))
payload = base64.b64encode(pickle.dumps(RCE()))
print(payload.decode())
Tout ce qui appelle ensuite pickle.loads(base64.b64decode(payload)) exécute id. Sinks courants : blobs de session ou de cache, valeurs Authorization/cookie, payloads de file de messages, fichiers de modèle ML, et toute API qui accepte un objet picklé. La même chose vaut pour cPickle, shelve, dill et joblib, qui enveloppent pickle.
Reconnaissez pickle sur le réseau à ses opcodes et à son . final (le protocole 0 est quasi-ASCII ; les protocoles supérieurs sont binaires et souvent encodés en base64).
Autres sinks Python#
-
yaml.load(PyYAML) sans loader sûr construit des objets Python arbitraires via le tag!!python/object/apply:!!python/object/apply:os.system ["id"]yaml.load(data)exécute cela sousyaml.Loader/UnsafeLoader(et leyaml.loadpar défaut d'avant 5.1 sans loader).FullLoadera été spécifiquement conçu pour rejeter l'application de callable arbitraire et ne l'exécute pas par défaut ; il n'était exploitable que via des contournements historiques précis que PyYAML actuel a corrigés.yaml.safe_loadne l'exécute jamais. Testez à la fois le sink et le loader exact utilisé. -
jsonpickle.decodereconstruit des objets Python à partir de JSON qui porte des cléspy/objectetpy/reduce, donnant la même primitive__reduce__via un payload d'apparence JSON. -
numpy.load(allow_pickle=True),pandas.read_pickle,joblib.loadettorch.loaddésérialisent avecpickleen coulisses, donc charger un fichier.npy/.pkl/de modèle non fiable est la même primitive.
Notes d'exploitation#
- Gardez les imports du payload minimaux et présents sur la cible (
os,subprocess,builtins) ; pour la sortie, utilisezsubprocess.check_outputet exfiltrez, ou visez un reverse shell puisque la sortie d'os.systemne vous est pas renvoyée. - Pour les environnements restreints qui bloquent
os,builtins.eval/execavec une chaîne construite atteint le même objectif. pickles'exécute avant que tout code applicatif n'inspecte l'objet, donc la validation d'entrée aprèsloads()arrive trop tard et est sans effet sur l'exploitation.
Outils#
- Payloads
__reduce__forgés à la main ; pker pour assembler des opcodes pickle complexes.
Références#
- Python manual: pickle (security warning)
- PortSwigger Web Security Academy: Insecure deserialization