ASP连接SQL Server数据库防注入实战指南:如何避免常见漏洞并提升网站安全性能

ASP连接SQL Server数据库防注入实战指南:如何避免常见漏洞并提升网站安全性能

引言:SQL注入攻击的威胁与ASP应用的脆弱性

SQL注入(SQL Injection)是Web应用中最常见且最具破坏性的安全漏洞之一。根据OWASP(开放式Web应用程序安全项目)的统计,SQL注入在历年的Web应用安全风险排行榜中始终位居前列。对于使用经典ASP(Active Server Pages)技术的遗留系统而言,由于其开发年代较早,往往缺乏现代的安全防护机制,因此更容易成为SQL注入攻击的目标。

当ASP应用通过动态拼接SQL语句的方式连接SQL Server数据库时,如果不对用户输入进行严格的验证和处理,攻击者就可以通过构造恶意输入来篡改原有的SQL逻辑,从而实现未经授权的数据访问、篡改甚至删除。例如,一个简单的登录页面如果直接将用户输入的用户名和密码拼接到SQL查询中,攻击者只需输入 ' OR '1'='1 这样的字符串,就可能绕过身份验证,直接登录系统。

本文将从ASP连接SQL Server的基础知识讲起,深入分析SQL注入的原理和常见攻击模式,并提供一套完整的、实战可行的防御方案。我们将重点介绍如何使用参数化查询(Parameterized Queries)来从根本上杜绝SQL注入,同时也会涵盖输入验证、最小权限原则、错误处理等其他重要的安全实践,帮助您全面提升ASP应用的安全性能。

一、ASP连接SQL Server基础回顾

在深入探讨安全防护之前,我们首先需要回顾一下ASP连接SQL Server的常规方式。这有助于我们理解漏洞产生的根源,并为后续的防御方案提供对比。

1.1 常用的数据库连接方式

在ASP中,连接SQL Server数据库通常使用ADODB.Connection对象。以下是一个典型的连接代码示例:

<%

Dim conn, connStr

' 定义连接字符串

connStr = "Provider=SQLOLEDB;Data Source=your_server_name;Initial Catalog=your_database_name;User ID=your_username;Password=your_password;"

' 创建连接对象

Set conn = Server.CreateObject("ADODB.Connection")

' 打开连接

On Error Resume Next

conn.Open connStr

If Err.Number <> 0 Then

Response.Write "数据库连接失败:" & Err.Description

Response.End

End If

On Error GoTo 0

%>

这段代码创建了一个ADODB.Connection对象,并使用连接字符串来连接SQL Server。连接字符串中包含了服务器地址、数据库名、用户名和密码等关键信息。需要注意的是,在实际生产环境中,应将连接字符串存储在安全的位置(如配置文件),而不是直接硬编码在页面中。

1.2 执行SQL查询的常规方式

连接建立后,我们通常使用ADODB.Recordset对象或Connection.Execute方法来执行SQL查询。例如,一个根据用户ID查询用户信息的函数可能如下所示:

<%

Function GetUserById(id)

Dim sql, rs

' 动态拼接SQL语句(不安全的方式)

sql = "SELECT * FROM Users WHERE UserID = " & id

Set rs = Server.CreateObject("ADODB.Recordset")

rs.Open sql, conn, 1, 1 ' 1,1表示只读、只向前游标

Set GetUserById = rs

End Function

%>

这个函数接收一个id参数,然后将其直接拼接到SQL语句中。这种写法虽然简单直接,但正是SQL注入漏洞的典型根源。接下来,我们将详细分析这种漏洞是如何被利用的。

二、SQL注入攻击原理深度解析

2.1 SQL注入的核心原理

SQL注入的核心在于将用户输入的数据当作SQL代码执行。当应用程序使用字符串拼接的方式构造SQL语句时,如果用户输入中包含了SQL特殊字符(如单引号、分号、注释符等),这些字符就会改变原有SQL语句的结构,从而执行攻击者预期的命令。

以2.2节中的GetUserById函数为例,假设我们传入的id参数值为105,那么生成的SQL语句是:

SELECT * FROM Users WHERE UserID = 105

这是一条正常的查询语。但如果攻击者传入的id值为105 OR 1=1,那么SQL语句就变成了:

SELECT * FROM Users WHERE UserID = 105 OR 1=1

由于1=1永远为真,这条语句将返回Users表中的所有记录,而不是仅限于ID为105的用户。这就是最基础的SQL注入攻击。

