[BJDCTF 2020]Ezphp

发现神秘base32字符,拉到cyberchef解一下msedge_6KfUAHsjnf

msedge_VpGzQgKuIc

访问对应路径拿到源码

<?php
highlight_file(__FILE__);
error_reporting(0); 

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) { 
    if (
        preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
        )  
        die('You seem to want to do something bad?'); 
}

if (!preg_match('/http|https/i', $_GET['file'])) {
    if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') { 
        $file = $_GET["file"]; 
        echo "Neeeeee! Good Job!<br>";
    } 
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) { 
    foreach($_REQUEST as $value) { 
        if(preg_match('/[a-zA-Z]/i', $value))  
            die('fxck you! I hate English!'); 
    } 
} 

if (file_get_contents($file) !== 'debu_debu_aqua')
    die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
    extract($_GET["flag"]);
    echo "Very good! you know my password. But what is flag?<br>";
} else{
    die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) || 
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) { 
    die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w="); 
} else { 
    include "flag.php";
    $code('', $arg); 
} ?>
This is a very simple challenge and if you solve it I will give you a flag. Good Luck!
fxck you! I hate English!

哇,这么多正则匹配看的好难受。这哪是ezphp,又骗我

这道题非常复杂,先来说下考点

考点

总共有四个变量(PHP超全局变量)

变量 说明
$_GET URL查询字符串中的参数(?a=1
$_POST POST请求体中的参数
$_REQUEST 默认包含$_GET$_POST$_COOKIEPOST覆盖GET
$_SERVER['QUERY_STRING'] URL解码前的原始查询字符串

$_SERVER['QUERY_STRING'] 拿到的是未经URL解码的原始字符串,为了绕过$_SERVER['QUERY_STRING'] ,可以对参数名做URL编码,就可以绕过shana、debu等关键词。

PHP正则匹配陷阱:

preg_match('/^xxx$/', $str)非多行模式下:

  • ^ 匹配字符串开头
  • $ 匹配字符串末尾(不包括末尾的换行符)
  • 如果字符串是 "xxx\n"$ 仍然能匹配到 xxx 后面的位置,所以正则通过

SHA1数组绕过:

sha1([])      // 返回 NULL
sha1([1])     // 返回 NULL
sha1([2])     // 返回 NULL
NULL === NULL // true

传入数组时,sha1()返回NULL,两个NULL相等。同时两个不同的数组([1][2])不相等,满足$a != $b

create_

create_function('$arg', 'echo $arg;');
// 等价于创建匿名函数:
// function anonymous($arg) { echo $arg; }

如果第二个参数是;}phpinfo();//

create_function('', ';}phpinfo();//');
// 实际生成的代码:
// function anonymous() { ;}phpinfo();// }

先闭合前面的{,然后执行phpinfo(),后面的//注释掉多余的}

取反(~

PHP中~是按位取反运算符。对字符串取反会得到不可见字符:

echo ~"phpinfo";
// 输出不可见字符(二进制数据)

这些不可见字符不包含字母,可以绕过/[a-zA-Z]/的正则检查。直接写取反,在执行时会得到phpinfo原指令

PHP伪协议

协议 用途
data://text/plain,内容 直接读取文本内容
php://filter/convert.base64-encode/resource=文件 Base64编码读取文件

然后就是怎么来绕过我们的大WAF

绕过

第一层:$_SERVER['QUERY_STRING'] 黑名单

preg_match('/shana|debu|aqua|cute|arg|code|flag|system|.../', $_SERVER['QUERY_STRING'])

黑名单匹配的是原始查询字符串,只要我们对字符串进行URL编码就能绕过,URL编码之后只有%和数字

第二层:debu参数正则陷阱

if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute')

这里就有两个要求

一是 debu必须匹配正则/^aqua_is_cute$/(这个正则匹配的意思是必须精准匹配字符串"aqua_is_cute"

二是 debu不能严格等于"aqua_is_cute"

我们可以通过在后面加一个换行符来绕过%0a(\n)

第三层:$_REQUEST无字母过滤

foreach($_REQUEST as $value) { 
    if(preg_match('/[a-zA-Z]/i', $value)) die();
}

不能有任何字母说是,He hate English还要用英语说话

我们可以利用$_REQUESTPOST覆盖GET特性

请求方式 参数
GET debu aqua_is_cute%0a(含字母)
POST debu 1(纯数字)

$_REQUEST合并时,POST覆盖GET,最终debu的值是1,无字母。其他参数同理

第四层:file_get_contents内容校验

if (file_get_contents($file) !== 'debu_debu_aqua') die();

这里要求$file指向的内容必须是debu_debu_aqua,那么用data://伪协议直接提供内容

data://text/plain,debu_debu_aqua

这个字符串包含字母,会被第三层过滤。在GET中传data://(URL编码),POST中传file=1覆盖,但$file变量已经在第二层条件通过后被赋值为GET的值,所以最终$file的值是data://text/plain,debu_debu_aqua

第五层:SHA1弱比较

if (sha1($shana) === sha1($passwd) && $shana != $passwd)

绕过这个,我们要传入数组

shana[]=1
passwd[]=2

这样

  • sha1([])返回NULL
  • NULL === NULL → true
  • [1] != [2] → true

第六层:$code和$arg双重过滤

if(preg_match('/^[a-z0-9]*$/isD', $code) || 
   preg_match('/fil|cat|flag|.../', $arg)) { 
    die();
}
条件 正则 匹配条件 结果
条件1 /^[a-z0-9]*$/isD 匹配 $code $code 全是小写字母或数字 preg_match() = 1触发 die
条件2 黑名单正则匹配 $arg $arg 包含黑名单中的词 preg_match() = 1触发 die

$code$arg这两个变量来源于extract($_GET["flag"]);

所以$code$arg来自GET参数flag[code]flag[arg]

这两个参数我们传什么呢,最后一行代码是

$code('', $arg);

如果 $code = "create_function",就变成了:

create_function('', $arg);

create_function 是 PHP 内置函数,作用是动态创建匿名函数

语法:

create_function('参数列表', '函数体代码');

例如:

create_function('$a', 'return $a*2;');
// 等价于
function anonymous($a) { return $a*2; }

那么我们的create_fuction能通过吗

可以的,它包含下划线,并不是纯字母

$arg呢?我们的目标是读取flag,黑名单里面过滤了很多函数,比如readfile('flag.php')file_get_contents('flag.php')include 'flag.php'require 'flag.php'等等

我们的require不在黑名单中,就用它了

Payload

取反构造读取flag的字符串

目标:读取rea1fl4g.php文件(根据题目提示,flag在这个文件中)

// 需要执行的代码
require("php://filter/convert.base64-encode/resource=rea1fl4g.php");

我们要生成取反之后字符的URL编码,大概这样

$cmd = "php://filter/convert.base64-encode/resource=rea1fl4g.php";
echo urlencode(~$cmd);

取反后的字符串包含不可见字符,在URL中必须用%编码传递。PHP接收到后,~'...'会先解URL编码,再取反,得到原始字符串。

create_function注入

create_function('', ';}require(~"...");//')
// 生成:
function anonymous() { ;}require(~"...");// }
// 先执行 require(...)

呃。我这边拿不到flag,所以只写个原理吧()