雖然PHP是世界上最好的語言,但是也有一些因為弱類型語言的安全性問題出現。WordPress歷史上就出現過由于PHP本身的缺陷而造成的一些安全性問題,如CVE-2014-0166?中的cookie偽造就是利用了PHP Hash比較的缺陷。 當然一般這種情況實戰中用到的不是很多,但是在CTF競賽中卻是一個值得去考察的一個知識點,特此記錄總結之。
一、精度繞過缺陷
理論
在用PHP進行浮點數的運算中,經常會出現一些和預期結果不一樣的值,這是由于浮點數的精度有限。盡管取決于系統,PHP 通常使用?IEEE 754
?雙精度格式,則由于取整而導致的最大相對誤差為?1.11e-16
。非基本數學運算可能會給出更大誤差,并且要考慮到進行復合運算時的誤差傳遞。下面看一個有趣的例子:
代碼安全掃描,
以十進制能夠精確表示的有理數如?0.1
?或?0.7
,無論有多少尾數都不能被內部所使用的二進制精確表示,因此不能在不丟失一點點精度的情況下轉換為二進制的格式。這就會造成混亂的結果:例如,floor((0.1+0.7)*10)
?通常會返回?7
?而不是預期中的?8
,因為該結果內部的表示其實是類似?7.9999999999999991118…
。
實踐
問鼎杯2017 老眼昏花網上很多write-up感覺就像是看著答案寫write-up,個人感覺真正的write-up中應該體現自己的思考在里面。
題目描述
php還有人用嗎?題目言簡意賅,讓我們把2017
這個值傳遞給服務器。
考察點
- PHP浮點精確度
write-up
what year is this?
所以第一反應是直接給year
參數賦值為2017
:
?year=2017
然而結果如下:
有提示了,說明year
這個參數是對的,但是2017
中不可以出現7
,這里如果不了解php精度的話,肯定是對2017
進行各種編碼繞過,但是這里對編碼也進行過濾了:
所以最后一種可能就是利用PHP精度來繞過:
?year=2016.99999999999
php代碼如何運行,
二、類型轉換的缺陷
理論
PHP提供了is_numeric
函數,用來變量判斷是否為數字。PHP弱類型語言的一個特性,當一個整形和一個其他類型行比較的時候,會先把其他類型intval數字化再比。
實踐
is_numeric()
用于判斷是否是數字,通常配合數值判斷。
案例代碼
<?phperror_reporting(0);$flag = 'flag{1S_numer1c_Not_S4fe}';$id = $_GET['id']; is_numeric($id)?die("Sorry...."):NULL; if($id>665){ echo $flag; } ?>
考察點
- PHP類型轉換缺陷
write-up
分析下代碼:首先對GET方式提交的參數id
的值進行檢驗。id
通過is_numeric
函數來判斷是否為數字,如果為數字的話,GG。如果不是數字的話,和665
進行比較,id
的值大于665
的時候輸出flag
。
乍看上去又好像不可能這里,但是如果知道PHP弱類型語言的一個特性,當一個整形和一個其他類型行比較的時候,會先把其他類型intval數字化再比
。這個特性的話就可以很好的繞過。
http://localhost/?id=666gg
PHP代碼發布、
三、松散比較符的缺陷
理論
php比較相等性的運算符有兩種,一種是嚴格比較,另一種是松散比較。
如果比較一個數字和字符串或者比較涉及到數字內容的字符串,則字符串會被轉換成數值并且比較按照數值來進行
嚴格比較符嚴格比較符,會先判斷兩種字符串的類型是否相等,再比較。
=== //全等
!== //不全等
php繞過?
松散比較符松散比較符,會先將字符串類型轉換成相同,再比較。
== //等于
!= //不等
PHP 會根據變量的值,自動把變量轉換為正確的數據類型。這一點和C 和 C++ 以及 Java 之類的語言明顯不同。雖然這樣PHP方便了程序員,但是隨之而來卻會帶來一些安全性的問題。
一個簡單的例子
<?php$a = null;$b = false;echo $a==$b; echo "<br>"; $c = ""; $d = 0; echo $c==$d ?>
由于php對變量自動轉換的特性,這里面的
$a==$b 與 $c==$d 均為真
php滲透?所以頁面輸出的結果為:
一個深入的例子
下面結合PHP 相等性比較缺陷再解釋下會好懂一點:
var_dump(0=="gg"); //true
var_dump(0==="gg"); //false var_dump(1=="gg"); //false
0
與gg
進行松散性質的不嚴格比較,會將gg
轉換為數值,強制轉換,由于gg
是字符串,轉化的結果是0
,所以 輸出?true
代碼?
0
與gg
進行嚴格 性質的嚴格比較,這里的gg
是字符串類型,和int類型的0
不相等,所以輸出?false
0
與gg
進行松散性質的不嚴格比較,會將gg
轉換為數值,強制轉換,由于gg
是字符串,轉化的結果是0
,不等于1
,所以輸出?false
var_dump(1=="1gg"); //true
var_dump(1=="gg1"); //false
1
與1gg
進行松散性質的不嚴格比較,這里1gg
被強制轉換為int類型的時候會從字符串的第一位開始做判斷進行轉換,這里的1gg
第一位是1
,所以這里1gg
被轉換為1
,所以輸出?true
1
與gg1
進行嚴格 性質的嚴格比較,字符串gg1
的第一位不是數字,所以它被強制轉換為0
,所以輸出?false
var_dump("0e123" == "0e456"); //true
var_dump("0e123" == "0eabc"); //flase
php漏洞是什么原因。這里比較特殊,字符串中出現了0e
,PHP手冊介紹如下:
當一個字符串欸當作一個數值來取值,其結果和類型如下:如果該字符串沒有包含'.','e','E'并且其數值值在整形的范圍之內
該字符串被當作int來取值,其他所有情況下都被作為float來取值, 該字符串的開始部分決定了它的值,如果該字符串以合法的數值開始,則使用該數值,否則其值為0。
0e123
與0e456
相互不嚴格性質比較的時候,會將0e
這類字符串識為科學技術法的數字,0的無論多少次方都是零,所以相等,輸出?true
0e123
與0eabc
相互進行不嚴格性質比較的時候,本應該將0e
這類字符串識為科學技術法的數字,但是這里的0e
后面跟著的是abc
,數學中科學計數的指數不可以包含字母。所以這里字符串中雖然是0e
開頭,但是后面的abc
卻不符合科學技法的規范,所以輸出是?false
實踐
md5繞過(Hash比較缺陷)南京郵電大學網絡攻防訓練平臺中一道比較經典的md5 collision
題,關于這道題目的WriteUp網上很多,但是真正深入分析的少之又少~~
題目描述
md5 collision源碼
<?php$md51 = md5('QNKCDZO');$a = @$_GET['a'];$md52 = @md5($a);if(isset($a)){ if ($a != 'QNKCDZO' && $md51 == $md52) { echo "nctf{*****************}"; } else { echo "false!!!"; } } else{ echo "please input a"; } ?>
考察點
- 簡單的PHP代碼審計
- PHP弱類型的Hash比較缺陷
write-up
從源碼中可以得輸入一個a的參數的變量,a首先不等于QNKCDZO
并且a得md5值必須等于QNKCDZO
加密后的md5值。 乍一看好像不可能存在這樣的值,但是這里QNKCDZO
加密后的md5值為0e830400451993494058024219903391
?這里是0e
開頭的,在進行等于比較的時候,PHP把它當作科學計數法,0的無論多少次方都是零。 所以這里利用上面的弱類型的比較的缺陷來進行解題:?a=s155964671a
字符串加密后md5
為0exxxx的字符串(x必須是10進制數字)列表
字符串 | md5 |
---|---|
QNKCDZO | 0e830400451993494058024219903391 |
240610708 | 0e462097431906509019562988736854 |
aabg7XSs | 0e087386482136013740957780965295 |
aabC9RqS | 0e041022518165728065344349536299 |
s878926199a | 0e545993274517709034328855841020 |
四、sha1() md5()加密函數漏洞缺陷
理論
md5()
和sha1()
對一個數組進行加密將返回 NULL
實踐
Boston Key Party CTF 2015: Prudential
題目描述
I dont think sha1 isbroken.Prove me wrong.
題目給了一個登陸框:
考察點
- sha1()函數漏洞缺陷
write-up
源代碼給出如下:
<html>
<head><title>level1</title> <link rel='stylesheet' href='style.css' type='text/css'> </head> <body> <?php require 'flag.php'; if (isset($_GET['name']) and isset($_GET['password'])) { if ($_GET['name'] == $_GET['password']) print 'Your password can not be your name.'; else if (sha1($_GET['name']) === sha1($_GET['password'])) die('Flag: '.$flag); else print '<p class="alert">Invalid password.</p>'; } ?> <section class="login"> <div class="title"> <a href="./index.txt">Level 1</a> </div> <form method="get"> <input type="text" required name="name" placeholder="Name"/><br/> <input type="text" required name="password" placeholder="Password" /><br/> <input type="submit"/> </form> </section> </body> </html>
分析一下核心登錄代碼如下:
if ($_GET['name'] == $_GET['password'])print 'Your password can not be your name.'; else if (sha1($_GET['name']) === sha1($_GET['password'])) die('Flag: '.$flag);
GET
類型提交了兩個字段name
和password
,獲得flag要求的條件是:
- name != password
- sha1(name) == sha1(password)
這個乍看起來這是不可能的,但是這里利用sha1()
函數在處理數組的時候由于無法處理將返回NULL
可以繞過if語句的驗證,if條件成立將獲得flag
。 構造語句如下:
?name[]=a&password[]=b
這里符合了2個拿到flag的條件:
- a不等于b
- name和password由于是數組,經過sha1()函數嫁給后都返回
NULL
拿到flag:?I_think_that_I_just_broke_sha1
拓展總結
經過驗證,不僅sha1()
函數無法處理數組,這里md5()
函數也有同樣的問題,在處理數組的時候,都將返回NULL
測試代碼如下:
<?php
error_reporting(0);
$flag = 'flag{I_think_that_I_just_broke_md5}';
if (isset($_GET['username']) and isset($_GET['password'])) { if ($_GET['username'] == $_GET['password']) print 'Your password can not be your username.'; else if (md5($_GET['username']) === sha1($_GET['password'])) die($flag); else print 'Invalid password'; } ?>
這里面的核心代碼如下:
if ($_GET['username'] == $_GET['password'])
并且得滿足:
if (md5($_GET['username']) === sha1($_GET['password']))
同樣利用md5()
函數無法處理數組的這個漏洞,構造get請求拿到flag:
?username[]=a&password[]=b
五、字符串處理函數漏洞缺陷
理論
strcmp()
函數:比較兩個字符串(區分大小寫).
用法如下:
int strcmp ( string $str1 , string $str2 )
具體的用法解釋如下:
參數 `str1`第一個字符串。
參數 `str2`第二個字符串。
如果 `str1` 小于 `str2` 返回 `< 0`; 如果 `str1` 大于 `str2` 返回 `> 0`; 如果兩者相等,返回 0。
這個函數接受到了不符合的類型,例如數組
類型,函數將發生錯誤。但是在5.3
之前的php中,顯示了報錯的警告信息后,將return 0
?!!!! 也就是雖然報了錯,但卻判定其相等了。
ereg()
函數:字符串正則匹配。strpos()
函數:查找字符串在另一字符串中第一次出現的位置,對大小寫敏感。
這2個函數都是用來處理字符串的,但是在傳入數組參數后都將返回NULL
。
實踐
Boston Key Party CTF 2015: Northeastern Univ
題目描述
Of course, a timing attack might be the answer, but Im quite sure that you can do better than that. 題目給了一個登陸框:
考察點
- 字符串處理函數漏洞缺陷
write-up
給出源代碼如下:
<html>
<head><title>level3</title> <link rel='stylesheet' href='style.css' type='text/css'> </head> <body> <?php require 'flag.php'; if (isset($_GET['password'])) { if (strcmp($_GET['password'], $flag) == 0) die('Flag: '.$flag); else print '<p class="alert">Invalid password.</p>'; } ?> <section class="login"> <div class="title"> <a href="./index.txt">Level 3</a> </div> <form method="get"> <input type="text" required name="password" placeholder="Password" /><br/> <input type="submit"/> </form> </section> </body> </html>
分析一下核心登錄代碼如下:
if (strcmp($_GET['password'], $flag) == 0)
這里使用了==
松散比較了$flag
和通過GET方式提交的password
的值,如果想等的話,拿到flag。這里用的是==
松散性質的比較,再利用字符串處理數組時將會報錯,在5.3
之前的php中,顯示了報錯的警告信息后,將return 0
。所有這里將password
參數指定為數組,利用函數漏洞拿到flag
:
拓展總結
除了strcmp()
函數外,ereg()
和strpos()
函數在處理數組的時候也會異常,返回NULL
。測試代碼如下:
<?phperror_reporting(0);$flag = 'flag{P@ssw0rd_1s_n0t_s4fe_By_d0uble_Equ4ls}';if (isset ($_GET['password'])) { if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE) echo 'You password must be alphanumeric'; else if (strpos ($_GET['password'], '--') !== FALSE) die($flag); else echo 'Invalid password'; } ?>
將參數password賦值一個數組傳遞進去:
http://localhost/?password[]=gg
ereg()函數
是處理字符串的,傳入數組后返回NULL
,NULL
和?FALSE
,是不恒等(===)的,滿足第一個if
條件;而strpos()函數
也是處理字符串的,傳入數組后返回NUL
L,NULL!==FALSE
,滿足條件,拿到flag:
六、parse_str函數變量覆蓋缺陷
理論
parse_str
函數的作用就是解析字符串并注冊成變量,在注冊變量之前不會驗證當前變量是否存在,所以直接覆蓋掉已有變量。
void parse_str ( string $str [, array &$arr ] )
str 輸入的字符串。
arr 如果設置了第二個變量 arr,變量將會以數組元素的形式存入到這個數組,作為替代。
實踐
測試代碼:
<?php
error_reporting(0);
$flag = 'flag{V4ri4ble_M4y_Be_C0verEd}';
if (empty($_GET['b'])) { show_source(__FILE__); die(); }else{ $a = "www.sqlsec.com"; $b = $_GET['b']; @parse_str($b); if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) { echo $flag; }else{ exit('your answer is wrong~'); } } ?>
考察點
- parse_str變量覆蓋缺陷
write-up
找到核心代碼:
@parse_str($b);
這里使用了parse_str函數來傳遞b的變量值
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) 這里用到的是文章上面的知識點md5()函數缺陷
因為這里用到了parse_str
函數來傳遞b
,if的語句的條件是拿$a[0]
來比較的,有因為這里的變量a的值已經三是固定的了:
$a = "www.sqlsec.com";
這里其實是我博客的地址~~ 不過不重要。 整體代碼乍看起來又不可能,但是利用變量覆蓋函數的缺陷這里可以對a
的變量進行重新賦值,后面的的if語句再利用本文前面提到的md5()
比較缺陷進行繞過:
http://localhost/?b=a[0]=240610708
參考文獻
- PHP 比較運算符
- PHP Float 浮點型
- PHP 類型比較表
- PHP 弱類型總結
- PHP Hash比較存在缺陷,影響大量Web網站登錄認證、忘記密碼等關鍵業務
- PHP代碼審計片段講解(入門代碼審計、CTF必備)
- 淺談PHP弱類型安全
- NJCTF2017 線上賽 web 題解
- CTF之PHP黑魔法總結
- Some features of PHP in CTF
- PHP浮點數運算精度的問題
- php strcmp()漏洞
- 危險的is_numeric——PHPYun 2015-06-26 二次注入漏洞分析
- 【代碼審計】變量覆蓋漏洞詳解
*本文作者:國光,轉載請注明FreeBuf.COM