Ir al contenido
  1. Writeups/

HackTheBox - Global Hyperlink Zone

·11 mins
Nicolás Seral
Autor
Nicolás Seral
bla bla bla bla

CHALLENGE DESCRIPTION

Beneath Qubitrix’s corporate towers lies the Global Hyperlink Zone - their prototype quantum internet. Its five access nodes authenticate through specific quantum gate patterns. Retrieve the sequence, stabilize the hyperlink, and force entry into their hidden backbone. One wrong move, and the link collapses.

Datos iniciales:

  • server.py: Código fuente del servidor.
  • 154.57.164.73:32491: Conexión al servidor

Fundamentos
#

Qubits
#

A diferencia de los bits normales, que pueden tener un valor de 0 o 1, un qubit puede tener un valor de \(0\), \(1\), o una superposición entre ellos. Esto último significa que en un momento dado, el valor de un qubit no necesariamente está determinado, sino que se encuentra en un estado puramente probabilístico, y su valor colapsará a \(0\) o \(1\) al medirlo.

El valor al que colapse al medirlo dependerá de lo que se denomina amplitudes de probabilidad (\(\alpha\), \(\beta\)). La probabilidad de que el qubit sea \(0\) o \(1\) se describe mediante su estado cuántico, representado por la siguiente ecuación:

$$ \ket{\psi} = \alpha\ket{0} + \beta\ket{1} $$

\(\alpha\) y \(\beta\) son las amplitudes de probabilidad. Estas amplitudes son números complejos, es decir, pueden ser negativos e imaginarios, lo que permite que ambas amplitudes interfieran entre sí cancelándose y sumándose.

La probabilidad real (un valor entre \(0\) y \(1\)) de que el qubit colapse a \(0\) al medirlo es el cuadrado de su amplitud: \(\alpha^2\). Para el \(1\), la probabilidad es \(\beta^2\). La suma de ambas debe dar 1 por definición.

Para poder visualizar bien estos estados, se suele usar la esfera de Bloch:

Y como representar números complejos (\(\alpha\), \(\beta\)) espacialmente es complicado, suelen traducirse a coordenadas 3D para representarlos en la esfera como otros dos valores \(\theta\) y \(\phi\):

$$ \alpha = cos(\frac{\theta}{2}),\space\beta = e^{i\phi} sin(\frac{\theta}{2})\ \to\ \ket{\psi} = cos(\frac{\theta}{2})\ket{0} + e^{i\phi} sin(\frac{\theta}{2})\ket{1} $$
  • \(\theta\) mide la probabilidad de colapso: Si es \(0\), la probabilidad de medir 0 en ese qubit es del 100%. Si es \(1\), la probabilidad de medir 1 es del 100%. Si tiene un valor de \(90º\), la probabilidad de medir \(0\) o \(1\) es exactamente 50/50.
  • \(\phi\) no cambia la probabilidad de medir \(0\) o \(1\) absolutamente nada, pero afecta a cómo interfiere el qubit a otros al aplicarles puertas lógicas.

Representación
#

Para poder aplicar puertas lógicas cuánticas a los qubits, primero hay que dar un paso en la representación: El estado de un qubit se representa como un vector columna de dos dimensiones.

$$ \text{Estado } \ket{0}=\ \begin{pmatrix} 1 \\ 0 \end{pmatrix} $$

$$ \text{Estado } \ket{1}=\ \begin{pmatrix} 0 \\ 1 \end{pmatrix} $$

Por tanto, cualquier estado es simplemente

$$ \text{Estado } \ket{\psi}=\ \alpha\begin{pmatrix} 1 \\ 0 \end{pmatrix} + \beta\begin{pmatrix} 0 \\ 1 \end{pmatrix} = \begin{pmatrix} \alpha \\ \beta \end{pmatrix} $$

Por otro lado, las puertas lógicas, que no hemos visto aún, se representan como matrices. Por ejemplo, la puerta X (Equivalente al NOT), se representa como

$$ X=\ \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} $$

Si se lo aplicamos a un qubit en estado \(\ket{0}\):

$$ X\ket{0}=\ \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ 1 \end{pmatrix} = \ket{1} $$

Inicio
#

