For the complete documentation index, see llms.txt. This page is also available as Markdown.
ADAV-EVASIONDCSYNCDICTIONARY-ATTACKGPO ABUSEINSECURE DESERIALIZATIONLINUXLINUX-PRIVESCPASSWORD PROFILINGSENSITIVE FILE DISCLOSURESHADOW CREDENTIALSSLIVER-C2SQLIWINDOWSWINDOWS-PRIVESC

CTOS

Challenge Lab (Medium) - by Aibel Jaic Aju

The following post by 0xb0b is licensed under CC BY 4.0


Scenario

Objective

CTOS Corporation delivers cutting-edge managed services, cloud solutions, and cybersecurity expertise to clients of all sizes. You have been hired to perform their annual penetration test against 3 high-value targets in the Active Directory environment. Your task is to identify all vulnerabilties and (if possible) elevate your privileges to Domain Admin.

Initial Access

You have been provided VPN access to their internal network, but no other information.

Summary

Summary

In CTOS, we begin without credentials against a three-host environment: a Domain Controller DC01.CTOS.CORP, a Windows workstation IT-WS01, and a Linux web server WEB-01. Initial enumeration yields no unauthenticated access on the Windows hosts, but WEB-01 exposes a web application on port 80 where a Feroxbuster scan reveals a /portal endpoint behind a /login page. A boolean-based SQL injection bypass as admin grants access to the developer portal, which links to the application's source code. Reviewing app.py reveals an insecure pickle.loads deserialization on the session cookie with no sanitization, which we exploit by crafting a malicious pickle payload to gain a reverse shell as phil. A pspy64 scan reveals a cron job run by john executing /opt/backup/backup.sh, which blindly cats a config file from phil's staging directory into a world-readable log. We plant a symlink pointing backup_config to /home/john/.ssh/id_rsa, leaking John's private key into the backup log and granting SSH access. As john, membership in the disk group allows raw block device access, which we leverage through debugfs to dump the root-only /etc/krb5.keytab file, extracting the NTLM hash of the svc_web service account via keytabextract.py. With svc_web, BloodHound enumeration maps the full attack path to Domain Admins, and access to the IT_Onboarding SMB share on IT-WS01 reveals a password policy document describing how initial credentials are constructed. We craft a wordlist from LDAP-queried user attributes and password-spray against the domain, cracking l_conrad's password and gaining a WinRM session on IT-WS01. A PrivescCheck scan identifies the CTOSInventorySvc service with AllAccess for BUILTIN\Users, which we abuse by swapping its binPath to a Sliver service stager, starting the service, and catching a session as NT AUTHORITY\SYSTEM. From the Administrator's Documents folder we recover a KeePass database, crack it with john against rockyou.txt, and retrieve the svc_infra_mgr password. Following the BloodHound path, we use svc_infra_mgr's GenericWrite over it_ops_lead to perform a Shadow Credentials attack via Certipy, recovering the NT hash. As it_ops_lead, we leverage AddMember on policy_automation_group through bloodyAD, then abuse the group's WriteOwner on the Default Domain Controllers Policy via pygpoabuse to create a scheduled task that adds a new Domain Admin. Finally, we authenticate as the newly created user and perform a DCSync through secretsdump.py to retrieve the krbtgt hash.

Recon

We'll start by running a port scan on each host and take a look at the most notable services first.

DC01

We use rustscan -b 500 -a 10.1.237.164 --top -- -sC -sV -Pn to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts -sC, service and version detection -sV, and treats the host as online without ICMP echo -Pn.

A batch size of 500 trades speed for stability, the default 1500 balances both, while much larger sizes increase throughput but risk missed responses and instability.

The target 10.1.237.164 is a Domain Controller, DC01.CTOS.CORP, in the CTOS.CORP domain. DNS is exposed on port 53 and Kerberos on port 88, alongside Kerberos password change on port 464. LDAP and LDAPS services are available on ports 389 and 636, with the Global Catalog on ports 3268 and 3269. SMB is exposed via ports 139 and 445 with message signing enabled and required. Remote management and access are available through RDP on port 3389 and WinRM on ports 5985. The host also exposes AD Web Services on port 9389, RPC over HTTP on port 593, and several MSRPC endpoints on ports 135, 49664-49722. Notably, the LDAP/LDAPS certificates are issued by an enterprise CA named CTOS-DC01-CA indicating Active Directory Certificate Services is present in the environment.

