Java代审7-1:远程代码执行

百家 作者:锦行科技 2022-03-24 23:50:19


远程系统命令执行(操作系统命令注入或简称命令注入)是一种注入漏洞。攻击者注入的payload将作为操作系统命令执行。仅当Web应用程序代码包含操作系统调用并且调用中使用了用户输入时,才可能进行OS命令注入攻击。
在JavaWeb中,远程命令执行的方式有很多种:
1.命令注入
2.代码注入
3.表达式注入
4.SSTI注入
在下面的案例分析中会逐一讲解到上面的案例,但是在代码注入中,有一类是通过反序列化实现,这个会放在后面讲反序列化的案例中讲解。


0×01 产生原因

1、命令注入

在某种开发需求中,需要引入对系统本地命令的支持来完成特定功能,当未对输入做过滤时,则会产生命令注入。


2、代码注入

在正常的java程序中注入一段java代码并执行,即用户输入的数据当作java代码进行执行。


3、表达式注入

SpEL(Spring Expression Language)表达式注入, 是一种功能强大的表达式语言、用于在运行时查询和操作对象图,由于未对参数做过滤可造成任意命令执行。


4、SSTI注入

SSTI(Server Side Template Injection) 服务器模板注入, 服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容。


0×02 命令注入

2.1 应用场景

在开发过程中,开发人员可能需要对系统文件进行移动、删除或者执行一些系统命令,这时如果执行的命令用户可控,就会导致命令执行漏洞。
或者在一些公司的调控系统或者是监控系统中需要对集群主机的存活性进行检测时,调用命令执行模块对主机进行检测时导致恶意注入。


2.2 漏洞原理

这种漏洞原理很简单,主要就是找到执行系统命令的函数,看命令是否可控。
java 程序中执行系统命令的函数如下:
Runtime.exec
Process
ProcessBuilder.start
GroovyShell.evaluate


2.3 场景复现

2.3.1 Runtime实现
public class RunTimeDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {


String cmd = req.getParameter("cmd");
Process exec = Runtime.getRuntime().exec(cmd);
InputStream is = exec.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));

resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();


try {
exec.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}

String s = null;
while ((s = reader.readLine()) != null) {
System.out.println(s);
out.write(s + "<br/>");
}

out.close();
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}


# 对于多命令的注入,需要用到的就是连接符,常见的连接符有:
  • ; 多个命令顺序执行,命令之间无任何逻辑关系

  • | 前面命令输出结果作为后面命令的输入内容

  • || 逻辑或,当前面命令执行失败后,后面命令才会执行,否则后面命令不执行

  • & 前面命令执行后继续执行后面命令

  • && 逻辑与,当前面命令执行成功后,后面命令才会执行,否则后面命令不执行


但是在Runtime.getRuntime().exec()的实现中,存在着一些特殊的情况
  • 不存在命令注入的情况


//这种写法不存在命令注入
String ip = req.getParameter("ip");
Process exec = Runtime.getRuntime().exec("ping " + ip);
当我们输入 127.0.0.1&ipconfig 时,拼接出来的系统命令就是 ping 127.0.0.1&ipconfig,该命令在系统终端下是能正常运行的,但是在 Java 环境下就会运行失败,这里因为 Java 在程序处理的过程中,会将 127.0.0.1&ipconfig 当做一个完整的字符串而非两条命令,因此上面的代码不存在命令注入。


执行ping 127.0.0.1:


执行127.0.0.1&ipconfig:
- 使用连接符导致参数报错


String cmd = "ping 127.0.0.1 ; curl http://127.0.0.1:800";
String cmd = "ping 127.0.0.1 | curl http://127.0.0.1:800";
String cmd = "ping 127.0.0.1 || curl http://127.0.0.1:800";
String cmd = "ping 127.0.0.1 & curl http://127.0.0.1:800";
String cmd = "ping 127.0.0.1 && curl http://127.0.0.1:800";