La idea del challenge es que consigamos hacer un circuito cuántico que haga algo que se nos pida. Si abrimos el código fuente del servidor, veremos que se trata de un sistema que nos permite introducir puertas lógicas cuánticas y sus respectivos parámetros. La librería en uso es Qiskit, que permite generar circuitos cuánticos y simularlos o ejecutarlos en ordenadores cuánticos reales.

Antes de nada, nos fijamos en el código, veremos que en generate_circuit() está la siguiente instrucción:

1
2
3
def generate_circuit(self, instructions: str):
    circuit = QuantumCircuit(5)
    ...

Esto significa que el circuito usará 5 qubits.

Funcionamiento
#

El flujo del programa es el siguiente, según el código fuente:

  • El usuario introduce una serie de puertas lógicas cuánticas y los qubits a los que se aplican, con la sintaxis PUERTA:QUBITS;PUERTA:QUBITS;PUERTA:QUBITS... (y QUBITS es p.ej 1 o 5,2).
  • Luego se ejecuta el circuito y se miden los valores de los 5 qubits.
  • Se repite la ejecución y medición del circuito 256 veces. El proceso en cada ejecución es el siguiente:
    • Cada vez, los qubits inician en el estado \(\ket{0}\)
    • Se aplican las puertas lógicas especificadas en orden.
    • Al llegar al final del circuito, se miden los estados de los qubits, haciéndoles colapsar a \(0\) o \(1\).
    • El resultado de cada ejecución se guarda en memoria como una cadena de bits en la que cada uno representa el valor de su respectivo qubit en esa ejecución. P.ej 10110

Requisitos
#

Para conseguir el flag, ha de cumplirse lo siguiente:

  • El circuito funciona (evidentemente).
1
2
if not circuit:
    return False
  • Ningún qubit puede dar como resultado siempre 0 o siempre 1 en las 256 ejecuciones. Tienen que comportarse de forma aleatoria.
1
2
3
shares = [ int(share, 2).to_bytes(32, byteorder = "big") for share in shares ]
if any(set(share) in ({0}, {255}) for share in shares):
    return False
  • Los qubits 0, 1 y 3 deben dar exactamente la misma secuencia de ceros y unos en las 256 ejecuciones. Lo mismo para los qubits 2 y 4. Además, la secuencia de los qubits 2,4 debe ser diferente a la del grupo 0,1,3.
1
2
3
4
5
6
7
if (
    shares[0] == shares[1] and
    shares[1] == shares[3] and
    shares[2] == shares[4] and
    shares[4] != shares[0]
):
    return True

Puertas lógicas
#

Las puertas que pone a nuestra disposición el programa son las siguientes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 1 PARÁMETRO
if   gate == "H": circuit.h(params[0]) # 1
elif gate == "X": circuit.x(params[0]) # 2
elif gate == "S": circuit.s(params[0]) # 3
elif gate == "T": circuit.t(params[0]) # 4

# 2 PARÁMETROS
if   gate == "CX": circuit.cx(params[0], params[1]) # 5
elif gate == "CY": circuit.cy(params[0], params[1]) # 6
elif gate == "CZ": circuit.cz(params[0], params[1]) # 7

De 1 parámetro
#

Puerta \(H\)
#

Crea una superposición. Mezcla las amplitudes \(\alpha\) y \(\beta\) de forma que el qubit tenga exactamente un 50% de colapsar a 0 y un 50% de colapsar a 1.

$$ H=\frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix} $$

Puerta \(X\)
#

La NOT vista antes. Intercambia las amplitudes \(\alpha\) y \(\beta\). Es como un giro de 180º en la esfera de Bloch.

$$ X=\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} $$

Puerta \(S\)
#

El estado \(\ket{0}\) no se toca, pero si el qubit tiene alguna probabilidad de ser \(\ket{1}\), se multiplica esa parte por \(i\). Esto equivale a rotar el qubit 90º por el ecuador, sin modificar la probabilidad de colapso.

$$ S=\begin{pmatrix} 1 & 0 \\ 0 & i \end{pmatrix} $$

Puerta \(T\)
#

Exactamente igual que la S, pero en lugar de rotar 90º, rota 45º.

$$ T=\begin{pmatrix} 1 & 0 \\ 0 & e^{i\pi/4} \end{pmatrix} $$

De 2 parámetros
#

Estas son puertas controladas. Trabajan (en este caso) sobre 2 qubits, y por tanto sobre vectores de 4 elementos: \(\ket{00},\ket{01},\ket{10} \text{y}\ket{11}\).

