nosql注入初探

                    <link rel="stylesheet" href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/editerView/markdown_views-f23dff6052.css">
                    <link rel="stylesheet" href="https://csdnimg.cn/release/blogv2/dist/mobile/css/edit_views_md-250d69367f.min.css">
                    <link rel="stylesheet" href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/style-e504d6a974.css">
                    <h2><a id="_0"></a>一、理论</h2> 

1、NoSQL 注入(非关系型数据库)

NoSQL 注入是一种漏洞,攻击者能够干扰应用程序对 NoSQL 数据库进行的查询。NoSQL 注入可能使攻击者能够:

  • 绕过身份验证或保护机制。
  • 提取或编辑数据。
  • 导致拒绝服务。
  • 在服务器上执行代码。

NoSQL 数据库以传统 SQL 关系表以外的格式存储和检索数据。它们使用广泛的查询语言,而不是像 SQL 这样的通用标准,并且关系约束较少。

2、NoSQL 数据库

NoSQL 数据库以传统 SQL 关系表以外的格式存储和检索数据。它们旨在处理大量非结构化或半结构化数据。因此,与 SQL 相比,它们通常具有更少的关系约束和一致性检查,并且在可伸缩性、灵活性和性能方面具有显著优势。

与 SQL 数据库一样,用户使用应用程序传递给数据库的查询与 NoSQL 数据库中的数据进行交互。但是,不同的 NoSQL 数据库使用广泛的查询语言,而不是像 SQL(结构化查询语言)这样的通用标准。这可以是自定义查询语言,也可以是 XML 或 JSON 等通用语言。

3、NoSQL 数据库模型

NoSQL 数据库种类繁多。为了检测NoSQL数据库中的漏洞,它有助于理解模型框架和语言。

一些常见的 NoSQL 数据库类型包括:

  • 文档存储 - 这些存储将数据存储在灵活的半结构化文档中。他们通常使用以下格式 JSON、BSON 和 XML,并使用 API 或查询语言进行查询。示例包括 MongoDB 和 Couchbase。
  • 键值存储 - 这些存储以键值格式存储数据。每个数据字段都与唯一的键字符串相关联。 根据唯一键检索值。示例包括 Redis 和 Amazon DynamoDB。
  • 宽列存储 - 这些存储将相关数据组织到灵活的列族中,而不是传统行。例子 包括 Apache Cassandra 和 Apache HBase。
  • 图形数据库 - 它们使用节点来存储数据实体,并使用边来存储实体之间的关系。例子 包括 Neo4j 和 Amazon Neptune。

4、NoSQL 注入的类型

有两种不同类型的 NoSQL 注入:

  • 语法注入 - 当您可以中断 NoSQL 查询语法时,就会发生这种情况,使您能够注入自己的语法 有效载荷。该方法类似于 SQL 注入中使用的方法。但是,攻击的性质各不相同 值得注意的是,由于 NoSQL 数据库使用一系列查询语言、查询语法类型和不同的数据结构。
  • 运算符注入 - 当您可以使用 NoSQL 查询运算符操作查询时,会发生这种情况。

在本主题中,我们将了解如何测试 NoSQL 漏洞,然后重点介绍如何利用 MongoDB 中的漏洞,MongoDB 是最流行的 NoSQL 数据库。我们还提供了一些实验室,以便您可以练习所学知识。

二、原理及实验

1、NoSQL 语法注入

您可以通过尝试破坏查询语法来检测 NoSQL 注入漏洞。为此,请通过提交模糊字符串和特殊字符来系统地测试每个输入,如果应用程序未充分清理或筛选这些字符串和特殊字符,则这些字符串和特殊字符会触发数据库错误或其他一些可检测的行为。

如果您知道目标数据库的 API 语言,请使用与该语言相关的特殊字符和模糊字符串。否则,请使用各种模糊字符串来面向多种 API 语言。

1. 在 MongoDB 中检测语法注入

考虑一个显示不同类别产品的购物应用程序。当用户选择碳酸饮料类别时,其浏览器会请求以下 URL:

https://insecure-website.com/product/lookup?category=fizzy

这会导致应用程序发送 JSON 查询,以从 MongoDB 数据库中的集合product中检索相关产品:

this.category == 'fizzy'

