Using Python To Get A Shell Without A Shell

Posted by Dan Lawson on October 27, 2017

Introduction

Many times while conducting a pentest, I need to script something up to make my life easier or to quickly test an attack idea or vector. Recently I came across an interesting command injection vector on a web application sitting on a client's internet-facing estate. There was a page, running in Java, that allowed me to type arbitrary commands into a form, and have it execute them. While developer-provided webshells are always nice, there were a few caveats. The page was expecting directory listing style output, which was then parsed and reformatted. If the output didn't match this parsing, no output to me. Additionally, there was no egress. ICMP, and all TCP/UDP ports including DNS were blocked outbound.

I was still able to leverage the command injection to compromise not just the server, but the entire infrastructure it was running on. After the dust settled, the critical report was made, and the vulnerability was closed, I thought the entire attack path was kind of fun, and decided to share how I went about it. Since I enjoy being a free man and only occasionally visit prisons, I've created a simple boot2root style VM that has a similar set of vulnerabilities to use in a walkthrough. If you'd like to play along at home, or give it a shot yourself first, it can be downloaded here:

https://depthsecurity-my.share...

Initial Attack 

After the VM boots up, you should be presented with a nice login screen which helpfully (hopefully) presents the IP address picked up from DHCP.

2017-10-06_11-52-33.png#asset:568

First thing to do is run a quick scan on that IP.

2017-10-06_12-39-07.png#asset:569

Browsing to that URL brings you to a generic Tomcat start page.

2017-10-06_12-45-07.png#asset:570

If I don't need to be sneaky, I'll usually look for low hanging fruit first, and use tools like Nikto to enumerate the site.

2017-10-06_12-49-04.png#asset:571

That might be interesting indeed...

The Webshell

Browsing to the test.jsp site reveals the following:

2017-10-06_12-54-58.png#asset:573

Entering in the recommended "ls -l /tmp" command outputs the following:

2017-10-06_12-58-02.png#asset:574

This immediately tells us a couple of things:

  • We are able to execute commands directly on the system
  • The output is being formatted to fit in that table

The first thing we'll want to establish is exactly what we can do with the command injection. Will it run arbitrary commands, or only ls? What happens when it gets non "ls -l" style output?

Let's try just sending "ls", which will just list out the directories without any extra information.

2017-10-06_13-04-35.png#asset:575

We don't get any errors, but nothing is returned. Now let's test it against something that will have a lot of varying input of different shapes and sizes.

2017-10-06_13-08-09.png#asset:576

This tells us that not only can we run our own arbitrary commands but as long as the output matches some format, it will display back to us.

In a real life scenario, now is where I'd attempt to enumerate egress and treat this as a blind injection. For the sake of this article, we already know that all outbound egress from the target is blocked, even DNS. If we can't get usable output, we are pretty much done with exploitation.

Now that we know this, we'll work on getting a somewhat usable shell, and come back this later. In order to modify all of the output to match a pattern, we'll need to be able to use bash redirection. Since this is a Java application, the command is most likely being directly piped to Runtime.Exec(), and won't support redirection. Let's test it and verify.

Command 1:

2017-10-06_12-58-02.png#asset:574

Now let's try adding a "grep" in there:

2017-10-06_13-12-52.png#asset:577

As you can see, the output is exactly the same. The "grep" was ignored.

Luckily, there is an awesome blog post by Markus Wulftange at http://codewhitesec.blogspot.r... which explains exactly why this is, and how to get around it. In short, in order to have fully working redirection, you must prepend the command with the following:

sh -c $@|sh . echo

Let's try the earlier grep command:

2017-10-06_13-27-12.png#asset:578

It worked! But now we have another problem. That is getting to be pretty unwieldy to type in over and over again. We need a way to automate the requests, and that way is Python.

Making Life Easy

NOTE: There are a million ways to script things out, and most of those will probably be cleaner, more efficient, and much more professional than what I pump out here. The key thing to remember is this: The purpose of scripting this out in the first place is to save time and effort. If I spend an hour searching for the best way to code something that was only going to save me ten minutes anyways, I've wasted time and defeated the whole purpose. Most of my code is written as what I can push out to do my work for me in as little time as necessary. I think this is a pretty common tenant in this industry.