2.2 常见的SQL注入攻击模式

2.2.1 绕过登录验证

假设有一个登录页面,其后端代码如下:

<%

Dim username, password, sql, rs

username = Request.Form("username")

password = Request.Form("password")

sql = "SELECT * FROM Users WHERE Username = '" & username & "' AND Password = '" & password & "'"

Set rs = Server.CreateObject("ADODB.Recordset")

rs.Open sql, conn, 1, 1

If Not rs.EOF Then

' 登录成功

Session("UserID") = rs("UserID")

Response.Redirect "welcome.asp"

Else

' 登录失败

Response.Write "用户名或密码错误"

End If

%>

攻击者可以在用户名输入框中输入:admin' --,密码可以任意输入。生成的SQL语句变为:

SELECT * FROM Users WHERE Username = 'admin' --' AND Password = 'any_password'

在SQL Server中,--是单行注释符,因此从--开始到行尾的内容都会被忽略。最终执行的语句等价于:

SELECT * FROM Users WHERE Username = 'admin'

如果存在用户名为admin的用户,攻击者无需知道密码即可登录该账户。

2.2.2 数据泄露与 UNION 攻击

如果攻击者能够控制查询条件,他们可能使用UNION语句来合并查询结果,从而获取其他表的数据。例如,假设原查询是:

SELECT Username, Email FROM Users WHERE UserID = 1

攻击者传入的id值为1 UNION SELECT Username, Password FROM Admins,则最终的SQL语句为:

SELECT Username, Email FROM Users WHERE UserID = 1 UNION SELECT Username, Password FROM Admins

这将返回两部分结果:第一部分是正常查询的用户信息,第二部分是Admins表中的用户名和密码。如果页面将查询结果全部输出,攻击者就能直接看到管理员的敏感信息。

2.2.3 数据库结构探测与命令执行

更高级的攻击者可以通过注入来探测数据库的表结构,甚至执行系统命令。例如,使用SELECT语句结合sys.tables或INFORMATION_SCHEMA.TABLES来获取数据库中的表名:

SELECT name FROM sys.tables

如果攻击者能够将这条语句注入到查询中,就能逐步了解数据库的结构,为后续的攻击做准备。

在某些配置不当的情况下,如果SQL Server启用了xp_cmdshell存储过程,攻击者甚至可以通过注入执行操作系统命令,例如:

EXEC xp_cmdshell 'dir C:\'

这将列出服务器C盘的目录,造成极大的安全风险。

三、防御SQL注入的核心策略:参数化查询

3.1 为什么参数化查询是终极解决方案

参数化查询(Parameterized Queries)是防御SQL注入的最有效方法。它的原理是将SQL语句的结构和用户输入的数据分开处理。在参数化查询中,SQL语句的模板是固定的,用户输入的数据会被当作纯粹的值来处理,而不会被解释为SQL代码。无论用户输入中包含什么特殊字符,都不会改变SQL语句的结构。

例如,对于查询SELECT * FROM Users WHERE UserID = ?,其中的?是占位符。当执行时,我们为占位符提供一个值105 OR 1=1,数据库会将这个值当作一个完整的字符串来处理,最终执行的逻辑是WHERE UserID = '105 OR 1=1',由于UserID是整数类型,这个条件永远为假,从而避免了注入攻击。

3.2 在ASP中使用参数化查询

在ASP中,我们可以使用ADODB.Command对象来实现参数化查询。ADODB.Command对象允许我们定义SQL模板,并添加参数。

3.2.1 基本语法与示例

以下是一个使用ADODB.Command进行参数化查询的完整示例:

<%

Dim cmd, param, rs, userId

' 假设从请求中获取用户ID

userId = Request.QueryString("id")

' 创建Command对象

Set cmd = Server.CreateObject("ADODB.Command")

cmd.ActiveConnection = conn ' conn是之前创建的ADODB.Connection对象

cmd.CommandText = "SELECT * FROM Users WHERE UserID = ?"

cmd.CommandType = 1 ' 1表示adCmdText,即执行SQL文本

' 添加参数

Set param = cmd.CreateParameter("@UserID", 3, 1, , userId) ' 3=adInteger, 1=adParamInput

cmd.Parameters.Append param

' 执行查询

Set rs = cmd.Execute()

If Not rs.EOF Then

Response.Write "用户名:" & rs("Username") & "
"

Response.Write "邮箱:" & rs("Email")

Else

