Capture the Flag Exercises: Part Two

If you have followed along with Part One, you should have Linux and Windows virtual machine templates, and terraform scripts to create the network topology.

To configure the hosts and add vulnerabilities, we will be using Ansible. The vulnerabilities were going to be adding in are designed to be quick and easy to exploit.


Working with Dynamic Inventories

Since some of our hosts have DHCP assigned IP addresses, we will need to write an inventory script to query the Proxmox API and determine the allocated IP addresses. The following Python code serves this purpose.

#!/usr/bin/env python3

import json
import requests
import sys
import urllib3

PROXMOX_HOST = 'https://192.168.1.201:8006'
USERNAME = 'root@pam'
PASSWORD = 'Password1'
VERIFY_SSL = False
NODE = 'pve'

if not VERIFY_SSL:
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_auth_ticket():
    url = f"{PROXMOX_HOST}/api2/json/access/ticket"
    data = {'username': USERNAME, 'password': PASSWORD}
    response = requests.post(url, data=data, verify=VERIFY_SSL)
    response.raise_for_status()
    result = response.json()['data']
    return result['ticket'], result['CSRFPreventionToken']

def get_all_vmids(ticket):
    headers = {'Cookie': f"PVEAuthCookie={ticket}"}
    url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu"
    response = requests.get(url, headers=headers, verify=VERIFY_SSL)
    response.raise_for_status()
    vms = response.json()['data']
    return [vm['vmid'] for vm in vms]

def get_vm_ip(ticket, vmid):
    headers = {'Cookie': f"PVEAuthCookie={ticket}"}
    url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu/{vmid}/agent/network-get-interfaces"
    try:
        response = requests.get(url, headers=headers, verify=VERIFY_SSL)
        if response.status_code != 200:
            return None
        data = response.json().get('data', {})
        interfaces = data.get('result', [])
        for interface in interfaces:
            for addr in interface.get('ip-addresses', []):
                ip = addr.get('ip-address')
                if ip and not ip.startswith('127.') and not ip.startswith('169.254.') and ':' not in ip:
                    return ip
    except Exception as e:
        print(f"Error reading VM {vmid}: {e}", file=sys.stderr)
        return None
    return None

def get_vm_os_type(ticket, vmid):
    headers = {'Cookie': f"PVEAuthCookie={ticket}"}
    url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu/{vmid}/agent/get-osinfo"
    try:
        response = requests.get(url, headers=headers, verify=VERIFY_SSL)
        if response.status_code != 200:
            return None
        data = response.json().get('data', {})
        os_info = data.get('result', {})
        os_name = os_info.get('name', '').lower()

        if 'windows' in os_name:
            return 'windows'
        return 'linux'
    except Exception as e:
        print(f"Error reading OS info for VM {vmid}: {e}", file=sys.stderr)
        return None

def build_inventory():
    ticket, _ = get_auth_ticket()
    headers = {'Cookie': f"PVEAuthCookie={ticket}"}

    url = f"{PROXMOX_HOST}/api2/json/nodes/{NODE}/qemu"
    response = requests.get(url, headers=headers, verify=VERIFY_SSL)
    response.raise_for_status()
    vms = response.json()['data']

    hosts = []
    hostvars = {}

    for vm in vms:
        vmid = vm['vmid']
        vm_name = vm.get('name', f'vm-{vmid}')
        ip = get_vm_ip(ticket, vmid)
        os_type = get_vm_os_type(ticket, vmid)

        if ip:
            hosts.append(vm_name)
            
            # Default to Linux settings
            hostvars[vm_name] = {
                'ansible_host': ip,
                'ansible_user': 'bordergate',
                'ansible_password': 'Password1',
                'ansible_connection': 'ssh'
            }

            if os_type == 'windows':
                hostvars[vm_name].update({
                    'ansible_user': 'Administrator',
                    'ansible_connection': 'winrm',
                    'ansible_winrm_transport': 'ntlm',
                    'ansible_winrm_port': 5985,
                    'ansible_winrm_server_cert_validation': 'ignore',  
                    'ansible_password': 'Password1' 
                })

    inventory = {
        'all': {
            'hosts': hosts,
            'vars': {}
        },
        '_meta': {
            'hostvars': hostvars
        }
    }
    return inventory

def main():
    if len(sys.argv) == 2 and sys.argv[1] == '--list':
        inventory = build_inventory()
        print(json.dumps(inventory, indent=2))
    elif len(sys.argv) == 2 and sys.argv[1] == '--host':
        print(json.dumps({})) 
    else:
        print("Usage: inventory.py --list|--host <hostname>")
        sys.exit(1)