So far, we haven't been able to find anything on the machine without credentials. Nevertheless we will generate a hosts file entry with the following command:

We add the following entry to our /etc/hosts file.

IT-WS01

We use rustscan -b 500 -a 10.1.247.115 --top -- -sC -sV -Pn to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts -sC, service and version detection -sV, and treats the host as online without ICMP echo -Pn.

A batch size of 500 trades speed for stability, the default 1500 balances both, while much larger sizes increase throughput but risk missed responses and instability.

The target 10.1.247.115 is a domain-joined Windows workstation, IT-WS01.CTOS.CORP, in the CTOS.CORP domain. SMB is exposed via port 445 with message signing enabled but NOT required. Remote management and access are available through RDP on port 3389 and WinRM on port 5985. Web Services for Devices (WSDAPI) is exposed on port 5357 (Microsoft HTTPAPI httpd 2.0). The host also exposes the standard MSRPC endpoint mapper on port 135 and a dynamic MSRPC endpoint on port 49668.

Also here we haven't been able to find anything on the machine without credentials. Nevertheless we will generate a hosts file entry with the following command:

We add the following entry to our /etc/hosts file.

WEB-01

We use rustscan -b 500 -a 10.1.234.105 --top -- -sC -sV -Pn to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts -sC, service and version detection -sV, and treats the host as online without ICMP echo -Pn.

A batch size of 500 trades speed for stability, the default 1500 balances both, while much larger sizes increase throughput but risk missed responses and instability.

The target 10.1.234.105 is a Linux host running Ubuntu. SSH is exposed on port 22 and a web server on port 80 is hosting a "Home | Enterprise Technology Solutions" page.

We'll take a closer look at the website hosted on port 80 and start by visiting it in our browser. At first glance, it seems to be a static page with not much to offer.

We visit every link we can find and come across a contact form. We could try to leverage some blind XSS to hijack the session of a user who reviews the messages, since we'll see right away that there is a login page. However, we are not successful in doing so.

We continue with a directory scan using Feroxbuster and do find a /portal endpoint that redirects to a /login page.

Access as admin on Developer Portal

We're visiting the /login page...

They attempt to log in as admin using a boolean-based SQL injection... and succeed.

We have now access to the developer portal.

Shell as phil on WEB-01

If we scroll down a bit, we'll find some interesting links. Among them is one that reveals the source code of the page, which was originally provided for an IT audit.

We'll download the archive and unzip it.

We take a look at app.py and find an insecure deserialization vulnerability without any sanitization or other countermesaures. The session cookie is loaded via pickle.load, which would allow us to execute arbitrary Python code on the server since we are in control of the cookie.

We generate a pickle payload that executes a reverse shell using the following script. Pickle deserialization is exploitable because the __reduce__ method lets us control what gets called during unpickling. Here we return (os.system, (CMD,)), so when the target deserializes our object, it executes CMD via os.system.

We hadn't had much success getting a bash reverse shell to execute, so we try to run a Python reverse shell using Python, which we grabbed from revshells.com.

We craft the paayload.

Next, we set up our listener to catch the reverse shell. We are using Penelope.

Now we replace the value of the ctos_session cookie with our payload and reload the page. We get a 500 internal server error, but we also receive a conneciton to our listener.

We try to run some commands, but they fail. the PATH environment variables are not set. We fix this by setting it manually. We are the user phil.

Shell as john on WEB-01

While running a scan with pspy64, we noticed that a job pops up by the user with ID 1001 running /opt/backup/backup.sh.

The user that runs the job is john. This might be the opportunity to move lateraly to john since there seem no other options to escalate from phil.

We see that we are able to read the script that is run by john. The script creates a zip backup of /opt/ctos_portal and writes it to the static/ directory, then checks phil's staging directory for a backup_config file. If its present, it dumps that file's contents into a world-readable log and deletes the file.

Because the script blindly cats $CONFIG_FILE into a readable log, we can replace backup_config with a symlink to any file we want to read only readable by john, and on the next run the script follows the link and leaks that file's contents into backup.log.

We try to read the SSH private key of john and create a symlink as follows.

After a short duration we inspect the backup.log and find the private key of john.

