专题 web网站渗透 第 4 / 4 篇

拿到Shell后。你还要知道什么?

清酒 13 分钟 59 次读过

后渗透

声明

本文涉及的操作均在本地授权的渗透测试环境(多层内网靶场)中完成,仅用于安全学习与防御加固参考。

从上一阶段结束之后你手里就多了一样东西。屏幕里多出了一道命令提示符,能敲命令,能看文件,像是机器好像已经归你了。

这一阶段要说明的是:能使用目标的命令行并不等同于拿到控制权。

image

进去了之后先看一眼脚底下,你在哪?

光靠一个命令提示符的代表含义其实很窄,它只是说明你在一个进程的上下文里面拿到了一部分执行命令的能力,至于这个进程属于谁、跑在什么上面、能碰到什么东西,光一个命令行什么都看不出来。

三种情况最常见。

  • 缺少权限

Web服务通常是以www-data、nginx这种低权限身份运行,只要有加固,你拿到的会话也大概率是这样的身份。只能翻自己的目录,无法更改系统配置,更无法读取别人的家目录,也安装不了软件。

  • 不在那台服务器上

很多人在进入命令行里都会默认自己已经在服务器里,但是现在应用跑在容器里面也基本是默认的选项了,你进入的可能只是一个容器,而不一定是宿主。

  • 连接不一定稳定

基本的会话为了保持畅通都会建立在一条反向连接上,如果出现网络重启、网络抖动、对方清理一次进程,都会影响连接,这样的连接又不一定能再重现一次。

小结

在进入后的第一件事并不是继续完成渗透,而是进行一次盘点。

我要知道什么信息

盘点的动作可以压缩成四问:Linux和Windows有两种不同的问法。

回答什么问题 Linux怎么看 Windows怎么看
我是谁 id whoami /user、whami /groups、whoami /priv
这台机器什么样 uname -a、cat /etc/os-release systeminfo、wmic qfe list
我在这里能做什么 sudo -l whoami /priv返回的特权清单
我能去哪里 ip addr、route、arp -a ipconfig /add、route print、arp -a

Windows的那一条whoami /user的返回结果主要看末尾的数字,500以内的是内置管理员,501为来来宾,1000以上的都是为新建的普通账号。

image

特权清单也要看具体项。SeDebugPrivilege(调试程序)、SeBackupPrivilege(备份文件目录)、SeLoadDriverPrivilege(加载和卸载设备驱动程序)这一类权限如果显示已启用,说明这个身份能做的事情有很多。可以用来做本地提权的着力点,无论是攻还是防都要重点关注。

image

Linux上的sudo -l所呈现的结果很直接。直接出现了(ALL)NOPASSWD:ALL,这就代表这个用户可以以任意身份(包括root)执行任意命令。

image

网段的作用

这个问题很容易被忽略,落点往往很重要,如果一台机器连接着多个网段,就代表又能这台机器同时连接着内外网有两张网卡,这样的机器就可以选择做内网横向的跳板。

三、也许你根本不在这个机器上

这是目前网上写的四行的容器检测清单,目前有两条不太好用了:

  • 根目录有没有/.dockernv这个文件
  • cat /proc/1/cgroup里有没有docker、kubepods字样
  • systemd-detect-virt是否返回docker或者是none
  • 根目录有没有/ver/run/docker.sock

逐个拿来拆开了看比直接抄要有用的多。

/.dockerenv能用,且它基本是单向的。这个文件的来源是由docker创建的,containerd、CRI-O、Podman、Kubernetes都没有。所以有这个文件基本可以判定是Docker容器;没有它也什么都说明不了。它只是一个空文件,想要删掉或者伪造都很容易。

image

cgroup很容易误判。/docker/<id>、/kubepode/...这样的字样是cgroup v1的产物。像cgroup v2把所有的层级都给统一了,容器里打开的/proc/1/cgroup也就只有这一行。

image

一个"docker"的字样也没有。这时候如果只是照着清单判断的话很容易判定“不是在容器以内”——但是恰恰相反我是在一台跑着Spring Boot的靶机上实测的,它百分百是容器。

systemd-detect-virt有两个问题。首先它属于systemd,精简一点的镜像里没有装,敲下去返回的只有command not found;其次它返回的值不只有"docker或者none"二选一,它能给出的还有lxc、podman、kvm、qemu等多种结果,在cgroup v2下还经常给出none。用这一条做依据,基本等于用不可能得到的东西下结论。

