HackademINT

Club de cybersécurité de Telecom SudParis


Bitshift

Auteur : Headorteil ft Bdenneu & Frazew

----------------------------------

L’énoncé

Nous avons réussi à obtenir un compte sur le serveur web d’échange de messages de la Shadow League disponible à l’URL suivante :

http://quals.shadow-league.org:8002

Le compte en question “test:test” (très original) dispose de peu de droits, saurez-vous élever vos privilèges ?

Nos informations révèlent également que le développeur a raté ses partiels de crypto …

Tatonnons

Petite overview

On nous donne donc un lien ver un site sur lequel on peut entrer un ID, un mot de passe. On note également la présence d’une checkbox “Remember me”.

La page d'accueil

On tente des choses

On essaye donc de se connecter avec test/test en cochant la checkbox afin de set le cookie de session.

On tombe alors sur une page de session.

La page de session

On observe que quand on se connecte, on fait appel au lien rememberme.php?userid=500&json. En modifiant le lien, par exemple en faisant une requete à http://quals.shadow-league.org:8002/rememberme.php?userid=a&json, on obtient l’erreur suivante : “LFSR: invalid argument : iterations must be an integer”

Sur la piste du LFSR

On sait donc quel procédé a été utilisé : le LFSR. Reste à savoir sur quoi. On suppose alors que c’est sur le rememberme et que l’ID représente le rang de la LFSR, on vérifie cette hypothèse en convertissant le rememberme en binaire.

On applique alors le principe du LFSR une fois pour passer à l’user 501, seuls deux cas sont alors possibles étant donné qu’on itère qu’une seule fois (J’explique plus bas en détail comment fonctionne un LFSR): 110111000111101011000110 peut devenir 111011100011110101100011 ou 011011100011110101100011. On essaye les 2 valeurs en modifiant le rememberme par les valeurs en hexadecimal des deux cas possibles et l’userid par 501. On obtient une connexion pour 111011100011110101100011 (EE3D63), on valide alors notre hypothèse, on sait désormais comment progresser parmi les utilisateurs. Reste à savoir comment récupérer l’ID et le rememberme de l’admin.

Ne sachant pas vraiment quoi faire d’autre, on essaye de réitérer pour obtenir les rememberme d’autres utilisateurs. Et seulement un cran après, à l’utilisateur 502, surprise! On nous donne plein d’infos :

L'utilisateur 502

La chasse aux cookies

Le but

On va alors essayer de récupérer les cookies de tous les users pour pouvoir caractériser au mieux notre LFSR pour pouvoir obtenir le cookier de l’admin (le rang 100), si vous ne voyez pas pourquoi on fait tout ça vous comprendrez mieux quand j’expliquerai le fonctionnement du LFSR.

Comment qu’on fait?

On sait quels sont les ID pris par des utilisateurs et on dispose de hashs pour certains d’entre eux. On peut aisément bruteforce des termes consécutifs mais difficilement des termes espacés de plusieurs itérations du LFSR. On va donc pouvoir obtenir les rangs 500-512 en bruteforcant pas à pas mais pour accéder aux id 400-412 et 500-512, on va devoir disposer d’au moins un rememberme dans ces intervalles. On va donc casser le Hash de l’user 402 et 601 puis refaire ce qu’on avait fait pour passer de l’ID 500 à l’ID 501. (On fera la meme chose mais en décalant de l’autre sens pour trouver les rememberme des users 401 puis 400 et 600).

Scriptons tout ça

import requests

def success(x,i):
    #Pour savoir si le cookie est bon
    url = "http://quals.shadow-league.org:8002/index.php"
    cookies = {
        "PHPSESSID":'ibidl33l4avsfpovo76rgeedu2', #PHPSESSID a changer pour se connecter
        "rememberme":x,
        "userid":str(i)
        }
    r = requests.get(url,cookies=cookies)
    return not(b"Invalid" in r.content)

remember = 'dc7ac6'
id_ = 500
data = []
while id_ < 512:
    lastbits = bin(int(remember,16))[2:-1] #On recupere les bits
    lastbits = "0" * ((23-len(lastbits))%23) + lastbits #On effectue un padding pour ne pas perdre un 0 en premiere position
    test0 = hex(int('0'+lastbits,2))[2:]
    test1 = hex(int('1'+lastbits,2))[2:]
    if (success(test0,id_+1)): #On teste les deux cookies possibles
        data += [(id_+1,test0)]
        remember = test0
    elif (success(test1,id_+1)):
        data += [(id_+1,test1)]
        remember = test1
    else:
        print('ERREUR')
        break
    id_ += 1
print(data)

Plus de données

Maintenant qu’on a récupéré les rememberme des users 500-512, on veut le reste. Une recherche sur votre moteur de recherche favori vous apprendra que nos hashs sont des hash Apache, on va donc casser les hashs en utilisant

hashcat -m 1600 -a 0 'hash' rockyou.txt

On peut ensuite réutiliser noter script pour enfin avoir la liste complète des rememberme.

Le Fameux LFSR

Qu’est ce qu’on veut?

On a désormais toutes les cartes en main pour retrouver le rememberme de l’admin. Pour ce faire on va devoir remonter le LFSR jusqu’au rang 100.

Qu’est ce qu’un LFSR

Un LFSR, registre à décalage à rétroaction linéaire en français est un registre (oui oui) qui est utilisé pour chiffrer de l’information. Un joli gif étant plus parlant qu’une longue explication, en voici un qui vous permettra de comprendre comment passer d’un rang N à un rang N+1 pour un LFSR donné.

LFSR

Vous comprenez alors pourquoi on avait besoin de plus d’utilisateurs que seulement les 500-512 : on a 24 inconnues (c’est la taille du registre), il nous faut donc au moins 24 équations.