if __name__ == '__main__':
    main()

Log into the director system (which has access to all configured networks), and test the dynamic inventory management is working.

bordergate@N0-DIRECTOR:~$ sudo apt install ansible sshpass
bordergate@N0-DIRECTOR:~$ export ANSIBLE_HOST_KEY_CHECKING=False
bordergate@N0-DIRECTOR:~$ ansible -i ./inventory.py N2-DEMETER -m win_ping
N2-DEMETER | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
bordergate@N0-DIRECTOR:~$ ansible -i ./inventory.py N1-ZEUS -m ping
N1-ZEUS | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}


Adding Vulnerabilities

N1-ZEUS

This system has the following vulnerabilities.

  • SNMP v1 configuration that shows the network 2 range
  • IP routing is enabled, so the system will forward traffic to network 2
  • The system has a web server listening that’s only accessible from network 2. This contains credentials to access the system using SSH

Create an Ansible configuration (CONFIGS/N1-ZEUS.yaml)

- hosts: N1-ZEUS
  user:  root
  become: true
  tasks: 
  - name: Set configuration file path
    set_fact:
      config_file_path: '../FILES/N1-ZEUS/'

  - name: install nginx
    apt: pkg=nginx state=present
  - name: install snmpd
    apt: pkg=snmpd state=present

  - name: Enable IP forwarding
    ansible.builtin.shell: echo 1 > /proc/sys/net/ipv4/ip_forward
  - name: Clear previous IPtables rules
    ansible.builtin.shell: iptables -F
  - name: Enable NAT
    ansible.builtin.shell: iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
  - name: Drop ICMP on input chain
    ansible.builtin.shell: iptables -I INPUT -p icmp --icmp-type 8 -j DROP
  - name: Drop ICMP on forward chain
    ansible.builtin.shell: iptables -I FORWARD -p icmp --icmp-type 8 -j DROP
  - name: Drop SMB
    ansible.builtin.shell: iptables -I FORWARD -p tcp --dport 445 -j DROP
  - name: Drop SSH
    ansible.builtin.shell: iptables -I FORWARD -p tcp --dport 22 -j DROP

  - name: Remove all 'listen 80' or 'listen 80 default_server' directives
    replace:
      path: /etc/nginx/sites-available/default
      regexp: '^\s*listen\s+80.*;'
      replace: ''

  - name: Update NGINX to listen only on 172.16.24.7
    lineinfile:
      path: /etc/nginx/sites-available/default
      regexp: '^(\s*)listen\s+.*;'
      line: '    listen 172.16.24.7:80;'
      state: present
      backrefs: yes

  - name: Copy SNMP configuration file
    copy:
      dest: /etc/snmp/snmpd.conf
      content: |
        sysLocation    Sitting on the Dock of the Bay
        sysContact     Me <me@example.org>
        sysServices    72
        agentaddress udp:0.0.0.0:161
        view   systemonly  included   .1.3.6.1.2.1.1
        view   systemonly  included   .1.3.6.1.2.1.25.1
        rocommunity public
        rocommunity6 public default -V systemonly
        rouser authPrivUser authpriv -V systemonly
        includeDir /etc/snmp/snmpd.conf.d

  - name: Copy sysctl configuration to enable IP forwarding
    copy:
      dest: /etc/sysctl.conf
      content: |
        net.ipv4.ip_forward=1

  - name: Restart SNMP daemon
    service:
      name: snmpd
      state: started

  - name: Restart Nginx
    service:
      name: nginx
      state: reloaded

  - name: Ensure zeus user exists
    user:
      name: zeus
      shell: /bin/bash
      state: present
      create_home: yes
      password: "{{ 'ZeusJupiter' | password_hash('sha512') }}"
      groups: sudo
      append: yes

  - name: Deploy robots.txt file
    ansible.builtin.copy:
      dest: /var/www/html/robots.txt
      content: |
        zeus:ZeusJupiter
      owner: root
      group: root
      mode: '0644'
      force: yes

  - name: Create FLAG
    copy:
      dest: /root/FLAG.txt
      content: |
          FLAG{5a1684d7d326b054f5e4e6c6e2cd4a9507049b96}

N1-HERA

This system has the following vulnerabilities.

  • An open NFS share leaks credentials, which allows access over SSH
  • A mis-configured cron job to elevate to root
  • The system is duel homed, which provides access to network 2
