diff --git a/src/main/frontend/src/api/index.js b/src/main/frontend/src/api/index.js index 66db25a..7cb4199 100644 --- a/src/main/frontend/src/api/index.js +++ b/src/main/frontend/src/api/index.js @@ -90,4 +90,8 @@ export function deleteFile(name) { return api.post('/files/delete', { name }) } +export function uploadFile(formData) { + return api.post('/files/upload', formData) +} + export default api diff --git a/src/main/frontend/src/index.css b/src/main/frontend/src/index.css index b55bf85..4192acd 100644 --- a/src/main/frontend/src/index.css +++ b/src/main/frontend/src/index.css @@ -906,6 +906,32 @@ select { font-family: 'Inter', sans-serif; } +.file-upload-zone { + border: 2px dashed var(--border); + border-radius: 8px; + padding: 18px; + text-align: center; + cursor: pointer; + transition: all .2s; + margin-bottom: 10px; + color: var(--text-muted); + font-size: 13px; + font-family: 'Inter', sans-serif; +} +.file-upload-zone:hover { + border-color: var(--primary); + color: var(--primary); +} +.file-upload-zone.drag-over { + border-color: var(--primary); + background: rgba(99, 102, 241, 0.08); + color: var(--primary); +} +.file-upload-zone.uploading { + opacity: 0.6; + pointer-events: none; +} + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0ms !important; diff --git a/src/main/frontend/src/pages/Dashboard.jsx b/src/main/frontend/src/pages/Dashboard.jsx index 1158c7d..451c0cc 100644 --- a/src/main/frontend/src/pages/Dashboard.jsx +++ b/src/main/frontend/src/pages/Dashboard.jsx @@ -4,7 +4,8 @@ import { useTheme } from '../context/ThemeContext' import { getStatus, toggleServer, getGadgets, generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs, - getFiles, downloadFile, deleteFile as apiDeleteFile + getFiles, downloadFile, deleteFile as apiDeleteFile, + uploadFile } from '../api' export default function Dashboard() { @@ -53,6 +54,9 @@ export default function Dashboard() { const logEndRef = useRef(null) const [files, setFiles] = useState([]) const [filesLoading, setFilesLoading] = useState(false) + const [dragOver, setDragOver] = useState(false) + const [uploading, setUploading] = useState(false) + const fileInputRef = useRef(null) const ROUTING_OPTIONS = ['Basic', 'ELProcessor', 'Groovy', 'jdbcBypass1', 'jdbcBypass2', 'ldap2rmi', 'SnakeYaml', 'XStream', 'MemoryXXE'] @@ -149,7 +153,7 @@ export default function Dashboard() { setMsg({ success: '', error: res.data.error || 'Generation failed' }) } } catch (e) { - setMsg({ success: '', error: 'Failed to generate payload' }) + setMsg({ success: '', error: e.response?.data?.error || 'Failed to generate payload' }) } finally { setLoading(false) } } @@ -279,6 +283,56 @@ export default function Dashboard() { } catch { /* ignore */ } } + function handleDragOver(e) { + e.preventDefault() + e.stopPropagation() + setDragOver(true) + } + + function handleDragLeave(e) { + e.preventDefault() + e.stopPropagation() + setDragOver(false) + } + + function handleDrop(e) { + e.preventDefault() + e.stopPropagation() + setDragOver(false) + const droppedFiles = e.dataTransfer.files + if (droppedFiles.length > 0) { + doUpload(droppedFiles[0]) + } + } + + function handleFileSelect(e) { + const selectedFiles = e.target.files + if (selectedFiles.length > 0) { + doUpload(selectedFiles[0]) + } + e.target.value = '' + } + + async function doUpload(file) { + setUploading(true) + setMsg({ success: '', error: '' }) + try { + const formData = new FormData() + formData.append('file', file) + const res = await uploadFile(formData) + if (res.data.success) { + setMsg({ success: 'Uploaded: ' + res.data.name, error: '' }) + fetchFiles() + } else { + setMsg({ success: '', error: res.data.error || 'Upload failed' }) + } + } catch (e) { + setMsg({ success: '', error: 'Upload failed' }) + } finally { + setUploading(false) + } + } + useEffect(() => { if (activeJndiTab !== 'logs') return fetchLogs() @@ -814,6 +868,25 @@ export default function Dashboard() { {filesLoading ? 'Loading...' : 'Refresh'} +
fileInputRef.current?.click()} + > + + {uploading ? ( + Uploading... + ) : ( + Drop file here or click to upload + )} +
{files.length === 0 ? (
No saved files
diff --git a/src/main/java/com/qi4l/JYso/Starter.java b/src/main/java/com/qi4l/JYso/Starter.java index a3f8a76..d9ed254 100644 --- a/src/main/java/com/qi4l/JYso/Starter.java +++ b/src/main/java/com/qi4l/JYso/Starter.java @@ -38,7 +38,12 @@ public class Starter { if (args[0].equals("-y")) { JYsoMode = true; - ysoserial.run(args); + try { + ysoserial.run(args); + } catch (Exception e) { + System.err.println("[!] " + e.getMessage()); + System.exit(1); + } } } } diff --git a/src/main/java/com/qi4l/JYso/gadgets/Config/ysoserial.java b/src/main/java/com/qi4l/JYso/gadgets/Config/ysoserial.java index e203029..91c3815 100644 --- a/src/main/java/com/qi4l/JYso/gadgets/Config/ysoserial.java +++ b/src/main/java/com/qi4l/JYso/gadgets/Config/ysoserial.java @@ -84,9 +84,7 @@ public class ysoserial { //载入gadget final Class> payloadClass = ObjectPayload.Utils.getPayloadClass(payloadType); if (payloadClass == null) { - System.err.println("Invalid payload type '" + payloadType + "'"); - printUsage(options); - System.exit(1); + throw new IllegalArgumentException("Invalid payload type '" + payloadType + "'"); } diff --git a/src/main/java/com/qi4l/JYso/web/JYsoWebApplication.java b/src/main/java/com/qi4l/JYso/web/JYsoWebApplication.java index 0b156c5..f1efd9b 100644 --- a/src/main/java/com/qi4l/JYso/web/JYsoWebApplication.java +++ b/src/main/java/com/qi4l/JYso/web/JYsoWebApplication.java @@ -15,6 +15,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import javax.servlet.DispatcherType; +import javax.servlet.MultipartConfigElement; import java.awt.*; import java.awt.datatransfer.StringSelection; import java.util.EnumSet; @@ -70,7 +71,9 @@ public class JYsoWebApplication { context.addServlet(new ServletHolder(new AuthServlet()), "/api/auth/login"); - context.addServlet(new ServletHolder(new JettyApiServlet()), "/api/*"); + ServletHolder apiHolder = new ServletHolder(new JettyApiServlet()); + apiHolder.getRegistration().setMultipartConfig(new MultipartConfigElement("")); + context.addServlet(apiHolder, "/api/*"); context.addFilter(new FilterHolder(new SpaFallbackFilter()), "/*", EnumSet.of(DispatcherType.REQUEST)); diff --git a/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java b/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java index b8ab31e..9afc5b9 100644 --- a/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java +++ b/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java @@ -11,16 +11,21 @@ import com.qi4l.JYso.gadgets.Config.Config; import com.qi4l.JYso.gadgets.Config.ysoserial; import javax.servlet.ServletException; +import javax.servlet.annotation.MultipartConfig; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; +@MultipartConfig public class JettyApiServlet extends HttpServlet { @Override @@ -58,6 +63,8 @@ public class JettyApiServlet extends HttpServlet { handleFileDownload(req, resp); } else if ("/files/delete".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) { handleFileDelete(req, resp); + } else if ("/files/upload".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) { + handleFileUpload(req, resp); } else { resp.setStatus(404); resp.getWriter().write("{\"error\":\"Not found\"}"); @@ -159,6 +166,34 @@ public class JettyApiServlet extends HttpServlet { resp.getWriter().write(JSON.toJSONString(result)); } + private void handleFileUpload(HttpServletRequest req, HttpServletResponse resp) throws IOException { + Map result = new LinkedHashMap<>(); + try { + Part filePart = req.getPart("file"); + if (filePart == null || filePart.getSize() == 0) { + result.put("success", false); + result.put("error", "no file uploaded"); + resp.getWriter().write(JSON.toJSONString(result)); + return; + } + String fileName = filePart.getSubmittedFileName(); + if (fileName == null || fileName.isEmpty()) { + fileName = "uploaded_file"; + } + Path targetPath = Paths.get(fileName); + try (InputStream input = filePart.getInputStream()) { + Files.copy(input, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + result.put("success", true); + result.put("name", fileName); + result.put("size", Files.size(targetPath)); + } catch (ServletException e) { + result.put("success", false); + result.put("error", "upload failed: " + e.getMessage()); + } + resp.getWriter().write(JSON.toJSONString(result)); + } + private void handleServersStart(HttpServletRequest req, HttpServletResponse resp) throws IOException { JSONObject json = readJson(req); Map result = new LinkedHashMap<>(); @@ -236,27 +271,39 @@ public class JettyApiServlet extends HttpServlet { resp.getWriter().write(JSON.toJSONString(result)); return; } - boolean nowRunning; + boolean nowRunning = false; switch (server.toLowerCase()) { case "ldap": - if (LdapServer.isRunning) LdapServer.stop(); - else new Thread(() -> { try { LdapServer.start(); } catch (Exception ignored) {} }, "ldap-toggler").start(); - nowRunning = !LdapServer.isRunning; + if (LdapServer.isRunning) { + LdapServer.stop(); + } else { + new Thread(() -> { try { LdapServer.start(); } catch (Exception ignored) {} }, "ldap-toggler").start(); + nowRunning = true; + } break; case "ldaps": - if (LdapsServer.isRunning) LdapsServer.stop(); - else new Thread(() -> { try { LdapsServer.start(); } catch (Exception ignored) {} }, "ldaps-toggler").start(); - nowRunning = !LdapsServer.isRunning; + if (LdapsServer.isRunning) { + LdapsServer.stop(); + } else { + new Thread(() -> { try { LdapsServer.start(); } catch (Exception ignored) {} }, "ldaps-toggler").start(); + nowRunning = true; + } break; case "http": - if (HTTPServer.isRunning) HTTPServer.stop(); - else new Thread(() -> { try { HTTPServer.start(); } catch (Exception ignored) {} }, "http-toggler").start(); - nowRunning = !HTTPServer.isRunning; + if (HTTPServer.isRunning) { + HTTPServer.stop(); + } else { + new Thread(() -> { try { HTTPServer.start(); } catch (Exception ignored) {} }, "http-toggler").start(); + nowRunning = true; + } break; case "rmi": - if (RMIServer.isRunning) RMIServer.stop(); - else new Thread(() -> { try { RMIServer.start(); } catch (Exception ignored) {} }, "rmi-toggler").start(); - nowRunning = !RMIServer.isRunning; + if (RMIServer.isRunning) { + RMIServer.stop(); + } else { + new Thread(() -> { try { RMIServer.start(); } catch (Exception ignored) {} }, "rmi-toggler").start(); + nowRunning = true; + } break; default: result.put("success", false);