JAVA 命令执行 学习笔记

JAVA 命令执行 学习笔记

  • 前言
  • 关于exec
  • java.lang.Runtime
    • 1.获取类
    • 2.获取可调用方法
    • 3.调用方法
    • 一些补充
  • java.lang.ProcessBuilder
    • 执行不带参数的系统命令
    • 执行携带参数的系统命令
      • 1.public ProcessBuilder(List command)
      • 2.public ProcessBuilder(String... command)
  • 如何调用类的私有方法
  • 关于Runtime.getRuntime().exec某些时候会失效这个问题

前言

今天来了解一下Java的命令执行,命令执行在安全领域中的应用还是非常广的,得好好理解学习一下,至于下面用到的一些知识点如果忘记了的话可以去上一篇文章JAVA RMI学习笔记看看,本文依旧是跟着Epicccal师傅来学习总结的

关于exec

Runtime类,顾名思义就是运行时环境,Runtime的exec方法是Java中最常见的执行命令的方式 ,现在我们先来分析一下exec的执行过程

import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class lmonstergg {

    public static void main(String[] args) throws IOException {
        Process p = Runtime.getRuntime().exec("whoami"); //执行系统命令
        InputStream is=p.getInputStream();  //获取进程p的标准输出流作为输入字节流
        InputStreamReader isr=new InputStreamReader(is);  //将字节流转换为字符流
        BufferedReader br=new BufferedReader(isr);  //为字符流提供缓冲区,便于读取整块数据
        String line=null;
        while((line=br.readLine())!=null)
        {
            System.out.println(line);
        }
    }
}

经典命令执行实验代码,我们在exec那里下断点然后步入
JAVA 命令执行 学习笔记_第1张图片
这里又返回一个exec方法,进去看看
JAVA 命令执行 学习笔记_第2张图片
这里又返回一个exec方法,继续跟进
JAVA 命令执行 学习笔记_第3张图片
这里返回一个ProcessBuilder对象,该对象调用了start方法

ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE 1.5之前,都是由Process类处理实现进程的控制管理。每个 ProcessBuilder 实例管理一个进程属性集。它的start() 方法利用这些属性创建一个新的 Process 实例。start() 方法可以从同一实例重复调用,以利用相同的或相关的属性创建新的子进程。

通过代码不难看出 , exec()方法执行命令的原理是通过 ProcessBuilder 对象创建了一个执行命令的子进程

java.lang.Runtime

java.lang.Runtime 类的 exec() 方法是Java中最常见的执行命令的方式

1.获取类

正常情况下 , 我们想要拿到一个除系统类以外的类 , 必须要先 import 后才能使用 . 否则会出现 cannot find symbol 等报错 . 但是在进行利用时 , 系统是不会让你随意加载类的

但是 , 通过 Class.forName(className) 获取类则没有这个限制 , 我们可以通过 forName() 方法加载任何类 .

如下所示
JAVA 命令执行 学习笔记_第4张图片

2.获取可调用方法

拿到了 java.lang.Runtime 类后 , 我们肯定想知道该类可调用的方法 . 于是通过 className.getMethods() , className.getDeclaredMethods() 来获取类的方法
JAVA 命令执行 学习笔记_第5张图片
从输出信息中可以找到我们想要执行的 exec() 方法

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {

    public String prt(String name)
    {
        return name;
    }
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        String name="lmonstergg";
        Class cls=Class.forName("java.lang.Runtime");
        Method[] methods = cls.getMethods();
        Method[] declaredMethods = cls.getDeclaredMethods();
        System.out.println("getMethods获取的方法:");
        for(Method m:methods)
            System.out.println(m);
        //getDeclaredMethods()方法获取的所有方法
        System.out.println("getDeclaredMethods获取的方法:");
        for(Method m:declaredMethods)
            System.out.println(m);
    }
}

3.调用方法

拿到了类 , 拿到了类的方法 , 就可以通过反射实例化对象并通过 invoke() 调用方法了
JAVA 命令执行 学习笔记_第6张图片
发现程序抛出了异常 , 报错信息为 : Exception in thread “main” java.lang.IllegalAccessException: Class test7 can not access a member of class java.lang.Runtime with modifiers “private”

简单的翻译一下 , 结果为 : test7 类无法访问 java.lang.Runtime 类中带有 “private” 修饰符的成员变量 / 成员函数 .

这里就很有意思了 , 在第二步中输出类的方法时 , exec()方法的修饰符全部为 “public” . 那么这个 “private” 修饰符是哪来的呢 ?

在通过 cls.newInstance() 构造实例对象时 , 会默认调用无参构造函数 . 难道这个无参构造函数是私有的吗 ?

