SQL-Injection 06.06

1. Einleitung

SQL-Injection ist eine kritische Sicherheitslücke, die auftritt, wenn ein Angreifer bösartigen SQL-Code in eine Abfrage einfügt, die von einer Anwendung an eine Datenbank gesendet wird. Dies geschieht häufig, wenn Benutzereingaben direkt in SQL-Abfragen eingebettet werden, ohne sie angemessen zu validieren oder zu bereinigen. SQL-Injections können zu schwerwiegenden Folgen führen, darunter:

  1. Unbefugter Zugriff auf Daten: Angreifer können sensible Informationen wie Benutzerdaten, Passwörter und finanzielle Informationen extrahieren.

  2. Datenmanipulation: Angreifer können Daten in der Datenbank ändern oder löschen.

  3. Datenbankübernahme: Angreifer können administrative Rechte übernehmen und die gesamte Datenbank kompromittieren. 1

Darüber hinaus können SQL-Injections auch die Integrität und Verfügbarkeit der Datenbank beeinträchtigen. Ein Angreifer könnte beispielsweise eine Denial-of-Service (DoS)-Attacke durchführen, indem er die Datenbank mit einer großen Anzahl von Anfragen überflutet. Außerdem besteht das Risiko, dass Angreifer Schadsoftware in die Datenbank einschleusen, die sich auf andere Systeme im Netzwerk ausbreiten kann. Um solche Angriffe zu verhindern, ist es wichtig, sichere Programmierpraktiken zu befolgen, wie die Verwendung vorbereiteter Anweisungen (Prepared Statements) und die regelmäßige Durchführung von Sicherheitsüberprüfungen und Penetrationstests.

2. Grundlagen:

Für unser Experiment verwenden wir unsere alte react-App.

function LoginPage(){
        const dataBaseUrl = "http://localhost:5000";
        const usernameInputRef = useRef(null);
        const passwordInputRef = useRef(null);
        const [message, setMessage] = useState("--");

        async function submitHandler(event){
            event.preventDefault();
            const form = document.getElementById("form");
            if(!usernameInputRef.current || !passwordInputRef.current || !form){
                return;
            }
            const username = usernameInputRef.current.value //getting username from input form
            const password = passwordInputRef.current.value //getting password from input form

            form.reset();

            try{
                const response = await fetch(`${dataBaseUrl}/users/${username}`);
                const user_data = await response.json();
                const user = user_data.user;
                if(!user){
                    throw(new Error());
                }
                if(user.password === password){
                    setMessage("User:" + username + " logged in!");
                }else{
                    throw(new Error());
                }
            }catch(error){
                setMessage("Wrong password or username");
            }
        }
    }

Die LoginPage verwendet React-Hooks wie useRef und useState, um Referenzen für die Eingabefelder und den Zustand für Nachrichten zu verwalten. Beim Absenden des Login-Formulars greift die submitHandler-Funktion auf die eingegebenen Werte zu, setzt das Formular zurück und versucht, die Benutzerinformationen vom Server abzurufen. Wenn der Benutzer gefunden wird und das Passwort übereinstimmt, wird eine Erfolgsmeldung angezeigt, andernfalls eine Fehlermeldung. Das Formular enthält Eingabefelder für Benutzername und Passwort sowie einen Anmeldebutton.

Die RegisterPage funktioniert ähnlich, mit zusätzlichen Feldern für die E-Mail-Adresse. Die submitHandler-Funktion prüft, ob der Benutzer bereits existiert. Wenn ja, wird eine Fehlermeldung angezeigt. Andernfalls wird ein neuer Benutzer erstellt und eine Erfolgsmeldung ausgegeben. Auch hier gibt es Eingabefelder für Benutzername, E-Mail und Passwort sowie einen Registrierungsbutton.

Damals lief die Anwendung mit einer Key-Value-Datenbank von Google 4. Um nun eine SQL-Injection besser visualisieren zu können, haben wir eine kleine API geschrieben, die sich mit einer lokalen Datenbank auf dem PC verbindet. Übersicht Diese API umfasst zwei Endpunkte: getUser und createUser. Sie ermöglicht das Abrufen von Benutzerdaten basierend auf dem Benutzernamen und das Erstellen eines neuen Benutzers. Beide Endpunkte verwenden unsichere SQL-Abfragen, die anfällig für SQL-Injections sind.

Endpunkt: getUser

Dieser Endpunkt dient dazu, Benutzerdaten anhand des Benutzernamens abzurufen.

Pfad: /users/:username

HTTP-Methode: GET

username (Pfadparameter): Der Benutzername des Benutzers, dessen Daten abgerufen werden sollen. Funktionsweise:

Der Benutzername wird aus den URL-Parametern extrahiert. Eine SQL-Abfrage wird erstellt, um den Benutzer anhand des Benutzernamens zu finden. Wenn ein Fehler auftritt, wird der Fehler mit dem Statuscode 400 zurückgegeben. Wenn kein Benutzer gefunden wird, wird eine Antwort mit found: false gesendet. Wenn ein Benutzer gefunden wird, wird eine Antwort mit found: true und den Benutzerdaten gesendet.

Über die API kann die react-Anwendung direkt mit der Datenbank kommunizieren. Dabei erhält sie über HTTP-Methoden Daten, die sie in eine SQL-Abfrage einfügt. Zum Login oder zur Registrierung kann sie einen Benutzer und dessen Daten abfragen, aber auch einen neuen Benutzer anlegen. Das Problem an dieser API ist, dass man durch das Einschleusen von User Inputs auch die SQL Query zu seinen Gunsten verändern kann.

    export const getUser = (req, res) => {
    const { username } = req.params;
    pool.query(`SELECT * FROM users WHERE username = "${username}"`, (error, result) => {
        if (error) {
            res.status(400).send(error);
            return;
        }
        if (result.length === 0) {
            res.send({ found: false });
        } else {
            res.send({ found: true, user: result[0] });
        }
    });
}

Endpunkt: createUser

Dieser Endpunkt dient dazu, einen neuen Benutzer zu erstellen.

Pfad: /users

HTTP-Methode: POST

Request Body:

email (String): Die E-Mail-Adresse des Benutzers. password (String): Das Passwort des Benutzers. username (String): Der Benutzername des Benutzers.

Funktionsweise: Die Benutzerdaten werden aus dem Anforderungstext extrahiert. Eine SQL-Abfrage wird erstellt, um den neuen Benutzer in die Datenbank einzufügen. Wenn ein Fehler auftritt, wird der Fehler mit dem Statuscode 400 zurückgegeben. Wenn der Benutzer erfolgreich erstellt wird, wird das Ergebnis zurückgegeben.

export const createUser = (req, res) => {
    const user = req.body;
    pool.query(`INSERT INTO users (email, password, username) VALUES ("${user.email}", "${user.password}", "${user.username}");`, (error, result, fields) => {
        if (error) {
            res.status(400).send(error);
        } else {
            res.send(result);
        }
    });
}

Als Datenbank wird MySQL verwendet. Um Änderungen in dieser Datenbank gut visualisieren zu können, verwenden wir das kostenlose Tool DBeaver. Damit lassen sich auch auf einfache Weise neue Tabellen und Spalten erstellen. Außerdem kann man mit diesem Tool auch eigene SQL-Abfragen schreiben und ausführen.

_images/DBeaver_logo.png

([Abb.1]): Dbeaver Logo

3. Versuchsdurchführung

Zuerst muss auf dem Rechner eine lokale rationale Datenbank erstellt werden. Wir haben dafür die bereits erwähnte MySQL Datenbank verwendet. Wir haben Windows zum herunterladen der Datenbak benutzt. 2 Dazu muss man den MySQL Installer von der MySQL-Website herunter laden und danach führt man den Installer aus und wählt die Standardkonfiguration.

Der MySQL-Server sollte automatisch auf Windows starten. Falls nicht, muss man ihn über den MySQL Workbench oder dem Dienstmanager starten.

Jetzt kann man einen MySQL-Benutzer anlegen und die Datenbank einrichten Man meldet sich als Root-Benutzer an:

mysql -u root -p

Dann erstellt man eine neue Datenbank:

CREATE DATABASE testdb;

Dann erstellt man einen neuen Benutzer und gewährt ihm Rechte:

CREATE USER 'exampleuser'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON exampledb.* TO 'exampleuser'@'localhost';
FLUSH PRIVILEGES;

3 Nachdem man Dbeaver installiert und gestartet hat, muss man folgende Schritte einleiten, um eine Verbindung zur Datenbank aufzubauen. Man öffnet DBeaver und klickt auf das New Database Connection-Symbol (oder gehe zu Database > New Database Connection). Man wählt nun MySQL aus der Liste der Datenbanktypen und klickt auf Next. Dann gibt man die Verbindungsdetails ein:

Host: localhost

Port: 3306 (Standardport für MySQL)

Database: testdb

Username: user

Password: 1234

Nun klickt man auf Test Connection, um sicherzustellen, dass die Verbindung funktioniert. Wenn der Test erfolgreich ist, klickt man auf Finish, um die Verbindung zu speichern. Jetzt ist man bereit, die MySQL-Datenbanken über DBeaver zu verwalten. Man kann Abfragen ausführen, Tabellen erstellen und Datenbankstruktur visuell betrachten.

Wie schon erwähnt musste die React-App angepasst werden damit sie sich mit der API verbindet statt der Datenbank von Google 4. Zum einen musste natürlich die Datenbank-URL geändert werden. Da die API lokal auf dem Port 5000 läuft, muss die URL entsprechend so aussehen:

const dataBaseUrl = "http://localhost:5000";

Das Format der Abfrage musste auch geändert werden. Statt „.json“ am Ende der URL, reicht nun der username aus. Sodass, die API Pfade richtig angesteuert werden.

const response = await fetch(`${dataBaseUrl}/users/${username}`);

Für die Versuchsdurchführung muss die API sowie die react-app lokal auf dem PC mit

npm start

gestartet werden. Zudem muss sichergestellt werden das der MySQL server gestartet ist.

Drop-Table SQL-Injection

Eine Drop-Table SQL-Injection ist eine gefährliche Art von Angriff, bei dem der Angreifer eine bösartige SQL-Abfrage in ein Eingabefeld einfügt, um eine oder mehrere Tabellen der Datenbank zu löschen. Dieser Angriff nutzt Schwachstellen in der Verarbeitung von Benutzereingaben durch die Anwendung aus. Die erste SQL-Injection, die man auf das System anwenden kann ist diese:

"; DROP TABLE users; --
_images/drop_table.png

Erklärung der Abfrage

Diese Abfrage versucht, einen Benutzer mit einem leeren Benutzernamen zu finden.

„;“ Das Semikolon beendet die ursprüngliche Abfrage.

„DROP TABLE users;“: Dieser Befehl löscht die users-Tabelle aus der Datenbank.

„–“ : Die doppelten Bindestriche kommentieren den Rest der SQL-Abfrage aus, um Syntaxfehler zu vermeiden.

Wenn ein User diesen Befehl in das username-Textfeld eingibt, wird der komplette table users gelöscht.

_images/sql_injection_2.png

Die SQL-Query würde nämlich dann so aussehen:

SELECT * FROM users where username = ""; DROP TABLE users; --";

Wenn diese SQL-Injection erfolgreich ist, wird die gesamte users-Tabelle gelöscht, was zu einem vollständigen Verlust aller Benutzerdaten führt. Dies kann schwerwiegende Konsequenzen haben, insbesondere in einer Produktionsumgebung.

Select all users SQL-Injection

