HITCON ZeroDay 通報整理器 - 自動化整理你的漏洞通報管理
身為一個經常在 HITCON ZeroDay 上通報漏洞的白帽駭客,我發現自己需要一個工具來整理和管理這些通報記錄,手動一筆一筆複製實在太耗時,而且當通報數量越來越多時,統計和分析就變得非常困難,於是我開發了這個 JavaScript 工具,可以直接在瀏覽器中執行,自動抓取所有通報記錄並匯出成 CSV 和 JSON 格式。
🎯 為什麼需要這個工具?
問題背景
HITCON ZeroDay 是台灣重要的漏洞通報平台,當你累積了數十筆甚至上百筆通報後,想要:
- 📊 統計分析 - 查看自己通報的漏洞類型分布
- 📝 製作報告 - 整理通報記錄做成個人履歷
- 💾 資料備份 - 定期備份自己的通報內容
- 📈 趨勢追蹤 - 觀察自己的通報趨勢
技術挑戰
最初我嘗試用 Python 的 requests 和 BeautifulSoup 來抓取,但遇到了 Cloudflare 的 JavaScript Challenge:
# 這段 Python 程式碼會失敗
response = requests.get('https://zeroday.hitcon.org/user/kevin2758/vulnerability')
# 結果:403 Forbidden
即使設定了正確的 cookies 和 headers,Cloudflare 還是能檢測出這是自動化請求。經過研究後發現,最簡單且有效的方法就是直接在瀏覽器中執行 JavaScript。
✨ 解決方案:瀏覽器 Console 腳本
這個工具的核心概念很簡單:既然 Cloudflare 會擋自動化工具,那我們就讓程式在「真實的瀏覽器」中執行。
主要特色
1. 兩種模式
快速模式
- 只抓取列表頁資訊
- 適合快速統計和查看
完整模式
- 額外抓取每筆通報的詳細內容
- 包含漏洞類型、詳細描述、修補建議
- 適合製作完整報告或備份
2. 自動化處理
- 自動翻頁抓取所有通報
- 自動下載 CSV 和 JSON 檔案
- 提供即時進度顯示
🚀 使用方法
步驟 1:登入 HITCON ZeroDay
前往 https://zeroday.hitcon.org 並登入你的帳號。
步驟 2:開啟開發者工具
在通報列表頁面按下 F12開啟開發者工具,然後切換到 Console 標籤。
步驟 3:執行腳本
複製以下完整程式碼,貼到 Console 中,然後按 Enter。
步驟 4:選擇模式
程式會詢問你要使用哪個模式:
選擇模式:
1. 快速模式 - 只抓列表資訊
2. 完整模式 - 包含每篇詳細內容
選擇模式 (1=快速, 2=完整):
- 輸入
1→ 快速模式 - 輸入
2→ 完整模式(推薦)
步驟 5:等待完成
程式會自動:
- 抓取所有頁面的通報列表
- (完整模式)逐筆抓取詳細內容
- 顯示統計資料
- 自動下載 CSV 和 JSON 檔案
📊 輸出格式
CSV 欄位說明
快速模式:
序號, ZD_ID, 標題, 風險等級, 狀態, 受影響組織, 通報日期, 詳細頁URL
完整模式(額外 4 個欄位):
..., 漏洞類型, 相關網址, 詳細描述, 修補建議
實際範例
序號,ZD_ID,標題,風險等級,狀態,受影響組織,通報日期,詳細頁URL,漏洞類型,相關網址,詳細描述,修補建議
1,ZD-2025-01374,XXXX科技 客戶檢驗報告外洩,高,審核完成,XXXX科技股份有限公司,2025/11/03,https://zeroday.hitcon.org/vulnerability/ZD-2025-01374,資訊洩漏 (Information Leakage),https://xxx/report/,"檢驗報告儲存目錄開啟 Directory Listing,任何人無需認證即可下載所有客戶的檢驗報告","1. 立即關閉目錄訪問並移除所有公開報告 2. 實施身份驗證"
💻 完整程式碼
以下是完整的工具程式碼,可以直接複製使用:
/**
* HITCON ZeroDay 通報抓取工具 - 完整版
* 包含詳細頁面內容抓取
*
* 使用方法:
* 1. 登入 HITCON ZeroDay
* 2. 前往 https://zeroday.hitcon.org/user/kevin2758/vulnerability
* 3. 按 F12 打開開發者工具
* 4. 切到 Console 標籤
* 5. 複製貼上整個腳本並按 Enter
* 6. 選擇模式(快速/完整)
* 7. 等待自動抓取完成
* 8. 會自動下載 CSV 和 JSON 檔案
*/
(async function() {
console.log('='.repeat(60));
console.log('HITCON ZeroDay 通報抓取工具 - 完整版');
console.log('='.repeat(60));
console.log('');
// 設定
const BASE_URL = 'https://zeroday.hitcon.org/user/kevin2758/vulnerability';
const DELAY_MS = 10000; // 列表頁之間延遲
const DETAIL_DELAY_MS = 5000; // 詳細頁之間延遲
// 儲存所有通報
let allReports = [];
/**
* 解析風險等級
*/
function parseRiskLevel(classList) {
if (classList.contains('col-risk-critical')) return '嚴重';
if (classList.contains('col-risk-high')) return '高';
if (classList.contains('col-risk-medium')) return '中';
if (classList.contains('col-risk-low')) return '低';
return '未知';
}
/**
* 取得總頁數
*/
function getTotalPages() {
const lastPageElem = document.querySelector('.last-page');
return lastPageElem ? parseInt(lastPageElem.textContent.trim()) : 1;
}
/**
* 抓取指定頁面的 HTML
*/
async function fetchPage(pageNum) {
const url = pageNum === 1 ? BASE_URL : `${BASE_URL}/page/${pageNum}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const html = await response.text();
// 創建一個臨時 DOM 來解析
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return doc;
} catch (error) {
console.error(`抓取第 ${pageNum} 頁失敗:`, error);
throw error;
}
}
/**
* 抓取詳細頁面
*/
async function fetchDetailPage(zdId) {
const url = `https://zeroday.hitcon.org/vulnerability/${zdId}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return doc;
} catch (error) {
console.error(` ⚠️ 抓取 ${zdId} 詳細頁失敗:`, error);
return null;
}
}
/**
* 從詳細頁面提取資訊
*/
function extractDetailInfo(doc) {
if (!doc) return {};
const detail = {};
try {
// 漏洞類型
const infoSection = doc.querySelector('.vul-detail-section .section-content.info');
if (infoSection) {
const items = infoSection.querySelectorAll('li');
items.forEach(li => {
const text = li.textContent.trim();
if (text.startsWith('類型:')) {
detail.漏洞類型 = text.replace('類型:', '').trim();
}
});
}
// 相關網址
const urlSection = doc.querySelector('.vu-d-url .urls');
if (urlSection) {
detail.相關網址 = urlSection.textContent.trim();
}
// 詳細描述
const descSection = doc.querySelector('#detail');
if (descSection) {
// 移除 HTML 標籤,只保留文字
const tempDiv = document.createElement('div');
tempDiv.innerHTML = descSection.innerHTML;
detail.詳細描述 = tempDiv.textContent.trim().replace(/\s+/g, ' ');
}
// 修補建議
const remedySection = Array.from(doc.querySelectorAll('.vul-detail-section')).find(section => {
const title = section.querySelector('.sec-title');
return title && title.textContent.includes('修補建議');
});
if (remedySection) {
const content = remedySection.querySelector('.section-content');
if (content) {
detail.修補建議 = content.textContent.trim().replace(/\s+/g, ' ');
}
}
} catch (error) {
console.error(' 解析詳細頁時出錯:', error);
}
return detail;
}
/**
* 從文檔中抓取通報
*/
function scrapeFromDocument(doc) {
const reports = [];
const items = doc.querySelectorAll('li.strip.allow-edit');
console.log(` 找到 ${items.length} 筆通報`);
items.forEach((item, index) => {
try {
// ZD ID
const codeElem = item.querySelector('li.code');
const zdId = codeElem ? codeElem.textContent.replace('HZD Code:', '').trim() : '';
// 標題和 URL
const titleElem = item.querySelector('h4.title a');
const title = titleElem ? titleElem.textContent.trim() : '';
const detailUrl = titleElem ? titleElem.getAttribute('href') : '';
// 風險等級
const riskElem = item.querySelector('li.risk');
const risk = riskElem ? parseRiskLevel(riskElem.classList) : '未知';
// 狀態
const statusElem = item.querySelector('li.status');
const status = statusElem ? statusElem.textContent.replace('Status:', '').trim() : '';
// 組織名稱
const vendorElem = item.querySelector('li.vendor .v-name-short');
const vendor = vendorElem ? vendorElem.textContent.trim() : '';
// 日期
const dateElem = item.querySelector('li.date');
const date = dateElem ? dateElem.textContent.replace('Date:', '').trim() : '';
const report = {
序號: allReports.length + reports.length + 1,
ZD_ID: zdId,
標題: title,
風險等級: risk,
狀態: status,
受影響組織: vendor,
通報日期: date,
詳細頁URL: detailUrl ? `https://zeroday.hitcon.org${detailUrl}` : '',
// 以下欄位在完整模式下會填入
漏洞類型: '',
相關網址: '',
詳細描述: '',
修補建議: ''
};
reports.push(report);
} catch (error) {
console.error(` 解析第 ${index + 1} 筆通報時出錯:`, error);
}
});
return reports;
}
/**
* 轉換成 CSV 格式
*/
function convertToCSV(reports) {
if (reports.length === 0) return '';
// CSV 標題
const headers = Object.keys(reports[0]);
const csvHeaders = headers.join(',');
// CSV 內容
const csvRows = reports.map(report => {
return headers.map(header => {
const value = report[header] || '';
// 處理包含逗號、引號、換行的值
const escaped = value.toString().replace(/"/g, '""');
return `"${escaped}"`;
}).join(',');
});
return csvHeaders + '\n' + csvRows.join('\n');
}
/**
* 下載檔案
*/
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* 下載 CSV
*/
function downloadCSV(reports) {
const csv = convertToCSV(reports);
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
const filename = `hitcon_reports_${timestamp}.csv`;
downloadFile('\ufeff' + csv, filename, 'text/csv;charset=utf-8;');
console.log(`✓ 已下載: ${filename}`);
}
/**
* 下載 JSON
*/
function downloadJSON(reports) {
const json = JSON.stringify(reports, null, 2);
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
const filename = `hitcon_reports_${timestamp}.json`;
downloadFile(json, filename, 'application/json');
console.log(`✓ 已下載: ${filename}`);
}
/**
* 主要執行流程
*/
async function main() {
try {
// 取得總頁數(從當前頁面)
const totalPages = getTotalPages();
console.log(`總共有 ${totalPages} 頁通報\n`);
// 選擇模式
const modes = {
'1': { name: '快速模式', fetchDetails: false },
'2': { name: '完整模式(包含詳細內容)', fetchDetails: true }
};
console.log('%c選擇模式:', 'font-weight: bold; font-size: 14px;');
console.log('1. 快速模式 - 只抓列表資訊');
console.log('2. 完整模式 - 包含每篇詳細內容(約 ' + Math.ceil(totalPages * 10 * DETAIL_DELAY_MS / 1000 / 60) + ' 分鐘)');
const modeChoice = prompt('選擇模式 (1=快速, 2=完整):', '1');
if (!modeChoice || !modes[modeChoice]) {
console.log('已取消或選擇無效');
return;
}
const mode = modes[modeChoice];
console.log(`\n使用 ${mode.name}\n`);
// 確認執行
if (totalPages > 5 || mode.fetchDetails) {
const estimatedTime = mode.fetchDetails
? Math.ceil(totalPages * 10 * DETAIL_DELAY_MS / 1000)
: Math.ceil(totalPages * DELAY_MS / 1000);
const proceed = confirm(
`${mode.name}\n` +
`總共 ${totalPages} 頁\n` +
`預計需要 ${estimatedTime} 秒\n\n` +
`要繼續嗎?`
);
if (!proceed) {
console.log('已取消');
return;
}
}
// 先抓取當前頁(第 1 頁)
console.log(`[1/${totalPages}] 正在抓取第 1 頁(當前頁)...`);
const firstPageReports = scrapeFromDocument(document);
allReports.push(...firstPageReports);
console.log(`✓ 第 1 頁完成,累計 ${allReports.length} 筆通報`);
// 逐頁抓取其他頁面
for (let page = 2; page <= totalPages; page++) {
console.log(`\n[${page}/${totalPages}] 正在抓取第 ${page} 頁...`);
const doc = await fetchPage(page);
const reports = scrapeFromDocument(doc);
allReports.push(...reports);
console.log(`✓ 第 ${page} 頁完成,累計 ${allReports.length} 筆通報`);
if (page < totalPages) {
await new Promise(resolve => setTimeout(resolve, DELAY_MS));
}
}
console.log('\n' + '='.repeat(60));
console.log(`列表頁抓取完成!總共 ${allReports.length} 筆通報`);
console.log('='.repeat(60));
// 如果是完整模式,抓取每篇詳細內容
if (mode.fetchDetails) {
console.log('\n開始抓取詳細內容...\n');
for (let i = 0; i < allReports.length; i++) {
const report = allReports[i];
const progress = `[${i + 1}/${allReports.length}]`;
console.log(`${progress} ${report.ZD_ID} - ${report.標題.substring(0, 30)}...`);
const detailDoc = await fetchDetailPage(report.ZD_ID);
const detailInfo = extractDetailInfo(detailDoc);
// 合併詳細資訊
Object.assign(report, detailInfo);
if ((i + 1) % 10 === 0) {
console.log(` 已完成 ${i + 1}/${allReports.length} 筆`);
}
// 延遲避免請求太快
if (i < allReports.length - 1) {
await new Promise(resolve => setTimeout(resolve, DETAIL_DELAY_MS));
}
}
console.log('\n' + '='.repeat(60));
console.log('詳細內容抓取完成!');
console.log('='.repeat(60));
}
// 顯示統計
console.log('');
const stats = {
總數: allReports.length,
嚴重: allReports.filter(r => r.風險等級 === '嚴重').length,
高: allReports.filter(r => r.風險等級 === '高').length,
中: allReports.filter(r => r.風險等級 === '中').length,
低: allReports.filter(r => r.風險等級 === '低').length
};
console.table(stats);
// 下載檔案
console.log('\n正在下載檔案...');
downloadCSV(allReports);
downloadJSON(allReports);
console.log('\n🎉 完成!已下載 CSV 和 JSON 檔案');
console.log('\n你也可以在 Console 中輸入以下指令查看資料:');
console.log('- window.hitconReports // 查看所有通報');
console.log('- window.hitconReports[0] // 查看第一筆通報');
console.log('- window.hitconReports.filter(r => r.風險等級 === "嚴重") // 篩選嚴重風險');
if (mode.fetchDetails) {
console.log('- window.hitconReports[0].詳細描述 // 查看詳細描述');
}
// 儲存到 window 物件方便查看
window.hitconReports = allReports;
} catch (error) {
console.error('發生錯誤:', error);
console.log('\n如果遇到問題,可以試試:');
console.log('1. 重新整理頁面後再執行');
console.log('2. 確認已登入 HITCON ZeroDay');
console.log('3. 選擇快速模式(如果完整模式失敗)');
}
}
// 開始執行
await main();
})();⚠️ 注意事項
使用限制
- 網路穩定性:完整模式需要較長時間,建議在網路穩定時執行
- 請求頻率:程式已經加入延遲,避免對伺服器造成負擔
- 使用者名稱:記得將
BASE_URL中的kevin2758改成你的使用者名稱
客製化建議
調整延遲時間
const DELAY_MS = 10000; // 列表頁延遲
const DETAIL_DELAY_MS = 5000; // 詳細頁延遲
修改使用者名稱
const BASE_URL = 'https://zeroday.hitcon.org/user/YOUR_USERNAME/vulnerability';
只抓取前幾頁(測試用)
const totalPages = Math.min(getTotalPages(), 3); // 只抓前 3 頁
🎯 總結
這個工具解決了我長期以來的痛點,讓通報管理變得更加輕鬆,透過瀏覽器 Console 執行的方式,如果你也在使用 HITCON ZeroDay,不妨試試這個工具!
Member discussion