

Java命令注入原理并结合Java Instrument技术
source link: http://4hou.win/wordpress/?p=44949
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、前言
命令注入:恶意用户构造恶意请求,对一些执行系统命令的功能点进行构造注入,从而达到执行命令的效果。
二、演示环境搭建
这里采用springboot+swagger搭建一个模拟的web环境:启动成功后访问: http://localhost:8090/swagger-ui.html#/commandi 主要有三个接口:
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.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
主要分为两大类,第一类入参为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
主要也分为两大类,第一类入参为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="{" + "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” 一定要加下双引号,不然会出现异常 运行结果如下:
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使用运行结果: 看了 l1nk3r关于使用编码,linux下可以用bash的base64编码来解决这个特殊字符的问题。这里的利用条件一定要是这个 入参String完全可控 ,或者存在参数注入。
/bin/sh -c {echo,dG91Y2glMjAvdG1wL3p6eno=}|{base64,-d}|{bash,-i}
我们运行下这个绕过的方法: 运行结果如下:
成功执行:
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,转载请注明来自FreeBuf.COM
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK