0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nucleiのテンプレートからadminパネルのURLを得たりshodan検索したりするツールを作った

Posted at

WebアプリごとのloginとかadminパネルとかのデフォルトURLをいちいち探すのが面倒だなあと思っていたのだけど、とりあえず nuclei のテンプレート使えば早いんじゃね?ということが頭に浮かんだので作った。
何かに書いておかないと忘れるのでメモ。

📁 フォルダ構成

product-finder/
├── app.py                       # アプリ本体
├── utils/
│   └── extract_templates.py     # nuclei テンプレートから情報抽出するスクリプト
├── data/
│   └── nuclei_data.json         # extract_templates.py が生成するデータファイル
├── nuclei-templates/            # nuclei テンプレートリポジトリ(自動 clone される)
│   └── ...
├── templates/
│   ├── index.html               # 検索・一覧表示用テンプレート
│   └── show_template.html       # YAML ファイル内容表示用

🛠 セットアップ手順

1. 依存ライブラリインストール

$ pip install -r requirements.txt

2. nuclei-templates のクローンとデータ生成

python utils/extract_templates.py

これで nuclei-templates/ が clone され、YAML ファイルから管理系テンプレートを抽出し、data/nuclei_data.json に保存される。


3. Flask アプリ起動

python app.py

ブラウザで http://localhost:5004/ にアクセス。


🖥 使い方

🔍 /

初期画面。検索フォームだけが表示される。

🔎 /search?q=jenkins

検索キーワードにマッチするプロダクトを表示(ページネーション付き)

📃 /all

すべてのテンプレートを表示(50件ずつページ分割)


🔗 各テンプレートの情報

表示される情報 説明
プロダクト名 nuclei テンプレートから自動抽出
パス http.path 情報(管理画面 URL パターン)
Shodan metadata.shodan-query があればそれを使用、なければ http.title ベースで生成
Google 自動生成された inurl: クエリ
テンプレートを見る YAML の中身をブラウザで整形表示

できること

  • 各プロダクトのデフォルトの/adminとか/loginがわかる
  • そこを探す検索URLが自動でできる

やってないこと

  • データの並べ替え
  • 変更したいならsqliteとか使った方がよさそう

以下ソースコードと画像

app.py

from flask import Flask, render_template, request
import os, json
import math

app = Flask(__name__)

from flask import send_from_directory

@app.route('/nuclei-templates/<path:filename>')
def serve_nuclei_templates(filename):
    return send_from_directory('nuclei-templates', filename)
    
app = Flask(__name__)

@app.route('/show_template/<path:filename>')
def show_template(filename):
    file_path = os.path.join('nuclei-templates', filename)
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()
    return render_template('show_template.html', content=content)

# 読み込み済みのデータファイル (extract_templates.py で生成)

with open("data/nuclei_data.json", "r", encoding="utf-8") as f:
    DATA = json.load(f)

PER_PAGE = 50

@app.route("/")
def index():
    # クエリがない場合は空表示
    return render_template("index.html", results=[], query="", total=0, page=1, pages=0)

@app.route("/search")
def search():
    query = request.args.get("q", "").lower()
    page = int(request.args.get("page", 1))
    matched = [d for d in DATA if query in d["product"].lower()]
    total = len(matched)
    pages = math.ceil(total / PER_PAGE)
    results = matched[(page - 1) * PER_PAGE : page * PER_PAGE]
    return render_template("index.html", results=results, query=query, total=total, page=page, pages=pages)

@app.route("/all")
def show_all():
    page = int(request.args.get("page", 1))
    total = len(DATA)
    pages = math.ceil(total / PER_PAGE)
    results = DATA[(page - 1) * PER_PAGE : page * PER_PAGE]
    return render_template("index.html", results=results, query="", total=total, page=page, pages=pages)

if __name__ == "__main__":
    app.run(debug=True,port=5004)

utils/extract_templates.py

import os
import yaml
import json
from urllib.parse import quote
import subprocess

def extract_product_name(name):
    # 最初の単語だけを抜き出してプロダクト名にする
    return name.strip().split()[0]

def clone_templates(repo_url="https://github.com/projectdiscovery/nuclei-templates.git", path="nuclei-templates"):
    if not os.path.exists(path):
        print(f"Cloning {repo_url} into {path} ...")
        subprocess.run(["git", "clone", "--depth", "1", repo_url, path])
    else:
        print(f"Templates already exist in {path}")

