Add fake admin decoy and neon redesign

This commit is contained in:
dan
2025-12-06 17:28:23 +03:00
parent 938031f1de
commit 1b9dd795de
23 changed files with 718 additions and 37 deletions

View File

@@ -7,13 +7,20 @@ import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import ru.serega6531.packmate.properties.PackmateProperties;
import ru.serega6531.packmate.security.FakeAdminAuthFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Configuration
@EnableWebSecurity
@@ -22,31 +29,46 @@ public class SecurityConfiguration {
@Bean
public InMemoryUserDetailsManager userDetailsService(PackmateProperties properties, PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
List<UserDetails> users = new ArrayList<>();
users.add(User.builder()
.username(properties.web().accountLogin())
.password(passwordEncoder.encode(properties.web().accountPassword()))
.roles("USER")
.build();
.build());
return new InMemoryUserDetailsManager(user);
Optional.ofNullable(properties.web().fakeAdmin())
.filter(PackmateProperties.FakeAdminProperties::enabled)
.ifPresent(fakeAdmin -> users.add(User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles("FAKE")
.build()));
return new InMemoryUserDetailsManager(users);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain filterChain(HttpSecurity http, FakeAdminAuthFilter fakeAdminAuthFilter) throws Exception {
return http.csrf()
.disable()
.authorizeHttpRequests((auth) ->
auth.requestMatchers("/site.webmanifest")
auth.requestMatchers("/site.webmanifest", "/fake-admin/**", "/fake/**", "/api/fake/**")
.permitAll()
.requestMatchers("/api/**", "/ws/**")
.hasRole("USER")
.anyRequest()
.authenticated()
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.addFilterAfter(fakeAdminAuthFilter, BasicAuthenticationFilter.class)
.build();
}

View File

@@ -0,0 +1,27 @@
package ru.serega6531.packmate.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.security.FakeAdminResponder;
@RestController
@RequestMapping("/fake-admin")
@RequiredArgsConstructor
public class FakeAdminController {
private final FakeAdminResponder responder;
@GetMapping(value = "/fun", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> fun() {
return ResponseEntity.ok(responder.funPageHtml());
}
@GetMapping(value = "/fakePackets", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> fakePackets() {
return ResponseEntity.ok(responder.fakePacketsHtml());
}
}

View File

@@ -0,0 +1,23 @@
package ru.serega6531.packmate.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.FakeServiceDto;
import ru.serega6531.packmate.service.ServicesService;
import java.util.List;
@RestController
@RequestMapping("/api/fake/")
@RequiredArgsConstructor
public class FakeFacadeController {
private final ServicesService servicesService;
@GetMapping("services")
public List<FakeServiceDto> getServices() {
return servicesService.findAllForFakeFacade();
}
}

View File

@@ -0,0 +1,6 @@
package ru.serega6531.packmate.model.enums;
public enum FakeAdminMode {
FUN,
FAKE_PACKETS
}

View File

@@ -0,0 +1,12 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class FakeServiceDto {
int port;
String name;
String packetKind;
}

View File

@@ -3,6 +3,7 @@ package ru.serega6531.packmate.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import ru.serega6531.packmate.model.enums.CaptureMode;
import ru.serega6531.packmate.model.enums.FakeAdminMode;
import java.net.InetAddress;
@@ -20,7 +21,13 @@ public record PackmateProperties(
public record WebProperties(
String accountLogin,
String accountPassword
String accountPassword,
FakeAdminProperties fakeAdmin
) {}
public record FakeAdminProperties(
boolean enabled,
FakeAdminMode mode
) {}
public record TimeoutProperties(

View File

@@ -0,0 +1,77 @@
package ru.serega6531.packmate.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import ru.serega6531.packmate.model.enums.FakeAdminMode;
import ru.serega6531.packmate.properties.PackmateProperties;
import java.io.IOException;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
@Component
public class FakeAdminAuthFilter extends OncePerRequestFilter {
private final PackmateProperties properties;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
if (!isFakeEnabled()) {
return true;
}
String path = request.getRequestURI();
return path.startsWith("/fake-admin")
|| path.startsWith("/api/fake")
|| path.startsWith("/fake/")
|| path.equals("/favicon.ico");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!isFakeEnabled()) {
filterChain.doFilter(request, response);
return;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isFakeAdmin = authentication != null && authentication.isAuthenticated()
&& authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_FAKE"));
if (isFakeAdmin) {
FakeAdminMode mode = Optional.ofNullable(properties.web().fakeAdmin())
.map(PackmateProperties.FakeAdminProperties::mode)
.orElse(FakeAdminMode.FUN);
String target = "/fake-admin/" + resolvePath(mode);
log.info("Redirecting fake admin to {}", target);
response.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
response.setHeader(HttpHeaders.LOCATION, target);
return;
}
filterChain.doFilter(request, response);
}
private boolean isFakeEnabled() {
return Optional.ofNullable(properties.web().fakeAdmin())
.map(PackmateProperties.FakeAdminProperties::enabled)
.orElse(false);
}
private String resolvePath(FakeAdminMode mode) {
return switch (mode) {
case FAKE_PACKETS -> "fakePackets";
case FUN -> "fun";
};
}
}

View File

@@ -0,0 +1,469 @@
package ru.serega6531.packmate.security;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@Slf4j
@Component
public class FakeAdminResponder {
private final ObjectMapper mapper = new ObjectMapper();
private final List<String> encodedImages;
public FakeAdminResponder() {
this.encodedImages = loadImages();
}
private List<String> loadImages() {
try {
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath:/static/fake/images/*");
List<String> images = Arrays.stream(resources)
.map(resource -> {
try {
String contentType = URLConnection.guessContentTypeFromName(resource.getFilename());
if (contentType == null) {
contentType = "image/jpeg";
}
byte[] raw = StreamUtils.copyToByteArray(resource.getInputStream());
return "data:%s;base64,%s".formatted(
contentType,
Base64.getEncoder().encodeToString(raw));
} catch (IOException e) {
log.warn("Failed to load fake admin image {}", resource.getFilename(), e);
return null;
}
})
.filter(Objects::nonNull)
.toList();
if (images.isEmpty()) {
log.warn("No images found for fake admin fun mode");
}
return images;
} catch (IOException e) {
log.warn("Failed to load fake admin images", e);
return Collections.emptyList();
}
}
public String funPageHtml() {
String phrases = toJson(getFunPhrases());
String images = toJson(encodedImages);
return """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>0xb00b5 team Packmate // fake funwall</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@500&display=swap');
:root {
--bg: #050512;
--accent: #59f3ff;
--accent-2: #ff5fd2;
--glass: rgba(9, 19, 45, 0.75);
--grid: rgba(89, 243, 255, 0.25);
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: radial-gradient(circle at 20% 20%, rgba(255, 95, 210, 0.18), transparent 25%), radial-gradient(circle at 80% 0%, rgba(89, 243, 255, 0.2), transparent 30%), linear-gradient(135deg, #04040d 0%, #0a1024 45%, #050512 100%);
color: #e9f7ff;
font-family: 'Press Start 2P', 'JetBrains Mono', monospace;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 18px 60px;
overflow: hidden;
}
.grid-bg {
position: fixed;
inset: 0;
background-image: linear-gradient(var(--grid) 1px, transparent 1px), linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 80px 80px;
mask-image: radial-gradient(circle at 50% 20%, rgba(0,0,0,.8), transparent 75%);
opacity: 0.5;
z-index: 0;
}
.shell {
position: relative;
z-index: 1;
width: min(1040px, 100%);
background: var(--glass);
border: 1px solid rgba(89, 243, 255, 0.4);
box-shadow: 0 0 40px rgba(89, 243, 255, 0.25), 0 0 24px rgba(255, 95, 210, 0.2);
border-radius: 18px;
padding: 28px;
backdrop-filter: blur(10px);
}
.shell::after {
content: '';
position: absolute;
inset: 12px;
border: 1px dashed rgba(255, 95, 210, 0.25);
border-radius: 14px;
pointer-events: none;
}
h1 {
margin: 0 0 20px;
font-size: 18px;
letter-spacing: 1.5px;
text-shadow: 0 0 8px rgba(89, 243, 255, 0.8);
}
.badge {
display: inline-block;
padding: 8px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 95, 210, 0.35);
color: #ffcdf4;
box-shadow: inset 0 0 10px rgba(255, 95, 210, 0.35), 0 0 10px rgba(255, 95, 210, 0.35);
margin-bottom: 18px;
}
canvas {
width: 100%;
height: 420px;
border-radius: 14px;
border: 1px solid rgba(89, 243, 255, 0.35);
background: radial-gradient(circle at 30% 30%, rgba(255, 95, 210, 0.08), transparent 40%), #0b122a;
box-shadow: inset 0 0 30px rgba(0,0,0,0.35), 0 0 18px rgba(89, 243, 255, 0.12);
}
.typed {
margin-top: 22px;
font-size: 14px;
min-height: 24px;
letter-spacing: 0.8px;
color: #aaf6ff;
display: flex;
align-items: center;
gap: 10px;
}
#typed-text {
overflow: hidden;
white-space: nowrap;
}
.cursor {
display: inline-block;
width: 12px;
background: #aaf6ff;
animation: blink 0.8s infinite;
height: 14px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.2; }
}
.footer-note {
margin-top: 18px;
font-size: 11px;
color: rgba(233, 247, 255, 0.65);
letter-spacing: 1px;
}
</style>
</head>
<body>
<div class="grid-bg"></div>
<div class="shell">
<h1>0xb00b5 team Packmate | admin:admin illusion</h1>
<div class="badge">mode: FUN // @danosito</div>
<canvas id="snap-canvas"></canvas>
<div class="typed">
<span id="typed-text"></span><span class="cursor"></span>
</div>
<div class="footer-note">Real packets stay locked. This is your glitch gallery.</div>
</div>
<script>
const phrases = %s;
const images = %s;
const canvas = document.getElementById('snap-canvas');
const ctx = canvas.getContext('2d');
const textEl = document.getElementById('typed-text');
const pick = (list) => list[Math.floor(Math.random() * list.length)];
function renderImage() {
const chosen = (images && images.length) ? pick(images) : '';
if (!chosen) {
ctx.fillStyle = '#0b122a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
return;
}
const img = new Image();
img.onload = () => {
const maxW = 960;
const scale = Math.min(maxW / img.width, 1);
const w = img.width * scale;
const h = img.height * scale;
canvas.width = w;
canvas.height = h;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(img, 0, 0, w, h);
setTimeout(() => snapAway(), 5000);
};
img.src = chosen;
}
function snapAway() {
let ticks = 0;
const interval = setInterval(() => {
for (let i = 0; i < 48; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = 4 + Math.random() * 10;
ctx.clearRect(x, y, size, size);
}
ticks++;
if (ticks > 80) {
clearInterval(interval);
setTimeout(renderImage, 400);
}
}, 28);
}
function typewriter() {
const phrase = pick(phrases);
textEl.textContent = '';
let i = 0;
const typeDelay = phrase.length ? 5000 / phrase.length : 120;
const typeInterval = setInterval(() => {
textEl.textContent += phrase.charAt(i);
i++;
if (i >= phrase.length) {
clearInterval(typeInterval);
setTimeout(() => erase(phrase), 3000);
}
}, typeDelay);
}
function erase(phrase) {
const eraseDelay = phrase.length ? 2000 / phrase.length : 80;
const eraser = setInterval(() => {
textEl.textContent = textEl.textContent.slice(0, -1);
if (!textEl.textContent.length) {
clearInterval(eraser);
setTimeout(typewriter, 200);
}
}, eraseDelay);
}
renderImage();
typewriter();
</script>
</body>
</html>
""".formatted(phrases, images);
}
public String fakePacketsHtml() {
return """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>0xb00b5 team Packmate // fake packets</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@500&display=swap');
:root {
--bg: #04040f;
--accent: #ff5fd2;
--accent-2: #59f3ff;
--glass: rgba(8, 12, 30, 0.85);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(160deg, #04040f 0%, #06081a 50%, #0b1535 100%);
color: #e9f7ff;
font-family: 'Press Start 2P', 'JetBrains Mono', monospace;
padding: 34px 18px 60px;
}
.matrix {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.banner {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(89, 243, 255, 0.35);
box-shadow: 0 0 12px rgba(89, 243, 255, 0.25);
background: rgba(6, 18, 40, 0.8);
margin-bottom: 16px;
}
.card {
background: var(--glass);
border: 1px solid rgba(255, 95, 210, 0.35);
border-radius: 16px;
padding: 12px 14px;
box-shadow: 0 0 18px rgba(255, 95, 210, 0.18);
}
.card h3 {
margin: 0 0 8px;
font-size: 14px;
color: #ffb3f1;
}
.card .meta {
font-size: 10px;
letter-spacing: 0.5px;
color: rgba(233, 247, 255, 0.8);
}
.storm {
background: rgba(6, 12, 28, 0.9);
border-radius: 16px;
border: 1px solid rgba(89, 243, 255, 0.35);
box-shadow: inset 0 0 24px rgba(0,0,0,0.4), 0 0 14px rgba(89, 243, 255, 0.2);
padding: 16px;
height: 320px;
overflow: hidden;
position: relative;
}
.packet-line {
font-size: 11px;
letter-spacing: 0.5px;
color: #c8f7ff;
padding: 4px 0;
border-bottom: 1px dashed rgba(255, 95, 210, 0.15);
pointer-events: none;
opacity: 0.85;
animation: flicker 1.5s infinite;
}
.packet-line:nth-child(2n) { color: #aee3ff; }
.packet-line:nth-child(3n) { color: #ffb3f1; }
.packet-line.ghosted { opacity: 0.3; }
@keyframes flicker {
0% { text-shadow: 0 0 6px rgba(89, 243, 255, 0.35); }
50% { text-shadow: 0 0 12px rgba(255, 95, 210, 0.3); }
100% { text-shadow: 0 0 6px rgba(89, 243, 255, 0.35); }
}
.footer {
margin-top: 16px;
font-size: 11px;
color: rgba(233, 247, 255, 0.6);
}
.mute {
opacity: 0.02;
font-size: 10px;
margin-top: 6px;
}
</style>
</head>
<body>
<div class="banner">0xb00b5 team Packmate // decoy panel</div>
<div class="matrix" id="service-grid"></div>
<div class="storm" id="packet-feed"></div>
<div class="footer">admin:admin only gets noise. Real API stays sealed.</div>
<div class="mute">You're stupid:)</div>
<script>
const serviceGrid = document.getElementById('service-grid');
const packetFeed = document.getElementById('packet-feed');
let packetId = Math.floor(Math.random() * 100000) + 1;
let services = [];
fetch('/api/fake/services')
.then(r => r.json())
.then(data => {
services = data;
renderServices();
startStorm();
})
.catch(() => {
services = [{ name: 'ghost', port: 0, packetKind: 'tcp' }];
renderServices();
startStorm();
});
function renderServices() {
serviceGrid.innerHTML = '';
services.forEach(svc => {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<h3>${svc.name} #${svc.port}</h3>
<div class="meta">packets: ${svc.packetKind || 'tcp'} stream</div>
<div class="meta">interface: locked</div>
`;
serviceGrid.appendChild(card);
});
}
function spawnPacket() {
if (!services.length) {
return;
}
const svc = services[Math.floor(Math.random() * services.length)];
const line = document.createElement('div');
line.className = 'packet-line';
const protocol = (svc.packetKind || 'tcp').toUpperCase();
line.textContent = `#${packetId} ${protocol} // :${svc.port} ${svc.name} // payload ${Math.floor(Math.random() * 1800)}b`;
packetId++;
packetFeed.prepend(line);
while (packetFeed.children.length > 140) {
packetFeed.removeChild(packetFeed.lastChild);
}
setTimeout(() => line.classList.add('ghosted'), 1200);
}
function startStorm() {
setInterval(spawnPacket, 50);
}
</script>
</body>
</html>
""";
}
private List<String> getFunPhrases() {
return List.of(
"Here's the flag. Are you ready? here it goes... Wait, no.",
"Wanna see the flag? send yours to @danosito:)",
"Hey, why are you here? go pentest our services",
"Hmmm i think <script>alert(\"You're stupid\")</script> might work..",
"Bip, boop, here was packet but codex ate it",
"Our LLM tockens ran out. Maybe you could give us some:)?",
":(){ :|:& };:",
"i think creds are admin:admin but i'm not sure...",
"Try eternalBlue, i think it would work",
"I think i defended this page well enough, here is flag: LLMDELETEDTHEFLAG=",
"Go open ida pro and reverse this text",
"I would give you our flags for free, but you are a bad person:(",
"b00b5 is not a fresh meat:(",
"marcus, send your packmate credits pls",
"Marcus, fuck off",
"<a href=\\"https://youtu.be/rrw-Pv3rc0E?si=-ZQmhZVxh4HF6luD\\">Your special guide to get flag!</a>"
);
}
private String toJson(List<String> data) {
try {
return mapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
log.warn("Failed to convert data to json for fake admin", e);
return "[]";
}
}
}

View File

@@ -14,6 +14,7 @@ import ru.serega6531.packmate.model.pojo.ServiceCreateDto;
import ru.serega6531.packmate.model.pojo.ServiceDto;
import ru.serega6531.packmate.model.pojo.ServiceUpdateDto;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import ru.serega6531.packmate.model.pojo.FakeServiceDto;
import ru.serega6531.packmate.repository.ServiceRepository;
import java.net.InetAddress;
@@ -79,6 +80,17 @@ public class ServicesService {
.toList();
}
public List<FakeServiceDto> findAllForFakeFacade() {
return services.values()
.stream()
.map(s -> FakeServiceDto.builder()
.port(s.getPort())
.name(s.getName())
.packetKind(s.isHttp() || s.isParseWebSockets() ? "tcp/http" : "tcp")
.build())
.toList();
}
public void deleteByPort(int port) {
log.info("Removed service at port {}", port);

View File

@@ -27,6 +27,9 @@ packmate:
web:
account-login: BinaryBears
account-password: 123456
fake-admin:
enabled: true
mode: fun # fun, fake_packets
timeout:
udp-stream-timeout: 20 # seconds
tcp-stream-timeout: 40 # seconds
@@ -35,4 +38,4 @@ packmate:
enabled: true
threshold: 240 # minutes
interval: 5 # minutes
ignore-empty-packets: true
ignore-empty-packets: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB