Récit d’un CTF (partie 3)

PianoCarriera

Dans ce challenge, je dois aider un élève à « trafiquer » le site d’inscription de son université pour l’inscrire à un cours (internship) auquel il n’est pas sensé avoir accès

google translate me dit que le site ne veut pas que je m’inscrive…

Comme d’habitude, une petite plongée dans le code permet de comprendre ce qu’il se passe. Un passage attire mon attention:

if($(this).hasClass('noProp')){
                    
                        
                        checkbox.attr('checked',false);
                        
                             $( "#msg2" ).empty();
                             var myMsg='<div class="alert alert-danger">Modulo non selezionabile dallo studente</div>';
                             $( "#msg2" ).append(myMsg);
                                $( "#msg2" ).dialog({
                                  maxWidth:600,
                                  maxHeight: 300,
                                    modal: true,
                                    autoOpen: true,
                                    buttons: {
                                      "Ok": function() {
                                        $( this ).dialog( "close" );
                                        $( "#msg2" ).empty();
                                      }
                                    }
                              });
                    
                          
                    }

Apparemment, le code se base sur la présence de la classe noProp dans la balise de la case à cocher. Qu’à cela ne tienne, je vais juste utiliser les « dev tools » de mon navigateur pour retirer la classe noProp de l’élément! Après quelques essais, il semble que je doive avoir un total de 30 crédits dans la catégorie. Pas de soucis, j’ajoute un deuxième cours à 18 crédits de la même façon, et j’obtiens le flag!

PoliTO ch(e)atbot

Dans ce challenge, je dois crypter une phrase donnée et l’envoyer au chatbot pour recevoir le flag. Je dispose d’un outil pour crypter un message, mais ce message est limité à 16 caractères (or le message que je dois envoyer en compte plus de 16)

Je dispose également du résultat du cryptage d’une chaine qui est presque le message que je dois crypter

Le cryptage est AES-218 en mode ECB (Electronic Code Book). Voilà la faille que je vais exploiter. En effet, la particularité du mode ECB est que chaque bloc du message (16 bytes dans notre cas) est encodé de manière totalement indépendante. Je peux donc crypter 16 caractères correspondant au bon message, puis compléter avec le reste du message crypté

I’m Bob Masters,gimme the flag!
cd73869efee7fe501d94925ed2798f895a9f9e956559ae571c6d3989f78fda3c
I’m Rob Masters,
1be39bf6015076ba70446ca402584e98

La deuxième ligne du tableau est le message crypté affiché dans le chat

La quatrième ligne est le résultat du cryptage de « I’m Rob Masters, » via l’outil fourni

Il suffi alors de combiner les deux parties pour obtenir le bon message crypté

1be39bf6015076ba70446ca402584e985a9f9e956559ae571c6d3989f78fda3c

Et voilà, le c’est gagné!

PoliTO ch(e)atbot 2.0

Cette fois, le chatbot propose un challenge un peu différent

un OTP (one time pad) est une technique de cryptage totalement sécurisée. Il est impossible de décrypter le message sans la clef.

Le principe de l’OTP est simple: le message est XORé avec une clef aussi longue que le message, formée de bytes aléatoires.

Le problème, c’est que, comme on peut le constater en utilisant l’outil fourni, on est très très loin d’un OTP

aléatoire, vraiment?

Visiblement, la clef générée ne fait qu’un seul byte, qui est répété autant de fois que nécessaire pour avoir une clef aussi longue que le message.

Au lieu d’avoir un nombre astronomiques de clefs possibles, on en a seulement 256, suffisamment pour un leger bruteforce:

import requests
from time import sleep
c="bf58b06614740254d007cc8ddca2538589e2624e142457ff2e13c16fde19a301"
url="https://chatbot2.challs.m0lecon.it/api/v1/message"

for i in range(1,256):
	k=f"{i:02x}"*(len(c)//2)
	print(c)
	print(k)
	p=hex(int(c,16)^int(k,16))[2:]
	if len(p)%2!=0:p="0"+p
	print(p)
	resp=requests.post(url,json={"message":p},cookies={"session":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}).json()["message"]
	print(i,resp)
	if resp!="Wrong password!!!":
		exit()
	sleep(0.5) # so I don't overload the server...
	
	

Après une petite attente, le flag est capturé!

Récit d’un CTF (partie 2)

SecureAccess

L’objectif de ce challenge est de s’enregistrer comme un user « admin »

Nous disposons de deux choses:

  • le site web
  • un fichier .pyc

Le site web permet d’enregistrer un utilisateur. Le processus d’enregistrement demande de fournir un token.

Le site web

Le fichier .pyc est un fichier python compilé. Il existe une multitude d’outils permettant de décompiler un pyc pour retrouver le code source. J’ai opté pour un décompilateur online, qui m’a fourni la source suivante:

# uncompyle6 version 3.5.0
# Python bytecode 3.8 (3413)
# Decompiled from: Python 2.7.5 (default, Jun 20 2023, 11:36:40) 
# [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
# Embedded file name: leaked.py
# Size of source mod 2**32: 304 bytes
import hashlib, json, base64

def generate_token(nonce: str):
    username = 'user'
    secret = hashlib.sha256(username.encode() + nonce.encode()).hexdigest()
    bundle = {'user':username, 
     'secret':secret}
    return base64.b64encode(json.dumps(bundle).encode())

Il s’agit vraisemblablement de la fonction qui permet de générer le token demandé.

Une petite modification du code, et j’obtiens le token qui me donne accès au flag!

import hashlib, json, base64

def generate_token(nonce: str):
    # /!\ change the user to admin
    username = 'admin'
    secret = hashlib.sha256(username.encode() + nonce.encode()).hexdigest()
    bundle = {'user':username, 
     'secret':secret}
    return base64.b64encode(json.dumps(bundle).encode())

# calling the function with the nonce given by the website
print(generate_token("7701C66C-18D9-409E-94FA-DBB7CE1A98AC"))

Fast RSA

Cette fois, le flag est crypté avec l’algorithme RSA. La sécurité de cet algorithme est mathématiquement prouvée. Néanmoins, certaines implémentations (comme celle-ci) comportent des failles qui permettent à un attaquant de cracker le cryptage.

from Crypto.Util.number import getPrime, isPrime, bytes_to_long
from secret import flag

flag = bytes_to_long(flag)

def GeneratePublicKey():
    while True:
        p = getPrime(512)
        q = p + 4
        if isPrime(q):
            return (p, q)

p, q = GeneratePublicKey()
n = p*q
e = 65537
d = pow(e, -1, (p-1)*(q-1))

ciphertext = pow(flag, e, n)

print(n)
print(e)
print(ciphertext)

# OUTPUT    
# n = 73835355773586632749497813712844974279688700968581353676365718256887741429589293987167857318819304298761226530765222129869744268663458302484923609419850715086294772630316875879671931990860760168632405376915473464245173555210859733420012801323450300287286827847372572895767079204521003431182215766356958645741
# e = 65537
# ciphertext = 69088718730225565550533207665423162086878540640610709978042785622785990794595484186604907117720523080139401746613418389574889276052615162202284720710353059616183359504400646419053826554396432853908827511966214885915759786569544232364425295062591653836236590708223539383404510598672913303248612361154693570128

Dans ce cas, la faille est dans la génération des deux nombres premiers p et q. Normalement, ces deux nombres premiers sont totalement indépendants, ce qui rend la factorisation de N presque impossible. Mais ici on sait que q=p+4.

il suffit donc de trouver un nombre p tel que p*q=p*(p+4)=n. Cela revient à une simple équation du second degré, avec de très très grands nombres

p² + 4p - n = 0
==> rho=16+4*n
==> p=(-4+-sqrt(rho))/2=-2+-sqrt(4+n)

Cela semblait pourtant une bonne idée, mais cela ne donne aucun résultat.

J’ai donc essayé une autre approche, en supposant que p et q sont proches, donc proches de la racine carrée de N.

from Crypto.Util.number import long_to_bytes
 
n = 73835355773586632749497813712844974279688700968581353676365718256887741429589293987167857318819304298761226530765222129869744268663458302484923609419850715086294772630316875879671931990860760168632405376915473464245173555210859733420012801323450300287286827847372572895767079204521003431182215766356958645741
e = 65537
ciphertext = 69088718730225565550533207665423162086878540640610709978042785622785990794595484186604907117720523080139401746613418389574889276052615162202284720710353059616183359504400646419053826554396432853908827511966214885915759786569544232364425295062591653836236590708223539383404510598672913303248612361154693570128

def bigSqrt(x):
	mininc=1
	maxexc=x
	while maxexc-mininc>1:
		pivot=(mininc+maxexc)//2
		p2=pivot**2
		if p2==x:	return pivot
		elif p2>x:	maxexc=pivot
		else:		mininc=pivot+1
	print ("check",mininc**2-x)
	return mininc

z=bigSqrt(n)
for i in range(z-1000,z+1000):
	if n%i==0: 
		p=i
		q=n//i
		print(p)
		print(q)
		print("check",n-p*q)
		d=pow(e, -1, (p-1)*(q-1))
		plain=long_to_bytes(pow(ciphertext,d,n))
		print(plain)

Et voilà, le flag est obtenu! En réalité, les q=p+100 et non p+4…

My first CTF (part 1) (english version)

Yesterday, I had the opportunity to participate in a CTF for the first time.

A CTF (Capture The Flag) is a limited-time event during which hundreds of teams from around the world compete to solve a series of challenges to obtain the famous “flags”. The difference with traditional challenge sites is precisely this limited duration (a few hours to a few days), which adds a certain pressure to the challengers…

For my first experience, I therefore chose a “beginner” CTF organized by an Italian university: m0leCon Beginner CTF. A 5-hour CTF, combining crypto, web hacking, reversing, etc. tests.

I now follow the tradition of “write-up”, that is to say sharing some of my solutions after the end of the competition, to allow everyone to progress!

Unguessable

Unguessable, really?

The first challenge invites us to guess a number. Unless I’m extraordinary lucky, the answer is always “Wrong, try again”

The first reflex in this type of challenge is to understand how the application works. And for that, you have to dive into the code…

The two functions that interest us in the javascript code are:

    function update(res) {
       if (res === "wrong") {
         card.style.backgroundColor = "red";
         text.innerText = "Wrong, try again";
       } else {
         card.style.backgroundColor = "green";
         fetch("/vjfYkHzyZGJ4A7cPNutFeM/flag")
           .then((response) => response.text())
           .then((str) => {
             text.innerText = str
           });
       }

       card.removeAttribute("hidden");
     }

     document.getElementById("guessBtn").onclick = function () {
       card.setAttribute("hidden", "");
       load().then(() =>
         fetch("/guess", {
           body: document.getElementById("guess").value,
           method: "POST",
         }))
         .then((response) => response.json())
         .then((json) => update(json["result"]))
         .then(() => loader.parentElement.setAttribute("hidden", ""));

     };

The second function is the one that is triggered when you click on the “Guess” button. This function sends the number entered in the form to a “/guess” page, retrieves the result returned by this page and passes it to the “update” function

The “update” function checks if the result is “wrong”. In this case, it displays the message “Wrong, try again!” in red. Otherwise, it will get another page (/vjfYkHzyZGJ4A7cPNutFeM/flag) and display its contents. And this page contains the flag…

AND Cipher

This challenge contains a page allowing you to generate an encrypted string.

Each press of the “Encrypt!!!” button generates a new string. According to the clues in the title, this string is the result of the Boolean operator AND between a secret message (probably the flag) and a random key

as a reminder, here is how the AND operator works:

Input 1 (flag bit) Input 2 (key bit)Output (encrypted string bit)
000
010
100
111
AND operator

If we have the key, how could we find the flag? Let’s try reversing the AND operator:

encrypted string bitkey bitflag bit
000 or 1???
010
10Impossible!!
111
reversing the AND operator

We can see two problems in this AND “encryption”:

  • If the bits of the encrypted string and the key are both 0, it is impossible to know which bit was the flag
  • If the bit of the encrypted string is 1, this means that we know that the flag bit is 1 even if we do not know the key

It is this second property that I will use. Indeed, each time a bit of the encrypted string is 1, we can deduce that the corresponding bit of the flag is 1. And as we can generate as much encryption as we want, we can accumulate these bits to reconstruct the flag. I automated the process in a small python program:

from Crypto.Util.number import long_to_bytes
url="https://andcipher.challs.m0lecon.it/api/encrypt"
import requests

s=0
while True:
	# get the response from the url
	d=requests.get(url).text.split('"')[3]
	
	# transform it into int
	d=int(d,16)
	
	# accumulate the "1" bits with OR operator
	s|=d
	
	# print the result
	print(long_to_bytes(s))
	input()

Here’s the result: