NovaForge
Challenge Lab (Hard) - by 2ubZ3r0
The following post by 0xb0b is licensed under CC BY 4.0
Scenario
Objective
NovaForge delivers enterprise software, intelligent automation, and internal management platforms targeted towards mid-sized companies. They have hired Hack Smarter to perform an internal network penetration test, with a special focus on Active Directory.
Your task is to enumerate the network and see if you can fully compromise the domain.
Initial Access
The client has provided you with VPN access to their network, but no credentials.
Summary
Summary
In NovaForge, we begin with internal enumeration of two hosts a Domain Controller DC.novaforge.local exposing DNS on port 53, Kerberos on 88, LDAP on 389 and 636, SMB on 445, IIS on 80, hMailServer SMTP on 25 and 587, POP3 on 110, IMAP on 143, RDP on 3389, and WinRM on 5985; and a file server STORAGE.novaforge.local exposing SMB on 445, RDP on 3389, and WinRM on 5985. Initial SMB enumeration fails without credentials, so we pivot to the web service and discover the vhosts intranet.novaforge.local and jobs.novaforge.local via FFuF fuzzing. The intranet portal warns about credential theft via XML files, while the jobs portal instructs applicants to email john.doe@novaforge.local with attachments in a whitelist that includes XML.
We generate a malicious XML file with ntlm_theft.py, spin up Responder, and use swaks to send the attachment via the unauthenticated SMTP relay on the DC. The user john.doe opens the file, leaking an NTLMv2 hash that we crack with hashcat. Authenticated SMB access to STORAGE reveals a Shared folder containing a browser security policy mandating Opera and an access overview document identifying ryan.collins (remote management) and chuck.harrys (sysadmin with access to an internal portal on port 5000).
We run BloodHound as john.doe and find no direct paths, only a Kerberoastable svc_it_admin with uncrackable AES-256 encryption. Falling back to bloodyAD get writable, we discover write access over a deleted m.lee object, which we restore from the Deleted Objects container. A second BloodHound collection reveals a long chain from john.doe to the TIER1-SUPPORT OU: we abuse GenericWrite on m.lee via targeted Kerberoasting with an msDS-SupportedEncryptionTypes downgrade to RC4, crack the resulting $krb5tgs$23$ hash, then chain WriteDACL over steve.wills, WriteOwner over IT Support Users, ForceChangePassword over noah.sanders, GenericAll over steve.miller, and finally GenericAll over the OU containing ryan.collins and daniel.brooks, resetting passwords along the way.
daniel.brooks is a member of Remote Management Users and grants us a WinRM shell on the DC, where we find the user flag alongside a link to Opera. Recalling the browser policy, we upload DumpBrowserSecrets.exe and dump OperaData.json, recovering the credentials of chuck.harrys. We pivot to STORAGE as ryan.collins via WinRM, confirm the internal service listening on 127.0.0.1:5000, and use Chisel to establish a reverse port forward back to our attacker machine. Logging into the admin portal as chuck.harrys, we disable SMB signing on STORAGE.
With signing disabled, we exploit CVE-2025-33073 for NTLM reflection on Windows Server 2025. As daniel.brooks, a member of the NovaForge DNS Operators group, we use bloodyAD to plant a crafted DNS A record pointing an encoded hostname at our machine, spin up ntlmrelayx targeting smb://storage.novaforge.local, and coerce authentication using NetExec's coerce_plus module. The relayed authentication yields the local Administrator hash on STORAGE. We WinRM in as Administrator and discover david.cokx's credentials in ConsoleHost_history.txt, then dump LSA and SAM to recover frank.white.
BloodHound shows frank.white holds ForceChangePassword over svc_it_admin, and david.cokx can modify the Protected Users group. We reset svc_it_admin, remove it from Protected Users as david.cokx to lift the NTLM authentication block, and confirm access. From here, we exploit svc_it_admin's WriteSPN over both STORAGE$ and DC$, combined with its constrained delegation to storage.novaforge.local, to create an SPN-jacking scenario. We clear the cifs/STORAGE.novaforge.local SPN from STORAGE$ using addspn.py, relocate it onto DC$, then use getST.py with S4U2self and S4U2proxy to impersonate the Administrator, leveraging the -altservice flag to retarget the ticket to CIFS/DC.novaforge.local. Pass-the-Ticket via psexec.py grants us a SYSTEM shell on the DC, yielding the root flag at C:\Users\Administrator\root.txt.
Recon
Storage
We use rustscan -b 500 -a 10.0.0.101 --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.0.0.101 is a host named STORAGE.novaforge.local, a member of the NOVAFORGE domain. SMB is exposed on port 445 with message signing enabled and required, and port 139 was not found open in this scan. RPC endpoint mapping is available via MSRPC on ports 135 and 49676. Remote management and access are available through RDP on port 3389 and WinRM on port 5985, the latter identifying only as Microsoft-HTTPAPI/2.0 with no title.

