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。