That all being said, the first thing I do is build some sort of basic shell framework. Here we have some very basic Python code:

2017-10-12_13-09-55.png#asset:613


From here, I can fire up ipython and import the newly created class.

2017-10-06_14-13-19.png#asset:579

It works! Although the output is still really ugly. Let's add another function in to make that prettier. Updated code:

2017-10-12_13-12-51.png#asset:614

Before we continue, let's just do a couple of quick things in iPython to make our lives a little easier. The first thing we'll do is import readline. The readline library will give us command history in the fake shell we are going to build next. We'll also create a function for reloading our class, so we can quickly reload it after making changes.

2017-10-12_14-06-18.png#asset:620

Here is the reason I like to run stuff like this directly in iPython. I can reload the class, and even go so far as to create a local function to reload the class for me. Continuing on...

2017-10-06_14-38-45.png#asset:580

That looks much better, but I don't want to have to keep typing in "a.run_cmd" every time. Let's make it more psuedo-shellish. We'll add in another function to give sort of a command prompt:

2017-10-12_13-14-43.png#asset:616


Now we can run it much cleaner like so:

2017-10-06_14-53-42.png#asset:581

Great! Now we can look at directory listings. Going back to what we found earlier, let's see if we can figure out how to bypass, or at least control the formatting.

2017-10-06_14-58-33.png#asset:582

We can see now that if we prepend the results with "a b c d e f g h", then anything after will fill up the last slot in the table. We can append the following on the end of the command. This will add the text before every line of returned text.

while read line; do echo "a b c d e f g h $line"; done;

Let's modify our class to do this automatically and filter out the results so we only get the information we want.

2017-10-12_13-17-12.png#asset:615


Now we get much more useful output:

2017-10-06_15-09-54.png#asset:583

This is totally workable at this point, but we'll add a couple more tweaks onto it, just to make it more like a real shell. First, we'll add "2>&1" before the "| while", so any errors get redirected to us and we see those as well. Next, we'll track the current directory, and create our own "cd" commands. Finally, we'll roll this into the prompt to give us a nice shell-like feel to everything.

2017-10-12_13-20-55.png#asset:617


Now we have a proper(ish) shell. Looking around on the system, we notice the .ssh directory in the web user's home folder. Inside there, we have a full set of keys.

2017-10-06_15-32-36.png#asset:584

Since SSH is blocked from the outside, it stands to reason that it is probably running but firewalled off. While we can't SSH interactively anywhere from here (since we don't have a real shell), we can still SSH non-interactively, sending a single command at a time. Let's verify that SSH is running and try to run a command as root.

2017-10-06_15-33-57.png#asset:585

Ok, it won't be that easy. Let's check /home and see what other users exist on this machine.

2017-10-06_15-39-50.png#asset:586

Jackpot! And since we are lazy, we aren't going to bother typing in "ssh bill@localhost" every time. Instead, we'll just update our class to prepend that. Our updated code:

2017-10-12_13-22-22.png#asset:618


Now let's try it again and see if we are running as the user "bill":

2017-10-06_15-44-24.png#asset:587

Excellent. Now, just to check, let's see what kind of sudo rights Bill has. If Bill requires a password at all, then we would be out of luck, since we aren't interactive, and don't have his actual password. Maybe we'll get lucky...

2017-10-06_15-48-05.png#asset:589

Well, would you look at that. In the real world, this would (hopefully) never happen. Hopefully. Okay I've only seen it happen exactly once.

2017-10-06_15-49-11.png#asset:588

Just for the sake of it, let's look at the actual GET request used to retrieve that flag:

2017-10-12_13-24-55.png#asset:619


Conclusion

The script definitely has a lot of room for improvement and may even be useful as a cleaned-up framework. The idea of this exercise was more to demonstrate how a pentester could use Python not just to write tools, but to simplify a relatively complex attack. The real-world test I mentioned above didn't lead me to root on one box, but did give me access to a hundred+ other boxes via the SSH key.

Have Questions?
Get Answers