O Ansible impedirá a execução de 'rm -rf /' em um script de shell

23

Isso é baseado nesta questão de fraude aqui. O problema descrito é ter um script bash que contém algo com o efeito de:

rm -rf {pattern1}/{pattern2}

... que se ambos os padrões incluírem um ou mais elementos vazios serão expandidos para pelo menos uma instância de rm -rf /, assumindo que o comando original foi transcrito corretamente e o OP estava fazendo expansão de chaves em vez de expansão de parâmetros .

Na explicação do OP sobre a fraude , ele afirma:

O comando é inofensivo, mas parece que quase ninguém percebeu.

A ferramenta Ansible evita esses erros, mas [...] ninguém parecia saber disso, caso contrário eles saberiam que o que eu descrevi não poderia acontecer.

Portanto, supondo que você tenha um script de shell que emita um rm -rf /comando por meio de expansão entre chaves ou expansão de parâmetros, é verdade que o uso do Ansible impedirá que esse comando seja executado e, em caso afirmativo, como isso acontece?

A execução rm -rf /com privilégios de root é realmente "inofensiva" desde que você esteja usando o Ansible para fazer isso?

aroth
fonte
4
Debati o que fazer com essa pergunta, mas finalmente decidi votar e responder, de modo a avançar para finalmente colocar toda essa bagunça ridícula no passado onde ela pertence.
Michael Hampton
Eu acho que a resposta realmente está na rmfonte, que analisei abaixo.
Aaron Hall

Respostas:

54

Eu tenho máquinas virtuais, vamos explodir várias delas! Pela ciência.

[root@diaf ~]# ansible --version
ansible 2.0.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides

Primeira tentativa:

[root@diaf ~]# cat killme.yml 
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      command: "rm -rf {x}/{y}"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC /bin/sh -c '( umask 22 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374 `" )'
localhost PUT /tmp/tmprogfhZ TO /root/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374/command
localhost EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374/command; rm -rf "/root/.ansible/tmp/ansible-tmp-1461128819.56-86533871334374/" > /dev/null 2>&1'
changed: [localhost] => {"changed": true, "cmd": ["rm", "-rf", "{x}/{y}"], "delta": "0:00:00.001844", "end": "2016-04-20 05:06:59.601868", "invocation": {"module_args": {"_raw_params": "rm -rf {x}/{y}", "_uses_shell": false, "chdir": null, "creates": null, "executable": null, "removes": null, "warn": true}, "module_name": "command"}, "rc": 0, "start": "2016-04-20 05:06:59.600024", "stderr": "", "stdout": "", "stdout_lines": [], "warnings": ["Consider using file module with state=absent rather than running rm"]}
 [WARNING]: Consider using file module with state=absent rather than running rm


PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

OK, commandapenas passa os literais e nada acontece.

Que tal nosso desvio de segurança favorito raw?

[root@diaf ~]# cat killme.yml
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      raw: "rm -rf {x}/{y}"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC rm -rf {x}/{y}
ok: [localhost] => {"changed": false, "invocation": {"module_args": {"_raw_params": "rm -rf {x}/{y}"}, "module_name": "raw"}, "rc": 0, "stderr": "", "stdout": "", "stdout_lines": []}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0

Não vá de novo! Quão difícil pode ser excluir todos os seus arquivos?

Ah, mas e se fossem variáveis ​​indefinidas ou algo assim?

[root@diaf ~]# cat killme.yml
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      command: "rm -rf {{x}}/{{y}}"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
fatal: [localhost]: FAILED! => {"failed": true, "msg": "'x' is undefined"}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @killme.retry

PLAY RECAP *********************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1

Bem, isso não funcionou.

Mas e se as variáveis ​​forem definidas, mas vazias?

[root@diaf ~]# cat killme.yml 
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      command: "rm -rf {{x}}/{{y}}"
  vars:
    x: ""
    y: ""
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC /bin/sh -c '( umask 22 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105 `" )'
localhost PUT /tmp/tmp78m3WM TO /root/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105/command
localhost EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105/command; rm -rf "/root/.ansible/tmp/ansible-tmp-1461129132.63-211170666238105/" > /dev/null 2>&1'
fatal: [localhost]: FAILED! => {"changed": true, "cmd": ["rm", "-rf", "/"], "delta": "0:00:00.001740", "end": "2016-04-20 05:12:12.668616", "failed": true, "invocation": {"module_args": {"_raw_params": "rm -rf /", "_uses_shell": false, "chdir": null, "creates": null, "executable": null, "removes": null, "warn": true}, "module_name": "command"}, "rc": 1, "start": "2016-04-20 05:12:12.666876", "stderr": "rm: it is dangerous to operate recursively on ‘/’\nrm: use --no-preserve-root to override this failsafe", "stdout": "", "stdout_lines": [], "warnings": ["Consider using file module with state=absent rather than running rm"]}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @killme.retry

PLAY RECAP *********************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1

Finalmente, algum progresso! Mas ainda reclama que eu não usei--no-preserve-root .

Claro, também me avisa que eu deveria tentar usar o filemódulo e state=absent. Vamos ver se isso funciona.