docker.sodk的整体方向没有问题,但是写法有些窄。他可能被挂到任意一个路径,只有ls一个位置会漏,按照类型找:find / -xdev -type s。更一点是,文件在也不等同于能用——还需要用 curl --unix-socket /var/run/docker.sock http://localhost/version只有返回200,才能算真的能指挥外面的守护进程。

想要更稳可以看看挂载表:

head -3 /proc/self/mountinfo

根挂载的是overlay,路径里带着io.containerd.snapshotter.v1.overlayfs/snapshots/明显看得出来自己正坐在镜像分层上。这个不调cgroup版本,v1、v2都能成立。

image

可以再看看其他的旁证:

容器的证据

主机名是一串十六进制(Docker默认取容器ID的前12位,如:1e9286ab15e9);PID1是你应用自己的进程而不是systemd;/prov下只有个位数的进程;没有/boot;/dev里没有宿主的设备。

在确认自己进入的是容器后先不要急着提权,更重要的是看看能不能通往外界。

先检查是否是容器特权:

grep CapEff /proc/self/status

如果输出是 0000003fffffffff那就说明它是用--privileged拉起来的,相当于容器没关门;我的那台结果是00000000a80425fb,Docker 的默认能力集,这道门是关着的。

image

第二件事才是轮到socket。他如果能访问的话量级就变了:它是和宿主Docker守护进程通信的接口,能碰到它,就能让宿主替你起一个挂载了宿主根目录的新容器,容器这层边界当场被推出去。

但边界往外通不通,不止这两条路。共享宿主 PID 命名空间(能看见宿主的全部进程)、共享宿主网络、把宿主目录挂进来(哪怕只读,也够读走 /etc/shadow)、cgroup v1 下可写的 release_agent,都是同一个量级的口子。最根本的一条是:容器和宿主共用内核,内核漏洞天然就是逃逸面,跟 socket 在不在无关。所以"容器是隔离边界"这话本身就要打折,它是一道比虚拟机薄得多的墙。

还有一点容易写错。我的靶机得到的 uname -r如下:

image

它是跑在Docker Desktop的WSL2虚拟机上,如果从容器里逃逸出去拿到的也只是一台虚拟机的root,而不是物理机的。容器、虚拟机、物理机,三层结论要一层层写。

报告怎么写

所以如果有报告的话不用去写“已拿下宿主机”,只写自己拿到的实际东西:容器内root,非特权,未见docker.sock挂载,外层是WSL2虚拟机。

四、转折点,这个地方开始就不用漏洞了

前面几个阶段讲的是:找到目标在跑什么软件,查版本号,比对公开的漏洞编号,然后找一个能用的攻击代码进行渗透。整个逻辑主要围绕漏洞展开。

走到这一步,这套逻辑就变了。

就拿一套多层的内网靶场举例,它的后半段的三个机器是这样的情况:一个正常的Redis服务、一个正常的phpMyAdmin服务、一个正常的PostgerSQL服务。

这三个的问题主要出现在弱密码上,与任何漏洞编号没有任何的关系,因为软件本身都只是按照设计工作,问题出现在别处。

image

前半程 后半程
依托 软件的缺陷 系统的正常功能
凭证来源 不需要 密码、哈希、票据、私钥、配置文件
进门方式 绕过认证 走正门,用合规的流程登录
难点 找到能用的攻击代码 凑齐能用的证据

前半程就像撬锁,后半程像配钥匙。因为后半程使用系统本来就认的东西。

这也能体现出来,为什么日志上很难看出异常。一次登录请求的格式、走的路径和触发的事件和一次远程登录没有什么区别。真正留下的差别往往在登录来源、频次这些外围信息中。

既然路要从该凭据先走就要知道凭据的来源在哪。大概有两类。

一类是系统为了不让人反复输入密码暂存的。例如Windows上的登录会话、缓存的票据、内存中留存的哈希都是,而Linux上有sudo的票据缓存。这一类东西跟着登录状态走,一人注销就少一大半。

另一类就是为了省事被人所以写死在了某处。如SSH私钥文件、应用的配置文件、数据库连接串、脚本中的环境变量,都是属于这一类。

怎么预防被利用?

第一类:只能靠缩短留存的时间防止窗口出现。

第二类:是完全可以避免的。配置文件的读权限,并且私钥不要跟代码放一起、生产环境该不该全网用一个连接串,这些都比装防护插件有用。

