diff --git a/.gitignore b/.gitignore index f1c4ea203..9d6cc2acc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,14 @@ testing/unit/services/output/ make/DEBIAN/postinst testrun.log + +# Ignore vagrant files +test_vm/.vagrant/ + +# Ingnore TLS test certificates +test_vm/certs/ +test_vm/*.cnf +test_vm/*.csr +test_vm/*.pem +test_vm/*.key +test_vm/*.crt \ No newline at end of file diff --git a/test_vm/README.MD b/test_vm/README.MD index 9a69f91e1..4ab0b130e 100644 --- a/test_vm/README.MD +++ b/test_vm/README.MD @@ -8,10 +8,14 @@ This guide will help you set up a Vagrant VM with two network interfaces: ## 1. Install Required Packages and Dependencies + + ```bash +sudo wget -O- https://apt.releases.hashicorp.com/gpg | sudo apt-key add - +sudo add-apt-repository -y "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" sudo apt-get update -sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virt-manager \ - ruby ruby-dev libvirt-dev zlib1g-dev ebtables dnsmasq-base vagrant +sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils \ + ruby ruby-dev libvirt-dev zlib1g-dev ebtables dnsmasq-base vagrant ansible ``` @@ -47,7 +51,13 @@ sudo ip link delete enbr99 type bridge ## 6. Create an Isolated libvirt Network Without IP and DHCP -### Create the file /tmp/hostonly-noip.xml: +### Create the file /tmp/hostonly-noip.xml + +```bash +nano /tmp/hostonly-noip.xml +``` + +file content ```xml @@ -60,6 +70,8 @@ sudo ip link delete enbr99 type bridge ### Create and start the network: ```bash +sudo virsh net-destroy hostonly-noip || true +sudo virsh net-undefine hostonly-noip || true sudo virsh net-define /tmp/hostonly-noip.xml sudo virsh net-start hostonly-noip sudo virsh net-autostart hostonly-noip @@ -76,8 +88,46 @@ You should see hostonly-noip with status active. ## 8. Start the Virtual Machine +COMPLIANT DEVICE + +```bash +./mock_ethtool.sh +hash -r +DEVICE=COMPLIANT vagrant up --provider=libvirt +DEVICE=COMPLIANT vagrant provision +./create_certificate.sh +``` + + +NON-COMPLIANT DEVICE (WITHOUT TLS) + +```bash +./restore_ethtool.sh +hash -r +DEVICE=NON-COMPLIANT vagrant up --provider=libvirt +DEVICE=NON-COMPLIANT vagrant provision +``` + +NON-COMPLIANT DEVICE (TLS ONLY) ```bash -vagrant up --provider=libvirt +./restore_ethtool.sh +hash -r +DEVICE=NON-COMPLIANT-TLS vagrant up --provider=libvirt +DEVICE=NON-COMPLIANT-TLS vagrant provision ``` -If you see "Permission denied to /var/run/libvirt/libvirt-sock", check your libvirt group membership and restart your session. \ No newline at end of file + +NOT-DETECTED DEVICE + +```bash +./mock_ethtool.sh +hash -r +DEVICE=NOT-DETECTED vagrant up --provider=libvirt +DEVICE=NOT-DETECTED vagrant provision +``` + +If you see "Permission denied to /var/run/libvirt/libvirt-sock", check your libvirt group membership and restart your session. + + +## 9. Start Testrun + diff --git a/test_vm/Vagrantfile b/test_vm/Vagrantfile index da820d783..8d01960f4 100644 --- a/test_vm/Vagrantfile +++ b/test_vm/Vagrantfile @@ -1,18 +1,55 @@ Vagrant.configure("2") do |config| - config.vm.box = "bento/ubuntu-24.04" + config.vm.box = "cloud-image/ubuntu-24.04" + config.vm.box_version = "20251126.0.0" config.vm.hostname = "ubuntu-vm" - config.vm.network "private_network", + if ENV['DEVICE'] == 'COMPLIANT' + config.vm.network "private_network", libvirt__network_name: "hostonly-noip", auto_config: false, - mac: "52:54:00:12:34:56" + mac: "f0:d4:e2:f2:f5:41" + end + if ENV['DEVICE'] == 'NON-COMPLIANT-TLS' + config.vm.network "private_network", + libvirt__network_name: "hostonly-noip", + auto_config: false, + mac: "f0:d4:e2:f2:f5:42" + end + if ENV['DEVICE'] == 'NON-COMPLIANT' + config.vm.network "private_network", + libvirt__network_name: "hostonly-noip", + auto_config: false, + mac: "54:53:52:51:50:49" + end + if ENV['DEVICE'] == 'NOT-DETECTED' + config.vm.network "private_network", + libvirt__network_name: "hostonly-noip", + auto_config: false, + mac: "f0:d4:e2:f2:f5:43" + end + config.vm.provider :libvirt do |libvirt| libvirt.memory = 2048 libvirt.cpus = 2 libvirt.graphics_type = "none" end - if ENV['USE_ANSIBLE'] == '1' + if ENV['DEVICE'] == 'COMPLIANT' + config.vm.provision "ansible" do |ansible| + ansible.playbook = "provision-compliant.yml" + end + end + if ENV['DEVICE'] == 'NON-COMPLIANT' + config.vm.provision "ansible" do |ansible| + ansible.playbook = "provision-non-compliant.yml" + end + end + if ENV['DEVICE'] == 'NON-COMPLIANT-TLS' + config.vm.provision "ansible" do |ansible| + ansible.playbook = "provision-non-compliant-tls.yml" + end + end + if ENV['DEVICE'] == 'NOT-DETECTED' config.vm.provision "ansible" do |ansible| - ansible.playbook = "provision.yml" + ansible.playbook = "provision-not-detected.yml" end end end diff --git a/test_vm/create_cert_chain.sh b/test_vm/create_cert_chain.sh new file mode 100755 index 000000000..a9107ee7d --- /dev/null +++ b/test_vm/create_cert_chain.sh @@ -0,0 +1,135 @@ +#!/bin/bash +set -e + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +VM_USER=vagrant +VM_PASS=vagrant +VM_IP="$1" +SSHPASS="sshpass -p $VM_PASS" + +BASE_DIR="$(pwd)" +WORKDIR="$BASE_DIR/multi_ca_demo" +CERTS_DIR="$BASE_DIR/certs" +NGINX_CN="$VM_IP" + +mkdir -p "$WORKDIR" +mkdir -p "$CERTS_DIR" +cd "$WORKDIR" + +# Create CA extension config for intermediates +cat > ca_ext.cnf < openssl_nginx.cnf < nginx_fullchain.pem + +echo "All certificates generated." + +# 7. Copy certs and key to VM +$SSHPASS scp -o StrictHostKeyChecking=no nginx.key ${VM_USER}@${VM_IP}:/home/vagrant/nginx.key +$SSHPASS scp -o StrictHostKeyChecking=no nginx_fullchain.pem ${VM_USER}@${VM_IP}:/home/vagrant/nginx_fullchain.pem +$SSHPASS scp -o StrictHostKeyChecking=no int2CA.pem ${VM_USER}@${VM_IP}:/home/vagrant/int2CA.pem +$SSHPASS scp -o StrictHostKeyChecking=no int1CA.pem ${VM_USER}@${VM_IP}:/home/vagrant/int1CA.pem +$SSHPASS scp -o StrictHostKeyChecking=no rootCA.pem ${VM_USER}@${VM_IP}:/home/vagrant/rootCA.pem + +# 8. Move certs to correct locations and configure nginx on VM +$SSHPASS ssh -o StrictHostKeyChecking=no ${VM_USER}@${VM_IP} "sudo mv /home/vagrant/nginx.key /etc/ssl/private/nginx.key" +$SSHPASS ssh -o StrictHostKeyChecking=no ${VM_USER}@${VM_IP} "sudo mv /home/vagrant/nginx_fullchain.pem /etc/ssl/certs/nginx_fullchain.pem" +$SSHPASS ssh -o StrictHostKeyChecking=no ${VM_USER}@${VM_IP} "sudo mv /home/vagrant/int2CA.pem /etc/ssl/certs/int2CA.pem" +$SSHPASS ssh -o StrictHostKeyChecking=no ${VM_USER}@${VM_IP} "sudo mv /home/vagrant/int1CA.pem /etc/ssl/certs/int1CA.pem" +$SSHPASS ssh -o StrictHostKeyChecking=no ${VM_USER}@${VM_IP} "sudo mv /home/vagrant/rootCA.pem /etc/ssl/certs/rootCA.pem" + +$SSHPASS ssh -o StrictHostKeyChecking=no ${VM_USER}@${VM_IP} "sudo bash -c 'cat > /etc/nginx/sites-available/default <" + exit 1 +fi + +# Save the initial working directory +WORKDIR="$(pwd)" + +# Check and install sshpass if not present +if ! command -v sshpass &> /dev/null; then + echo "sshpass not found. Installing..." + if [ -x "$(command -v apt)" ]; then + sudo apt update + sudo apt install -y sshpass + elif [ -x "$(command -v yum)" ]; then + sudo yum install -y epel-release + sudo yum install -y sshpass + elif [ -x "$(command -v brew)" ]; then + brew install hudochenkov/sshpass/sshpass + else + echo "Please install sshpass manually." + exit 1 + fi +fi + +VM_USER=vagrant +VM_PASS=vagrant +VM_IP="$1" +CA_DIR=~/myCA + +SSHPASS="sshpass -p $VM_PASS" + +# 1. Generate key and CSR on VM via ssh +$SSHPASS ssh -o StrictHostKeyChecking=no ${VM_USER}@${VM_IP} "cat > /home/vagrant/openssl_ip.cnf" < /etc/nginx/sites-available/default < /dev/null <<'EOF' +#!/bin/bash +if [[ "$1" == "enbr99" ]]; then + cat < + openssl req -x509 -nodes -days 365 -newkey rsa:2048 + -keyout {{ ssl_key }} -out {{ ssl_cert }} + -subj "/CN=localhost" + args: + creates: "{{ ssl_cert }}" + + - name: Create nginx HTTPS config + copy: + dest: /etc/nginx/sites-available/ssl_default + content: | + server { + listen 443 ssl; + server_name _; + ssl_certificate {{ ssl_cert }}; + ssl_certificate_key {{ ssl_key }}; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + location / { + return 200 'nginx HTTPS is up\n'; + } + } + server { + listen 80; + server_name _; + location / { + return 200 'nginx HTTP is up\n'; + } + } + + - name: Enable nginx HTTPS config + file: + src: /etc/nginx/sites-available/ssl_default + dest: /etc/nginx/sites-enabled/ssl_default + state: link + force: yes + + - name: Remove default nginx config (if exists) + file: + path: /etc/nginx/sites-enabled/default + state: absent + + - name: Ensure nginx is started and enabled + systemd: + name: nginx + state: restarted + enabled: yes + + + - name: Copy fail_tls_handshake.sh script + copy: + dest: "{{ fail_tls_script }}" + mode: '0755' + content: | + #!/bin/bash + iface="{{ iface }}" + ip_addr="{{ bind_ip }}" + host="{{ target_host }}" + port="{{ target_port }}" + LOG="/tmp/tls_test.log" + echo "=== $(date) ===" >> $LOG + echo "IP on $iface:" >> $LOG + ip -4 addr show dev $iface >> $LOG + echo "Routing table:" >> $LOG + ip route >> $LOG + # Wait for IP to appear on ens5 + for i in {1..60}; do + if ip -4 addr show dev $iface | grep -q "$ip_addr"; then + break + fi + sleep 2 + done + if ! ip -4 addr show dev $iface | grep -q "$ip_addr"; then + echo "No IP $ip_addr on $iface" >> $LOG + exit 1 + fi + # TLS 1.2 + echo "Running TLS 1.2 handshake..." >> $LOG + openssl s_client -connect $host:$port -tls1_2 -cipher "CAMELLIA256-SHA256" -bind $ip_addr -ign_eof < /dev/null >> $LOG 2>&1 + + - name: Create systemd service for failing TLS handshake + copy: + dest: /etc/systemd/system/fail-tls-handshake.service + content: | + [Unit] + Description=Fail TLS handshake via ens5 from {{ bind_ip }} + + [Service] + Type=oneshot + ExecStart={{ fail_tls_script }} + + - name: Create systemd timer for failing TLS handshake + copy: + dest: /etc/systemd/system/fail-tls-handshake.timer + content: | + [Unit] + Description=Run fail TLS handshake every 2 seconds + + [Timer] + OnBootSec=5 + OnUnitActiveSec=2 + + [Install] + WantedBy=timers.target + + - name: Reload systemd + shell: systemctl daemon-reload + + - name: Enable and start fail-tls-handshake.timer + systemd: + name: fail-tls-handshake.timer + enabled: yes + state: started + + # BLOCK ICMP + + - name: Ensure UFW is installed + apt: + name: ufw + state: present + + - name: Set UFW default policy to deny incoming + command: ufw default deny incoming + + - name: Set UFW default policy to allow outgoing + command: ufw default allow outgoing + + - name: Allow SSH + command: ufw allow 22/tcp + + - name: Allow HTTP + command: ufw allow 80/tcp + + - name: Allow SNMP + command: ufw allow 443/tcp + + - name: Reload UFW + command: ufw reload diff --git a/test_vm/provision-non-compliant.yml b/test_vm/provision-non-compliant.yml new file mode 100644 index 000000000..e3bc8a446 --- /dev/null +++ b/test_vm/provision-non-compliant.yml @@ -0,0 +1,698 @@ +--- +- hosts: all + become: true + vars: + dhcp_iface: ens5 + dhcp_subnet: "10.10.10.0" + dhcp_netmask: "255.255.255.0" + dhcp_range_start: "10.10.10.100" + dhcp_range_end: "10.10.10.200" + dhcp_router: "10.10.10.1" + dhcp_dns: "8.8.8.8, 8.8.4.4" + device_mac: "54:53:52:51:50:49" + wrong_ip: "10.10.10.99" + target_ip: "10.10.10.1" + iface: "ens5" + vars: + scapy_venv_path: /opt/scapy-venv + ntp_script_path: /usr/local/bin/send_ntp_both.py + ntp_service_path: /etc/systemd/system/ntp-both-sender.service + dst_mac: "ff:ff:ff:ff:ff:ff" + dhcp_ntp_ip: "10.10.10.5" + other_ntp_ip: "8.8.8.8" + + tasks: + + # Common setup tasks + + - name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + ignore_errors: yes + timeout: 100 + + - name: Install Python3, pip, venv, and netcat + apt: + name: + - python3 + - python3-pip + - python3-venv + - libcap2-bin + - net-tools + - netcat-openbsd + - isc-dhcp-client + state: present + + # Set up networking on ens5 + + - name: Run dhclient on ens5 (in background) + shell: nohup dhclient ens5 & + async: 0 + poll: 0 + + # NTP + + - name: Create Python venv for scapy + command: python3 -m venv {{ scapy_venv_path }} + args: + creates: "{{ scapy_venv_path }}/bin/activate" + changed_when: false + + - name: Install scapy in venv + command: "{{ scapy_venv_path }}/bin/pip install scapy" + changed_when: false + + - name: Copy NTP sender script to VM (dynamic src_ip, both servers) + copy: + dest: "{{ ntp_script_path }}" + mode: '0755' + content: | + from scapy.all import Ether, IP, UDP, NTP, sendp, get_if_addr + import time + + iface = "{{ iface }}" + src_mac = "{{ device_mac }}" + dst_mac = "{{ dst_mac }}" + dhcp_ntp_ip = "{{ dhcp_ntp_ip }}" + other_ntp_ip = "{{ other_ntp_ip }}" + + while True: + src_ip = get_if_addr(iface) + # Send to DHCP-provided NTP server + pkt1 = Ether(src=src_mac, dst=dst_mac) / \ + IP(src=src_ip, dst=dhcp_ntp_ip) / \ + UDP(sport=123, dport=123) / \ + NTP(version=3) + sendp(pkt1, iface=iface, verbose=False) + print(f"Sent NTPv4 packet to DHCP NTP server {dhcp_ntp_ip} from {src_ip}") + + # Send to non-DHCP NTP server + pkt2 = Ether(src=src_mac, dst=dst_mac) / \ + IP(src=src_ip, dst=other_ntp_ip) / \ + UDP(sport=123, dport=123) / \ + NTP(version=3) + sendp(pkt2, iface=iface, verbose=False) + print(f"Sent NTPv4 packet to non-DHCP NTP server {other_ntp_ip} from {src_ip}") + + time.sleep(1) + + - name: Create systemd unit for continuous NTP both sender + copy: + dest: "{{ ntp_service_path }}" + mode: '0644' + content: | + [Unit] + Description=Continuous NTP Packet Sender (DHCP and non-DHCP) + After=network.target + + [Service] + Type=simple + ExecStart={{ scapy_venv_path }}/bin/python {{ ntp_script_path }} + Restart=always + + [Install] + WantedBy=multi-user.target + + - name: Reload systemd for NTP both sender + systemd: + daemon_reload: yes + + - name: Enable and start NTP both sender service + systemd: + name: ntp-both-sender + enabled: yes + state: started + + - name: Assign temporary static IP to ens5 for DHCP server startup + command: ip addr add 10.10.10.14/24 dev ens5 + ignore_errors: yes + + # Custom Modbus TCP server NON-COMPLIANT + + - name: Copy Modbus TCP server script (pyModbusTCP) + copy: + src: ./python/src/modbus_server_non_compliant.py + dest: /home/vagrant/modbus_server_non_compliant.py + owner: vagrant + group: vagrant + mode: '0644' + + - name: Create systemd unit for custom Modbus server + copy: + dest: /etc/systemd/system/custom-modbus-server.service + mode: '0644' + content: | + [Unit] + Description=Custom Modbus TCP Error Server + After=network.target + + [Service] + Type=simple + ExecStart=/usr/bin/python3 /home/vagrant/modbus_server_non_compliant.py + Restart=always + + [Install] + WantedBy=multi-user.target + + - name: Reload systemd + systemd: + daemon_reload: yes + + - name: Enable and start custom Modbus server + systemd: + name: custom-modbus-server + enabled: yes + state: started + + - name: Show custom Modbus server status + systemd: + name: custom-modbus-server + register: modbus_status + changed_when: false + + # FTP server + + - name: Kill old fake FTP netcat processes + shell: pkill -f 'nc -l -s .* -p 21' || true + + ignore_errors: yes + - name: Copy fake FTP netcat script + copy: + dest: /usr/local/bin/fake-ftp-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "220 Fake FTP server ready\r\n" | nc -l -s $ens5_IP -p 21 + done + - name: Run fake FTP netcat script as background process + shell: nohup /usr/local/bin/fake-ftp-netcat-ens5.sh > /var/log/fake-ftp-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-ftp-netcat-ens5.log + + + # SSH server configuration + + - name: Gather facts + setup: + + - name: Set ens4_ip from Ansible facts + set_fact: + ens4_ip: "{{ ansible_ens4.ipv4.address | default('') }}" + + - name: Fail if ens4_ip is not set + fail: + msg: "ens4_ip is not set! Please check network configuration." + when: ens4_ip == '' + + - name: Debug ens4 IP + debug: + msg: "ens4 IP is {{ ens4_ip }}" + + - name: Stop and disable ssh.socket (prevents auto-start on all interfaces) + service: + name: ssh.socket + state: stopped + enabled: no + ignore_errors: yes + + - name: Ensure only ens4 is in ListenAddress for sshd + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^ListenAddress' + line: "ListenAddress {{ ens4_ip }}" + state: present + insertafter: '^#Port' + + - name: Remove all other ListenAddress lines from sshd_config + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^ListenAddress (?!{{ ens4_ip }})' + state: absent + + - name: Restart sshd + service: + name: ssh + state: restarted + + - name: Show what is listening on port 22 + shell: ss -tlnp | grep :22 || netstat -tlnp | grep :22 || true + register: port22 + changed_when: false + + - debug: + var: port22.stdout_lines + + + - name: Create fake SSH banner systemd service for ens5:22 + copy: + dest: /etc/systemd/system/fake-ssh-banner.service + mode: '0644' + content: | + [Unit] + Description=Fake SSH Banner on ens5:22 + After=network-online.target + Wants=network-online.target + + [Service] + Type=simple + ExecStart=/bin/bash -c '\ + while true; do \ + ens5_IP=$(ip -4 addr show ens5 | grep -oP "(?<=inet\s)\d+(\.\d+){3}"); \ + if [[ -n "$ens5_IP" ]]; then \ + echo "ens5 IP detected: $ens5_IP"; \ + break; \ + fi; \ + echo "Waiting for ens5 IP..."; \ + sleep 2; \ + done; \ + while true; do echo -e "SSH-1.99-OpenSSH_1.99\r\n" | nc -l -s $ens5_IP -p 22; done' + Restart=always + + [Install] + WantedBy=multi-user.target + + - name: Reload systemd + systemd: + daemon_reload: yes + + - name: Restart fake-ssh-banner service + systemd: + name: fake-ssh-banner.service + state: restarted + enabled: yes + + # Telnet server setup + + - name: Kill old fake Telnet netcat processes + shell: pkill -f 'nc -l -s .* -p 23' || true + ignore_errors: yes + - name: Copy fake Telnet netcat script + copy: + dest: /usr/local/bin/fake-telnet-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "Fake Telnet server\r\n" | nc -l -s $ens5_IP -p 23 + done + - name: Run fake Telnet netcat script as background process + shell: nohup /usr/local/bin/fake-telnet-netcat-ens5.sh > /var/log/fake-telnet-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-telnet-netcat-ens5.log + + # SMTP (TCP/25) + + - name: Kill old fake SMTP netcat processes + shell: pkill -f 'nc -l -s .* -p 25' || true + ignore_errors: yes + - name: Copy fake SMTP netcat script + copy: + dest: /usr/local/bin/fake-smtp-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "220 fake-smtp ESMTP Postfix\r\n" | nc -l -s $ens5_IP -p 25 + done + - name: Run fake SMTP netcat script as background process + shell: nohup /usr/local/bin/fake-smtp-netcat-ens5.sh > /var/log/fake-smtp-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-smtp-netcat-ens5.log + + # POP3 (TCP/110) + + - name: Kill old fake POP3 netcat processes + + shell: pkill -f 'nc -l -s .* -p 110' || true + ignore_errors: yes + - name: Copy fake POP3 netcat script + copy: + dest: /usr/local/bin/fake-pop3-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "+OK Fake POP3 server ready\r\n" | nc -l -s $ens5_IP -p 110 + done + - name: Run fake POP3 netcat script as background process + shell: nohup /usr/local/bin/fake-pop3-netcat-ens5.sh > /var/log/fake-pop3-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-pop3-netcat-ens5.log + + # IMAP (TCP/143) + + - name: Kill old fake IMAP netcat processes + shell: pkill -f 'nc -l -s .* -p 143' || true + ignore_errors: yes + - name: Copy fake IMAP netcat script + copy: + dest: /usr/local/bin/fake-imap-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "* OK Fake IMAP4rev1 Service Ready\r\n" | nc -l -s $ens5_IP -p 143 + done + - name: Run fake IMAP netcat script as background process + shell: nohup /usr/local/bin/fake-imap-netcat-ens5.sh > /var/log/fake-imap-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-imap-netcat-ens5.log + + # HTTP (TCP/80) + + - name: Kill old fake HTTP netcat processes + shell: pkill -f 'nc -l -s .* -p 80' || true + ignore_errors: yes + - name: Copy fake HTTP netcat script + copy: + dest: /usr/local/bin/fake-http-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK" | nc -l -s $ens5_IP -p 80 + done + - name: Run fake HTTP netcat script as background process + shell: nohup /usr/local/bin/fake-http-netcat-ens5.sh > /var/log/fake-http-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-http-netcat-ens5.log + + # SNMP (UDP/161) + + - name: Kill old fake SNMP netcat processes + shell: pkill -f 'nc -u -l -s .* -p 161' || true + ignore_errors: yes + - name: Copy fake SNMP netcat script + copy: + dest: /usr/local/bin/fake-snmp-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + head -c 48 /dev/zero | nc -u -l -s $ens5_IP -p 161 + done + - name: Run fake SNMP netcat script as background process + shell: nohup /usr/local/bin/fake-snmp-netcat-ens5.sh > /var/log/fake-snmp-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-snmp-netcat-ens5.log + + # VNC (TCP/5901) + + - name: Kill old fake VNC netcat processes + shell: pkill -f 'nc -l -s .* -p 5901' || true + ignore_errors: yes + - name: Copy fake VNC netcat script + copy: + dest: /usr/local/bin/fake-vnc-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "RFB 003.008\n" | nc -l -s $ens5_IP -p 5901 + done + - name: Run fake VNC netcat script as background process + shell: nohup /usr/local/bin/fake-vnc-netcat-ens5.sh > /var/log/fake-vnc-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-vnc-netcat-ens5.log + + # TFTP (UDP/69) + + - name: Kill old fake TFTP netcat processes + shell: pkill -f 'nc -u -l -s .* -p 69' || true + ignore_errors: yes + - name: Copy fake TFTP netcat script + copy: + dest: /usr/local/bin/fake-tftp-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + echo -ne "\x00\x05\x00\x01File not found\x00" | nc -u -l -s $ens5_IP -p 69 + done + - name: Run fake TFTP netcat script as background process + shell: nohup /usr/local/bin/fake-tftp-netcat-ens5.sh > /var/log/fake-tftp-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-tftp-netcat-ens5.log + + # NTP (UDP/123) + + - name: Kill old fake NTP netcat processes + shell: pkill -f 'nc -u -l -s .* -p 123' || true + ignore_errors: yes + - name: Copy fake NTP netcat script + copy: + dest: /usr/local/bin/fake-ntp-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then break; fi + sleep 2 + done + while true; do + head -c 48 /dev/zero | nc -u -l -s $ens5_IP -p 123 + done + - name: Run fake NTP netcat script as background process + shell: nohup /usr/local/bin/fake-ntp-netcat-ens5.sh > /var/log/fake-ntp-netcat-ens5.log 2>&1 & + args: + creates: /var/log/fake-ntp-netcat-ens5.log + + # BACnet + + - name: Copy fake BACnet netcat script + copy: + dest: /usr/local/bin/fake-bacnet-netcat-ens5.sh + mode: '0755' + content: | + #!/bin/bash + while true; do + ens5_IP=$(ip -4 addr show ens5 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + if [[ -n "$ens5_IP" ]]; then + echo "ens5 IP detected: $ens5_IP" + break + fi + echo "Waiting for ens5 IP..." + sleep 2 + done + while true; do + echo -ne "Fake BACnet server\n" | nc -u -l -s $ens5_IP -p 47808 + done + + + + + # DHCP + + - name: Install ISC DHCP server + apt: + name: isc-dhcp-server + state: present + update_cache: no + + - name: Configure DHCP server to listen on {{ dhcp_iface }} + lineinfile: + path: /etc/default/isc-dhcp-server + regexp: '^INTERFACESv4=' + line: 'INTERFACESv4="{{ dhcp_iface }}"' + state: present + + - name: Configure DHCP subnet for ens5 + copy: + dest: /etc/dhcp/dhcpd.conf + content: | + default-lease-time 10; + max-lease-time 10; + authoritative; + subnet {{ dhcp_subnet }} netmask {{ dhcp_netmask }} { + range {{ dhcp_range_start }} {{ dhcp_range_end }}; + option routers {{ dhcp_router }}; + option domain-name-servers {{ dhcp_dns }}; + } + + - name: Ensure {{ dhcp_iface }} is up (required for dhcpd to start) + command: ip link set {{ dhcp_iface }} up + + - name: Restart and enable isc-dhcp-server + service: + name: isc-dhcp-server + state: restarted + enabled: yes + + - name: Check DHCP server status + shell: systemctl is-active isc-dhcp-server + register: dhcp_status + + - name: Show DHCP server status + debug: + msg: "DHCP server status: {{ dhcp_status.stdout }}" + + + + # BLOCK ICMP + + - name: Ensure UFW is installed + apt: + name: ufw + state: present + + - name: Set UFW default policy to deny incoming + command: ufw default deny incoming + + - name: Set UFW default policy to allow outgoing + command: ufw default allow outgoing + + - name: Allow FTP + command: ufw allow 21/tcp + + - name: Allow SSH + command: ufw allow 22/tcp + + - name: Allow Telnet + command: ufw allow 23/tcp + + - name: Allow SMTP + command: ufw allow 25/tcp + + - name: Allow POP3 + command: ufw allow 110/tcp + + - name: Allow IMAP + command: ufw allow 143/tcp + + - name: Allow HTTP + command: ufw allow 80/tcp + + - name: Allow SNMP + command: ufw allow 161/udp + + - name: Allow VNC + command: ufw allow 5901/tcp + + - name: Allow TFTP + command: ufw allow 69/udp + + - name: Allow NTP + command: ufw allow 123/udp + + - name: Allow BACnet + command: ufw allow 47808/udp + + - name: Allow Modbus (if TCP) + command: ufw allow 502/tcp + + - name: Allow DHCP server port + command: ufw allow 67/udp + + - name: Allow DHCP client port + command: ufw allow 68/udp + + - name: Enable UFW + command: ufw --force enable + + + - name: Reload UFW + command: ufw reload + + - name: Release DHCP lease and flush IP from ens5 + shell: dhclient -r ens5 && ip addr flush dev ens5 + ignore_errors: yes + + - name: Block all ICMP echo requests (ping) using sysctl (IPv4) + sysctl: + name: net.ipv4.icmp_echo_ignore_all + value: 1 + state: present + sysctl_set: yes + reload: yes + + - name: Block ICMPv6 echo-request (ping) on all interfaces (IPv6) + ansible.builtin.iptables: + chain: INPUT + protocol: ipv6-icmp + icmp_type: echo-request + jump: DROP + state: present + ip_version: ipv6 + + - name: Disable IPv6 system-wide via sysctl + sysctl: + name: net.ipv6.conf.all.disable_ipv6 + value: 1 + state: present + sysctl_set: yes + reload: yes + + - name: Disable IPv6 on default interface via sysctl + sysctl: + name: net.ipv6.conf.default.disable_ipv6 + value: 1 + state: present + sysctl_set: yes + reload: yes + + - name: Disable IPv6 on loopback via sysctl + sysctl: + name: net.ipv6.conf.lo.disable_ipv6 + value: 1 + state: present + sysctl_set: yes + reload: yes + + + - name: Remove temporary static IP from ens5 + command: ip addr del 10.10.10.14/24 dev ens5 + ignore_errors: yes + + handlers: + - name: Restart vsftpd + service: + name: vsftpd + state: restarted \ No newline at end of file diff --git a/test_vm/provision-not-detected.yml b/test_vm/provision-not-detected.yml new file mode 100644 index 000000000..4f9f6472c --- /dev/null +++ b/test_vm/provision-not-detected.yml @@ -0,0 +1,82 @@ +--- +- hosts: all + become: true + tasks: + + # Set up networking on ens5 + - name: Install dependencies + apt: + name: + - isc-dhcp-client + - ufw + state: present + update_cache: yes + + - name: Run dhclient on ens5 (in background) + shell: nohup dhclient ens5 & + async: 0 + poll: 0 + + # ARP packet control + - name: Copy arp_control.sh script + copy: + dest: /usr/local/bin/arp_control.sh + mode: '0755' + content: | + #!/bin/bash + iface="ens5" + ip_addr="" + while true; do + ip_addr=$(ip -4 addr show dev $iface | awk '/inet / {print $2}' | cut -d/ -f1) + if [[ -n "$ip_addr" ]]; then + ip link set dev $iface arp off + echo "$(date): IP $ip_addr detected, ARP disabled on $iface" + else + ip link set dev $iface arp on + echo "$(date): No IP on $iface, ARP enabled" + fi + sleep 2 + done + + - name: Run arp_control.sh in background + shell: nohup /usr/local/bin/arp_control.sh & + async: 0 + poll: 0 + + # DNS request setup + + - name: Create systemd service for curl + ansible.builtin.copy: + dest: /etc/systemd/system/curl-google.service + content: | + [Unit] + Description=Curl google.com + + [Service] + Type=oneshot + ExecStart=/usr/bin/curl -s -L -o /dev/null -w "%{http_code}\n" https://www.google.com + + - name: Create systemd timer for curl + ansible.builtin.copy: + dest: /etc/systemd/system/curl-google.timer + content: | + [Unit] + Description=Run curl google.com every 5 seconds + + [Timer] + OnBootSec=5 + OnUnitActiveSec=5 + + [Install] + WantedBy=timers.target + + - name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: yes + + - name: Enable and start curl-google.timer + ansible.builtin.systemd: + name: curl-google.timer + enabled: yes + state: started + diff --git a/test_vm/provision.yml b/test_vm/provision.yml deleted file mode 100644 index ed666c0f1..000000000 --- a/test_vm/provision.yml +++ /dev/null @@ -1,113 +0,0 @@ ---- -- hosts: all - become: true - tasks: - - name: Install DHCP client - apt: - name: isc-dhcp-client - state: present - update_cache: yes - - - name: Run dhclient on eth1 (in background) - shell: nohup dhclient eth1 & - async: 0 - poll: 0 - - - name: Install netstat for network troubleshooting - apt: - name: net-tools - state: present - update_cache: yes - - - name: Install BACnet dependencies - apt: - name: - - git - - gcc - - make - - bison - - flex - - libpcap-dev - state: present - update_cache: yes - tags: bacnet - - - name: Clone BACnet stack repository - git: - repo: https://github.com/bacnet-stack/bacnet-stack.git - dest: /opt/bacnet-stack - version: master - tags: bacnet - - - name: Build BACnet stack - make: - chdir: /opt/bacnet-stack - target: all - tags: bacnet - - - name: Copy bacserv-wrapper.sh - copy: - dest: /usr/local/bin/bacserv-wrapper.sh - mode: '0755' - content: | - #!/bin/bash - IFACE=eth1 - while true; do - IP=$(ip -4 addr show $IFACE | awk '/inet / {print $2}' | cut -d/ -f1) - if [ -n "$IP" ]; then - export BACNET_IP=$IP - exec /opt/bacnet-stack/bin/bacserv - fi - sleep 2 - done - tags: bacnet - - - name: Create systemd unit for bacserv - copy: - dest: /etc/systemd/system/bacserv.service - mode: '0644' - content: | - [Unit] - Description=BACnet Server (auto-restart on IP change) - After=network-online.target - Wants=network-online.target - - [Service] - Type=simple - ExecStart=/usr/local/bin/bacserv-wrapper.sh - Restart=always - RestartSec=5 - - [Install] - WantedBy=multi-user.target - tags: bacnet - - - name: Reload systemd and enable bacserv service - systemd: - name: bacserv - enabled: yes - state: restarted - daemon_reload: yes - tags: bacnet - - - name: Check if bacserv process is running - shell: pgrep -fl bacserv - register: bacserv_process - changed_when: false - tags: bacnet - - - name: Show information about bacserv process - debug: - var: bacserv_process.stdout_lines - tags: bacnet - - - name: Show last lines of bacserv log - shell: tail -n 20 /opt/bacnet-stack/bacserv.log - register: bacserv_log - changed_when: false - ignore_errors: true - - - name: Display bacserv log - debug: - var: bacserv_log.stdout_lines - tags: bacnet \ No newline at end of file diff --git a/test_vm/python/requirements.txt b/test_vm/python/requirements.txt new file mode 100644 index 000000000..8e7a8b454 --- /dev/null +++ b/test_vm/python/requirements.txt @@ -0,0 +1 @@ +pyModbusTCP==0.3.0 \ No newline at end of file diff --git a/test_vm/python/src/modbus_server.py b/test_vm/python/src/modbus_server.py new file mode 100644 index 000000000..cc47e019c --- /dev/null +++ b/test_vm/python/src/modbus_server.py @@ -0,0 +1,20 @@ +"""Modbus TCP server implementation.""" +from pyModbusTCP.server import ModbusServer, DataBank + +if __name__ == "__main__": + # Run server on port 502 + + server = ModbusServer(host="0.0.0.0", port=502, no_block=True) + server.start() + print("Modbus TCP server started on port 502") + + # Initialize data bank with zeros + DataBank.set_words(0, [0]*100) # Holding registers + DataBank.set_bits(0, [0]*100) # Coils + + try: + while True: + pass # Keep the server running + except KeyboardInterrupt: + print("Stopping server...") + server.stop() diff --git a/test_vm/python/src/modbus_server_non_compliant.py b/test_vm/python/src/modbus_server_non_compliant.py new file mode 100644 index 000000000..698d0ec85 --- /dev/null +++ b/test_vm/python/src/modbus_server_non_compliant.py @@ -0,0 +1,60 @@ +""" +test_vm.python.src.modbus_server_non_compliant +""" + +import socket +import struct + + +MODBUS_PORT = 502 +MODBUS_EXCEPTION_CODE = 0x02 # Illegal Data Address + + +def parse_mbap_header(data): + if len(data) < 7: + return None + tid, pid, length, uid = struct.unpack(">HHHB", data[:7]) + return tid, pid, length, uid + + +def build_exception_response(request, function_code, exception_code): + tid, pid, _, uid = parse_mbap_header(request) + resp_fc = function_code | 0x80 + pdu = struct.pack("BB", resp_fc, exception_code) + mbap = struct.pack(">HHHB", tid, pid, len(pdu) + 1, uid) + return mbap + pdu + + +def handle_request(conn, data): + if len(data) < 8: + return + # tid, pid, length, uid = parse_mbap_header(data) + function_code = data[7] + response = build_exception_response( + data, function_code, MODBUS_EXCEPTION_CODE) + conn.sendall(response) + + +def run_server(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("0.0.0.0", MODBUS_PORT)) + sock.listen(5) + print(f"Modbus server started on port {MODBUS_PORT}") + while True: + conn, addr = sock.accept() + print(f"Connection from {addr}") + try: + while True: + data = conn.recv(1024) + if not data: + break + handle_request(conn, data) + except Exception as e: + print(f"Error: {e}") + finally: + conn.close() + + +if __name__ == "__main__": + run_server() diff --git a/test_vm/restore_ethtool.sh b/test_vm/restore_ethtool.sh new file mode 100755 index 000000000..2a65b601f --- /dev/null +++ b/test_vm/restore_ethtool.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +echo "=== Removing mock ethtool from /usr/local/bin (if exists) ===" +if [ -f /usr/local/bin/ethtool ]; then + sudo rm /usr/local/bin/ethtool + echo "Mock ethtool removed." +else + echo "No mock ethtool found in /usr/local/bin." +fi + +echo "=== Resetting Bash command hash ===" +hash -r + +echo "=== Checking which ethtool is now active ===" +which ethtool +type ethtool + +echo "=== Testing ethtool enbr99 ===" +ethtool enbr99 || echo "If you see 'No such file or directory', open a new + terminal and try again." + +echo "=== Done! If you still see the mock error, open a new terminal or run 'hash -r' again. ===" \ No newline at end of file