[root@diaf ~]# cat killme.yml 
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      file: path="{{x}}/{{y}}" state=absent
  vars:
    x: ""
    y: ""
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml    
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC /bin/sh -c '( umask 22 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388 `" )'
localhost PUT /tmp/tmpUqLzyd TO /root/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388/file
localhost EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388/file; rm -rf "/root/.ansible/tmp/ansible-tmp-1461129394.62-191828952911388/" > /dev/null 2>&1'
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_args": {"backup": null, "content": null, "delimiter": null, "diff_peek": null, "directory_mode": null, "follow": false, "force": false, "group": null, "mode": null, "original_basename": null, "owner": null, "path": "/", "recurse": false, "regexp": null, "remote_src": null, "selevel": null, "serole": null, "setype": null, "seuser": null, "src": null, "state": "absent", "validate": null}, "module_name": "file"}, "msg": "rmtree failed: [Errno 16] Device or resource busy: '/boot'"}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @killme.retry

PLAY RECAP *********************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1

Boas notícias, pessoal! Começou a tentar excluir todos os meus arquivos! Mas, infelizmente, ocorreu um erro. Vou deixar consertar isso e fazer com que o manual destrua tudo usando o filemódulo como um exercício para o leitor.


NÃO gere nenhum playbook que você veja além deste ponto! Você verá o porquê em um momento.

Finalmente, para o golpe de estado ...

[root@diaf ~]# cat killme.yml
---
- hosts: localhost
  gather_facts: False
  tasks:
    - name: Die in a fire
      raw: "rm -rf {{x}}/{{y}}"
  vars:
    x: ""
    y: "*"
[root@diaf ~]# ansible-playbook -l localhost -vvv killme.yml
Using /etc/ansible/ansible.cfg as config file
1 plays in killme.yml

PLAY ***************************************************************************

TASK [Die in a fire] ***********************************************************
task path: /root/killme.yml:5
ESTABLISH LOCAL CONNECTION FOR USER: root
localhost EXEC rm -rf /*
Traceback (most recent call last):
  File "/usr/lib/python2.7/site-packages/ansible/executor/process/result.py", line 102, in run
  File "/usr/lib/python2.7/site-packages/ansible/executor/process/result.py", line 76, in _read_worker_result
  File "/usr/lib64/python2.7/multiprocessing/queues.py", line 117, in get
ImportError: No module named task_result

Esta VM é um ex-papagaio !

Curiosamente, o acima falhou em fazer qualquer coisa em commandvez de raw. Apenas imprimiu o mesmo aviso sobre o uso filecom state=absent.

Eu vou dizer que parece que se você não estiver usando rawisso, existe alguma proteção contra o rmmal. Você não deve confiar nisso, no entanto. Examinei rapidamente o código de Ansible e, embora encontrasse o aviso, não encontrei nada que realmente suprimisse a execução do rmcomando.

Michael Hampton
fonte
10
+1 para a ciência. Eu tinha mais um para o nome do host, mas seria fraude; p /
Journeyman Geek
Parece que você pode ter um sistema de arquivos montado em /boot.
84104
1
@ 84104 Engraçado, isso. Por pura coincidência, booté a primeira entrada de diretório em /. Portanto, nenhum arquivo foi perdido.
Michael Hampton
5
@aroth Exatamente! Mas, para a ciência, tente rm -rf {{x}}/{{y}}quando yestiver definido como "*". A --no-preserve-rootverificação é útil para o que é, mas não o tira de todas as situações possíveis; é fácil o suficiente para ignorar. É por isso que essa pergunta não foi identificada como uma farsa imediatamente: levando em consideração o mau inglês e os aparentes erros de sintaxe, é plausível .
Michael Hampton
1
Além disso raw, um problema cronpode ser outra maneira de destruir um sistema.
84104
3

O Ansible impedirá a execução de rm -rf /um script de shell?

Inspecionei a fonte coreutils rm , que tem o seguinte:

  if (x.recursive && preserve_root)
    {
      static struct dev_ino dev_ino_buf;
      x.root_dev_ino = get_root_dev_ino (&dev_ino_buf);
      if (x.root_dev_ino == NULL)
        error (EXIT_FAILURE, errno, _("failed to get attributes of %s"),
               quoteaf ("/"));
    }

A única maneira de apagar a raiz é ultrapassar esse bloco de código. A partir desta fonte :

struct dev_ino *
get_root_dev_ino (struct dev_ino *root_d_i)
{
  struct stat statbuf;
  if (lstat ("/", &statbuf))
    return NULL;
  root_d_i->st_ino = statbuf.st_ino;
  root_d_i->st_dev = statbuf.st_dev;
  return root_d_i;
}

Interpreto que isso significa que a função get_root_dev_inoretorna nula /e, portanto, rm falha.

A única maneira de ignorar o primeiro bloco de código (com recursão) é ter --no-preserve-roote ele não usa uma variável de ambiente para substituir, portanto, ele teria que ser passado explicitamente para rm.

Eu acredito que isso prova que a menos que Ansible passa explicitamente --no-preserve-rootpara rm, ele não vai fazer isso.

Conclusão

Não acredito que o Ansible previna explicitamente rm -rf /porque rmele o impede.

Aaron Hall
fonte