cai/tools/vm_to_docker.py

699 lines
23 KiB
Python
Executable File

#!/usr/bin/env python3
"""
VM to Docker Converter
Converts virtual machine images (OVA/VMDK) to Docker containers by extracting the actual filesystem
REQUIREMENTS:
- Docker installed and running
- qemu-utils package (for VMDK conversion): sudo apt-get install qemu-utils
- sudo access (for mounting disk images)
- Sufficient disk space (3x the VM size for conversion)
SUPPORTED SYSTEMS:
- Ubuntu 18.04, 20.04, 22.04 LTS
- Debian 10, 11
- Linux Mint (based on Ubuntu)
- Other Linux distributions with apt package manager
DEPENDENCIES TO INSTALL:
sudo apt-get update
sudo apt-get install -y qemu-utils fdisk mount rsync
USAGE:
# Extract actual VM filesystem (requires sudo):
sudo python3 vm_to_docker.py <vm_image.ova> --name <docker_image_name>
# Use template mode (no sudo required, creates generic container):
python3 vm_to_docker.py <vm_image.ova> --name <docker_image_name> --standard
# Test the container after creation:
sudo python3 vm_to_docker.py <vm_image.ova> --name <docker_image_name> --test
EXAMPLE:
sudo python3 /home/acceleration/cai/tools/vm_to_docker.py /tmp/whowantstobeking.ova --name whowantstobeking_full
PROCESS:
1. Extracts OVA archive to find VMDK files
2. Converts VMDK to raw disk format using qemu-img
3. Mounts the raw disk to extract filesystem (requires sudo)
4. Creates Dockerfile that merges VM filesystem with base image
5. Builds Docker image with all original services and files
NOTES:
- CTF mode (default) attempts to extract the actual VM filesystem
- Standard mode uses a template without extracting VM content
- The script falls back to template mode if filesystem extraction fails
- Temporary files are cleaned up automatically after conversion
"""
import os
import sys
import tarfile
import tempfile
import shutil
import subprocess
import argparse
import re
from pathlib import Path
from typing import List, Dict, Optional
import json
class SimpleVMToDockerConverter:
def __init__(self, vm_path: str, output_name: str = None):
self.vm_path = Path(vm_path)
self.output_name = output_name or self.vm_path.stem.lower().replace(' ', '_')
self.work_dir = Path(tempfile.mkdtemp(prefix='vm2docker_'))
self.docker_dir = self.work_dir / 'docker'
self.docker_dir.mkdir(exist_ok=True)
def download_and_extract(self) -> Path:
"""Download and extract OVA file"""
print(f"[*] Processing: {self.vm_path}")
extracted_dir = self.work_dir / 'extracted'
extracted_dir.mkdir(exist_ok=True)
if self.vm_path.suffix.lower() == '.ova':
print("[*] Extracting OVA archive")
with tarfile.open(self.vm_path, 'r') as tar:
tar.extractall(extracted_dir)
# Find VMDK files
vmdk_files = list(extracted_dir.glob('*.vmdk'))
if vmdk_files:
print(f"[+] Found {len(vmdk_files)} VMDK file(s)")
return vmdk_files[0]
return None
def convert_vmdk_to_raw(self, vmdk_path: Path) -> Path:
"""Convert VMDK to raw disk image using qemu-img"""
print(f"[*] Converting VMDK to raw format")
raw_path = self.work_dir / 'disk.raw'
# Check if qemu-img is available
result = subprocess.run(['which', 'qemu-img'], capture_output=True)
if result.returncode != 0:
print("[!] qemu-img not found.")
print("[!] Please install it with: sudo apt-get install qemu-utils")
print("[*] Attempting to use Docker to convert VMDK...")
# Alternative: try using a Docker container to convert
result = subprocess.run([
'docker', 'run', '--rm', '-v', f'{vmdk_path.parent}:/data',
'alpine', 'sh', '-c',
f'apk add --no-cache qemu-img && qemu-img convert -f vmdk -O raw /data/{vmdk_path.name} /data/disk.raw'
], capture_output=True, text=True)
if result.returncode == 0:
# Move the converted file to our work directory
shutil.move(str(vmdk_path.parent / 'disk.raw'), str(raw_path))
print(f"[+] Converted using Docker container")
return raw_path
else:
print(f"[!] Docker conversion failed: {result.stderr}")
return None
# Convert VMDK to raw using local qemu-img
result = subprocess.run(
['qemu-img', 'convert', '-f', 'vmdk', '-O', 'raw', str(vmdk_path), str(raw_path)],
capture_output=True, text=True
)
if result.returncode == 0:
print(f"[+] Converted to raw format: {raw_path}")
return raw_path
else:
print(f"[!] Conversion failed: {result.stderr}")
return None
def mount_and_extract_filesystem(self, raw_path: Path) -> Path:
"""Mount raw disk and extract filesystem"""
print("[*] Extracting filesystem from disk image")
print("[!] NOTE: This requires sudo access to mount the disk image")
fs_dir = self.docker_dir / 'rootfs'
fs_dir.mkdir(exist_ok=True)
try:
# Get partition info
result = subprocess.run(
['fdisk', '-l', str(raw_path)],
capture_output=True, text=True
)
# Parse partition offset
lines = result.stdout.split('\n')
offset = None
for line in lines:
if 'Linux' in line and not 'swap' in line.lower():
parts = line.split()
if len(parts) > 1:
start_sector = int(parts[1])
offset = start_sector * 512 # Default sector size
print(f"[+] Found Linux partition at offset {offset}")
break
if offset is None:
offset = 1048576 # Common default for first partition
print(f"[*] Using default partition offset {offset}")
# Create mount point
mount_point = self.work_dir / 'mount'
mount_point.mkdir(exist_ok=True)
# Mount the image with sudo
print("[*] Mounting disk image (requires sudo)...")
mount_cmd = [
'sudo', 'mount', '-o', f'loop,offset={offset},ro',
str(raw_path), str(mount_point)
]
result = subprocess.run(mount_cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"[+] Mounted filesystem at {mount_point}")
# Copy filesystem to docker directory
print("[*] Copying filesystem (this may take a while)...")
copy_cmd = [
'sudo', 'cp', '-a',
str(mount_point) + '/.', str(fs_dir) + '/'
]
subprocess.run(copy_cmd)
# Fix ownership
subprocess.run(['sudo', 'chown', '-R', f'{os.getuid()}:{os.getgid()}', str(fs_dir)])
# Unmount
subprocess.run(['sudo', 'umount', str(mount_point)], capture_output=True)
print(f"[+] Filesystem extracted to {fs_dir}")
return fs_dir
else:
print(f"[!] Mount failed: {result.stderr}")
print("[!] Please ensure you have sudo access and try again")
return None
except Exception as e:
print(f"[!] Error extracting filesystem: {e}")
import traceback
traceback.print_exc()
return None
def create_dockerfile_from_template(self, detected_os: str = "ubuntu") -> Path:
"""Create a Dockerfile based on detected OS"""
print(f"[*] Creating Dockerfile for {detected_os}-based system")
dockerfile_path = self.docker_dir / 'Dockerfile'
# Base Dockerfile that simulates a VM environment
dockerfile_content = f"""FROM ubuntu:20.04
# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
# Update and install basic services
RUN apt-get update && apt-get install -y \\
openssh-server \\
apache2 \\
mysql-server \\
python3 \\
python3-pip \\
net-tools \\
vim \\
curl \\
wget \\
sudo \\
systemctl \\
&& rm -rf /var/lib/apt/lists/*
# Configure SSH
RUN mkdir /var/run/sshd && \\
echo 'root:password' | chpasswd && \\
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \\
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
# Configure Apache
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
# Create startup script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Expose common ports
EXPOSE 22 80 443 3306
# Set entrypoint
ENTRYPOINT ["/entrypoint.sh"]
"""
# Create entrypoint script
entrypoint_content = """#!/bin/bash
# Start SSH service
service ssh start
# Start Apache
service apache2 start
# Start MySQL
service mysql start
# Keep container running
tail -f /dev/null
"""
# Write files
with open(dockerfile_path, 'w') as f:
f.write(dockerfile_content)
entrypoint_path = self.docker_dir / 'entrypoint.sh'
with open(entrypoint_path, 'w') as f:
f.write(entrypoint_content)
print(f"[+] Dockerfile created at {dockerfile_path}")
return dockerfile_path
def build_docker_image(self) -> bool:
"""Build Docker image"""
print(f"[*] Building Docker image: {self.output_name}")
result = subprocess.run([
'docker', 'build', '-t', self.output_name, '.'
], cwd=self.docker_dir, capture_output=True, text=True)
if result.returncode == 0:
print(f"[+] Docker image built successfully: {self.output_name}")
return True
else:
print(f"[!] Docker build failed: {result.stderr}")
return False
def create_dockerfile_from_filesystem(self, fs_dir: Path) -> Path:
"""Create a Dockerfile that uses the extracted filesystem"""
print("[*] Creating Dockerfile with extracted filesystem")
dockerfile_path = self.docker_dir / 'Dockerfile'
# Analyze the filesystem to determine the base OS
os_release = fs_dir / 'etc/os-release'
base_image = 'ubuntu:20.04' # default
if os_release.exists():
with open(os_release, 'r') as f:
content = f.read()
if 'Ubuntu 18' in content:
base_image = 'ubuntu:18.04'
elif 'Ubuntu 20' in content:
base_image = 'ubuntu:20.04'
elif 'Mint' in content:
# Linux Mint is based on Ubuntu
if '19' in content:
base_image = 'ubuntu:18.04'
else:
base_image = 'ubuntu:20.04'
print(f"[+] Detected OS, using base image: {base_image}")
# Create Dockerfile that copies the entire filesystem
dockerfile_content = f"""FROM {base_image}
# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
# Install essential packages that might be missing
RUN apt-get update && apt-get install -y \\
systemd \\
systemd-sysv \\
sudo \\
net-tools \\
iproute2 \\
rsync \\
&& rm -rf /var/lib/apt/lists/*
# Create a staging directory for the filesystem
RUN mkdir -p /vm_filesystem
# Copy the extracted filesystem to staging
COPY rootfs/ /vm_filesystem/
# Merge the VM filesystem with the container
# This preserves both the base image and VM content
RUN rsync -av --ignore-existing /vm_filesystem/etc/ /etc/ || true && \\
rsync -av --ignore-existing /vm_filesystem/home/ /home/ || true && \\
rsync -av --ignore-existing /vm_filesystem/opt/ /opt/ || true && \\
rsync -av --ignore-existing /vm_filesystem/root/ /root/ || true && \\
rsync -av --ignore-existing /vm_filesystem/srv/ /srv/ || true && \\
rsync -av --ignore-existing /vm_filesystem/var/ /var/ || true && \\
rsync -av --ignore-existing /vm_filesystem/usr/ /usr/ || true && \\
rm -rf /vm_filesystem
# Fix permissions
RUN chmod 755 /root && \\
chmod 644 /etc/passwd /etc/shadow /etc/group || true
# Create necessary directories
RUN mkdir -p /var/run/sshd /run/systemd/system || true
# Create entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Expose common CTF ports
EXPOSE 21 22 23 80 443 631 3000 3306 5432 6379 7654 8080 8181 8888 9000
# Set entrypoint
ENTRYPOINT ["/entrypoint.sh"]
"""
# Create entrypoint that starts services found in the system
entrypoint_content = """#!/bin/bash
# Start services based on what's installed
if [ -f /usr/sbin/sshd ]; then
echo "[*] Starting SSH..."
service ssh start || /usr/sbin/sshd -D &
fi
if [ -f /usr/sbin/apache2 ]; then
echo "[*] Starting Apache..."
service apache2 start || /usr/sbin/apache2ctl start
fi
if [ -f /usr/sbin/nginx ]; then
echo "[*] Starting Nginx..."
service nginx start || /usr/sbin/nginx
fi
if [ -f /usr/bin/mysqld_safe ]; then
echo "[*] Starting MySQL..."
service mysql start || /usr/bin/mysqld_safe &
fi
if [ -f /usr/sbin/vsftpd ]; then
echo "[*] Starting FTP..."
service vsftpd start || /usr/sbin/vsftpd &
fi
# Check for any Node.js apps
if [ -f /usr/bin/node ] || [ -f /usr/bin/nodejs ]; then
# Look for common Node app locations
for app_dir in /var/www /opt /home/*/app; do
if [ -f "$app_dir/package.json" ]; then
echo "[*] Found Node.js app in $app_dir"
cd "$app_dir"
if [ -f "app.js" ]; then
node app.js &
elif [ -f "server.js" ]; then
node server.js &
elif [ -f "index.js" ]; then
node index.js &
fi
fi
done
fi
# Keep container running
echo "[+] All services started. Container ready."
tail -f /dev/null
"""
# Write files
with open(dockerfile_path, 'w') as f:
f.write(dockerfile_content)
entrypoint_path = self.docker_dir / 'entrypoint.sh'
with open(entrypoint_path, 'w') as f:
f.write(entrypoint_content)
print(f"[+] Dockerfile created at {dockerfile_path}")
return dockerfile_path
def create_ctf_dockerfile(self) -> Path:
"""Create a Dockerfile specifically for CTF challenges"""
print("[*] Creating CTF-oriented Dockerfile")
dockerfile_path = self.docker_dir / 'Dockerfile'
# CTF-specific Dockerfile
dockerfile_content = """FROM ubuntu:18.04
# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
# Update and install CTF-relevant services
RUN apt-get update && apt-get install -y \\
openssh-server \\
apache2 \\
php \\
libapache2-mod-php \\
mysql-server \\
python \\
python3 \\
netcat \\
nmap \\
tcpdump \\
vim \\
gcc \\
make \\
gdb \\
net-tools \\
curl \\
wget \\
sudo \\
ftp \\
vsftpd \\
telnetd \\
xinetd \\
&& rm -rf /var/lib/apt/lists/*
# Create vulnerable user
RUN useradd -m -s /bin/bash ctfuser && \\
echo 'ctfuser:ctfpassword' | chpasswd && \\
echo 'root:r00t' | chpasswd
# Configure SSH for CTF
RUN mkdir /var/run/sshd && \\
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \\
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
# Configure Apache
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf && \\
a2enmod php7.2
# Create some CTF flags
RUN echo "FLAG{docker_conversion_success}" > /root/flag.txt && \\
echo "FLAG{web_server_flag}" > /var/www/html/flag.txt && \\
chmod 644 /var/www/html/flag.txt
# Create vulnerable web app
RUN echo '<?php if(isset($_GET["cmd"])) { system($_GET["cmd"]); } ?>' > /var/www/html/shell.php
# Configure FTP
RUN echo "local_enable=YES" >> /etc/vsftpd.conf && \\
echo "write_enable=YES" >> /etc/vsftpd.conf
# Create startup script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Expose CTF ports
EXPOSE 21 22 23 80 443 3306 8080
# Set entrypoint
ENTRYPOINT ["/entrypoint.sh"]
"""
# Create CTF entrypoint script
entrypoint_content = """#!/bin/bash
# Start services
service ssh start
service apache2 start
service mysql start
service vsftpd start
# Create some interesting files
echo "Welcome to the CTF challenge!" > /home/ctfuser/welcome.txt
echo "Can you find all the flags?" > /home/ctfuser/hint.txt
# Set weak permissions (intentionally vulnerable)
chmod 777 /tmp
chmod 755 /home/ctfuser
# Keep container running
tail -f /dev/null
"""
# Write files
with open(dockerfile_path, 'w') as f:
f.write(dockerfile_content)
entrypoint_path = self.docker_dir / 'entrypoint.sh'
with open(entrypoint_path, 'w') as f:
f.write(entrypoint_content)
print(f"[+] CTF Dockerfile created at {dockerfile_path}")
return dockerfile_path
def convert(self, ctf_mode: bool = True) -> bool:
"""Main conversion process"""
print(f"\n{'='*60}")
print(f"VM to Docker Converter")
print(f"{'='*60}")
print(f"Source: {self.vm_path}")
print(f"Target: {self.output_name}")
print(f"Mode: {'Extract VM' if ctf_mode else 'Template'}")
print(f"{'='*60}\n")
try:
vmdk_file = None
fs_dir = None
# Extract if OVA
if self.vm_path.suffix.lower() == '.ova':
vmdk_file = self.download_and_extract()
if vmdk_file:
print(f"[+] Found VMDK: {vmdk_file.name}")
elif self.vm_path.suffix.lower() == '.vmdk':
vmdk_file = self.vm_path
# Try to extract filesystem from VMDK
if vmdk_file and ctf_mode:
raw_disk = self.convert_vmdk_to_raw(vmdk_file)
if raw_disk:
fs_dir = self.mount_and_extract_filesystem(raw_disk)
# Create appropriate Dockerfile
if fs_dir and fs_dir.exists():
# Use extracted filesystem
print("[+] Using extracted VM filesystem")
self.create_dockerfile_from_filesystem(fs_dir)
elif ctf_mode:
# Fall back to CTF template
print("[!] Could not extract filesystem, using CTF template")
self.create_ctf_dockerfile()
else:
# Use standard template
self.create_dockerfile_from_template()
# Build Docker image
if not self.build_docker_image():
return False
print(f"\n[+] Conversion completed successfully!")
print(f"[+] Docker image: {self.output_name}")
print(f"\n[*] To run the container:")
print(f" docker run -d --name {self.output_name}_container -p 2222:22 -p 8080:80 {self.output_name}")
print(f"\n[*] To access the container:")
print(f" SSH: ssh ctfuser@localhost -p 2222 (password: ctfpassword)")
print(f" Web: http://localhost:8080")
return True
except Exception as e:
print(f"[!] Error during conversion: {e}")
import traceback
traceback.print_exc()
return False
finally:
# Cleanup
if self.work_dir.exists():
print(f"\n[*] Cleaning up work directory")
shutil.rmtree(self.work_dir, ignore_errors=True)
def download_file(url: str, dest_path: Path) -> bool:
"""Download file from URL"""
print(f"[*] Downloading: {url}")
print(f" Destination: {dest_path}")
# Use curl with progress bar
result = subprocess.run(['curl', '-L', '-o', str(dest_path), '--progress-bar', url])
if result.returncode == 0 and dest_path.exists():
size_mb = dest_path.stat().st_size / (1024*1024)
print(f"[+] Downloaded successfully: {size_mb:.2f} MB")
return True
print(f"[!] Download failed")
return False
def main():
parser = argparse.ArgumentParser(description='Simple VM to Docker converter')
parser.add_argument('vm_image', help='Path or URL to VM image (OVA/VMDK)')
parser.add_argument('-n', '--name', help='Docker image name', default=None)
parser.add_argument('--standard', action='store_true', help='Use standard mode instead of CTF mode')
parser.add_argument('--test', action='store_true', help='Test the resulting container')
args = parser.parse_args()
# Handle URL input
vm_path = args.vm_image
if vm_path.startswith('http://') or vm_path.startswith('https://'):
url = vm_path
filename = url.split('/')[-1]
vm_path = Path(tempfile.gettempdir()) / filename
if not download_file(url, vm_path):
print("[!] Failed to download VM image")
sys.exit(1)
else:
vm_path = Path(vm_path)
if not vm_path.exists():
print(f"[!] File not found: {vm_path}")
sys.exit(1)
# Create converter
converter = SimpleVMToDockerConverter(str(vm_path), args.name)
# Run conversion
success = converter.convert(ctf_mode=not args.standard)
if success and args.test:
print("\n[*] Testing the Docker container...")
container_name = f"{converter.output_name}_test"
# Stop any existing test container
subprocess.run(['docker', 'rm', '-f', container_name], capture_output=True)
# Run container with port mappings
print(f"[*] Starting container: {container_name}")
result = subprocess.run([
'docker', 'run', '-d',
'--name', container_name,
'-p', '2222:22',
'-p', '8080:80',
'-p', '2121:21',
converter.output_name
], capture_output=True, text=True)
if result.returncode == 0:
container_id = result.stdout.strip()
print(f"[+] Container started: {container_id[:12]}")
# Wait for services to start
print("[*] Waiting for services to initialize...")
subprocess.run(['sleep', '3'])
# Check container status
print("\n[*] Container status:")
subprocess.run(['docker', 'ps', '-f', f'name={container_name}'])
# Test services
print("\n[*] Testing services:")
print(" SSH: ssh ctfuser@localhost -p 2222")
print(" Web: http://localhost:8080")
print(" FTP: ftp://localhost:2121")
# Show logs
print("\n[*] Container logs:")
subprocess.run(['docker', 'logs', '--tail', '20', container_name])
print(f"\n[+] Test container is running: {container_name}")
print(f"[*] To stop it: docker rm -f {container_name}")
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()