WordPress是使用PHP语言开发的博客平台,用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站。也可以把 WordPress当作一个内容管理系统(CMS)来使用。前些日子,RIPS放了一个WordPress5.1的CSRF漏洞通过本文将对此次CSRF漏洞进行详细分析,RCE相关的分析见后续分析文章
预备知识
在wordpress中,超级管理员是可以在评论中写入任何代码而不被过滤的
比如,在评论中输入
![154c5a104bfab52804d10e8c0523e4a8.png](https://i-blog.csdnimg.cn/blog_migrate/39d7fc4e663b985c2af4bc84b09aef1f.jpeg)
![25ffef09c63708cf59ce76c45902f8d0.png](https://i-blog.csdnimg.cn/blog_migrate/5e71664f6dda1e93f11957b1432fffb4.jpeg)
直接弹框
但是超级管理员在提交评论表单时,wordpress需要校验其Nonce值
想理解这个漏洞,首先要了解下wordpress的Nonce ( number used once )防御机制
Wordpress的Nonce ( number used once ) 机制,是用来防止CSRF而引进的。WordPress会为一些请求提供一个随机数进行校验,以防止未授权的请求的发生。
来看下wordpress的Nonce机制是如何使用的:
1、使用wp_create_nonce生成 nonce值:
![5a6bc8b84b4028c00b99d170e8874467.png](https://i-blog.csdnimg.cn/blog_migrate/c4a295454699d81f3bcb6cc48e9b1603.jpeg)
可见,其实nonce值与$i、$action、$uid、$token有关
这里的$i 是nonce创建的时间相关变量,由wp_nonce_tick()生成,其余的$action、$uid、$token很好理解。
由这里我们可以看出,nonce的生成,与其操作也是有关系的
2、将生成的 nonce传递给需要提交时验证的前端模板
3、需要验证的表单被提交后,验证其中nonce,例如下图中,本次漏洞点
![17662dad060474990c460048654f4e82.png](https://i-blog.csdnimg.cn/blog_migrate/693fde6be1e878bb9f4a6e13e7e8ef22.jpeg)
Nonce讲解完毕,言归正传,分析本次漏洞
漏洞分析
理论上,如果没法通过Nonce验证,后续的操作会直接被终止,而且在csrf攻击中,攻击者是没有办法伪造管理员实时的Nonce值。
但从本次漏洞处来看,如下图
![0700850e2293bbcbc3a1b4f6521300a8.png](https://i-blog.csdnimg.cn/blog_migrate/900ea223d9777e086ef7b287e20c7c7e.jpeg)
这里虽然没有通过Nonce的验证(wp_verify_nonce),但是并未终止操作。Wordpress在这里使用了两个过滤方法对后续的数据进行过滤。
至于为什么没有终止,而采用了如下的过滤逻辑,据说是因为WordPress其中有一些特殊的功能例如trackbacks and pingbacks会受到该值的影响,笔者没有进一步考究,感兴趣的同学可以自己分析下。
到目前为止,我们虽然没有合法的nonce值,但我们的payload仍然幸存,
接下来,看看逻辑里的 kses_init_filters()这个方法
![90c3d1345749f516f5d49d7d77449094.png](https://i-blog.csdnimg.cn/blog_migrate/8e1386168bdf8a73358e11b8923a0f5d.jpeg)
超级管理员&非法Nonce情况:
我们用超级管理员身份提交一个评论,但是改包,把&_wp_unfiltered_html_comment改为空,使其通过不了Nonce校验,如下图
![fca56ae5748a7910245df86f4bc37fd9.png](https://i-blog.csdnimg.cn/blog_migrate/7fac193efe4e06a97cda8fb3aef2e3be.jpeg)
果然进入下图断点
![21738e3afb0e4689bd3a0262744a8555.png](https://i-blog.csdnimg.cn/blog_migrate/c332ea2b83559a2cfff4933f6dca58fc.jpeg)
紧接着,进入如下断点
![e9641190b5915222754394891d527b03.png](https://i-blog.csdnimg.cn/blog_migrate/2f3e3b3e497fbaa81033fac2968ff05a.jpeg)
使用wp_filter_post_kses对输入的数据进行过滤
普通用户情况:
此时用普通用户进行评论
![37c816b6b800cb7f5a6089538a0d49fe.png](https://i-blog.csdnimg.cn/blog_migrate/463b3ba4a75f47dfa905ccffff830e8b.jpeg)
![96f7c751e2063b60ef22e247179343fb.png](https://i-blog.csdnimg.cn/blog_migrate/b75b0391a5e34a0518db2e2cabdc050b.jpeg)
直接调用wp_filter_kses进行过滤
以上思路以及明朗了
超级管理员&合法nonce ->不做任何过滤
超级管理员&不合法nonce ->wp_filter_post_kses
普通用户 –>wp_filter_kses
先来看看普通用户提交和超级管理员无nonce提交时调用的过滤函数有什么区别
普通用户提交过滤函数:
![c6080a655996839af40603ac75186b7c.png](https://i-blog.csdnimg.cn/blog_migrate/412d40dcd1875197af6386ac810b5f47.jpeg)
超级管理员无nonce提交过滤函数:
![bcfd0ecfa2632bb314e4be7a715d6343.png](https://i-blog.csdnimg.cn/blog_migrate/8ec90bc32966c8d99c5832f1530e0932.jpeg)
可以看出只是wp_kses中第二个参数不同,一个是current_filter(),一个是’post’
这里不同的,对应wp_kses中,应该是allowed_html参数值
这里举个普通用户评论的例子,普通用户提交评论,current_filter()方法返回的值是pre_comment_content,也就是说allowed_html参数值为pre_comment_content。可见下图动态调试结果
![db07a26cf011962e9aead786f74c9f3c.png](https://i-blog.csdnimg.cn/blog_migrate/3b03c51348b0bbfe67012aa8bd07fa45.jpeg)
对应的,超级管理员无nonce提交时,这里的allowed_html参数值为”post”
那么allowed_html值不同,到底会有什么区别呢?
$allowed_html被传入wp_kses_split方法
![6ecab3be406fcc152424d5fcd1fa8570.png](https://i-blog.csdnimg.cn/blog_migrate/f9cfc6af30b28194b52d3439ef8cc031.jpeg)
进一步看wp_kses_split
![629c7c165f473a1c4beac79edfe86921.png](https://i-blog.csdnimg.cn/blog_migrate/bcc63b7721b515432901a55e31e8b325.jpeg)
注意到这里$pass_allowed_html = $allowed_html;
现在$allowed_html传给了$pass_allowed_html
我们要看看这两个不同的$allowed_html最终传递到哪里被用到
跟进_wp_kses_split_callback,$allowed_html传给了wp_kses_split2
![6725714df9eba28e5cdc4a1dd7783cfe.png](https://i-blog.csdnimg.cn/blog_migrate/ee87a97a4fae2621e22a6f047187092c.jpeg)
跟进wp_kses_split2,$allowed_html被传给了wp_kses_attr
![d2c74f8316b689413713e4fc1e9487bb.png](https://i-blog.csdnimg.cn/blog_migrate/3e69c2e44432de8ef2f42a088e1bfaed.jpeg)
跟进wp_kses_attr,$allowed_html被传给了wp_kses_allowed_html
![f83b3f31f4b71ff2532c89b697064a6a.png](https://i-blog.csdnimg.cn/blog_migrate/86f8a44a72d9c14bc3bc4042e57c6c89.jpeg)
跟进wp_kses_allowed_html
一路跟踪,到了这里,$allowed_html终于有作用了
![15b8d7da26e68827882304d101d83374.png](https://i-blog.csdnimg.cn/blog_migrate/e82810609ffcde4ee59d0e9f428e0915.jpeg)
回顾一下,
超级管理员无nonce提交时,这里的allowed_html参数值为”post”
普通用户提交评论时, allowed_html参数值为”pre_comment_content”。
首先看超级管理员无nonce提交吗,allowed_html参数值为”post”,进入post分支
![4be9931991d231a1ba64af6307621d52.png](https://i-blog.csdnimg.cn/blog_migrate/332d08db82a190fccac9cc6b26a95c11.jpeg)
可以看到这里有一个wp_kses_allowed_html方法,跟进去看看
![168cde346b0fd179dfe027970c2fbae0.png](https://i-blog.csdnimg.cn/blog_migrate/8ae9f5f5f474ec9bce6c6c0042952ff9.jpeg)
相当于一个白名单机制,再看看白名单上都有什么,看看$allowedposttags
![9cbbd3254c2289954971c513861b0999.png](https://i-blog.csdnimg.cn/blog_migrate/380d7f9a54ac0bd35ad8df19b070ce1b.jpeg)
这里’a’标签运行’rel’属性
再看看普通用户提交评论时, allowed_html参数值为”pre_comment_content”情况。
![725871ea6d3a68e312c43d89add175e5.png](https://i-blog.csdnimg.cn/blog_migrate/b18b6fa6d537e94ae2626443e38bc220.jpeg)
这里白名单是$allowedtags
![0e7c3d67286940df36ace5ca182eca20.png](https://i-blog.csdnimg.cn/blog_migrate/8fabe545b9833e5c14d44521d9f2c925.jpeg)
只允许’href’与’title’
看到这里,明白wp_filter_post_kses 、wp_filter_ kses两个过滤函数有什么区别了吗?
可以用’rel’属性与不可以用’rel’,有什么区别呢?如何造成这次的csrf呢?看下图
wp-includesformatting.php
![6dff58052d51baf5c51ee65a5e596d11.png](https://i-blog.csdnimg.cn/blog_migrate/662ab9f472ae0dcec9f921eeec66480c.jpeg)
可以看到属性值在没有被转义处理的情况下就再次拼接在一起,
在a标签最终被拼接时,title的属性会被封装到双引号中,这样我们就可以构造数据使其闭合,从而执行js
Payload:
![21a46b3022f661bfa058d7872a152078.png](https://i-blog.csdnimg.cn/blog_migrate/097e2afcf8244dc9b833cb462af61db8.jpeg)
被双引号包裹后
![16c3ec03d161d159256d30dfafe88331.png](https://i-blog.csdnimg.cn/blog_migrate/96e1ac644e3402d004f9ff421bfa2a57.jpeg)
单鼠标放置时,js执行
但是这个wp_rel_nofollow_callback哪里来的呢?
看一下wordpress对comment_content都采用了哪些默认的过滤器
wp-includesdefault-filters.php
![5f00fa547f84af0cefb2b9f434ab78bf.png](https://i-blog.csdnimg.cn/blog_migrate/a0f60c293100d764ecb31d13beba58ef.jpeg)
![73e590b5cd2a1087309690668d3c1e7f.png](https://i-blog.csdnimg.cn/blog_migrate/9ea4e7257beede2a3053813fad6f1158.jpeg)
上图三个分别是:
wp_rel_nofollow
convert_invalid_entities
balanceTags
看下wp_rel_nofollow
![20b5567ab51d2931519ff0cb07bec9e2.png](https://i-blog.csdnimg.cn/blog_migrate/f4d42e525f74c4bee956f9c5c743387a.jpeg)
wp_rel_nofollow_callback是在这里被加载并使用的
结语
最后,整理下流程
此次漏洞的流程是:
(超级管理员&不合法nonce) ->(wp_filter_post_kses)->(’rel’属性在白名单中逃逸)->(wordpress加载默认评论内容过滤器wp_rel_nofollow)->(加载wp_rel_nofollow_callback) ->(未过滤并用双引号包裹title值)->(js执行)->(RCE