ディレクトリ
0x00の序文
ノー
0x01の分析
この脆弱性は問題外右判断を推測、「匿名ユーザーがプライベート・ページにアクセスすることができます」と記載されています。あなたは問題がどこにあるか知って取得したい場合は、最初のWPのgetのページ(ページ)原則/ポスト(記事を)知っている、との問題であることを知るために右の判断とロジックのロジックを見つける必要があります。
ここでは、/ WP-含む/クラスwp.phpメイン関数WPのコア処理フローから直接見始める:メイン()
public function main( $query_args = '' ) {
$this->init();//获取当前用户信息
$this->parse_request( $query_args );//解析路由,匹配路由模式,取出匹配的路由中的用户输入参数(比如year,month等)赋值给$this->query_vars。(并将部分用户参数绑定到$this->query_vars中)。然后进行一些过滤操作。
$this->send_headers();//设置HTTP响应头,比如Content-Type等
$this->query_posts();//根据$this->query_vars等参数,获取posts/pages
$this->handle_404();
$this->register_globals();
do_action_ref_array( 'wp', array( &$this ) );
}
の$ this - >のinit()の呼び出し直下wp_get_current_user現在のユーザーのメタ情報を格納するWP_Userクラスは、$ current_user-に署名していない()$ CURRENT_USERグローバル変数、> ID === 0。
次に入力します$ this-> parse_request、このルーティング機能は、主の$ this - > query_vars初期化し、処理のために使用されています。二つの部分に分け観点は、最初の部分は、処理経路、ルート一致書き換えモードです。
public function parse_request( $extra_query_vars = '' ) {
global $wp_rewrite;
...
// Fetch the rewrite rules.
$rewrite = $wp_rewrite->wp_rewrite_rules();//加载所有路由重写规则,用于与当前请求路径进行匹配
if ( ! empty( $rewrite ) ) {
...
if ( empty( $request_match ) ) {
...
} else {
foreach ( (array) $rewrite as $match => $query ) {//匹配路由规则
...
if ( preg_match( "#^$match#", $request_match, $matches ) || preg_match( "#^$match#", urldecode( $request_match ), $matches ) ) {
...
// Got a match.
$this->matched_rule = $match;//找到匹配成功的rewrite规则,立即break
break;
}
}
}
if ( isset( $this->matched_rule ) ) {
...
$query = addslashes( WP_MatchesMapRegex::apply( $query, $matches ) );//规则化用户请求url,以与路由进行完美对应
$this->matched_query = $query;
// Parse the query.
parse_str( $query, $perma_query_vars );
...
}
...
}
第二の部分、ユーザパラメータ解析、>の$ this - を配置query_varsの値
class WP{
...
public $public_query_vars = array( 'm', 'p', 'posts', 'w', 'cat',
'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence',
'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order',
'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second',
'name', 'category_name', 'tag', 'feed', 'author_name', 'static',
'pagename', 'page_id', 'error', 'attachment', 'attachment_id',
'subpost', 'subpost_id', 'preview', 'robots', 'taxonomy', 'term',
'cpage', 'post_type', 'embed' );
...
public function parse_request( $extra_query_vars = '' ) {
...
...
<接上第一部分>
foreach ( $this->public_query_vars as $wpvar ) {
if ( isset( $this->extra_query_vars[ $wpvar ] ) ) {
$this->query_vars[ $wpvar ] = $this->extra_query_vars[ $wpvar ];
} elseif ( isset( $_GET[ $wpvar ] ) && isset( $_POST[ $wpvar ] ) && $_GET[ $wpvar ] !== $_POST[ $wpvar ] ) {
wp_die( __( 'A variable mismatch has been detected.' ), __( 'Sorry, you are not allowed to view this item.' ), 400 );
} elseif ( isset( $_POST[ $wpvar ] ) ) {
$this->query_vars[ $wpvar ] = $_POST[ $wpvar ];
} elseif ( isset( $_GET[ $wpvar ] ) ) {
$this->query_vars[ $wpvar ] = $_GET[ $wpvar ];
} elseif ( isset( $perma_query_vars[ $wpvar ] ) ) {
$this->query_vars[ $wpvar ] = $perma_query_vars[ $wpvar ];
}
...
}
...
}
同じ名前の主要パラメータで来た場合には、トラバースます$ this-> public_query_varsメンバ変数ここで見ることができ、ます$ this-> query_varsに直接割り当てられています。ここでは、唯一の$ this - を制御することができ、言うことです>名前は$でのキー値をquery_vars this-> public_query_varsでは、これらのキーのみを制御しています。
array( 'm', 'p', 'posts', 'w', 'cat',
'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence',
'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order',
'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second',
'name', 'category_name', 'tag', 'feed', 'author_name', 'static',
'pagename', 'page_id', 'error', 'attachment', 'attachment_id',
'subpost', 'subpost_id', 'preview', 'robots', 'taxonomy', 'term',
'cpage', 'post_type', 'embed' );
main()関数の先頭に戻ります:
public function main( $query_args = '' ) {
$this->init();//获取当前用户信息
$this->parse_request( $query_args );//解析路由,匹配路由模式,取出匹配的路由中的用户输入参数(比如year,month等)赋值给$this->query_vars。(并将部分用户参数绑定到$this->query_vars中)。然后进行一些过滤操作。
$this->send_headers();//设置HTTP响应头,比如Content-Type等
$this->query_posts();//根据$this->query_vars等参数,获取posts/pages
$this->handle_404();
$this->register_globals();
do_action_ref_array( 'wp', array( &$this ) );
}
次の$ this - > SEND_HEADERSは()HTTPレスポンスヘッダを設定するために使用され、もはやフォロー、直接の$ this - > query_postsを()に従うと、次の行があり、これは表示ポスト/ページどこに便利です、この分析の焦点です。
query_posts(/wp-includes/class-wp-query.php:get_postsにいくつかのメンバ変数の初期化後に)最初のセット()。ここでも、コード、およびこの記事はので、ここでの脆弱性、表示ロジックポスト/ページおよび認証ではなく、その他の詳細とメインディッシュ「プライベートページを表示する権限がない」ためのものですので。
ここでは、最初の構築物のSQLクエリポスト/ページに声明し、その後はの$ this - >の記事をチェックアウトして、結果を割り当てます。
$split_the_query = apply_filters( 'split_the_query', $split_the_query, $this );
if ( $split_the_query ) {
$this->request = "SELECT $found_rows $distinct {$wpdb->posts}.ID FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits";
...
$ids = $wpdb->get_col( $this->request );//查询数据库,获取post/page的id
if ( $ids ) {
$this->posts = $ids;
$this->set_found_posts( $q, $limits );//通过id获取page/post
_prime_post_caches( $ids, $q['update_post_term_cache'], $q['update_post_meta_cache'] );
} else {
$this->posts = array();
}
} else {
$this->posts = $wpdb->get_results( $this->request );//获取post的内容
$this->set_found_posts( $q, $limits );
}
アップその方法の使用に$のsplit_the_queryに取得するには、2つの方法があります。今split_the_queryに従わないために2つの方法のため、違いはありません。
最初に私がログイン時間、および要求URLはwordpress-5.2.3/index.php
、ここでは、SQL文の中に構造を見て
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 10
ここでwp_posts.post_status = 'publish'
の制限我々は唯一のポストであるpost_typeのパブリックステータス=「ポスト」レコードを、見ることができます。
第二に着陸管理者が同じURLにアクセスし、SQL文は次のようになります
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'private') ORDER BY wp_posts.post_date DESC LIMIT 0, 10
1のほかOR wp_posts.post_status = 'private'
、他の部分は、それはとても建設はどこ推測、プライベートポスト(がらくた)の状態を見ることができ、管理者アカウントのためと言うことです、まったく同じですwp_posts.post_status=?
近くの可能な認証動作を行いました。
見上げる、私はどこpost_statusステートメントを構築するために場所を見つけました
$q_status = array();
if ( ! empty( $q['post_status'] ) ) {//由于本路由中无法设置post_status的值,因此第一个if语句块不看
$statuswheres = array();
$q_status = $q['post_status'];
...//根据$q_status构造where子句
} elseif ( ! $this->is_singular ) {
$where .= " AND ({$wpdb->posts}.post_status = 'publish'";
...
if ( $this->is_admin ) {
// Add protected states that should show in the admin all list.
$admin_all_states = get_post_stati(
array(
'protected' => true,
'show_in_admin_all_list' => true,
)
);
foreach ( (array) $admin_all_states as $state ) {
$where .= " OR {$wpdb->posts}.post_status = '$state'";
}
}
if ( is_user_logged_in() ) {
// Add private states that are limited to viewing by the author of a post or someone who has caps to read private states.
$private_states = get_post_stati( array( 'private' => true ) );
foreach ( (array) $private_states as $state ) {
$where .= current_user_can( $read_private_cap ) ? " OR {$wpdb->posts}.post_status = '$state'" : " OR {$wpdb->posts}.post_author = $user_id AND {$wpdb->posts}.post_status = '$state'";
}
}
$where .= ')';
}
ここでは、唯一、このような民間のようないくつかの他のpost_statusを追加し、その後is_adminとis_user_logged_in()によると、()文ブロック、ショースプライス公共のelseifを調べる必要があります。私たちの目標は、「アクセスプライベートコンテンツにユーザーがログインしていません」に、我々はこのELSEIFを入力しない場合、これは、唯一の論理的な観点から、バイパスis_adminまたはis_user_logged_in()(もちろん、ない可能性が高い)基本的な欠陥にするかどうかを考えていないですので、アップポスト/ページのすべてを読むことができないだろうどこ文ブロックは、これを構築しませんか?
この条件はis_singular正論理(例えば真など)することができます>(!の$ this - > is_singular)ELSEIF、私たちの目標は、の$ this - を作ることです。見つけるためにバックトラックこの変数
$this->is_singular = $this->is_single || $this->is_page || $this->is_attachment;
長い間、我々はこれら三つの変数は任意の値とすることができ作るとしては、より明白な、アップ見つけることは、真実であるとして、それはこの場所ということです。
if ( ( '' != $qv['attachment'] ) || ! empty( $qv['attachment_id'] ) ) {
$this->is_single = true;
$this->is_attachment = true;
} elseif ( '' != $qv['name'] ) {//wp_posts.post_name
$this->is_single = true;
} elseif ( $qv['p'] ) {//wp_posts.ID
$this->is_single = true;
} elseif ( ( '' !== $qv['hour'] ) && ( '' !== $qv['minute'] ) && ( '' !== $qv['second'] ) && ( '' != $qv['year'] ) && ( '' != $qv['monthnum'] ) && ( '' != $qv['day'] ) ) {
$this->is_single = true;
} elseif ( '' != $qv['static'] || '' != $qv['pagename'] || ! empty( $qv['page_id'] ) ) {
$this->is_page = true;
$this->is_single = false;
} else {
...
}
添付ファイル、名前、P、静的などのような:私たちはうまくいくつかのキーの$ QVを設定することをこれは示しています、。$ QVをバックトラックすることにより、それが発見されました$qv=&$this->query_vars;
。我々は唯一の$ this -上記で制御することができますキーquery_vars>それらにpublic_query_varsあります
array( 'm', 'p', 'posts', 'w', 'cat',
'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence',
'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order',
'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second',
'name', 'category_name', 'tag', 'feed', 'author_name', 'static',
'pagename', 'page_id', 'error', 'attachment', 'attachment_id',
'subpost', 'subpost_id', 'preview', 'robots', 'taxonomy', 'term',
'cpage', 'post_type', 'embed' );
私たちは見ることができます:添付ファイル、名前、P、静的な制御我々はできるこれらのキー限り、うまくurlパラメータで直接転送します。はっきりis_singleの最後のelseif文ブロックが偽であるため除いて、比較することで見つけることができ、残りはちょうどポスト/ページ/添付ファイルを取る、つまり、真であるあなたは、pパラメータを渡した場合でも、また、パラメータ名を通して見ることができますwp_posts.IDデータのみが、データベース内の一致を検索するために、名前のパラメータは、wp_posts.post_nameデータが同じ一致渡されました。従ってのみ着信静的= XXX、プライベート後ろ両方の制限をバイパスして、すべてのデータを抽出することができる比較によって。
要求されたデータ型の制限の開始に続いて、ページ/ポスト/添付ファイル。
if ( 'any' == $post_type ) {
$in_search_post_types = get_post_types( array( 'exclude_from_search' => false ) );
if ( empty( $in_search_post_types ) ) {
$where .= ' AND 1=0 ';
} else {
$where .= " AND {$wpdb->posts}.post_type IN ('" . join( "', '", array_map( 'esc_sql', $in_search_post_types ) ) . "')";
}
} elseif ( ! empty( $post_type ) && is_array( $post_type ) ) {
$where .= " AND {$wpdb->posts}.post_type IN ('" . join( "', '", esc_sql( $post_type ) ) . "')";
} elseif ( ! empty( $post_type ) ) {
$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_type = %s", $post_type );
$post_type_object = get_post_type_object( $post_type );
} elseif ( $this->is_attachment ) {
$where .= " AND {$wpdb->posts}.post_type = 'attachment'";
$post_type_object = get_post_type_object( 'attachment' );
} elseif ( $this->is_page ) {
$where .= " AND {$wpdb->posts}.post_type = 'page'";
$post_type_object = get_post_type_object( 'page' );
} else {
$where .= " AND {$wpdb->posts}.post_type = 'post'";
$post_type_object = get_post_type_object( 'post' );
}
あなたは空のpost_typeを見ることができるときis_page post_typeがページをtrueに設定されている場合、あなたはデータのみのページタイプを取得することができます。
静的= XXXを設定することで、あなたは次のようにSQL文の最後は見ることができ試運転の後、何post_statusは、パブリックまたはプライベートを制限することではありませんがあります。
SELECT wp_posts.* FROM wp_posts WHERE 1=1 AND wp_posts.post_type = 'page' ORDER BY wp_posts.post_date DESC
この時点で、すべてのページには、すべてのこれらの記事がレンダリングされるかどうかを確認するには、以下の、の$ this - >ポストに格納されています。以下は、関連するコードです
// Check post status to determine if post should be displayed.
if ( ! empty( $this->posts ) && ( $this->is_single || $this->is_page ) ) {
$status = get_post_status( $this->posts[0] );//获取$this->posts中的第一个元素的post_status
...
$post_status_obj = get_post_status_object( $status );
// If the post_status was specifically requested, let it pass through.
if ( ! $post_status_obj->public && ! in_array( $status, $q_status ) ) {//如果post_status_obj的public属性为true或post_status在$q_status中,则不进入此if。由于本文前面已经分析$q_status不可控且为空,因此主要看第一个条件。
if ( ! is_user_logged_in() ) {
// User must be logged in to view unpublished posts.
$this->posts = array();//无权限查看
} else {
if ( $post_status_obj->protected ) {
...更细的鉴权
} elseif ( $post_status_obj->private ) {
if ( ! current_user_can( $read_cap, $this->posts[0]->ID ) ) {
$this->posts = array();//无权限查看
}
} else {
$this->posts = array();//无权限查看
}
}
}
...
}
ます$ this-以来>ポストは、私たちがページを読むためにある、と裁判官が入りますので、もし最初is_pageは、真です。そして、そこに興味深いのは、ある最初の記事で、次の取得の$ this - >ポスト、それは公共の缶であれば文の場合は、2番目に入らない、ので、直接それを「エコー認証」をバイパス一部。限り、我々はの$ this - >状態に公共のための投稿の最初の記事を保証するとして。最も古い順によって、私たちは、大きな可能性のために一般的に古いポスト公的機関、なぜなら正シーケンスASCの照会である、上の記事を置くことができます。
SQL文の前に
SELECT wp_posts.* FROM wp_posts WHERE 1=1 AND wp_posts.post_type = 'page' ORDER BY wp_posts.post_date DESC
>うquery_vars我々は、URLに= ASCを順序を追加することができている限り、上昇を制御したり、バックトラッキングによって見つけます$ this-の降順ため[「順序」]。
上記の分析整頓ロジックを想起し、入ってくる静的= XXX - > is_page ===真 - > is_singular ===真 - >節が制限されていない何もどこプライベート/パブリック/ ... - >すべてのページを取得する - >最後チェック権限は、最初のページのみが認証の前に表示されます。
ページのみを取得するとき、それは最終的に表示する前に認証を行いますので、このロジック抽象化されたノウハウは、/ポストは、問題ありません。それが唯一の最後の表示の最初のデータの前に認証バイパスの動作を確認しますので、私たちの主な関心事は、複数のデータを得ることです。複数のデータを同時に保証アクセスでも句回避の制限が真となっているの$ this - > is_single、ます$ this-> is_page、ます$ this-> is_attachment 1を確保します。
公式パッチアウトロジックは、このパッチバイパス静的変数、缶を除去して?これらの場所のメンバ変数の初期化まず見て:
if ( ( '' != $qv['attachment'] ) || ! empty( $qv['attachment_id'] ) ) {
$this->is_single = true;
$this->is_attachment = true;
} elseif ( '' != $qv['name'] ) {//wp_posts.post_name
$this->is_single = true;
} elseif ( $qv['p'] ) {//wp_posts.ID
$this->is_single = true;
} elseif ( ( '' !== $qv['hour'] ) && ( '' !== $qv['minute'] ) && ( '' !== $qv['second'] ) && ( '' != $qv['year'] ) && ( '' != $qv['monthnum'] ) && ( '' != $qv['day'] ) ) {
-$this->is_single = true;
} elseif ( '' != $qv['static'] || '' != $qv['pagename'] || ! empty( $qv['page_id'] ) ) {
$this->is_page = true;
$this->is_single = false;
} else {
...
}
いくつかのこれらの条件はそれほどis_singleで何の論理的な問題、存在しません、クエリの結果の前にその条件のすべてが<= 1に限定されている場合は、静的ステートメントブロックに加えて、見つかっ再び行くことにプログラムに持ち込まれた場合意味。公式修理パッチはに、静的なパラメータを削除することでelseif(''!=$qv['pagename'] || !empty($qv['page_id']))
あり、この条件も制限は一つだけを得ることができますが、これは偽is_singleがなぜ知っているではありません。安全であると表示されますか?
0x02の思考
いくつか考えた後に複数のデータを得ることができ、節が依然として脆弱性をトリガすることができる制限がない場合、このパッチは、基本的に問題を解決しない感じます。ただ、いくつかの条件がクエリの結果である場合<= 1に制限されますが、これは本当に安全であることを、言いましたか?プログラムは、これらのパラメータはスプライスに似ている場合はwhere ... wp_posts.post_name like $qv['name']
まだ問題を抱えている、と言うここでは起動しません。私はおそらく明白な場所、そのような使い方を見ていない、少し見えますが、少し機能の底部と一部が、最初にここの穴を残していないがあります。
0x03の概要
脆弱性の分析での後方思考を掘るしようとしているが、理由はSQLインジェクションの私の分析で、これらの脆弱性のデシリアライズはもっと前にこのロジック傷やいくつかの奇妙なを掘るためです。論理的な欠陥のために、私はポイントシステムによる逆推力装置は、「自然」が、実現していない論理機能モジュールのエラーが最初の理解を通じて表示され、その後、公式の差分を行うために組み合わせる必要があることをSQLインジェクション、XSSの脆弱性の分析に適していないと思いますそれは良くなります。
0x04のリファレンス
CVE-2019から17671
の影響を受けるバージョン
解析のWordpress 5.2.3不正なページビューの脆弱性(CVE-2019から17671)へ