perf:前端大改3

This commit is contained in:
qi4l
2026-05-01 00:23:07 +08:00
parent b5a258c8b7
commit 02e817167e
10 changed files with 1124 additions and 17 deletions
+4
View File
@@ -68,4 +68,8 @@ export function getConfig() {
return api.get('/config')
}
export function getLogs(count) {
return api.get('/logs', { params: { count } })
}
export default api
+150
View File
@@ -533,6 +533,14 @@ code, pre, .mono {
transform: translateX(calc(100% + 2px));
}
.tab-segment-control.tab-3::before {
transform: translateX(calc(200% + 4px));
}
.tab-segment-control.tabs-3::before {
width: calc(33.333% - 4px);
}
.tab-segment-btn {
position: relative;
z-index: 1;
@@ -646,6 +654,8 @@ code, pre, .mono {
color: var(--text-secondary);
margin-top: 14px;
white-space: pre-wrap;
word-break: break-all;
overflow-wrap: break-word;
max-height: 160px;
overflow-y: auto;
}
@@ -658,6 +668,40 @@ input[type="checkbox"] {
cursor: pointer;
}
.base64-toggle {
width: 44px;
height: 26px;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 13px;
position: relative;
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast);
outline: none;
flex-shrink: 0;
}
.base64-toggle:focus-visible {
box-shadow: 0 0 0 3px var(--accent-glow);
}
.base64-toggle.active {
background: var(--accent);
border-color: var(--accent);
}
.base64-toggle-thumb {
width: 20px;
height: 20px;
background: #FFFFFF;
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
transition: transform var(--transition-fast);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.base64-toggle.active .base64-toggle-thumb {
transform: translateX(18px);
}
label.form-group {
display: flex;
align-items: center;
@@ -667,6 +711,112 @@ label.form-group {
cursor: pointer;
}
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
color: var(--text-secondary);
transition: all var(--transition-fast);
padding: 0;
}
.copy-btn:hover {
background: var(--accent-glow);
border-color: var(--accent);
color: var(--accent);
}
.advanced-section {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 4px;
}
.adv-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
}
.adv-check {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
padding: 4px 0;
}
.adv-check em {
font-style: normal;
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
}
.adv-check input[type="checkbox"] {
width: 14px;
height: 14px;
margin: 0;
flex-shrink: 0;
}
select {
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23999999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 30px;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.log-container {
background: #0d1117;
border: 1px solid #30363d;
border-radius: 12px;
padding: 12px;
max-height: 380px;
overflow-y: auto;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.65;
transition: background var(--transition-fluid), border-color var(--transition-fluid);
}
.log-line {
color: #c9d1d9;
white-space: pre-wrap;
word-break: break-all;
padding: 1px 0;
}
.log-empty {
color: #8b949e;
text-align: center;
padding: 32px 0;
font-family: 'Inter', sans-serif;
font-size: 13px;
}
@media (max-width: 768px) {
.page-shell { padding: 48px 16px 32px; }
.grid-2 { grid-template-columns: 1fr; }
+233 -13
View File
@@ -1,9 +1,9 @@
import { useState, useEffect, useMemo, useCallback } from 'react'
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTheme } from '../context/ThemeContext'
import {
getStatus, toggleServer, getGadgets,
generatePayload as apiGenerate, updateConfig, setAuthToken
generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs
} from '../api'
export default function Dashboard() {
@@ -21,6 +21,18 @@ export default function Dashboard() {
const [selectedGadget, setSelectedGadget] = useState('')
const [payloadCmd, setPayloadCmd] = useState('')
const [payloadResult, setPayloadResult] = useState('')
const [encodeBase64, setEncodeBase64] = useState(true)
const [copied, setCopied] = useState(false)
const [showAdvanced, setShowAdvanced] = useState(false)
const [inherit, setInherit] = useState(false)
const [obscure, setObscure] = useState(false)
const [dcfp, setDcfp] = useState('')
const [dirtyType, setDirtyType] = useState('')
const [dirtyLength, setDirtyLength] = useState('')
const [noComSun, setNoComSun] = useState(false)
const [mozillaClassLoader, setMozillaClassLoader] = useState(false)
const [rhino, setRhino] = useState(false)
const [utf8Overlong, setUtf8Overlong] = useState(false)
const [payloadSubTab, setPayloadSubTab] = useState('gadget')
const [gadgetOpen, setGadgetOpen] = useState(false)
const [jndiPayloadResult, setJndiPayloadResult] = useState('')
@@ -35,6 +47,9 @@ export default function Dashboard() {
const [ldapsClassLoaderResult, setLdapsClassLoaderResult] = useState('')
const [routing, setRouting] = useState('ELProcessor')
const [routingOpen, setRoutingOpen] = useState(false)
const [logLines, setLogLines] = useState([])
const [logLoading, setLogLoading] = useState(false)
const logEndRef = useRef(null)
const ROUTING_OPTIONS = ['Basic', 'ELProcessor', 'Groovy', 'jdbcBypass1', 'jdbcBypass2', 'ldap2rmi', 'SnakeYaml', 'XStream', 'MemoryXXE']
@@ -120,7 +135,8 @@ export default function Dashboard() {
setLoading(true)
setMsg({ success: '', error: '' })
try {
const res = await apiGenerate({ gadget: gadgetName, command: payloadCmd, filename: saveFilename })
const res = await apiGenerate({ gadget: gadgetName, command: payloadCmd, filename: saveFilename, encodeBase64,
inherit, obscure, dcfp, dirtyType, dirtyLength, noComSun, mozillaClassLoader, rhino, utf8Overlong })
if (res.data.success) {
setPayloadResult(res.data.message || 'Payload generated successfully')
setMsg({ success: 'Payload generated', error: '' })
@@ -133,6 +149,24 @@ export default function Dashboard() {
} finally { setLoading(false) }
}
async function handleCopyPayload() {
if (!payloadResult) return
try {
await navigator.clipboard.writeText(payloadResult)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch { /* ignore */ }
}
async function copyText(text) {
if (!text) return
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch { /* ignore */ }
}
function handleGenerateJndiPayload() {
const gadgetName = (jndiGadgetInput || selectedGadget || '').trim()
if (!gadgetName || !payloadCmd) return
@@ -200,8 +234,26 @@ export default function Dashboard() {
navigate('/login', { replace: true })
}
async function fetchLogs() {
try {
const res = await getLogs(100)
setLogLines(res.data.logs || [])
} catch { /* ignore */ }
}
useEffect(() => { loadStatus(); loadGadgets() }, [])
useEffect(() => {
if (activeJndiTab !== 'logs') return
fetchLogs()
const id = setInterval(fetchLogs, 2000)
return () => clearInterval(id)
}, [activeJndiTab])
useEffect(() => {
if (logEndRef.current) logEndRef.current.scrollIntoView({ behavior: 'smooth' })
}, [logLines])
return (
<div>
<div className="dashboard-bg" />
@@ -293,7 +345,7 @@ export default function Dashboard() {
<div className="glass-card section-enter">
<div className="control-bar">
<div className={'tab-segment-control' + (activeJndiTab === 'payload' ? ' config-tab' : '')}>
<div className={'tab-segment-control tabs-3' + (activeJndiTab === 'payload' ? ' config-tab' : '') + (activeJndiTab === 'logs' ? ' tab-3' : '')}>
<button
className={'tab-segment-btn' + (activeJndiTab === 'config' ? ' active' : '')}
onClick={() => setActiveJndiTab('config')}
@@ -304,7 +356,13 @@ export default function Dashboard() {
className={'tab-segment-btn' + (activeJndiTab === 'payload' ? ' active' : '')}
onClick={() => setActiveJndiTab('payload')}
>
Payload Generator
Payload
</button>
<button
className={'tab-segment-btn' + (activeJndiTab === 'logs' ? ' active' : '')}
onClick={() => setActiveJndiTab('logs')}
>
Logs
</button>
</div>
</div>
@@ -432,13 +490,40 @@ export default function Dashboard() {
{loading ? 'Generating...' : 'Generate'}
</button>
{jndiPayloadResult && (
<div className="payload-output" style={{ marginTop: 14 }}>{jndiPayloadResult}</div>
<div className="payload-output" style={{ marginTop: 14, position: 'relative', paddingRight: 42 }}>
{jndiPayloadResult}
<button className="copy-btn" onClick={() => copyText(jndiPayloadResult)} title="Copy to clipboard">
{copied ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M3 11V3h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
)}
</button>
</div>
)}
{rmiPayloadResult && (
<div className="payload-output" style={{ marginTop: 8 }}>{rmiPayloadResult}</div>
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{rmiPayloadResult}
<button className="copy-btn" onClick={() => copyText(rmiPayloadResult)} title="Copy to clipboard">
{copied ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M3 11V3h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
)}
</button>
</div>
)}
{ldapsPayloadResult && (
<div className="payload-output" style={{ marginTop: 8 }}>{ldapsPayloadResult}</div>
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{ldapsPayloadResult}
<button className="copy-btn" onClick={() => copyText(ldapsPayloadResult)} title="Copy to clipboard">
{copied ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M3 11V3h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
)}
</button>
</div>
)}
</div>
)}
@@ -482,26 +567,152 @@ export default function Dashboard() {
{loading ? 'Generating...' : 'Generate'}
</button>
{classLoaderResult && (
<div className="payload-output" style={{ marginTop: 14 }}>{classLoaderResult}</div>
<div className="payload-output" style={{ marginTop: 14, position: 'relative', paddingRight: 42 }}>
{classLoaderResult}
<button className="copy-btn" onClick={() => copyText(classLoaderResult)} title="Copy to clipboard">
{copied ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M3 11V3h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
)}
</button>
</div>
)}
{rmiClassLoaderResult && (
<div className="payload-output" style={{ marginTop: 8 }}>{rmiClassLoaderResult}</div>
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{rmiClassLoaderResult}
<button className="copy-btn" onClick={() => copyText(rmiClassLoaderResult)} title="Copy to clipboard">
{copied ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M3 11V3h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
)}
</button>
</div>
)}
{ldapsClassLoaderResult && (
<div className="payload-output" style={{ marginTop: 8 }}>{ldapsClassLoaderResult}</div>
<div className="payload-output" style={{ marginTop: 8, position: 'relative', paddingRight: 42 }}>
{ldapsClassLoaderResult}
<button className="copy-btn" onClick={() => copyText(ldapsClassLoaderResult)} title="Copy to clipboard">
{copied ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M3 11V3h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
)}
</button>
</div>
)}
</div>
)}
</div>
)}
{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={fetchLogs} disabled={logLoading}>
{logLoading ? 'Loading...' : 'Refresh'}
</button>
</div>
<div className="log-container">
{logLines.length === 0 ? (
<div className="log-empty">No events yet. Start a server or wait for incoming requests.</div>
) : (
logLines.map((line, i) => (
<div key={i} className="log-line">{line}</div>
))
)}
<div ref={logEndRef} />
</div>
</div>
)}
</div>
</>
)}
{mode === 'gadget' && (
<div className="glass-card section-enter">
<div className="header" style={{ padding: 0, marginBottom: 16 }}>
<h2>Payload Generator</h2>
<div style={{ position: 'relative' }}>
<button
className={`btn btn-secondary${showAdvanced ? ' active-tab' : ''}`}
style={{ padding: '6px 14px', fontSize: 12, width: 'auto' }}
onClick={() => setShowAdvanced(v => !v)}
>
{showAdvanced ? 'Hide Options' : 'Advanced Options'}
</button>
</div>
<div className="control-bar" style={{ marginBottom: 12 }}>
<label className="form-group" style={{ marginBottom: 0, display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
<div className={`base64-toggle${encodeBase64 ? ' active' : ''}`}
onClick={() => setEncodeBase64(v => !v)}
role="switch"
aria-checked={encodeBase64}
tabIndex={0}
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setEncodeBase64(v => !v); } }}>
<div className="base64-toggle-thumb" />
</div>
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-secondary)' }}>Base64 Encode Output</span>
</label>
</div>
{showAdvanced && (
<div className="advanced-section section-enter">
<div className="adv-grid">
<label className="adv-check">
<input type="checkbox" checked={inherit} onChange={e => setInherit(e.target.checked)} />
<span>Inherit AbstractTranslet <em>(-i)</em></span>
</label>
<label className="adv-check">
<input type="checkbox" checked={obscure} onChange={e => setObscure(e.target.checked)} />
<span>Obscure (reflection bypass RASP) <em>(-o)</em></span>
</label>
<label className="adv-check">
<input type="checkbox" checked={noComSun} onChange={e => setNoComSun(e.target.checked)} />
<span>Force org.apache.XXX.TemplatesImpl <em>(-ncs)</em></span>
</label>
<label className="adv-check">
<input type="checkbox" checked={mozillaClassLoader} onChange={e => setMozillaClassLoader(e.target.checked)} />
<span>Mozilla DefiningClassLoader <em>(-mcl)</em></span>
</label>
<label className="adv-check">
<input type="checkbox" checked={rhino} onChange={e => setRhino(e.target.checked)} />
<span>Rhino Engine <em>(-rh)</em></span>
</label>
<label className="adv-check">
<input type="checkbox" checked={utf8Overlong} onChange={e => setUtf8Overlong(e.target.checked)} />
<span>UTF-8 Overlong Encoding <em>(-utf)</em></span>
</label>
</div>
<div className="adv-grid" style={{ gridTemplateColumns: '1fr 1fr', marginTop: 10 }}>
<div className="form-group" style={{ marginBottom: 0 }}>
<label style={{ fontSize: 12 }}>DefineClassFromParameter <em>(-dcfp)</em></label>
<input type="text" value={dcfp} placeholder="parameter name"
onChange={e => setDcfp(e.target.value)} />
</div>
<div className="form-group" style={{ marginBottom: 0 }}>
<label style={{ fontSize: 12 }}>Dirty Type <em>(-dt)</em></label>
<select value={dirtyType} onChange={e => setDirtyType(e.target.value)}
style={{ width: '100%', padding: '9px 12px', borderRadius: 'var(--input-radius)', border: '1px solid var(--border-color)', background: 'var(--bg-input)', color: 'var(--text-primary)', fontSize: 13, fontFamily: 'inherit', outline: 'none' }}>
<option value="">None</option>
<option value="1">1: Random Hashable Collections</option>
<option value="2">2: LinkedList Nesting</option>
<option value="3">3: TC_RESET in Serialized Data</option>
</select>
</div>
<div className="form-group" style={{ marginBottom: 0 }}>
<label style={{ fontSize: 12 }}>Dirty Length <em>(-dl)</em></label>
<input type="number" value={dirtyLength} placeholder="length/counts"
onChange={e => setDirtyLength(e.target.value)} />
</div>
<div></div>
</div>
</div>
)}
<div style={{ position: 'relative', marginTop: showAdvanced ? 14 : 0 }}>
<div className="form-group">
<label>Gadget</label>
<input type="text"
@@ -543,7 +754,16 @@ export default function Dashboard() {
{loading ? 'Generating...' : 'Generate'}
</button>
{payloadResult && (
<div className="payload-output" style={{ marginTop: 14 }}>{payloadResult}</div>
<div className="payload-output" style={{ marginTop: 14, position: 'relative', paddingRight: 42 }}>
{payloadResult}
<button className="copy-btn" onClick={handleCopyPayload} title="Copy to clipboard">
{copied ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M3 11V3h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
)}
</button>
</div>
)}
</div>
)}
@@ -0,0 +1,49 @@
package com.qi4l.JYso.web;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ApiAuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String path = req.getRequestURI();
if (!path.startsWith("/api/") || path.equals("/api/auth/login")) {
chain.doFilter(request, response);
return;
}
String authHeader = req.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
resp.setStatus(401);
resp.setContentType("application/json");
resp.getWriter().write("{\"error\":\"Unauthorized\"}");
return;
}
String token = authHeader.substring(7);
if (!AuthServlet.validateToken(token)) {
resp.setStatus(401);
resp.setContentType("application/json");
resp.getWriter().write("{\"error\":\"Invalid token\"}");
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
@@ -0,0 +1,57 @@
package com.qi4l.JYso.web;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.qi4l.JYso.web.config.JYsoWebPasswordProvider;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class AuthServlet extends HttpServlet {
static final ConcurrentHashMap<String, String> tokens = new ConcurrentHashMap<>();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
StringBuilder sb = new StringBuilder();
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 password = body.getString("password");
if (!"qi".equals(username) || !JYsoWebPasswordProvider.getPassword().equals(password)) {
Map<String, String> error = new HashMap<>();
error.put("error", "Invalid credentials");
resp.setStatus(401);
resp.getWriter().write(JSON.toJSONString(error));
return;
}
String token = UUID.randomUUID().toString();
tokens.put(token, username);
Map<String, String> result = new HashMap<>();
result.put("token", token);
result.put("username", username);
resp.getWriter().write(JSON.toJSONString(result));
}
static boolean validateToken(String token) {
return token != null && tokens.containsKey(token);
}
}
@@ -33,6 +33,8 @@ public class JYsoWebApplication {
public static void start(String[] args) {
logo();
RequestLogCollector.install();
webPassword = WebPasswordGenerator.generatePassword();
JYsoWebPasswordProvider.setPassword(webPassword);
@@ -47,7 +49,7 @@ public class JYsoWebApplication {
}
}
System.out.println(ansi().render("@|green [+]|@ Web GUI starting >> http://127.0.0.1:" + webPort));
System.out.println(ansi().render("@|green [+]|@ Web GUI started on http://127.0.0.1:" + webPort));
System.out.println(ansi().render("@|yellow [+]|@ ============================================"));
System.out.println(ansi().render("@|yellow [+]|@ Username: qi"));
System.out.println(ansi().render("@|yellow [+]|@ Password: " + webPassword));
@@ -74,8 +76,6 @@ public class JYsoWebApplication {
context.addServlet(new ServletHolder(new StaticResourceServlet()), "/");
server.start();
System.out.println(ansi().render("@|green [+]|@ Web GUI started on http://127.0.0.1:" + webPort));
CountDownLatch latch = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down...");
@@ -0,0 +1,368 @@
package com.qi4l.JYso.web;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.qi4l.JYso.HTTPServer;
import com.qi4l.JYso.LdapServer;
import com.qi4l.JYso.LdapsServer;
import com.qi4l.JYso.RMIServer;
import com.qi4l.JYso.Starter;
import com.qi4l.JYso.gadgets.Config.Config;
import com.qi4l.JYso.gadgets.Config.ysoserial;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class JettyApiServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
String path = req.getRequestURI();
if (path.startsWith("/api/")) {
path = path.substring(4);
}
try {
if ("/status".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) {
handleStatus(resp);
} else if ("/servers/start".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) {
handleServersStart(req, resp);
} else if ("/servers/stop".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) {
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 {
resp.setStatus(404);
resp.getWriter().write("{\"error\":\"Not found\"}");
}
} catch (Exception e) {
resp.setStatus(500);
resp.getWriter().write("{\"success\":false,\"error\":\"" + escapeJson(e.getMessage()) + "\"}");
}
}
private void handleStatus(HttpServletResponse resp) throws IOException {
Map<String, Object> status = new LinkedHashMap<>();
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 {
int count = 100;
String countParam = req.getParameter("count");
if (countParam != null) {
try { count = Integer.parseInt(countParam); } catch (NumberFormatException ignored) {}
}
List<String> logLines = RequestLogCollector.getLines(count);
Map<String, Object> result = new LinkedHashMap<>();
result.put("logs", logLines);
resp.getWriter().write(JSON.toJSONString(result));
}
private void handleServersStart(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req);
Map<String, Object> result = new LinkedHashMap<>();
List<String> started = new ArrayList<>();
List<String> errors = new ArrayList<>();
boolean startLdap = json.getBooleanValue("ldap");
boolean startLdaps = json.getBooleanValue("ldaps");
boolean startHttp = json.getBooleanValue("http");
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) {
new Thread(() -> { try { LdapServer.start(); } catch (Exception ignored) {} }, "ldap-starter").start();
started.add("LDAP");
}
if (startHttp && !HTTPServer.isRunning) {
new Thread(() -> { try { HTTPServer.start(); } catch (Exception ignored) {} }, "http-starter").start();
started.add("HTTP");
}
if (startLdaps && !LdapsServer.isRunning) {
new Thread(() -> { try { LdapsServer.start(); } catch (Exception ignored) {} }, "ldaps-starter").start();
started.add("LDAPS");
}
if (startRmi && !RMIServer.isRunning) {
new Thread(() -> { try { RMIServer.start(); } catch (Exception ignored) {} }, "rmi-starter").start();
started.add("RMI");
}
result.put("started", started);
result.put("errors", errors);
result.put("success", true);
resp.getWriter().write(JSON.toJSONString(result));
}
private void handleServersStop(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req);
Map<String, Object> result = new LinkedHashMap<>();
String server = json.getString("server");
if (server == null) {
result.put("success", false);
result.put("error", "server name required");
resp.getWriter().write(JSON.toJSONString(result));
return;
}
switch (server.toLowerCase()) {
case "ldap": LdapServer.stop(); break;
case "ldaps": LdapsServer.stop(); break;
case "http": HTTPServer.stop(); break;
case "rmi": RMIServer.stop(); break;
default:
result.put("success", false);
result.put("error", "unknown server: " + server);
resp.getWriter().write(JSON.toJSONString(result));
return;
}
result.put("success", true);
result.put("server", server);
result.put("status", buildStatusMap());
resp.getWriter().write(JSON.toJSONString(result));
}
private void handleServersToggle(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req);
Map<String, Object> result = new LinkedHashMap<>();
String server = json.getString("server");
if (server == null) {
result.put("success", false);
result.put("error", "server name required");
resp.getWriter().write(JSON.toJSONString(result));
return;
}
boolean nowRunning;
switch (server.toLowerCase()) {
case "ldap":
if (LdapServer.isRunning) LdapServer.stop();
else new Thread(() -> { try { LdapServer.start(); } catch (Exception ignored) {} }, "ldap-toggler").start();
nowRunning = !LdapServer.isRunning;
break;
case "ldaps":
if (LdapsServer.isRunning) LdapsServer.stop();
else new Thread(() -> { try { LdapsServer.start(); } catch (Exception ignored) {} }, "ldaps-toggler").start();
nowRunning = !LdapsServer.isRunning;
break;
case "http":
if (HTTPServer.isRunning) HTTPServer.stop();
else new Thread(() -> { try { HTTPServer.start(); } catch (Exception ignored) {} }, "http-toggler").start();
nowRunning = !HTTPServer.isRunning;
break;
case "rmi":
if (RMIServer.isRunning) RMIServer.stop();
else new Thread(() -> { try { RMIServer.start(); } catch (Exception ignored) {} }, "rmi-toggler").start();
nowRunning = !RMIServer.isRunning;
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(); }
result.put("success", true);
result.put("server", server);
result.put("running", nowRunning);
result.put("status", buildStatusMap());
resp.getWriter().write(JSON.toJSONString(result));
}
private void handleGadgets(HttpServletResponse resp) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (String name : Starter.caseInsensitiveObjectPayloadMap.keySet()) {
Map<String, Object> item = new LinkedHashMap<>();
String shortName = name.substring(name.lastIndexOf('.') + 1);
item.put("name", shortName);
item.put("fullName", name);
list.add(item);
}
resp.getWriter().write(JSON.toJSONString(list));
}
private void handlePayloadGenerate(HttpServletRequest req, HttpServletResponse resp) throws Exception {
JSONObject json = readJson(req);
Map<String, Object> result = new LinkedHashMap<>();
String gadget = json.getString("gadget");
String command = json.getString("command");
String saveFilename = json.getString("filename");
boolean encodeBase64 = json.getBooleanValue("encodeBase64");
if (gadget == null || command == null) {
result.put("success", false);
result.put("error", "gadget and command are required");
resp.getWriter().write(JSON.toJSONString(result));
return;
}
boolean keepFile = (saveFilename != null && !saveFilename.trim().isEmpty());
String filename = keepFile ? saveFilename.trim() : "1.ser";
java.util.List<String> argList = new java.util.ArrayList<>();
argList.add("-y");
argList.add("-g");
argList.add(gadget);
argList.add("-p");
argList.add(command);
argList.add("-f");
argList.add(filename);
if (json.getBooleanValue("inherit")) argList.add("-i");
if (json.getBooleanValue("obscure")) argList.add("-o");
if (json.getBooleanValue("noComSun")) argList.add("-ncs");
if (json.getBooleanValue("mozillaClassLoader")) argList.add("-mcl");
if (json.getBooleanValue("rhino")) argList.add("-rh");
if (json.getBooleanValue("utf8Overlong")) argList.add("-utf");
String dcfpVal = json.getString("dcfp");
if (dcfpVal != null && !dcfpVal.trim().isEmpty()) {
argList.add("-dcfp");
argList.add(dcfpVal.trim());
}
String dirtyTypeVal = json.getString("dirtyType");
String dirtyLengthVal = json.getString("dirtyLength");
if (dirtyTypeVal != null && !dirtyTypeVal.trim().isEmpty()
&& dirtyLengthVal != null && !dirtyLengthVal.trim().isEmpty()) {
argList.add("-dt");
argList.add(dirtyTypeVal.trim());
argList.add("-dl");
argList.add(dirtyLengthVal.trim());
}
ysoserial.run(argList.toArray(new String[0]));
Path filePath = Paths.get(filename);
byte[] data = Files.readAllBytes(filePath);
result.put("success", true);
if (encodeBase64) {
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) {
Files.deleteIfExists(filePath);
}
if (keepFile) {
result.put("saved", filename);
}
resp.getWriter().write(JSON.toJSONString(result));
}
private void handleConfigUpdate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req);
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("config", buildStatusMap());
resp.getWriter().write(JSON.toJSONString(result));
}
private Map<String, Object> buildStatusMap() {
Map<String, Object> status = new LinkedHashMap<>();
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");
return status;
}
private 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);
}
private String escapeJson(String s) {
if (s == null) return "null";
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
@@ -0,0 +1,122 @@
package com.qi4l.JYso.web;
import java.io.OutputStream;
import java.io.PrintStream;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RequestLogCollector {
private static final int MAX_LINES = 200;
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final List<String> lines = new ArrayList<>();
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static PrintStream originalOut;
private static boolean installed = false;
public static synchronized void install() {
if (installed) return;
installed = true;
originalOut = System.out;
System.setOut(new PrintStream(new TeeOutputStream(originalOut), true));
}
private static void addLine(String rawLine) {
String clean = rawLine
.replaceAll("\u001B\\[[;\\d]*m", "")
.replaceAll("\u001B\\[[;\\d]*[@-~]", "")
.trim();
if (clean.isEmpty()) return;
String timestamp = LocalTime.now().format(TIME_FMT);
String entry = "[" + timestamp + "] " + clean;
lock.writeLock().lock();
try {
lines.add(entry);
while (lines.size() > MAX_LINES) {
lines.remove(0);
}
} finally {
lock.writeLock().unlock();
}
}
public static List<String> getLines(int count) {
lock.readLock().lock();
try {
int size = lines.size();
if (size <= count || count <= 0) {
return new ArrayList<>(lines);
}
return new ArrayList<>(lines.subList(size - count, size));
} finally {
lock.readLock().unlock();
}
}
public static List<String> getLines() {
return getLines(0);
}
private static class TeeOutputStream extends OutputStream {
private final OutputStream target;
TeeOutputStream(OutputStream target) {
this.target = target;
}
private final StringBuilder lineBuffer = new StringBuilder();
@Override
public void write(int b) {
try {
target.write(b);
} catch (Exception ignored) {
}
if (b == '\n' || b == '\r') {
if (lineBuffer.length() > 0) {
String line = lineBuffer.toString();
lineBuffer.setLength(0);
if (!line.isEmpty()) {
addLine(line);
}
}
} else {
lineBuffer.append((char) b);
}
}
@Override
public void write(byte[] buf, int off, int len) {
try {
target.write(buf, off, len);
} catch (Exception ignored) {
}
for (int i = off; i < off + len; i++) {
char c = (char) buf[i];
if (c == '\n' || c == '\r') {
if (lineBuffer.length() > 0) {
addLine(lineBuffer.toString());
lineBuffer.setLength(0);
}
} else {
lineBuffer.append(c);
}
}
}
@Override
public void flush() {
try {
target.flush();
} catch (Exception ignored) {
}
}
}
}
@@ -0,0 +1,66 @@
package com.qi4l.JYso.web;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
public class SpaFallbackFilter implements Filter {
private byte[] indexContent;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
try {
URL indexUrl = getClass().getClassLoader().getResource("static/index.html");
if (indexUrl != null) {
try (InputStream is = indexUrl.openStream()) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) bos.write(buf, 0, n);
indexContent = bos.toByteArray();
}
}
} catch (Exception ignored) {
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String path = req.getRequestURI();
if (path.startsWith("/api/")) {
chain.doFilter(request, response);
return;
}
String lower = path.toLowerCase();
boolean hasFileExt = lower.contains(".") && !lower.endsWith(".html");
if (!hasFileExt && indexContent != null) {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setStatus(200);
resp.setContentLength(indexContent.length);
OutputStream os = resp.getOutputStream();
os.write(indexContent);
os.flush();
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
@@ -0,0 +1,71 @@
package com.qi4l.JYso.web;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class StaticResourceServlet extends HttpServlet {
private static final Map<String, String> MIME_TYPES = new HashMap<>();
static {
MIME_TYPES.put("html", "text/html");
MIME_TYPES.put("htm", "text/html");
MIME_TYPES.put("css", "text/css");
MIME_TYPES.put("js", "application/javascript");
MIME_TYPES.put("mjs", "application/javascript");
MIME_TYPES.put("json", "application/json");
MIME_TYPES.put("png", "image/png");
MIME_TYPES.put("jpg", "image/jpeg");
MIME_TYPES.put("jpeg", "image/jpeg");
MIME_TYPES.put("gif", "image/gif");
MIME_TYPES.put("svg", "image/svg+xml");
MIME_TYPES.put("ico", "image/x-icon");
MIME_TYPES.put("woff", "font/woff");
MIME_TYPES.put("woff2", "font/woff2");
MIME_TYPES.put("ttf", "font/ttf");
MIME_TYPES.put("eot", "application/vnd.ms-fontobject");
MIME_TYPES.put("txt", "text/plain");
MIME_TYPES.put("xml", "application/xml");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String path = req.getRequestURI();
if (path.startsWith("/")) {
path = path.substring(1);
}
URL resource = getClass().getClassLoader().getResource(path);
if (resource == null) {
resource = getClass().getClassLoader().getResource("static/" + path);
}
if (resource == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
String ext = "";
int dot = path.lastIndexOf('.');
if (dot >= 0) {
ext = path.substring(dot + 1).toLowerCase();
}
String mime = MIME_TYPES.getOrDefault(ext, "application/octet-stream");
resp.setContentType(mime);
try (InputStream is = resource.openStream();
OutputStream os = resp.getOutputStream()) {
byte[] buf = new byte[8192];
int n;
while ((n = is.read(buf)) != -1) {
os.write(buf, 0, n);
}
}
}
}