- hosts: N1-HERA
  user:  root
  become: true
  tasks:
  - name: Set configuration file path
    set_fact:
      config_file_path: '../FILES/N1-HERA/'

  - name: install nfs-kernel-server
    apt: pkg=nfs-kernel-server state=present

  - name: Copy NFS information leak
    copy:
      dest: /etc/exports
      content: |
        /srv     *(ro,sync,subtree_check)

  - name: Create user 'hera'
    user:
      name: hera
      password: "{{ 'hercules' | password_hash('sha512') }}"
      shell: /bin/bash
      home: /home/hera
      state: present
      create_home: yes

  - name: Copy NFS information leak
    copy:
      dest: /srv/creds.txt
      content: |
        hera:hercules

  - name: Restart NFS daemon
    service:
      name: nfs-kernel-server.service
      state: restarted

  - name: Create vulnerable script
    copy:
      dest: /usr/local/bin/backup.sh
      content: |
          #!/bin/bash
          echo "Backup started..."
      owner: root
      group: root
      mode: '0777'

  - name: Make sure script is executable
    file:
      path: /usr/local/bin/backup.sh
      mode: '0777'

  - name: Add cron job running as root
    cron:
      name: "Root cron for backup.sh"
      user: root
      job: "/usr/local/bin/backup.sh"
      minute: "*/1"

  - name: Create FLAG
    copy:
      dest: /root/FLAG.txt
      content: |
          FLAG{00c5d6c5df2e4a4494e1b2631eae9fbb52607d76}


N1-AEOLUS

This system runs a TFTP server that contains credentials which can be used to SSH into it. In addition, it’s running a DNS server for the pantheon.local domain, which supports zone transfers.

- hosts: N1-AEOLUS
  user: root
  become: true
  tasks:
    - name: Set configuration file path
      set_fact:
        config_file_path: '../FILES/N1-AEOLUS'

    # DNS Server Setup
    - name: Install BIND9 DNS server
      apt:
        name: bind9
        state: present
        update_cache: yes

    - name: Configure named.conf.options
      copy:
        dest: /etc/bind/named.conf.options
        content: |
          options {
              directory "/var/cache/bind";
              allow-transfer { any; };
              recursion yes;
              allow-recursion { any; };
              listen-on { any; };
              listen-on-v6 { any; };
              dnssec-validation no;
          };

    - name: Configure zone in named.conf.local
      copy:
        dest: /etc/bind/named.conf.local
        content: |
          zone "pantheon.local" {
              type master;
              file "/etc/bind/zones/db.pantheon.local";
              allow-transfer { any; };
          };

    - name: Create zone directory
      file:
        path: /etc/bind/zones
        state: directory
        owner: bind
        group: bind
        mode: '0755'

    - name: Create zone file for pantheon.local
      copy:
        dest: /etc/bind/zones/db.pantheon.local
        content: |
          ;
          ; BIND data file for pantheon.local
          ;
          $TTL    604800
          @       IN      SOA     ns1.pantheon.local. admin.pantheon.local. (
                                2         ; Serial
                           604800         ; Refresh
                            86400         ; Retry
                          2419200         ; Expire
                           604800 )       ; Negative Cache TTL
          ;
          @       IN      NS      ns1.pantheon.com.
          ns1     IN      A           172.16.24.99
          demeter     IN      A       172.16.24.70
          hermes      IN      A       172.16.24.55
          apollo      IN      A       172.16.24.75
          hades       IN      A       172.16.24.24

    - name: Restart BIND9
      service:
        name: bind9
        state: restarted
        enabled: yes


    - name: Install atftpd
      apt:
        name: atftpd
        state: present
        update_cache: yes

    - name: Create TFTP root directory
      file:
        path: /srv/tftp
        state: directory
        owner: nobody
        group: nogroup
        mode: '0755'

    - name: Configure atftpd options
      copy:
        dest: /etc/default/atftpd
        content: |
          USE_INETD=false
          OPTIONS="--daemon --port 69 --retry-timeout 1 --verbose=5 /srv/tftp"

    - name: Enable and restart atftpd
      service:
        name: atftpd
        state: restarted
        enabled: yes

    - name: Create user 'aeolus'
      user:
        name: aeolus
        password: "{{ 'dinlas' | password_hash('sha512') }}"
        state: present
        shell: /bin/bash
        groups: sudo

    - name: Copy credentials to TFTP directory
      copy:
        dest: /srv/tftp/startup-config
        content: |
          aeolus:dinlas
        mode: '0644'

    - name: Create FLAG
      copy:
        dest: /root/FLAG.txt
        content: |
          FLAG{07ca234f133dcf10cd5812c3faef980f954505d2}