We try to authenticate anonymously and via guest against SMB using NetExec, but without success. The account is disabled. Nevertheless, we generate a hosts file entry with the following command:

We add the following entry to our /etc/hosts file.
DC
We use rustscan -b 500 -a 10.0.0.100 --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.0.0.100 is a Domain Controller DC.novaforge.local for the domain novaforge.local. DNS is exposed on port 53 via Simple DNS Plus, 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. A web server (Microsoft IIS 10.0) is hosting a "NovaForge | Dark Enterprise Suite — Video Loop" page on port 80. Mail services are also present, with hMailServer providing SMTP on ports 25 and 587, POP3 on port 110, and IMAP on port 143. Remote management and access are available through RDP on port 3389 and WinRM on ports 5985. The host also exposes a .NET Message Framing service on port 9389, RPC over HTTP on ports 593 and 49669, and numerous additional MSRPC endpoints on ports 135, 49664–49671, 49686, 49695, 49707, and 49711.

We try to authenticate anonymously and via guest against SMB using NetExec, but without success. The account is disabled. Nevertheless, we generate a hosts file entry with the following command:

We add the following entry to our /etc/hosts file.
We proceed to the website on port 80 on the domain controller and initially find only a static page. For now, we do not find anything useful here.

Also, a directory scan does not reveal any useful endpoints.
We perform a VHOST scan using FFuF and are able to identify the vhosts intranet and jobs.

We add the following to our /etc/hosts file:
The intranet site also appears to be purely static and does not provide any other specific endpoints. However, there is an indication that attackers are stealing credentials via XML files... possibly through NTLM theft.

The jobs site also appears to be just a static page.

However, if we select one of the open positions, we are directed to a form to submit our application via email. It doesn't seem to be working, though. We do find some valuable information here. Applications are received via email and reviewed manually by john.doe@novaforge.local.
Only certain file extensions are allowed, but XML is one of them. This matches the file type mentioned on the intranet portal.

Access as john.doe
Although macro files are not allowed, which prevents direct code execution, we may still be able to use NTLM theft to get the hash of the user who opens the file.
NTLM theft works by embedding a reference to a remote resource inside a file. When the file is opened by the victim, their machine automatically tries to authenticate to that resource, leaking their NetNTLM hash. We can then try to crack this hash.

We prepare the file using ntlm_theft.py.

We spin up Responder to catch the hash.

Next, we use swaks to send an email with the ntlm_theft XML file attached to john.doe@novaforge.local from an arbitrary mail account. It looks like no authentication is required, and the email is sent successfully.

After a short delay, we retrieve the hash of the user john.doe.

We try to crack the NTLMv2-SSP hash and are successful.

We try to authenticate with the credentials against SMB on the storage and DC machines using NetExec while enumerating the shares. We successfully authenticate. We also spot the Shared share, to which we have read access.

We connect to the share using smbclient.py and are able to retrieve two PDF files.

The NovaForge Browser Security Policy.pdf requires all employees and contractors to use the Opera browser for internal services and not to save their credentials in it. We also learn that there might be an internal administration portal on the storage machine at port 5000. We did not see this port in our port scan.
So that means we should perhaps consider - if we gain remote access to the storage - trying to extract credentials from the respective users' Opera browsers and try to log into the internal portal.


The StorageAccessOverview.pdf lists several users who might be helpful. Of particular note are ryan.collins, who has remote management access, and chuck.harrys, who is a system administrator and has access to the internal system. We may be able to get credentials for the internal portal through access to chuck.harrys.

BloodHound Enumeration I
Now that we have a domain user's credentials and access, we start enumerating the domain using BloodHound.

We query for all Domain Admins but only find the Administrator user as the domain admin.

We inspect our owned user, john.doe. This user does not hold any special privileges, outbound object control, or membership in a special group. It looks like we may have hit a wall.

We look for Kerberoastable users and do find the user svc_it_admin as such.

We could obtain the $krb5tgs$18$ blob of svc_it_admin through Kerberoasting, which corresponds to AES-256 encryption. Unfortunately, this hash type is too strong to crack offline within a practical timeframe.
As a last resort we enumerate the writable objects to identify an alternative path that is not covered in the BloodHound data:
CN=Users (CREATE_CHILD): We can create new objects, such as users or groups, inside this container.
CN=Deleted Objects (CREATE_CHILD; WRITE / WRITE OWNER / WRITE DACL): We have full control over the recycle bin, so we can take ownership, rewrite its permissions, and manipulate deleted objects.
CN=S-1-5-11 in ForeignSecurityPrincipals (WRITE): We can modify this object, which represents the Authenticated Users group.
CN=john.doe (WRITE): We can edit our own account's attributes.
CN=m.lee (deleted) (WRITE): We can modify the deleted m.lee account, which lets us restore and take control of it.

