☰
从Heartbleed到Dirty COW:HTB Valentine靶机提权之路
2026/10/20 14:49:51 网站建设 项目流程

这台Valentine是HTB上很有年代感的一台靶机。打完它会让你有一种“把2014到2016年的安全新闻联播了一次”的感觉:先是Heartbleed(CVE-2014-0160)从内存里捞私钥打点,再顺着残留的tmux会话偷梁换柱拿到目标身份,最后用Dirty COW(CVE-2016-5195)向内核借力提权到root。整个过程没有任何花哨的0day,全是那个年代真实存在的经典漏洞利用组合。

这篇writeup适合正在刷HTB、准备OSCP,或者想系统回顾历史漏洞利用手法的朋友。我会把每条命令、每个判断逻辑、每个坑都讲清楚,不是单纯贴一遍命令流,而是站在“当时我拿到这个shell时是怎么想下一步”的角度来复盘。中间涉及的所有操作都发生在授权靶机环境中,老规矩,未授权系统上拿这些手法做测试是绝对不允许的。

1. 机器速览与初始侦察

1.1 先从端口扫描开始

HTB的机器不需要什么花哨的侦察姿势,直接全端口扫一遍最快。我习惯先跑一个快速的TCP全端口探测,再针对开放端口做服务版本识别:

nmap -p- --min-rate 1000 -T4 -oA valentine_nmap 10.10.10.99

这台机器开放了三个端口:

PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 5.9p1 Debian 5.9p1-5 (protocol 2.0) 80/tcp open http Apache httpd 2.2.22 443/tcp open ssl/http Apache httpd 2.2.22

从SSH版本和Apache版本来看,这是一台很老的Debian机器。当时我的第一反应:这种老系统上大概率有Heartbleed漏洞,因为OpenSSL 1.0.1系列直到1.0.1f之前几乎全版本中招。顺手再用nmap的脚本验证一下:

nmap --script ssl-heartbleed -p 443 10.10.10.99

结果很快就出来了,443端口确实存在心跳漏洞的指纹特征。第一步算是打完了,但真正的问题是怎么把漏洞变成实实在在的“打点”机会。

1.2 目录爆破与线索汇整

拿到一个Web入口,目录扫描是少不了的。我用gobuster扫了一轮常见字典,发现几个值得关注的路径:

gobuster dir -u https://10.10.10.99 -k -w /usr/share/wordlists/dirb/common.txt -t 50

结果里有三个比较关键的点:/dev目录、/encode和/decode这两个看起来像“加密解密工具”的页面。进入/dev目录后发现里面有几个文件,其中一个是hint文件,明确提到了“Heartbleed”相关的hint,里面还有一串看起来需要解码的内容。

顺着提示把内容用/decode页面处理一下,就能解码出一段有意义的字符串。这个步骤其实像极了真实渗透里“从README或注释里找突破口”的过程。我记得当时正好把HTTPS证书信息也看了一下,证书是自签名的,里面还有一个叫web_stencil的SVG文件,这些暂时没直接利用价值,但可以告诉我这台机器大概是个什么角色——像是某个业务机的模板页面。

到这里,整台机器的方向已经非常明确了:突破点就在HTTPS服务上的Heartbleed漏洞。难点在于怎么把漏洞利用转化为真正的shell,这一步也算是这台机器最有“考古感”的部分。

2. Heartbleed 漏洞打点

2.1 心跳机制与漏洞成因

Heartbleed漏洞本质上是OpenSSL实现TLS心跳扩展时的一个越界读取问题。TLS心跳协议本身很简单:客户端发给服务端一个“心跳请求”数据包,里面包含一段负载数据和一个长度字段,服务端再把这个负载原样回给客户端,用来确认连接还活着。

漏洞出在OpenSSL对长度字段的校验上。正常情况下,服务端应该检查“你声明要往回读的长度”是否真的小于等于“实际收到的负载长度”。但受影响的OpenSSL版本直接把长度字段当成了从内存中读取数据的大小,没有做边界检查。于是服务端会把内存里超出负载区域的一大段数据也一并返回给客户端。

这段话可能有点抽象,我用一个生活化的类比:心跳请求相当于你在信封上写了“附上10页纸”,但信封里实际只装了1页纸,服务端在回信时却直接按10页纸的量给你复印内存内容,多出来的9页就是内存里其他进程或连接留下的敏感数据。每次最多能越界读取64KB的内存,多抓几次就能从碎片里拼出大量信息。

2.2 利用脚本一步步取出内存

当时最顺手的办法是用Metasploit里的openssl_heartbleed辅助模块,设置好目标IP和端口后把VERBOSE打开,直接跑:

msf6 > use auxiliary/scanner/ssl/openssl_heartbleed msf6 auxiliary(scanner/ssl/openssl_heartbleed) > set RHOSTS 10.10.10.99 msf6 auxiliary(scanner/ssl/openssl_heartbleed) > set RPORT 443 msf6 auxiliary(scanner/ssl/openssl_heartbleed) > set VERBOSE true msf6 auxiliary(scanner/ssl/openssl_heartbleed) > run

模块跑起来后,每次会返回一段4096字节的二进制数据,这些数据里有大量看似乱码的内容,但夹杂着若干可读的ASCII字符串。我习惯把这些dump下来的原始数据重定向到本地文件里,方便后续统一分析:

msf6 auxiliary(scanner/ssl/openssl_heartbleed) > set OUTPUT_FILE /tmp/hb_dumps.bin

抓几轮之后,直接用strings加grep看有没有关键词:

strings /tmp/hb_dumps.bin | grep -Ei "BEGIN|password|user|key|mail|root"

这个方法虽然简单粗暴,却是当年Heartbleed利用中最常见的路径。如果你在写自己的利用脚本,逻辑上也一样:多次建立TLS连接,每次用不同长度的心跳请求打过去,把返回的内存片段全部保存下来,最后做字符串提取。

2.3 从内存碎片中捞到SSH私钥

在靶机的场景里,内存中被带出来的“宝藏”是OpenSSL私钥。抓取的原始数据中会反复出现BEGIN RSA PRIVATE KEY和END RSA PRIVATE KEY之间的内容。由于每次返回的内存片段可能不完整,一个完整的私钥通常需要从多段dump里拼接出来。

我的处理方式是把含私钥的片段存到独立文件里,用Python或者直接手动根据BEGIN/END标记切出中间的部分,再拼成一个完整的PEM文件。有一点需要注意:OpenSSL私钥文件首尾标记之间是BASE64编码的DER数据,拼接时不要混入多余空行或日志输出,否则SSH登录时会直接报格式错误。

保存好私钥后,先本地检查一下格式:

cat id_rsa chmod 600 id_rsa ssh-keygen -l -f id_rsa

如果是加密私钥,SSH登录会要求输入passphrase。在这台机器上,Heartbleed内存中同时还能抓到一段明文密码,这个密码就是私钥的加密口令。这也符合真实场景中“内存泄露往往能拼出不止一种敏感信息”的特点,不要死盯着私钥本身,周围的用户名、密码、cookie、会话票据都可能有用。

3. SSH落地与用户权限获取

3.1 私钥整理与登录

把私钥权限改成600之后,直接尝试SSH登录。这台机器的用户是holy,为什么是这个名字?其实在Heartbleed dump出来的内存片段里,能隐约看到holy相关的字符串,某种程度这是在提示你从内存中恢复身份信息后可以直接用这个用户名去尝试登录。

登录命令长这样:

ssh -i id_rsa holy@10.10.10.99

输入私钥的passphrase之后,就顺利进入了holy用户的shell。这一步是整个靶机里最有“考古价值”的部分:Heartbleed本身只是越权读取内存,但它泄露出来的私钥直接成为了进入系统的凭证。放在2014年,真实世界中不少电商、邮件服务器也因此被脱裤——私钥、密码、会话票据全都会被心跳请求带走。

3.2 用户目录内的细节检查

进入系统之后我按老规矩走一遍:

id whoami sudo -l ls -la /home/holy/ find / -writable -type f 2>/dev/null | head -50

holy用户没有sudo权限,但目录里藏着几个有意思的文件。一个是hint,内容继续暗示这台机器和Heartbleed、tmux有关;还有一个名为.bash_history的文件,里面能看到root用户曾经执行过的命令记录。这个历史记录里就残留着tmux会话创建和使用的痕迹,路径指向/tmp/.x。

这种“上一级用户留下的垃圾文件”在真实环境中非常常见。系统管理员跑运维脚本后忘了清理历史记录,或者把临时socket文件留在/tmp下,都是低权限用户扩大战果的好机会。

3.3 定位tmux会话

根据历史记录里的线索,我直接在/tmp下找:

ls -la /tmp/ file /tmp/.x

果不其然,一个隐藏文件.x,文件类型显示为“tmux socket”。所谓tmux socket就是tmux主控进程与终端客户端之间通信用的Unix域套接字文件。如果你知道这个socket路径而且文件权限允许,就可以直接附加上去,进入那个tmux会话。

这里的关键点在于:tmux对会话的“身份校验”非常弱。它不像SSH那样有密码或证书验证,只要系统层面允许你对socket文件有读写权限,它就会把你当成该会话的合法参与者。从安全角度讲,这可以理解为“上传了一个已经在运行的对讲机频道,你有频道地址就能进去旁听和说话”。

确认socket路径后,先用tmux -S /tmp/.x ls看看当前会话状态,能列出会话说明当前用户可以访问这个socket。

4. tmux 会话继承提权

4.1 tmux socket 机制简述

这里想稍微展开讲讲tmux本身的会话隔离设计,因为理解它才能理解“会话继承”为什么会变成一种提权手段。

tmux在运行时会创建一个后台服务进程(server),所有会话状态都由这个进程统一维护。每个会话通过一个Unix socket文件暴露出来,客户端(也就是你执行的tmux attach命令)通过socket和server通信。正常情况下,多用户环境下tmux会把socket放在/tmp下,并设置严格的权限限制其他用户访问。

但在某些运维场景下,管理员为了偷懒,会直接用tmux new -d -s task把任务挂在后台,再手动指定socket路径放在可写目录,比如/tmp/.x。如果创建会话时tmux server的umask比较宽松,或者干脆把socket权限设成了644,那么同一台机器上的低权限用户就能绕过身份验证直接接入这个会话,继承会话当前用户的能力。

这台Valentine的情况就是:root用户之前用tmux建了一个会话,socket被留在了/tmp/.x,而且holy用户对它拥有访问权。所以理论上holy可以直接附加到这个root会话里,在对方已经打开的shell中执行命令。

4.2 附加会话实现用户身份转换

实际利用非常顺滑:

tmux -S /tmp/.x attach

attach成功后会直接进入一个已打开的终端界面,前几行就是这个会话的历史输出。这个shell看起来就是个普通的root shell,验证一下:

id uid=0(root) gid=0(root) groups=0(root)

到这里,root权限就到手了。整个过程没有输入任何密码,没有利用任何内核漏洞,纯粹是“不安全的tmux遗留会话”被低权限用户捡了便宜。这也是很多老机器里常见的横向移动手法:不用再花精力去破解证书或者搞提权exp,把别人留下的会话直接占为己有。

在实际测试中有一个细节要注意:如果你tmux attach时遇到“sessions should be nested with care”之类的告警,通常是因为当前shell本身已经在一个tmux会话里了。此时可以直接按提示加上-u更新选项,或者用tmux -S /tmp/.x attach -t 会话名指定目标子会话。如果告警只是提示而不阻塞输入,直接继续操作也没问题。

4.3 拿到root之后还不急着收工

很多writeup到这里就结束了,因为root shell已经入手。但我个人习惯是先读flag,再顺手确认一下这台机器上的其他可实验漏洞。这台Valentine的内核版本是Linux 3.2.0-4-amd64,这个版本正好处在Dirty COW漏洞的影响范围内。

既然都已经到root了,为什么还要再把Dirty COW过一遍?主要原因有两个:一是这种老内核机器非常适合作为Dirty COW的练习场,在真实工作中你遇到一台老Linux服务器时要能想起来这个漏洞,并能快速利用;二是如果tmux会话这条路径在某些情况下被修补了(比如socket权限被修正了),Dirty COW就是替补提权路线。所以我决定在拿到root之后再做一次完整的Dirty COW提权复现,下面这一部分也写上。

5. Dirty COW 提权补充验证

5.1 内核版本与漏洞匹配检查

Dirty COW全称Dirty Copy-On-Write,编号CVE-2016-5195。它影响Linux内核的get_user_page机制,允许本地用户利用竞态条件修改只读文件的映射,即使该文件本来是只读且不可写的。实际利用中最经典的打法就是直接覆盖/usr/bin/passwd里的用户记录,往/etc/passwd中写入一个新的root权限账号。

先确认内核版本:

uname -a cat /etc/os-release

这台机器是Debian 7系列,内核3.2.0-4-amd64,明显处于受影响范围内(Linux kernel版本在4.8.3之前且已合并2.4.x分支的某些版本都有问题)。确认后就可以找exp编译。

5.2 利用源码编译与执行

当时我用的是社区里流传比较广的dirty.c源码,这个exp通过ptrace加/proc/self/mem写入的方式触发race condition。源码本身不复杂,但有几个细节很容易踩坑。

首先编译时要链上crypt库,因为exp需要生成加密后的密码写入passwd的密码字段:

gcc -pthread dirty.c -o dirty -lcrypt

如果本地系统缺少libcrypt-dev之类的开发库,可以用apt-get install libcrypt-dev补上,或者在靶机上直接用靶机自带的gcc编译。老Debian 7默认自带了gcc 4.7系列,直接编译没有问题。

编译完成后运行:

./dirty 123456

这里的参数是给新建的root用户设置密码。exp运行时会提示你选择一个新用户名,默认是firefart,如果你不改它,它就会覆盖/etc/passwd。覆盖的过程本身比较粗暴:把passwd文件备份后生成一个新的passwd,在这个新文件里输入一个新用户条目,UID和GID都设为0。

我等了大概几十秒,exp输出成功信息。然后按它提示的内容再执行:

su firefart

输入刚才设置的密码,就能直接切到root身份。最后最要紧的一步,立刻恢复原始的passwd文件,避免系统账户信息被污染:

cp /tmp/passwd.bak /etc/passwd sync

这台机器上我用tmux已经拿到了root,再用Dirty COW过一遍纯属复现验证,所以没有长时间保留被篡改的passwd。在实际渗透测试中,如果为了保持系统稳定性,同样建议拿到shell后尽快恢复原始文件。

5.3 提权回显与系统稳定措施

Dirty COW利用完成后要格外注意两点:第一,exp运行期间会大量触发内核竞态,系统可能变得卡顿,这时候不要急躁,正常情况下几十秒内能写完passwd文件;第二,如果系统是只读挂载或者/etc/passwd所在分区不可写,需要先重新挂载为rw,例如mount -o remount,rw /。虽然老Debian默认不太会遇到这个情况,但我曾经在模拟环境的容器里遇到过,提前留意能省不少时间。

另外提醒一下,Dirty COW本身是一个内核级漏洞,利用过程可能触发内核告警日志。查看dmesg时如果看到与COW相关的异常输出,属于正常现象,不用纠结。

6. 问题排查与绕过实录

6.1 Heartbleed抓取成功率低怎么办

在实际打点阶段,Heartbleed利用经常会遇到一种情况:脚本跑了很多轮,但返回的内存片段几乎都是重复的,或者全是无意义的空数据。原因是服务端如果接受了多个心跳请求,每次返回的内存偏移可能相同,所以重复数据比例很高。

我当时的处理办法是放弃单次dump,转而循环多次请求,把每次的片段都存下来,再用个小脚本提取不同偏移位置上的数据。如果用Metasploit,可以把ACTION设置为DUMP模式,这时模块会自动抓取多轮并保持文件。若用第三方脚本,核心思路是多个连接并发打,加大数据样本的多样性和覆盖率。

6.2 tmux attach 握手失败原因

tmux附加失败主要有三类原因。第一类是socket文件权限不够,会直接报权限拒绝,解决办法是检查当前用户是否属于socket文件属主或所属组,或者把socket权限调成可读可写。第二类是当前终端嵌套在另一个tmux会话里,容易遇到嵌套警告,按前面说的用-u选项或指定子会话名解决。第三类是socket文件虽然存在,但对应的tmux server进程已经退出,表现是attach时卡住或直接报错,这时socket文件就是个僵尸文件,没有利用价值。

6.3 Dirty COW 编译错误的内核差异

不同内核版本对dirty.c的兼容性差异主要集中在内核头文件引用和offsetof宏的冲突上。如果在编译时看到/usr/include/linux/...路径下的头文件报错,多数是因为系统安装的libc-dev版本过新而内核源码较旧。一个简单粗暴的解决方法是先执行make clean,然后把源码里引用的#include <linux/...>替换成相对路径或者删掉冗余引用,多数exp能直接编译过。要是gcc版本太老导致语法不兼容,就换个变体exp,社区里针对不同内核版本有很多修改版。

6.4 经验清单:这类老漏洞机器的通用打法

最后把这种老靶机的通用思路整理一遍,方便以后遇到类似机器时快速对照:

  1. 看到老版本OpenSSL,第一件事就是验证Heartbleed,别急着爆破目录。
  2. Heartbleed抓回来的内存不要直接肉眼翻,优先搜BEGIN、password、user、token这些关键字。
  3. 拿到SSH私钥后,尝试登录多个可能用户,不要只试root或当前默认用户名。
  4. 低权限用户目录下的.bash_history一定要看,经常能翻到上一个用户的操作流。
  5. 在/tmp、/var/tmp、/dev/shm里找隐藏文件和socket文件,用file命令判断类型,看到tmux或screen相关的socket就试着附加进去。
  6. 老内核先拿uname -a与CVE库对照,遇到3.2、3.8、4.4附近版本一定要想到Dirty COW这一大类。

我在实际打完这台机器后最大的体会是:很多在当年风靡一时的漏洞利用,放到今天看其实都变成了“开箱即用”的固定套路。但恰恰是这些老套路,把系统权限模型里的一些根本问题完完整整暴露了出来——内存越界读取、会话身份信任、内核COW机制的逻辑漏洞,每一个放到今天的现代系统里都还有类似的变形。对想打好渗透基础的人来说,Valentine是一台非常值得手动复现的靶机,尤其是Heartbleed那一环,值得你自己写脚本多玩几遍,而不是只跑Metasploit模块就完事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询