N1-ARES

ARES contains the following vulnerabilities.

  • The Guest account is enabled, and allows RDP access.
  • A Windows service with weak privileges allows for privilege escalation
  • The system is duel homed, allowing access to network 2
- hosts: N1-ARES
  tasks: 
  - name: Set configuration file path
    set_fact:
      config_file_path: '../FILES/N1-ARES/'

  - name: Disable Windows update
    win_service:
      name: Windows update
      start_mode: disabled
      state: stopped

  - name: Create user 'ares'
    win_user:
      name: ares
      password: 'P@ssw0rd123!'
      state: present
      groups:
        - Users
      password_never_expires: yes
      user_cannot_change_password: no

  - name: Disable Windows firewall
    win_firewall:
      state: disabled
      profiles:
      - Domain
      - Private
      - Public
    tags: disable_firewall

  - name: Enable guest account
    win_command: net user Guest /active:yes

  - name: Add Guest to Remote Desktop Users group
    win_group_membership:
      name: "Remote Desktop Users"
      members:
        - Guest
      state: present

  - name: Enable insecure guest logons
    win_regedit:
      path: HKLM:\Software\Policies\Microsoft\Windows\Lanmanworkstation
      name: "AllowInsecureGuestAuth"
      data: "1"
      type: dword

  - name: Add anonymous to everyone group
    win_regedit:
      path: HKLM:\SYSTEM\CurrentControlSet\Control\Lsa
      name: "everyoneincludesanonymous"
      data: "1"
      type: dword

  - name: Disable null session restrictions
    win_regedit:
      path: HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
      name: "restrictnullsessaccess"
      data: "0"
      type: dword

  - name: Enable RDP in the registry
    win_regedit:
      path: HKLM:\System\CurrentControlSet\Control\Terminal Server
      name: fDenyTSConnections
      data: 0
      type: dword

  - name: Ensure RDP service is running and set to auto-start
    win_service:
      name: TermService
      start_mode: auto
      state: started

  - name: Create service directory
    win_file:
      path: C:\vulnsvc
      state: directory

  - name: Download service executable
    ansible.windows.win_get_url:
      url: http://192.168.24.250/service.exe
      dest: C:\vulnsvc\service.exe

  - name: Set permissions so 'Users' can overwrite the .exe file
    win_acl:
      path: C:\vulnsvc\service.exe
      user: Users
      rights: FullControl
      type: allow
      state: present
      inherit: ContainerInherit,ObjectInherit

  - name: Create and start the vulnerable Windows service
    win_service:
      name: VulnService
      display_name: Vulnerable Service
      description: Insecure service for privesc testing
      path: 'cmd.exe /c C:\vulnsvc\service.exe'
      start_mode: auto
      state: started

  - name: Copy flag file
    copy:
      dest: C:\Users\Administrator\Desktop\FLAG.txt
      content: |
        FLAG{2ff3e0b8bb5cdac3a2ea7baca6b48be0ed919de2}

For the vulnerable Windows service, I’m using the following code.

#include <windows.h>

#define SERVICE_NAME "MySampleService"

SERVICE_STATUS_HANDLE g_StatusHandle;
HANDLE g_StopEvent;

void SetStatus(DWORD state) {
    SERVICE_STATUS status = {
        .dwServiceType = SERVICE_WIN32_OWN_PROCESS,
        .dwCurrentState = state,
        .dwControlsAccepted = (state == SERVICE_RUNNING) ? SERVICE_ACCEPT_STOP : 0,
        .dwWin32ExitCode = 0
    };
    SetServiceStatus(g_StatusHandle, &status);
}

VOID WINAPI ServiceCtrlHandler(DWORD ctrl) {
    if (ctrl == SERVICE_CONTROL_STOP) {
        SetStatus(SERVICE_STOP_PENDING);
        SetEvent(g_StopEvent);
    }
}

VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv) {
    g_StatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, ServiceCtrlHandler);
    if (!g_StatusHandle) return;

    SetStatus(SERVICE_START_PENDING);

    g_StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (!g_StopEvent) {
        SetStatus(SERVICE_STOPPED);
        return;
    }

    SetStatus(SERVICE_RUNNING);
    WaitForSingleObject(g_StopEvent, INFINITE);
    CloseHandle(g_StopEvent);

    SetStatus(SERVICE_STOPPED);
}

