今天来了解一下Java的命令执行,命令执行在安全领域中的应用还是非常广的,得好好理解学习一下,至于下面用到的一些知识点如果忘记了的话可以去上一篇文章JAVA RMI学习笔记看看,本文依旧是跟着Epicccal师傅来学习总结的
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那里下断点然后步入
这里又返回一个exec方法,进去看看
这里又返回一个exec方法,继续跟进
这里返回一个ProcessBuilder对象,该对象调用了start方法
ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE 1.5之前,都是由Process类处理实现进程的控制管理。每个 ProcessBuilder 实例管理一个进程属性集。它的start() 方法利用这些属性创建一个新的 Process 实例。start() 方法可以从同一实例重复调用,以利用相同的或相关的属性创建新的子进程。
通过代码不难看出 , exec()方法执行命令的原理是通过 ProcessBuilder 对象创建了一个执行命令的子进程
java.lang.Runtime 类的 exec() 方法是Java中最常见的执行命令的方式
正常情况下 , 我们想要拿到一个除系统类以外的类 , 必须要先 import 后才能使用 . 否则会出现 cannot find symbol 等报错 . 但是在进行利用时 , 系统是不会让你随意加载类的
但是 , 通过 Class.forName(className) 获取类则没有这个限制 , 我们可以通过 forName() 方法加载任何类 .
拿到了 java.lang.Runtime 类后 , 我们肯定想知道该类可调用的方法 . 于是通过 className.getMethods() , className.getDeclaredMethods() 来获取类的方法
从输出信息中可以找到我们想要执行的 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);
}
}
拿到了类 , 拿到了类的方法 , 就可以通过反射实例化对象并通过 invoke() 调用方法了
发现程序抛出了异常 , 报错信息为 : 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.lang.Runtime 类的构造方法的确使用了 “private” 修饰符 . 我们知道 “private” 修饰符修饰的方法只能在当前类中被调用 , 外部是不可见的 . 那么设计者为什么要这么做呢 ?
我对 Java 设计模式并不了解 , 因此这里引用 Phith0n师傅 的一段话 , 说的非常形象 .
这一种比较常见的设计模式 , 被称为 "单例模式 / 工厂模式"
类似一个 Web 应用 , 数据库连接应该只在服务启动时建立一次 , 而不是每次访问数据库时都建立一个连接 .
因此开发人员可以把数据库连接写在构造函数中 , 并赋予该函数 "private" 修饰符 . 然后编写一个静态方法来获取该连接 .
这样 , 只有在类初始化时会调用一次构造函数 , 建立数据库连接 . 后面只需要通过静态方法就能获取数据库连接 , 避免了建立多个数据库链接 .
我们再看代码 , 发现的确存在一个 getRuntime() 的静态方法 , 并且返回了 java.lang.Runtime 的实例对象 .
您可能会疑惑 , 这里的构造方法是 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);
}
}
}
通过 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(类)
通过上面的内容我们知道了Runtime的exec方法执行命令,实际上是调用了ProcessBuilder的start方法创建了一个进程执行了命令,既然如此我们可以考虑直接通过 ProcessBuilder 类来执行命令
那我们先看看ProcessBuilder 类的构造函数
可以看到 , 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);
}
}
}
通过 className.getConstructor( parameterType ).newInstance( parameterName ) 来调用含有参数parameter的构造函数
由于 cmdarray 参数的类型是 List , 所以我们执行的命令的类型也必须是 List , 此时可以用 Arrays.asList() 方法将一个可变长参数或者数组转换成 List 类型 .
由于 start() 方法没有参数 , 所以直接调用 Method.invoke(obj) 就可以了
有两种方法来执行携带参数的系统命令,其实就是怎么用ProcessBuilder的两种构造函数来分别执行系统命令,下面分别来说一下
这里还是通过 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);
}
}
}
这是上文所说的 ProcessBuilder 类的第二个构造函数 , 也就是专门用于执行携带参数的系统命令的构造函数 .
可以看到 , 该构造方法的参数是也是一个可变长参数 . 可变长参数代表着不定长度的参数 ,表示该形参可以接受多个参数值,多个参数值被当成数组传入,所以
public void test(String[] names)
等价于
public void test(String ... names)
有关可变参数的具体内容可看这里
因此,我们可以将 String[].class 作为参数传递给 getConstructor() 方法 , 告诉 ProcessBuilder 调用第二个构造方法来处理携带参数的系统命令 .
Phith0n 师傅 的文档里是这么写的 .
但是这么写并不能编译成功 . Javac会抛出异常!
Warning: non-varargs call of varargs method with inexact argument type for last parameter
警告: 最后一个参数使用了不精确的变量类型的 varargs 方法的非 varargs 调用
其中 varargs 代表可变参数 , 那么这个报错是什么意思呢 ?
在 Windows 下的 Javac 给出了提示 : 对于 varargs 调用 , 应使用 Object
于是我们按照这个方法来写
// 定义一个一维数组实例 , 其中包含了要执行的命令.-
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.lang.Runtime 执行系统命令时 , 由于该类的构造方法 Runtime() 是一个私有方法 , 所以我们不能调用该方法 , 只能通过 getRuntime() 静态方法来返回一个 Runtime 实例对象 , 然后再调用 exec() 方法 . 为此还提到了 " 单例模式 " 这种设计模式 .
也就是说 , 我们无法直接获取到私有构造方法的 . 那么是否有其他方法来获取私有构造方法呢 ?
java.lang.reflect.AccessibleObject.class 中存在这么一个方法 : setAccessible(boolean flag)
来看下官方文档中是怎么定义这个方法的 .
从中我们可以知道 , 当该方法的参数被设置为 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() 方法 。
这个问题大家可以看下这篇文章,本文就不重复分析了。
参考文章