Next, we save the content of the private key to our machine and adjust the permission. Now we are able to SSH into WEB-01 with the private key. On our id check we see that john is member of the disk group, which might come in handy later since that will allow us to read any file through the access to the underlying block devices. But more on that later.

Access as svc_web on IT-WS01

The Linux machine appears to be domain joined. We have a krb5.conf and a krb5.keytab file in place. We've already seen something similar in the Anomaly Scenario:

The krb5.conf file configures Kerberos authentication for the CTOS.CORP realm, specifying 172.16.11.140 as both the KDC and admin server. DNS-based discovery of realms and KDCs is disabled, and tickets are set to a 24-hour lifetime with 7-day renewable periods. The domain ctos.corp and its subdomains are mapped to the CTOS.CORP Kerberos realm.

Besides the krb5.conf file we have a keytab file krb5.keytab.

A keytab is a file that stores one or more Kerberos principals and their encrypted secret keys so a service or script can authenticate to a Kerberos KDC non-interactively. Because it contains credentials, a keytab must be tightly protected restricted permissions and secure storage to prevent unauthorized access or impersonation. Checking the privileges only root has read access to the keytab file.

Recalling our previous find, we know we are member of the disk group. Membership in the disk group grants us raw read/write access to the underlying block devices bypassing file permissions entirely.

We list the block devices to identify the root filesystem partition and identify /dev/nvme0n1p2 as the root partition.

The tool debugfs is a filesystem debugger that lets us traverse and read the filesystem directly through the block device, bypassing file permissions entirely. Using it, we can copy a root-only file like the keytab file to /tmp and are able to read if afterwards.

With the following command we are able to list the entries in our retrieved /tmp/krb5.keytab showing each principal, timestamp, and the stored keys in hexadecimal with their key versions. We are able to identifiy the principal svc_web.

Next we want to extract the secrets from the keytab file. The following article shows how to use Keytab files in a red team engagement including extracting the NTLM hash from a keytab file:

We spin up a web server on the target machine in the /tmp directory to exfiltrate the keytab file.

Next, we retrieve the keytab file.

After downloading the keytab file we try to extract the NTLM hash using the tool suggested in the article mentioned before. We are able to retreive the NT hash of svc_web.

We test the credentails of svc_backup using NetExec and successfully authenticate. We also notice an IT_Onboarding share that is readable that might hold valuable information like default credentials.

BloodHound Enumeration

Now that we have credentials we'll enumerate the domain using BloodHound. To run the the collector with the hash we need to add the LM hash of an empty string: aad3b435b51404eeaad3b435b51404ee.

We inspect the user we just compromised, but do not find any special privileges or memberships.

The only Domain Admin is the Administrator user.

If we query for the shortest path to Domain Admins we get a glimpse of how we might to proceed later. The svc_infra_mgr account has GenericWrite permissions over the it_ops_lead user, which allows us to perform a targeted Kerberoast or shadow credential attack to gain access to it_ops_lead. That user, in turn, has an AddMember permission over the policy_automation_group, which can WriteOwner on the Default Domain Controllers Policy, allowing to escalate through GPO abuse to create a new admin user via a scheduled task. If we can compromise it_ops_lead, we can add the user to the policy_automation_group and follow the depicted attack path to compromise the domain.

Shell as l_conrad on IT-WS01

However, since we currently have no path to escalate to the high value users found in our BloodHound analysis, we must proceed differently. During our credentials test, we had already determined that the user svc_web has read access to the IT_Onboarding share, which may contain default credentials or other sensitive information. We connect to the share using smbclient.py and retrieve the SEC-POL-2026.pdf document.

This policy describes of how the temporary intial password is constructed.

We take note of the conditions to craft a script to generate a wordlist for all the users present in the domain. We also need the first and lastname of the users to construct the password...

Using NetExec's --query option we perform a raw LDAP query to retrieve all users with their first and last names.

Next we craft a script to generate a wordlist of passwords to the corresponding usernames.

Since credentials.txt contains username:password pairs andNetExec cant use that format as a combined credential file, we split it into the two lists. Next, we perform a password spray with the resulting wordlists and do find a valid credential pair for the user l_conrad.

We inpect our BloodHound results an mark the user as owned, but the user does not hold any special privileges or group memberships either.