int main() {
    SERVICE_TABLE_ENTRY table[] = {
        { SERVICE_NAME, ServiceMain },
        { NULL, NULL }
    };
    return StartServiceCtrlDispatcher(table) ? 0 : GetLastError();
}

Compile the service using MinGW.

x86_64-w64-mingw32-gcc service.c -o myservice.exe -ladvapi32

N2-APOLLO

APOLLO is a Windows system with VNC configured to auto-login as the administrator.

- hosts: N2-APOLLO
  tasks:
  - name: Set configuration file path
    set_fact:
      config_file_path: '../FILES/N2-APOLLO/'

  - name: Add a static route back to Network One
    ansible.windows.win_command: route -p add 192.168.24.0 mask 255.255.255.0 172.16.24.7

  - name: Ensure the "Winlogon" registry path exists
    ansible.windows.win_regedit:
      path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
      name: AutoAdminLogon
      type: string
      data: "1"
      state: present
    ignore_errors: yes

  - name: Ensure DefaultUserName registry key exists for auto-login
    ansible.windows.win_regedit:
      path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
      name: DefaultUserName
      type: string
      data: Administrator
      state: present
    ignore_errors: yes

  - name: Ensure DefaultPassword registry key exists for auto-login
    ansible.windows.win_regedit:
      path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
      name: DefaultPassword
      type: string
      data: "Password1"  
      state: present
    ignore_errors: yes

  - name: Ensure ForceAutoLogon registry key exists
    ansible.windows.win_regedit:
      path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
      name: ForceAutoLogon
      type: string
      data: "1"
      state: present
    ignore_errors: yes

  - name: Ensure DefaultDomainName registry key exists for auto-login
    ansible.windows.win_regedit:
      path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
      name: DefaultDomainName
      type: string
      data: "WORKGROUP"
      state: present
    ignore_errors: yes

  - name: Disable lock screen
    ansible.windows.win_regedit:
     path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization
     name: NoLockScreen
     type: dword
     data: 1

  - name: Disable password on wake/resume
    ansible.windows.win_regedit:
     path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI
     name: DisableAcrylicBackgroundOnLogon
     type: dword
     data: 1

  - name: Disable Windows update
    win_service:
      name: Windows update
      start_mode: disabled
      state: stopped

  - name: Download TightVNC MSI
    ansible.windows.win_get_url:
      url: http://172.16.24.250/tightvnc-2.8.81-gpl-setup-64bit.msi
      dest: C:\Windows\Temp\tightvnc-setup.msi

  - name: Install TightVNC
    ansible.windows.win_package:
      path: C:\Windows\Temp\tightvnc-setup.msi
      arguments: /quiet
      state: present

  - name: Allow TightVNC connections with no password
    ansible.windows.win_regedit:
      path: HKLM:\SOFTWARE\TightVNC\Server
      name: UseVncAuthentication
      data: 0
      type: dword

  - name: Ensure TightVNC allows multiple connections
    ansible.windows.win_regedit:
      path: HKLM:\SOFTWARE\TightVNC\Server
      name: AlwaysShared
      data: 1
      type: dword

  - name: TightVNC
    win_service:
      name: TightVNC Server
      start_mode: auto
      state: started

  - name: Copy flag file
    copy:
      dest: C:\Users\Administrator\Desktop\FLAG.txt
      content: |
        FLAG{46e77971a498e89b5ac767dbfe0edadf06ddb4d3}

  - name: Reboot the system
    win_reboot:
      reboot_timeout: 600

N2-DEMETER

A Windows host running Tomcat that can be exploited to gain administrative access to the host.