Response.Write "未找到该用户"

End If

rs.Close

Set rs = Nothing

Set cmd = Nothing

%>

3.2.2 代码详细解析

创建Command对象:Set cmd = Server.CreateObject("ADODB.Command") 创建一个命令对象。

设置连接:cmd.ActiveConnection = conn 将命令与已有的数据库连接关联起来。

定义SQL模板:cmd.CommandText = "SELECT * FROM Users WHERE UserID = ?" 这里的?是参数占位符。

创建参数:cmd.CreateParameter("@UserID", 3, 1, , userId) 创建一个参数对象。

第一个参数"@UserID"是参数名(可选,但建议使用)。

第二个参数3是数据类型,3对应adInteger(整数)。其他常用类型包括:200(adVarChar,字符串)、135(adDBDate,日期)等。这些常量值可以在adovbs.inc文件中找到,或者直接使用数值。

第三个参数1表示参数方向,1对应adParamInput(输入参数)。其他方向包括:2(adParamOutput,输出参数)、3(adParamInputOutput,输入输出参数)、4(adParamReturnValue,返回值)。

第四个参数是参数的大小(对于字符串类型需要指定),这里省略。

第五个参数userId是实际传递给参数的值。

添加参数:cmd.Parameters.Append param 将创建的参数添加到命令的参数集合中。

执行命令:Set rs = cmd.Execute() 执行查询并返回结果集。

3.2.3 处理多个参数

如果SQL语句中有多个参数,只需重复添加参数的步骤即可。注意,参数的顺序必须与SQL模板中占位符的顺序一致。

<%

Dim cmd, param1, param2, rs, username, password

username = Request.Form("username")

password = Request.Form("password")

Set cmd = Server.CreateObject("ADODB.Command")

cmd.ActiveConnection = conn

cmd.CommandText = "SELECT * FROM Users WHERE Username = ? AND Password = ?"

cmd.CommandType = 1

' 添加第一个参数:用户名

Set param1 = cmd.CreateParameter("@Username", 200, 1, 50, username) ' 200=adVarChar, 长度50

cmd.Parameters.Append param1

' 添加第二个参数:密码

Set param2 = cmd.CreateParameter("@Password", 200, 1, 50, password) ' 200=adVarChar, 长度50

cmd.Parameters.Append param2

Set rs = cmd.Execute()

' ... 处理结果 ...

%>

3.3 存储过程与参数化查询

在ASP中调用存储过程也是一种非常安全的方式,因为它本质上也是参数化的。以下是一个调用存储过程的示例:

假设我们在SQL Server中创建了一个存储过程:

CREATE PROCEDURE GetUserById

@UserID INT

AS

BEGIN

SELECT * FROM Users WHERE UserID = @UserID

END

在ASP中调用该存储过程:

<%

Dim cmd, param, rs, userId

userId = Request.QueryString("id")

Set cmd = Server.CreateObject("ADODB.Command")

cmd.ActiveConnection = conn

cmd.CommandText = "GetUserById" ' 存储过程名称

cmd.CommandType = 4 ' 4=adCmdStoredProc,表示执行存储过程

' 添加参数

Set param = cmd.CreateParameter("@UserID", 3, 1, , userId)

cmd.Parameters.Append param

Set rs = cmd.Execute()

' ... 处理结果 ...

%>

使用存储过程的好处是:

更强的封装性:SQL逻辑在数据库端,便于维护和优化。

更高的安全性:即使前端代码存在拼接漏洞,只要存储过程内部使用参数,也能避免注入。

更好的性能:存储过程的执行计划可以被缓存和重用。

四、辅助防御措施:多层防护体系

虽然参数化查询是防御SQL注入的核心,但安全是一个系统工程,需要多层防护。以下是一些重要的辅助措施。

4.1 输入验证与过滤

即使使用了参数化查询,对用户输入进行验证仍然是必要的。这可以防止其他类型的攻击,并确保数据的合法性。

4.1.1 白名单验证

白名单验证只允许符合特定规则的输入通过。例如,如果某个输入必须是整数,我们可以验证它是否只包含数字:

<%

Function IsNumericString(str)

Dim i, char

If Len(str) = 0 Then

IsNumericString = False

Exit Function

End If

For i = 1 To Len(str)

char = Mid(str, i, 1)

If char < "0" Or char > "9" Then

IsNumericString = False

Exit Function

End If

Next

IsNumericString = True

End Function

' 使用示例

Dim userId

userId = Request.QueryString("id")

If Not IsNumericString(userId) Then

Response.Write "无效的用户ID"

Response.End

End If

' 然后使用参数化查询处理合法的userId

%>

对于更复杂的输入(如邮箱、日期),可以使用正则表达式进行验证。虽然ASP本身不支持原生正则表达式,但可以通过VBScript的RegExp对象实现:

<%

Function IsValidEmail(email)

Dim regEx

Set regEx = New RegExp

regEx.Pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"

regEx.IgnoreCase = True

IsValidEmail = regEx.Test(email)

Set regEx = Nothing

End Function

%>

4.1.2 输入净化(Input Sanitization)

虽然参数化查询可以防止注入,但有时我们仍需要对输入进行净化,例如去除HTML标签以防止XSS攻击。对于SQL Server,特别需要注意的是单引号的处理。如果由于某些原因必须使用拼接(强烈不推荐),至少应该将单引号转义为两个单引号:

<%

' 不推荐的做法,仅作为历史遗留系统的补丁

Function SafeString(str)

SafeString = Replace(str, "'", "''")

End Function

Dim userInput

userInput = "O'Brien"

Dim safeInput

safeInput = SafeString(userInput) ' 结果为 "O'Brien"

' 拼接后的SQL: SELECT * FROM Table WHERE Name = 'O'Brien'

' 这样可以防止单引号提前闭合字符串

%>

但请注意,这种方法仍然存在其他风险(如二进制注入、堆叠查询等),因此参数化查询始终是首选。

4.2 最小权限原则

数据库连接账户的权限应该被严格限制。在ASP应用中,连接数据库的账户不应该拥有系统管理员(sysadmin)权限,甚至不应该拥有对所有表的完全访问权限。

最佳实践:

为ASP应用创建一个专用的数据库用户。

仅授予该用户执行必要操作的权限。例如,如果应用只需要查询Users表,就只授予SELECT权限。

如果应用需要调用存储过程,只授予EXECUTE权限。

绝不授予DROP TABLE、SHUTDOWN等危险权限。

这样即使发生注入,攻击者能造成的破坏也会被限制在最小范围内。

4.3 错误处理与信息隐藏

默认情况下,ASP在发生数据库错误时会将详细的错误信息返回给浏览器。这些信息可能包含数据库结构、表名、字段名等,为攻击者提供线索。

不安全的错误处理:

<%

' 默认情况下,错误会直接显示

Set rs = conn.Execute("SELECT * FROM NonExistentTable")

%>

浏览器可能会显示:

Microsoft OLE DB Provider for SQL Server error '80040e37'

Object 'NonExistentTable' does not exist in database.

安全的错误处理:

<%

On Error Resume Next

Set rs = conn.Execute("SELECT * FROM NonExistentTable")

If Err.Number <> 0 Then

' 记录错误日志(记录到文件或事件查看器)

' 可以使用FileSystemObject写入日志文件

Dim fso, logFile

Set fso = Server.CreateObject("Scripting.FileSystemObject")

logFile = fso.OpenTextFile("C:\logs\error.log", 8, True)

logFile.WriteLine "[" & Now & "] Error: " & Err.Number & " - " & Err.Description

logFile.Close

Set fso = Nothing

' 向用户显示通用错误信息

Response.Write "系统繁忙,请稍后再试。"

Response.End

End If

On Error GoTo 0

%>

这样,用户只能看到友好的错误提示,而详细的错误信息被记录在服务器端,供管理员排查问题。

4.4 使用Web应用防火墙(WAF)

Web应用防火墙可以在请求到达ASP应用之前拦截恶意流量。WAF通常基于规则集来识别和阻止常见的攻击模式,包括SQL注入。例如,它可以检测请求参数中是否包含UNION、SELECT、EXEC等关键字。

虽然WAF不能替代代码层面的安全措施,但作为纵深防御的一环,它可以提供额外的保护,特别是对于无法立即修改的遗留代码。

常见的WAF产品包括:

Microsoft的URL Scan(适用于IIS)

ModSecurity(开源,可部署在Apache或IIS上)

云服务商提供的WAF(如阿里云WAF、AWS WAF)

五、实战案例:改造一个不安全的登录系统

让我们通过一个完整的案例,将一个存在SQL注入漏洞的登录系统改造为安全的系统。

5.1 改造前的不安全代码

<%

If Request.Form("action") = "login" Then

Dim username, password, sql, rs