Eine Select-All SQL-Injection ist eine Art von Angriff, bei dem der Angreifer eine manipulierte SQL-Abfrage in ein Eingabefeld einfügt, um unberechtigten Zugriff auf alle Datensätze in einer Tabelle zu erhalten. Dieser Angriff nutzt Schwachstellen in der Verarbeitung von Benutzereingaben durch die Anwendung aus. Auf das System kann dann diese Select * SQL-Injection angewendet werden:

user"); select * from users where ("1") = ("1

Erklärung der Abfrage

Sie fügt allgemein einen neuen Benutzer mit den angegebenen Werten ein.

„;““ : Das Semikolon beendet wieder die ursprüngliche Abfrage.

„SELECT * FROM users WHERE („1“) = („1“);“ : Dieser Befehl gibt alle Datensätze aus der users-Tabelle zurück, da die Bedingung „1“ = „1“ immer wahr ist.

Wenn der Benutzer nun diesen Text in das username-Textfeld beim Registrieren eingibt, werden alle Benutzer samt Daten preisgeben.

_images/sql_injection_1.png

Die „console.log“ ausgabe wurde absichtlich vergessen zu entfernen. Sie gibt nähmlich die Fehlermeldungen der API aus. Durch den Angriff gibt sie nun aber auch alle User auf der User Tabelle aus.

console.log("Result Message: " + JSON.stringify(message_data));

Die entstehende SQL-Query wird danach so aussehen:

INSERT INTO users (email, password, username) values (test@gmx.de, testpassword, user); select * from users where ("1") = ("1");

Wenn diese SQL-Injection erfolgreich ist, werden alle Benutzerdaten aus der users-Tabelle abgerufen und möglicherweise angezeigt oder weiterverarbeitet. Dies kann zu einem massiven Datenleck führen, da vertrauliche Informationen wie E-Mail-Adressen und Passwörter offengelegt werden.

Schutzmaßnahmen

Schutzmaßnahmen Um diese Art von Angriffen zu verhindern, sollte man immer vorbereitete Anweisungen (Prepared Statements) oder Parametrisierte Abfragen verwenden. Diese trennen die SQL-Anweisungen von den Daten und verhindern so das Einschleusen von bösartigem SQL-Code.

Hier ist eine verbesserte Version der API um Angriffe zu vermeiden:

export const getUser = (req, res) => {
    const { username } = req.params;
    const query = 'SELECT * FROM users WHERE username = ?';
    pool.query(query, [username], (error, result) => {
        if (error) {
            return res.status(400).send(error);
        }
        if (result.length === 0) {
            return res.send({ found: false });
        }
        res.send({ found: true, user: result[0] });
    });
};

export const createUser = (req, res) => {
    const { username, password, email } = req.body;
    const query = 'INSERT INTO users (username, password, email) VALUES (?, ?, ?)';
    pool.query(query, [username, password, email], (error, result) => {
        if (error) {
            return res.status(400).send(error);
        }
        res.send("User has been created!");
    });
};

In dieser Version verwenden die SQL-Abfragen Platzhalter (?) für die Eingabewerte, die von der Datenbank-Engine sicher eingebettet werden können. Nachdem die Schutzmaßnahmen ergriffen wurden kann ein Angreifer zum einen nicht mehr die komplette Tabelle löschen,

_images/protection_measure_drop_table.png

sowie alle user mit SELECT * abfragen. Da die SQL-Anweisungen von den Daten getrennt sind, können bösartige Eingaben nicht die Struktur der SQL-Abfrage verändern. Die Eingabewerte werden als reine Daten behandelt, nicht als Teil der SQL-Syntax. Wenn ein Angreifer versucht, exampleUser“; DROP TABLE users; – als Benutzernamen einzugeben, wird der gesamte String als ein Wert behandelt und kann die Abfrage nicht beeinträchtigen.

_images/protection_measure_select_all.png

Ebenso wird auch eine Select-All-Attacke gekonnt als ein Wert behandelt und wird somit unschädlich gemacht. Natürlich sollte man auch den console.log entfernen, da er nicht für den Benutzer bestimmt war.

