Day1
AWDP
ezjs
1.Fix
大概把所有的路径穿越给 ban 掉就好了
用了一些奇怪的操作:new URL(`http://123/${path}`).pathname
,这样得到的就是过滤后的正常路径了。
patch 如下:
*** app.js Tue Jul 23 10:30:21 2024 --- app-fix.js Tue Jul 23 10:30:21 2024 *************** *** 7,14 **** const path = require("path"); function createDirectoriesForFilePath(filePath) { ! const dirname = path.dirname(filePath); fs.mkdirSync(dirname, { recursive: true }); } --- 7,17 ---- const path = require("path"); + function filter(path){ + return new URL(`http://1/${path}`).pathname + } function createDirectoriesForFilePath(filePath) { ! const dirname = path.dirname(filter(filePath)); fs.mkdirSync(dirname, { recursive: true }); } *************** *** 105,113 **** return res.status(400).send('Filename parameter is required'); } ! const filePath = path.join(__dirname, 'uploads', filename); ! if (filePath.endsWith('.ejs')) { return res.status(400).send('Invalid file type.'); } --- 108,116 ---- return res.status(400).send('Filename parameter is required'); } ! const filePath = path.join(__dirname, 'uploads', filter(filename)); ! if (filePath.includes('.ejs')) { return res.status(400).send('Invalid file type.'); } *************** *** 132,141 **** } const new_file = newPath.toLowerCase(); ! const oldFilePath = path.join(__dirname, 'uploads', oldPath); ! const newFilePath = path.join(__dirname, 'uploads', new_file); ! if (newFilePath.endsWith('.ejs')){ return res.status(400).send('Invalid file type.'); } if (!oldPath) { --- 135,144 ---- } const new_file = newPath.toLowerCase(); ! const oldFilePath = path.join(__dirname, 'uploads', filter(oldPath)); ! const newFilePath = path.join(__dirname, 'uploads', filter(new_file)); ! if (newFilePath.includes('.ejs')){ return res.status(400).send('Invalid file type.'); } if (!oldPath) {
2.Break
打的时候没想到,后面才想到的
方法是往 node_modules 里写入文件比如 node_modules/ejss/index.js
,然后通过 render 渲染 ejss
为后缀的文件,即可做到RCE。
ShareCard
1.Fix
比赛的时候用的 filter,把所有传来的参数过滤一下即可。
*** app.py Thu Jul 11 14:33:06 2024 --- app-fix.py Tue Jul 23 10:50:42 2024 *************** *** 15,20 **** --- 15,26 ---- def is_safe_callable(self, obj) -> bool: return False + def filter(str): + banned_chrs = "{}%[]_" + for ch in banned_chrs: + if ch in str: + return False + return True class Info(BaseModel): name: str *************** *** 34,40 **** def create_card(): if request.method == "GET": return safer_render_template("create.html") ! if request.form.get('style')!=None: open('templates/style.css','w').write(request.form.get('style')) info=Info(**request.form) if info.avatar not in os.listdir('avatars'): --- 40,46 ---- def create_card(): if request.method == "GET": return safer_render_template("create.html") ! if request.form.get('style')!=None and filter(request.form.get('style')): open('templates/style.css','w').write(request.form.get('style')) info=Info(**request.form) if info.avatar not in os.listdir('avatars'): *************** *** 45,59 **** --- 51,71 ---- qrcode.make(share_url).save(qr_img,'png') qr_img.seek(0) share_img = base64.b64encode(qr_img.getvalue()).decode() + if filter(share_url): return safer_render_template("created.html", share_url=share_url, share_img=share_img) + else: + return safer_render_template("created.html") @app.route("/showCard", methods=["GET"]) def show_card(): token = request.args.get("token") data = jwt.decode(token, rsakey.publickey().exportKey(), algorithms=jwt.algorithms.get_default_algorithms()) info = Info(**data) + if filter (info.name) and filter(info.signature) and info.avatar: info.parse_avatar() return safer_render_template("show.html", info=info) + else: + return safer_render_template("show.html", info=None) @app.route("/", methods=["GET"]) def index():
赛后发现很好修,把 render 的 sandbox 改回原来的类就行了
*** app.py Thu Jul 11 14:33:06 2024 --- app-fix.py Tue Jul 23 10:41:17 2024 *************** *** 24,30 **** self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode() def safer_render_template(template_name, **kwargs): ! env = SaferSandboxedEnvironment(loader=current_app.jinja_env.loader) return env.from_string(open('templates/'+template_name).read()).render(**kwargs) app = Flask(__name__) --- 24,30 ---- self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode() def safer_render_template(template_name, **kwargs): ! env = SandboxedEnvironment(loader=current_app.jinja_env.loader) return env.from_string(open('templates/'+template_name).read()).render(**kwargs) app = Flask(__name__)
2.Break
因为只给了读权限,走style.css SSTI 拿全局变量的 RSA key,然后走前面打路径穿越
poc:
{{ info.__class__.parse_avatar.__globals__.rsakey.__dict__ }}
update:比赛时没做出来,思路来自@mnixry 师傅
剩下两个 java 没看是队友打的,不管了
Day2
Pentest
给了两个可以打的公网 ip。一个是企业官网和考勤数据,另一个是 springboot 的好像 Ruoyi 的服务端(ERW)。
考勤数据可以注册 admin 覆盖密码提权(后面没看
ERW
Ruoyi 走堆泄露的 api 端口 heapdump
得到 shirokey,可以用 rememberme 洞拿到 rce,种马之后加用户或者 .ssh/authorized_keys
连 ssh 进内网。
内网有八台机子对应八道题,用 fscan 扫一下基本能匹配上。
Jenkins
直接扫弱密码,能扫到admin admin123
,登录进去走 script console 拿到命令执行,可以选择用 msf 进行后渗透操作。
PC-…88(忘名字了
只开了 3306,弱密码 root 123456
连上然后任意写文件上传 sys_exec库得到无回显 RCE。同样用 msf 拿到完整 shell 之后后渗透,提权开端口。
RODC
上一台中连上 RDP 之后发现桌面上 wps 点开发现有 RODC 主机上用户名和密码,随便连一个都是管理员权限。
Gitlab
Jenkins后台有 Gitlab 的token,拿到之后可以对 Gitlab 进行操作得到 RCE。
(好像是原题,可以参考https://fushuling.com/index.php/2023/10/10/春秋云境·privilege/
DC
打的太慢了,做到这里没什么时间了。
似乎是打黄金票据从RODC 提升到 DC。