WordPress SQL注入漏洞與提權(quán)分析
WordPress是大多數(shù)站長用的發(fā)布平臺,大多數(shù)時候,常見WordPress插件漏洞,這次WordPressSQL注入漏洞(CVE-2015-5623、CVE-2015-2213)為其核心功能出現(xiàn)SQL注入,足以導致...
WordPress核心功能SQL注入漏洞分析
威脅響應中心研究員對Wordpress核心功能SQL注入漏洞(編號為CVE-2015-5623和CVE-2015-2213)進行了詳細的分析
0x00 漏洞概述
在twitter上看到Wordpress核心功能出現(xiàn)SQL注入漏洞,想學習下,就深入的跟了下代碼,結(jié)果發(fā)現(xiàn)老外留了好大的一個坑。雖然確實存在注入問題,但是卻沒有像他blog中所說的那樣可以通過訂閱者這樣的低權(quán)限來觸發(fā)SQL注入漏洞。
這個Wordpress漏洞系列的文章目前更新了兩個部分,一個是通過權(quán)限繞過實現(xiàn)訂閱者權(quán)限寫一篇文章到回收站,另一個就是通過寫入的這篇文章來實現(xiàn)SQL注入漏洞。這兩個漏洞的描述,TSRC的phithon寫的文章其實已經(jīng)很清楚了,我這里從我分析的角度來介紹這兩個漏洞的形成、利用以及phithon省略掉的原文部分內(nèi)容。
0x01 越權(quán)提交文章
_wpnonce的獲取
在講越權(quán)漏洞之前,我們需要介紹一下Wordpress后臺的_wpnonce參數(shù),這個參數(shù)主要是用來防止CSRF攻擊的token。后臺大多數(shù)敏感功能都會通過,當前用戶信息、功能名稱、操作對象id等內(nèi)容生成token,所以我們很難在沒有token的時候進行某些功能的使用。這個CSRF的防護機制間接地導致之后SQL注入很難再低權(quán)限情況下觸發(fā)(因為看不到token),后面講SQL注入漏洞時,我們會詳細的聊這個問題有多坑。
之所以要把_wpnonce的獲取放在前面講,是因為,我們需要一個讓我們可以使用編輯提交文章功能的token。這個功能的token我們可以通過訪問后臺post.php的post-quickdraft-save功能來獲取,嚴格的來說這個獲取方式也算是一個信息泄露漏洞,官方也在新版本中進行了修補。下面我我們來看下這個token泄露的原因。部分代碼如下:
case 'post-quickdraft-save': // Check nonce and capabilities $nonce = $_REQUEST['_wpnonce']; $error_msg = false; // For output of the quickdraft dashboard widget require_once ABSPATH . 'wp-admin/includes/dashboard.php'; if ( ! wp_verify_nonce( $nonce, 'add-post' ) ) $error_msg = __( 'Unable to submit this form, please refresh and try again.' ); if ( ! current_user_can( 'edit_posts' ) ) $error_msg = __( 'Oops, you don’t have Access to add new drafts.' ); if ( $error_msg ) return wp_dashboard_quick_press( $error_msg );
從上面代碼我們可以看出這個功能在發(fā)現(xiàn)出現(xiàn)錯誤時,會將錯誤信息通過wp_dashboard_quick_press這個函數(shù)打印到頁面上,而這個函數(shù)生成的頁面的代碼中有這樣一行:
- PHP wp_nonce_field( 'add-post' ); 1 wp_nonce_field('add-post
生成了add-post功能的_wpnonce,也就是說,即使我們做了一些被禁止的操作,返回頁面中也會出現(xiàn)這個_wpnonce。
越權(quán)提交與條件競爭
如phithon文章所說,由于GP使用邏輯混亂而導致的驗證繞過。我們來看post.php文件在開始檢驗文章是否存在的代碼:
if ( isset( $_GET['post'] ) ) $post_id = $post_ID = (int) $_GET['post'];elseif ( isset( $_POST['post_ID'] ) ) $post_id = $post_ID = (int) $_POST['post_ID'];else $post_id = $post_ID = 0; global $post_type, $post_type_object, $post; if ( $post_id ) $post = get_post( $post_id ); if ( $post ) { $post_type = $post->post_type; $post_type_object = get_post_type_object( $post_type );}
可以看到在先提取GET中的post參數(shù)作為文章id來提取文章信息,如果沒有GET的post參數(shù),再去用POST的post_ID參數(shù)來提取。而真正檢驗用戶是否對文章具有編輯權(quán)限,則是在edit_post中進行的,而這里的判斷參數(shù)是從POST中提取的:
function edit_post( $post_data = null ) { global $wpdb; if ( empty($post_data) ) $post_data = &$_POST; // Clear out any data in internal vars. unset( $post_data['filter'] ); $post_ID = (int) $post_data['post_ID']; $post = get_post( $post_ID ); $post_data['post_type'] = $post->post_type; $post_data['post_mime_type'] = $post->post_mime_type; if ( ! empty( $post_data['post_status'] ) ) { $post_data['post_status'] = sanitize_key( $post_data['post_status'] ); if ( 'inherit' == $post_data['post_status'] ) { unset( $post_data['post_status'] ); } } $ptype = get_post_type_object($post_data['post_type']); if ( !current_user_can( 'edit_post', $post_ID ) ) { if ( 'page' == $post_data['post_type'] ) wp_die( __('You are not allowed to edit this page.' )); else wp_die( __('You are not allowed to edit this post.' )); }
我們繼續(xù)看這段代碼最后的if判斷,判斷當前用戶是否有編輯這篇文章的權(quán)限。這個判斷最終的操作是在map_meta_cap這個函數(shù)中進行的:
case 'edit_post': case 'edit_page': $post = get_post( $args[0] ); if ( empty( $post ) ) break; if ( 'revision' == $post->post_type ) { $post = get_post( $post->post_parent ); }
可以看出如果文章是不存在的,那么就會break出switch,在函數(shù)結(jié)束時返回$caps變量,而$caps在函數(shù)開始的時候已經(jīng)被定義為一個空的數(shù)組了,也就是說,這里會返回一個空數(shù)組。下面我們來向前走,返回到調(diào)用map_meta_cap的has_cap函數(shù)中,看看后續(xù)的操作
public function has_cap( $cap ) { if ( is_numeric( $cap ) ) { _deprecated_argument( __FUNCTION__, '2.0', __('Usage of user levels by plugins and themes is deprecated. Use roles and capabilities instead.') ); $cap = $this->translate_level_to_cap( $cap ); } $args = array_slice( func_get_args(), 1 ); $args = array_merge( array( $cap, $this->ID ), $args ); $caps = call_user_func_array( 'map_meta_cap', $args ); // Multisite super admin has all caps by definition, Unless specifically denied. if ( is_multisite() && is_super_admin( $this->ID ) ) { if ( in_array('do_not_allow', $caps) ) return false; return true; } $capabilities = apply_filters( 'user_has_cap', $this->allcaps, $caps, $args, $this ); $capabilities['exist'] = true; // Everyone is allowed to exist foreach ( (array) $caps as $cap ) { if ( empty( $capabilities[ $cap ] ) ) return false; } return true; }
看最后那個foreach,它檢測$caps中的各個元素在$capabilities中是否有不存在的,如果有那么就return false,但是$caps是個空數(shù)組,這樣很容易我們就獲得了一個true,權(quán)限校驗成功繞過。那么這樣我們可以得到的一個信息就是,我們可以通過這個缺陷去嘗試update一個并不存在的文章。
那么現(xiàn)在問題來了,直接update一個不存在的文章是沒有意義的,因為數(shù)據(jù)庫執(zhí)行SQL是肯定會報錯,我們要怎么才能成功創(chuàng)建一篇文章呢?
在校驗權(quán)限之后,數(shù)據(jù)庫執(zhí)行操作之前,post.php中有這樣一段代碼:
if ( isset( $post_data['tax_input'] ) ) { foreach ( (array) $post_data['tax_input'] as $taxonomy => $terms ) { // Hierarchical taxonomy data is already sent as term IDs, so no conversion is necessary. if ( is_taxonomy_hierarchical( $taxonomy ) ) { continue; } if ( ! is_array( $terms ) ) { $comma = _x( ',', 'tag delimiter' ); if ( ',' !== $comma ) { $terms = str_replace( $comma, ',', $terms ); } $terms = explode( ',', trim( $terms, " /n/t/r/x0B," ) ); } $clean_terms = array(); foreach ( $terms as $term ) { // Empty terms are invalid input. if ( empty( $term ) ) { continue; } $_term = get_terms( $taxonomy, array( 'name' => $term, 'fields' => 'ids', 'hide_empty' => false, ) );
如果在POST的數(shù)據(jù)中存在名為tax_input的數(shù)組參數(shù),那么就對參數(shù)中的值使用逗號進行切割,然后對分割出來的每個內(nèi)容進行一次select查詢。那么這里我們可以想象一下,如果我們post_ID中填寫的是對當前最新文章ID+1的數(shù)值,并且在tax_input這個參數(shù)添加特別多的內(nèi)容,導致它不停地select查詢,我們是不是在這個停滯的時間當中插入一篇文章(這篇文章的ID剛好是最新文章ID+1),那么后面的update是不是就有意義了?