Darüber hinaus ist es wichtig, Eingabevalidierung durchzuführen, um sicherzustellen, dass alle Benutzereingaben nur die erwarteten Daten enthalten. Gespeicherte Prozeduren (Stored Procedures) können ebenfalls helfen, SQL-Befehle von der Anwendungsebene zu trennen und die Möglichkeit zu verringern, dass Benutzereingaben die Struktur der SQL-Abfragen beeinflussen.

Es ist ratsam, der Datenbank nur die minimal notwendigen Rechte zu gewähren, um den Schaden im Falle eines Angriffs zu begrenzen (Least Privilege). Der Einsatz von Web Application Firewalls (WAF) kann dazu beitragen, SQL-Injection-Versuche zu erkennen und zu blockieren, bevor sie die Anwendung erreichen. Regelmäßige Sicherheitsüberprüfungen und Penetrationstests sind unerlässlich, um Schwachstellen in der Anwendung zu identifizieren und zu beheben.

Alle Benutzereingaben sollten ordnungsgemäß „escaped“ werden, wenn sie nicht durch vorbereitete Anweisungen oder gespeicherte Prozeduren verarbeitet werden können, um sicherzustellen, dass sie als Daten und nicht als SQL-Befehle interpretiert werden. Die Verwendung von Object-Relational Mappers (ORMs) kann das Risiko von SQL-Injections verringern, da sie Datenbankzugriffe abstrahieren und intern vorbereitete Anweisungen verwenden. Implementieren Sie starke Zugangskontrollen und führen Sie regelmäßig Audits durch, um unbefugte Zugriffe oder ungewöhnliche Aktivitäten in der Datenbank zu erkennen.

4. Fazit und Probleme

Die API und die React-Anwendung sind anfällig für SQL-Injection, da sie Benutzereingaben direkt in SQL-Abfragen einfügen, ohne diese zu validieren oder zu bereinigen. Ein Angreifer kann diese Schwachstelle ausnutzen, um sensible Informationen wie Passwörter aller Benutzer abzurufen oder die gesamte Tabelle zu löschen. Um SQL-Injection zu verhindern, sollten Entwickler vorbereitete Anweisungen (Prepared Statements) oder parametrisierte Abfragen verwenden, die Benutzereingaben sicher in die SQL-Abfragen einfügen. Durch diese Maßnahmen wird das Risiko von SQL-Injections erheblich reduziert und die Integrität und Sicherheit der Datenbank gewährleistet. Darüber hinaus ist es wichtig, regelmäßig Sicherheitsüberprüfungen und Penetrationstests durchzuführen, um potenzielle Schwachstellen frühzeitig zu erkennen und zu beheben. Die Implementierung von Eingabevalidierung, der Einsatz von Web Application Firewalls (WAF) und die Begrenzung der Datenbankrechte (Least Privilege) sind weitere wichtige Schutzmaßnahmen. Sicherheitsbewusstsein und Best Practices bei der Entwicklung von Anwendungen sind entscheidend, um solche Sicherheitslücken zu vermeiden und die Daten der Benutzer zu schützen. Durch kontinuierliche Weiterbildung der Entwickler im Bereich der IT-Sicherheit und die Implementierung umfassender Sicherheitsrichtlinien kann das Risiko von SQL-Injection und anderen Angriffen weiter minimiert werden.

5. Literaturangaben

1

SQL-Injection: https://www.cloudflare.com/de-de/learning/security/threats/sql-injection/

2

MySQL Install: https://dev.mysql.com/downloads/installer/

3

DBeaver install: https://dbeaver.io/download/

4(1,2)

Google Firebase: https://firebase.google.com/

Abbildungen:

Abb.1

: Quelle: https://upload.wikimedia.org/wikipedia/commons/thumb/b/b5/DBeaver_logo.svg/1200px-DBeaver_logo.svg.png