- name: N2-DEMETER - Tomcat Install
  hosts: N2-DEMETER
  gather_facts: yes
  vars:
    java_installer_url: http://172.16.24.250/OpenJDK8U-jdk_x64_windows_hotspot_8u402b06.msi
      #https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u402-b06/OpenJDK8U-jdk_x64_windows_hotspot_8u402b06.msi
    java_installer_path: C:\Temp\OpenJDK8.msi
    java_install_dir: 'C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot'
    tomcat_version: 8.0.24
    tomcat_zip_url: http://172.16.24.250/apache-tomcat-8.0.24-windows-x64.zip
      #https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.24/bin/apache-tomcat-8.0.24-windows-x64.zip
    install_dir: 'C:\Tomcat'
    unzip_dir: 'C:\Tomcat\apache-tomcat-8.0.24'

  tasks:
    - name: Set configuration file path
      set_fact:
        config_file_path: '../FILES/N2-DEMETER/'

    - name: Add a static route back to Network One
      ansible.windows.win_command: route add -p 192.168.24.0 mask 255.255.255.0 172.16.24.7

    - name: Disable Windows Defender Real-Time Protection via registry
      win_regedit:
        path: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection
        name: DisableRealtimeMonitoring
        data: 1
        type: dword
        state: present

    - name: Exclude C:\ from Windows Defender scanning
      win_shell: "Add-MpPreference -ExclusionPath C:"

    - name: Ensure C:\Temp exists
      ansible.windows.win_file:
        path: C:\Temp
        state: directory

    - name: Disable Windows update
      win_service:
        name: Windows update
        start_mode: disabled
        state: stopped

    - name: Download Java JDK 8 installer
      ansible.windows.win_get_url:
        url: "{{ java_installer_url }}"
        dest: "{{ java_installer_path }}"

    - name: Install Java JDK 8 silently
      ansible.windows.win_package:
        path: "{{ java_installer_path }}"
        arguments: INSTALL_SILENT=Enable
        product_id: ''
        state: present

    - name: Set JAVA_HOME system environment variable
      ansible.windows.win_environment:
        name: JAVA_HOME
        value: "{{ java_install_dir }}"
        level: machine
        state: present

    - name: Ensure Tomcat install directory exists
      ansible.windows.win_file:
        path: "{{ install_dir }}"
        state: directory

    - name: Download Tomcat ZIP
      ansible.windows.win_get_url:
        url: "{{ tomcat_zip_url }}"
        dest: "{{ install_dir }}\\tomcat.zip"

    - name: Unzip Tomcat
      win_unzip:
        src: "{{ install_dir }}\\tomcat.zip"
        dest: "{{ install_dir }}"
        remote_src: yes
      ignore_errors: yes

    - name: Configure tomcat-users.xml
      win_copy:
        content: |
          <tomcat-users xmlns="http://tomcat.apache.org/xml"
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
                    version="1.0">
            <user username="tomcat" password="tomcat" roles="manager-gui,manager-script"/>
          </tomcat-users>
        dest: "{{ unzip_dir }}\\conf\\tomcat-users.xml"


    - name: Install Tomcat as a Windows Service
      ansible.windows.win_shell: |
        set "JAVA_HOME={{ java_install_dir }}" && set "CATALINA_HOME={{ unzip_dir }}" && "{{ unzip_dir }}\\bin\\service.bat" install
      args:
        executable: cmd
      register: tomcat_service_install

    - name: Show Tomcat service install output
      debug:
        var: tomcat_service_install.stdout_lines

    - name: Start Tomcat service
      ansible.windows.win_service:
        name: Tomcat8
        state: started
        start_mode: auto

    - name: Copy flag file
      copy: 
        dest: C:\Users\Administrator\Desktop\FLAG.txt
        content: | 
          FLAG{a3d32a92a8b7ea9dce971974e954786ed7a684ba}

N2-HADES

This is a Linux system running MySQL. The root user can login to MySQL remotely, to extract the password for the ‘alice’ user account, which is an administrator.

