前段时间一直在刷ctfshow的SQL注入,刷了快一个星期,总算是刷完了,既然刷完了,总得有个产出吧
于是我打算写这篇文章,以便于一些刚入门CTF比赛web方向的师傅们对盲注有个了解
(这篇文章请在学会最简单的联合查询注入后再观看,效果更佳)
我学盲注真的学了好久好久,联合注入大概是2024年的11/12月就已经大概明白了
25年一整年也陆陆续续尝试学过,但是最后都放弃了(大家别学我,这种懒鬼就是拉出去打靶的典型)
盲注好像我一直都没看到一篇比较好的文章去介绍它,我是参考大佬exp然后自己悟出来的
于是我打算自己写一篇,希望刚入门的师傅们在学习盲注时不会那么困难
那么下面正式开始了,有些地方可能会有些啰嗦,还请大家耐心看完,真的就差把数据库底层给拆了
什么时候用到盲注
普通的SQL注入题目中,查询的数据是可以回显到页面上的,例如id=1时:姓名xxx;年龄:xx;性别:1;
这个时候我们可以通过union结合information_schema数据库进行联合注入,拿到flag,所有查询结果都是页面上可见的
那么有这么一种情况:查询不可见呢?
什么情况下查询不可见?
大家入门SQL注入时其实就和它遇见过
登录框
简单的考察,可能就是万能密码admin’ or 1=1#
登陆成功就有flag了
但是它也可以考难一点
考察盲注,每次提交,只告诉你登陆成功/登陆失败,除此之外没任何信息,或者再给你个报错提示(例如SQL语句没有完全闭合,它会提示"错误"之类的)
这就很头疼
分析三种情况:
登陆成功:数据库里有对应数据
登录失败:数据库没对应数据
错误:SQL语句错了
如果在不考虑错误的情况下
查询的结果只有两种(成功和失败)
既然登陆成功和登录失败是显示在页面上的
那么我们可不可以通过这个信息,进行注入呢
我们先从简单案例看起
admin’ or 1=1#
万能密码,老朋友了
它的意思很明显,满足其中一个条件就能登陆成功
为什么它万能
因为以前很多数据库是这样写的
SELECT * FROM users WHERE username = ‘admin’ AND password = ‘123456’;
查询所有用户名为admin且密码为123456的用户
如何判断登录成功呢?
如果查询有结果(返回真)
就是查询成功
如果我在username输入admin’ or 1=1#
SELECT * FROM users WHERE username = ‘admin’ or 1=1#’ AND password = ‘123456’;
#号后面全被注释了
语句变成SELECT * FROM users WHERE username = ‘admin’ or 1=1
username=admin 或 1=1
1=1一定是成立的
or的特性就是只要有一个为真就返回真
除了or之外,还有and
and的特性是:两个为真才为真
username=admin and 1=1
此时前后都成立,万能密码正常执行
但是如果变成
admin and 1=2呢?
或者说,username里面根本不存在admin这条数据呢?
可以用数据库试试
建一个数据库
-- 创建教学演示数据库
CREATE DATABASE IF NOT EXISTS sql_injection_demo;
USE sql_injection_demo;
-- =====================================================
-- 1. 用户表 - 最基本的SQL注入演示
-- =====================================================
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL, -- 明文存储密码,演示目的
email VARCHAR(100),
role VARCHAR(20) DEFAULT 'user'
);
INSERT INTO users (username, password, email, role) VALUES
('admin', 'admin123', 'admin@demo.com', 'admin'),
('john', 'password123', 'john@demo.com', 'user'),
('alice', 'alice2023', 'alice@demo.com', 'user'),
('bob', 'bobiscool', 'bob@demo.com', 'user'),
('charlie', 'charlie123', 'charlie@demo.com', 'moderator');
-- =====================================================
-- 2. 产品表 - 用于演示基于搜索框的注入
-- =====================================================
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
price DECIMAL(10,2),
category VARCHAR(50)
);
INSERT INTO products (name, description, price, category) VALUES
('笔记本电脑', '高性能笔记本电脑,适合办公和游戏', 5999.00, '电子产品'),
('智能手机', '最新款智能手机,高清屏幕', 3999.00, '电子产品'),
('办公椅', '人体工学办公椅,舒适耐用', 899.00, '家具'),
('咖啡机', '全自动意式咖啡机', 1299.00, '家电'),
('机械键盘', '青轴机械键盘,适合编程', 399.00, '电子产品'),
('显示器', '27寸4K显示器', 2499.00, '电子产品');
-- =====================================================
-- 3. 订单表 - 演示联合查询注入
-- =====================================================
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
product_id INT,
quantity INT,
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'pending',
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
INSERT INTO orders (user_id, product_id, quantity, status) VALUES
(2, 1, 1, 'completed'),
(2, 5, 2, 'completed'),
(3, 2, 1, 'pending'),
(4, 3, 1, 'completed'),
(5, 4, 1, 'shipped'),
(1, 6, 1, 'processing');
-- =====================================================
-- 4. 敏感数据表 - 演示通过注入获取敏感信息
-- =====================================================
CREATE TABLE credit_cards (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
card_number VARCHAR(20), -- 明文存储信用卡号,演示目的
expiry_date VARCHAR(10),
cvv VARCHAR(4), -- 明文存储CVV,演示目的
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO credit_cards (user_id, card_number, expiry_date, cvv) VALUES
(1, '4111111111111111', '12/25', '123'),
(2, '5555555555554444', '08/24', '456'),
(3, '378282246310005', '03/26', '789'),
(4, '6011111111111117', '11/25', '234'),
(5, '3530111333300000', '09/25', '567');
-- =====================================================
-- 5. 日志表 - 演示二次注入
-- =====================================================
CREATE TABLE logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_ip VARCHAR(45),
user_agent TEXT,
action VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO logs (user_ip, user_agent, action) VALUES
('192.168.1.100', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'login'),
('192.168.1.101', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', 'login'),
('192.168.1.102', 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)', 'purchase'),
('192.168.1.103', 'Mozilla/5.0 (Linux; Android 11)', 'search');
-- =====================================================
-- 6. 留言板表 - 演示存储型XSS+SQL注入结合
-- =====================================================
CREATE TABLE comments (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
comment TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO comments (user_id, comment) VALUES
(2, '这个网站真不错!'),
(3, '商品质量很好'),
(4, '发货速度很快'),
(5, '客服态度很好');
-- =====================================================
-- 创建一些额外的用户数据用于演示
-- =====================================================
CREATE TABLE user_profiles (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT UNIQUE,
full_name VARCHAR(100),
phone VARCHAR(20),
address TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO user_profiles (user_id, full_name, phone, address) VALUES
(1, '管理员', '13800138001', '北京市朝阳区xxx街道'),
(2, '张三', '13912345678', '上海市浦东新区xxx路'),
(3, '李四', '15812345678', '广州市天河区xxx大街'),
(4, '王五', '17712345678', '深圳市南山区xxx科技园'),
(5, '赵六', '16612345678', '杭州市西湖区xxx路');
-- 显示所有表和数据
SELECT '=== 数据库创建完成 ===' AS status;
SELECT '用户表' AS table_name, COUNT(*) AS record_count FROM users UNION
SELECT '产品表', COUNT(*) FROM products UNION
SELECT '订单表', COUNT(*) FROM orders UNION
SELECT '信用卡表', COUNT(*) FROM credit_cards UNION
SELECT '日志表', COUNT(*) FROM logs UNION
SELECT '留言表', COUNT(*) FROM comments UNION
SELECT '用户资料表', COUNT(*) FROM user_profiles;
复制粘贴就行
接下来使用users表作为演示

users表数据如上
select * from users where username=‘admin’;

能查出数据
select * from users where username=‘admin’ or 1=1#’;

or的万能密码也没问题
接下来是and

能查出admin数据

and 1=2时,查不出任何数据

or 1=2时,能查出admin
为什么?
where username=admin’ or 1=2#
成功查询的条件是什么?
要么username有admin这一条
要么1=2
1=2肯定不成立
所以直接执行前面的
where username = admin
admin’ or 1=1#能返回所有数据,是因为前后都为真(有admin这条数据,并且1=1绝对成立),所以where条件永远为真

什么是where为真?
select * from users where username=‘admin’;
username=‘admin’
这是一个等式,如果数据库真有admin数据的话,它就是真,没有就是假
有admin数据时
select * from users where username=‘admin’
数据库知道有这条数据(真),然后就把admin对应的信息返回给了用户
那为什么admin’ or 1=1#返回的是全部呢?
select * from users where username=‘admin’ or 1=1#’
这里的where有两个条件:username=admin或1=1
不管username这个限定成不成立,1=1是永远成立的

username随便输,它都可以返回全部数据
因为只返回一条数据,是由于username=admin的限制
但是我输入的是admin122222
username根本没这条数据
所以username这个条件为假,不存在
但是1=1为真
所以输出了全部数据(没有username的限制了)
我们知道,真假在计算机中可以表示为1和0
而类似username=’admin’这种等式
它们会返回真/假
也就是可以看作它们返回1和0
如果我这样构造呢?
select * from users where username=‘admin and 1=if(database()=‘CTF’,1,0)’
这是什么意思?
if(database()=‘CTF’,1,0)’
判断当前数据库名是不是叫CTF
是就返回1,不是就返回0
那语句执行后
是叫CTF,语句变成
select * from users where username=’admin and 1=1
不是叫CTF,语句变成
select * from users where username=’admin and 1=0
前一个能查出数据,后面一个查不出
在登录功能中,查出数据代表登陆成功
查不出代表登录失败
也就是如果此时的数据库名真的叫CTF
页面会返回登陆成功
这不就成功获取了数据库名吗
页面并没有直接输出,而是靠我们"问"出来的
这只是获取了数据库名啊
比赛要的是读数据,而不是简单的获取数据库名
接下来介绍一个最常用的语句
admin' and if(substr((select group_concat(f1ag) from ctfshow_fl0g),1,1)) like 'a%',1,0)#
有些没见过的函数,这里解释一下
substr:从目标字符串中截取子字符串
例如substr(‘admin’,1,1)
意思是在admin字符串中,从第一位开始,取出一个字符
简单来说就是取第一个字符(a)
再比如substr(‘admin’,2,3)
从第二位开始,取三个字符
也就是dmi
group_concat:当查询结果有多行时,将它们拼接为一行,用,隔开

没使用group_concat时
多行输出,后端处理不了(因为正常业务预期是只处理一行数据,只要查出来了就登陆成功)

使用group_concat时
这里由于不能直接group_concat(*)
我就换成password了,演示而已,一样的
这里直接单行输出了所有用户的密码
接下来是like
表示模糊匹配
搭配通配符%使用
比如database() like ‘a%’
意思是当前数据库名,是不是a开头,并且后面还有内容
database() like ‘ab%’
当前数据库名,是不是ab开头,并且后面还有内容
当like右边的值完全等于左边,它也可以起到判断相等的作用
database() like ‘abcdefg’
当前数据库名,是不是abcdefg
那like和=有什么区别吗?
=不支持%通配符
所以用like
最后是if
学过任何一门编程语言都应该接触过它(从汇编开始入门计算机的大佬当我没说)
MySQL的if语法是这样的
if(表达式,结果1,结果2)
表达式就像1=1,也就是需要判断的东西
结果1:如果表达式为真,需要执行的代码
结果2:如果表达式为假,需要执行的代码
把它们结合一下,如何构造语句呢?
admin’这里就不用说了,闭合
admin’ and 1=(构造一个等式,等号右边准备接收布尔值1或0)
admin’ and 1=if(database() like ‘%’,1,0)(这是什么意思,只有一个通配符?表示当前数据库名是不是有字符,这是一句废话,数据库名肯定有字符,所以它肯定返回真(1),如果此时页面显示成功,说明存在盲注,语句构造对了)
接下来
admin’ and 1=if(database() like ‘a%’,1,0)(数据库名第一个字母是不是a,是就返回1,不是就返回0,如果页面没提示登录成功等字眼,那就说明不是a开头,换)
admin’ and 1=if(database() like ‘b%’,1,0)(是不是b开头)
…
一直尝试到返回登陆成功
接下来判断第二位也是如此(假设数据库名是users)
admin’ and 1=if(database() like ‘ua%’,1,0)
admin’ and 1=if(database() like ‘ub%’,1,0)
…
爆到最后面
admin’ and 1=if(database() like ‘usersa%’,1,0)
users后面一位怎么爆都不对,那就说明它就只有5位了
数据库名得到为users
接下来是表名,开始不一样了
admin’ and 1=if(substr((select table_name from information_schema.tables where table_schema=database())1,1) like ‘a%’,1,0)
什么鬼?怎么变成这样了
拆开来分析
substr((select table_name from information_schema.tables where table_schema=database())1,1) like 'a%'
这是表达式,再拆
substr((select table_name from information_schema.tables where table_schema=database()),1,1)
还是有点乱,再拆
(select table_name from information_schema.tables where table_schema=database())
这里看懂了吧,联合查询的语句,获取表名的,为什么要加括号?
因为这是子查询(在题目原有的SQL语句里面又进行一次查询,所以要有括号)
substr((select table_name from information_schema.tables where table_schema=database()),1,1)
代回来,后面的1,1
表示取查询结果的第一个字符
substr((select table_name from information_schema.tables where table_schema=database())1,1) like 'a%'
回到最初
取查询结果的第一个字符,判断是不是a
接下来以此类推
判断第二位
admin' and 1=if(substr((select table_name from information_schema.tables where table_schema=database())1,2) like 'ad%',1,0)
判断第三位
admin' and 1=if(substr((select table_name from information_schema.tables where table_schema=database())1,2) like 'adm%',1,0)
有没有发现一个问题?
一直都是机械式的重复操作? 只是把substr的第二个数字和%前面的字母给换了 其他地方不变 重复执行某个操作,直到达成某个条件 在编程中,可以使用循环代替人工 先写到这吧 (吃饭去了,肚子饿…) 下一篇教大家如何编写盲注自动化脚本
评论(1)