SQL注入汇总(持续更新)

shellder 发布于 2025-10-30 66 次阅读


写这篇博客的起因是还是在虾皮的web安全工程师面试中被问到常见的数据库有哪些的时候,只说上来了mysql和Oracle,把sqlserver,sqlite啥的都忘了,就想着在补充自己不足的同时也把学过的sql注入进行一个输出汇总吧。

首先先把自己面试被绊倒的数据库例子补充一下。数据库有两大主流类别,即关系型数据库(Relational DB)非关系型数据库(NoSQL),除此之外,还有一种由两种混合而来的新型数据库(NewSQL)

关系型数据库是将数据以表格的形式存储,每张表都有行和列,而表与表之间则是通过主键和外键来进行连接的建立联系,常见的代表又MySQL,PostgreSQL,Oracle,SQL Server,MariaDB,SQLite

非关系型数据库则是值不以传统的表格的形式存储的数据库,有着设计灵活,扩展性强的特点,非常适合存储非结构化的数据,如JSON,日志等。非关系型数据库又可以分为键值型(Key-Value),这种类似于字典,一个key对应一个value,其中value的值可以是字符串,对象,二进制数据等。这种数据库的查询速度极快,其基于内存或者高效的hash算法。代表数据库有:Redis,Amazon DynamoDB,Memcached等。适合用于缓存计算结果或会话数据(如Session);再者就是文档型数据库(Document Store),其数据是以文档的形式进行的存储,尝试用如XML,JSON,BOSN等格式,每个文档都可以相当于关系数据库中的一行,但是有着灵活的结构,文档之间的结构可以不一样。代表数据库有:MongoDB,CouchDB,RavenDBM,该类型的数据库常用于内容管理系统(CMS),用户资料存储(各个用户的资料可能不一样),日志、商品、评论数据的存储;列式数据库(Column-Family Store),列式数据库是按照列来存储数据的,每个列族(Column Family)包含若干个列数据,可以独立扩展,而该类型的数据库在查询特定列的适合更加高效,减少了I/O,同时也会使用简化版查询语言如CQL,代表数据库有:Apache Cassandra,HBase,ScyllaDB,适合应用于如大模型日志存储与分析,时间序列数据,推荐系统数据等方面;图数据库(Graph Database),该类型数据用节点(Node)和边(Edge)的方式来存储数据,专门用于表示复杂关系,适合处理关联性强的数据,对于关系的查询有着很高的效率,但不适合用于批量统计,如求均值,该类型的数据库使用图查询语言如Cypher(Neo4j)或Gremlin。,代表数据库有:Neo4j,ArangoDB,JanusGraph等,应用场景有:社交网络(用于表示好友关系,关注关系等),推荐系统(商品和用户以及兴趣的关联),知识图谱,欺诈检测和网络分析(如:资金流向图)。

新型数据库(NewSQL),新型数据库是一种融合了SQL和NoSQL优点的新型数据库,保留了传统数据的事务一致性(原子性 Atomicity:事务中的操作要么全部成功,要么全部失败,如转账失败是钱会回退到原账户;一致性 Consistency:事务的执行前后数据都必须满足约束规则,如A向B转账,转账前后两人的账户总额不变;隔离性 Isolation:并发事务之间互不干扰,如一个用户进行转账时,另一个用户无法看到中间状态;持久性 Durability:事务一旦完成,数据永久保存不会丢失,如系统崩溃不会引起银行账户余额的变动;ACID)的同时,又拥有了NoSQL的分布式扩展能力,有着支持标准SQL,保证强一致性,天生分布式,可恒星扩展的特点,适合大型的,高并发的业务系统(如银行级事务系统,大型互联网平台),代表数据库有TiDB,CockroachDB,Google Spanner,OceanBase

好,数据库类型聊完了,该回到正题,首先什么是SQL注入呢?简单的说当用户的输入被包含到我们的SQL查询的时候,就有可能会发生SQL注入。而SQL注入又可以分为三种类型:内带(In-Band)盲注(Blind)外带(Out-of-Band)