def extract_templates(base_dir="nuclei-templates", output_file="data/nuclei_data.json"):
    entries = []

    for root, _, files in os.walk(base_dir):
        for file in files:
            if not file.endswith(".yaml"):
                continue
            full_path = os.path.join(root, file)
            try:
                with open(full_path, "r", encoding="utf-8") as f:
                    yml = yaml.safe_load(f)
                    name = yml.get("info", {}).get("name", "Unknown")
                    product = extract_product_name(name)
                    tags = yml.get("info", {}).get("tags", [])
                    if isinstance(tags, str):
                        tags = [t.strip() for t in tags.split(",")]

                    if not any(tag in tags for tag in ["panel", "admin", "login", "dashboard"]):
                        continue  # 管理系っぽいテンプレだけに絞る

                    # requests, http, dnsなど複数形式に対応
                    for section_key in ["requests", "http"]:
                        reqs = yml.get(section_key, [])
                        if not reqs:
                            continue
                        for req in reqs:
                            paths = req.get("path", [])
                            if isinstance(paths, str):
                                paths = [paths]
                            for path in paths:
                                metadata = yml.get("info", {}).get("metadata", {})
                                raw_shodan_q = metadata.get("shodan-query")
                                shodan_q = raw_shodan_q if isinstance(raw_shodan_q, str) else f'http.title:"{product}"'

                                google_q = f'inurl:{path} intitle:"{product}"'
                                entries.append({
                                    "product": product,
                                    "path": path,
                                    "tags": tags,
                                    "template": os.path.relpath(full_path, base_dir),
                                    "template_file": os.path.basename(full_path),  # ここでテンプレートファイル名を追加
                                    "shodan_url": f"https://www.shodan.io/search?query={quote(shodan_q)}",
                                    "google_url": f"https://www.google.com/search?q={quote(google_q)}"
                                })
            except Exception as e:
                print(f"Error parsing {full_path}: {e}")

    os.makedirs(os.path.dirname(output_file), exist_ok=True)
    with open(output_file, "w", encoding="utf-8") as out:
        json.dump(entries, out, indent=2, ensure_ascii=False)
    print(f"Extracted {len(entries)} entries and saved to {output_file}")

if __name__ == "__main__":
    clone_templates()
    extract_templates()


スクリーンショット 2025-04-09 17.36.44.png

templates/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Nuclei 管理パネル検索</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container mt-4">
    <h2>プロダクト管理パネル検索</h2>

    <form method="get" action="/search" class="mb-3 d-flex">
        <input name="q" class="form-control me-2" value="{{ query }}" placeholder="プロダクト名 (例: Jenkins)">
        <button class="btn btn-primary" type="submit">検索</button>
    </form>

    <p>
        <a href="/all" class="btn btn-secondary btn-sm">全件表示</a>
        {% if total %}
        <span class="ms-3">ヒット件数: {{ total }} 件</span>
        {% endif %}
    </p>

    {% if results %}
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th>プロダクト</th>
                <th>URL</th>
                <th>Shodan</th>
                <th>Google</th>
                <th>テンプレート</th>
            </tr>
        </thead>
        <tbody>
            {% for r in results %}
            <tr>
                <td>{{ r.product }}</td>
                <td>{{ r.path | replace("{{BaseURL}}", "") }}</td>
                <td><a href="{{ r.shodan_url }}" target="_blank">Shodan</a></td>
                <td><a href="{{ r.google_url | replace('%7B%7BBaseURL%7D%7D', '') }}" target="_blank">Google</a></td>
                <td><a href="{{ url_for('show_template', filename=r.template) }}" target="_blank">テンプレートを見る</a></td>
            </tr>
            {% endfor %}
        </tbody>
    </table>

    <!-- ページネーション -->
    {% if pages > 1 %}
    <nav>
        <ul class="pagination">
            {% for p in range(1, pages + 1) %}
            <li class="page-item {% if p == page %}active{% endif %}">
                <a class="page-link" href="?q={{ query }}&page={{ p }}">{{ p }}</a>
            </li>
            {% endfor %}
        </ul>
    </nav>
    {% endif %}
    {% else %}
        <p class="text-muted">検索キーワードを入力してください。</p>
    {% endif %}
</body>
</html>

スクリーンショット 2025-04-09 17.36.14.png

templates/show_template.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テンプレート内容</title>
  <!-- Highlight.js スタイル -->
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
  <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
  <script>hljs.highlightAll();</script>
  <style>
    body {
      font-family: sans-serif;
      margin: 2rem;
    }
    pre {
      background-color: #f0f0f0;
      padding: 1rem;
      border-radius: 8px;
      overflow-x: auto;
    }
  </style>
</head>
<body>
  <h1>テンプレート内容</h1>
  <pre><code class="language-yaml">{{ content | e }}</code></pre>
</body>
</html>

requirements.txt

blinker==1.9.0
click==8.1.8
Flask==3.1.0
gitdb==4.0.12
GitPython==3.1.44
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
PyYAML==6.0.2
smmap==5.0.2
Werkzeug==3.1.3
0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?