1. 程式人生 > >由Typecho 深入理解PHP反序列化漏洞

由Typecho 深入理解PHP反序列化漏洞

前言

Typecho是一個輕量版的部落格系統,前段時間爆出getshell漏洞,網上也已經有相關的漏洞分析釋出。這個漏洞是由PHP反序列化漏洞造成的,所以這裡我們分析一下這個漏洞,並藉此漏洞深入理解PHP反序列化漏洞。

一、 PHP反序列化漏洞

1.1 漏洞簡介

PHP反序列化漏洞也叫PHP物件注入,是一個非常常見的漏洞,這種型別的漏洞雖然有些難以利用,但一旦利用成功就會造成非常危險的後果。漏洞的形成的根本原因是程式沒有對使用者輸入的反序列化字串進行檢測,導致反序列化過程可以被惡意控制,進而造成程式碼執行、getshell等一系列不可控的後果。反序列化漏洞並不是PHP特有,也存在於Java、Python等語言之中,但其原理基本相通。

1.2 漏洞原理

接下來我們通過幾個例項來理解什麼是PHP序列化與反序列化以及漏洞形成的具體過程,首先建立1.php檔案檔案內容如下:

檔案中有一個TestClass類,類中定義了一個$variable變數和一個PrintVariable函式,然後例項化這個類並呼叫它的方法,執行結果如下:

這是一個正常的類的例項化和成員函式呼叫過程,但是有一些特殊的類成員函式在某些特定情況下會自動呼叫,稱之為magic函式,magic函式命名是以符號__開頭的,比如__construct當一個物件建立時被呼叫,__destruct當一個物件銷燬時被呼叫,__toString當一個物件被當作一個字串被呼叫。為了更好的理解magic方法是如何工作的,在2.php中增加了三個magic方法,__construct, __destruct和__toString。

執行結果如下,注意還有其他的magic方法,這裡只列舉了幾個。

php允許儲存一個物件方便以後重用,這個過程被稱為序列化。為什麼要有序列化這種機制呢?因為在傳遞變數的過程中,有可能遇到變數值要跨指令碼檔案傳遞的過程。試想,如果在一個指令碼中想要呼叫之前一個指令碼的變數,但是前一個指令碼已經執行完畢,所有的變數和內容釋放掉了,我們要如何操作呢?難道要前一個指令碼不斷的迴圈,等待後面指令碼呼叫?這肯定是不現實的。serialize和unserialize就是用來解決這一問題的。serialize可以將變數轉換為字串並且在轉換中可以儲存當前變數的值;unserialize則可以將serialize生成的字串變換回變數。讓我們在3.php中新增序列化的例子,看看php物件序列化之後的格式。

輸出如下

O表示物件,4表示物件名長度為4,”User”為類名,2表示成員變數個數,大括號裡分別為變數的型別、名稱、長度及其值。想要將這個字串恢復成類物件需要使用unserialize重建物件,在4.php中寫入如下程式碼

執行結果

magic函式__construct和__destruct會在物件建立或者銷燬時自動呼叫,__sleep方法在一個物件被序列化的時候呼叫,__wakeup方法在一個物件被反序列化的時候呼叫。在5.php中新增這幾個magic函式的例子。

執行結果

OK,到此我們已經知道了magic函式、序列化與反序列化這幾個重要概念,那麼這個過程漏洞是怎麼產生的呢?我們再來看一個例子6.php

這段程式碼包含兩個類,一個example和一個process,在process中有一個成員函式close(),其中有一個eval()函式,但是其引數不可控,我們無法利用它執行任意程式碼。但是在example類中有一個__destruct()解構函式,它會在指令碼呼叫結束的時候執行,解構函式呼叫了本類中的一個成員函式shutdown(),其作用是呼叫某個地方的close()函式。於是開始思考這樣一個問題:能否讓他去呼叫process中的close()函式且$pid變數可控呢?答案是可以的,只要在反序列化的時候$handle是process的一個類物件,$pid是想要執行的任意程式碼程式碼即可,看一下如何構造POC

執行效果