内带型SQL注入是最容易利用也是最容易被检测的SQL注入类型,这里的内带指的是利用漏洞和接受结果的通信方式相同,如在某网页上发现了SQL注入漏洞,而利用该漏洞所获取到的数据也同样会回显到相同的网页。而内带型SQL注入又可以细分为:基于错误的SQL注入Error-Based SQL Injection),基于联合体的SQL注入(Union-Based SQL Injection

首先讨论基于错误的SQL注入,该类型的SQL注入最适合用来获取数据库结构,因为数据库的错误信息会回显在浏览器上,我们可以借此来枚举整个数据库。而发现该类型的SQL注入漏洞的常用方法是利用sql语句破坏原有的sql逻辑使之发生错误,常用的payload是单引号(')或引号(")。

再者就是基于联合的SQL注入(Union-Based)该类型的sql注入利用union和select语句向浏览器返回额外的查询结果,常见的payload类似于1 union select 1,2 这里要注意的一点是union查询的列数需要与原本查询的列数相同,如果不同可以使用数字进行填充,而在确定列数后,我们就可以进行数据查询回显了,如原本的sql查询是三列,我们可以利用payload如下来查询数据库名:1 union select 1,2,database() 随后会在原本显示3的位置显示数据库名。接下来有了数据库名我们需要表名,payload如下:
0 UNION SELECT 1,2,group_concat(table_name) FROM information_schema.tables WHERE table_schema = '数据库名',其中group_concat()`方法会从返回的多行数据中提取指定的列,并将其合并成一个以逗号分隔的字符串。如group_concat(table_name)会将所有的表名合为一行输出,如结果可能为:users,orders,products,admin。而information_schema是mysql内置的元数据数据库,其中保存了所有的数据库,表,列的信息,information_schema.tables意味着去查所有表的信息(如表名,所属数据库等)。在拿到表名后,下面需要知道表中的列名:0 UNION SELECT 1,2,group_concat(column_name) FROM information_schema.columns WHERE table_name = '表名'。其中information_schema.columns是MySQL 的元数据视图,保存了所有表的列信息(table_schema, table_name, column_name, ordinal_position, data_type 等。最后我们在得知了数据库名,表名,列名后,我们就可以获取最终的数据了:0 UNION SELECT 1,2,group_concat(列名,':',列名 SEPARATOR '<br>') FROM 表名。group_concat()默认用逗号分隔,但可以用 SEPARATOR 指定其他分隔符(此处想用 <br> 把每条记录换行显示为 HTML 换行)。在这里需要提一句,group_concat()只能把多行拼为一起,如果想要拼接多个参数需要使用CONCAT()或CONCAT_WS(),上述的payload在某些mysql版本会报错。而CONCAT()是mysql用来拼接多个字符串的函数,如CONCAT(str1, str2, str3, …)但如果其中又null,则会整体输出null,而CONCAT_WS()是CONCAT()的增强版,会自动忽略null。

与可以在浏览器上回显注入结果不同,盲注型sql注入几乎或完全没有反馈来确认注入的查询是否成功,而盲注型sql注入又可以分为基于bool值的(Boolean Based)基于时间的(Time-Based )。其中盲注型sql注入经常出现的一个地方是登录页面,假如登陆页面的sql查询逻辑是:select * from users where username='%username%' and password='%password%' LIMIT 1;可以使用payload:' OR 1=1;--,使得查询的逻辑转变为:select * from users where username='' and password='' OR 1=1;其中1=1永真,所以导致查询结果返回真。

写到这里容我发个疯吧,昨天参加某公司的面试,hr提前一天打电话来,第二天面,面试官全程不开摄像头,面完秒挂,属实是被kpi面了,妈的,纯浪费时间,无限的想骂人。

首先如果是基于bool值的sql注入,我们往往只能收到一个状态值,可能只有两种响应结果如true和false,我们可以根据这两种状态返回来判断我们的payload是否生效,如检测是否存在该用户户名,以及利用union联合查询时列数是否匹配,这些信息都是可以根据返回的状态值来进行一个判断,对于数据库中具体的值,我们可以使用like和通配符来进行枚举,如:admin123' UNION SELECT 1,2,3 where database() like 's%';--该payload用于判断数据库名是否以s开头,如果收到返回值为false则证明数据库名不以s开头,我们可以继续枚举其他字母,如果返回true则我们继续枚举下一个字符,admin123' UNION SELECT 1,2,3 where database() like 'sa%';--之后确定数据库名后接着确定表名,列名,数据库具体内容,最终得到我们想要结果。

再者就是基于时间的sql注入,不同于基于bool值的,基于时间的sql注入完全没有返回值,我们需要通过一些技巧判断我们的payload是否生效,如想要判断表的列数可以使用payload:admin123' UNION SELECT SLEEP(5);--如果结果的返回并没有延迟,那么证明了注入的识别,我们可以继续使用新的payload:admin123' UNION SELECT SLEEP(5),2;--如果发生五秒延迟,则证明了我们的payload注入成功,我们可以利用和bool类型相同的方式来枚举数据库名,表名,列名以及数据库中存储的值。

外带型的sql注入并不常见,它需要特定的 数据库功能/扩展应用业务逻辑支持(例如数据库能发起 HTTP/DNS/SMB/LDAP 请求或能执行系统命令)。除此之外,目标服务器还需要能从网络上访问攻击者可控的地址(DNS、HTTP、SMB 等)。很多内网/生产环境默认有出站控制,或 DNS 被限制,因而较难成功。该类型漏洞的利用过程如下:首先,攻击者向存在该漏洞的网站注入payload,然后网站发起sql查询同时payload被执行,最后payload会携带一个http请求或其他请求,使得数据库服务器向攻击者搭建的http服务发起请求。

除了这些基本的sql注入方式,还有一些高级的技巧,如二次注入(Second-order SQL injection, stored SQL injection).二次SQL注入指的是攻击者的payload在注入后被存储在数据库中,在另一sql请求时被当作sql语句的一部分而造成的sql注入。这种类型的sql注入可以绕过前端防御措施,例如基本输入验证或数据清理,这些措施仅在初始数据输入时发生。由于有效载荷在第一步不会造成干扰 ,因此很容易被受害者忽视。举例:如攻击者最开始输入的payload:'; DROP TABLE tablename; -- 可能会被使用如real_escape_string()的函数进行转义,但是在其他的sql查询中如果查询到了这条payload则会导致该payload被执行,也就是二次注入的发生。
除了二次注入,在某些情况下,http请求的user-agent头也可能会记录到log数据库中,我们可以利用这个进行注入。假设某 Web 应用程序将 HTTP 请求中的 User-Agent 插入到名为 logs 的数据库表中,并提供一个端点来查看这些日志。比如:INSERT INTO logs (user_Agent) VALUES ('$userAgent');攻击者可以通过操控 User-Agent 标头来注入 SQL 代码。例如,User-Agent: ' UNION SELECT username, password FROM user; -- 如果服务器没有对 User-Agent 进行适当过滤,注入的 SQL 代码将会被执行,攻击者可能能获取到 user 表中的用户名和密码。
还有一种是利用存储的sql注入,如果应用程序中存储了恶意 SQL 代码(比如账号的用户名),并且这个恶意代码在后续的查询、返回或渲染过程中被执行。也就是说,攻击者通过在用户注册、提交数据或其他地方将恶意 SQL 代码存储到数据库中,在后续的操作中可能会触发该代码。
某些系统会解析XML 或 JSON 数据,并在 SQL 查询中使用解析后的数据,并且未正确过滤输入 , 则可能存在注入漏洞。如:{ "username": "admin' OR '1'='1--", "password": "password" }

一些情况防御者往往会使用过滤的方式来防止sql注入的发生,故此如何规避过滤非常重要,常见的规避过滤的方法主要是对我们的payload进行编码,如url编码(URL Encoding)十六进制编码(Hexadecimal Encoding)unicode编码(Unicode Encoding)
URL 编码:URL 编码是一种常用方法,其中字符使用百分号 (%) 后跟其十六进制 ASCII 值来表示。例如,payload ' OR 1=1-- 可以编码为 %27%20OR%201%3D1--
十六进制编码:十六进制编码是另一种使用十六进制值构建 SQL 查询的有效技术。例如,查询 SELECT * FROM users WHERE name = 'admin' 可以编码为 SELECT * FROM users WHERE name = 0x61646d696e 。通过将字符表示为十六进制数,攻击者可以绕过在处理输入之前未解码这些值的过滤器。
Unicode编码:Unicode 编码使用 Unicode 转义序列来表示字符。例如,字符串 admin 可以编码为 \u0061\u0064\u006d\u0069\u006e 。此方法可以绕过仅检查特定 ASCII 字符的过滤器,因为数据库将正确处理编码后的输入。
除此之外,or可以用||来代替,&&替代and,id = 5在=被过滤的情况下也可以替换为id between 5 and 5。而在程序会对单引号和双引号进行过滤时,可以使用CONCAT()函数来构造不带引号的字符串,如CONCAT(0x61, 0x64, 0x6d, 0x69, 0x6e)可以用来构造admin。在不允许有空格的情况下,首先可以使用注释代替空格,如使用SELECT/**/*FROM/**/users/**/WHERE/**/name/**/='admin' 代替 SELECT * FROM users WHERE name = 'admin';其次,可以使用换行符(\t)制表符(\n)代替空格,如SELECT\t*\tFROM\tusers\tWHERE\tname\t=\t'admin' ;最后还有可以使用URL编码字符来代替不同的空格,如 %09 (水平制表符)、 %0A (换行符)、 %0C (换页符)、 %0D (回车符)和 %A0 (不间断空格)。这些字符可以替换有效负载中的空格。而在SQL 关键字被过滤时,通常可以通过改变其大小写或添加内联注释来分解它们来绕过,如:SElEcT * FrOm users 或 SE/**/LECT * FROM/**/users

sql注入的补救措施
首先可以使用预处理语句,参数化查询。在预编译查询中,开发人员首先编写 SQL 查询语句,然后将用户输入作为参数添加进去。编写预编译语句可以确保 SQL 代码结构保持不变,并且数据库能够区分查询语句和数据。
再者可以进行用户的输入验证,输入验证对于保护 SQL 查询语句的内容至关重要。使用允许列表可以将输入限制为仅允许某些特定字符串,或者使用编程语言中的字符串替换方法来过滤允许或禁止的字符。
除此之外,还可以用户的输入进行转义过滤,在一些sql语法中常见的字符前添加\进行转义,如单引号双引号等。

再来介绍一下NoSQL注入,这里以MongoDB为例。不同于mysql数据库,MongoDB中的数据并不存放在表中,而是存放在文档中,如:{"_id" : ObjectId("5f077332de2cdf808d26cd74"), "username" : "lphillips", "first_name" : "Logan", "last_name" : "Phillips", "age" : "65", "email" : "lphillips@example.com" }其中ObjectId 是 MongoDB 内部用于唯一标识文档的 12 字节数据类型。如果我们想要进行一个查询,如筛选出性别为男性且姓氏为 test 的文档如下:['gender' => 'male', 'last_name' => 'test'],而如果我们想检索所有年龄小于 50 岁的文档,可以使用以下筛选条件:['age' => ['$lt'=>'50']],其中$lt 是 MongoDB 的比较操作符之一,表示 "小于"(less than)。
而NoSQL注入可以分为两种类型,即语法注入Syntax Injection)和操作符注入Operator Injection)。其中语法注入类似于sql注入,如针对db.users.find({ username: userInput });攻击者输入admin' OR '1'='1 整体逻辑将会变为db.users.find({ username: 'admin' OR '1'='1' });
而操作符注入则是假设应用程序通过以下方式执行身份验证:db.users.find({ username: userInput, password: password });攻击者输入{ "$ne": null }整体的查询逻辑会变成db.users.find({ username: { "$ne": null }, password: password });查询条件 username != null 会始终为真,因此绕过了原本的身份验证。而`$nin` 操作符允许我们通过指定条件来创建过滤器,筛选出包含特定字段(而非值列表)的文档,如['username'=>['$nin'=>['admin'] ], 'password'=>['$ne'=>'aweasdf']]这条指令指示数据库返回所有用户名不是 admin 且密码不是 aweasdf 的用户。因此,我们现在获得了对另一个用户帐户的访问权限。
如果想要获取到登陆的密码,可以利用$regex操作符,用于执行 正则表达式(Regular Expression) 查询。它允许我们在 MongoDB 中查找符合特定模式的字符串数据。首先要知道密码的长度,可以使用payload:^.{7}$,其中^表示字符串的开始。.表示任意字符。{7}表示7个任意字符。$表示字符串的结束。意思是寻找一个 密码长度为7 的用户,可以进行多次尝试,最终确定密码的长度,如最终密码长度为5;随后利用正则表达式猜测密码首位字符,如:^c….$ 意思是密码以c开头,后面四个点代表任意字符,进行多次类似操作最终可以提取到完整密码。

为了防御 NoSQL 注入攻击,关键的补救措施是确保查询语句和用户输入之间不存在任何混淆。这可以通过使用参数化查询来解决,参数化查询将查询命令和用户输入分开,从而避免引擎混淆。此外,应始终使用 NoSQL 解决方案的内置函数和过滤器来避免语法注入。最后,还可以使用输入验证和清理功能来过滤并删除语法和运算符字符。

此作者没有提供个人介绍。
最后更新于 2025-11-01