Nevertheless we try to get a session using evil-winrm on IT-WS01 as l_conrad and are successful.

Shell as NT AUTHORITY\SYSTEM on IT-WS01

Now with a remote session we use the latest version of PrivescCheck to enumerate the target machine.

We download the resulting report...

And we see that there is a service called CTOSInventorySvc that BUILTIN\Users has AllAccess over this service through the SCM, meaning our current user can modify it. Since AllAccess includes rights like SERVICE_CHANGE_CONFIG and SERVICE_START, we can repoint the service binary path to a payload of our choice and start it to execute it as LocalSystem.

So, as the next step, we'll try to change the binPath service to our Sliver service stager, which we've already used in the Kiosk scenario and then start the service to establish a session in Sliver as an NT/AUTHORITY SYSTEM.

We grab the service stager we build for the Kiosk lab:

Prepare a custom stager

First we need to prepare the stager. This is the one we created for the lab Kiosk built into a service boiler plate.

The stager fetches raw shellcode from a chosen URL and loads it directly into memory as bytes. The payload is not embedded in the binary, allowing it to be changed without recompiling.

It calls VirtualAlloc to reserve and commit memory with execute, read, and write permissions, then copies the downloaded shellcode into that memory using unsafe pointer operations.

The execution is transferred to the allocated memory address using syscall.Syscall, handing control to the shellcode.

We compile the stager as follows on our exegol instance:

The addition of -ldflags="-H windowsgui" ensures that we compile it as an application and not a console app, so it wont spawn a terminal.

Since the stager got caught due to sample submission, but want to reuse it, we use a simple trick and use the -s -w ldflags for compilation. Using -s -w strips debug symbols and DWARF metadata from the Go binary, changing its hash and reducing recognizable signatures, which can help avoid detection because antivirus engines that previously flagged the original sample submission may no longer match the modified compiled file exactly. We might see this neat trick later again.

Generate shell code

During generation without the -G tag, which disables the encoder, no shellcode could be successfully generated. The resulting shellcode was always empty. This may be related to the underlying architecture on which I am operating, namely ARM:

https://github.com/BishopFox/sliver/issues/1114

Next, we need run the Sliver server and generate the shell code to connect to our sliver instance.

We do this as follows:

We run a web server from which the stager and the shellcode can be fetched.

Setup listener

We set up the listener in sliver as follows:

Finally we upload the stager, change the binPath of the service to point to our stager and run the service.

We get a session and interact with it as follows.

We are NT AUTHORITY\SYSTEM on IT-WS01.

Access as svc_infra_mgr

In the administrator's Documents folder, we find a KeePass file. We download it directly.

On our machine we generate a hash using keepass2john to eventually crack the keepass file. It turns out the file is poorly secured by a weak password

We unlock the keepass file with the cracked password and are able to retrieve the svc_infra_mgr users password.

Next, we test the credentials against SMB using NetExec and successfully authenticate.

Access as it_ops_lead

We recall our finding from our intial BloodHound enumeration and see that we can now follow the path we have intially uncovered:

If we query for the shortest path to Domain Admins we get a glimpse of how we might to proceed later. The svc_infra_mgr account has GenericWrite permissions over the it_ops_lead user, which allows us to perform a targeted Kerberoast or shadow credential attack to gain access to it_ops_lead. That user, in turn, has an AddMember permission over the policy_automation_group, which can WriteOwner on the Default Domain Controllers Policy, allowing to escalate through GPO abuse to create a new admin user via a scheduled task. If we can compromise it_ops_lead, we can add the user to the policy_automation_group and follow the depicted attack path to compromise the domain.

We perform a shadow credentials attack on it_ops_lead and are able to retrieve the NT hash.

Next, we test the credentials against SMB using NetExec and successfully authenticate.

Access as Administrator on DC01

With access as it_ops_lead we add the user to the policy_automation_group using bloodyAD.

Next, we leverage the WriteOwner permission over the default domain controllers policy by using the pygpoabuse to create a scheduled task to create a new admin user. This requires the gpo-id. We have that already from our BloodHound enumeration.

After a short duration we test if the gpo has been applied and the scheduled task has been run. It has and we see we can now authenticate with our newly created user.

Finally we can perform a DC sync attack on the domain controller and retrieve the hash of the krbtgt user.

Last updated