當我們序列化的字串進行反序列化時就會按照我們的設定生成一個example類物件,當指令碼結束時自動呼叫__destruct()函式,然後呼叫shutdown()函式,此時$handle為process的類物件,所以接下來會呼叫process的close()函式,eval()就會執行,而$pid也可以進行設定,此時就造成了程式碼執行。這整個攻擊線路我們稱之為ROP(Return-oriented programming)鏈,其核心思想是在整個程序空間內現存的函式中尋找適合程式碼片斷(gadget),並通過精心設計返回程式碼把各個gadget拼接起來,從而達到惡意攻擊的目的。構造ROP攻擊的難點在於,我們需要在整個程序空間中搜索我們需要的gadgets,這需要花費相當長的時間。但一旦完成了“搜尋”和“拼接”,這樣的攻擊是無法抵擋的,因為它用到的都是程式中合法的的程式碼,普通的防護手段難以檢測。反序列化漏洞需要滿足兩個條件:

1、程式中存在序列化字串的輸入點
2、程式中存在可以利用的magic函式

接下來通過Typecho的序列化漏洞進行實戰分析。

二 Typecho漏洞分析

漏洞的位置發生在install.php,首先有一個referer的檢測,使其值為一個站內的地址即可繞過。

入口點在232行

這裡將cookie中的__typecho_config值取出,然後base64解碼再進行反序列化,這就滿足了漏洞發生的第一個條件:存在序列化字串的輸入點。接下來就是去找一下有什麼magic方法可以利用。先全域性搜尋__destruct()和__wakeup()

找到兩處__destruct(),跟進去沒有可利用的地方,跟著程式碼往下走會例項化一個Typecho_Db,位於var\Typecho\Db.php,Typecho_Db的建構函式如下

在第120行使用.運算子連線$adapterName,這時$adapterName如果是一個例項化的物件就會自動呼叫__toString方法(如果存在的話),那全域性搜尋一下__toString()方法。找到3處

前兩處無法利用,跟進第三處,__toString()在var\Typecho\Feed.php 223行

跟進程式碼在290處有如下程式碼

如何$item['author']是一個類而screenName是一個無法被直接呼叫的變數(私有變數或根本就不存在的變數),則會自動呼叫__get() magic方法,進而再去尋找可以利用的__get()方法,全域性搜尋

共匹配到10處,其中在var\Typecho\Request.php中的程式碼可以利用,跟進

再跟進到get函式

接著進入_applyFilter函式

可以看到array_map和call_user_func函式,他們都可以動態的執行函式,第一個引數表示要執行的函式的名稱,第二個引數表示要執行的函式的引數。我們可以在這裡嘗試執行任意程式碼。接下來梳理一下整個流程,資料的輸入點在install.php檔案的232行,從外部讀入序列化的資料。然後根據我們構造的資料,程式會進入Db.php的__construct()函式,然後進入Feed.php的__toString()函式,再依次進入Request.php的__get()、get()、_applyFilter()函式,最後由call_user_func實現任意程式碼執行,整個ROP鍊形成。構造POC如下

POC的22行其實與反序列化無關,但是不加這一行程式就不會有回顯,因為在 install.php 的開頭部分呼叫了程式呼叫了ob_start(),它會開啟緩衝區並將要輸出的內容都放進緩衝區,想要使用的時候可以再取出。但是我們的物件注入會在後續的程式碼中造成資料庫錯誤

然後會觸發exception,其中的ob_end_clean()會將緩衝區中的內容清空,導致無法回顯。

想要解決這個問題需要在ob_end_clean()執行之前是程式退出,兩種方法:

1、使程式跳轉到存在exit()的程式碼段

2、使程式提前報錯,退出程式碼

POC中使用的是第二種方法

解決了上述問題後就可以執行任意程式碼並能看到回顯了,執行的時候在http頭新增referre使其等於一個站內地址,然後在cookie中新增欄位__typecho_config,其值為上述exp的輸出。

有些利用方式並不需要回顯,比如寫個shell什麼的,POC如下

執行結果,在根目錄生成shell.php