所以针对这种情况实现的远程命令执行不支持多命令的拼接。
但是情况不是绝对的,如果开发在开发的时候使用的是下面这种命令会导致对命令的注入:
String cmd = "cmd /c ping 127.0.0.1&calc";


2.3.2 ProcessBuilder(Process)实现
public class ProcessBuilderDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

String output = null;
resp.setContentType("text/html;charset=utf-8");//解决页面编码问题
PrintWriter out = resp.getWriter();

// String cmd = req.getParameter("cmd");
// 模拟接收到的参数
String cmd ="cmd /c ping 127.0.0.1&calc";
String[] cmdSplit = cmd.split(" ");


ArrayList<String> cmdLines = new ArrayList<>();
for (int i = 0; i < cmdSplit.length; i++) {
cmdLines.add(cmdSplit[i]);
}
System.out.println(cmdLines.toString());

//使用ProcessBuilder创建命令执行对象
ProcessBuilder builder = new ProcessBuilder(cmdLines);
builder.redirectErrorStream(true);
Process process = builder.start();

//使用文件流读取输出结果
InputStream is = process.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));

while ((output = br.readLine()) != null) {
out.write(output + "</br>");
}

out.close();
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}


真实的生产环境下,例如是用来检测主机的存活,就有可能有下面的两种写法:
# 写法1
String cmdStart1 ="ping ";//后端写死
String cmdEnd1 = "127.0.0.1&whoami";//接收数据


执行结果:
如果是这种情况,连接符依然是不能产生作用,多命令的注入依然无法生效。但是下面这种写法就会造成多命令注入的情况。


# 写法2
String cmdStart2 ="cmd /c ping ";//后端写死
String cmdEnd2 = "127.0.0.1&&whoami";//接收数据


执行结果:
这种情况下,除了“;”,“||”达不到我们的目的,其他的连接符均能实现多命令注入.

2.3.3 Groovy实现
1、Groovy介绍
Groovy是Apache 旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。Groovy与 Java可以很好的互相调用并结合编程 ,比如在写 Groovy 的时候忘记了语法可以直接按Java的语法继续写,也可以在 Java 中调用 Groovy 脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。

2、特点
  • 同时支持静态和动态类型;

  • 支持运算符重载;

  • 本地语法列表和关联数组;

  • 对正则表达式的本地支持;

  • 各种标记语言,如XML和HTML原生支持;

  • Groovy对于Java开发人员来说很简单,因为Java和Groovy的语法非常相似;

  • 可以使用现有的Java库;

  • Groovy扩展了java.lang.Object;


3、代码注入利用
依赖导入
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.7</version>
</dependency>


利用形式
Runtime.getRuntime().exec("calc")和"whoami".execute() 本质相同
println "whoami".execute().text 还支持回显
同时Groovy也支持单引号闭合字符串。

4、简单的复现
由于Groovy作为一个库使用,它的使用方式有多种形式,在这里只做一种的常用的讲解,其他的使用方式在额外的章节中讲解。

4-1、编写测试类
import groovy.lang.GroovyShell;
import org.junit.Test;

public class MyTest {
@Test
public void test4() {
String cmd = "'calc'.execute()";
GroovyShell shell = new GroovyShell();
shell.evaluate(cmd);
}
}
这种方式就是将上面的命令传进来,直接执行,这种会出现在一些后台系统中执行系统命令的地方中使用,接收参数的形式可能为:
String input="ping 127.0.0.1";  //用户输入需要执行的命令
String cmd = "println \""+input+"\".execute().text"; //获取执行命令的回显


这种执行多命令的payload:
cmd /c ping 127.0.0.1&&whoami
输出结果:

在这里没有做编码处理,输出乱码,但是已经达到我们多命令注入的目的了。



推 荐 阅 读




漏洞预警 | 向日葵远程命令执行漏洞

Java代审6:XSS和SSRF

Java代审5:SQL 注入-Mybatis复现



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接