# CVE-2019-3396 Velocity Server Side Template Injection ## Setup Link download: `https://www.atlassian.com/software/confluence/downloads/binary/atlassian-confluence-6.9.0.zip` Ở đây mình sử dụng JDK và JRE bản 1.8 nha. ```! java -version java version "1.8.0_181" Java(TM) SE Runtime Environment (build 1.8.0_181-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode) ``` Nếu các bạn gặp lỗi `JRE_HOME "C:\Program Files\Java\jdk1.8.0_181\jre" contains spaces. Please change to a location without spaces if this causes problems` thì các bạn có thể thay thế `Program Files` thành `C:\PROGRA~1\Java\jdk1.8.0_181\jre`. Tiếp theo đó cần phải setup database `PostgreSQL`, ở đây mình sử dụng file `docker-compose.yml` để setup. Lưu ý là cần phải để ở cùng interface network với Confluence. ```yml! version: '2' services: db: image: postgres:12.8-alpine ports: - 5432:5432 environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=confluence - POSTFRES_USER=postgres ``` Mình gom lại thư viện bằng command này nhằm add vào intellij phục vụ cho việc debug: ```powershell! mkdir confluence_libs;Get-ChildItem -Path . -Filter *.jar -Recurse | ForEach-Object {Copy-Item $_.FullName -Destination confluence_libs} ``` Tiếp theo đó là setup JVM debug ở file `bin/setenv.bat`: `set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 ` Các bạn nhớ set giá trị cho `confluence.home` ở file `confluence/WEB-INF/classes/confluence-init.properties` Mình đặt là như này: `confluence.home=C:/Users/khanh/Downloads/confluence_data` Sau khi các bước cài đặt hoàn tất, nếu thấy trang này thì việc cài đặt đã hoàn tất. ![image](https://hackmd.io/_uploads/BJKjc5tyA.png) ## Phân tích Đầu tiên các bạn tạo một page mới như này: + `Create -> Blank page` ![image](https://hackmd.io/_uploads/S1xu6F5y0.png) + Sau khi vào page vừa mới tạo, bấm save lại. ![image](https://hackmd.io/_uploads/HJHiaY9yC.png) + Bấm vào edit, sau đó bấm `+` rồi chọn `Others macros`, tìm `widget Connector` ![image](https://hackmd.io/_uploads/SJ110KcJA.png) ![image](https://hackmd.io/_uploads/By_-0KcJR.png) + Ở đây mình điền vào thông tin như này: ![image](https://hackmd.io/_uploads/r1F4CK51C.png) + Sau đó, các bạn bấm vào preview, rồi xem các HTTP request ở Burp. Ở đây có một request đến preview, đem nó vào repeater. ![image](https://hackmd.io/_uploads/SJSuAK51C.png) + Giờ chúng ta thêm vào một parameter ở `params` là `_template` với giá trị là file chúng ta muốn đọc. ![image](https://hackmd.io/_uploads/r1vkycc1C.png) Why? + Để ý thì ở đây chúng ta có GET HTTP request, thông tin trả về là một class của `Widget Connector` được gọi đến nên giờ chúng ta sẽ vào trong đó coi xem sao. ![image](https://hackmd.io/_uploads/ryiVJ5qJ0.png) + Trong file jar của thư viện thì chúng ta có thấy một class khả nghi là `Widget Macro` nên giờ chúng ta sẽ thử vào đây coi xem sao, đặt breakpoint ở chỗ hàm `execute`. ![image](https://hackmd.io/_uploads/BJ26kq9kC.png) + Thấy được là hàm `execute` đã trigger breakpoint, bây giờ chúng ta sẽ trace nó thôi. ![image](https://hackmd.io/_uploads/r1qQx9cJ0.png) + Ở đây chúng ta thấy được là nó đã call đến `this.renderManager.getEmbeddedHtml` ở `com/atlassian/confluence/extra/widgetconnector/RenderManager.class`, giờ vào đó rồi tìm đến implemention của hàm `getEmbeddedHtml` ![image](https://hackmd.io/_uploads/SJ6Fecqy0.png) + Ở đây hàm `renderSupporter.iterator` nhằm tìm ra loại render nào phù hợp với URL mà chúng ta đưa vào. ![image](https://hackmd.io/_uploads/Hyyjlq5JC.png) ![image](https://hackmd.io/_uploads/SJUx-cqkC.png) + Sau khi tìm ra được thì nó sẽ gọi đến điều kiện if để check xem liệu nó có match với URL regex ở `com/atlassian/confluence/extra/widgetconnector/video/YoutubeRenderer.class/YOUTUBE_URL_PATTERN` ![image](https://hackmd.io/_uploads/HkROW99yC.png) ![image](https://hackmd.io/_uploads/HyfDb99JC.png) + Ở đây nếu điều kiện if được thỏa mãn thì chúng ta sẽ vào trong hàm `com/atlassian/confluence/extra/widgetconnector/video/YoutubeRenderer.class/getEmbeddedHtml` ![image](https://hackmd.io/_uploads/HJzCZ5cyC.png) Ở đây để ý thì nó sẽ gọi đến 2 hàm là `getEmbedUrl` và `setDefaultParam`, nhảy vào hàm thứ 2 để check. + Thấy rằng có một parameter ẩn rất là khả nghi là `_template` nên chúng ta sẽ phải để ý nó :sunglasses:, lưu ý là nó cũng là nơi mà chúng ta vừa đặt vào xem file nào mà chúng ta muốn đọc bằng file prototol. Mục đích của hàm này nhằm đưa các params vào trong context của template Velocity. ![image](https://hackmd.io/_uploads/H15rzqckA.png) ![image](https://hackmd.io/_uploads/Bk0pfc5JR.png) + Nhảy vào hàm `this.velocityRenderService.render`, chúng ta sẽ tới `com/atlassian/confluence/extra/widgetconnector/services/DefaultVelocityRenderService.class/render`. Ở hàm này sau khi set context thì nó sẽ gọi đến hàm `getRenderedTemplate` ở cùng class. ![image](https://hackmd.io/_uploads/HJRn45cy0.png) ![image](https://hackmd.io/_uploads/SJR64cqkA.png) + Jump tiếp vào hàm `getRenderedTemplate`, chúng ta sẽ tới một hàm cũng gọi một hàm cùng tên là `getRenderedTemplate` với biến `templateName` là đến từ param `_template`. ![image](https://hackmd.io/_uploads/S1QQS951C.png) + Tiếp theo nó sẽ gọi đến hàm `getRenderedTemplateWithoutSwallowingErrors` ![image](https://hackmd.io/_uploads/Syfrr99kC.png) ![image](https://hackmd.io/_uploads/rkNYrq5k0.png) + Ở đây chúng ta thấy rằng nó đang gọi đến hàm `getTemplate`, jump vào đó thử. Tiếp đó là ` getVelocityEngine().getTemplate` ![image](https://hackmd.io/_uploads/rJAbIqc1C.png) + Jump vào `getTemplate` tiếp. ![image](https://hackmd.io/_uploads/SyMHLq910.png) + Ở đây chúng ta có thể thấy đang gọi đến hàm `getResource`. Ở hàm này chúng ta sẽ tính toán `resourceKey` dựa trên type và name từ cache, chúng ta có thể coi nó như là cache key cũng được. Từ đó chúng ta có thể suy ra nếu có thay đổi payload thì cũng phải thay đổi `resourceKey` để nó có thể load payload mới. ![image](https://hackmd.io/_uploads/r1uccqq1R.png) ![image](https://hackmd.io/_uploads/Hy4_Uq51C.png) Giờ chúng ta thứ thay đổi giá trị của `_template`, thì khi nó gọi hàm `this.globalCache.get(resourceKey)` ,nếu khi load giá trị bằng null thì sẽ gọi đến else rồi dùng `loadResource` để load template. + Thấy được rằng nó gọi đến 4 resource loader ở `com/atlassian/confluence/util/velocity/ConfigurableResourceManager.class`: ![image](https://hackmd.io/_uploads/SyGi35c10.png) ```! com.atlassian.confluence.setup.velocity.HibernateResourceLoader org.apache.velocity.runtime.resource.loader.FileResourceLoader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader com.atlassian.confluence.setup.velocity.DynamicPluginResourceLoader ``` + Giờ chúng ta thử vào trong `FileResourceLoader` + Ở hàm `org/apache/velocity/runtime/resource/loader/FileResourceLoader.class/getResourceStream`,chúng ta thấy nó xài hàm `normalizePath` nhằm fix lỗi directory traversal. ```java! public static final String normalizePath(String path) { String normalized = path; if (path.indexOf(92) >= 0) { normalized = path.replace('\\', '/'); } if (!normalized.startsWith("/")) { normalized = "/" + normalized; } while(true) { int index = normalized.indexOf("//"); if (index < 0) { while(true) { index = normalized.indexOf("%20"); if (index < 0) { while(true) { index = normalized.indexOf("/./"); if (index < 0) { while(true) { index = normalized.indexOf("/../"); if (index < 0) { return normalized; } if (index == 0) { return null; } int index2 = normalized.lastIndexOf(47, index - 1); normalized = normalized.substring(0, index2) + normalized.substring(index + 3); } } normalized = normalized.substring(0, index) + normalized.substring(index + 2); } } normalized = normalized.substring(0, index) + " " + normalized.substring(index + 3); } } normalized = normalized.substring(0, index) + normalized.substring(index + 1); } } ``` Chính vì vậy mà chúng ta chỉ có thể đọc file ở trong folder `confluence` ![image](https://hackmd.io/_uploads/rk9Wej5yR.png) ![image](https://hackmd.io/_uploads/ryHfei5yA.png) ![image](https://hackmd.io/_uploads/r1oBloqk0.png) + Nếu dùng file protocol thì sẽ dùng resource loader là `ClasspathResourceLoader` ![image](https://hackmd.io/_uploads/SyRMZockC.png) + Nhảy đến hàm `getResourceAsStream` ở `org/apache/velocity/util/ClassUtils.class` ![image](https://hackmd.io/_uploads/rydcbi5JR.png) + Nhảy tiếp vào hàm `getResourceAsStream`, chúng ta thấy nó sẽ tạo một object của class `URL` để gọi đến hàm `openStream` ![image](https://hackmd.io/_uploads/rJR4zj5yC.png) ![image](https://hackmd.io/_uploads/ByJk4sqkA.png) ![image](https://hackmd.io/_uploads/BkaPVi5kA.png) Ở đây chính là sink của lỗi SSRF, đó là lí do chúng ta có thể xài http hoặc file protocol. + Quay trở lại với lỗi SSTI,nhờ gửi connection để lấy template content, chúng ta sẽ bắt đầu lại ở hàm `com/atlassian/confluence/util/velocity/VelocityUtils.class/renderTemplateWithoutSwallowingErrors` để thực hiện render template bằng velocity. ![image](https://hackmd.io/_uploads/HytjBo5yC.png) + Jump vào hàm merge, chúng ta sẽ đến `org/apache/velocity/Template.class/merge` ![image](https://hackmd.io/_uploads/BJQmIo9y0.png) + Ở đây, chúng ta thấy được sink của SSTI là sử dụng `SimpleNode` để có thể parse template. ![image](https://hackmd.io/_uploads/rk78LicJ0.png) + Giờ chúng ta sẽ thử popup calc xem có được không, ở đây mình sẽ sử dụng $height vì nó ở trong context, các bạn có thể thay thế bằng cái nào cũng được. Từ đây mình sẽ sử dụng reflection để có thể RCE. ![image](https://hackmd.io/_uploads/H1p-vi5JR.png) `$height.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc")` ![image](https://hackmd.io/_uploads/HylSPsq1R.png) Payload để lấy được output RCE ```! #set ($exp="test") #set ($a=$exp.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec($command)) #set ($input=$exp.getClass().forName("java.lang.Process").getMethod("getInputStream").invoke($a)) #set($sc = $exp.getClass().forName("java.util.Scanner")) #set($constructor = $sc.getDeclaredConstructor($exp.getClass().forName("java.io.InputStream"))) #set($scan=$constructor.newInstance($input).useDelimiter("\\A")) #if($scan.hasNext()) $scan.next() #end ``` ![image](https://hackmd.io/_uploads/SkEBqscJA.png)