Will Ansible evitará la ejecución de 'rm -rf /' en un script de shell

23

Esto se basa en esta pregunta falsa aquí. El problema descrito es tener un script bash que contiene algo en el sentido de:

rm -rf {pattern1}/{pattern2}

... que si ambos patrones incluyen uno o más elementos vacíos se expandirá al menos a una instancia de rm -rf /, suponiendo que el comando original se transcribió correctamente y que el OP estaba haciendo la expansión de llaves en lugar de la expansión de parámetros .

En la explicación del OP del engaño , afirma:

El comando [...] es inofensivo pero parece que casi nadie se ha dado cuenta.

La herramienta Ansible previene estos errores, pero [...] nadie parecía saberlo, de lo contrario sabrían que lo que he descrito no podría suceder.

Entonces, suponiendo que tenga un script de shell que emita un rm -rf /comando a través de la expansión de llaves o la expansión de parámetros, ¿es cierto que el uso de Ansible evitará que se ejecute ese comando? Y si es así, ¿cómo lo hace?

¿Ejecutar rm -rf /con privilegios de root es realmente "inofensivo" siempre que esté usando Ansible para hacerlo?

Aroth
fuente
44
Debatí qué hacer con esta pregunta, pero finalmente decidí votar y responderla, para avanzar finalmente y poner todo este lamentable y ridículo desastre en el pasado donde pertenece.
Michael Hampton
Creo que la respuesta realmente se encuentra en la rmfuente, que analicé a continuación.
Aaron Hall

Respuestas:

54

Tengo máquinas virtuales, explotémoslas. Para la ciencia.

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

Primer intento:

[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, entonces commandsolo pasa los literales y no pasa nada.

¿Qué tal nuestro bypass de seguridad 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

No vayas de nuevo! ¿Qué tan difícil puede ser eliminar todos tus archivos?

Ah, pero ¿y si fueran variables indefinidas o algo así?

[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

Bueno, eso no funcionó.

Pero, ¿qué pasa si las variables están definidas, pero vacías?

[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, un poco de progreso! Pero aún se queja de que no lo usé --no-preserve-root.

Por supuesto, también me advierte que debería intentar usar el filemódulo y state=absent. A ver si eso 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

¡Buenas noticias para todos! ¡Comenzó a tratar de eliminar todos mis archivos! Pero desafortunadamente se encontró con un error. Dejaré arreglar eso y conseguir que el libro de jugadas destruya todo usando el filemódulo como ejercicio para el lector.


¡NO ejecute ningún libro de jugadas que vea más allá de este punto! Verás por qué en un momento.

Finalmente, para el golpe de gracia ...

[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 máquina virtual es un ex loro !

Curiosamente, lo anterior no pudo hacer nada en commandlugar de raw. Simplemente imprimió la misma advertencia sobre el uso filecon state=absent.

Voy a decir que parece que si no estás usando raweso, hay alguna protección contra la rmpérdida de control. Sin embargo, no debes confiar en esto. Eché un vistazo rápido al código de Ansible y, aunque encontré la advertencia, no encontré nada que pudiera suprimir la ejecución del rmcomando.

Michael Hampton
fuente
10
+1 para ciencia. Me gustaría +1 más por el nombre de host, pero sería un fraude; p /
Journeyman Geek
Parece que podría tener un sistema de archivos montado en /boot.
84104
1
@ 84104 Divertido, eso. Por pura coincidencia, bootes la primera entrada de directorio en /. Entonces no se perdieron archivos.
Michael Hampton
55
@aroth ¡Exactamente! Pero, para la ciencia, intente rm -rf {{x}}/{{y}}cuando yesté configurado en "*". El --no-preserve-rootcheque es útil para lo que es, pero no lo sacará de todas las situaciones posibles; es lo suficientemente fácil de evitar. Es por eso que esa pregunta no se detectó como un engaño de inmediato: teniendo en cuenta el mal inglés y los aparentes errores de sintaxis, es plausible .
Michael Hampton
1
Además raw, un mal cronpodría ser otra forma de destruir un sistema.
84104
3

¿Ansible evitará la ejecución de rm -rf /un script de shell?

Inspeccioné la fuente de coreutils rm , que tiene lo siguiente:

  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 ("/"));
    }

La única forma de borrar desde la raíz es pasar este bloque de código. De esta fuente :

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 esto significa que la función get_root_dev_inodevuelve nulo /y, por lo tanto, rm falla.

La única forma de omitir el primer bloque de código (con recursión) es tener --no-preserve-rooty no utiliza una variable de entorno para anular, por lo que debería pasarse explícitamente a rm.

Creo que esto prueba que a menos que Ansible pase explícitamente --no-preserve-roota rm, no lo hará.

Conclusión

No creo que Ansible lo evite explícitamente rm -rf /porque él rmmismo lo impide.

Aaron Hall
fuente