免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

SQL注入如何判断数据库类型:报错、注释符与特征函数

SQL注入如何判断数据库类型:报错、注释符与特征函数 or 11 --打进去页面纹丝不动换成 or 11 #立刻蹦出一段报错——就差一个注释符的写法目标的数据库候选名单基本就被砍掉一半了。做过 SQL 注入测试的人多半都遇到过这种同一句话、不同方言的场面明明逻辑是对的payload 就是不通排查半天发现不是过滤而是数据库类型猜错了。判断数据库类型是 SQL 注入测试里性价比最高、也最容易被新手跳过的一步。它不产生任何战果看起来只是准备工作但后面每一步——从字符串拼接、注释符写法、系统表名字到报错注入用哪个函数、盲注怎么构造——全都建立在我知道对面是谁这个前提上。方向错了越努力越远。这篇内容面向三类人正在打靶场、刷 CTF 的 Web 安全入门者做授权渗透测试、需要快速定位目标技术栈的安全从业者以及想知道攻击者是怎么认出我的数据库的、准备做防御加固的开发与运维。所有演示都建立在已获授权的测试环境自建靶场、CTF 题目、明确授权的渗透项目之上未经授权的测试属于违法行为这条线不能碰。下面我把判断数据库类型的完整思路拆开讲重点是每一步为什么这么做以及踩过的坑。1. 为什么认数据库比背 Payload更值得先花十分钟1.1 同一个注入点换一个数据库就是另一套语法世界很多人学注入是从背 payload 开始的 or 11 --背得滚瓜烂熟一到实战就发现这里不对那里不对。根本原因是SQL 注入不是一门语言是五门语言。MySQL、SQL Server、Oracle、PostgreSQL、SQLite 这几家虽然都自称标准 SQL但在真正写注入语句时差异大到几乎是不同的方言。举几个最直观的例子字符串拼接MySQL 用CONCAT()Oracle、PostgreSQL、SQLite 用||SQL Server 用。注释符MySQL 支持#和--注意--后面必须跟一个空白字符其他几家用--#在 URL 里还得写成%23。系统表MySQL 查information_schemaSQL Server 有sysobjects、syscolumnsOracle 用all_tables、user_tablesPostgreSQL 走pg_catalogSQLite 只有一张sqlite_master。取版本MySQL 用version()或versionSQL Server 用versionOracle 得查v$versionPostgreSQL 用version()SQLite 用sqlite_version()。把这些列出来你就明白了如果你不知道对面是 MySQL却一直用||去拼接字符串结果永远是逻辑为假或者直接报错。你以为是目标有 WAF 在拦其实是自己在用错误的方言跟对方说话。1.2 判断顺序是有讲究的从成本最低的方式开始试实践中判断数据库类型我不建议上来就一堆函数乱炸。正确顺序应该按获取信息的成本和对目标的影响从低到高排看报错信息。这是成本最低的目标把数据库错误原样吐出来你几乎不用构造什么 payload随手一个单引号就能看到数据库品牌和版本。看运算符和注释符的反应。报错被吞掉的情况下用字符串拼接符、注释符做逻辑试探通过页面正常/异常来推断。打特征函数和系统表。前两步都拿不到确定结论再用version()、version这类特征函数配合联合查询或报错注入去按指纹。盲注场景下的适配。目标既不报错、布尔差异也不明显时才动用时间盲注用SLEEP()MySQL、WAITFOR DELAYSQL Server、pg_sleep()PostgreSQL这些睡眠函数来做最保险的确认。这个顺序的意义在于能靠报错解决的问题就不要去猜能靠逻辑判断解决的问题就不要上时间盲注。时间盲注一秒钟一个字符效率低还容易触发风控能不用就不用。1.3 乱打 Payload 的代价不只是浪费时间新手常犯的一个错误是拿一堆 payload 挨个往上怼看着努力其实在给目标的安全设备送样本。现在稍微正规一点的系统前端后面都挂着 WAF 或 RASP你连着丢五六个明显是注入特征的字符串请求可能已经被拉黑或者限流了。更隐蔽的代价是打草惊蛇有些系统会把可疑请求记录进日志并告警运维看到告警后可能立刻临时加固、切流量或者加规则。等你好不容易摸清了数据库类型注入点已经没了。所以正确的做法是把判断数据库类型当作一次性、低噪声的信息收集来做先观察再少量、精准地试探尽量在三五个请求内拿到结论而不是地毯式轰炸。注意在正式项目里任何注入试探都必须在授权范围内进行并且要提前和甲方确认好测试窗口、限流策略和回滚方案。这不是流程上的形式主义是真实项目里保命的东西。2. 报错信息最快也最容易被忽略的指纹来源2.1 各家数据库的报错模板对照在能报错的环境里判断数据库类型几乎不需要技巧因为数据库厂商自己就把品牌、版本、甚至驱动信息写在了错误信息里。下面是几家的典型报错片段记住关键特征词一眼就能认出来。数据库典型报错片段关键识别词MySQLYou have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ...MySQL server versionSQL ServerUnclosed quotation mark after the character string/Incorrect syntax nearMicrosoft OLE DB、SQL ServerOracleORA-01756: quoted string not properly terminated/ORA-00933ORA-开头的五位数错误码PostgreSQLERROR: unterminated quoted string at or near .../PG::SyntaxErrorpg_、PG::、ERROR:前缀SQLiteSQLite/JDBCDriver/unrecognized token/near ...: syntax errorSQLite这里面最有辨识度的是 Oracle 和 PostgreSQL。Oracle 的ORA-错误码体系非常独特几乎不可能和其他数据库混淆PostgreSQL 的报错则以ERROR:开头而且经常带着LINE 1:这样的上下文行号。SQL Server 的报错经常裹着一层驱动信息像Microsoft OLE DB Provider for SQL Server或者System.Data.SqlClient.SqlException看到.NET的异常栈基本就能断定是 SQL Server。MySQL 反而是最朴素的报错句式相对固定会直接告诉你MySQL server version甚至把版本号写在提示里。2.2 端口和中间件的旁证别把网上的经验当圣旨网上流传着很多端口对应数据库的说法比如 3306 是 MySQL、1433 是 SQL Server、1521 是 Oracle、5432 是 PostgreSQL。这些在内网直接连数据库的场景里确实有用但它们只能当旁证不能当结论。原因很简单真实项目里你打的是 Web 应用看到的是 80 或 443 端口数据库真实的监听端口被藏在防火墙后面。你能观察到的旁证其实是这些中间件类型X-Powered-By: PHP/7.4大概率配 MySQLASP.NET系的站点常常连 SQL ServerJava 站点常见 MySQL 或 Oracle。URL 后缀.php、.aspx、.jsp、.do配合中间件推断技术栈。报错页面风格IIS 的默认错误页、Tomcat 的 500 页面、Nginx 的 502都能反推后端环境。这些旁证的价值在于缩小范围让你优先去试某一两家的特征而不是五家一起上。但它们不能替代直接确认。我见过 PHP 站点连 PostgreSQL 的、也见过 Java 项目连 SQL Server 的光靠中间件猜出错概率并不低。2.3 报错被吞掉之后还有哪些半报错信号现实是现在大部分系统都不直接把数据库错误展示给用户了生产环境关掉了 debug、统一了错误页、或者干脆只返回一个系统繁忙。这种情况下报错信息这条路就断了。不过半报错信号还是有的值得留意HTTP 状态码变化。正常的参数请求返回 200注入一个单引号变成 500说明错误确实产生了只是被中间件包装了。响应长度变化。有些框架会返回一个固定的错误页模板长度和正常页面明显不同哪怕内容看起来人畜无害。响应时间变化。某些数据库在语法错误时会立刻返回而在执行复杂语句时会慢一点这个差异在盲注时很有用。加单引号和加双引号的差异。如果加单引号报 500、加双引号正常说明单引号确实是拼接进了 SQL 里。这是判断是否存在注入点和注入点用哪种引号包裹的关键一步。把这一步做扎实了再往下走比盲目上 payload 有效得多。确认存在报错、确认引号类型本身就是判断数据库类型的前置动作。3. 无报错场景用运算符和注释符逼供当目标把数据库错误藏得严严实实就得换思路不靠它主动说而是靠它对不同写法的反应来反推。这里面最锋利的两个工具是字符串拼接符和注释符。3.1 字符串拼接符一根测谎仪字符串拼接符之所以好用是因为各家数据库对它的处理方式差异极大而且这个差异会直接体现在返回结果里不需要任何系统表权限。先看 SQL Server 和 MySQL 的对比。在 SQL Server 里ab的结果是字符串ab因为在两个字符串之间会做拼接而在 MySQL 里ab会被当作数值运算两个非数字字符串转成数字都是 0结果是0。于是就有了一个非常干净的判断手法构造一个能回显的注入点注入ab这样的表达式回显ab→ 强烈指向SQL Server。回显0→ 强烈指向MySQL。直接报错比如无效的数字 → 指向Oracle或PostgreSQL因为这两家不允许字符串参与算术运算。再看||运算符。||在 Oracle、PostgreSQL、SQLite 里是标准的字符串拼接符a||b稳定返回ab。但在 MySQL 里||默认是逻辑或a||b会被当成布尔运算返回1或0除非服务端开启了PIPES_AS_CONCAT模式。所以再构造一次注入a||b回显ab→ 指向Oracle / PostgreSQL / SQLite。回显1或者0→ 指向MySQL且没开拼接兼容模式。把这两个试探结合起来五家数据库基本就能分辨清楚试探表达式MySQLSQL ServerOraclePostgreSQLSQLiteab0ab报错报错报错a||b1/0报错ababab这个方法最妙的地方是不需要任何系统表权限、不需要报错信息、不依赖版本纯靠表达式语义判断非常适合权限受限的注入点。3.2 注释符#、--和那个被忽略的空格注释符的差异是另一个高性价比的信号但新手最常在这里翻车——不是不知道差异而是不知道细节。MySQL支持#也支持--。注意MySQL 里的--必须后面跟一个空白字符空格、Tab 或者换行否则它不构成注释会被当成两个减号参与运算。这就是为什么很多人写 or 11 --通不过改成 or 11 --或者 or 11 #才行。SQL Server支持--不支持#。Oracle / PostgreSQL / SQLite支持--不支持#。在 URL 场景下#属于片段标识符会被浏览器和部分客户端截断所以通常要写成%23才能正确送到服务端。这一点在手工注入时特别容易吃亏你明明写了#请求到了服务端变成了空payload 后面剩下的部分被当成正常 SQL 执行结果当然是错的。判断手法很简单构造 or 11 --和 or 11 #两组 payload观察哪个能让逻辑为真而页面正常只有#生效 → MySQL。--生效、#不生效 → SQL Server / Oracle / PostgreSQL / SQLite需要结合其他信号进一步区分。3.3 布尔盲注和时间盲注的适配如果连回显都没有页面只有有数据和没数据两种状态那就得用布尔盲注。布尔盲注判断数据库类型靠的是构造一个只在特定数据库里为真的条件。举个思路利用各数据库特有的函数或者行为差异。比如length(abc)在 MySQL 里取长度是3但 Oracle 里LENGTH同样可用只是对空字符串的处理不同——Oracle 里等价于NULL。再比如substring/substr的写法差异MySQL 用substring(str, pos, len)或substrSQL Server 用substringOracle 用substr。时间盲注则是最后的手段。各家的睡眠函数MySQLSLEEP(5)或BENCHMARK(10000000, MD5(a))SQL ServerWAITFOR DELAY 0:0:5Oracledbms_pipe.receive_message((a), 5)PostgreSQLpg_sleep(5)注入 and SLEEP(5)--如果页面卡了 5 秒MySQL 基本可以确认如果没反应就换WAITFOR DELAY试 SQL Server。这种试探一次只影响一个请求噪声小、准确率高代价是每个请求都要等好几秒所以能用前面的方法判断出来就别用时间盲注。4. 特征函数与系统表给数据库按手印走到这一步说明报错看不到、运算符信号也不明确得直接上指纹函数。这一层的原则是优先用低权限就能调用的函数和表避免还没确认类型就先把权限撞死。4.1 版本函数不同数据库的不同叫法version()这个函数名字在几家里的可用性是不一样的MySQLversion()和version都行version是系统变量写法。SQL Server只能用version没有version()函数。PostgreSQLversion()可用返回一大段包含版本和编译信息的文本。Oracleversion()在 SQL 语句里通常不可用得查v$version视图或者用banner字段。SQLitesqlite_version()没有version()。所以一个很自然的试探是注入union select version()或者等价的结构看是否报未知函数。报错说找不到version函数 → 优先怀疑 SQL Server 或 SQLite。返回8.0.xx这种纯版本号 → MySQL 或 SQLite。返回一大段带PostgreSQL字样的完整描述 → PostgreSQL。报ORA-错误 → Oracle。MySQL 还有个很好用的边界datadir、hostname、basedir这些系统变量是 MySQL 独有的能用出来就基本锁定 MySQL 了。4.2 系统表名推理链上的关键一环系统表是判断数据库类型最硬的证据之一因为表名几乎不会有歧义MySQLinformation_schema.schemata存库名、information_schema.tables存表名、information_schema.columns存字段名。SQL Serverinformation_schema也有但更常被用的是sysobjects、syscolumns、master..sysdatabases。Oracleall_tables、user_tables、all_tab_columns而且 Oracle 有一个dual伪表select 1 from dual是它的标志性写法。PostgreSQLinformation_schema和pg_catalog.pg_tablescurrent_database()也很有代表性。SQLite只有一张sqlite_master所有表结构都存在里面。试探方式是构造联合查询去查这些表哪家能查出来、哪家报表不存在直接就分出来了。不过要注意权限MySQL 的information_schema对普通用户基本开放但 SQL Server 的sysobjects、Oracle 的all_tables在不同权限下可见性差异很大低权限用户可能查不到任何东西。所以查系统表报错不代表不是那个数据库也可能只是权限不够。4.3 联合查询下的字段数推断与报错注入的方言用联合查询之前必须先确定字段数这一步和数据库类型关系不大但和后续注入等价关系很大。order by N二分法或者union select 1,2,3...逐个试报错到不报错的分界点就是字段数。确定字段数之后报错注入的写法差异就体现出来了MySQLextractvalue(1, concat(0x7e, (select ...)))和updatexml(1, concat(0x7e, (select ...)), 1)靠的是 XPath 报错。版本太新或者函数被禁用时就没辙。SQL Serverconvert(int, (select ...))直接把字符串转成整数触发类型转换错误报错信息里就带着你要的数据。Oracleutl_inaddr.get_host_name()、ctxsys.drithsx.sn()、xmltype()这几个是经典的报错注入入口但很多都依赖特定权限或者组件不一定可用。PostgreSQLcast(version() as int)这种类型转换报错。这几种写法的差异本身就是身份证明。你注一个extractvalue上去MySQL 报的是 XPath 相关的错误其他数据库报的是未知函数一看就分清了。5. 靶场里跑一遍完整的判断链路5.1 前置观察先搞清楚注入点长什么样拿最常见的数字型/字符型注入点举例假设靶场地址是一个带id参数的页面。第一步不是急着注 payload是把参数值做几个变化观察页面?id1正常显示。?id1返回 500 或者空白 → 单引号被拼进了 SQL很可能是字符型注入。?id1 and 11和?id1 and 12返回不同 → 逻辑注入有效是布尔型。?id11和?id2返回一样 → 可能是数字型注入。这一步做完你至少知道了注入点的引号类型和是否回显。很多新手跳过这一步直接注特征函数结果把参数没进 SQL误判成数据库类型不对。5.2 完整流程记录假设观察结论是单引号报错、页面有回显、报错被中间件吞掉了。那么可以按下面的顺序走第一步用注释符试探方言。构造?id1--、?id1#、?id1--看哪个能恢复正常显示。假设#生效而--不生效初步锁定 MySQL 方向。第二步用字符串拼接确认。构造?id1 and abab--看是否逻辑为真如果为假说明不做字符串拼接再构造?id1 and a||bab--如果为真说明||成立但在 MySQL 里||默认是逻辑或所以这里要小心——需要用a||b的返回值来区分。更稳的做法是找一个回显位直接联合查询把表达式的结果打出来union select 1, ab, 3 union select 1, a||b, 3看回显的是ab、0还是1对照前面的表就能确定。第三步用版本函数做最终确认。把回显位换成version()或versionunion select 1, version(), 3 union select 1, version, 3能回显出版本号的基本就落定了。如果version()报未知函数、version也不行就换成sqlite_version()试试 SQLite。第四步查系统表补齐信息。确认是 MySQL 之后union select 1, group_concat(schema_name), 3 from information_schema.schemata union select 1, group_concat(table_name), 3 from information_schema.tables where table_schemadatabase() union select 1, group_concat(column_name), 3 from information_schema.columns where table_nameusers到这一步库名、表名、字段名就都出来了。这也是热词里高频搜索的MySQL 数据库如何通过 SQL 注入获取所有的数据库名的标准路径。5.3 关键词被过滤之后的变通思路靶场里经常会对union、select、information_schema这些词做关键词过滤。这时候判断数据库类型的套路不变只是要换写法大小写变形UnIoN SeLeCt很多简单过滤对大小写不敏感。内联注释/*!50000union*/ /*!50000select*/MySQL 会把注释里的内容当正常语句执行。URL 编码和二次编码%75nion、%2575nion。等价替换information_schema.tables换成information_schema.TABLES或者用sys.schema_table_statistics这类替代视图。这里有个经验判断数据库类型用的试探词其实很少version、version、#、--、||这些词被专门过滤的概率不高。也就是说即便后面查表被拦得很惨认数据库这一步通常还是能顺利完成的。先做这一步再去想怎么绕过更深的过滤顺序上更划算。提示绕过过滤的所有思路同样只在授权靶场和授权项目里使用。真实项目里如果发现目标有明确的反注入机制应把它当作测试结论记录下来而不是想办法硬闯。6. 判断数据库类型时的典型误判与排查链路6.1 被中间件静默改写的请求你以为的响应不是你以为的这是我踩过最多次的坑。你构造的 payload 明明是对的页面却给出一个模棱两可的结果排查半天发现是中间件在中间动过手脚。常见情况包括某些反向代理会对可疑字符做转义把变成%27或者直接加反斜杠某些 WAF 会拦截并返回一个 200 的假页面某些框架对参数做了统一 trim把你 payload 末尾用来补注释的空格给吃掉了。特别是--后面那个空格被 trim 掉之后--就不构成注释整个 payload 的执行结果自然和预期不同。排查方法把 payload 用工具重放逐字符确认服务端实际收到的内容或者用一个探测回显的参数比如让页面把你输入的字符串原样打出来看看服务端拿到的到底是什么。先确认服务端收到了什么再去分析数据库执行了什么顺序不能反。6.2 参数化查询造成的假成功还有一种更隐蔽的误判你以为注入生效了其实人家用了参数化查询你的输入根本没有参与 SQL 拼接。典型表现是?id1 and 12--和?id1 and 11--返回完全一样。这时候不能立刻下这是 MySQL的结论因为你的 payload 整个被当成字符串参数了等于什么都没发生。验证方法构造一个会让 SQL 结构发生变化的输入比如?id1 order by 100--如果报错说明字段数不够注入确实生效如果不报错也不变化那八成是参数化查询这个点根本不是注入点。不要在非注入点上判断数据库类型那是无源之水。6.3 编码、字符集和大小写带来的干扰最后是编码问题。MySQL 的#在 URL 里要写成%23这是常识但很多人不知道某些客户端库会二次编码或者不编码。中文、特殊符号、宽字符比如%bf%27在不同字符集下的处理结果也不同可能导致 payload 的引号被吞掉或者被拆开。排查建议手工测试时尽量用最朴素的 ASCII 字符避开中文和特殊符号URL 参数里的#、、、空格这些特殊字符先查清楚它们在当前编码环境下会变成什么。在 URL 里会被解析成空格这一点经常让ab这类试探失效——你想传的是加号服务端收到的却是空格。这种细节看文档看不出来只能靠一次次的实测积累。7. 从防守方角度怎么让数据库类型不那么容易被认出来既然判断数据库类型靠的是报错、运算符差异和特征函数那防守的思路也就很清楚了。第一统一错误处理。生产环境一定要关闭详细错误回显把数据库异常统一包装成通用错误页。这一条能直接掐断报错识库这条最快的路径。同时要注意不只是页面内容要统一HTTP 状态码和响应长度也尽量保持一致别让攻击者通过状态码差异反推。第二全面使用参数化查询。这是防 SQL 注入的根本手段。参数化之后用户的输入永远被当成数据而不是代码运算符试探、注释符试探全部失效——因为根本没有拼接这一步。第三最小权限原则。给应用连接数据库的账号只授予必要的库表权限回收information_schema之外的系统表访问权限。这样即便注入成立攻击者也读不到其他库的结构信息。对 Oracle尤其要注意收回all_tables、v$version这类视图的访问。第四限制危险函数。MySQL 的extractvalue、updatexml、load_fileSQL Server 的xp_cmdshellPostgreSQL 的pg_sleep这些函数如果没有业务需要可以在配置层面禁掉或者限制调用权限。少了这些特征函数报错注入和时间盲注的可用性会大幅下降。第五部署 WAF 并关注告警。WAF 不能百分百拦住注入但它能提高攻击成本。更重要的是WAF 的告警日志是发现有人正在试探你的第一手线索。看到大量、||、version()特征的请求就该去查一查对应的接口了。我在实际做安全加固的时候发现很多团队把精力都放在拦上却忽视了藏和限权。其实统一错误页加最小权限这两条成本极低但能砍掉攻击者大部分低成本的信息收集手段。攻击者判断数据库类型越快后续利用就越顺反过来每一层信息的收敛都会让对方的判断过程变慢、变贵、变得更容易暴露。最后分享一个我自己的小习惯每次做授权测试我会先花五分钟把目标的技术栈、错误页风格、常见接口的响应特征记在一个小本子上再动手注入。这五分钟看起来没用但它能帮我在后续所有判断里减少一半的误判——因为很多时候决定成败的不是 payload 写得多花哨而是你有没有先看清对面站的是谁。
返回列表