- hosts: N2-HADES
  user:  root
  become: true
  tasks: 
  - name: Set configuration file path
    set_fact:
      config_file_path: '../FILES/N2-HADES/'

  - name: Add new default gateway
    ansible.builtin.command: ip route add default via 172.16.24.1 dev eth0
    ignore_errors: yes

  - name: Update apt package cache
    ansible.builtin.apt:
      update_cache: yes
    async: 60
    poll: 10

  - name: install python3 SQL
    apt: pkg=python3-pymysql state=present

  - name: install MariaDB
    apt: pkg=mariadb-server state=present

  - name: Copy MariaDB configuration
    copy:
      dest: /etc/mysql/mariadb.conf.d/50-server.cnf
      content: |
        [server]

        [mysqld]
        pid-file                = /run/mysqld/mysqld.pid
        basedir                 = /usr
        bind-address            = 0.0.0.0
        expire_logs_days        = 10
        character-set-server  = utf8mb4
        collation-server      = utf8mb4_general_ci
        [embedded]
        [mariadb]
        [mariadb-10.6]

  - name: Restart MariaDB
    service:
     name: mariadb
     state: restarted

  - name: Set root password and switch auth plugin to mysql_native_password
    ansible.builtin.shell: |
      mysql -u root <<EOF
      SET PASSWORD FOR 'root'@'localhost' = PASSWORD('MySuperSecureRootPW!');
      FLUSH PRIVILEGES;
      EOF
    args:
      executable: /bin/bash

  - name: Create /root/.my.cnf with empty password for MySQL root
    ansible.builtin.copy:
     dest: /root/.my.cnf
     content: |
       [client]
       user=root
       password=MySuperSecureRootPW!
     owner: root
     group: root
     mode: '0600'

  - name: Create database 'users'
    community.mysql.mysql_db:
      name: users
      state: present

  - name: Create user 'alice' with password
    mysql_user:
      name: alice
      password: "DownTheRabbitHole..."
      host: '%'
      state: present

  - name: Create user 'alice'
    user:
      name: alice
      password: "{{ 'DownTheRabbitHole...' | password_hash('sha512') }}"
      state: present
      shell: /bin/bash
      groups: sudo

  - name: Grant privileges to 'alice' on 'users' database
    mysql_user:
      name: alice
      host: '%'
      priv: "users.*:SELECT,INSERT,UPDATE,DELETE,CREATE,INDEX,DROP,ALTER,CREATE TEMPORARY TABLES,LOCK TABLES"
      state: present

  - name: Allow remote access to MySQL root without password
    mysql_user:
      name: root
      password: ""
      host: '%'
      state: present
      priv: "*.*:ALL"

  - name: Create users table
    mysql_query:
      query: |
        CREATE TABLE IF NOT EXISTS users (
          userid INT(11) NOT NULL AUTO_INCREMENT,
          username VARCHAR(150) NOT NULL,
          password VARCHAR(150) NOT NULL,
          PRIMARY KEY (userid)
        );
      login_db: users

  - name: Insert 'alice' into 'users' table
    mysql_query:
      query: |
        INSERT INTO users (username, password)
        VALUES ('alice', 'DownTheRabbitHole...')
      login_db: users        

  - name: Insert 'charlie' into 'users' table
    mysql_query:
      query: |
        INSERT INTO users (username, password) 
        VALUES ('charlie', 'TiCiaMyEaTeN')
      login_db: users

  - name: Insert 'bob' into 'users' table
    mysql_query:
      query: |
        INSERT INTO users (username, password) 
        VALUES ('bob', 'cHianTAlAiNO')
      login_db: users

  - name: Copy FLAG
    copy:
      dest: /root/FLAG.txt
      content: |
        FLAG{59e653e13ca373f0bc05b0b449fbd6427c9e0f53}

N2-HERMES

Another Linux host, this time hosting credentials on an FTP server.

- hosts: N2-HERMES
  user:  root
  become: true
  tasks: 
  - name: Set configuration file path
    set_fact:
      config_file_path: '../FILES/N2-HERMES'

  - name: Add new default gateway
    ansible.builtin.command: ip route add default via 172.16.24.1 dev eth0
    ignore_errors: yes

  - name: install vsftpd
    apt: pkg=vsftpd state=present
  - name: install samba
    apt: pkg=samba state=present
  - name: install postfix
    apt: pkg=postfix state=present

  - name: Copy VSFTP configuration
    copy:
      dest: /etc/vsftpd.conf
      content: |
        listen=NO
        listen_ipv6=YES
        anonymous_enable=YES
        local_enable=YES
        dirmessage_enable=YES
        use_localtime=YES
        xferlog_enable=YES
        connect_from_port_20=YES
        secure_chroot_dir=/var/run/vsftpd/empty
        pam_service_name=vsftpd
        rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
        rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
        ssl_enable=NO

  - name: Create user 'bob'
    user:
      name: bob
      password: "{{ 'Secret123' | password_hash('sha512') }}"
      state: present
      shell: /bin/bash
      groups: sudo

  - name: Copy credentials
    copy:
      dest: /srv/ftp/creds.txt
      content: |
        bob:Secret123

  - name: Restart vsftpd daemon
    service:
      name: vsftpd
      state: restarted
  - name: Restart samba daemon
    service:
      name: smbd
      state: restarted
  - name: Restart postfix daemon
    service:
      name: postfix
      state: restarted


N3-PROMETHEUS

This is a Linux host that leaks a MD5 hashed password using the finger daemon. For privilege escalation, we’re using a custom SetUID binary that can read arbitrary files.

 #include <stdio.h>