我们随便找一个Java项目的源代码来看看java.lang.Runtime
JAVA 命令执行 学习笔记_第7张图片
可以发现 , java.lang.Runtime 类的构造方法的确使用了 “private” 修饰符 . 我们知道 “private” 修饰符修饰的方法只能在当前类中被调用 , 外部是不可见的 . 那么设计者为什么要这么做呢 ?

我对 Java 设计模式并不了解 , 因此这里引用 Phith0n师傅 的一段话 , 说的非常形象 .

这一种比较常见的设计模式 , 被称为 "单例模式 / 工厂模式"

类似一个 Web 应用 , 数据库连接应该只在服务启动时建立一次 , 而不是每次访问数据库时都建立一个连接 .

因此开发人员可以把数据库连接写在构造函数中 , 并赋予该函数 "private" 修饰符 . 然后编写一个静态方法来获取该连接 .

这样 , 只有在类初始化时会调用一次构造函数 , 建立数据库连接 . 后面只需要通过静态方法就能获取数据库连接 , 避免了建立多个数据库链接 .

我们再看代码 , 发现的确存在一个 getRuntime() 的静态方法 , 并且返回了 java.lang.Runtime 的实例对象 .

JAVA 命令执行 学习笔记_第8张图片
您可能会疑惑 , 这里的构造方法是 Runtime() , 实例化的过程并没有写在构造函数里啊?

个人认为这是无关紧要的 , 只需要确保实例化过程只进行一次就行了 , 以 Java 反射为例 , 在类初始化时会执行 static{} 代码块中的内容( 详见本文开头 ) , 所以会执行一遍实例化过程 . 由于该过程被赋予了 “private” 修饰符 , 所以后面就再也不能访问它了 . 结果是一样的 .

这里也引出了 class.newInstance() 方法执行成功的两个关键点 :

类必须要有无参构造函数 .

类的构造函数不能是私有的 , 也就是不能通过 "private" 修饰符来修饰构造函数 .

有了这些结论 , 我们就可以通过 Java 反射机制来执行 exec() 方法了 .

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {

    public String prt(String name)
    {
        return name;
    }
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException {
        String name="lmonstergg";
        Class cls=Class.forName("java.lang.Runtime");
        Method mgetruntime=cls.getMethod("getRuntime");
        Method mexec=cls.getMethod("exec",String.class);
        Object obj=mgetruntime.invoke(null);
        Process p=(Process)mexec.invoke(obj,"ipconfig");
        InputStream is=p.getInputStream();  //获取进程p的标准输出流作为输入字节流
        InputStreamReader isr=new InputStreamReader(is);  //将字节流转换为字符流
        BufferedReader br=new BufferedReader(isr);  //为字符流提供缓冲区,便于读取整块数据
        String line=null;
        while((line=br.readLine())!=null)
        {
            System.out.println(line);
        }
    }
}

JAVA 命令执行 学习笔记_第9张图片
命令成功执行了,接下来我们分析下代码

通过 Method mGetRuntime = cls.getMethod("getRuntime"); 和 Method mExec = cls.getMethod("exec",String.class); 分别获取 getRuntime() 方法和 exec() 方法.

通过 getRuntime() 的 invoke(null) 方法获取 Runtime 实例对象 . 由于调用的是静态方法 , 所以省略 obj 参数 , 由于 getRuntime() 方法没有参数 , 所以这里参数数组为 null .

通过 exec() 的 invoke(obj , args[]) 方法来执行命令 . 这里 obj 是 Runtime 实例对象 , 通过上一步骤获得 , 参数则为系统命令 "ifconfig" .

获取执行结果的字节流 , 将其处理成字符流 , 最后输出字符串 .

一些补充

关于上面的代码,在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。

关于 Object obj = mGetRuntime.invoke(null); 这个点

invoke() 的方法参数为什么是一个类( Class ) 呢?

我们可以通过 对象.方法名 来调用实例方法 , 类名.方法名 来调用静态方法 , 那么反过来 , 方法名.invoke(对象) 不就可以映射成 方法名.invoke(类)
JAVA 命令执行 学习笔记_第10张图片

java.lang.ProcessBuilder

通过上面的内容我们知道了Runtime的exec方法执行命令,实际上是调用了ProcessBuilder的start方法创建了一个进程执行了命令,既然如此我们可以考虑直接通过 ProcessBuilder 类来执行命令

那我们先看看ProcessBuilder 类的构造函数
JAVA 命令执行 学习笔记_第11张图片
可以看到 , ProcessBuilder 类有两个构造函数 , 这两个函数定义都非常简单,一个用于执行没有参数的命令 , 一个用于执行携带参数的命令,需要注意 : command 参数的类型是 List

所以 , 我们接下来调用 java.lang.ProcessBuilder.start() 方法 , 创建子进程来执行命令

执行不带参数的系统命令

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class test {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException {
        String name="lmonstergg";
        Class cls=Class.forName("java.lang.ProcessBuilder");
        //上面这段获取类
        Object obj=cls.getConstructor(List.class).newInstance(Arrays.asList("ipconfig"));
        //上面这段用className.getConstructor( parameterType ).newInstance( parameterName )模式构造对象,该对象的参数为ipconfig,Arrays.asList用于转换使其类型为List
        Method mstart=cls.getMethod("start");
        //上面这段获取start方法
        Process p=(Process)mstart.invoke(obj);
        //调用该方法
        InputStream is=p.getInputStream();  //获取进程p的标准输出流作为输入字节流
        InputStreamReader isr=new InputStreamReader(is);  //将字节流转换为字符流
        BufferedReader br=new BufferedReader(isr);  //为字符流提供缓冲区,便于读取整块数据
        String line=null;
        while((line=br.readLine())!=null)
        {
            System.out.println(line);
        }
    }
}

JAVA 命令执行 学习笔记_第12张图片

通过 className.getConstructor( parameterType ).newInstance( parameterName ) 来调用含有参数parameter的构造函数

由于 cmdarray 参数的类型是 List , 所以我们执行的命令的类型也必须是 List , 此时可以用 Arrays.asList() 方法将一个可变长参数或者数组转换成 List 类型 .

由于 start() 方法没有参数 , 所以直接调用 Method.invoke(obj) 就可以了

执行携带参数的系统命令

有两种方法来执行携带参数的系统命令,其实就是怎么用ProcessBuilder的两种构造函数来分别执行系统命令,下面分别来说一下

1.public ProcessBuilder(List command)

这里还是通过 Arrays.asList() 方法 . 由于该方法的参数可以是一个可变长参数 , 所以我们可以直接把携带参数的系统命令写到一个数组中 , 然后通过该方法转换成列表 .

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class test {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException {
        String name="lmonstergg";
        Class cls=Class.forName("java.lang.ProcessBuilder");
        //上面这段获取类
        Object obj=cls.getConstructor(List.class).newInstance(Arrays.asList("netstat","-r"));
        //上面这段用className.getConstructor( parameterType ).newInstance( parameterName )模式构造对象,该对象的参数为ipconfig,Arrays.asList用于转换使其类型为List
        Method mstart=cls.getMethod("start");
        //上面这段获取start方法
        Process p=(Process)mstart.invoke(obj);
        //调用该方法
        InputStream is=p.getInputStream();  //获取进程p的标准输出流作为输入字节流
        InputStreamReader isr=new InputStreamReader(is);  //将字节流转换为字符流
        BufferedReader br=new BufferedReader(isr);  //为字符流提供缓冲区,便于读取整块数据
        String line=null;
        while((line=br.readLine())!=null)
        {
            System.out.println(line);
        }
    }
}

JAVA 命令执行 学习笔记_第13张图片

2.public ProcessBuilder(String… command)

这是上文所说的 ProcessBuilder 类的第二个构造函数 , 也就是专门用于执行携带参数的系统命令的构造函数 .
JAVA 命令执行 学习笔记_第14张图片
可以看到 , 该构造方法的参数是也是一个可变长参数 . 可变长参数代表着不定长度的参数 ,表示该形参可以接受多个参数值,多个参数值被当成数组传入,所以

public void test(String[] names)
等价于
public void test(String ... names)

有关可变参数的具体内容可看这里

因此,我们可以将 String[].class 作为参数传递给 getConstructor() 方法 , 告诉 ProcessBuilder 调用第二个构造方法来处理携带参数的系统命令 .

Phith0n 师傅 的文档里是这么写的 .
JAVA 命令执行 学习笔记_第15张图片
但是这么写并不能编译成功 . Javac会抛出异常!

Warning: non-varargs call of varargs method with inexact argument type for last parameter

警告: 最后一个参数使用了不精确的变量类型的 varargs 方法的非 varargs 调用

其中 varargs 代表可变参数 , 那么这个报错是什么意思呢 ?

在 Windows 下的 Javac 给出了提示 : 对于 varargs 调用 , 应使用 Object
JAVA 命令执行 学习笔记_第16张图片
于是我们按照这个方法来写