username = Request.Form("username")

password = Request.Form("password")

' 极其危险的拼接方式

sql = "SELECT UserID, Username FROM Users WHERE Username = '" & username & "' AND Password = '" & password & "'"

Set rs = Server.CreateObject("ADODB.Recordset")

rs.Open sql, conn, 1, 1

If Not rs.EOF Then

Session("UserID") = rs("UserID")

Session("Username") = rs("Username")

Response.Redirect "dashboard.asp"

Else

Response.Write ""

End If

rs.Close

Set rs = Nothing

End If

%>

登录

用户名:

密码:

5.2 改造后的安全代码

<%

' 包含数据库连接文件

%>

404 Not Found

404 Not Found


nginx

<%

' 定义输入验证函数

Function IsValidInput(str, inputType)

Dim regEx

Set regEx = New RegExp

Select Case inputType

Case "username"

' 用户名只允许字母、数字、下划线,长度3-20

regEx.Pattern = "^[a-zA-Z0-9_]{3,20}$"

Case "password"

' 密码至少包含字母和数字,长度6-20

regEx.Pattern = "^(?=.*[a-zA-Z])(?=.*[0-9]).{6,20}$"

Case Else

IsValidInput = False

Exit Function

End Select

regEx.IgnoreCase = True

IsValidInput = regEx.Test(str)

Set regEx = Nothing

End Function

If Request.Form("action") = "login" Then

Dim username, password, cmd, paramUser, paramPass, rs

username = Trim(Request.Form("username"))

password = Trim(Request.Form("password"))

' 1. 输入验证

If Not IsValidInput(username, "username") Then

Response.Write ""

Response.End

End If

If Not IsValidInput(password, "password") Then

Response.Write ""

Response.End

End If

' 2. 使用参数化查询

Set cmd = Server.CreateObject("ADODB.Command")

cmd.ActiveConnection = conn

' 注意:密码在数据库中应该是加密存储的,这里为了演示直接比较

' 实际应用中应该存储哈希值,并使用参数化查询比较哈希

cmd.CommandText = "SELECT UserID, Username FROM Users WHERE Username = ? AND Password = ?"

cmd.CommandType = 1

' 添加用户名参数

Set paramUser = cmd.CreateParameter("@Username", 200, 1, 50, username)

cmd.Parameters.Append paramUser

' 添加密码参数(实际应用中应传递加密后的哈希值)

Set paramPass = cmd.CreateParameter("@Password", 200, 1, 50, password)

cmd.Parameters.Append paramPass

' 3. 执行查询

On Error Resume Next

Set rs = cmd.Execute()

If Err.Number <> 0 Then

' 错误处理

Response.Write ""

' 记录错误日志...

Response.End

End If

On Error GoTo 0

' 4. 验证结果

If Not rs.EOF Then

Session("UserID") = rs("UserID")

Session("Username") = rs("Username")

' 登录成功,记录日志

Response.Redirect "dashboard.asp"

Else

Response.Write ""

End If

rs.Close

Set rs = Nothing

Set cmd = Nothing

End If

%>

安全登录

用户名(3-20位字母数字):

密码(6-20位,需包含字母和数字):

5.3 改造要点总结

输入验证:使用正则表达式对用户名和密码进行严格的格式验证,确保输入符合预期。

参数化查询:使用ADODB.Command对象和参数占位符,彻底杜绝SQL注入可能。

错误处理:添加On Error Resume Next和错误检查,防止错误信息泄露。

前端验证:HTML5的pattern属性提供了客户端的即时反馈,但后端验证是必不可少的。

密码安全:虽然示例中直接比较明文密码,但实际应用中必须使用哈希算法(如bcrypt、PBKDF2)对密码进行单向加密存储,并在比较时对用户输入的密码进行同样的哈希处理。

六、高级主题:应对复杂场景与遗留系统

6.1 动态表名和列名的处理

参数化查询的一个限制是不能将表名或列名作为参数传递。例如,以下代码是无效的:

' 错误示例

cmd.CommandText = "SELECT * FROM ? WHERE UserID = ?"

cmd.Parameters.Append cmd.CreateParameter("@Table", 200, 1, 50, "Users")

如果应用需要动态指定表名或列名,必须采用白名单验证的方式。

<%

Function IsValidTableName(tableName)

Dim allowedTables

allowedTables = Array("Users", "Products", "Orders")

Dim i

For i = 0 To UBound(allowedTables)

