ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL注入原理与防御实战:从绕过技巧到参数化查询加固

SQL注入原理与防御实战:从绕过技巧到参数化查询加固 1. SQL注入原理与本质拆解1.1 从一条SQL语句说起注入到底发生在哪一步聊SQL注入之前我建议你先忘掉那些花里胡哨的Payload回到最原始的问题为什么会有这个漏洞绝大多数SQL注入根源就一句话开发者把外部输入直接拼进了SQL语句。我给你画个最简单的场景。假设一个博客网站文章详情页的URL长这样https://example.com/article.php?id1后端代码以PHP为例很可能是这样写的$id $_GET[id]; $sql SELECT title, content FROM articles WHERE id $id; $result mysqli_query($conn, $sql);看起来没问题对吧用户访问id1就显示第一篇文章id2就显示第二篇。但你想过没有——如果id这个参数的值不是开发者预期的数字呢比如用户把URL改成https://example.com/article.php?id1 AND 11拼接出来的SQL语句就变成了SELECT title, content FROM articles WHERE id 1 AND 11如果改成https://example.com/article.php?id1 AND 12SQL语句就变成SELECT title, content FROM articles WHERE id 1 AND 12两条语句的结果截然不同前者正常返回文章后者因为条件不成立什么都查不到。当你的输入能改变SQL语句的语义时注入就已经发生了。这里我可以用个生活类比SQL注入有点像你在一张已经写好的填空题答卷上不仅填了答案还在空白处写了附加条件。阅卷老师数据库看到的是完整语句自然而然就按你写的逻辑去执行了。所以核心就一句话当用户输入与SQL语句结构未分离时攻击者就能通过构造特殊输入改变SQL语句的原始意图执行非预期的数据库操作。理解了这一点后面所有的注入技巧都是围绕如何构造特殊输入展开的。1.2 注入点的三种典型分类搞清楚原理之后我们需要学会给注入点分类。分类的意义在于不同类型的注入点检测方法、利用方式和Payload构造思路完全不同。按参数类型分有三种最常见的情况。数字型注入参数直接被当作数字处理拼接时不加引号。例如$id $_GET[id]; $sql SELECT * FROM products WHERE id $id;这种注入最简单直接用AND 11、AND 12就能测试不需要考虑引号闭合问题。字符型注入参数被引号包裹例如$name $_GET[name]; $sql SELECT * FROM users WHERE username $name;这时候直接加AND 11是无效的因为拼出来的SQL是SELECT * FROM users WHERE username admin AND 11你需要先把前面的引号闭合掉比如输入admin AND 11 --拼接后SELECT * FROM users WHERE username admin AND 11 --这里的单引号闭合了前面的引号--把后面多余的引号注释掉。搜索型注入LIKE查询例如$keyword $_GET[keyword]; $sql SELECT * FROM articles WHERE title LIKE %$keyword%;这种需要同时考虑两侧的引号和百分号% AND 11 --拼接后SELECT * FROM articles WHERE title LIKE %% AND 11 --%按获取信息的方式分又有四种类型特征利用方式显错注入查询结果或错误信息直接回显到页面直接UNION查询或报错函数布尔盲注页面不回显数据只返回真/假两种状态通过页面差异逐字符猜解时间盲注真假结果页面无差异但可触发延迟用SLEEP()等函数延时判断报错注入数据库报错信息包含查询结果利用UPDATEXML等函数抛出错误初学者往往从显错注入入手因为最直观。但真实场景里盲注才是常态很多站点的错误信息都被屏蔽了就只能靠盲注一点一点挤数据。1.3 为什么二十年过去了它依然是头号公敌SQL注入从1998年被公开提出到现在二十多年过去了OWASP Top 10里它从未缺席这绝不是一个已经过时的老漏洞。原因有三层。第一人工拼接SQL的代码至今仍然大量存在。很多老旧系统、外包项目、内部管理系统代码质量参差不齐。哪怕是2024年了我依然能在众测平台看到不少生产环境的SQL注入有些还是后台管理系统的数据价值极高。第二数据库里存的东西越来越值钱。个人隐私、支付信息、订单数据随便拖一个库出来在黑产市场都是明码标价的。攻击者花半小时拖出几百万条数据收益极高。第三自动化工具的加持让攻击门槛极低。sqlmap这种工具一跑点到即用的攻击方式让很多脚本小子都能上手。你不需要真正理解SQL语句是怎么被篡改的只需要知道哪里有参数可以测就够了。这也意味着作为防守方你不得不比攻击者更懂注入的每个细节。2. 基础注入实操从最简单的例子到登录绕过2.1 五分钟上手显错注入的完整流程我建议所有初学者都用一个本地靶场做实验比如Sqli-labs或者DVWA不要在未经授权的网站上测试。下面我用一个很常规的例子把显错注入的完整流程走一遍。假设目标URL是http://192.168.1.10/sqli-labs/Less-1/?id1页面正常显示了一条用户信息。第一步永远是探测注入点方法是在参数后面加一个单引号http://192.168.1.10/sqli-labs/Less-1/?id1如果页面报错或者返回了与正常请求截然不同的页面说明单引号影响了SQL语句结构说明这里很可能存在注入。Sqli-labs这个关卡比较友好直接把拼接后的SQL语句打印出来了你就能清楚看到自己的输入是怎么拼进去的。第二步确认闭合方式。Less-1的SQL语句是SELECT * FROM users WHERE id$id LIMIT 0,1所以我们需要把输入构造成1 --或1 --注意--后面至少要有一个空格。在URL里可以写成http://192.168.1.10/sqli-labs/Less-1/?id1 --页面如果恢复到了正常显示说明闭合成功我们的输入已经跳出了引号的限制。第三步判断字段数。用ORDER BY逐次递增id1 ORDER BY 3 -- id1 ORDER BY 4 --当ORDER BY 3正常、ORDER BY 4报错时说明原查询只有3个字段。第四步确认显示位置。用UNION SELECT占位id0 UNION SELECT 1,2,3 --这里把id设为0是为了让原查询结果为空这样UNION的后续结果就能直接显示在页面上。如果页面某处出现了数字2或3那个位置就是我们可以利用的显示位。第五步获取数据。假设显示位是2和3那么id0 UNION SELECT 1,database(),version() --就能在页面上看到当前数据库名和数据库版本号。接下来查表名、查字段名、拖数据流程都是一步步扩展的。这五个步骤看起来简单但每一步都有讲究。特别是ORDER BY这个思路本质上是利用排序报错来判断字段数量比盲试UNION SELECT少很多次请求效率高得多。2.2 万能密码与登录绕过最经典的注入场景在众多SQL注入案例中登录绕过可能是最有名的一个。几乎每一个CTF题单里都有SQL注入绕过登录现实中很多管理后台的弱防护也能用这套思路打穿。先看一段典型的危险登录代码$user $_POST[username]; $pass $_POST[password]; $sql SELECT * FROM admin WHERE username $user AND password $pass; $result mysqli_query($conn, $sql); if (mysqli_num_rows($result) 0) { // 登录成功 } else { // 登录失败 }这段代码的判定逻辑是查询结果存在记录则登录成功。攻击者要做的就是让SQL语句的结果集不为空。最经典的万能密码之一用户名admin OR 11 -- 密码随机输入拼接后的SQL变成了SELECT * FROM admin WHERE username admin OR 11 -- AND password xxx注意看--把后面的密码判断整个注释掉了。而OR 11永远为真所以整个WHERE条件永远成立查询返回所有admin表中的记录登录成功。还有另一种更短小的万能密码用户名 OR 11 -- 密码随机或者利用#注释符用户名admin#拼接后SELECT * FROM admin WHERE username admin# AND password xxx#后面的所有内容都被注释掉密码判断直接消失只要能查到admin这个用户就能登录。为什么这些Payload如此简单却依然有效因为很多开发者在写登录逻辑时潜意识里觉得用户输入的用户名和密码只是数据没有想过它们会被当作SQL代码执行。你输入admin OR 11 --代码完全没做任何过滤原样拼进了SQL语句——在数据库看来这不是登录凭据是查询逻辑本身。我在写渗透测试报告时经常给开发方解释这一点千万不要相信任何用户输入前端做了校验不代表后端安全后端做了转义不代表万无一失。唯一靠谱的方案是参数化查询这个我们后面专门讲。2.3 显错注入的进阶姿势利用报错函数直接拿数据有些场景下页面不会把查询结果直接显示出来但会把数据库报错信息打印到页面上。这时候就可以用报错注入。MySQL中常用的报错函数有UPDATEXML、EXTRACTVALUE它们的原理是当XPath表达式格式错误时MySQL会把表达式内容拼进错误信息里抛出来。以UPDATEXML为例id1 AND UPDATEXML(1, CONCAT(0x7e, DATABASE()), 1) --0x7e是波浪号~的十六进制编码作用是确保拼接出来的路径包含非法字符触发报错。执行后页面上会出现类似这样的错误信息XPATH syntax error: ~security你看到什么了数据库名security被带出来了。同样的方法可以查表名id1 AND UPDATEXML(1, CONCAT(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schemadatabase() LIMIT 0,1)), 1) --逐条把表名、字段名、记录都报出来。这种注入的优势是不需要页面有回显位置只需要报错信息能显示。很多后台对UNION注入做了显示位限制却忘了屏蔽错误信息是常见的防护盲区。3. 高级过滤绕过与实战中的判断技巧3.1 遇见WAF和关键字过滤时怎么绕现实中大多数系统并不会裸奔至少会加一层过滤规则或者WAF。常见的过滤思路有关键字黑名单union、select、and、or、空格等和参数类型校验两种。但绕过思路往往万变不离其宗找一个语义等价、但不符合过滤规则的表达方式。空格被过滤用注释符替代id1/**/AND/**/11/**/--或者用括号id1AND(11)--或者用Tab、换行符等空白字符URL编码为%09、%0a等。AND、OR被过滤用符号替代AND → OR → ||写成id1 11 --一样有效。UNION、SELECT被过滤有几种思路第一种大小写变体。很多过滤规则只匹配小写输入UnIoN sElEcT就能绕过。第二种内联注释。MySQL支持一种特殊语法/*!50000SELECT*/括号内的内容会被当作SQL语句执行但字符串本身包含的不是连续的SELECT可以绕过简单关键字匹配id0 UNION /*!50000SELECT*/ 1,2,3 --第三种利用同义词或替代函数。比如用INFORMATION_SCHEMA.COLUMNS时可以把COLUMNS换成COLUMNS0之类的表达式。逗号被过滤LIMIT 0,1可以改写成LIMIT 1 OFFSET 0SUBSTR(string,1,1)可以改写成SUBSTR(string FROM 1 FOR 1)。这些都是很经典的绕过手法我没有办法穷举所有情况核心思路是先搞清楚过滤规则针对的是哪些字符再针对性地找等价表达。我自己在实战中一般先用一个请求探测过滤规则比如输入AND看是否被拦输入看是否被拦输入%0a看是否被拦一轮下来过滤规则的轮廓基本就清楚了。有一点要注意不要过度依赖绕过技巧。真实场景中很多WAF是商业产品规则库更新很快你花几个小时绕过的规则可能下一次扫描就被拦截。绕过最大的价值在于应急利用真正要解决问题还是得从代码层面根治。3.2 盲注入门页面不显示数据时怎么获取信息盲注在实战中的出场率远高于显错。原因是很多生产系统要么关闭了错误提示要么不直接回显数据库内容。但页面总能区分查询有结果和查询无结果这个差异就能用来猜数据。布尔盲注的核心思路是把要猜的信息拆成一个个条件判断通过页面返回的差异来证实每个判断的真假。比如要猜数据库名的第一个字符可以问这样的问题id1 AND SUBSTR(DATABASE(),1,1)a --如果页面正常显示说明第一个字符是a如果页面无结果说明不是。逐字符猜下去就能拼出完整数据库名。手工猜太慢了通常要借助Burp Suite的Intruder模块来自动化。比如把请求发给Intruder设置字典为字母数字和常见符号逐个替换a里的内容再根据响应长度差异判断哪些请求返回了正常页面。时间盲注更极端一点页面真假返回完全一样只能通过时间差来判断。最典型的函数是SLEEP()id1 AND IF(SUBSTR(DATABASE(),1,1)a, SLEEP(3), 0) --如果页面执行了SLEEP(3)响应时间会明显变慢3秒左右说明条件为真。用二分法逐位判断效率也不至于太低。我第一次带着学员实操时间盲注时他问过一个很有意思的问题既然盲注这么费劲为什么攻击者不用sqlmap直接跑答案很简单sqlmap在真实场景中很容易被WAF识别拦截而且对加密流量、复杂闭合条件的处理能力有限。手工盲注虽然慢但往往能深入到自动化工具到不了的地方。这也是为什么很多CTF题的高级阶段会强制你用盲注思路解题。3.3 靶场推荐与学习路径规划学习SQL注入最重要的是有合法的、可控的实验环境。我强烈建议按下面的顺序搭建自己的靶场。Sqli-labs目前最经典的SQL注入靶场一共76关从基础数字型、字符型注入开始一步步到堆叠注入、报错注入、布尔盲注、时间盲注、二次注入、宽字节注入等。每一关都会显示当前SQL语句对理解原理非常有帮助。DVWADamn Vulnerable Web Application集成了SQL注入、XSS、文件上传等多种漏洞有low、medium、high、impossible四个安全等级适合练习绕过技巧。它的phpMyAdmin界面方便你对照查询结果验证自己的Payload。Pikachu中文界面的漏洞靶场覆盖了大部分OWASP Top 10漏洞场景提供的SQL注入-字符型以及搜索型等场景很适合新手入门。建议的学习路径是先在Sqli-labs把前20关手工完整走一遍确保每一步都理解原理再做DVWA四个等级逐步升级防护测试最后去CTF平台比如BUUCTF做几道真实的注入题感受一下在限制条件下的利用思路。没有人可以在没有授权的情况下对真实网站进行SQL注入测试这是违法甚至犯罪行为。靶场设计的初衷就是让你在安全环境中充分试错。4. 从攻防两端聊聊SQL注入的真实面貌4.1 为什么代码审计能找到更深的洞很多初学者偏爱用工具扫描但我要泼一盆冷水自动化扫描能发现的往往是最浅层的SQL注入。真正有技术含量的注入需要靠代码审计去挖。原因在于工具是黑盒的它只能看到输入输出看不到代码逻辑。而代码审计是白盒的你能直接看到SQL语句是怎么拼接的有哪些输入点被忽略了。举个例子有些系统对单条SQL做了参数化处理但多个参数动态拼接时却出了岔子。比如PDO预编译确实可以防止注入但如果代码里用了-query()或者字符串拼接的方式把变量塞进去即使其他地方都用了预编译这个点依然是漏洞。还有一类隐藏很深的情况数据在进入数据库之前被存储之后又被取出拼进另一条SQL。这就是二次注入。比如用户注册时输入的用户名被存进数据库后台管理员查看某用户信息时代码把这个存储在数据库里的用户名直接拼接进了一条SQL查询——由于进入数据库之前系统没有对内容进行足够的过滤在二次拼接时注入就发生了。这样的漏洞工具往往发现不了因为工具发起请求时看到的只是一个普通的注册页面输入也不会报错。而代码审计人员可以看到存储到数据库的值和后续取用拼接的位置从而构造出攻击链。4.2 实战中的审计常见切入点在代码审计中我一般从几个线索入手快速定位SQL注入风险。一是原生SQL拼接函数。PHP里常见的mysqli_query()、PDO::query()、-exec()Java里常见的Statement.executeQuery()Python里常见的cursor.execute(sql % params)写法只要参数不是完全由内部常量组成就要认真追一下参数来源。二是外部输入入口。优先关注$_GET、$_POST、$_REQUEST、$_COOKIE、请求头、multipart/form-data中的参数。这些输入可控如果流向了拼接函数基本就是高危点。三是历史遗留代码。老项目的维护性差一个接一个的开发手写SQL也正常越是没有规范的团队越容易出问题。这里我放一个自己审计时经常检查的伪代码示例用的是Python风格def get_user_by_name(name): sql SELECT * FROM users WHERE username name cursor.execute(sql) # 这里是漏洞点发现这样的代码后我需要往前推一下name来自哪里。如果它是经过Web层校验后才传进来的还要确认校验是否可以被绕过。很多开发者的校验逻辑只检查了长度和字符集但忽略了特殊字符这种校验等于没有。4.3 开发侧最容易种下注入的三种习惯和开发者交流多了我发现糟糕的编码习惯有三个共性。第一个是为了灵活而拼接。很多开发者觉得参数化查询写起来太板不如字符串拼接方便。项目上线后需求变动频繁SQL越来越复杂拼接点越来越多最后整个查询逻辑就成了一个输入—拼接—执行的黑洞。第二个是过度信任前端和上游系统。前端限制输入框不能输入特殊字符后端就觉得安全了。实际上攻击者可以直接用Burp Suite修改请求完全绕过前端校验。内部系统的数据也不一定可信一条数据在入库时是合法的从库里取出再拼进另一条SQL时可能就会变成注入Payload。第三个是对错误信息过于友好。很多框架默认开启debug模式数据库报错会直接打印SQL语句和堆栈。这不仅是信息泄露也会帮助攻击者判断注入点位置和闭合方式。上线前务必关闭debug统一错误页。5. 从加固到防御彻底堵住SQL注入5.1 参数化查询最有效的根治法我在前面反复强调参数化查询这里展开讲透。参数化查询PreparedStatement的核心思想是让SQL语句的结构和参数数据分开传输。SQL语句先被数据库解析和编译确定结构之后参数再以纯数据的形式传入。此时无论参数里包含什么内容都只会被当成数据处理永远不会被当作SQL语句的一部分执行。以PHP的PDO为例$stmt $pdo-prepare(SELECT * FROM users WHERE username :username AND password :password); $stmt-execute([ :username $username, :password $password, ]);即使$username传的是admin OR 11 --数据库也只会把它当作username字段的一个普通值去匹配永远不会出现OR 11被逻辑执行的情况。Java的PreparedStatement同理String sql SELECT * FROM users WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password);Python的pymysql参数化写法cursor.execute(SELECT * FROM users WHERE username %s AND password %s, (username, password))注意不要把参数直接格式化进SQL语句字符串再执行那样等于没用。参数化查询的关键在于数据库驱动能够识别出参数和SQL语句的边界。实测下来参数化查询对SQL注入的防御效果是最彻底的因为它从架构上消除了输入被当作代码执行的可能性而不是依赖于过滤规则是否足够完善。5.2 纵深防御白名单、转义与最小权限虽然参数化查询能解决99%的常规注入但依然有其他层面需要加固这就是纵深防御的思路。输入验证与白名单对于确定类型的参数比如ID严格要求必须是正整数非法的直接拒绝。这种白名单校验比黑名单过滤有效得多因为白名单定义了只允许这些内容而不是试图枚举所有不允许的内容。数据库权限最小化Web应用连接数据库的账号应当只授予它执行查询所需的必要权限。如果应用只需要SELECT就不要给它INSERT、UPDATE、DELETE等高风险权限更不要用root或dba权限。这样即使注入成功攻击者能做的也极其有限。关闭错误信息生产环境将数据库错误信息统一转为友好提示不向用户暴露SQL片段、堆栈信息。这能直接增加攻击者判断闭合方式的难度。WAF作为补充WAFWeb应用防火墙可以加一层防护但要明确它是兜底不是主力而且绕WAF的手法几乎和WAF更新一样快。我在第四章里已经讲过绕过的思路防守方应该假设WAF可能会被绕过然后在这个前提下继续问自己就算攻击者过了WAF应用代码能不能扛住5.3 修复与回归验证的落地实操仅仅在代码里改了参数化查询并不代表这个漏洞就彻底修复了。我见过不少开发者改了代码也测了几个POC但漏掉了其他入口和数据流导致同样的问题在其他接口上复现。推荐一套完整的修复验证流程。第一步代码审计修复。把涉及用户输入的SQL拼接点全部替换为参数化查询同时清理掉调试残留的错误输出。第二步本地回归测试。在靶机或者测试环境重放之前的攻击Payload确认均无法注入。注意要覆盖之前做过的所有注入类型显错、布尔盲注、时间盲注、报错注入等。第三步接口全面排查。如果注入点是用户ID那么应用里所有以用户ID为参数的接口都可能存在同样问题。用同一个Payload去测试所有相关接口确认没有遗漏。第四步验证业务功能完整性。参数化之后一些原本依赖拼接的复杂查询比如动态排序、动态查询条件可能会受影响需要回归测试确保正常功能不受影响。第五步上线前的自动化扫描。可以用一些开源的漏洞扫描器快速扫一遍接口把基础测试做过再配合人工验证。在实际项目中我见过很多因为只修了当前一个点、没做全量排查而导致的漏洞复发。安全修复是个系统工程不是改一行代码就结束的事。6. 常见问题与排查技巧实录6.1 为什么我的Payload在靶场里有效换了环境就失效这是个非常典型的问题。原因通常有三个。第一数据库类型不同。MySQL、Oracle、SQL Server、PostgreSQL、SQLite它们的语法细节和注入方式差异很大。比如--注释符在MySQL后面要带一个空格但有些数据库不需要LIMIT在SQL Server里根本不能用要用TOPOracle的分页写法完全不一样。换环境之前先确认目标数据库是什么类型。第二闭合方式不同。同一个字段在A站点的SQL里可能是id$id在B站点是id$id在C站点是id$id。你的Payload必须跟着闭合方式走。判断方法是先分别尝试单引号、双引号、括号)看哪种输入会引发页面异常。第三有过滤规则或WAF拦截。很多靶场是完全没有过滤的因此Payload可以直接用。但真实环境可能有WAF你的Payload会在到达应用之前就被拦截。可以先用一个普通请求验证是否被拦截再用Burp Suite对比排查。6.2 怎么判断注入点到底存不存在这是新手问得最多的问题之一。我的建议是不要只靠一个Payload就下结论要做多步验证。以数字型参数为例你先请求?id1正常返回然后请求?id1 AND 11应该还是正常返回再请求?id1 AND 12如果页面变成空白或者无数据就说明AND 12这个假条件被SQL解析了注入大概率存在。再用ORDER BY或者时间延迟验证一下避免误报。如果三个测试都指向注入那基本可以确认了。这里要提醒一个误区不要一见到?id就默认有注入。如果后端用了安全框架、ORM、参数化查询你输入AND 11会被当成id字段的值直接查询而不会产生逻辑变化。测试的目的是区分输入被当作数据处理和输入被当作代码拼接。6.3 我的几个高频踩坑记录下面这些坑我基本都在实际项目里踩过值得记下来。坑一忘记URL编码。直接在浏览器地址栏输入没问题但输入空格、#、会被浏览器解析为特殊字符。比如URL里出现#之后的内容浏览器根本不会发到服务器。正确做法是在Burp Suite或编码工具里把Payload编码后再发送。坑二把搜索型注入当作字符型处理。搜索型的SQL语句是LIKE %$keyword%你需要先闭合%否则Payload根本不会生效。很多时候我习惯先输入看报错再输入%看能否被解析就是为了判断是否存在%包裹。坑三显错注入里使用了ORDER BY判断字段数却发现UNION SELECT的列数始终对不上。这种情况往往是因为原查询中包含了DISTINCT、GROUP BY或者多表联查字段数和你想象的不一样。还是那句话以实际报错信息为准逐个调整列数。坑四盲注脚本中用一次请求得到的响应就判断真假忽略了网络波动。最好的做法是每次请求至少发两次前后对比响应差异。如果第一次3秒、第二次0.2秒就不能轻信可能要重试确认。这个习惯在时间盲注中尤其重要。6.4 测试工具推荐与使用心得工具不在多顺手就行。我自己在SQL注入测试中最常用的组合是这三件套。Burp Suite抓包、改包、重放、Intruder自动化猜解几乎是必备工具。它最核心的使用场景是当你手工测试某个Payload时先用Burp拦截把原始请求看清楚再修改参数、发送、观察响应比浏览器地址栏高效太多了。sqlmap自动化检测和利用工具。虽然我前面说了很多手工思路但sqlmap依然是效率神器。它的命令本身也很有讲究sqlmap -u http://target.com/article.php?id1 --dbs--dbs是枚举数据库-D 数据库名 --tables是枚举表-T 表名 --columns是枚举字段-C 字段名 --dump是导数据。当注入点确认存在后用sqlmap拖数据的速度远快于手工。浏览器DevTools不要小看它。在调试前端请求、查看网络响应时非常方便。有时候我判断一个参数是否被编码处理直接看NetWork面板的请求详情就够了。最后还是要重申一句这些技术和工具请一定在合法的、有授权的环境中使用。学习安全是为了让系统变得更安全而不是相反。
返回列表