Estos estados se definen mediante el producto tensorial y quedan así:

$$ \ket{00}=\begin{pmatrix} 1 \\ 0 \\ 0 \\ 0 \end{pmatrix},\space\ket{01}=\begin{pmatrix} 0 \\ 1 \\ 0 \\ 0 \end{pmatrix},\space\ket{10}=\begin{pmatrix} 0 \\ 0 \\ 1 \\ 0 \end{pmatrix},\space\ket{11}=\begin{pmatrix} 0 \\ 0 \\ 0 \\ 1 \end{pmatrix} $$

En \(\ket{AB}\), \(A\) es el estado del primer qubit y \(B\) el del segundo. Solo explico la puerta CX porque las otras dos (CY,CZ) son más complejas y no sirven en este reto.

Puerta \(CX\) (CNOT)
#

Si el primer qubit ("qubit de control") es 1, aplica una puerta X al segundo (qubit objetivo).

$$ CX=\begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{pmatrix} $$

En sí, lo que hace es cambiar el orden de las dos últimas filas en la matriz columna 4x1.

  • \(\ket{00}\) se queda como \(\ket{00}\)
  • \(\ket{01}\) se queda como \(\ket{01}\)
  • \(\ket{10}\) se transforma en \(\ket{11}\)
  • \(\ket{11}\) se transforma en \(\ket{10}\)

Creando el circuito
#

Recordando los requisitos para conseguir el flag, necesitamos hacer dos grupos diferentes cuyos elementos siempre den el mismo resultado: Los qubits (\(0,1,3\)) y (\(2,4\)). Además, debe haber aleatoriedad, esto significa que al menos vamos a necesitar puertas Hadamard (\(H\)) para generarla, puesto que todos los qubits inician a \(\ket{0}\).

Para conseguir grupos de qubits con las mismas secuencias, una forma de entrelazar las probabilidades de dos qubits es mediante una puerta \(H\) y otra \(CX\).

Por ejemplo, vamos a entrelazar las probabilidades del qubit 0 y 1 primero. La prueba de esto la hago con SageMath, aunque realmente vale cualquier software similar.

Primero, ambos empiezan como \(\ket{0}\) (ket0)

1
2
3
4
5
6
# Definiendo ket0 y ket1
ket0 = matrix([[1],[0]])
ket1 = matrix([[0],[1]])

qubit0 = ket0 #control
qubit1 = ket0 #objetivo

Aplicamos una puerta \(H\) al qubit de control.

1
2
3
4
5
# definimos la puerta H
H = 1/sqrt(2)*matrix([[1,1],[1,-1]])

# aplicamos la puerta H
qubit0 = H*qubit0

Ahora calculamos el vector de 4 elementos (que une los qubits \(0\) y \(1\))

1
2
3
4
5
6
7
# Función inútil que abstrae una línea.
# Calcula el producto tensorial de los estados del qubit 1 y 2 (ketA,ketB)
def calcularKetsCompuestos(ketA, ketB):
    # ketA = qubit control, ketB = qubit objetivo
    return ketA.tensor_product(ketB)

qubits01 = calcularKetsCompuestos(qubit0,qubit1)

Ahora qubits01 será algo así:

$$ \text{qubits01}=\begin{pmatrix} 1/\sqrt{2} \\ 0 \\ 1/\sqrt{2} \\ 0 \end{pmatrix} $$

Ahora, como sabemos que la puerta \(CX\) invierte el orden de las dos últimas filas, podemos aplicarla a qubits01:

1
2
3
4
5
# Definimos la puerta CX
CX = matrix([[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]])

# La aplicamos a los qubits.
qubits01 = CX*qubits01

Ahora se habrán intercambiado las dos filas inferiores, por lo que qubits01 será:

$$ \text{qubits01}=\begin{pmatrix} 1/\sqrt{2} \\ 0 \\ 0 \\ 1/\sqrt{2} \end{pmatrix} $$

Esto significa que las probabilidades reales son las siguientes (recordemos que son el cuadrado de la amplitud de probabilidad de cada estado):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
print(f"Probabilidad de |00>: {qubits01[0][0]^2}")
print(f"Probabilidad de |01>: {qubits01[1][0]^2}")
print(f"Probabilidad de |10>: {qubits01[2][0]^2}")
print(f"Probabilidad de |11>: {qubits01[3][0]^2}")

