Ansible Playbooks
Now that we’ve learned how Ad-hoc commands work,
let’s move on to a more common method,
which will take advantage of Ansible playbooks.
As before, we’ll first take a look at how playbooks are intended to function
and then discuss how to exploit them.
Playbooks allow sets of tasks to be scripted
so they can be run routinely at points in time.
This is useful for combining various setup tasks,
such as adding user accounts and settings
to a new server
or updating large numbers of machines at once.
Playbooks are written using the YAML markup language.
Let’s try a simple playbook on our controller.
The name value just gives a name to the playbook being run,
and hosts specifies which hosts from the inventory
this playbook should be run on.
We can specify groups,
individual hosts, or “all”.
The gather_facts value will gather information,
or “facts”, about the machine.
These facts are returned in a JSON format,
then parsed by the controller
to be used during processing of the playbook.
This process fills the {{ansible_hostname}}
and {{ansible-distribution}} variables
in our output.
Both variables are the results of the initial fact-gathering process.
The tasks line specifies a new task to be performed,
labeled with a name.
The task also has a msg value
containing a string with our output to be displayed
when the task is run.
A task is just a call to an Ansible module.
The Ansible documentation contains a full list of available modules.
In our playbook above, we run the debug module
and provide a parameter of msg
with a message to display.
We can run the playbook using the ansible-playbook command.
The playbook was run on the victim system
and was able to retrieve the victim’s hostname and Linux distribution type.
Playbooks can also include a “become” line,
if we want the scripts to be run as root.
Alternatively, we can include a username
if we want to run as someone else.
Playbooks are used more frequently than ad-hoc commands
because they allow sysadmins to script tasks they want to repeat more than once.
Ad-hoc commands are useful for one-off actions.
However, if a sysadmin wants to reconfigure
systems the same way multiple times,
run multiple tasks on the same sets of machines,
or gather specific sets of information from different machines
at different times,
playbooks can be very handy.
Exploiting Playbooks for Ansible Credentials
We’ve discussed normal practice for Ansible,
but as attackers, we are interested in the potential exploit.
Of course, if we have root access
or access to the Ansible administrator account on the Ansible controller,
we can run ad-hoc commands or playbooks
as the Ansible user on all nodes,
typically with elevated or root access.
In addition, if Ansible is set up to use SSH for authentication to nodes,
we could steal the Ansible administrator user’s private key
from their home folder
and log in to the nodes directly.
All of these are options if we’re already root
on the controller.
Unfortunately, these methods require root
or Ansible admin account access, which we might not have.
Let’s explore additional options available to us as a non-root user.
If stored playbooks on the controller are in a world-readable location
or we have access to the folder they’re stored in,
we can search for hardcoded credentials.
In some cases, it may be necessary
or desirable for an administrator to avoid configuring a public key
on a node machine.
In this case, it’s possible for the administrator to run commands on the node
using SSH usernames and passwords instead.
In the following example, in our controller VM,
the administrator of our Ansible controller
wanted to create a file
in the linuxvictim machine,
but they needed to authenticate to the system
as the offsec user.
This user does not have the ansibleadm user’s key
in its authorized_keys file
file and so the sysadmin needed to use the offsec user’s username
and password to authenticate.
To do this, the administrator hardcoded the offsec user’s credentials
in the playbook.
The credentials are stored with the keyword
ansible_become_pass.
The script indicates that the user that the script is becoming
(in this case offsec)
has a password of “lab”.
We can run the playbook,
and verify that the file is written.
If we don’t have access to run ansible-playbook
but have read access to playbooks themselves,
we may be able to harvest sensitive credentials
and compromise nodes
used by Ansible.
Ansible does have newer features such as Ansible Vault,
which can securely store credentials for use in playbooks.
This allows user to encrypt
or decrypt files or strings
using a password.
On our controller VM, we find another playbook
called writefilevault.yaml.
If we examine the contents, there is a different password type listed.
The !vault keyword lets Ansible know that the value is vault-encrypted.
As an attacker, we can copy the section of the encrypted payload
and attempt to crack it offline.
Let’s place the value into a text file
called test.yml,
making sure to remove any leading whitespace.
Again, in order to crack the password, we need to convert it to a format
that John the Ripper or Hashcat can use.
To do that, we can use the ansible2john
utility included with JTR.
If we run ansible2john on the file,
it returns a string in a workable format for Hashcat to use.
We’ll copy the string into a new file called testhash.txt.
Now we can run hashcat on our file to crack the vault password,
using the rockyou wordlist.
As indicated in the output,
Hashcat was able to crack the vault password.
Back on our controller VM,
we can copy the original encrypted vault string into a text file,
and pipe it to ansible-vault decrypt.
We’re prompted for our vault password,
which hashcat told us is “spongebob”,
and then vault will provide us with the original,
unencrypted password
stored in the playbook for the offsec user.
Decrypting encrypted files
(as opposed to strings) is essentially the same process,
since files are encrypted using the same encryption scheme.
Weak Permissions on Ansible Playbooks
Another option we have at our disposal for exploiting Ansible environments
is to take advantage of playbooks
that we have write access to.
If the playbook files used on the controller have world-writable permissions
or if we can find a way to write to them,
perhaps through an exploit,
we can inject tasks
that will then be run the next time the playbook is run.
In the controller VM, the getinfowritable.yaml playbook
has lax permissions,
allowing anyone within the “ansible” group
to write to it.
In this particular scenario, we assume we have compromised the bystander account,
which is in the “ansible” group.
Because of this, the user has write access to the playbooks folder
through group permissions.
We can use our permissions to edit the getinfowritable.yaml playbook
Let’s modify they file by adding few tasks to it.
We could completely overwrite the playbook if we wanted to,
but that would change its intended functionality.
This is likely to be noticed by the administrator,
especially if the playbook is run frequently.
It’s much more discreet to keep the original functionality intact,
tack on several new tasks,
and add the become value
to ensure the playbook
is run as root.
The first task we inserted creates the /root/.ssh folder
and sets the appropriate permissions on it.
The second task creates the authorized_keys file
and sets its permissions.
The last task copies our public key into the root user’s authorized_keys file,
appending it to the end if the file already exists.
In this case, we’ve used the public key from our Kali VM.
If the playbook is run by the ansibleadm user,
our key is added to the root user’s account
on the linuxvictim host.
Once it is added, we are able to SSH to the linuxvictim machine from our Kali VM
as root.
There may be a situation where we want to run shell commands
directly on the machine.
To do this, we insert the commands we want to run in a command Ansible task
in the getinfowritable playbook we used earlier.
There are a few unfamiliar options here:
async,
poll,
and shell.
Typically, when an Ansible playbook is run,
it “blocks” or waits for a response to report back to the controller.
If we specify the async parameter with any timeout value,
the command will run asynchronously.
The timeout value is disregarded
because the poll setting of 0 makes the async value irrelevant.
This tells Ansible not to poll the process for results
but just let it run on its own
until the execution of the playbook is complete.
The shell value specifies the shell command we want to run.
If we run the playbook as before,
then check the /tmp directory on the linuxvictim host,
we notice that the command was run successfully.