If tableName = allowedTables(i) Then

IsValidTableName = True

Exit Function

End If

Next

IsValidTableName = False

End Function

Dim tableName

tableName = Request.QueryString("table")

If Not IsValidTableName(tableName) Then

Response.Write "无效的表名"

Response.End

End If

' 使用白名单验证后,可以安全地拼接表名

Dim cmd, sql

sql = "SELECT * FROM " & tableName & " WHERE UserID = ?"

Set cmd = Server.CreateObject("ADODB.Command")

cmd.ActiveConnection = conn

cmd.CommandText = sql

' ... 添加参数 ...

%>

6.2 应对堆叠查询(Stacked Queries)

堆叠查询允许在一次执行中运行多条SQL语句,例如SELECT * FROM Users; DELETE FROM Users。参数化查询可以防止堆叠查询攻击,因为数据库驱动会将整个字符串视为一个参数值,而不是多条语句。

但在某些旧版本的SQL Server或特定配置下,如果使用Connection.Execute直接执行拼接的字符串,堆叠查询可能生效。因此,除了使用参数化查询,还应确保:

数据库连接字符串中不包含不必要的权限。

如果可能,禁用存储过程中不必要的权限。

6.3 遗留系统的渐进式改造

对于无法一次性重写的大型遗留系统,可以采用渐进式改造策略:

审计与识别:使用工具(如SQLMap)或代码审查,识别出所有存在SQL注入风险的点。

优先处理高风险点:优先修复登录、支付、管理员操作等高风险功能。

封装数据库访问层:创建一个统一的数据库访问函数库,强制所有新的数据库操作使用参数化查询。

逐步替换:在维护过程中,逐步将旧的拼接式查询替换为参数化查询。

部署WAF:在改造期间,部署WAF作为临时保护措施。

七、总结与最佳实践清单

7.1 核心防御原则

永远不要信任用户输入:所有用户输入都是潜在的威胁。

使用参数化查询:这是防御SQL注入的黄金标准。

最小权限:数据库账户只拥有完成任务所需的最小权限。

错误处理:隐藏详细的错误信息,记录日志供内部排查。

纵深防御:结合输入验证、参数化查询、权限控制和WAF,构建多层防护。

7.2 代码审查清单

在审查ASP代码时,检查以下项目:

[ ] 所有SQL查询是否都使用了参数化查询(ADODB.Command)?

[ ] 是否还有直接拼接用户输入到SQL语句的代码?

[ ] 数据库连接账户是否具有不必要的权限?

[ ] 错误处理是否完善,是否会泄露敏感信息?

[ ] 用户输入是否经过了适当的验证和过滤?

[ ] 是否使用了存储过程来封装复杂的业务逻辑?

7.3 持续的安全意识

安全不是一次性的任务,而是一个持续的过程。随着技术的发展,新的攻击手段不断出现。因此,建议:

定期进行安全审计和渗透测试。

关注安全社区的最新动态,及时应用安全补丁。

对开发团队进行安全培训,提高整体的安全意识。

通过遵循本文提供的指南和最佳实践,您可以显著提升ASP应用的安全性,有效防御SQL注入攻击,保护您的数据和业务免受侵害。即使在现代Web开发技术盛行的今天,维护好这些遗留的ASP系统仍然具有重要的现实意义。

相关推荐

新科技巡礼:2017十大前沿科技产品有哪些?
日博365怎么样

新科技巡礼:2017十大前沿科技产品有哪些?

📅 07-13 👁️ 9635
杨延昭:夜空中最亮的星
365365bet

杨延昭:夜空中最亮的星

📅 01-04 👁️ 3727
如何使用WxPusher向个人微信推送发送实时消息,比如定时任务等
DNF魔岩石速刷方法
365比分官网

DNF魔岩石速刷方法

📅 07-07 👁️ 6580
口袋妖怪复刻开服规律揭秘
日博365怎么样

口袋妖怪复刻开服规律揭秘

📅 08-07 👁️ 9653
國立臺南大學
365比分官网

國立臺南大學

📅 01-01 👁️ 4245
猪皮冻的毛吃了对身体有害吗
日博365怎么样

猪皮冻的毛吃了对身体有害吗

📅 01-15 👁️ 5931
哪些行业需要做网站
365比分官网

哪些行业需要做网站

📅 01-05 👁️ 1285
云手机是什么?华为云手机硬核专利曝光!科创人工智能ETF华宝(589520)盘中涨近1.4%