Do Not Trust! Ghidra Arbitrary Command Execution Through Project Sharing

Archiving the intermediate analysis result and sharing with other researchers is a common practice in the field of binary analysis. Popular disassemblers such as IDA and Ghidra also provide the project archiving functionality to help researchers communicating their results. However, it turns out that such project sharing is not secure (at all). For example, researchers have found that an XXE vulnerability existed in the project parsing logic of Ghidra, which can be abused to achieve arbitrary file read or even command execution on a particular platform(i.e., Windows). In this post, we are presenting an even more powerful exploit chain discovered in Ghidra, which can be exploited via a malicious project archive to execute arbitrary code in the victim machine no matter what platform he is using.

Vulnerablity Description

A path traversal vulnerability exists in RestoreTask.java from package ghidra.app.plugin.core.archive. This vulnerability allows attackers to overwrite arbitrary files on the system. To achieve arbitrary code execution, one of the solutions is to overwrite some critical ghidra modules, e.g., decompile module (In this case we need to know the installation path of ghidra).

Exploit Analysis

The arbitrary code execution exploit chain consists of two nodes. The first node overwrites arbitrary files in the system, and the second node executes the malware containing in the crafted project file.

Overwrite Arbitrary File

In Ghidra, every project is a jar archive file. When restoring a project, Ghidra will extract the project files and store them in the file system. However, Ghidra doesn’t check whether the file name of every archive entity is legal or not. By inserting a malicious binary with a file name starting with ../, attackers are able to let ghidra overwrite arbitrary files on the system.

The vulnerability locates at Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/RestoreTask.java
The unjarArchive method iterates every file entity in the project archive and stores it on the file system directly using its original filename recording in the archive.

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
private void unjarArchive(TaskMonitor monitor) throws IOException {
JarInputStream jarIn = new JarInputStream(new FileInputStream(jarFile));
JarReader reader = new JarReader(jarIn);

// position input stream to the entry after the .gpr entry since
// we don't want to create that file
jarIn.getNextJarEntry();

// the next entry should be JAR_FORMAT
jarIn.getNextJarEntry();

File projectFile = projectLocator.getMarkerFile();
File projectDir = projectLocator.getProjectDir();
if (projectFile.exists()) {
throw new DuplicateFileException("Project already exists: " + projectFile);
}
if (projectDir.exists()) {
throw new DuplicateFileException("Project already exists: " + projectDir);
}

reader.createRecursively(projectDir.getAbsolutePath() + File.separator, monitor);

jarIn.close();
if (monitor.isCancelled()) {
return;
}

// Remove unwanted project properties file - will get re-created when project is opened
File file = new File(projectDir, ArchivePlugin.PROJECT_PROPERTY_FILE);
file.delete();

// Remove unwanted project state file
file = new File(projectDir, ArchivePlugin.PROJECT_STATE_FILE);
file.delete();

// Remove unwanted project state save directory
file = new File(projectDir, ArchivePlugin.OLD_PROJECT_SAVE_DIR);
FileUtilities.deleteDir(file);

// Remove unwanted project groups directory
file = new File(projectDir, ArchivePlugin.OLD_PROJECT_GROUPS_DIR);
FileUtilities.deleteDir(file);

// Remove unwanted folder property files
removeOldFolderProperties(projectDir);

// create the .gpr file for the new project

if (!projectFile.createNewFile()) {
throw new IOException("Couldn't create file " + projectFile.getAbsolutePath());
}
}

Code Execution

As demonstrated by the following code, the DecompileProcess.java locating in the ghidra.app.decompiler package will executes the decompile under GHIDRA_INSTALL_DIR/Ghidra/Features/Decompiler/os/YOUR_ARCH/ folder. This method will be called when ghidra is initiated and start to decompile the binary.

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
private void setup() throws IOException {
if (disposestate != DisposeState.NOT_DISPOSED) {
throw new IOException("Decompiler has been disposed");
}
if (nativeProcess != null) {
// Something bad happened to the process or the interface
// and now we try to restart
nativeProcess.destroy(); // Make sure previous bad process is killed
nativeProcess = null;
}
if (exepath == null) {
throw new IOException("Could not find decompiler executable");
}
try {
nativeProcess = runtime.exec(exepath);

nativeIn = nativeProcess.getInputStream();
nativeOut = nativeProcess.getOutputStream();
statusGood = true;
}
catch (IOException e) {
disposestate = DisposeState.DISPOSED_ON_STARTUP_FAILURE;
statusGood = false;
Msg.showError(this, null, "Problem launching decompiler",
"Please report this stack trace to the Ghidra Team", e);
throw e;
}
}

Attack Demo

Here is a demo of the attack behavior. Youtube