Restore m.lee
As john.doe, we abuse our write access over the deleted m.lee object to restore the account from the Deleted Objects container.

BloodHound Enumeration II
Let's enumerate the domain again, since the user m.lee was missing from our BloodHound data before we recovered it.

Now we see that the user john.doe has GenericWrite permissions over m.lee.

Let's query for the Shortest Path from Owned Objects.
The Shortest Path from Owned Objects reveals a chain of Active Directory permissions that allows movement from one account to the next, starting from the owned john.doe account.
The john.doe account holds GenericWrite over m.lee, which allows control of m.lee through a targeted Kerberoast attack.
The user m.lee holds WriteDACL over steve.wills, which allows full rights to be granted over the account and control of steve.wills to be taken.
The user steve.wills holds WriteOwner over the IT Support group, which allows ownership of the group to be claimed, membership rights to be granted, and the account to be added to IT Support.
Membership in IT Support grants ForceChangePassword over noah.sanders, which allows the account's password to be reset and control of noah.sanders to be taken.
The user noah.sanders holds GenericAll over steve.miller, which allows full control of the account.
Finally, the user steve.miller holds GenericAll over the TIER1-SUPPORT Organizational Unit, which allows control of the OU and the objects it contains, namely ryan.collins and daniel.brooks.

Shell as ryan.collins / daniel.brooks on DC
We follow the attack path depicted above to obtain a shell as ryan.collins.
Access as m.lee
First, we gain access as m.lee through a targeted Kerberoast attack. Since we hold GenericWrite over m.lee, we can set a temporary Service Principal Name on the account, request a service ticket for it, and capture the crackable hash.

Using our GenericWrite over m.lee, we downgrade the account's supported encryption types by setting msDS-SupportedEncryptionTypes to 4, which forces the KDC to issue RC4-encrypted $krb5tgs$23$ tickets instead of AES:
With the weaker encryption type now enforced, we re-run the targeted Kerberoast against m.lee to obtain a crackable RC4 hash:

We crack the hash and gain the password of m.lee.

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

Access as steve.wills
After cracking the RC4 hash to recover the password for m.lee, we abuse our WriteDACL over steve.wills to grant m.lee FullControl over the account:
We confirm and apply the GenericAll right for m.lee over steve.wills:
With full control established, we reset the password of steve.wills to take over the account:

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

Access as noah.sanders
To gain access as noah.sanders, we need to become a member of the IT Support Users group so we can change the user's password through the ForceChangePassword permission.
First, we abuse our WriteOwner over the IT Support group to set steve.wills as the owner:
With the ownership claimed, we grant steve.wills the rights to modify the group's membership:
Finally, we add steve.wills to the IT Support Users group:

Next, we use the ForceChangePassword permission granted through our IT Support Users membership to reset the password of noah.sanders and take over the account:

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

Access as steve.miller
As noah.sanders, we abuse our GenericAll over steve.miller to reset the account's password.

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

Shell as ryan.collins
I overlooked the step of explicitly granting the user steve.miller fullcontrol permission on the OU while drafting the write-up.
As steve.miller, we need to to grant ourselves full control over the OU and the objects it contains to leverage a GenericAll on the TIER1-SUPPORT OU to take control over the members of the OU. Either by using bloodyAD or dcaledit.
As steve.miller, we leverage the GenericAll on the TIER1-SUPPORT OU to reset the password of ryan.collins and take over the account:

Next, we connect to the DC as ryan.collins but there is no flag yet.

Shell as daniel.brooks
We take a closer look at daniel.brooks and see that this user is also a member of the Remote Management Users group, which would allow us to open a session on the DC.

Likewise, we reset the password of daniel.brooks, the other child object under the TIER1-SUPPORT OU, to take over that account as well:
Next, we connect to the DC as daniel.brooks and there we have the user flag and a link to the Opera Browser.

Access as chuck.harrys
Since this user seems to have Opera installed, we recall our finding from the share.

Maybe that user has stored the credentials in the browser.

We had no success with the following two tools to harvest the credentials...
... but DumpBrowserSecrets.exe does the job.
We upload the binary to the target machine...

... and follow the instructions from the repository.
We extract all entries from all browsers into an encrypted pack.


We should end up with a JSON file called OperaData.json. This file contains the credentials of chuck.harrys.

We test the credentials of chuck.harrys using NetExec and successfully authenticate against the domain controller.

Shell as ryan.collins on storage
Next, we connect to the share as ryan.collins.
And a service on port 5000 is indeed running internally.

Shell as local Administrator on Storage
Next, we try to expose the port using Chisel and connect to the service using the credentials of chuck.harrys.
We setup the chisel server on our attacker machine.

