9 min read

HITCON ZeroDay Report Organizer: Automate Managing Your Vulnerability Disclosures

HITCON ZeroDay Report Organizer: Automate Managing Your Vulnerability Disclosures

As a white-hat hacker who regularly reports vulnerabilities on HITCON ZeroDay, I found myself needing a tool to organize and manage all of these report records. Copying them out one by one by hand was simply too time-consuming, and once the number of reports started to pile up, doing any kind of statistics or analysis became a real pain. So I built this JavaScript tool that runs directly in the browser, automatically scraping every report record and exporting it to CSV and JSON.

🎯 Why do you need this tool?

The problem

HITCON ZeroDay is a major vulnerability disclosure platform in Taiwan. Once you've accumulated dozens or even hundreds of reports, you'll want to:

  • 📊 Run statistics - See the distribution of vulnerability types across your reports
  • 📝 Produce reports - Compile your disclosure records into a personal portfolio
  • 💾 Back up data - Periodically back up the content of your reports
  • 📈 Track trends - Observe how your reporting activity trends over time

The technical challenge

My first attempt was to scrape everything with Python's requests and BeautifulSoup, but I ran straight into Cloudflare's JavaScript Challenge:

# This Python code will fail
response = requests.get('https://zeroday.hitcon.org/user/kevin2758/vulnerability')
# Result: 403 Forbidden

Even with the correct cookies and headers set, Cloudflare could still tell it was an automated request. After some research, I discovered that the simplest and most effective approach was to run the JavaScript directly inside the browser.

✨ The solution: a browser Console script

The core idea behind this tool is simple: since Cloudflare blocks automation tools, we just let the code run inside a "real browser" instead.

Key features

1. Two modes

Quick mode

  • Only scrapes the information on the list pages
  • Great for a fast overview and quick statistics

Full mode

  • Additionally scrapes the detailed content of each report
  • Includes the vulnerability type, full description, and remediation advice
  • Ideal for producing a complete report or backup

2. Automated processing

  • Automatically paginates through and scrapes every report
  • Automatically downloads the CSV and JSON files
  • Shows real-time progress

🚀 How to use it

Step 1: Log in to HITCON ZeroDay

Head over to https://zeroday.hitcon.org and log in to your account.

Step 2: Open the developer tools

On the report list page, press F12 to open the developer tools, then switch to the Console tab.

Step 3: Run the script

Copy the complete code below, paste it into the Console, and press Enter.

Step 4: Choose a mode

The script will ask which mode you want to use:

選擇模式:
1. 快速模式 - 只抓列表資訊
2. 完整模式 - 包含每篇詳細內容

選擇模式 (1=快速, 2=完整):
  • Enter 1 → Quick mode
  • Enter 2 → Full mode (recommended)

Step 5: Wait for it to finish

The script will automatically:

  1. Scrape the report lists across all pages
  2. (Full mode) Scrape the detailed content of each report one by one
  3. Display the statistics
  4. Automatically download the CSV and JSON files

📊 Output format

CSV columns explained

Quick mode:

序號, ZD_ID, 標題, 風險等級, 狀態, 受影響組織, 通報日期, 詳細頁URL

Full mode (4 additional columns):

..., 漏洞類型, 相關網址, 詳細描述, 修補建議

A real example

序號,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. 實施身份驗證"

💻 The complete code

Here is the full tool code, ready to copy and use:

/**
 * HITCON ZeroDay Report Scraper - Full Version
 * Includes scraping of detail-page content
 *
 * How to use:
 * 1. Log in to HITCON ZeroDay
 * 2. Go to https://zeroday.hitcon.org/user/kevin2758/vulnerability
 * 3. Press F12 to open the developer tools
 * 4. Switch to the Console tab
 * 5. Copy and paste the whole script and press Enter
 * 6. Choose a mode (quick / full)
 * 7. Wait for the scraping to finish
 * 8. The CSV and JSON files will download automatically
 */

