php_bugs(php代码审计基础)

前言

以下都是以下代码审计的一些基础漏洞,适合新手学习~

extract 变量覆盖

extract(array,extract_rules,prefix)
该函数使用数组键名作为变量名,使用数组键值作为变量值。但是当变量中有同名的元素时,该函数默认将原有的值给覆盖掉。这就造成了变量覆盖漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

$flag='xxx';
extract($_GET);
if(isset($shiyan))
{
$content=trim(@file_get_contents($flag));
if($shiyan==$content)
{
echo'ctf{xxx}';
}
else
{
echo'Oh.no';
}
}

?>

1、文件将get方法传输进来的值通过extrace()函数处理。
2、通过两个if语句分别判断是否存在gift变量,和变量gift的值和变量content的值是否相等。变量content的值是通过读取变量test的值获取到的。如果两个变量相等输出flag。如果不相等,输出错误。
但是我们并不知道test的值是什么?所以我们使用变量覆盖漏洞,重新给test赋值。

例如:$GET[‘test’]=’a’,被extract()函数处理后,就变成了$test=’a’,有与之同名的变量$test = ‘‘;,将其值覆盖掉。并且get方法传输的gift参数的值也为a。这样,$gift=$content。就可以获得flag。

构造我们的payload:
?shiyan=&flag=

绕过过滤的空白字符

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
76

<?php

$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;

经过分析提交的数需要满足三个条件才能拿到flag:

1
2
3
4
5
6
7
8
9
1.is_numeric($_REQUEST['number'])$req['number']!=strval(intval($req['number'])is_numeric函数判断是否为数字 这两句要求提交的数不能是数字包括小数

2.if(intval($req["number"])!=$intval(strrev($req["number"])))这句的要求为该数的反转的整数值应该等于它本身的整数值即是一个回文数

3.is_palindrome_number($req["number"]))这句要求提交的数不是一个回文数
从要求的条件我们可以构造一个绕过第一个条件和第三个条件即可

is_numeric函数在开始判断前,会先跳过所有空白字符可是题目获取$req[‘number’]的时候明明使用trim过滤了空白字符这时候我们可以引入\f(也就是%0c)在数字前面,来绕过最后那个is_palindrome_number函数,而对于前面的数字判断,因为intval和is_numeric都会忽略这个字符,所以不会影响。
所以我们构造payload=URL?%00%0c131即可绕过上面的条件获得flag

多重加密

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

<?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');
}

?>

发现这段

1
2
3
4
5
6
7
8
9
10
11
if($login['user'] === 'ichunqiu')
{
echo $flag;
}else if($row['pass'] !== $login['pass']){
echo 'unserialize injection!!';
}else{
echo "(╯‵□′)╯︵┴─┴ ";
}
}else{
header('Location: index.php?error=1');
}

login[‘user’]从这段来

1
2
3
4
5
if(isset($requset['token']))
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
$db = new db();
$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');

发现requset中获取token值然后对它进行unserialize,gzuncompress,base64_decode等一系列操作得到login,

1
2
3
unserialize:对单一的已序列化的变量进行操作,将其转换回 PHP 的值

gzuncompress:解压被压缩的字符串

然后在login中获取键为user的值,如果等于ichunqiu就得出flag

那么我们只要对user:ichunqiu进行一系列逆操作就行

1
2
3
4
5
6
<?php
$a = array("user"=>'ichunqiu');
$a = base64_encode(gzcompress(serialize($a)));
echo $a

?>

SQL注入_with rollup绕过

题目地址: http://ctf5.shiyanbar.com/web/pcat
在第一层的filter里面就过滤了常用的SQL关键词,所以常规的SQL 注入就不行了。如果输入了filter里面的语句,网页返回“水可载舟,亦可赛艇!”

1
2
3
4
5
1' or 1 limit 1 offset 0#

1' or 1 limit 1 offset 1#

1' or 1 limit 1 offset 2#

