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

3
.gitignore vendored
View File

@@ -1,4 +1,7 @@
references_for_redisign
src/main/resources/static/*
!src/main/resources/static/fake
!src/main/resources/static/fake/**
*.pcap
data

View File

@@ -1,10 +1,10 @@
<div align="center">
# Packmate
# 0xb00b5 team Packmate
</div>
### [[EN](README_EN.md) | RU]
Утилита перехвата и анализа трафика для CTF.
Утилита перехвата и анализа трафика для CTF, переосмысленная в пиксельном неоне.
#### Фичи:
* Поддерживает перехват живого трафика и обработку pcap файлов
@@ -23,14 +23,18 @@
* Разархивирует GZIP в HTTP на лету
* Разархивирует сжатые WebSockets
* Расшифровывает TLS на RSA при наличии приватного ключа
* Обманка для входа `admin:admin` с режимами `fun` и `fake_packets`, чтобы любопытные так и не добрались до настоящих пакетов
![Скриншот главного окна](screenshots/Screenshot.png)
### Обманка admin:admin
Для входа с кредами `admin:admin` добавлена обманка (включена по умолчанию). Управляется через переменные `PACKMATE_FAKE_ADMIN_ENABLED` и `PACKMATE_FAKE_ADMIN_MODE` (`fun` или `fake_packets`) и не дает добраться до настоящего интерфейса.
## Быстрый запуск
Для быстрого запуска Packmate следует использовать [этот стартер](https://gitlab.com/packmate/starter/-/blob/master/README.md).
Для быстрого запуска 0xb00b5 team Packmate следует использовать [этот стартер](https://gitlab.com/packmate/starter/-/blob/master/README.md).
## Полный запуск
Ниже следует инструкция для тех, кто хочет собрать Packmate самостоятельно.
Ниже следует инструкция для тех, кто хочет собрать 0xb00b5 team Packmate самостоятельно.
### Клонирование
Поскольку этот репозиторий содержит фронтенд как git submodule, его необходимо клонировать так:
@@ -56,7 +60,7 @@ git submodule update --init --recursive
sudo docker compose up --build -d
```
При успешном запуске Packmate будет видно с любого хоста на порту `65000`.
При успешном запуске 0xb00b5 team Packmate будет видно с любого хоста на порту `65000`.
БД будет слушать на порту 65001, но будет разрешать подключения только с localhost.
## Использование
@@ -64,5 +68,5 @@ sudo docker compose up --build -d
<div align="right">
*desu~*
*@danosito*
</div>

View File

@@ -1,10 +1,10 @@
<div align="center">
# Packmate
# 0xb00b5 team Packmate
</div>
### [EN | [RU](README.md)]
Advanced network traffic flow analyzer for A/D CTFs.
Advanced network traffic flow analyzer for A/D CTFs with a pixel-neon twist.
#### Features:
* Can monitor live traffic or analyze pcap files
@@ -23,14 +23,18 @@ Advanced network traffic flow analyzer for A/D CTFs.
* Can automatically decompress GZIPed HTTP
* Can automatically deflate WebSockets with permessages-deflate extension
* Can automatically decrypt TLS with RSA using given private key (like Wireshark)
* Decoy login for `admin:admin` with `fun` and `fake_packets` modes so snoopers never see the real data
![Main window](screenshots/Screenshot.png)
### Admin:admin decoy
The admin:admin credentials now trigger a decoy (enabled by default). Configure it via `PACKMATE_FAKE_ADMIN_ENABLED` and `PACKMATE_FAKE_ADMIN_MODE` (`fun` or `fake_packets`) to keep everyone away from the real interface.
## Quick Start
To quickly start using Packmate, use [this starter](https://gitlab.com/packmate/starter/-/blob/master/README_EN.md).
To quickly start using 0xb00b5 team Packmate, use [this starter](https://gitlab.com/packmate/starter/-/blob/master/README_EN.md).
## Full Build
Below are the instructions for those who want to build Packmate on their own.
Below are the instructions for those who want to build 0xb00b5 team Packmate on their own.
### Cloning
As this repository contains frontend part as a git submodule, it has to be cloned like this:
@@ -56,7 +60,7 @@ After filling in env file you can launch the app:
sudo docker-compose up --build -d
```
If everything went fine, Packmate will be available on port `65000` from any host.
If everything went fine, 0xb00b5 team Packmate will be available on port `65000` from any host.
Database with listen on port 65001, but will only accept connections from localhost.
## Usage

View File

@@ -8,6 +8,8 @@ services:
PCAP_FILE: ${PACKMATE_PCAP_FILE:-}
WEB_LOGIN: ${PACKMATE_WEB_LOGIN:-BinaryBears}
WEB_PASSWORD: ${PACKMATE_WEB_PASSWORD:-123456}
FAKE_ADMIN_AUTH_ENABLED: ${PACKMATE_FAKE_ADMIN_ENABLED:-true}
FAKE_ADMIN_MODE: ${PACKMATE_FAKE_ADMIN_MODE:-fun}
OLD_STREAMS_CLEANUP_ENABLED: ${PACKMATE_OLD_STREAMS_CLEANUP_ENABLED:-false}
OLD_STREAMS_CLEANUP_INTERVAL: ${PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL:-5}
OLD_STREAMS_CLEANUP_THRESHOLD: ${PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD:-240}

View File

@@ -20,6 +20,8 @@ CMD [ "java", "-Djava.net.preferIPv4Stack=true", "-Djava.net.preferIPv4Addresses
"--packmate.capture-mode=${MODE}", "--packmate.pcap-file=${PCAP_FILE}", \
"--packmate.interface-name=${INTERFACE}", "--packmate.local-ip=${LOCAL_IP}", \
"--packmate.web.account-login=${WEB_LOGIN}", "--packmate.web.account-password=${WEB_PASSWORD}", \
"--packmate.web.fake-admin.enabled=${FAKE_ADMIN_AUTH_ENABLED}", \
"--packmate.web.fake-admin.mode=${FAKE_ADMIN_MODE}", \
"--packmate.cleanup.enabled=${OLD_STREAMS_CLEANUP_ENABLED}", \
"--packmate.cleanup.interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \
"--packmate.cleanup.threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \

View File

@@ -1,6 +1,6 @@
## Настройка
Packmate использует настройки из файла `.env` (в той же папке, что и `docker-compose.yml`)
0xb00b5 team Packmate использует настройки из файла `.env` (в той же папке, что и `docker-compose.yml`)
### Основные настройки
```dotenv
@@ -10,13 +10,17 @@ PACKMATE_LOCAL_IP=10.20.1.1
PACKMATE_WEB_LOGIN=SomeUser
# Пароль для web-авторизации
PACKMATE_WEB_PASSWORD=SomeSecurePassword
# Включает обманку при вводе admin:admin
PACKMATE_FAKE_ADMIN_ENABLED=true
# fun или fake_packets - варианты обманки
PACKMATE_FAKE_ADMIN_MODE=fun
```
### Режим работы
Packmate поддерживает три основных режима работы: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - это основной режим работы во время CTF. Packmate обрабатывает живой трафик и сразу выводит результаты.
2. `FILE` - обрабатывает трафик из pcap файлов. Полезен для анализа трафика с прошедших CTF, где не был запущен Packmate, или тех, где невозможно запустить его на вулнбоксе.
3. `VIEW` - Packmate не обрабатывает трафик, а только показывает уже обработанные стримы. Полезен для разборов после завершения CTF.
0xb00b5 team Packmate поддерживает три основных режима работы: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - это основной режим работы во время CTF. 0xb00b5 team Packmate обрабатывает живой трафик и сразу выводит результаты.
2. `FILE` - обрабатывает трафик из pcap файлов. Полезен для анализа трафика с прошедших CTF, где не был запущен 0xb00b5 team Packmate, или тех, где невозможно запустить его на вулнбоксе.
3. `VIEW` - 0xb00b5 team Packmate не обрабатывает трафик, а только показывает уже обработанные стримы. Полезен для разборов после завершения CTF.
<details>
<summary>Настройка LIVE</summary>
@@ -62,7 +66,7 @@ PACKMATE_MODE=VIEW
</details>
### Очистка БД
На крупных CTF через какое-то время накапливается большое количество трафика. Это замедляет работу Packmate и занимает много места на диске.
На крупных CTF через какое-то время накапливается большое количество трафика. Это замедляет работу 0xb00b5 team Packmate и занимает много места на диске.
Для оптимизации работы, рекомендуется включить регулярную очистку БД от старых стримов. Это будет работать только в режиме `LIVE`.
```dotenv
@@ -79,7 +83,7 @@ PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```dotenv
# Пароль от БД. Из-за того, что БД принимает подключения только с localhost, менять его необязательно, но можно, для дополнительной безопасности.
PACKMATE_DB_PASSWORD=K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb
# Версия Packmate. Можно изменить, если нужно использовать другой образ из docker registry.
# Версия 0xb00b5 team Packmate. Можно изменить, если нужно использовать другой образ из docker registry.
BUILD_TAG=latest
```

View File

@@ -1,6 +1,6 @@
## Setup
Packmate uses properties from the `.env` file (in the same directory as `docker-compose.yml`)
0xb00b5 team Packmate uses properties from the `.env` file (in the same directory as `docker-compose.yml`)
### Primary settings
```dotenv
@@ -10,13 +10,17 @@ PACKMATE_LOCAL_IP=10.20.1.1
PACKMATE_WEB_LOGIN=SomeUser
# Password for the web interface
PACKMATE_WEB_PASSWORD=SomeSecurePassword
# Enable decoy flow for admin:admin login
PACKMATE_FAKE_ADMIN_ENABLED=true
# fun or fake_packets - pick the decoy flavor
PACKMATE_FAKE_ADMIN_MODE=fun
```
### Modes of operation
Packmate supports 3 modes of operation: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - the usual mode during a CTF. Packmate processes live traffic and instantly displays the results.
2. `FILE` - processes traffic from pcap files. Useful to analyze traffic from past CTFs where Packmate wasn't launched, or CTFs where it's impossible to use it on the vulnbox.
3. `VIEW` - Packmate does not process any traffic, but simply shows already processed streams. Useful for post-game analyses.
0xb00b5 team Packmate supports 3 modes of operation: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - the usual mode during a CTF. 0xb00b5 team Packmate processes live traffic and instantly displays the results.
2. `FILE` - processes traffic from pcap files. Useful to analyze traffic from past CTFs where 0xb00b5 team Packmate wasn't launched, or CTFs where it's impossible to use it on the vulnbox.
3. `VIEW` - 0xb00b5 team Packmate does not process any traffic, but simply shows already processed streams. Useful for post-game analyses.
<details>
<summary>LIVE setup</summary>
@@ -62,7 +66,7 @@ PACKMATE_MODE=VIEW
</details>
### Database cleanup
On large CTFsб after some time a lot of traffic will pile up. This can slow Packmate down and take a lot of drive space.
On large CTFsб after some time a lot of traffic will pile up. This can slow 0xb00b5 team Packmate down and take a lot of drive space.
To optimize the workflow, it is recommended to enable periodical database cleanup of old streams. It will only work in the `LIVE` mode.
```dotenv
@@ -79,7 +83,7 @@ PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```dotenv
# Database password. Considering it only listens on localhost, it's not mandatory to change it, but you can do it for additional security.
PACKMATE_DB_PASSWORD=K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb
# Packmate version. Change it if you want to use a different version from the docker registry.
# 0xb00b5 team Packmate version. Change it if you want to use a different version from the docker registry.
BUILD_TAG=latest
```

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

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