PHP代碼審計05之正則使用不當

前言

根據紅日安全寫的文章,學習PHP代碼審計的第五節內容,題目均來自PHP SECURITY CALENDAR 2017,講完題目會用一道CTF的題目和實例來加深鞏固。這是之前寫的,有興趣可以去看看:
PHP代碼審計01之in_array()函數缺陷
PHP代碼審計02之filter_var()函數缺陷
PHP代碼審計03之實例化任意對象漏洞
PHP代碼審計04之strpos函數使用不當

漏洞分析

下面看題目,代碼如下:

題目漏洞是正則使用不嚴謹導致任意文件刪除的漏洞,現在來具體分析,引起漏洞的地方在上面代碼的21行,這裡用到了preg_replace()函數,我們打開PHP手冊來看看對這個函數的定義如下:

了解了函數的用法,看上面代碼,[^a-z.-_] 表示匹配除了 a 字符到 z 字符和. 字符到 _ 字符之間的所有字符,但是沒有考慮到目錄路徑字符。這就直接可以任意刪除文件,例如構造如下參數:

action=delete&data=../../config.php
將刪除config.php文件。

CTF練習

通過上面的講解,來用一道CTF題目來練習一下,也是關於正則的問題,先看代碼:

//index.php
<?php
include 'flag.php';
if  ("POST" == $_SERVER['REQUEST_METHOD'])
{
    $password = $_POST['password'];
    if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
    {
        echo 'Wrong Format';
        exit;
    }
    while (TRUE)
    {
        $reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
        if (6 > preg_match_all($reg, $password, $arr))
            break;
        $c = 0;
        $ps = array('punct', 'digit', 'upper', 'lower');
        foreach ($ps as $pt)
        {
            if (preg_match("/[[:$pt:]]+/", $password))
            $c += 1;
        }
        if ($c < 3) break;
        if ("42" == $password) echo $flag;
        else echo 'Wrong password';
        exit;
    }
}
highlight_file(__FILE__);
?>
//flag.php
<?php $flag = "HRCTF{Pr3g_R3plac3_1s_Int3r3sting}";?>

這道題目考察了是否熟悉PHP正則表達的字符類,大體是下面這個表格:

alnum 字母和數字 header
alpha 字母
ascii 0 – 127的ascii字符
blank 空格和水平製表符
cntrl 控制字符
digit 十進制數(same as \d)
graph 打印字符, 不包括空格
lower 小寫字母
print 打印字符,包含空格
punct 打印字符, 不包括字母和數字
space 空白字符 (比\s多垂直製表符)
upper 大寫字母
word 單詞字符(same as \w)
xdigit 十六進制數字

想要更加詳細的了解,建議翻閱PHP手冊,了解了字符類,下面來分析代碼,上面一共三處正則表達,第一處如下:

if (0 >= preg_match(‘/[1]{12,}$/’, $password))
它表示的含義是匹配到可打印字符12往上包含12,^表示必須某類字符開頭,$表示必須某類字符結尾。
第二處正則如下:
$reg = ‘/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/’;
if (6 > preg_match_all($reg, $password, $arr))
break;
它表示的含義是,把連續的字符,數字,大寫,小寫作為一段,最少分成六段,比如Test+0He 會分為T est + 0 H e六段。
下面看第三處正則:
$ps = array(‘punct’, ‘digit’, ‘upper’, ‘lower’);
foreach ($ps as $pt)
{
if (preg_match(“/[[:$pt:]]+/”, $password))
$c += 1;
}
if ($c < 3) break;
if (“42” == $password) echo $flag;
這裡的含義是輸入的字符必須包含字符,數字,大寫,小寫其中的三種往上。最後與42進行弱類型比較,都符合就輸出flag,現在都解讀清楚了,讓咱們構造payload結果如下:

實例分析

通過例題和CTF題目的講解,是不是感覺棒棒的,現在咱們來分析實例吧,實例是LvyeCMS3.1,是基於ThinkPHP3.2.3框架。這個實例存在的漏洞也是函數使用不規範被繞過,導致任意文件刪除。下面來具體分析:
先查看入口文件index.php

可以看到公共目錄,應用目錄等一些信息。接下來再看看目錄結構:

而漏洞在Application/Template/Controller/StyleController.class.php文件中,具體如下:

看代碼第117行,這裡是獲取目錄路徑,參數也是我們可以控制的,再向後看,用到了str_replace()函數,它是個字符串替換函數,具體說明如下:

再這裡起到的作用就是將’..\’, ‘../’, ‘./’, ‘.\’替換為空。但是這裡是可以繞過的,如果我們輸入…..///呢,會發生什麼?是不是正好構造成了../,舉個小例子會更清楚,如下:

構造出../我們就可以穿越目錄了,現在訪問install.php文件會提示已安裝,如下圖:

然後嘗試刪除lvyecms/Application/Install/目錄下的 install.lock 文件,構造payload如下:

//www.xxx.com/index.php?g=Template&m=Style&a=delete&dir=…..///Application/Install/&file=install.lock

現在訪問install.php,發現確實刪除了,如下圖:

小結

通過這篇文章的學習與講解,是不是對PHP的正則了解的更多了呢,下一篇文章會對parse_str函數缺陷進行學習和講解。一起加油吧!


  1. [:graph:] ↩︎