(async function() {
    console.log('='.repeat(60));
    console.log('HITCON ZeroDay 通報抓取工具 - 完整版');
    console.log('='.repeat(60));
    console.log('');

    // Config
    const BASE_URL = 'https://zeroday.hitcon.org/user/kevin2758/vulnerability';
    const DELAY_MS = 10000; // Delay between list pages
    const DETAIL_DELAY_MS = 5000; // Delay between detail pages

    // Store all reports
    let allReports = [];

    /**
     * Parse the risk level
     */
    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 '未知';
    }

    /**
     * Get the total number of pages
     */
    function getTotalPages() {
        const lastPageElem = document.querySelector('.last-page');
        return lastPageElem ? parseInt(lastPageElem.textContent.trim()) : 1;
    }

    /**
     * Fetch the HTML of a given page
     */
    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();

            // Create a temporary DOM to parse it
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            return doc;
        } catch (error) {
            console.error(`抓取第 ${pageNum} 頁失敗:`, error);
            throw error;
        }
    }

    /**
     * Fetch a detail page
     */
    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;
        }
    }

    /**
     * Extract information from a detail page
     */
    function extractDetailInfo(doc) {
        if (!doc) return {};

        const detail = {};

        try {
            // Vulnerability type
            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();
                    }
                });
            }

            // Related URLs
            const urlSection = doc.querySelector('.vu-d-url .urls');
            if (urlSection) {
                detail.相關網址 = urlSection.textContent.trim();
            }

            // Full description
            const descSection = doc.querySelector('#detail');
            if (descSection) {
                // Strip HTML tags, keep only the text
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = descSection.innerHTML;
                detail.詳細描述 = tempDiv.textContent.trim().replace(/\s+/g, ' ');
            }

            // Remediation advice
            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;
    }

    /**
     * Scrape reports from a document
     */
    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() : '';

                // Title and URL
                const titleElem = item.querySelector('h4.title a');
                const title = titleElem ? titleElem.textContent.trim() : '';
                const detailUrl = titleElem ? titleElem.getAttribute('href') : '';

                // Risk level
                const riskElem = item.querySelector('li.risk');
                const risk = riskElem ? parseRiskLevel(riskElem.classList) : '未知';

                // Status
                const statusElem = item.querySelector('li.status');
                const status = statusElem ? statusElem.textContent.replace('Status:', '').trim() : '';

                // Organization name
                const vendorElem = item.querySelector('li.vendor .v-name-short');
                const vendor = vendorElem ? vendorElem.textContent.trim() : '';

                // Date
                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}` : '',
                    // The fields below are populated in full mode
                    漏洞類型: '',
                    相關網址: '',
                    詳細描述: '',
                    修補建議: ''
                };

                reports.push(report);
            } catch (error) {
                console.error(`  解析第 ${index + 1} 筆通報時出錯:`, error);
            }
        });

        return reports;
    }

    /**
     * Convert to CSV format
     */
    function convertToCSV(reports) {
        if (reports.length === 0) return '';

        // CSV headers
        const headers = Object.keys(reports[0]);
        const csvHeaders = headers.join(',');

        // CSV rows
        const csvRows = reports.map(report => {
            return headers.map(header => {
                const value = report[header] || '';
                // Handle values containing commas, quotes, or newlines
                const escaped = value.toString().replace(/"/g, '""');
                return `"${escaped}"`;
            }).join(',');
        });

        return csvHeaders + '\n' + csvRows.join('\n');
    }

    /**
     * Download a file
     */
    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);
    }

    /**
     * Download 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('' + csv, filename, 'text/csv;charset=utf-8;');
        console.log(`✓ 已下載: ${filename}`);
    }

    /**
     * Download 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}`);
    }

    /**
     * Main execution flow
     */
    async function main() {
        try {
            // Get the total number of pages (from the current page)
            const totalPages = getTotalPages();
            console.log(`總共有 ${totalPages} 頁通報\n`);

            // Choose a mode
            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`);

            // Confirm before running
            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;
                }
            }

            // Scrape the current page first (page 1)
            console.log(`[1/${totalPages}] 正在抓取第 1 頁(當前頁)...`);
            const firstPageReports = scrapeFromDocument(document);
            allReports.push(...firstPageReports);
            console.log(`✓ 第 1 頁完成,累計 ${allReports.length} 筆通報`);

            // Scrape the remaining pages one by one
            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));

            // In full mode, scrape the detailed content of each report
            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);

                    // Merge in the detailed information
                    Object.assign(report, detailInfo);

                    if ((i + 1) % 10 === 0) {
                        console.log(`  已完成 ${i + 1}/${allReports.length} 筆`);
                    }

                    // Delay to avoid firing requests too fast
                    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));
            }

            // Show statistics
            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);

            // Download the files
            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].詳細描述 // 查看詳細描述');
            }

            // Save to the window object for easy inspection
            window.hitconReports = allReports;

        } catch (error) {
            console.error('發生錯誤:', error);
            console.log('\n如果遇到問題,可以試試:');
            console.log('1. 重新整理頁面後再執行');
            console.log('2. 確認已登入 HITCON ZeroDay');
            console.log('3. 選擇快速模式(如果完整模式失敗)');
        }
    }

    // Start
    await main();

})();

⚠️ Things to note

Limitations

  1. Network stability: Full mode takes a while, so run it when your connection is stable.
  2. Request rate: The script already includes delays to avoid putting a strain on the server.
  3. Username: Remember to change kevin2758 in BASE_URL to your own username.

Customization tips

Adjusting the delays

const DELAY_MS = 10000;        // List-page delay
const DETAIL_DELAY_MS = 5000;  // Detail-page delay

Changing the username

const BASE_URL = 'https://zeroday.hitcon.org/user/YOUR_USERNAME/vulnerability';

Scraping only the first few pages (for testing)

const totalPages = Math.min(getTotalPages(), 3); // Only scrape the first 3 pages

🎯 Wrapping up

This tool solved a long-standing pain point for me and made managing my reports far easier. By running it through the browser Console, it sidesteps Cloudflare entirely. If you're using HITCON ZeroDay too, give it a try!