发现2#时返回“一颗赛艇!” 其他都是“亦可赛艇!”———–说明数据库只有两条信息
利用group by pwd with rollup在查询中的一个特点,他可以返回pwd所在的那一条记录,通过limit控制返回哪一条,因此他不可以返回多条,一旦返回2条及以上,pwd就会为空,但同一条记录中的其他字段则是正常的
那么利用这一点令查询结果为空,我们输入的pwd也为空值,则构成了if(null==null)为true
payload:’ or 1=1 group by pwd with rollup limit 1 offset 2 #

ereg正则%00截断

地址:http://ctf5.shiyanbar.com/web/more.php

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>';
}
}
?>

ereg有两个漏洞:
①%00截断及遇到%00则默认为字符串的结束

②当ntf为数组时它的返回值不是FALSE
得到想要得到flag需要三个条件

1
2
3
4
5
6
7
8
1.ereg ("^[a-zA-Z0-9]+$", $_GET['password']) !=== FALSE即提交的password必须是只能是一个或者多个数字、大小写字母。
2.strlen($_GET['password']) < 8 && $_GET['password'] > 9999999 提交的password长度要小于8并且大小要大于99999999
3. strpos ($_GET['password'], '*-*') !== FALSE password里必须要有’-’

因为ereg()函数存在NULL截断漏洞,导致正则过滤被绕过,所以可以用%00来截断正则匹配
第二个条件长度小于8大小大于99999999可以用科学计数法来绕过

则最后构造password=1e8%00*-*即可得到flag

strcmp比较字符串

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$flag = "flag";
if (isset($_GET['a'])) {
if (strcmp($_GET['a'], $flag) == 0) //如果 str1 小于 str2 返回 < 0; 如果 str1大于 str2返回 > 0;如果两者相等,返回 0。

//比较两个字符串(区分大小写)
die('Flag: '.$flag);
else
print 'No';
}

?>

int strcmp ( string $str1 , string $str2 )
参数 str1第一个字符串。str2第二个字符串。
如果 str1 小于 str2 返回 < 0;
如果 str1 大于 str2 返回 > 0;
如果两者相等,返回 0
可知,传入的期望类型是字符串类型的数据,但是如果我们传入非字符串类型的数据的时候,这个函数将会有怎么样的行为呢?实际上,当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错的警告信息后,将return 0 !!!! 也就是虽然报了错,但却判定其相等了。这对于使用这个函数来做选择语句中的判断的代码来说简直是一个致命的漏洞,当然,php官方在后面的版本中修复了这个漏洞,使得报错的时候函数不返回任何值。strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL,而判断使用的是==,NULL==0是 bool(true)

sha()函数比较绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<?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>';
?>

===会比较类型,比如bool sha1()函数和md5()函数存在着漏洞,sha1()函数默认的传入参数类型是字符串型,那要是给它传入数组呢会出现错误,使sha1()函数返回错误,也就是返回false,这样一来===运算符就可以发挥作用了,需要构造username和password既不相等,又同样是数组类型

?name[]=a&password[]=b

SESSION验证绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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));
?>

http://127.0.0.1/Php_Bug/08.php?password=

删除cookies或者删除cookies的值

密码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>");

}
}
?>

用union select来返回一个已知明文的md5,payload:
user=’ union select ‘202cb962ac59075b964b07152d234b70’#&pass=123

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>";
}
?>

?id=%2568ackerDJ

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>");
}
}

?>

构造exp闭合绕过 admin’)#

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! Key is *********";
}
else{
echo "错误!你的IP不在访问列表之内!";
}
?>

HTTP头添加X-Forwarded-For:1.1.1.1

md5加密相等绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

$md51 = md5('QNKCDZO');
$a = @$_GET['a'];
$md52 = @md5($a);
if(isset($a)){
if ($a != 'QNKCDZO' && $md51 == $md52) {
echo "nctf{*****************}";
} else {
echo "false!!!";
}}
else{echo "please input a";}

?>

