Featured image of post Docker + Exploit-databaseで検索可能なExploitポータルを構築する

Docker + Exploit-databaseで検索可能なExploitポータルを構築する

Ubuntu Server上にExploit-databaseをDockerで構築し、検索可能なExploitポータルを作成しました。

倫理規定

この記事では、ハッキングに関するナレッジベースを構築し、LMStudioとObsidianを組み合わせて効率的に参照する方法について説明します。ハッキングに関する情報は倫理的に問題のある内容を含む可能性があるため、この記事では以下の倫理規定を遵守します。

  1. 合法性の遵守: 本記事で紹介する方法やツールは、合法的な目的でのみ使用されることを前提としています。不正アクセスや違法行為を助長する意図は一切ありません。
  2. 教育目的: 本記事の内容は、セキュリティ研究や教育、学習目的での利用を想定しています。実際の攻撃行為を推奨するものではありません。
  3. 責任の所在: 本記事の内容を利用して発生したいかなる問題についても、著者および配信者は一切の責任を負いません。利用者自身が法的および倫理的責任を負うものとします。

はじめに

春休みからハッキングの勉強を再開しようと思い立ち、前回の記事ではLMStudio + ObsidianでRAG環境を構築してナレッジベースを効率的に参照する方法について紹介しました。今回は、Exploit-databaseをローカルで検索可能なポータルとして構築する方法について紹介します。Exploit-databaseは、様々なソフトウェアやシステムの脆弱性情報と、それを突くためのExploitコードを集めたデータベースです。これをローカルで検索可能にすることで、ハッキングの勉強やペネトレーションテストの際に迅速に情報を取得できるようになります。Kali Linuxには標準でsearchsploitコマンドが搭載されていますが、今回はUbuntu Server上にDockerで構築する方法を紹介します。

環境

今回使用した環境は以下の通りです。

  • OS: 24.04.3 LTS (Noble Numbat)
  • CPU: AMD Ryzen 7 5825U with Radeon Graphics
  • RAM: 16GB
  • Docker: 29.2.0

システム構成

今回構築するシステムの構成は以下のようになっています。

  • Backend: Bun + ElysiaJS - Exploit-databaseのデータを検索するAPIを提供
  • Frontend: SvelteKit - 検索インターフェースを提供
  • Database: Exploit-database - 脆弱性情報とExploitコードのデータベース

Exploit-databaseの準備

まず、Exploit-databaseのデータを準備します。Exploit-databaseはGitLab上で公開されているため、以下のコマンドでクローンします(githubでないことに注意!)。

1
2
sudo git clone https://gitlab.com/exploit-database/exploitdb.git /opt/exploit-database
ln -sf /opt/exploit-database/searchsploit /usr/local/bin/searchsploit

プロジェクトディレクトリの作成

今回は、$HOME/searchsploit-portalというディレクトリにプロジェクトを作成します。

1
2
mkdir -p $HOME/searchsploit-portal
cd $HOME/searchsploit-portal

Backendの構築

searchsploit-portal直下にbackendディレクトリを作成し、Bun + ElysiaJSでAPIサーバーを構築します。

1
2
3
4
mkdir backend
cd backend
bun init -y
bun add elysia @elysiajs/cors

index.tsファイルがbackendディレクトリ直下にできるはずなので、以下のように編集します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Elysia, t } from 'elysia'
import { staticPlugin } from '@elysiajs/static'
import { cors } from '@elysiajs/cors'
import { join } from 'path'

const app = new Elysia()
    .use(cors())
    .use(staticPlugin({
        assets: join(import.meta.dir, 'dist'),
        prefix: '/'
    }))
    .group('/api', (app) => 
        app
            .get('/search', async ({ query }: { query: { q: string } }) => {
                const proc = Bun.spawn(["searchsploit", "--json", query.q]);
                const text = await new Response(proc.stdout).text();
                return JSON.parse(text);
            }, {
                query: t.Object({ q: t.String() })
            })
            .get('/content', async ({ query }: { query: { path: string } }) => {
                const proc = Bun.spawn(["searchsploit", "-x", query.path]);
                const content = await new Response(proc.stdout).text();
                return { content };
            }, {
                query: t.Object({ path: t.String() })
            })
    )
    // フロントエンドを配信
    .get('/', () => Bun.file(join(import.meta.dir, 'dist/index.html')))
    .listen(8000);

export type App = typeof app;
console.log(`🚀 Searchsploit Portal: http://localhost:8000`);