Comment arriver à nos fins?

En principe on devrait essayer de trouver les x0, x1 … x23 pour pouvoir inverser ce “polynome” et ainsi obtenir un nouveau polynome qui nous permettrait de reculer dans les ID.

Cependant, on va être plus malin et trouver directement le “polynome” qui est du bon sens.

Pour ce faire, on ne va pas résoudre le système dons les équations sont :

polynome appliqué au rang n = premier bit du registre n+1

mais le système dont les équations sont les suivantes :

polynome appliqué au rang n = dernier bit du registre n-1

On implémente tout ça

from constraint import *

A = [0xdc7ac6,
     0xee3d63,
     0xf71eb1,
     0x7b8f58,
     0xbdc7ac,
     0xdee3d6,
     0xef71eb,
     0xf7b8f5,
     0xfbdc7a,
     0x7dee3d,
     0xbef71e,
     0xdf7b8f,
     0x6fbdc7]


B = [0x5f3ab4,
     0xaf9d5a,
     0x57cead,
     0xabe756,
     0x55f3ab,
     0x2af9d5,
     0x957cea,
     0xcabe75,
     0xe55f3a,
     0x72af9d,
     0xb957ce,
     0xdcabe7,
     0x6e55f3]

C = [0x5a68a5,
     0x2d3452,
     0x169a29,
     0xb4d14,
     0x85a68a,
     0x42d345,
     0x2169a2,
     0x90b4d1,
     0xc85a68,
     0x642d34,
     0xb2169a,
     0x590b4d,
     0xac85a6]


def convert(T):
    """C'est une simple conversion de l'hexadécimal vers le binaire"""
    U=[]
    for a in range(len(T)):
        U.append([])
        e = str(bin(T[a]))[2:]
        while len(e) < 24:
            e = "0" + e
        for i in e:
            U[a].append(int(i))
    return U

#On convertit tous nos cookies en binaire.
X=convert(A)
Y=convert(B)
Z=convert(C)

def fct(T, a):
    """Cette fonction permet de générer nos équations"""
    def res(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14,
            x15, x16, x17, x18, x19, x20, x21, x22, x23):
        return (x0*T[0] + x1*T[1] + x2*T[2] + x3*T[3] + x4*T[4] + x5*T[5] +
    x6*T[6] + x7*T[7] + x8*T[8] + x9*T[9] + x10*T[10] + x11*T[11] + x12*T[12] +
    x13*T[13] + x14*T[14] + x15*T[15] + x16*T[16] + x17*T[17] + x18*T[18] +
    x19*T[19] + x20*T[20] + x21*T[21] + x22*T[22] + x23*T[23])%2 == a
    return res

problem = Problem()
poly = range(24)
problem.addVariables(poly, [0,1])

#Ici on va mettre en place notre système d'équations.
for i in range(1, len(X)):
    problem.addConstraint(fct(X[i], X[i-1][-1])  ,(poly[0], poly[1], poly[2],
                                                   poly[3], poly[4], poly[5],
                                                   poly[6], poly[7], poly[8],
                                                   poly[9], poly[10], poly[11],
                                                   poly[12], poly[13], poly[14],
                                                   poly[15], poly[16], poly[17],
                                                   poly[18], poly[19], poly[20],
                                                   poly[21], poly[22], poly[23]))
for i in range(1, len(Y)):
    problem.addConstraint(fct(Y[i], Y[i-1][-1])  ,(poly[0], poly[1], poly[2],
                                                   poly[3], poly[4], poly[5],
                                                   poly[6], poly[7], poly[8],
                                                   poly[9], poly[10], poly[11],
                                                   poly[12], poly[13], poly[14],
                                                   poly[15], poly[16], poly[17],
                                                   poly[18], poly[19], poly[20],
                                                   poly[21], poly[22], poly[23]))
for i in range(1, len(Z)):
    problem.addConstraint(fct(Z[i], Z[i-1][-1])  ,(poly[0], poly[1], poly[2],
                                                   poly[3], poly[4], poly[5],
                                                   poly[6], poly[7], poly[8],
                                                   poly[9], poly[10], poly[11],
                                                   poly[12], poly[13], poly[14],
                                                   poly[15], poly[16], poly[17],
                                                   poly[18], poly[19], poly[20],
                                                   poly[21], poly[22], poly[23]))

#Enfin on résoud le système.
solutions = problem.getSolutions()
print(solutions)

Le script est assez long mais il finit par trouver notre polynome :

[{0: 1, 1: 0, 2: 0, 3: 0, 4: 1, 5: 1, 6: 0, 7: 1, 8: 1, 9: 1, 10: 0, 11: 1, 12: 0, 13: 0, 14: 1, 15: 0, 16: 1, 17: 0, 18: 1, 19: 0, 20: 1, 21: 0, 22: 1, 23: 0}]

Plus qu’à remonter

On a plus qu’à appliquer le principe du LFSR pour revenir à l’utilisateur 100 :

def lastbit(state):
    pol = [1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
    d = 0
    for i in range(len(state)):
        d += state[i]*pol[i]
    return d%2

def before(state):
    return state[1:] + [lastbit(state)]

a = Y[0] #Y[0] étant le binaire du rememberme du l'user 400, on a défini Y dans le script précédant.

for i in range(300):
    a = before(a)

print(a)

Ce script nous renvoie

[1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1]

C’est à dire 8FC625 en hexadécimal.

On a plus qu’a mettre 8FC625 en rememberme et 100 en ID, on refresh la page et hop :

Le flag

On obtient au final en plus d’un PC bouillant le petit flag : SCE{Ze_Crypt0_Fl4g_H@GHG#FHGVK__JVYDRUFJKHJ9875423}