We upload Chisel and we run the client mode to establish a reverse port forward back to our attacking machine, exposing the target's internal service on 127.0.0.1:5000 through our listener on port 5000:

Now we can access the service in port 5000.

If we click on Admin Portal we are asked for credentials. We provide those of chuck.harrys.

We get redirected and are bale to disable the SMB siging from the storage share.

After disabling it, we verify the change by enumerating the target's SMB configuration, confirming that signing is now set to False.

This opens the host up to NTLM reflection via CVE-2025-33073 on Windows Server 2025.
The user daniel.brooks we already compromised is a member of the novaforge dns operaions group, which might allows us to create and modify DNS entries in the domain's zone.
We could abuse this by adding a DNS record that points a chosen hostname at our attacker-controlled machine, so that when a victim resolves and connects to that name, their authentication is coerced toward us.

As daniel.brooks, who holds DNS write permissions in NovaForge, we abuse this to add a crafted DNS A record that points to our machine:

Next, we set up an NTLM relay listener, configured to forward any captured authentication onward to the SMB service on the storage host.

To trigger the authentication, we use NetExecs's coerce_plus module against the storage host, supplying the encoded DNS record we planted as the listener target so the coerced machine authenticates toward our relay.

After a short delay we are able to retrieve the local Administrators hash.
This might take a second or thrid attempt.

We have now access to the storage as the local Administrator.

Access as david.cokx
From there we are able to find the credentials of the user david.cokx in the powershell command line history.

We test the credentails of david.cokx using NetExec and successfully authenticate against the storage machine.

We look up the user in our BloodHound data and see that this user has the permission to add members to the Protected Users group, which also allows removing those users from the group. The Protected Users group is a special protective security group that applies additional hardening to its members... such as disabling NTLM authentication, blocking weaker Kerberos encryptio, and preventing credential delegation and caching to make those accounts harder to compromise. This might come in handy later.

Access as frank.white
Using the Administrator hash we obtained, we dump the host's local secrets and credentials via LSA and SAM to harvest additional hashes for further access. We are able to retrieve the credentials of the user frank.white.

We test the credentails of frank.white using NetExec and successfully authenticate against the storage machine.

BloodHound Enumeration III
We'll look up frank.white in our Bloodhound data and see that the user holds theForceChangePassword permission over svc_it_admin, which allows the account's password to be reset.

When we query for the Shortest Path to Domain Admins, the BloodHound data shows that svc_it_admin holds WriteSPN over dc.novaforge.local, which allows the account's Service Principal Names to be modified.

Access as svc_it_admin
As frank.white, we abuse our ForceChangePassword right over svc_it_admin to reset the account's password.

We try to authenticate agains SMB using NetExec as svc_it_admin after changing its password, but without success.

The user is member of the protected users group.

We recall that we can remove members from those group with the compromisd user david.cokx.

As david.cook, we abuse our permission over the Protected Users group to remove svc_it_admin from it.
Now we are able to authenticate as svc_it_admin.

Shell as Domain Admin
Now we recall the WriteSPN permission over dc.novaforge.local as svc_it_admin.
The BloodHound data reveals an SPN-jacking opportunity built on Kerberos Constrained Delegation combined with DACL abuse.
The account svc_it_admin holds WriteSPN over storage.novaforge.local, which allows the listed SPN to be cleared from its original owner (a "live SPN-jacking"). svc_it_admin also holds WriteSPN over dc.novaforge.local, which allows that same SPN to be moved onto the Domain Controller object. By relocating the delegated SPN onto the DC and then impersonating a privileged user through the delegation, control of dc.novaforge.local can be obtained to become Domain Admin.
The account we control svc_it_admin is configured with constrained delegation and is AllowedToDelegate to storage.novaforge.local, meaning a storage SPN is listed in its msDS-AllowedToDelegateTo attribute. In SPN-jacking terms, svc_it_admin is the KCD service (serverA), storage.novaforge.local is the listed SPN owner (serverB), and dc.novaforge.local is the final target (serverC).
See https://www.thehacker.recipes/ad/movement/kerberos/spn-jacking


First, we clear the SPN from the STORAGE$ machine account to free it up for relocation (the "live SPN-jacking" step):

Next, we add that same SPN onto the DC$ machine account, effectively jacking the SPN over to the Domain Controller:

We then leverage the constrained delegation on svc_it_admin to request a service ticket, impersonating the Administrator user via S4U2self and S4U2proxy, while using the altservice option to retarget the resulting ticket to the CIFS service on the DC:

We export the resulting Kerberos ticket into our session so it can be used for authentication and use the impersonated ticket to authenticate to the Domain Controller via Pass-the-Ticket to obtain a SYSTEM shell as Administrator. From there we find the final flag at C:\Users\Administrator\root.txt.

Last updated