I am a slow walker, but I never walk backwards.

【CVE-2019-8451】Jira未授权SSRF漏洞分析&复现(附超详细JDB调试过程)

Posted on By Curz0n

0x00 前言

Atlassian Jira(鸡娃儿)是澳大利亚Atlassian公司的出品的项目与事务跟踪工具,被广泛应用于各大厂商任务跟踪、流程审批等系统。8月12号,Atlassian官方在其数据服务中心公布Jira系统中存在未授权SSRF漏洞,攻击者可以利用该漏洞未授权访问内网资源。

0x01 影响版本

Jira: version < 8.4.0,建议升级到8.4.0及以上版本;
Enterprise版: version < 7.13.9,企业版将在7.3.19版本中修复,但是该版本目前尚未发布;
官方发布的漏洞及整改详情参考:SSRF in the /plugins/servlet/gadgets/makeRequest resource - CVE-2019-8451

0x02 漏洞分析

1. 什么是SSRF

SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造好目标请求,通过控制服务器发起请求的安全漏洞。该漏洞把被控制的服务器作为跳板,借用服务器的身份访问未授权资源,通常,SSRF漏洞用于探测内网资源。概念可能有点抽象,具体攻击场景见下图,攻击者通过DMZ区的Web服务作为跳板,访问内网的其他服务。

2. 环境准备

在Docker Hub中搜索到了Jira的container,所以可以直接使用Docker来搭建Jira应用环境,pull一个存在漏洞的Jira版本:

docker pull cptactionhank/docker-atlassian-jira:8.0.0

创建Jira应用的容器实例,把容器内Jira服务8080端口映射到本地8081:

docker run --detach --publish 8081:8080 cptactionhank/atlassian-jira:8.0.0

启动容器,访问http://127.0.0.1:8081,完成Jira的初始化安装。根据官方披露的漏洞详情,存在SSRF漏洞的是/plugins/servlet/gadgets/makeRequest接口,先直接访问下接口,返回404,一脸懵逼中…

既然不能直接访问,那就需要从代码中寻找返回404的原因了。进入Docker容器的shell,应用根目录为/opt/atlassian/jira:

先直接根据接口关键字gadgets/makeRequest搜索下对应的servlet,发现除了刚才请求的日志文件,并没有对应的class文件:

