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/*
!src/main/resources/static/fake
!src/main/resources/static/fake/**
*.pcap *.pcap
data data

View File

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

View File

@@ -1,10 +1,10 @@
<div align="center"> <div align="center">
# Packmate # 0xb00b5 team Packmate
</div> </div>
### [EN | [RU](README.md)] ### [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: #### Features:
* Can monitor live traffic or analyze pcap files * 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 decompress GZIPed HTTP
* Can automatically deflate WebSockets with permessages-deflate extension * Can automatically deflate WebSockets with permessages-deflate extension
* Can automatically decrypt TLS with RSA using given private key (like Wireshark) * 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) ![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 ## 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 ## 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 ### Cloning
As this repository contains frontend part as a git submodule, it has to be cloned like this: 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 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. Database with listen on port 65001, but will only accept connections from localhost.
## Usage ## Usage

View File

@@ -8,6 +8,8 @@ services:
PCAP_FILE: ${PACKMATE_PCAP_FILE:-} PCAP_FILE: ${PACKMATE_PCAP_FILE:-}
WEB_LOGIN: ${PACKMATE_WEB_LOGIN:-BinaryBears} WEB_LOGIN: ${PACKMATE_WEB_LOGIN:-BinaryBears}
WEB_PASSWORD: ${PACKMATE_WEB_PASSWORD:-123456} 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_ENABLED: ${PACKMATE_OLD_STREAMS_CLEANUP_ENABLED:-false}
OLD_STREAMS_CLEANUP_INTERVAL: ${PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL:-5} OLD_STREAMS_CLEANUP_INTERVAL: ${PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL:-5}
OLD_STREAMS_CLEANUP_THRESHOLD: ${PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD:-240} OLD_STREAMS_CLEANUP_THRESHOLD: ${PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD:-240}
@@ -43,4 +45,4 @@ services:
test: [ "CMD-SHELL", "pg_isready -U packmate -p 65001" ] test: [ "CMD-SHELL", "pg_isready -U packmate -p 65001" ]
interval: 2s interval: 2s
timeout: 5s timeout: 5s
retries: 15 retries: 15

View File

@@ -20,10 +20,12 @@ CMD [ "java", "-Djava.net.preferIPv4Stack=true", "-Djava.net.preferIPv4Addresses
"--packmate.capture-mode=${MODE}", "--packmate.pcap-file=${PCAP_FILE}", \ "--packmate.capture-mode=${MODE}", "--packmate.pcap-file=${PCAP_FILE}", \
"--packmate.interface-name=${INTERFACE}", "--packmate.local-ip=${LOCAL_IP}", \ "--packmate.interface-name=${INTERFACE}", "--packmate.local-ip=${LOCAL_IP}", \
"--packmate.web.account-login=${WEB_LOGIN}", "--packmate.web.account-password=${WEB_PASSWORD}", \ "--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.enabled=${OLD_STREAMS_CLEANUP_ENABLED}", \
"--packmate.cleanup.interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \ "--packmate.cleanup.interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \
"--packmate.cleanup.threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \ "--packmate.cleanup.threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \
"--server.port=65000", "--server.address=0.0.0.0" \ "--server.port=65000", "--server.address=0.0.0.0" \
] ]
EXPOSE 65000 EXPOSE 65000

View File

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

View File

@@ -1,6 +1,6 @@
## Setup ## 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 ### Primary settings
```dotenv ```dotenv
@@ -10,13 +10,17 @@ PACKMATE_LOCAL_IP=10.20.1.1
PACKMATE_WEB_LOGIN=SomeUser PACKMATE_WEB_LOGIN=SomeUser
# Password for the web interface # Password for the web interface
PACKMATE_WEB_PASSWORD=SomeSecurePassword 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 ### Modes of operation
Packmate supports 3 modes of operation: `LIVE`, `FILE` и `VIEW`. 0xb00b5 team 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. 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 Packmate wasn't launched, or CTFs where it's impossible to use it on the vulnbox. 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` - Packmate does not process any traffic, but simply shows already processed streams. Useful for post-game analyses. 3. `VIEW` - 0xb00b5 team Packmate does not process any traffic, but simply shows already processed streams. Useful for post-game analyses.
<details> <details>
<summary>LIVE setup</summary> <summary>LIVE setup</summary>
@@ -62,7 +66,7 @@ PACKMATE_MODE=VIEW
</details> </details>
### Database cleanup ### 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. To optimize the workflow, it is recommended to enable periodical database cleanup of old streams. It will only work in the `LIVE` mode.
```dotenv ```dotenv
@@ -79,10 +83,10 @@ PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```dotenv ```dotenv
# Database password. Considering it only listens on localhost, it's not mandatory to change it, but you can do it for additional security. # 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_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 BUILD_TAG=latest
``` ```
To use the TLS decryption, you have to put the matching private key in the `rsa_keys` directory. To use the TLS decryption, you have to put the matching private key in the `rsa_keys` directory.
Database files are being saved in `./data`, so to reset the database, you need to delete this directory. Database files are being saved in `./data`, so to reset the database, you need to delete this directory.

View File

@@ -7,13 +7,20 @@ import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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.User;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import ru.serega6531.packmate.properties.PackmateProperties; 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 @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -22,31 +29,46 @@ public class SecurityConfiguration {
@Bean @Bean
public InMemoryUserDetailsManager userDetailsService(PackmateProperties properties, PasswordEncoder passwordEncoder) { public InMemoryUserDetailsManager userDetailsService(PackmateProperties properties, PasswordEncoder passwordEncoder) {
UserDetails user = User.builder() List<UserDetails> users = new ArrayList<>();
users.add(User.builder()
.username(properties.web().accountLogin()) .username(properties.web().accountLogin())
.password(passwordEncoder.encode(properties.web().accountPassword())) .password(passwordEncoder.encode(properties.web().accountPassword()))
.roles("USER") .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 @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http, FakeAdminAuthFilter fakeAdminAuthFilter) throws Exception {
return http.csrf() return http.csrf()
.disable() .disable()
.authorizeHttpRequests((auth) -> .authorizeHttpRequests((auth) ->
auth.requestMatchers("/site.webmanifest") auth.requestMatchers("/site.webmanifest", "/fake-admin/**", "/fake/**", "/api/fake/**")
.permitAll() .permitAll()
.requestMatchers("/api/**", "/ws/**")
.hasRole("USER")
.anyRequest() .anyRequest()
.authenticated() .authenticated()
) )
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic() .httpBasic()
.and() .and()
.headers() .headers()
.frameOptions() .frameOptions()
.sameOrigin() .sameOrigin()
.and() .and()
.addFilterAfter(fakeAdminAuthFilter, BasicAuthenticationFilter.class)
.build(); .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 org.springframework.boot.context.properties.ConfigurationProperties;
import ru.serega6531.packmate.model.enums.CaptureMode; import ru.serega6531.packmate.model.enums.CaptureMode;
import ru.serega6531.packmate.model.enums.FakeAdminMode;
import java.net.InetAddress; import java.net.InetAddress;
@@ -20,7 +21,13 @@ public record PackmateProperties(
public record WebProperties( public record WebProperties(
String accountLogin, String accountLogin,
String accountPassword String accountPassword,
FakeAdminProperties fakeAdmin
) {}
public record FakeAdminProperties(
boolean enabled,
FakeAdminMode mode
) {} ) {}
public record TimeoutProperties( 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.ServiceDto;
import ru.serega6531.packmate.model.pojo.ServiceUpdateDto; import ru.serega6531.packmate.model.pojo.ServiceUpdateDto;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage; import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import ru.serega6531.packmate.model.pojo.FakeServiceDto;
import ru.serega6531.packmate.repository.ServiceRepository; import ru.serega6531.packmate.repository.ServiceRepository;
import java.net.InetAddress; import java.net.InetAddress;
@@ -79,6 +80,17 @@ public class ServicesService {
.toList(); .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) { public void deleteByPort(int port) {
log.info("Removed service at port {}", port); log.info("Removed service at port {}", port);

View File

@@ -27,6 +27,9 @@ packmate:
web: web:
account-login: BinaryBears account-login: BinaryBears
account-password: 123456 account-password: 123456
fake-admin:
enabled: true
mode: fun # fun, fake_packets
timeout: timeout:
udp-stream-timeout: 20 # seconds udp-stream-timeout: 20 # seconds
tcp-stream-timeout: 40 # seconds tcp-stream-timeout: 40 # seconds
@@ -35,4 +38,4 @@ packmate:
enabled: true enabled: true
threshold: 240 # minutes threshold: 240 # minutes
interval: 5 # 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