• 1. Stay - Post
  • 2. Circles - Post
  • 3. Hollywood's_Bleeding - Post
  • 4. A_Thousand_Bad_Times - Post

初识JavaWeb安全(2)

初识JavaWeb安全(2)

前言

上回了解了基础的Java反射机制,以及如何执行系统命令的方法,下面我们还是从反射说起。

有参构造函数payload

有关newInstance

之前我们构造命令执行payload的时候知道了这个方法用于创建该类的一个对象,但实际上该方法并非任何类都能够创建对象,使用它时需要注意,以下两种情况不能够构建相应对象:

1.构造方法需要传入参数

import java.lang.Class.*;

public class main{
    public static void main(String[] args) throws Exception {
       Class c = Class.forName("Test");
       Test mytest = (Test) c.newInstance();
    }
}

class Test{
    public String words;
    public Test(String str1){
        this.words = str1;
    }
}

Java工厂模式中经常使用上面代码类似的方式创建类,但由于我们Test类的构造方法需要带有参数时,你会收到下面的报错(IDE并不会显示这里出现错误)

Idea报错提示,找不到此方法,这里可以发现newInstance()找的实际上是无参数构造方法,从源码的角度看得会更清楚一点:

2.构造方法是私有的

import java.lang.Class.*;

public class main{
    public static void main(String[] args) throws Exception {
       Class c = Class.forName("Test");
       Test mytest = (Test) c.newInstance();
    }
}

class Test{
    public String words;
    private Test(){
        this.words = "hello";
    }
}

从方法源码的角度来看,是会检查构造方法是否为公有方法:

举个例子,我们不能直接使用这样的方法来执行命令:

       Class c = Class.forName("java.lang.Runtime");
       c.getMethod("exec", String.class).invoke(c.newInstance(),"ls");

原因是因为java.lang.Runtime的构造方法为私有方法:

从源码上我们也能看出java.lang.Runtime类是采用单例模式,即不会出现多个实例的情况,当我们需要使用Runtime类型的对象时,只需要用getRuntime得到,而不需要再次创建一个新的实例,形象一点理解,这里Runtime的一个实例相当于一个shell,我们只需要一个shell执行命令,而不需要多个。

总结:

这里我们会发现newInstance()创建对象是有局限性的,在某些的Javapayload中使用了newinstance()创建对象,可能会因为以上两种情况失效,因此我们需要一个更泛用的创建对象方法,或者一个拥有非私有构造方法的执行命令类。

有关getConstructor

一点想法

getConstructor()getMethod类似,前者获得构造方法,后者获得普通方法,那么问题来了,能不能用getMethod获得一个类的构造方法呢?个人认为是不能的。

从源码上来看两者的区别

getMethod():

getConstructor:

最终返回reflect中的Constructor

Part1.第一种构造函数

Java除了java.lang.Runtime能够执行命令之外,还有一个类能够执行方法java.lang.ProcessBuilder,这个类拥有两个构造方法,我们先说第一种:

这个类可以通过下面这种方式执行命令:

import java.util.Arrays;
import java.util.List;

public class main{
    public static void main(String[] args) throws Exception {
        Class myclass = Class.forName("java.lang.ProcessBuilder");
        ((ProcessBuilder)myclass.getConstructor(List.class).newInstance(Arrays.asList("pycharm"))).start();
    }
}

这里需要使用强转类型,然而一般情况下,我们是不会控制格式类似与这样的代码,因此强转类型我们也需要使用反射完成,前文也说过了invoke可以用来执行函数(普通方法)

public class main{
    public static void main(String[] args) throws Exception {
        Class myclass = Class.forName("java.lang.ProcessBuilder");
        myclass.getMethod("start").invoke(myclass.getConstructor(List.class).newInstance(Arrays.asList("pycharm")));
    }
}

PS:invoke执行方法时,如果普通方法,第一个参数为该对象的实例,如果为静态方法,第一个参数为该类。

当然这里可能会产生疑惑