// 定义一个一维数组实例 , 其中包含了要执行的命令.-
String[] command = new String[]{"uname","-a"};
// 创建一个 Object 数组 , 将上一步创建的数组实例的引用赋值给这个 Object 数组
Object cmds[] = new Object[]{command};

如果对上面这种写法有疑问 , 可以参考 Java Object数组引用讨论 一文 . 将上面两步合并在一起 , 就变成了下面这一行代码.

// 这里尝试执行 uname 命令 , 携带参数 -a
Object obj = new Object[]{new String[]{"uname","-a"}}

最终修改后的代码如下所示 :

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class test {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException {
        Object cmd[]=new Object[]{new String[]{"netstat","-r"}};
        //Object cmd[]=new Object[]{}来承接对象
        Class cls=Class.forName("java.lang.ProcessBuilder");
        //上面这段获取类
        Object obj=cls.getConstructor(String[].class).newInstance(cmd);
        //上面这段用className.getConstructor( parameterType ).newInstance( parameterName )模式构造对象
        Method mstart=cls.getMethod("start");

        //上面这段获取start方法
        Process p=(Process)mstart.invoke(obj);
        //调用该方法
        InputStream is=p.getInputStream();  //获取进程p的标准输出流作为输入字节流
        InputStreamReader isr=new InputStreamReader(is);  //将字节流转换为字符流
        BufferedReader br=new BufferedReader(isr);  //为字符流提供缓冲区,便于读取整块数据
        String line=null;
        while((line=br.readLine())!=null)
        {
            System.out.println(line);
        }
    }
}

JAVA 命令执行 学习笔记_第17张图片
系统命令成功执行

如何调用类的私有方法

在讲通过 java.lang.Runtime 执行系统命令时 , 由于该类的构造方法 Runtime() 是一个私有方法 , 所以我们不能调用该方法 , 只能通过 getRuntime() 静态方法来返回一个 Runtime 实例对象 , 然后再调用 exec() 方法 . 为此还提到了 " 单例模式 " 这种设计模式 .

也就是说 , 我们无法直接获取到私有构造方法的 . 那么是否有其他方法来获取私有构造方法呢 ?

java.lang.reflect.AccessibleObject.class 中存在这么一个方法 : setAccessible(boolean flag)

来看下官方文档中是怎么定义这个方法的 .
JAVA 命令执行 学习笔记_第18张图片
从中我们可以知道 , 当该方法的参数被设置为 True 时 , 会取消 Java 语言访问检查 , 也就是取消对 public , protected , private 等修饰符的检查 .

但是 , 如果对象是 java.lang.Class.Constructor , 那么将会抛出异常 . 也就是说 , 我们不能通过 getConstructor() 方法来获取构造方法

这时我们可以使用 getDeclaredConstructor() 方法 ,该方法与 getConstructor() 方法最大的不同点在于 : 这个方法会返回指定参数类型的所有构造方法 . 包括 public , protected 以及 private 修饰符修饰的 .

而 getConstructor() 方法只会返回所有构造方法的一个子集 , 即 public 修饰符修饰的 .

因此 , 通过 getDeclaredConstructor() 方法 , 我们可以获取到私有构造方法 Runtime() . 并且 , 通过setAccessible(boolean flag)关闭 Java 语言访问检查时也不会再抛出异常 .

代码如下

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


public class test {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException {

        Class cls=Class.forName("java.lang.Runtime");
        //上面这段获取类
        Method mexec=cls.getMethod("exec",String.class);
        Constructor cst=cls.getDeclaredConstructor();
        cst.setAccessible(true);
        Object obj=cst.newInstance();

        //上面这段获取start方法
        Process p=(Process)mexec.invoke(obj,"netstat");
        //调用该方法
        InputStream is=p.getInputStream();  //获取进程p的标准输出流作为输入字节流
        InputStreamReader isr=new InputStreamReader(is);  //将字节流转换为字符流
        BufferedReader br=new BufferedReader(isr);  //为字符流提供缓冲区,便于读取整块数据
        String line=null;
        while((line=br.readLine())!=null)
        {
            System.out.println(line);
        }
    }
}

通过 getDelclaredConstructor() 方法获取到 Runtime() 构造方法 , 关闭 Java 语言访问检查 , 然后构建实例对象 . 最后通过 Method.invoke(obj , parameter) 调用 exec() 方法 。

通过这种方法 , 就可以直接访问任意类的私有构造方法了.
JAVA 命令执行 学习笔记_第19张图片

关于Runtime.getRuntime().exec某些时候会失效这个问题

这个问题大家可以看下这篇文章,本文就不重复分析了。

参考文章

你可能感兴趣的:(基础知识,Java,java,安全)