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 ベースで生成 |
自動生成された 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()
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>
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