若要测试输入是否易受攻击,请在参数值category中提交模糊字符串。MongoDB 的示例字符串为:

'"`{ ;$Foo} $Foo \xYZ 

使用此模糊字符串构造以下攻击:

https://insecure-website.com/product/lookup?category='%22%60%7b%0d%0a%3b%24Foo%7d%0d%0a%24Foo%20%5cxYZ%00

如果这导致原始响应发生更改,则可能表示未正确筛选或清理用户输入。

注意

NoSQL 注入漏洞可能发生在各种上下文中,您需要相应地调整模糊字符串。否则,您可能只是触发验证错误,这意味着应用程序永远不会执行您的查询。

在此示例中,我们通过 URL 注入模糊字符串,因此该字符串是 URL 编码的。在某些应用程序中,可能需要通过 JSON 属性注入有效负载。在这种情况下,此有效负载将变为

'\"`{\r;$Foo}\n$Foo \\xYZ\u0000
① 确定要处理的字符

若要确定应用程序将哪些字符解释为语法,可以注入单个字符'。例如,您可以提交 ,这会导致以下 MongoDB 查询:

this.category == '''

如果这导致原始响应发生更改,则可能表示该字符'已破坏查询语法并导致语法错误。您可以通过在输入中提交有效的查询字符串来确认这一点,例如通过转义引号:

this.category == '\''

如果这不会导致语法错误,这可能意味着应用程序容易受到注入攻击。

② 确认条件行为

检测到漏洞后,下一步是确定是否可以使用 NoSQL 语法影响布尔条件。

要对此进行测试,请发送两个请求,一个具有 false 条件' && 0 && 'x,另一个具有 true 条件' && 1 && 'x。例如,您可以使用条件语句,如下所示:

https://insecure-website.com/product/lookup?category=fizzy'+%26%26+0+%26%26+'x``https://insecure-website.com/product/lookup?category=fizzy'+%26%26+1+%26%26+'x

如果应用程序的行为不同,则表明 false 条件会影响查询逻辑,但 true 条件不会。这表明注入这种语法风格会影响服务器端查询。

③ 覆盖现有条件

现在,您已经确定可以影响布尔条件,可以尝试替代现有条件以利用此漏洞。例如,您可以注入一个始终计算为 true 的 JavaScript 条件,例如:'||1||'

https://insecure-website.com/product/lookup?category=fizzy%27%7c%7c%31%7c%7c%27

这会导致以下 MongoDB 查询:

this.category == 'fizzy'||'1'=='1'

由于注入的条件始终为 true,因此修改后的查询将返回所有项。这使您能够查看任何类别中的所有产品,包括隐藏或未知类别。

警告

将始终计算结果为 true 的条件注入 NoSQL 查询时要小心。尽管这在注入的初始上下文中可能无害,但应用程序通常会在多个不同的查询中使用来自单个请求的数据。例如,如果应用程序在更新或删除数据时使用它,则可能导致意外数据丢失。

实验室1:检测 NoSQL 注入

此实验室的产品类别筛选器由 MongoDB NoSQL 数据库提供支持。它容易受到 NoSQL 注入的影响。

要解决实验室问题,请执行 NoSQL 注入攻击,使应用程序显示未发布的产品。

寻找注入点,每个页面点击看看,然后看看URL和数据包

在种类中可能含有,进行测试,发现报错,说明可能有,尝试进行有效的注入进行测试

在这里插入图片描述

dsda'+'			//这里注意,要对+进行URL编码处理

在这里插入图片描述

说明这里含有注入,使用布尔注入尝试,记得URL编码,后面x只是随机一个字符

dsda' && 1 && 'x	//为ture
dsda' && 0 && 'x	//为false

在这里插入图片描述

在这里插入图片描述

上面相当于SQL中的and,那么使用或呢,

dsda'||1||'		//这里就是noSQL表示或的,这里两个||,是为了闭合字符'

在这里插入图片描述

上面就是noSQL中的布尔型,常用到的字符

您还可以在类别值后添加 null 字符。MongoDB可能会忽略空字符后的所有字符。这意味着MongoDB查询上的任何其他条件都将被忽略。例如,查询可能具有其他限制:this.released

this.category == 'fizzy' && this.released == 1

该限制仅用于显示已发布的产品this.released == 1。对于未发布的产品this.released == 0

在这种情况下,攻击者可以按如下方式构造攻击:

https://insecure-website.com/product/lookup?category=fizzy'%00

这会导致以下 NoSQL 查询:

this.category == 'fizzy'\u0000' && this.released == 1

如果 MongoDB 忽略 null 字符后的所有字符,则无需将 released 字段设置为 1。因此,将显示该类别fizzy中的所有产品,包括未发布的产品。

2、NoSQL 运算符注入

NoSQL 数据库通常使用查询运算符,它提供了指定数据必须满足才能包含在查询结果中的条件的方法。MongoDB 查询运算符的示例包括:

  • $where- 匹配满足 JavaScript 表达式的文档。
  • $ne- 匹配所有不等于指定值的值。
  • $in- 匹配数组中指定的所有值。
  • $regex- 选择值与指定正则表达式匹配的文档。

您可以注入查询运算符来操作 NoSQL 查询。为此,请系统地将不同的运算符提交到一系列用户输入中,然后查看响应中是否有错误消息或其他更改。

提交查询运算符

在 JSON 消息中,您可以将查询运算符作为嵌套对象插入。例如,{"username":"wiener"}变为 {"username":{"$ne":"invalid"}}

对于基于 URL 的输入,您可以通过 URL 参数插入查询运算符。例如,username=wiener变为username[$ne]=invalid .如果这不起作用,您可以尝试以下操作:

  1. 将请求方法从 GET转换为 POST
  2. 将标题Content-Type更改为application/json
  3. 将 JSON 添加到消息正文。
  4. 在 JSON 中注入查询运算符。

注意

您可以使用内容类型转换器扩展自动转换请求方法,并将 URL 编码的请求更改为 JSON。POST

在MongoDB中检测运算符注入

考虑一个易受攻击的应用程序,该应用程序在POST请求正文中接受用户名和密码:

{"username":"wiener","password":"peter"}

使用一系列运算符测试每个输入。例如,要测试用户名输入是否处理查询运算符,可以尝试以下注入:

{"username":{"$ne":"invalid"},"password":{"peter"}}

如果应用了运算符$ne,则会查询用户名不等于 invalid的所有用户。

如果用户名和密码输入都处理运算符,则可以使用以下有效负载绕过身份验证:

{"username":{"$ne":"invalid"},"password":{"$ne":"invalid"}}

此查询返回用户名和密码均不等于invalid

的所有登录凭据。因此,您将作为集合中的第一个用户登录到应用程序。若要定位帐户,可以构建包含已知用户名或您猜到的用户名的有效负载。例如:

{"username":{"$in":["admin","administrator","superadmin"]},"password":{"$ne":""}}
实验室2:利用 NoSQL 运算符注入绕过身份验证

此实验室的登录功能由 MongoDB NoSQL 数据库提供支持。它容易受到使用 MongoDB 运算符的 NoSQL 注入

若要解决实验室问题,请以用户身份登录应用程序。administrator

您可以使用以下凭据登录自己的帐户:。wiener:peter

首先抓取登录的数据包,然后对POST请求中的JSON数据进行修改,首先对username进行测试,发现支持处理运算符

{"username":{"$ne":"dijia"}}

在这里插入图片描述

继续测试,看password支持吗

{"password":{"$ne":"dasd"}}

在这里插入图片描述

那么修改运算符,这里使用的是$ne,返回的为不是xxx的,那么改为匹配XXX的,就可能只有一个返回的。

{"username":{"$regex":"admin.*"},"password":{"$ne":""}}
如果运算符被执行,就会内部运算用户名为 adminxxxx之类的,密码不为空的,然后登录

发现返回的账号是adminxxx,并且给出了cookie,那么这里直接修改cookie即可成功

在这里插入图片描述

题目中给出的administrator并非是指该账户,可能指的是有这个权限吧,可以多使用$regex匹配。

3、利用语法注入提取数据

在许多 NoSQL 数据库中,某些查询运算符或函数可以运行有限的 JavaScript 代码,例如 MongoDB 的运算符$where和函数 mapReduce()。这意味着,如果易受攻击的应用程序使用这些运算符或函数,数据库可能会在查询过程中评估 JavaScript。因此,您可以使用 JavaScript 函数从数据库中提取数据。

在 MongoDB 中泄露数据

考虑一个易受攻击的应用程序,该应用程序允许用户查找其他已注册的用户名并显示其角色。这会触发对 URL 的请求:

https://insecure-website.com/user/lookup?username=admin

这将导致集合的以下 NoSQL 查询:users

{"$where":"this.username == 'admin'"}

当查询使用运算符$where时,您可以尝试将 JavaScript 函数注入此查询,以便它返回敏感数据。例如,您可以发送以下有效负载:

admin' && this.password[0] == 'a' || 'a'=='b

这将返回用户密码字符串的第一个字符,使您能够逐个字符提取密码。

您还可以使用 JavaScript 函数match()来提取信息。例如,使用以下有效负载可以识别密码是否包含数字:

admin' && this.password.match(/\d/) || 'a'=='b
实验室3:利用 NoSQL 注入提取数据

此实验室的用户查找功能由 MongoDB NoSQL 数据库提供支持。它容易受到 NoSQL 注入的影响。

要解决实验室问题,请提取用户的密码,然后登录到其帐户。administrator

您可以使用以下凭据登录自己的帐户:。wiener:peter

首先抓取每一步的数据包,然后查看进行分析,发现在成功登录一个账户后,有一个数据包,通过查看,发现是确定账户,然后返回账户的一些信息,并且是以用户名为查询条件的

在这里插入图片描述

测试这里是否有注入点,按照之前的步骤,加上',发现报错

在这里插入图片描述

那么再加上'+'进行测试,也就是闭合字符,这里要进行URL编码处理

在这里插入图片描述

结果不同,说明这里应该有注入,尝试使用布尔型测试

' && 1 && 'x
' && 0 && 'x
'||1||'
需要进行编码哦

当使用' && 1 && 'x,返回正常

在这里插入图片描述

当使用' && 0 && 'x返回错误,说明这里有注入

在这里插入图片描述

尝试使用||,看能否全部显示,发现当前角色确实改为administrator,但是,这里的数据包是登录后才产生的数据包,这里并没有密码的返回,所以,还是不能以administrator的身份登录成功。

在这里插入图片描述

不过这里确实进行检索,说明可能使用了$where,因为布尔型也是可以进行注入的,所以,采用猜测字符的方式,其实和sql注入的理念差不多,主要是使用的语句不同

那么尝试使用语句,记得编码,因为这里知道wiener的密码,所以长度直接测试5和6 的差别

wiener' && this.password.length<6 || 'a'='b		猜长度
wiener' && this.password[0]=='p					猜具体某位的字符

长度小于6显示正常结果,长度小于5的时候直接不显示正确结果,所以长度为5

在这里插入图片描述

测试wiener的密码第一个字符为p,返回正确

在这里插入图片描述

修改为a的话,发现直接说未找到用户,因为是&&,必须前后都为true

在这里插入图片描述

那么测试到这里,大概知道流程了,这里就可以以administrator为目标了,因为通过前面也可以发现,这里的用户,只要是数据库中有的,都可以被检索,题目给了提示为administrator,所以测试

administrator' && this.password.length<30 || 'a'=='b	
	这里的后面||是为了闭合字符,因为是或的意思,所以只要后面为false,前面才会显示想要的
administrator' && this.password[0]=='a

测试长度,如果觉得一个个测,麻烦,可以丢到intruder模块,前面知道正确是什么样的

测到8和9之间的时候,可以发现明显的变化,所以可知,长度为8

在这里插入图片描述

知道长度后,就可以构造爆破密码的阶段了

在这里插入图片描述

密码每一位的下标

在这里插入图片描述

这里因为给了提示,密码仅使用小写字母,减少工作量

在这里插入图片描述

设置数据包中不包含这个信息的,那就是正确的

在这里插入图片描述

按照顺序拼接在一起就是密码,这里很短,所以不需要导出后,写个脚本然后处理成一行。exbfeids

在这里插入图片描述

在这里插入图片描述

标识字段名称

由于 MongoDB 处理不需要固定架构的半结构化数据,因此您可能需要先标识集合中的有效字段,然后才能使用 JavaScript 注入提取数据。

例如,要确定 MongoDB 数据库是否包含字段password,您可以提交以下有效负载:

https://insecure-website.com/user/lookup?username=admin'+%26%26+this.password!%3d'

再次为现有字段和不存在的字段发送有效负载。在此示例中,您知道该字段存在,因此您可以发送以下有效负载:username

admin' && this.username!=' 
admin' && this.foo!='

如果该字段password存在,则希望响应与现有字段 (username) 的响应相同,但与不存在的字段 (foo) 的响应不同。

如果要测试不同的字段名称,可以通过使用单词列表循环访问不同的潜在字段名称来执行字典攻击。

注意

您也可以使用 NoSQL 运算符注入来逐个字符提取字段名称。这使您能够识别字段名称,而无需猜测或执行字典攻击。我们将在下一节中教你如何做到这一点。

4、利用 NoSQL 运算符注入提取数据

即使原始查询不使用任何允许您运行任意 JavaScript 的运算符,您也可以自己注入其中一个运算符。然后,可以使用布尔条件来确定应用程序是否执行通过此运算符注入的任何 JavaScript。

在 MongoDB 中注入运算符

考虑一个易受攻击的应用程序,该应用程序在请求正文中接受用户名和密码:POST

{"username":"wiener","password":"peter"}

要测试是否可以注入运算符,您可以尝试将运算符添加为附加参数,然后发送一个条件计算结果为 false 的请求和另一个计算结果为 true 的请求。例如:$where

{"username":"wiener","password":"peter", "$where":"0"}
{"username":"wiener","password":"peter", "$where":"1"}

如果响应之间存在差异,这可能表示正在计算子句中的 JavaScript 表达式。$where

提取字段名称

如果注入了使您能够运行 JavaScript 的运算符,则可以使用该方法来提取数据字段的名称。例如,您可以提交以下有效负载:keys()

Object.keys 返回一个所有元素为字符串的数组,其元素来自于从给定的object上面可直接枚举的属性。这些属性的顺序与手动遍历该对象属性时的一致。

Object.keys(obj)

参数

  • obj

    要返回其枚举自身属性的对象。

返回值

一个表示给定对象的所有可枚举属性的字符串数组。

"$where":"Object.keys(this)[0].match('^.{0}a.*')"

这将检查用户对象中的第一个数据字段,并返回字段名称的第一个字符。这使您能够逐个字符提取字段名称。

使用运算符泄露数据

或者,不允许您运行 JavaScript 的运算符来提取数据。例如,您可以使用运算符$regex逐个字符提取数据。

考虑一个易受攻击的应用程序,该应用程序在请求正文中接受用户名和密码。例如:POST

{"username":"myuser","password":"mypass"}

您可以首先测试运算符是否按如下方式处理:$regex

{"username":"admin","password":{"$regex":"^.*"}}

如果对此请求的响应与提交错误密码时收到的响应不同,则表明应用程序可能容易受到攻击。您可以使用运算符$regex逐个字符提取数据。例如,以下有效负载会检查密码是否以a开头 :

{"username":"admin","password":{"$regex":"^a*"}}
实验室4:利用 NoSQL 运算符注入提取未知字段

此实验室的用户查找功能由 MongoDB NoSQL 数据库提供支持。它容易受到 NoSQL 注入的影响。

要解决实验室问题,请以 carlos登录

寻找注入点,能点的都点了,然后查看历史记录,发现可能是注入点的是登录和忘记密码界面,其他界面都是GET请求,并且请求的是产品ID,为整数型

尝试在登录的数据包中修改,因为是json格式,所以尝试运算符是否被处理。原本的输入发现不能登录

在这里插入图片描述

以运算符修改,并进行测试,发现运算符可以被处理,并且给了一个cookie

在这里插入图片描述

修改cookie,发现直接以wiener登录了

在这里插入图片描述

那么对carlos进行尝试,发现提示账户被锁定,必须修改密码

在这里插入图片描述

这里只能到忘记密码的数据包进行分析,发现也不行,返回登录数据包,尝试添加附加参数,看能否收到JavaScript注入的影响,尝试是否可以注入运算符,附加$where,不同,说明可以

在这里插入图片描述

在这里插入图片描述

尝试提取字段名称

"$where":"Object.keys(this)[1].match('^.{}.*')"

把json数据中的$where改为上面的 ,发送到intruder模块进行爆破,设置这两个为爆破点

第一个标识字符位置编号,以及 第二个标识角色本身

相当于第一个是下标,第二个是下标对应的字符,因为

在这里插入图片描述

设置payload

在这里插入图片描述

在这里插入图片描述

上面的把username换成carlos即可,这里记录一下,当测试结果返回Account locked: please reset your password时,说明应该是对的,应为还有一种返回是Account locked: please reset your password,很明显,锁定应该为对的。记录结果为username,那么再次猜测下一个数组试试

"$where":"Object.keys(this)[2].match('^.{1}.a*')"

在这里插入图片描述

发现,这里出的结果是password

在这里插入图片描述

再测试下一个呢

"$where":"Object.keys(this)[3].match('^.{1}.a*')"

发现是email

在这里插入图片描述

再试下一个呢

"$where":"Object.keys(this)[4].match('^.{1}.a*')"

发现resetToken,这个可就有意思了,字面理解,应该是重置的时候的令牌吧

在这里插入图片描述

再试一个数组,发现没有数据了,从爆破的结果得知

"$where":"Object.keys(this)[4]"=="resettoken"

回到GET请求的忘记密码的数据包,发现后面跟着什么参数,都会到特定的界面

在这里插入图片描述

那么在这里直接使用resetToken是否可以呢,因为这里不知道值,只是测试是否带有这个参数,发现有不一样的返回,说明在这个GET请求中含有参数resetToken,也就是如果知道重置令牌的话,可以不用后面的邮箱验证了

在这里插入图片描述

我们知道resetToken是在this这里得到的,所以应该是this.resetToken

再次回到含有运算符注入的登录数据包,修改成下面的参数,再次进行爆破

"$where":"this.YOURTOKENNAME.match('^.{§1§}§a§.*')"

爆破成功,把字符拼接就是resetToken的值,也就是重置令牌d71cd3de0b067575

在这里插入图片描述

然后知道令牌,并且在GET请求的忘记密码的数据包中,有resetToken的参数,所以在那里进行操作

在这里插入图片描述

直接绕过邮箱检测的阶段,修改密码后就可以登录成功了

在这里插入图片描述

5、基于定时的注入

有时,触发数据库错误不会导致应用程序的响应出现差异。在此情况下,您或许仍能通过使用 JavaScript 注入触发条件时间延迟来检测和利用此漏洞。

要执行基于时序的 NoSQL 注入,请执行以下操作:

  1. 多次加载页面以确定基准加载时间。
  2. 在输入中插入基于时序的有效负载。基于时序的有效负载在以下情况下会导致响应中的故意延迟 执行。例如,在成功注入时导致 5000 毫秒的故意延迟。{"$where": "sleep(5000)"}
  3. 确定响应的加载速度是否较慢。这表明注射成功。

如果密码带有字母,则以下基于时间的有效负载将触发时间延迟:a

admin'+function(x){var waitTill = new Date(new Date().getTime() + 5000);while((x.password[0]==="a") && waitTill > new Date()){};}(this)+'``admin'+function(x){if(x.password[0]==="a"){sleep(5000)};}(this)+'

6、防止 NoSQL 注入

防止 NoSQL 注入攻击的适当方法取决于所使用的特定 NoSQL 技术。因此,我们建议您阅读所选 NoSQL 数据库的安全文档。也就是说,以下广泛的指导方针也会有所帮助:

  • 使用接受字符的允许列表清理和验证用户输入。
  • 使用参数化查询插入用户输入,而不是将用户输入直接连接到查询中。
  • 若要防止操作员注入,请应用接受密钥的允许列表。

三、总结:

​ 可能是非关系数据库没接触的原因,或者JavaScript不熟悉的原因,整个过程很吃力,除了前面的布尔型都可以理解,最后一个实验有点没搞懂,主要就是Object.keys()这个函数。以及为什么在GET请求这里能想到令牌重置参数可能在这里,以至于绕过邮箱检测

posted @   whitehe  阅读(34)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· PPT革命!DeepSeek+Kimi=N小时工作5分钟完成?
· What?废柴, 还在本地部署DeepSeek吗?Are you kidding?
· DeepSeek企业级部署实战指南:从服务器选型到Dify私有化落地
· 程序员转型AI:行业分析
点击右上角即可分享
微信分享提示