Frontendの構築

続いて、searchsploit-portal直下にfrontendディレクトリを作成し、SvelteKitでフロントエンドを構築します。

1
2
3
4
5
6
mkdir ../frontend
cd ../frontend
bun create vite . --template react-ts
bun install
bun add @elysiajs/eden lucide-react react-syntax-highlighter
bun add -d tailwindcss postcss autoprefixer

続いて、作成されたfrontend/src/App.tsxを以下のように編集します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
import { useState } from 'react'
import { treaty } from '@elysiajs/eden'
import type { App } from '../../backend/index' // バックエンドへのパス
import { Search, Code, Copy, Terminal, ShieldAlert } from 'lucide-react'
import './index.css'

// ブラウザのURLから自動的にAPIの接続先を決定
const client = treaty<App>(window.location.origin)

export default function App() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<any>(null)
  const [content, setContent] = useState('')
  const [loading, setLoading] = useState(false)
  const [selectedPath, setSelectedPath] = useState('')

  const handleSearch = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!query) return
    setLoading(true)
    const { data, error } = await client.api.search.get({ query: { q: query } })
    if (!error) setResults(data)
    setLoading(false)
  }

  const loadContent = async (path: string) => {
    setSelectedPath(path)
    const { data, error } = await client.api.content.get({ query: { path } })
    if (!error && data) setContent(data.content)
  }

  return (
    <div className="flex h-screen bg-slate-950 text-slate-100 font-sans overflow-hidden">
      {/* Sidebar: Search & Results */}
      <div className="w-80 border-r border-slate-800 flex flex-col bg-slate-900/50">
        <div className="p-4 border-b border-slate-800">
          <div className="flex items-center gap-2 mb-4 text-blue-400">
            <ShieldAlert size={20} />
            <span className="font-bold tracking-tight text-sm uppercase">Searchsploit GUI</span>
          </div>
          <form onSubmit={handleSearch} className="relative">
            <Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-500" />
            <input
              type="text"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="e.g. ssh, rce, smb..."
              className="w-full bg-slate-950 border border-slate-700 rounded-lg py-2 pl-10 pr-4 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 transition-all"
            />
          </form>
        </div>

        <div className="flex-1 overflow-y-auto p-2 custom-scrollbar">
          {loading && <div className="p-4 text-center text-slate-500 animate-pulse text-sm">Searching DB...</div>}
          
          {results?.RESULTS_EXPLOIT?.map((exp: any, i: number) => (
            <button
              key={i}
              onClick={() => loadContent(exp.Path)}
              className={`w-full text-left p-3 mb-1 rounded-md transition-all border ${
                selectedPath === exp.Path 
                  ? 'bg-blue-600/20 border-blue-500/50 text-blue-100' 
                  : 'bg-transparent border-transparent hover:bg-slate-800/50 text-slate-400'
              }`}
            >
              <div className="text-[10px] font-mono opacity-60 mb-1 flex justify-between">
                <span>{exp.Platform}</span>
                <span>{exp.Type}</span>
              </div>
              <div className="text-xs font-medium leading-snug line-clamp-2">{exp.Title}</div>
            </button>
          ))}
        </div>
      </div>

      {/* Main: Content Viewer */}
      <div className="flex-1 flex flex-col bg-slate-950">
        {content ? (
          <>
            <div className="h-14 border-b border-slate-800 flex items-center justify-between px-6 bg-slate-900/30">
              <div className="flex items-center gap-3 text-slate-400 text-xs font-mono">
                <Terminal size={14} className="text-blue-500" />
                <span className="truncate max-w-md">{selectedPath}</span>
              </div>
              <button 
                onClick={() => navigator.clipboard.writeText(content)}
                className="flex items-center gap-2 bg-slate-100 hover:bg-white text-slate-950 px-4 py-1.5 rounded-md text-xs font-bold transition-all active:scale-95"
              >
                <Copy size={14} /> Copy to Obsidian
              </button>
            </div>
            <div className="flex-1 overflow-auto p-6">
              <pre className="text-xs font-mono leading-relaxed text-slate-300 whitespace-pre-wrap">
                <code>{content}</code>
              </pre>
            </div>
          </>
        ) : (
          <div className="flex-1 flex flex-col items-center justify-center text-slate-700">
            <Code size={48} className="mb-4 opacity-20" />
            <p className="text-sm italic">Select an exploit to view source code</p>
          </div>
        )}
      </div>
    </div>
  )
}

