Java命令注入原理并结合Java Instrument技术

林哲

发表文章数:996

专业SEO优化

  • 正规SEO优化手法
  • 承诺流量+权重提升
  • 强大的团队解决问题
  • 全心全意的服务
  • 立即咨询
    首页 » WEB安全 » Java命令注入原理并结合Java Instrument技术

    文章目录

    • 一、前言
    • 二、演示环境搭建
    • 三、java执行系统命令的方法
    • 3 Runtime
    • 四、ProcessBuilder
    • 五、分析结果
    • 参考

    一、前言

    命令注入:恶意用户构造恶意请求,对一些执行系统命令的功能点进行构造注入,从而达到执行命令的效果。

    二、演示环境搭建

    这里采用springboot+swagger搭建一个模拟的web环境:启动成功后访问:http://localhost:8090/swagger-ui.html#/commandi

    Java命令注入原理并结合Java Instrument技术主要有三个接口:
     

    1 /command/exec/string 主要实现Runtime.getRuntime().exec() 入参为String
    2 /command/exec/array 主要实现Runtime.getRuntime().exec() 入参为String[]
    3 /command/processbuilder 主要实现ProcessBuilder 入参为List

    源码:https://gitee.com/cor0ps/java-range.git这里取访问一个栗子:

    Java命令注入原理并结合Java Instrument技术
     

    三、java执行系统命令的方法

    - java.lang.Runtime.getRuntime().exec()
    - java.lang.ProcessBuilder
    - com.jcraft.jsch.ChannelExec

    特殊情况下method.invoke()也是执行命令,后续反序列化会细说。前置条件:如果我们需要执行系统管道(|)、;、&&等,我们必须要创建shell来执行命令。java shell

    符号&形式 说明
    cmd1|cmd2 |管道表示前一个命令执行的结果重定向给后一个命令,不管cmd1是否成功,cmd2都会执行
    cmd1;cmd2 多语句的分隔符
    cmd2&&cmd2 逻辑操作符,两个为真时返回真
    cmd1||cmd2 逻辑操作符,测试条件有一个为真返回真
    {cmd2,cmd2} 花括号扩展,扩展参数列表,命令依次进行扩展
    `cmd2,cmd2` 反引号的功能是命令替换,将反引号中的字符串做为命令来执行
    $(cmd2,cmd2) 用于变量替换,换句话说就是取变量的值

    3 Runtime

    Java命令注入原理并结合Java Instrument技术主要分为两大类,第一类入参为String类型,第二类入参为String[]类型
     

     public Process exec(String command);
    public Process exec(String cmdarray[]);

    我们先分析第一种情况,入参String:

     @ApiOperation(value = "命令执行", notes = "exec接受string参数")
      @PostMapping(value = "/exec/string", produces = MediaType.APPLICATION_JSON_VALUE)
      public ResponseResult execString(@RequestBody PathInfo path) throws IOException {
      String cmdStr;
      //1.日志注入 2.path本身校验防跨目录等等
      logger.info("Runtime.getRuntime().exec args:" + path);
      if(path.getType()==1)
      {
          cmdStr = "/bin/sh -c" + path;
      }else {
          cmdStr = "ping " + path;
      }
      String result=ShellExcute.Exec(cmdStr);
      // p.getInputStream();
      if (result != null) {
      return new ResponseResult<>(result, "执行成功", 200);
      }
      //System.out.println(result);
      return new ResponseResult<>("result is null", "执行成功", 200);
      }

    这里先分析下源码,分析发现代码会进入到exec(String command, String[] envp, File dir)函数中:

       public Process exec(String command, String[] envp, File dir)
      throws IOException {
      if (command.length() == 0)
          throw new IllegalArgumentException("Empty command");
      StringTokenizer st = new StringTokenizer(command);
      String[] cmdarray = new String[st.countTokens()];
      for (int i = 0; st.hasMoreTokens(); i++)
          cmdarray[i] = st.nextToken();
      return exec(cmdarray, envp, dir);
      }

    这里关注下StringTokenizer类及skipDelimiters方法

     public StringTokenizer(String str) {
      this(str, " \t\n\r\f", false);
      }

    这里会对传入的字符串进行处理,重点处理空格,\t\n\r\f字符,然后调用exec(cmdarray, envp, dir),继续跟踪发现最终调用ProcessBuilder:

    public Process exec(String[] cmdarray, String[] envp, File dir)
      throws IOException {
      return new ProcessBuilder(cmdarray)
          .environment(envp)
          .directory(dir)
          .start();
      }

    从这上面两段代码中我们可以知道:

    1、string入参被转化成string[];^_^
    2、Runtime最终执行在ProcessBuilder中,参数为String可变类型

    四、ProcessBuilder

    Java命令注入原理并结合Java Instrument技术主要也分为两大类,第一类入参为List类型,第二类入参为String可变参数类型(可以0到多个Object对象,或者一个Object[])
     

    public ProcessBuilder(List command);
    public ProcessBuilder(String... command);

    跟踪ProcessBuilder(String… command),发现入参将转化为List类型

     public ProcessBuilder(String... command) {
      this.command = new ArrayList<>(command.length);
      for (String arg : command)
          this.command.add(arg);
      }

    进入start()方法中,发现存在prog变量,为cmdarray[0]的值,就是/bin/sh或者ping;如果security不为null,就会进入checkExec()。最终进入ProcessImpl。

     return ProcessImpl.start(cmdarray,
                                   environment,
                                   dir,
                                   redirects,
                                   redirectErrorStream);

    进入之后发现可以看到最终调用的java.lang.UNIXProcess这个类执行命令,(和windows下的代码不相同),这里执行什么命令根据cmdarray[0] 来判断,最后调用forkAndExec ^3,来为命令创建环境等操作。从3和 4知道,Linux环境最终的执行都是java.lang.UNIXProcess类,那么我们可以使用类似百度OpenRASP的java Instrument技术,监控cmdarray参数,不用每次调试。百度的或者某为的都比较庞大复杂,可以使用我这个轻巧简单,可拓展。

     if ("java.lang.UNIXProcess".equals(className)) {
          try {
              ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
              CtBehavior[] ctBehaviors = ctClass.getDeclaredConstructors();
              for (CtBehavior cb : ctBehaviors) {
                 //System.out.println("UNIXProcess:" + cb.getName());
                  if (cb.getName().equals("UNIXProcess")) {
                      String src="https://www.freebuf.com/articles/web/{" +
                              "String prog_1=new String($1);" +
                              "String cmd_1=new String($2);" +
                              "System.out.println(\"unixprocess_result:\"+prog_1+\" \"+cmd_1);" +
                              "}";
                      cb.insertBefore(src);
                  }
              }
              bytesCode = ctClass.toBytecode();
      } catch (IOException e) {
          e.printStackTrace();
      } catch (CannotCompileException e) {
          e.printStackTrace();
      }
      }

    unixprocess_result为识别关键字,方便后续搜索用户执行的命令。开启方式,在VM Options添加如下语句:-javaagent:”/path/agent.jar” 一定要加下双引号,不然会出现异常

    Java命令注入原理并结合Java Instrument技术运行结果如下:
     

    Instrument Agent start!

    日志出现上面字段,表示我们成功运行,那么可以继续下步测试。

    五、分析结果

    Runtime.getRuntime.exec入参String和String[]对比:/command/exec/string请求对应的入参是String类型,发送如下body:

    {"path":"echo \"xxxx\t\n\r\f\">/tmp/xxx" ,"type":1}

    /command/exec/array请求对应的入参是String[]类型,发送如下body:

    {"path":"echo \"xxx\">/tmp/yyy" ,"type":1}

    依次发送上面的请求,得到的结果如下:

    #string类型会被StringTokenizer过滤
    unixprocess_result:prog:/bin/sh cmd:-cecho"xxxx">/tmp/xxx 未执行成功
    #string[]类型没有过滤
    unixprocess_result:prog:/bin/sh cmd:-cecho "xxx">/tmp/yyy 执行成功

    java instrument使用运行结果:

    Java命令注入原理并结合Java Instrument技术看了 l1nk3r关于使用编码,linux下可以用bash的base64编码来解决这个特殊字符的问题。这里的利用条件一定要是这个入参String完全可控,或者存在参数注入。
     

    /bin/sh -c {echo,dG91Y2glMjAvdG1wL3p6eno=}|{base64,-d}|{bash,-i}

    我们运行下这个绕过的方法:

    Java命令注入原理并结合Java Instrument技术运行结果如下:
    Java命令注入原理并结合Java Instrument技术成功执行:
    Java命令注入原理并结合Java Instrument技术
     

    javaagent源码:https://gitee.com/cor0ps/Agent.git

    参考

    1.https://mp.weixin.qq.com/s/ZS-hA03ykKleDjgN8oWZDw

    2.https://alvinalexander.com/java/java-exec-system-command-pipeline-pipe

    3.https://blog.csdn.net/GV7lZB0y87u7C/article/details/79860776

    4.https://www.cnblogs.com/rickiyang/p/11336268.html

    5.https://docs.huihoo.com/javaone/2015/CON3597-Having-Fun-with-Javassist.pdf

    *本文作者:buglab,转载请注明来自ALA林哲

    分享到:
    赞(0)

    评论 抢沙发

    7 + 4 =


    Vieu4.5主题
    专业打造轻量级个人企业风格博客主题!专注于前端开发,全站响应式布局自适应模板。
    切换注册

    登录

    忘记密码 ?

    切换登录

    注册