這是這幾天一直關注的漏洞了,wordpress
上個禮拜發布的4.2.4版本,其中提到修補了可能存在的SQL漏洞和多個XSS。 Check point
也很快發出了分析,我也來分析與復現一下最新的這個漏洞。
首先,說明一下背景。wordpress
中用戶權限分為訂閱者、投稿者、作者、編輯和管理員。
權限最低的是訂閱者,訂閱者只有訂閱文章的權限,wordpress
開啟注冊后默認注冊的用戶就是訂閱者。國內很多知名網站,如Freebuf
,用戶注冊后身份即為“訂閱者”。
我們先看到一個提權漏洞,通過這個提權漏洞,我們作為一個訂閱者,可以越權在數據庫里插入一篇文章。
Wordpress
檢查用戶權限是調用current_user_can
函數,我們看到這個函數:
調用的has_cap
方法,跟進
再次跟進map_meta_cap
函數:
可以見到,這個函數是真正檢查權限的。出錯誤的代碼在檢查’edit_post’
和’edit_page’
的部分:
可見,這里當$post
不存在的時候,直接break
出switch
邏輯了,后面所有檢查的代碼都沒有執行。
$post
是要編輯的文章的ID,也就是說,如果我要編輯一篇不存在的文章,這里不檢查權限直接返回。
正常情況下是沒有問題的,因為不存在的文章也沒有編輯一說了。
我們再看到后臺編輯文章的部分:/wp-admin/post.php
這里首先獲取$_GET[‘post’]
,找不到才獲取$_POST[‘post_ID’]
,也就是可以說此時的$post_ID
是來自GET
的。
但我們后面調用current_user_can
函數時傳入的post_ID
卻是來自POST
的:
這里就是一個邏輯問題,當我們在GET參數中傳入正確的postid
(這樣在get_post
的時候不會產生錯誤),而在POST
參數中傳入一個不存在的postid
,那么就能夠繞過檢查edit_post
權限的步驟。
但是這個邏輯錯誤暫時不能造成嚴重的危害,因為實際上編輯文章的代碼在edit_post
函數中,而這個函數取的post_ID
來自$_POST
。
wordpress
對于CSRF漏洞的防御措施是使用_wpnonce
(也就是token),而且它的token很嚴格,不同的操作有不同的token。
比如我們這里,如果想調用edit_post函數,需要經過以下邏輯:
check_admin_referer
就是檢查_wpnonce
的函數,當$post_type==’postajaxpost’
的時候,此時_wpnonce
的名字就是“add-postajaxpost”
。
那么怎么獲取名字為”add-postajaxpost”
的_wpnonce
呢?
看到上面一點的位置:
有個post-quickdraft-save
操作。這個操作是用來臨時儲存草稿的,只要用戶訪問這個操作,就會在數據庫post
表中插入一個status
為auto-draft
的新文章。
如上圖畫出來的步驟,因為我們不知道名字為”add-post”
的_wpnonce
,所以進入到wp_dashboard_quick_press
函數,跟進:
見上圖,很幸運的是,在這個函數中wordpress
居然自己把此時的_wpnonce
輸出在表單里了。
所以,只要我們訪問一次post-quickdraft-save
,就可以獲得add-post
的_wpnonce
,從而繞過check_admin_referer
函數。
這一節實際上是這個提權洞的真正核心,在我們拿到_wpnonce
后,進入edit_post
函數。
我們目的是去update一篇文章,但剛才0x01中說到,如果要繞過權限檢查的函數,需要傳入一個“不存在”的文章id。那么即使可以執行update,我們也不可能修改已經存在的文章呀?
這里實際上涉及到一個由競爭造成的邏輯漏洞。看到edit_post
函數代碼:
上面兩個圖應該很直觀了。在0x01中說到的current_user_can
被繞過以后,到最終執行update
語句中間,這一段代碼的執行時間是真空的。
比如我們傳入的tax_input=1,2,3,4…10000
,那么實際上那條查詢語句就要執行10000次,這是需要執行很長時間的。(在我自己的虛擬機上測試,執行10000次這條語句,大概需要5~10秒左右)
那么假設在這段時間內,有新插入的文章,那么我們之前那個“不存在”的id,不就可能可以存在了嗎(只需要把id設置為最新一篇文章id+1)? 但有個問題是,我們怎么在這段時間內插入一篇新的文章?因為在0x02中為了獲取_wpnonce
,已經執行過post-quickdraft-save
了。執行post-quickdraft-save
可以在數據庫插入一篇status
為auto-draft
的文章,但每個用戶最多只會插入一篇文章。
在check-point
的原文中,它提到的方法是,等待一個星期,wordpress
會自動將這篇文章刪除,而_wpnonce
會多保留一天,這樣在這天我們再次執行post-quickdraft-save
又可以插入一篇文章了。
我自己想了一下,其實沒必要這么麻煩。如果我們能夠再注冊一個身份為訂閱者的賬號,就可以再插入一篇文章了,所以我的POC是不需要等待一個禮拜的。
這三個漏洞組合起來,造成了一個提權漏洞。針對第一篇文章描述的提權漏洞,我寫了一個EXP,執行后訂閱者就可以在垃圾桶內插入一篇文章:
訪問文章編輯頁面可以看到這篇文章:
那么,僅僅是一個這樣的提權漏洞,實際上沒有太大意義。Check-point
第二篇文章里提到了一個因為這個提權漏洞導致的SQL注入。
先說這個注入的原理。
/wp-includes/post.php
如上圖。Wordpress
很多地方執行SQL語句使用的預編譯,但僅限于直接接受用戶輸入的地方。而上圖中明顯是一個二次操作,先用get_post_meta
函數從數據庫中取出meta,之后以字符串拼接的方式插入SQL語句。
這個地方造成一個二次注入。
我們來看看第一次是如何入庫的。首先wp_trash_post
是將文章刪除的方法,其中刪除文章后又調用wp_trash_post_comments
將文章下的評論也刪除了:
跟進wp_trash_post_comments
函數:
如上圖,可以看到這個“comment_approved”
其實也是從數據庫中取出來的。所以這個注入我稱之為“三次注入”。
那么我再繼續跟進,看看最早的comment_approved
是從哪來的。
實際上看到圖中的SQL語句就大概知道了,這個comment_approved
是comments
(評論)表的一個字段,我分別看了新增評論、修改評論兩個函數,發現修改評論的函數(edit_comment
)中,有涉及到這個字段:
所以,這一連串操作最后造成的結果就是一個SQL注入漏洞。
總結一下1234,整個利用過程如下:
利用快速草稿插入文章->越權編輯文章->插入評論->修改評論(惡意數據入庫)->刪除文章(惡意數據進入另一個庫)->反刪除文章(惡意數據被取出,直接插入SQL語句導致注入)
這里不得不提到check-point
的原文,原文的第二篇全文只字未提wordpress
的token
也就是_wpnonce
,但wordpress后臺幾乎所有操作都需要特定的_wpnonce
。在第一步中我們通過一處泄露點獲取了“add-postajaxpost
”的_wpnonce
,但實際上后面的每一步(增加、編輯評論、trash
文章、untrash
文章)都需要不同的_wpnonce
,那么這些_wpnonce
我們怎么獲得?
經過我的分析,最后實在找不到在訂閱者權限下怎么獲得“增加評論”和“反刪除文章”的_wpnonce
,而“修改評論”、“刪除文章”的_wpnonce
倒是可以在后臺找到。
另外,雖然前臺也可以增加評論,但前臺增加評論會檢查所屬文章是否是草稿、狀態是否是public
或private
,我們沒法給這篇文章以及其派生的預覽文章增加評論。
所以我把基礎賬號的權限提升一下,提升到可以發表文章與編輯文章的作者權限,再來對注入漏洞進行復現。
首先發表一篇文章,并在該文下回復一條評論:
我們再來修改這條評論:
在文章編輯頁面找到trash
的_wpnonce
,將該評論所屬的文章丟入垃圾箱:
再找到反刪除的_wpnonce
,從垃圾箱里反刪除這篇文章:
查看SQL執行記錄,發現已經注入成功,引入單引號:
最后,我來總結一下這個漏洞。
這個漏洞有兩個核心點,一是利用一個競爭條件邏輯錯誤,造成的一個越權漏洞;二是利用一個三次操作,導致最后的SQL注入漏洞。
這兩個核心技術點都是很有代表性的,通篇學習下來,不得不佩服洞主的思路和對wordpress
的研究深度。
但我也很遺憾,沒能分析出在最低權限下怎樣去注入,主要還是_wpnonce
的獲取導致漏洞利用上出現了一些問題。
另外,該SQL注入有一個不得不說的雞肋點,在污染數據第一次入庫的時候,數據庫中保存這個數據的字段只有20字符長度,所以導致最后我們的注入點是一個“存在長度限制”的注入點,對于這個長度限制的利用,我也暫時沒有想出更好的方法。
雖然存在長度限制,但因為注入點出現在update語句的評論表中,所以通過這個漏洞,可以將一整個站的評論全部置為0,對于像Freebuf這類社交性質的網站來說危害還是巨大的。
所以,我對這個SQL注入的評價是:利用上(可能)比較雞肋,但思路是絕對一流的,值得學習。