続いて、index.cssを以下のように編集します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@import "tailwindcss";

@theme {
}

@layer base {
  html, body, #root {
    height: 100%;
    margin: 0;
    padding: 0;
  }
  
  body {
    @apply bg-slate-950 text-slate-50 antialiased;
    display: block;
  }
}

/* スクロールバー*/
.custom-scrollbar::-webkit-scrollbar {
  width: 5px;
}
.custom-scrollbar::-webkit-scrollbar-track {
  background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
  @apply bg-slate-800 rounded-full;
}

また、tailwind.config.jsを以下のように編集します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

postcss.config.jsを以下のように編集します。

1
2
3
4
5
6
7
// frontend/postcss.config.js
export default {
  plugins: {
    '@tailwindcss/postcss': {},
    autoprefixer: {},
  },
}

frontend側のpackage.jsonのscriptsセクションを以下のように編集します。

1
2
3
4
5
6
7
"scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview",
    "deploy": "bun run build && rm -rf ../backend/dist && cp -r dist ../backend/"
  },

vite.config.tsを以下のように編集します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})

ビルドの実行

frontendディレクトリで以下のコマンドを実行し、フロントエンドをビルドしてbackend/distにコピーします。

1
2
bun run build
cp -r dist ../backend/

サーバーの起動

backendディレクトリで以下のコマンドを実行し、サーバーを起動します。

1
2
cd ../backend
bun run index.ts

するとおそらく、http://localhost:8000でExploitポータルが起動します。

Dockerでの運用

このままではサーバーを起動するたびに手動でコマンドを実行する必要があるため、Dockerでコンテナ化します。searchsploit-portal直下にDockerfileを作成し、以下の内容を記述します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# フロントエンドのビルド
FROM oven/bun:latest AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package.json frontend/bun.lock ./
RUN bun install
COPY frontend/ ./
# Tailwindの設定等を含めてビルド
RUN bun run build

# バックエンドの準備と実行
FROM oven/bun:latest
WORKDIR /app

# ExploitDBのインストールと searchsploit のセットアップ
RUN apt-get update && apt-get install -y git && \
    git clone https://gitlab.com/exploit-database/exploitdb.git /opt/exploit-database && \
    ln -sf /opt/exploit-database/searchsploit /usr/local/bin/searchsploit && \
    rm -rf /var/lib/apt/lists/*

# バックエンドの依存関係のインストール
COPY backend/package.json backend/bun.lock ./backend/
WORKDIR /app/backend
RUN bun install

# ソースコードと、ビルド済みのフロントエンドをコピー
COPY backend/ ./
COPY --from=frontend-builder /app/frontend/dist ./dist

# ポートの開放
EXPOSE 8000

# サーバー起動
CMD ["bun", "run", "index.ts"]
1
2
3
4
5
6
7
8
# docker-compose.yml
services:
  searchsploit-portal:
    build: .
    container_name: searchsploit-portal
    ports:
      - "13370:8000"
    restart: unless-stopped

その後、以下のコマンドでDockerイメージをビルドし、コンテナを起動します。

1
sudo docker compose up -d --build

これで、http://localhost:13370でExploitポータルが起動するはずです。

自動更新の設定

Exploit-databaseのデータは定期的に更新されるため、コンテナ内で自動的に更新する仕組みを作ります。dockerのcronコンテナを利用して、毎日1回searchsploitのデータベースを更新するように設定します。以下のコマンドをsearchsploit-portalディレクトリで実行します。

1
sudo crontab -e

するとnanoエディタが開くので、以下の行を追加します。

1
0 3 * * * docker exec -t searchsploit-portal git pull

こうすることで、毎日午前3時にsearchsploitのデータベースが更新されます。

また、起動時に更新を行うようにするため、DockerfileのCMDセクションを以下のように編集します。

1
CMD cd /opt/exploit-database && git pull && cd /app/backend && bun run index.ts

これで、コンテナ起動時にも最新のデータベースが取得されるようになります。

まとめ

今回は、Ubuntu Server上にDockerでExploit-databaseを構築し、検索可能なExploitポータルを作成する方法について紹介しました。このポータルを利用することで、ハッキングの勉強やペネトレーションテストの際に迅速に脆弱性情報とExploitコードを取得できるようになります。今後は他にも様々なツールを実装していきたいと思います。 それでは次回の記事でお会いしましょう。

出典

Made with Hugo & Stack
Hugo で構築されています。
テーマ StackJimmy によって設計されています。