L’ECW 2018 est un challenge Jeopardy français organisé par le Pôle d’Excellence Cyber en partenariat avec la Région Bretagne, Airbus et Thales. Intrusion est un scénario web en 4 challenges (+1 extra-challenge) se voulant réaliste. Il tourne principalement autour de Ruby on Rails et de la manipulation de cookies afin de passer administrateur sur le site de production.
Du 06/10 au 21/10 2018, il sert de pré-sélection pour l’épreuve finale de type Capture The Flag qui débutera le 21 novembre 2018 lors de la conférence de l’European Cyber Week à Rennes.
Challenge particulièrement intéressant sur plusieurs points. Il m’a permis d’apprendre plusieurs petits tricks. Qui plus est, j’ai passé énormément de temps dessus. Let’s go !
Table des matières :
Intrusion 1
Découverte et Recherche d’informations
Le challenge débute avec l’énoncé suivant et le site web d’une société.
Cette startup est prometteuse!
Ils proposent de collecter de nombreux identifiants dans un seul endroit...
Peut-on s'introduire dans leur system?
Une page d’authentification et une page “Work in Progress” sont disponibles. Quelques tests sur le formulaire d’authentification ne révèlent pas de vulnérabilités à ce niveau. Qui plus, l’indication “support us with cookies :)” de la page indique clairement que le challenge va tourner autour des cookies.
Après quelques recherches on inspecte les fichiers qui sont utilisés au chargement du site web.
Domaine de développement et Flag 1
Rien d’anormal… Sauf dans le fichier “thor.css” où un paragraphe semble avoir été rajouté manuellement.
Un nouveau sous-domaine ! Il semblerait s’agir du domaine de développement de l’application.. Peut être des vulnérabilités à exploiter ?
On file sur le sous-domaine et en observant les headers…
Premier flag : check !
Intrusion 2
Comme ça vous êtes arrivé dans un lieu inattendu?
Continuez de creuser!
Manipulation des requêtes HTTP
Nous sommes maintenant sur le domaine de développement et il faut trouver quoi faire. Une bonne première chose peut être de regarder de quoi les requêtes HTTP ont l’air et s’il est possible de les bidouiller un peu. Après quelques tentatives, on remarque un comportement étrange lorsqu’on modifie la méthode HTTP utilisée.
Ainsi, en utilisant la méthode “OPTIONS” on fait apparaître une étrange page d’erreur, très verbeuse… On a par exemple accès à notre cookie déchiffré et à certaines variables côté serveur.
Obtention d’une web console (CVE-2015-3224)
Ok, de nouvelles informations.. Que peut on en tirer ?
Une chose doit nous sauter aux yeux (bon, dans la vraie vie, cela m’a pris un peu de temps avant de le remarquer…) et il s’agit de la ligne :
HTTP_X_FORWARDED_FOR: "176.187.238.160, 10.1.10.0, 127.0.0.1"
Avec ça, il devrait être possible de spoofer une IP interne et ainsi se faire passer pour une machine interne au réseau !
On intercepte donc une nouvelle requête à laquelle on ajoute un header X-Forwarded-For: 127.0.0.1
et magie…
Une console web ! Cela pourrait s’avérer utile, car il s’agit d’une console qui va nous permettre d’exécuter des commandes côté serveur. J’ai appris après-coup qu’il s’agit d’une vulnérabilité connue, la CVE-2015-3224.
Bien ! Cependant, il a été nécessaire de modifier manuellement la requête afin d’obtenir cette console. Pour des soucis de confort, on aimerait que toutes les requêtes soient modifiées avec le header précédemment utilisé. Pas de soucis ! On configure Burp en spécifiant le scope (web150_dev.challenge-ecw.fr) et en lui disant de rajouter un header pour chaque requête. Une fois cela fait, on peut tester le bon fonctionnement du dispositif.
Parfait ! On passe à la suite
Fouille et Flag 2
On dispose maintenant d’un accès au serveur, via une sorte de shell. Que peut on faire ? Après quelques recherches au sujet de la console Rails, on découvre un moyen de lister le contenu d’un répertoire :
Il faut maintenant fouiller le serveur à la recherche d’informations intéressantes.. A force de parcourir les répertoires on tombe sur un fichier “web_console.rb” situé dans un des répertoires de configuration. Il s’agirait du point de départ logique étant donné que nous sommes dans cette console. Il est également possible de lire le contenu d’un fichier de la manière suivante.
Beaucoup d’informations contenues là dedans, notamment une longue chaine en hexadécimal. Une fois décodée…
466c616732203d20274543577b35393438343632323131643030633963656334363866643139346537366335667d27 ==> "Flag2 = 'ECW{5948462211d00c9cec468fd194e76c5f}'""
Second flag : check !
Intrusion hint
Découverte et Identification
Ce challenge a été rajouté en cours de route, afin d’aider les participants à avancer dans le scénario. A priori totalement décorrélé de l’environnement du scénario, on tombe sur une page unique présentant une boîte de dialogue ainsi que certains inscriptions relativement claires.
Il semble que nous soyions face à une injection SQL, étant donné les indications. On teste le comportement de l’application avec des données légitimes.
Les indications semblent orienter vers une Injection SQL utilisant la clause LIKE. En supposant que la requête utilisée par l’application soit de la forme :
SELECT <hint> from <table> where <message> LIKE "<entree utilisateur";
On peut tenter d’injecter le caractère %
dans la requête. Si injection il y a, l’application devrait nous répondre avec le nombre de lignes à récupérer.
Exploitation et Résultats
On a donc un POC fonctionnel. Afin de tester et encore une fois, en supposant qu’un flag de la forme “ECW{xxx}” soit caché, on peut tenter l’injection suivante E%
.
Parfait ! De cette façon, il va être possibilité d’énumérer en aveugle l’ensemble des indices cachés, en se basant sur le message retourné à l’utilisateur.
Exemple :
E% ==> "1 hint found in database"
EC% ==> "1 hint found in database"
ECA% ==> "O hint found in database"
ECW% ==> "1 hint found in database"
Ci-dessous, un script permettant de semi-automatiser le processus. En effet, il n’est pas rare d’avoir une requête qui ne passe pas. Etant donné que mon script ne gère pas les erreurs, il est nécessaire de le relancer en ajoutant manuellement les morceaux déjà récupérés.
import requests
import string
import sys
# Request needed informations
BASE_URL = "https://web150-hint.challenge-ecw.fr/search"
DATA = {
'request': '',
}
# Static variables
FALSE_KEYWORD = "0 hint found in database"
VULNERABLE_PARAM = "request="
INJECTION_USED = "request=E%"
# Password variables
result = ""
tmpResult = ""
charset = string.printable.replace("%", "").replace("i", "")
#charset = string.ascii_letters + string.digits + ".://{}"
if __name__ == "__main__":
print("\n##########################")
print("## ECW 2018 - SQLi ##")
print("############################\n")
print("[+] URL injected : %s" % BASE_URL)
print("[+] Vulnerable parameter : %s" % VULNERABLE_PARAM)
print("[+] Injection used : %s" % INJECTION_USED)
print("\n[+] Starting bruteforce\n")
for firstLetter in charset:
result = ""
DATA['request'] = firstLetter + "%"
req = requests.post(BASE_URL, data=DATA)
if FALSE_KEYWORD not in req.text:
#result += firstLetter
result = ""
#print("FOUND ONE ! letter : %c" % firstLetter)
for i in range(0,100):
for letter in charset:
DATA['request'] = result + letter + "%"
req = requests.post(BASE_URL, data=DATA)
if FALSE_KEYWORD not in req.text:
result += letter
print("result : %s" % result)
break;
Et voici maintenant un exemple de récupération d’un des indices.
Une fois l’ensemble des éléments ressortis, on obtient cela :
http://manpages.ubuntu.com/manpages/cosmic/en/man1/systemctl.1.html
https://gist.github.com/mbyczkowski/34fb691b4d7a100c32148705f244d028
/home/web200/smart_stuff/config/initializers/web_console.rb
/home/web200/smart_stuff/config/secrets.yml
ECW{ebbbb414c38020906fd34bdd49ceea36}
Flag intermédiaire : check !
Intrusion 3
Certaines fois privesc no signifie pas être root...
Ce flag est un point de passage vers la prochaine étape (ECW{<md5>})
Déchiffrement des cookies
Dans l’ordre, j’ai d’abord récupéré le lien vers le gist de Github. Ce dernier nous présente un morceau de code permettant de déchiffrer certains cookies en Ruby. Peut être est-ce le même système utilisé dans notre cas ?
require 'cgi'
require 'json'
require 'active_support'
def verify_and_decrypt_session_cookie(cookie, secret_key_base)
cookie = CGI::unescape(cookie)
salt = 'encrypted cookie'
signed_salt = 'signed encrypted cookie'
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
secret = key_generator.generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len]
sign_secret = key_generator.generate_key(signed_salt)
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
encryptor.decrypt_and_verify(cookie)
end
En se basant sur cette hypothèse, plusieurs éléments sont nécessaires, à savoir :
- Le sel de chiffrement ;
- Le sel de signature ;
- La clé permettant de chiffrer/déchiffrer le cookie.
Il faut donc trouver ces éléments. Pour cela, on repart dans une phase de recherche sur les fichiers du serveur, en commençant par le second fichier récupéré en indice : /home/web200/smart_stuff/config/secrets.yml
Bien ! Il semble que nous ayons trouvé des clés de chiffrement. Une pour le domaine de dev, et une pour autre chose (utile ?). Il nous manque donc les sels. A force de recherche, on tombe sur un fichier intéressant.
Deux variables encrypted_cookie_salt='ECW-secret-salt'
et encrypted_signed_cookie_salt='ECW-signature-secret-salt'
peuvent être ressorties. Parfait ! Nous avons tout ce dont nous avons besoin pour déchiffrer notre cookie.
Après de nombreux essais, tests et tentatives afin de déchiffrer un cookie en local (via le second script donné), je me suis plutôt tourné vers un traitement directement depuis la console web. Il s’avère que cela passe très bien avec le premier script donné.
Il est également à noter que le script nous retourne par défaut des données formatées en JSON. Or, dans notre cas, il s’agit de Marshal. Il m’a fallu un peu de temps avant de trouver pourquoi le cookie était mal déchiffré et surtout pour trouver ce qu’était du Marshal…
Forge et connexion au domaine de développement
On sait maintenant de quoi est fait le cookie. On peut y apercevoir un objet “User” ne contenant que des champs “nil” (=null). Logique étant donné que nous n’avons pas d’utilisateur. Mais… Serait il possible d’en usurper un, avec des fausses informations ?
Connaissant les différents éléments utilisés afin de déchiffrer le cookie, nous devrions être en mesure de pouvoir le chiffrer à nouveau.
La modification de l’objet User se fait de la façon suivante :
cookie['user']['password'] = "YouLostTheGame"
De cette façon, on peut modifier tous les champs nécessaires.
Une fois le cookie forgé, on peut tenter de modifier son propre cookie de session en interceptant une requête avec Burp. Résultat…
Connecté en admin sur le domaine de développement ! POC fonctionnel :)
Récupération du Flag 3
On sait maintenant comment déchiffrer un cookie, le modifier puis le chiffrer à nouveau afin de l’utiliser pour se connecter. Mais nous n’avons toujours pas d’information sur la localisation du flag 3…
On sait néanmoins que le but du challenge est de se connecter au domaine de production et l’énoncé du challenge 3 indique que le flag est placé sur un point de passage vers le dernier challenge. A partir de là, on se doute qu’il faut retrouver la clé de chiffrement du domaine de production.
Un indice n’a pas encore été utilisé.. Il s’agit de la page de manuel de “systemctl”. Peu de chances de pouvoir utiliser directement l’utilitaire au travers de notre console web. Cependant, il est possible que certaines configurations soient stockés dans des fichiers. Après quelques recherche sur systemctl, on apprend que des “units” peuvent être créées pour systemd. Ces “units” se comportent selon des directives écrites dans un fichier de configuration, situé dans le répertoire /etc/systemd/system/
. Et si nous allions faire un tour de ce côté ?
[NOTE] Je ne l’avais pas vu car je n’avais pas débloqué ce challenge avant de faire le hint, mais il s’avère que des indications sur l’utilisation de systemd sont également trouvables, notamment dans le fichier web_console.rb
On y trouve deux fichiers rails-dev.service
et rails-prd.service
. Rien de bien intéressant côté dev, cependant, du côté du fichier de configuration de la production…
La clé de la production ! Qui fait également office de flag pour le 3e challenge.
Troisième flag : check !
Intrusion 4
Le dernier Flag, vous devriez savoir où il est caché désormais!
Mais l'attraper est plus facile à dire qu'à faire ;)
Forge d’un cookie pour le domaine de production
Disposant maintenant de tous les éléments nécessaires. On commence par déchiffer le cookie que l’on récupère sur le domaine de production. Cookie qui semble d’ailleurs bien plus court que les précédents.
Pas d’utilisateur pour ce cookie.. Mais qu’est ce qui nous empêche d’en ajouter un ?
On reproduit la même procédure que pour le cookie de développement en modifiant simplement la clé de chiffrement par secret_key_base = "A_cookie_of_course"
et une fois le cookie forgé, on tente de l’injecter en interceptant une requête.
Connexion et Flag 4
Résultat du nouveau cookie…
Nous sommes connectés en administrateur sur le domaine de production ! Il suffit maintenant de chercher le dernier flag :).
La page /admin/console
est inaccessible et redirige vers l’accueil. Cependant, en fouillant dans les fichiers de l’application, on trouve un /admin
.
Je n’ai plus la capture d’écran sous la main, mais un accès à la page https://web150-smartstuff.challenge-ecw.fr/admin
retourne une page contenant le flag !
Flag 4 : check !! (Pour une raison que j’ignore, le flag n’était plus présent lorsque j’ai refait le challenge pour prendre des screenshots ¯_(ツ)_/¯).
Conclusion et RETEX
Un challenge plutôt intéressant et agréable dans son ensemble malgré quelques points négatifs, la position des flags. En effet, ces derniers étaient placés à des endroits qui nécessitaient peut être un peu de chance ou de guessing. Néanmoins, j’ai pesonnellement passé énormément de temps sur ce scénario et appris pas mal de petites choses ! GG aux organisateurs et à tous les participants !