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:
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.
First thing to do is run a quick scan on that IP.
Browsing to that URL brings you to a generic Tomcat start page.
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.
That might be interesting indeed...
Browsing to the test.jsp site reveals the following:
Entering in the recommended "ls -l /tmp" command outputs the following:
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.
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.
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.
Now let's try adding a "grep" in there:
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:
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:
From here, I can fire up ipython and import the newly created class.
It works! Although the output is still really ugly. Let's add another function in to make that prettier. Updated code:
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.
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...
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:
Now we can run it much cleaner like so:
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.
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.
Now we get much more useful output:
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.
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.
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.
Ok, it won't be that easy. Let's check /home and see what other users exist on this machine.
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:
Now let's try it again and see if we are running as the user "bill":
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...
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.
Just for the sake of it, let's look at the actual GET request used to retrieve that flag:
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.