`

==对比的时候会进行数据转换,0eXXXXXXXXXX 转成0了,如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行

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]);
}
}

?>

构造 id=1024.1212
intval函数四舍五入,取值为1024,但if语句中已成功绕过1024的限制

strpos数组绕过NULL与ereg正则%00截断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$flag = "flag";

if (isset ($_GET['nctf'])) {
if (@ereg ("^[1-9]+$", $_GET['nctf']) === FALSE)
echo '必须输入数字才行';
else if (strpos ($_GET['nctf'], '#biubiubiu') !== FALSE)
die('Flag: '.$flag);
else
echo '骚年,继续努力吧啊~';
}

?>

方法一: 既要是纯数字,又要有’#biubiubiu’,strpos()找的是字符串,那么传一个数组给它,strpos()出错返回null,null!==false,所以符合要求. 所以输入nctf[]= 那为什么ereg()也能符合呢?因为ereg()在出错时返回的也是null,null!==false,所以符合要求.

方法二: 字符串截断,利用ereg()的NULL截断漏洞,绕过正则过滤 http://127.0.0.1/Php_Bug/16.php?nctf=1%00#biubiubiu 错误 需将#编码 http://127.0.0.1/Php_Bug/16.php?nctf=1%00%23biubiubiu 正确

SQL注入or绕过

地址: http://chinalover.sinaapp.com/web15/index.php

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

#GOAL: login as admin,then get the flag;
error_reporting(0);
require 'db.inc.php';

function clean($str){
if(get_magic_quotes_gpc()){ //get_magic_quotes_gpc — 获取当前 magic_quotes_gpc 的配置选项设置
$str=stripslashes($str); //返回一个去除转义反斜线后的字符串(\' 转换为 ' 等等)。双反斜线(\\)被转换为单个反斜线(\)。
}
return htmlentities($str, ENT_QUOTES);
}

$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);

//$query='SELECT * FROM users WHERE name=\''admin\'\' AND pass=\''or 1 #'\';';

$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
$result=mysql_query($query);
if(!$result || mysql_num_rows($result) < 1){
die('Invalid password!');
}

echo $flag;

?>

注意上面clean function中的htmlentities()函数,它会把输入字符中的 ’ 或者 ” 转变为html实体,这样一来就无法闭合源代码中的 ’ 了,还有就是,如果php的magic_quotes_gpc是开启状态的话,我们输入的转义符也会被去掉的,不过既然这道题目能做,说明我们是可以使用转义符 \ ,我们解这道题的关键就是使用转义符 \ 来让源代码中
‘SELECT * FROM users WHERE name=\’’.$username.’\’ AND pass=\’’.$password.’\’;’
$username后面的 ’ 失效,只要 这个 ’ 失效,就能闭合name=后面的 ’ ,要达到这一目的,我们只需要让username=admin \即可,让后使password的值为一个永真式(or 1=1)就可以得到这道题的flag
这样提交的数据,会导致源代码中的SQL语句变为:

SELECT * FROM users WHERE name=’admin \’ AND pass=’ or 1=1
1
注入语句?username=admin \&password=or 1=1#

密码md5比较绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

if($_POST[user] && $_POST[pass]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$user = $_POST[user];
$pass = md5($_POST[pass]);
$query = @mysql_fetch_array(mysql_query("select pw from ctf where user=' $user '"));
if (($query[pw]) && (!strcasecmp($pass, $query[pw]))) {

//strcasecmp:0 - 如果两个字符串相等

echo "<p>Logged in! Key: ntcf{**************} </p>";
}
else {
echo("<p>Log in failure!</p>");
}
}

?>

//select pw from ctf where user=’’and 0=1 union select ‘e10adc3949ba59abbe56e057f20f883e’ #
?user=’and 0=1 union select ‘e10adc3949ba59abbe56e057f20f883e’ #&pass=123456

md5()函数===使用数组绕过

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
$flag = 'flag{test}';
if (isset($_GET['username']) and isset($_GET['password'])) {
if ($_GET['username'] == $_GET['password'])
print 'Your password can not be your username.';
else if (md5($_GET['username']) === md5($_GET['password']))
die('Flag: '.$flag);
else
print 'Invalid password';
}
?>
1
2
3
若为md5($_GET['username']) == md5($_GET['password']) 则可以构造: http://127.0.0.1/Php_Bug/18.php?username=QNKCDZO&password=240610708 因为==对比的时候会进行数据转换,0eXXXXXXXXXX 转成0了 也可以使用数组绕过 http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2

但此处是===,只能用数组绕过,PHP对数组进行hash计算都会得出null的空值 http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2

十六进制与数字比较

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

error_reporting(0);
function noother_says_correct($temp)
{
$flag = 'flag{test}';
$one = ord('1'); //ord — 返回字符的 ASCII 码值
$nine = ord('9'); //ord — 返回字符的 ASCII 码值
$number = '3735929054';
// Check all the input characters!
for ($i = 0; $i < strlen($number); $i++)
{
// Disallow all the digits!
$digit = ord($temp{$i});
if ( ($digit >= $one) && ($digit <= $nine) )
{
// Aha, digit not allowed!
return "flase";
}
}
if($number == $temp)
return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);

?>

这里,它不让输入1到9的数字,但是后面却让比较一串数字,平常的方法肯定就不能行了,大家都知道计算机中的进制转换,当然也是可以拿来比较的,0x开头则表示16进制,将这串数字转换成16进制之后发现,是deadc0de,在开头加上0x,代表这个是16进制的数字,然后再和十进制的 3735929054比较,答案当然是相同的,返回true拿到flag

echo dechex ( 3735929054 ); // 将3735929054转为16进制
结果为:deadc0de
构造: http://127.0.0.1/Php_Bug/20.php?password=0xdeadc0d

弱类型整数大小比较绕过

1
2
3
4
5
6
7
8
9
10
11
12
<?php

error_reporting(0);
$flag = "flag{test}";

$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;
if($temp>1336){
echo $flag;
}

?>

利用PHP弱类型的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型intval再比。如果输入一个1337a这样的字符串,在is_numeric中返回true,然后在比较时被转换成数字1337,这样就绕过判断输出flag。

http://127.0.0.1/php_bug/22.php?password=1337a

md5函数true绕过注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

<?php
error_reporting(0);
$link = mysql_connect('localhost', 'root', 'root');
if (!$link) {
die('Could not connect to MySQL: ' . mysql_error());
}
// 选择数据库
$db = mysql_select_db("security", $link);
if(!$db)
{
echo 'select db error';
exit();
}
// 执行sql
$password = $_GET['password'];
$sql = "SELECT * FROM users WHERE password = '".md5($password,true)."'";
var_dump($sql);
$result=mysql_query($sql) or die('<pre>' . mysql_error() . '</pre>' );
$row1 = mysql_fetch_row($result);
var_dump($row1);
mysql_close($link);
?>

当md5后的hex转换成字符串后,如果包含 ‘or’ 这样的字符串,那整个sql变成
SELECT * FROM admin WHERE pass = ‘’or’6
提供一个字符串:ffifdyop

md5后,276f722736c95d99e921722cf9ed621c

再转成字符串: ‘or’6

switch没有break 字符与0比较绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

error_reporting(0);

if (isset($_GET['which']))
{
$which = $_GET['which'];
switch ($which)
{
case 0:
case 1:
case 2:
require_once $which.'.php';
echo $flag;
break;
default:
echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
break;
}
}

?>

让我们包含当前目录中的flag.php,给which为flag,这里会发现在case 0和case 1的时候,没有break,按照常规思维,应该是0比较不成功,进入比较1,然后比较2,再然后进入default,但是事实却不是这样,事实上,在 case 0的时候,字符串和0比较是相等的,进入了case 0的方法体,但是却没有break,这个时候,默认判断已经比较成功了,而如果匹配成功之后,会继续执行后面的语句,这个时候,是不会再继续进行任何判断的。也就是说,我们which传入flag的时候,case 0比较进入了方法体,但是没有break,默认已经匹配成功,往下执行不再判断,进入2的时候,执行了require_once flag.php

PHP中非数字开头字符串和数字 0比较==都返回True

因为通过逻辑运算符让字符串和数字比较时,会自动将字符串转换为数字.而当字符串无法转换为数字时,其结果就为0了,然后再和另一个0比大小,结果自然为ture。注意:如果那个字符串是以数字开头的,如6ldb,它还是可以转为数字6的,然后和0比较就不等了(但是和6比较就相等) if($str==0) 判断 和 if( intval($str) == 0 ) 是等价的
可以验证:
<?php
$str=”s6s”;
if($str==0){ echo “返回了true.”;}
?>
要字符串与数字判断不转类型方法有:

方法一: $str=”字符串”;if($str===0){ echo “返回了true.”;}

方法二: $str=”字符串”;if($str==”0”){ echo “返回了true.”;} ,

此题构造:http://127.0.0.1/php_bug/25.php?which=aa

资料:

unserialize()序列化

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
<!-- 题目:http://web.jarvisoj.com:32768 -->

<!-- index.php -->
<?php
require_once('shield.php');
$x = new Shield();
isset($_GET['class']) && $g = $_GET['class'];
if (!empty($g)) {
$x = unserialize($g);
}
echo $x->readfile();
?>
<img src="showimg.php?img=c2hpZWxkLmpwZw==" width="100%"/>

<!-- shield.php -->

<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = '') {
$this -> file = $filename;
}

function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?>

<!-- showimg.php -->
<?php
$f = $_GET['img'];
if (!empty($f)) {
$f = base64_decode($f);
if (stripos($f,'..')===FALSE && stripos($f,'/')===FALSE && stripos($f,'\\')===FALSE
//stripos — 查找字符串首次出现的位置(不区分大小写)
&& stripos($f,'pctf')===FALSE) {
readfile($f);
} else {
echo "File not found!";
}
}
?>
1
说明flag在pctf.php,但showimg.php中不允许直接读取pctf.php,只有在index.php中可以传入变量class ,index.php中Shield类的实例$X = unserialize($g),$g = $_GET['class'];,$X中不知$filename变量,但需要找的是:$filename = "pctf.php",现$X已知,求传入的class变量值。 可以进行序列化操作:
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
<!-- answer.php -->
<?php

require_once('shield.php');
$x = class Shield();
$g = serialize($x);
echo $g;

?>

<!-- shield.php -->
<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = 'pctf.php') {
$this -> file = $filename;
}

function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?>

得到: O:6:”Shield”:1:{s:4:”file”;s:8:”pctf.php”;} 构造: http://web.jarvisoj.com:32768/index.php?class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

利用提交数组绕过逻辑

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
$role = "guest";
$flag = "flag{test_flag}";
$auth = false;
if(isset($_COOKIE["role"])){
$role = unserialize(base64_decode($_COOKIE["role"]));
if($role === "admin"){
$auth = true;
}
else{
$auth = false;
}
}
else{
$role = base64_encode(serialize($role));
setcookie('role',$role);
}
if($auth){
if(isset($_POST['filename'])){
$filename = $_POST['filename'];
$data = $_POST['data'];
if(preg_match('[<>?]', $data)) {
die('No No No!'.$data);
}
else {
$s = implode($data);
if(!preg_match('[<>?]', $s)){
$flag='None.';
}
$rand = rand(1,10000000);
$tmp="./uploads/".md5(time() + $rand).$filename;
file_put_contents($tmp, $flag);
echo "your file is in " . $tmp;
}
}
else{
echo "Hello admin, now you can upload something you are easy to forget.";
echo "<br />there are the source.<br />";
echo '<textarea rows="10" cols="100">';
echo htmlspecialchars(str_replace($flag,'flag{???}',file_get_contents(__FILE__)));
echo '</textarea>';
}
}
else{
echo "Sorry. You have no permissions.";
}
?>

这段代码首先会查看提交的请求中是否存在 <> 如果没有则将传入的数据(如果是数组)转化为字符串。如果其中存在 <> 则将flag生成在一个随机命名的文件中。 implode() 这个函数需要传入数组,如果传入的是字符串将报错,变量 $s 自然也就没有值。
想要通过Post请求的形式传入数组可以使用 data[0]=123&data[1]=<> 的形式传入数组,这样的话在执行 implode() 函数的时候就不会使 &s 为空,成功绕过这段逻辑拿到flag。