# OUTPUT:
Probabilidad de |00>: 1/2
Probabilidad de |01>: 0
Probabilidad de |10>: 0
Probabilidad de |11>: 1/2

Al medir los qubits, pueden darse dos situaciones únicamente, ambas con probabilidad 0.5:

  • Ambos qubits (\(0,1\)) tienen el valor \(0\).
  • Ambos qubits (\(0,1\)) tienen el valor \(1\).

Una vez tenemos esto, lo repetimos con el qubit \(3\).

Como ya hemos aplicado la puerta \(H\) al qubit \(0\), simplemente usamos una puerta \(CX\) con el qubit \(0\) como qubit de control y el qubit \(3\) como objetivo. Esto hará que el qubit \(3\) también dé siempre la misma medición que el \(0\).

Una vez hecho esto, tendremos el primer grupo con las mismas mediciones siempre, (\(0,1,3\)). Las puertas que tendremos que aplicar al conectarnos al servidor:

  • \(H\) a qubit \(0\)
  • \(CX\) a qubits \(0,1\)
  • \(CX\) a qubits \(0,3\)

Como con este primer grupo ya tenemos aleatoriedad verdadera gracias a la puerta \(H\) y necesitamos únicamente que el otro grupo (\(2,4\)) no tenga la misma secuencia, podemos simplemente hacer que sus qubits también tengan el mismo valor que el \(0\) y luego aplicarles una \(X\) (NOT). Esto hará que tengan exactamente la secuencia inversa. Las puertas para esto resultan:

  • \(CX\) a qubits \(0,2\)
  • \(CX\) a qubits \(0,4\)
  • \(X\) a qubit \(2\)
  • \(X\) a qubit \(4\)

Otra solución alternativa sería crear un grupo independiente nuevo aplicando una puerta \(H\) a, por ejemplo, el qubit \(2\), y luego entrelazar la probabilidad del qubit \(4\) con él. La distribución sería diferente, pero la solución valdría igual.

Probamos con la primera, la solución con la sintaxis que espera el programa es:

1
H:0;CX:0,1;CX:0,3;CX:0,2;CX:0,4;X:2;X:4

Además, por curiosidad, el circuito representado (sacado de aquí) sería algo así:

Ahora nos conectamos al servidor y mandamos el circuito.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
> ncat 154.57.164.73 32491

                 _             _       _      _         
                /\ \          / /\    / /\  /\ \        
               /  \ \        / / /   / / / /  \ \       
              / /\ \_\      / /_/   / / /_/ /\ \ \      
             / / /\/_/     / /\ \__/ / /___/ /\ \ \     
            / / / ______  / /\ \___\/ /\___\/ / / /     
           / / / /\_____\/ / /\/___/ /       / / /      
          / / /  \/____ / / /   / / /       / / /    _  
         / / /_____/ / / / /   / / /        \ \ \__/\_\ 
        / / /______\/ / / /   / / /          \ \___\/ / 
        \/___________/\/_/    \/_/            \/___/_/  
                                                
    
Welcome to the Global Hyperlink Zone! The first quantum internet prototype by Qubitrix.
Please send the instructions to initialize the hyperlink.
Specify the instructions : H:0;CX:0,1;CX:0,3;CX:0,2;CX:0,4;X:2;X:4
Hyperlink initialized successfully! Connection ID: HTB{M4ch_3s_s3lbs7!}

Y hemos acabado.

Relacionados

HackTheBox - Data

·6 mins
OS: Linux | Dificultad: Easy | Conceptos: Vulnerabilidad de Directory Traversal en Grafana, Dumpeo de Database, Crackeo de credenciales, Explotación de Regla Sudo de Docker montando el Filesystem del Host

HackTheBox - Orion

·6 mins
OS: Linux | Dificultad: Easy | Conceptos: Versión de Yii Framework expuesta, CVE en CraftCMS, Credencial de MariaDB crackeada, Reutilización de contraseñas, Vulnerabilidad en Telnet

HackTheBox - Nexus

·12 mins
OS: Linux | Dificultad: Easy | Conceptos: Enumeración de vhosts, Reutilización de credenciales, Authenticated RCE, Análisis de código fuente, Path Traversal, Timers de Systemd, Creación de objetos Git