五、想横向移动,要同时凑齐这三件事

凭据的影响也是有范围的,如果想去哪就去哪,这三个条件必须全部成立。

前提 不成立会怎么样
目标上要有对应权利 凭据不够权限,几种走法全部失败
端口能连得上 网络不通,凭据就算对了也没有用
接受留下痕迹 创建服务、创建进程、登录事件,差别就在于留下哪一种

第二条是多层内网里最容易被忽略的一条。还是用靶场举例子:这六个容器分别在三个网段,后面两个网段配的是内部网络,从外面无法连接。你使用最外层拿到的shell来扫内网能看见的只有同网段的一两台。

image

能继续往前走靠的是有同一个机器上有两张网卡。这样的机器在网络结构里就是跳板,它的价值不在于有什么,而是它的网卡能连接到哪里,找到了它就相当于找到了下一跳。

image

这个/proc/net/rpite里的地址和掩码是小端四字节十六进制,要把字节倒过来读:0064A8C0取00 64 A8 C0,倒过来就是C0 A8 64 00转为十进制也就是192.168.100.0;再把0065550A倒为0A 55 65 00就是10.85.101.0,就得到了这两条路线。第二个网段不是外面能够得着的。

这里就有一个问题:如果你现在到了内部网络,那你的手里应该是没有Nmap的,也没有nc,也可能没有ping。busybox里写echo > /dev/tcp/10.0.0.1/80这是很多人都背过的技巧,但是busybox的ash根本不支持这些语法,他会给你虚假的内容:端口明明开着但是,你得到的结论是closed。这时候是来行的Python socket脚本要更可靠,因为用的是最基础的connect_ex,不依赖任何命令存在于否。

要防守的不只是对外主机

防守方正好可以把这个结论反过来。真正要注意的不是只有对外开放服务的那一台主机,还有内部网络中同时跨着两个区域的主机。

六、五种走法,每个痕迹都不一样

Windows环境中横向的走法主要是这五种,差别不是能不能成,而是各自会留下什么痕迹。

走法 主要日志 磁盘中有没有残留
PSEXEC 4697、4624类型3 目标在 PSEXESVC.exe
SMBEXEC 4624 类型 3 写临时 .bat 文件
WMIEXEC 4688、父进程 wmiprvse.exe 纯内存,无落盘
ATEXEC 4698、4702 写任务 XML 文件
WinRM 4104、父进程 wsmprovhost.exe 纯内存,无落盘

第一列是人尽皆知的那部分,第二列才是能用来查案的东西。有文件落盘的走法,事后做取证最容易追,文件名、路径、时间都对得上;纯内存的走法磁盘上干干净净,只能靠进程行为去发现。

哈希传递和票据传递是另外两条路,本质上都是不碰明文密码、直接拿凭证的中间形态去登录。它们留下的线索也有固定位置:远程登录事件里的登录类型字段,以及 Kerberos 票据请求里出现异常的加密类型。

七、怎么防

对应的防守动作围绕破坏那三个前提展开。

第一,掐掉端口可达。 内部网段之间按需放通,办公网不该能直连到服务器的 445、135、5985 这些端口。

网段一层一层切开,攻击者每绕一次就得多一次现身。

第二,别让一个密码打通所有机器。 本地管理员账号在不同机器上用同一个密码,是哈希类手法能扩散开的前提。做本地账号密码的差异化,成本不高,性价比很高。

第三,管住凭据在内存里的留存。 关掉明文凭据缓存、开启对关键进程的内存保护,能直接抬高抓取的难度。这类配置在域环境里可以统一推送,属于一次性投入长期见效的那类。

第四,把该采的日志采上来。 事件转发集中到一处,盯住服务创建、任务创建、异常的远程登录这三类。手法可以换,落地到系统上总要触发其中的某一类,区别只是留哪一种。

四条里面,第一条最硬。网络不通的时候,后面几条就算全失守,攻击者也只能停在原地。

最后

这个阶段的名字听起来很带感,实际做的事相当枯燥:问清楚自己是谁、在哪台机器上有多大权限、哪些网段够得着、手上的凭据能用在哪。想明白这些,剩下的动作才谈得上选择。

如果只带走一句话,那就是拿到会话之后先别急着往前冲。先把上面那四个问题问完,再决定下一步往哪走。多数时候你会发现,顺着那个思路走出来的路,跟你凭感觉找的完全不是同一条。

有牛 写于 2026.10.10