newInstance只能调用无参数构造函数,这里确是调用的有参数的构造函数,实际上这里和上文的newInstance不同,上文的newInstance全名应该是Class.newInstance,这里是Constructor.newInstance,这里的newInstance可以根据获得的Constructor直接调用,而非Class.newInstance中的先找再调用。既然这个方法与之前那个不同我们就需要了解一下怎么用,根据源码注释:

有道云翻译了一下意思就是:

使用此对象表示的构造函数时,需要创建并初始化构造函数的新实例

声明类,并使用指定的初始化参数,各个参数将自动展开以进行匹配。

总结一下就是需要我们传入构造函数需要的参数,在这里是一个List类型,这里我们注意到,传入参数为Object... ,他实际上就是一个对象的数组,因此我们这里需要传入一个装有List类型数据的数组(可变长度参数不一定是直接传入数组,单个对象也可以),这也是为什么要使用Arrays.asList()将字符数组转化为列表的原因:

同样给出图示:

Part2.第二种构造函数:

第二种构造函数形式为:

这里传入的是String类型的可变参数,事实上可变参数在底层是一个数组也就是这里的

 public ProcessBuilder(String... command) {}
//等价于
 public ProcessBuilder(String[] command) {}

两者本质上是一个函数,例程为:

public class main{
    public static void main(String[] args) throws Exception {
        String[] word_list = {"hello","world"};
        Test my_test = new Test();
        my_test.say(word_list);
    }
}

class Test{
    public String words;
    public Test(){
        this.words = "hello";
    }
    public void say(String... saying){
        for (String word:saying){
            System.out.println(word);
        }
    }
}

因此在这里我们第二个构造函数使用时,只需要传入String[]类即可,同样的我们可以利用第二个构造函数执行系统命令,如下:

public class main{
    public static void main(String[] args) throws Exception {
        Class pb = Class.forName("java.lang.ProcessBuilder");
        pb.getMethod("start").invoke(pb.getConstructor(String[].class).newInstance(new String[][]{{"pycharm"}}));
    }
}

这里我们直接给出了反射类型的payload,涉及的内容和前文一样不再赘述。

  1. 虽然理论上可变String的可变参数是可以直接传入String的,但由于getConstructor寻找构造函数时,是通过参数类型寻找的,String...在底层表现为String[],因此这里不能传入String否则会直接报错找不到该方法,因此getConstructor需要传入的是String[].class
  2. 由于Constructor.newInstrance()需要传入构造函数参数类型的数组,因此我们不能只传入String[]{}而是需要将它在包裹一层String[]{},因此后面用的是String类型的二维数组。如果不是二维字符串数组,则会抛出一个非法错误的异常。

私有方法的payload

有关getDeclared:

getDeclared系列方法和get系列区别在于,前者获得是该类中所有已经声明的内容(你在该类里创建好的内容,无论变量还是方法,但不包括继承的内容),后者获得的是所有公有的内容(public) getConstructor在一些特殊情况下也可以获得私有的构造函数

有了这个方法之后,Runtime创建对象时的私有构造方法就可以解决了,我们通过getDeclaredConstructor获得其私有的构造方法,然后用getConstructor一样的方法调用他:

public class main{
    public static void main(String[] args) throws Exception {
        Class rt = Class.forName("java.lang.Runtime");
        Constructor c = rt.getDeclaredConstructor();
        c.setAccessible(true);
   rt.getMethod("exec",String.class).invoke(c.newInstance(),"pycharm");
    }
}

唯一不同的是会需要setAccessible(true)这行代码,这个反射的返回值为void,而不是调用该反射的类,所以不能放在一行反射代码里执行,只能单独执行。

参考文章:

深入理解Constructor之newInstance方法

P神的知识星球JAVA安全漫谈系列2~3


除非注明,ebounce文章均为原创,转载请以链接形式标明本文地址

本文地址:http://ebounce.cn/web/63.html

新评论

captcha
请输入验证码