#include <iostream>
#include <fstream>

// g++ read_config.c -o read_config

int main(int argc, char* argv[])
{
   if (argc == 1)
   {
      printf("Usage ./read_config <configuration_filename>\n");
   }
   if (argc == 2)
   {
        printf("Using configuration file: %s\n",argv[1]);
        std::string myText;
        std::ifstream MyReadFile(argv[1]);
        while (getline (MyReadFile, myText)) {
                std::cout << myText;
        }
        MyReadFile.close();
   }

    return 0;
}
- name: Configure N3-PROMETHEUS
  hosts: N3-PROMETHEUS
  become: true
  tasks:

    - name: Add new default gateway
      ansible.builtin.command: ip route add default via 10.0.24.1 dev eth0
      ignore_errors: yes

    - name: Ensure required packages are installed
      apt:
        name:
          - finger
          - fingerd
          - openbsd-inetd
        state: present
        update_cache: yes

    - name: Enable finger service in /etc/inetd.conf
      lineinfile:
        path: /etc/inetd.conf
        regexp: '^finger\s+stream'
        line: 'finger  stream  tcp  nowait  nobody  /usr/sbin/tcpd  /usr/sbin/in.fingerd'
        create: yes
        state: present

    - name: Ensure inetd is enabled and restarted
      systemd:
        name: openbsd-inetd
        enabled: true
        state: restarted

    - name: Ensure user exists
      user:
        name: prometheus
        shell: /bin/bash
        state: present
        create_home: yes
        password: "{{ 'Password1' | password_hash('sha512') }}"

    - name: Ensure finger can read the plan file
      ansible.builtin.shell: |
        chmod a+x /home/prometheus/

    - name: Create a .plan file for the user
      copy:
        dest: /home/prometheus/.plan
        content: |
          MD5:2ac9cb7dc02b3c0083eb70898e549b63
        owner: prometheus
        group: prometheus
        mode: '0644'

    - name: Check if tmux session for prometheus exists
      ansible.builtin.shell: "tmux has-session -t prometheus_session 2>/dev/null"
      register: tmux_session_exists
      failed_when: false
      ignore_errors: true
      become: true

    - name: Ensure prometheus has a tmux session
      ansible.builtin.shell: |
        su - prometheus -c "tmux new-session -d -s prometheus_session 'whoami; sleep 36000'"
      when: tmux_session_exists.rc != 0
      become: true
      ignore_errors: true

    - name: Attach tmux session for user
      ansible.builtin.shell: "su - prometheus -c 'tmux attach -t prometheus_session'"
      when: tmux_session_exists.rc == 0
      become: true

    - name: Ensure tmux session starts on reboot
      cron:
        name: 'Start tmux session for prometheus'
        user: prometheus
        special_time: 'reboot'
        job: 'tmux new-session -d -s prometheus_session "whoami; sleep 36000"'

    - name: Download read_config to /sbin/
      get_url:
        url: "http://10.0.24.250/read_config"
        dest: "/sbin/read_config"
        mode: '4755' # Set SUID root

    - name: Create /root/FLAG.txt
      copy:
        dest: /root/FLAG.txt
        content: "FLAG{9de37a38ab55a917a70cbf4adf4ce2f45c147c08}"

Host Configuration

Once all the Ansible configuration files have been created, we can use a bash script (configure_lab.sh) to configure all the hosts at once.

export ANSIBLE_HOST_KEY_CHECKING=False
#Network 1
ansible-playbook -i ./inventory.py CONFIGS/N1-ZEUS.yaml
ansible-playbook -i ./inventory.py CONFIGS/N1-HERA.yaml
ansible-playbook -i ./inventory.py CONFIGS/N1-ARES.yaml
ansible-playbook -i ./inventory.py CONFIGS/N1-AEOLUS.yaml

#Network 2
ansible-playbook -i ./inventory.py CONFIGS/N2-APOLLO.yaml
ansible-playbook -i ./inventory.py CONFIGS/N2-DEMETER.yaml
ansible-playbook -i ./inventory.py CONFIGS/N2-HADES.yaml
ansible-playbook -i ./inventory.py CONFIGS/N2-HERMES.yaml

#Network 3
ansible-playbook -i ./inventory.py CONFIGS/N3-PROMETHEUS.yaml

In Conclusion

At this stage, we have Ansible scripts to add vulnerabilities to the target hosts. Next, we need to work on improving the amount of randomisation in the CTF.