观察web.xml部署描述符文件,发现接口为/plugins/servlet/*的请求都会被com.atlassian.jira.plugin.servlet.ServletModuleContainerServlet处理:

把Docker容器中的代码复制到本地,使用jd-gui反编译ServletModuleContainerServlet.class如下:

代码中没有处理http请求的方法逻辑,那就应该在其父类中。在WEB-INF的classes目录下没有找到其父类,最后搜索发现其父类在atlassian-plugins-servlet-5.0.0.jar文件中:

反编译jar,com.atlassian.plugin.servlet.ServletModuleContainerServlet.class代码如下:

关键看第37行代码,这里先调用getPathInfo方法,代码详情如下,返回pathInfo信息,然后把pathInfo作为参数传入getServletModuleManager().getServlet()方法,这里应该是通过传入的pathInfo信息在getServlet方法里面获取到接口对应的servlet,具体JDB调试看看。

  private String getPathInfo(HttpServletRequest request) {
    String pathInfo = (String)request.getAttribute("javax.servlet.include.path_info");
    if (pathInfo == null) {
      pathInfo = request.getPathInfo();
    }
    return pathInfo;
  }

3. JDB动态调试

想要调试tomcat中运行的Web程序,需要先修改tomcat启动参数,让tomcat JVM工作在debug模式。Linux环境直接修改catalina.sh文件:

CATALINA_OPTS="-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8899"

重启容器,8899端口开启说明可以使用jdb调试了。

先attach目标进程,使用stop命令设置断点,然后在burpsuite的Repeater模块重放/plugins/servlet/gadgets/makeRequest接口,程序停止到com.atlassian.plugin.servlet.ServletModuleContainerServlet的第31行(如果发现jdb没有自动停止到断点,可以使用resume命令恢复线程):

使用jd-gui反编译atlassian-plugins-servlet-5.0.0.jar文件,结合反编译出来的伪代码,next命令继续往下调试,执行到第37行,如下:

第37行代码比较关键,需要知道getPathInfo的返回信息和getServlet方法里面获取到的servlet,具体调试过程如下,pathInfo的值等于/gadgets/makeRequest:

step命令继续步入,得到调用getServlet()方法的实现类如下:

反编译该class,方法内部逻辑比较简单,先获取一个key,然后通过key拿到descriptor,猜测descriptor就描述了接口servlet的相关信息,具体调试看一看:

调试结果如下

这里descriptor变量是个ServletModuleDescriptor对象,看看该对象的详细信息,发现它描述了servlet class信息:

这个class应该就是gadgets/makeRequest接口对应的servlet了,grep一下,发现该servlet定义在atlassian-gadgets-opensocial-plugin-4.3.9.jar文件中:

反编译jar文件,具体代码如下:

分析代码,可以看见在拿到请求信息后,会先获取请求头的X-Atlassian-Token属性,检查其值是否等于no-check,如果不等于,就直接返回404。到这里,我们好像找到访问/plugins/servlet/gadgets/makeRequest接口返回404的原因了,添加X-Atlassian-Token属性头,绕过if判断,重放数据包,状态码从404变成了400:

下断点,继续调试,跳转到父类org.apache.shindig.gadgets.servlet.MakeRequestServlet的doGet()方法:

grep搜索父类所在的jar包,没有结果,通过findjar在线搜索,结果显示该类包含在shindig-gadgets-xxx.jar的jar包中,然后搜索jar包名,结果如下:

原来org.apache.shindig.gadgets.servlet.MakeRequestServlet.class所在的jar包被包含在另外一个jar包里面:

解压出来,反编译shindig-gadgets-2.1.3.jar文件,对着反编译的伪代码继续调试,最后跟到org.apache.shindig.gadgets.servlet.MakeRequestHandler.fetch()方法,代码详情如下:

继续跟踪81行的buildHttpRequest方法,代码详情如下:

114行,先获取参数url的值,然后传入validateUrl方法验证,validateUrl方法实现在其父类,代码详情如下,先判断urlToValidate是否等于null,然后验证urlToValidate是不是一个http(s)协议请求:

在validateUrl方法下个断点,urlToValidate的值等于null:

因为我们测试时发起的请求没有携带url参数,所以request.getParameter("url")结果肯定等于null,最后在validateUrl方法中验证失败,抛出GadgetException异常,导致返回400的错误状态码。知道返回400的原因了,构造一个满足validateUrl方法验证的url参数,重放/plugins/servlet/gadgets/makeRequest接口请求,返回成功状态码200:

继续分析代码,ProxyBase.validateUrl方法返回一个Uri对象,接着MakeRequestHandler.buildHttpRequest方法拿着Uri对象new了一个HttpRequest对象,这个HttpRequest对象的请求目标就是/plugins/servlet/gadgets/makeRequest接口中url参数的值,最终把这个HttpRequest对象传入fetch方法:

接着继续调试分析,跟踪到atlassian-gadgets-opensocial-plugin-4.3.9.jar中的 com.atlassian.gadgets.renderer.internal.http.WhitelistAwareHttpClient.execute方法:

从方法名可知,validateRequestTargetAgainstWhitelist方法的意思就是检测请求的目标是否在白名单内,代码详情如下,如果不在白名单内,就抛出IllegalHttpTargetHostException异常:

继续跟踪,定位到com.atlassian.gadgets.renderer.internal.http.DelegatingWhitelist.allows()方法,代码详情如下,这个方法需要好好看看,有点学问在里面。

public boolean allows(URI uri) {
    return Iterables.any(Iterables.concat(ImmutableSet.of(this.whitelist, this.messageBundleWhiteList), this.optionalWhitelists), allowsP((URI)Preconditions.checkNotNull(uri, "uri")));
}

4. Google Guava

什么是Guava?Google Guava是对Java API的补充,已经进化为Java开发者的基础工具箱,详情见官方WIKI(API文档)。回到DelegatingWhitelist.allows()方法,一起分析下这句很长很长的代码。
Preconditions.checkNotNull(uri, “uri”),检测URI对象是否为空,如果为空就抛出异常信息”uri”,返回URI对象作为参数传入allowsP方法,然后new了一个WhitelistAllows对象返回放在Predicate里面:

  private Predicate<Whitelist> allowsP(URI uri) {
    return new WhitelistAllows(uri);
  }

ImmutableSet.of(this.whitelist, this.messageBundleWhiteList)创建一个不可变的set集合,然后和this.optionalWhitelists对象作为参数传入Iterables.concat方法,concat方法的作用是把传入的多个Iterable对象串联成懒视图,返回一个Iterable对象。最后把这个对象和Predicate对象作为参数传入Iterables.any方法,any方法的作用是判断Iterable中是否有元素满足断言条件,如果满足,就返回true并立即停止后续判断。
简单理解就是,传入Iterables.any方法的参数A是个迭代器对象,会依次迭代元素和参数B进行断言,参数B是个Predicate对象,断言条件声明在apply方法里面,下面写段demo代码帮助理解:

理解了Google Guava,我们回到代码中,可以看见其声明的断言条件如下图第44行代码,迭代出Whitelist元素对象,调用它的allows方法进行判断,从第29行的Iterables.concat方法可知,需要迭代的元素分别是this.whitelist、this.messageBundleWhiteList和this.optionalWhitelists:

在第44行代码下断点,调试一下,发现前两个元素断言返回false,符合条件的是第三个this.optionalWhitelists迭代器中的Whitelist对象(第三次cont命令才使程序正常运行):

Whitelist对象是动态代理生成的,接下来就是需要找出对象的具体实例了,这样才能分析它allows方法的具体实现,直接print动态对象的变量,获取到Whitelist的具体实例com.atlassian.jira.dashboard.JiraWhitelistPS:笔者为了获取动态代理的具体实例,折腾了一个下午,结果一个print就可以,一开始print的是动态类名,一直出错,结果需要print变量才可以出结果,傻逼了…#^%&):

反编译com.atlassian.jira.dashboard.JiraWhitelist.class,位置在WEB-INF\classes目录下,其allows方法详情如下:

在26行下个断点,获取到uriString和canonicalBaseUrl的值,return的时候有一个逻辑或的断言,如果满足请求的目标url是以应用的canonicalBaseUrl前缀开始,则返回true,所以,如果url参数值是以http(s)://应用服务IP:端口打头,那就可以让服务器正常请求这个url目标了。

0x03 漏洞复现

根据漏洞分析,我们知道请求的目标url只要是以应用的host开头,就可以通过白名单验证,使服务器正常发起http请求。构造poc如下:

/plugins/servlet/gadgets/makeRequest?url=http://ip:port@www.baidu.com

测试一下,成功绕过白名单检测,使服务器访问攻击者指定的url目标:

官方在8.4.0版本中已经修复该漏洞,修复后的代码逻辑如下,不再是简单的字符串前缀比较:

0x04 结语

漏洞本身比较简单,但是整个分析过程还是挺有意思的,文章内容写的比较啰嗦,权当学习笔记,同时希望看到这篇文章的同学脑袋里面能清晰的了解代码分析逻辑,不留疑惑,避免踩坑。笔者也是个小菜鸟,很多不专业的地方,如有错误,还请指教。

References:

Jira未授权SSRF漏洞(CVE-2019-8451)

版权声明:转载请注明出处,谢谢。https://github.com/curz0n