强网杯2020部分WriteUp

MISC——签到
打开题目即可看到flag
强网先锋——web辅助
打开题目,下载附件源码,共有 4 个文件,其中 index.php
与 play.php
,是序列化与反序列化的执行文件,class.php
是类文件,common.php
是工具文件。
在index.php
中,接收两个参数并生成对象,最后将对象信息经过处理后存进文件中:
在play.php
中,读取文件内容,并经过检查和处理后,反序列化成对象:
在class.php
中,存在几个类的定义,具体分析如下:
jungle类:读取
flag
的类- KS():执行系统命令读取了
flag
- __toString():调用了 KS()。因为 __toString() 的特性,当该类的对象被当字符串进行处理时,都会调用 __toString() ,例如
echo new jungle()
,当然除了echo
,还有其他方法也能触发调用 __toString(),如strlen()
、strstr()
等等
jungle 类是一个关键类,关乎到是否能读取
flag
,在这个类中,想要调用 KS() 读取flag
,就得利用 __toString() 的特性,让 jungle类对象 被当作字符串处理,即可读取flag
- KS():执行系统命令读取了
midsolo类:触发 jungle类__toString() 的类
- Gank():将 $this->name 进行了 stristr() 判断
- __invoke():调用了 Gank()
- __wakeup():给 $this->name 赋值为字符串
在 midsolo 类中,关键的地方在于 Gank() 方法中的 stristr() 判断。根据上面 jungle 类的分析,如果这里 $this->name 为 jungle类对象 时,那么就会 调用jungle类的__toString(),从而调用 KS() 读取到
flag
而在 __invoke() 方法,调用了 Gank(),也就是说,当 midsolo的类对象被当成方法调用 时,即可调用 Gank(),如下:
1 2 3 4
<?php $obj = new midsolo(); $obj(); ?>
最后 __wakeup() 中,给 $this->name 赋了值,如果 想让$this->name为jungle类对象的话,就得绕过__wakeup()
topsolo类:调用midsolo类对象 的类
- TP():判断 $this->name 是否为 function 或 object ,并且去执行它
- __destruct():析构方法,会调用 TP()
在 topsolo 类中,关键点就在于 TP(),会去执行 $this->name(),根据上面 midsolo 类的分析,如果 $this->name赋值为 midsolo 类对象,那么就可调用 midsolo 类的 __invoke() 与 Gank()
- player类:可以实际控制的类 player 为入口类,两个成员变量可控制,可以将成员变量赋值为 topsolo 类对象,这样就能依次调用上面的类方法了
根据上面的分析,其实已经可以看出点端倪了,可以根据上面的类来构造一个POP链
,获取flag
,如下:
|
|
最后的common.php
,存在三个方法:read()、write()、check()
其中 write() 在序列化的时候调用,将0x00*0x00
替换成\0*\0
,而 read() 则相反,在反序列化的时候调用,将\0*\0
替换成0x00*0x00
,而 check() 也是在反序列化的时候调用,检查反序列化的数据是否存在name
字段。其实看到这里,已经可以发现 write() 与 read() 存在 反序列化逃逸 的漏洞,这里我们先简单学习下 反序列化逃逸 的知识点。
首先,PHP 在反序列化时,会根据变量的类型来生成变量,如果是字符串类型,还会根据长度来生成字符串。而如果长度值超出了后面的定义的字符串的话,PHP 会继续向后取内容,直到取满相同长度的字符。这样说的可能有点抽象,举个例子演示一下:
|
|
执行结果如下:
我们最初定义name = 'xiaoming'
,age = 18
,在经过序列化并对序列化数据进行了改变,再次反序列化回来的时候值都被改变了,特别是age
,连类型都改变了,这是为什么呢?
在输出的第三行中,可以看到s:8
被改成了s:26
。这是什么意思呢?这代表name
成员是一个字符串类型,并且字符串的长度是 26。
当 PHP 收到修改后的数据后,读取到 26 时,就真的会以 26 的长度去读取内容赋值给 name
成员,所以说最后name
变成了xiaoming";s:3:"age";i:18;}
,将之前的age
的定义当成了name
的值。而读取了name
的内容之后,PHP 会继续反序列化,读取到我们自己构造的数据,于是将age
赋值成了xiaoming
。
再次回过头来看 write() 与 read(),它们的内容中都有\0*\0
与0x00*0x00
,这两段字符串的区别是长度不同,\0*\0
长度为 5,而0x00*0x00
长度为 3,可用如下代码做个实验:
|
|
访问?a=\0*\0&b=%00*%00
得到结果如下:
而read()
将\0*\0
替换成0x00*0x00
,那么替换后的字符串会比原字符串少2个字符,于是产生了漏洞。这里为了说得清楚一些,用player
来做个实验:
|
|
访问?a=\0*\0&b=123
,如下:
在序列化时,user
参数的值为\0*\0
,长度为5,而经过write()
方法后,user
参数的值和长度未发生任何变化,因为值中不包含0x00*0x00
,所以不会进行替换。
而在经过read()
后,因为user
参数的值中包含\0*\0
,于是值被替换成了0x00*0x00
,但长度并没有改变,依然还是5,PHP 会以 5 的长度来读取字符串,但实际上0x00*0x00
的长度只有 3,于是 PHP 继续向后读取,把";
也当成了字符串的一部分,然后继续反序列化,因为"
被当成了字符串,后面没有"
了,导致闭合失败,最终反序列化失败。
通过这种方式,就可以像之前的例子一样,靠长度不一致的问题,把反序列化中原本的对象信息给当成字符串,而自己再构造相应的数据,来达到修改对象中其他成员变量的值。
最后总结下上面的分析:
- 获取
flag
,需要通过POP链
- 在
play.php
文件中,反序列化player
类对象时存在反序列化逃逸,利用这点可以构造POP链
的类信息 - 受控制的参数为
player
类对象的user
和pass
,利用username
参数把user
成员填充为\0*\0
的形式,用来反序列化逃逸,利用password
的参数把pass
成员构造成POP链
类对象,用来获取flag
一步一步来,先把上面的POP链
的反序列化信息生成出来,如下:
|
|
由于只需要给pass
变量赋值,所以需要截取一段:
|
|
然后计算需要逃逸的字符长度,这里拿上面的图举例:
在这副图中,我们能控制的是这两个框中的内容,而我们的目标是控制pass
成员变成POP链
,如何控制呢?就需要进行逃逸,让user
把后面原本的pass
信息给吃掉,也就是将其作为user
的变量,这样我们构造的pass
信息就可以进行闭合,从而就能完全控制pass
的值了。所以这两个框之间的内容";s:7:"0x00*0x00pass";s:3:"
,就是需要逃逸的内容。
";s:7:"0x00*0x00pass";s:3:"
,一共占用 21 个字符长度(0x00
占一个字符长度),需要注意的是,这段字符串最后的s:3
,指的是传递给pass
的长度,也就是上面的POP链的长度
145,所以需要逃逸的字符串实际为";s:7:"0x00*0x00pass";s:145:"
,占用23个字符长度。
根据上面的结论,read()
转换一个\0*\0
,就会多出两个字符的长度,23个字符就需要 12对\0*\0
,12对\0*\0
会多出 24个字符的长度,所以POP链
得多加一个字符:
|
|
所以Payload
就变成了:
|
|
先在本机测试一下:
可以看到已经反序列化成功了,但是并没有执行成功,因为midsolo
类中有个__wakeup()
,将name
变成了字符串了,而不是 jungle类对象 了,需要绕过__wakeup()
,将midsolo
成员属性的个数改成2即可绕过,于是Payload
就变成了:
|
|
再次验证:
注意我在本地把执行的命令改成了ping
执行成功之后尝试去get flag
,先访问Payload
,然后访问play.php
,提示失败:
可以看到这里是在check()
被拦了
再回头看check()
,很简单的一个方法,就是判断反序列化数据中是否包含name
,包含就退出。而在Payload
中,的确是包含name
的,因为name
是其他类的成员属性,无法避免,所以在这里卡了很久,不知道咋绕过,最后问了下大佬,大佬说用 16进制代替字符串,也就是将name
变成 16进制的格式\6e\61\6d\65
,并且它的类型不能是小写的s
,需改成大写的S
,最后Payload
就变成了:
|
|
最后访问之后得到flag
:
强网先锋——Funhash
这道题主要考的是 PHP 中,关于弱类型比较与 md5() 相关的漏洞,如果接触得比较多的话,一下就应该做出来了,下面有两个参考链接,光看这两个链接都能把题做出来。我这里由于是第一次碰到这个题,所以还是记录下过程。
源码如下:
|
|
首先来看level 1
,需要hash1
与 md4(hash1)
相等,看到这里我第一想法是不可能,这机率太小了,然后还是写了个脚本爆破了一晚上,不出意外没有结果。第二天,网上搜资料的时候,才发现这里用的判断是!=
,结合 PHP 的弱类型比较一搜,果然这里有猫腻,经过测试发现,只要满足0e+数字
,后面的数字无论怎么变都是相等的,所以只需要构造hash1
是0e+数字
的格式,并且md4(hash1)
的结果也是0e+数字
的格式即可。于是再次构造脚本爆破,这里思路正确了,但是脚本构造的有问题,如下:
|
|
我这里想的是构造 1 ~ 32
位的字符,依次慢慢增加位数爆破,类似于笛卡尔积的模式,其实这样思路没有问题,最后肯定是能爆破出来的,只是时间有点长,在这里等了几个小时,发现没有结果,以为自己的思路有问题,于是咨询了下大佬,大佬丢了个脚本过来,如下:
|
|
大佬的脚本就是从 1 开始,依次 +1,然后加上前缀 0e
进行爆破。这样的方式其实并不算完美,会漏掉一些字符串,例如01
。不过这道题的确是得这样做,在跑了 10 多分钟之后,就找到结果了:
接着再来看level 2
,在level 2
,没有使用到==
与!=
,所以无法利用上面的方式进行绕过,在这里网上搜索了资料,得知 PHP 的md5()
无法处理数组,当构造hash2[]=1&hash3[]=2
,即可绕过这里的判断。
最后来看level 3
,这里是我第一眼看到题目时觉得最难的地方,前面两个都是进行有目的的哈希碰撞,这里完全没有目标,所以这里我没有做出来,最后问了大佬,大佬提醒我说 PHP 的 md5()
函数第二个参数为true
时,会产生漏洞。我拿着这信息去搜索了下,找到了相关的信息,大概就是当 md5()
函数第二个参数为true
时,该函数返回的是哈希值的16字符的二进制格式
,如下:
第一处,是md5('b',false)
的结果,这里返回的就是 32 字节的ascii
形式的哈希值,而第二处,是md5('b',true)
的结果,可以看到前面几个字节与第一处是一样的,相当于是第一处的二进制格式。在第二处的右边可以看到,这几个二进制数据,转化为ascii
码之后,变成了'or'xxx
,如果将这几个字符与页面上的SQL语句
拼接的话,就形成了下面的语句:
|
|
而在MySQL
中,or
后面只要不为0,即为真,所以这里相当于造成了SQL注入
,于是就满足了查询条件,得到了flag
,如下:
Payload:?hash1=0e251288019&hash2[]=1&hash3[]=2&hash4=ffifdyop
参考链接:
https://blog.csdn.net/zz_Caleb/article/details/84947371
https://www.cnblogs.com/piaomiaohongchen/p/10659359.html
强网先锋——upload
下载题目附件,可看到是一个数据包data.pcapng
,用Wireshark
打开,可看到都是HTTP
的数据:
右键追踪流 ==> TCP流
,在第 2 个流中,可看到上传了一张名为steghide.jpg
的图片,如下:
将图片数据提取出来,保存成图片文件,如下:
从上传的数据包里可看见,上传的文件名为steghide.jpg
,猜测与隐写工具steghide
有关,于是用steghide
查看一下文件信息:
发现有密码,随手一测123456
,发现密码正确:
于是提取flag.txt
,得到flag
:
参考链接:http://www.safe6.cn/article/102
强网先锋——红方辅助
下载附件,得到两个文件client.py
、enc.pcapng
先来看client.py
,该脚本就是读取文件并且加密后发送给服务器,主要逻辑如下:
- 打开文件并读取每一行数据
- 发送
G
到服务端 - 接收 4 字节服务端发来的数据
- 将每一行数据、接收到的 4 字节数据、计数变量一起传递给加密函数,得到两个值,
data
为加密后的数据 - 将刚才得到的数据发送给服务端
- 获取 4 字节服务端发来的数据,并与计数变量进行比较
从上面的流程可以看到,与服务端交互的过程是发送 ==> 接收 ==> 发送 ==> 发送 ==> 接收
现在再来看enc.pcapng
,打开之后右键选择追踪流 ==> TCP流
,在第 2 个流中可看到大量加密后的数据,如下:
可以看到其中有很多G
,可以判断这就是发送的数据包,在这里我们将流显示为原始数据
,看得更清晰:
可以很清晰地看出,这就是每一次发送接收的数据流,与之前分析的过程一模一样,都是发送 ==> 接收 ==> 发送 ==> 发送 ==> 接收
:
经过分析,服务端发送来的数据btime
为时间戳,而pcount
则是和计数变量一致的内容
接下来再来分析加密函数,如下:
其中返回的boffset
是offset
变量中的其中一个,这个通过观察流量包也能看出来,永远都是三个之中的一个。
enc
则是将一堆算出来的值,合在一起的东西。
在这个题中,我们主要是要知道它传输的内容到底是什么,也就是加密函数的形参data
,而在加密函数的 37 行这里,将data
一个一个取出来,进行一顿异或和模之后,得到的结果给enc
并传送给服务端。也就是说现在我们手上有加密的数据,加密的算法,现在需要写出解密算法,来将加密数据给解密出来。
首先来看加密函数的 35 行,这里是enc
首次被打包赋值,这几个值是解密的关键,而且这几个值没有被加密,可以从数据中得到。先来看看这个打包的格式<IIcB
,<
代表用小端存储,I
代表int
型,占 4 字节,c
代表char
型,占 1 字节,B
代表unsigned char
,占 1 字节。而在第一次的数据包中就对应如下这几个字节:
转换回来的话就是:
|
|
而下面的加密循环就变成了这样:
|
|
其中的t
也是能从数据包中找到的,所以最后就只剩c
不知道,也就是data
的每一个字符不知道是啥。
求c
怎么求呢,因为加密数据包已经知道了,所以它每次加密后的结果是知道的,例如第一个是a4
,那么chr((funcs['0'.decode()](ord(c) ^ ord(t[i]), 14) % 256)) = 0xa4
,在这种表达式下,c
的值肯定是0 ~ 255
之间的一个值,为什么呢,因为它是data
中的字符啊,而data
是可以显示的字符串,所以它的每一个字符都是可显示的,那么就肯定是0 ~ 255
的值,这时就可以进行枚举c
,从0 ~255
中枚举,哪一个满足这个表达式那么c
就为那个。
有了上面的思路,就可以写脚本了,先将数据包里的原始数据保存成data.txt
,然后运行脚本:
|
|
最后运行的结果如下, 把每一个字符提出来加上QWB{}就得到flag
:
强网先锋——主动
打开链接得到源码:
|
|
很基础的命令执行,先查看当前目录下的文件:
可看到flag.php
在当前目录下,但是因为上面的正则表达式无法直接读取文件,这里利用 Linux 的变量机制来绕过正则:
Payload:?ip=8.8.8.8;a="fla";b="g.php";cat%20"./"$a$b