Skip to content

Commit 9e5da81

Browse files
Merge pull request #3 from C4T-BuT-S4D/pomo/service-ark
Ark service
2 parents 0e2c2b5 + 3946c22 commit 9e5da81

29 files changed

+4232
-10
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,4 @@ dmypy.json
7474
.DS_Store
7575
.vscode
7676
.idea
77-
77+
.mise.toml

check.py

+20-9
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"sysctls",
4949
"privileged",
5050
"security_opt",
51+
"ulimits",
52+
"command",
5153
]
5254
SERVICE_REQUIRED_OPTIONS = ["pids_limit", "mem_limit", "cpus"]
5355
SERVICE_ALLOWED_OPTIONS = CONTAINER_ALLOWED_OPTIONS
@@ -213,8 +215,10 @@ def put(self, flag: str, flag_id: str, vuln: int):
213215
cmd = [str(self._exe_path), "put", HOST, flag_id, flag, str(vuln)]
214216
out, err = self._run_command(cmd)
215217

216-
self._fatal(len(out) <= 1024, "returned stdout is longer than 1024 characters")
217-
self._fatal(len(err) <= 1024, "returned stderr is longer than 1024 characters")
218+
self._fatal(len(out) <= 1024,
219+
"returned stdout is longer than 1024 characters")
220+
self._fatal(len(err) <= 1024,
221+
"returned stderr is longer than 1024 characters")
218222

219223
if self._attack_data:
220224
self._fatal(out, "stdout is empty")
@@ -241,7 +245,8 @@ def run_all(self, step: int):
241245

242246
for vuln in range(1, self._vulns + 1):
243247
flag = generate_flag(self._name)
244-
flag_id = self.put(flag=flag, flag_id=secrets.token_hex(16), vuln=vuln)
248+
flag_id = self.put(
249+
flag=flag, flag_id=secrets.token_hex(16), vuln=vuln)
245250
flag_id = flag_id.strip()
246251
self.get(flag, flag_id, vuln)
247252

@@ -330,9 +335,11 @@ def validate_file(self, f: Path):
330335
path = f.relative_to(BASE_DIR)
331336

332337
if f.name not in ALLOWED_YAML_FILES:
333-
self._error(f.suffix != ".yaml", f"file {path} has .yaml extension")
338+
self._error(f.suffix != ".yaml",
339+
f"file {path} has .yaml extension")
334340

335-
self._error(f.name != ".gitkeep", f"{path} found, should be named .keep")
341+
self._error(f.name != ".gitkeep",
342+
f"{path} found, should be named .keep")
336343

337344
if f.name == "docker-compose.yml":
338345
with f.open() as file:
@@ -355,7 +362,8 @@ def validate_file(self, f: Path):
355362
try:
356363
dc_version = float(dc["version"])
357364
except ValueError:
358-
self._error(False, f"version option in {path} is not float")
365+
self._error(
366+
False, f"version option in {path} is not float")
359367
return
360368

361369
self._error(
@@ -426,12 +434,14 @@ def validate_file(self, f: Path):
426434
else:
427435
context = build["context"]
428436
if "dockerfile" in build:
429-
dockerfile = f.parent / context / build["dockerfile"]
437+
dockerfile = f.parent / \
438+
context / build["dockerfile"]
430439
else:
431440
dockerfile = f.parent / context / "Dockerfile"
432441

433442
if self._error(
434-
dockerfile.exists(), f"no dockerfile found in {dockerfile}"
443+
dockerfile.exists(
444+
), f"no dockerfile found in {dockerfile}"
435445
):
436446
continue
437447

@@ -496,7 +506,8 @@ def validate_file(self, f: Path):
496506
for p in ALLOWED_CHECKER_PATTERNS:
497507
checker_code = checker_code.replace(p, "")
498508
for p in FORBIDDEN_CHECKER_PATTERNS:
499-
self._error(p not in checker_code, f'forbidden pattern "{p}" in {path}')
509+
self._error(p not in checker_code,
510+
f'forbidden pattern "{p}" in {path}')
500511

501512
def __str__(self):
502513
return f"Structure validator for {self._service.name}"

checkers/ark/ark_lib.py

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import random
2+
import re
3+
import string
4+
from dataclasses import dataclass
5+
6+
from checklib import * # type: ignore
7+
from pwn import context, remote
8+
9+
context.timeout = 10
10+
context.log_level = 'FATAL'
11+
12+
PORT = 13345
13+
14+
15+
@dataclass
16+
class SuggestedUser:
17+
username: str
18+
file_count: int
19+
file_size: int
20+
21+
22+
@dataclass
23+
class UserFile:
24+
path: str
25+
size: int
26+
27+
28+
@dataclass
29+
class File:
30+
path: str
31+
size: int
32+
33+
34+
class CheckMachine:
35+
def __init__(self, checker: BaseChecker):
36+
self.c = checker
37+
self.port = PORT
38+
39+
def connect(self) -> remote:
40+
r = remote(self.c.host, self.port)
41+
r.recvuntil(b'quit')
42+
r.recvuntil(b'> ')
43+
return r
44+
45+
def register(self, r: remote, username: str, password: str, status: Status = Status.MUMBLE):
46+
r.sendline(f'register {username} {password}'.encode())
47+
response = r.recvline().decode()
48+
r.recvuntil(b'> ')
49+
50+
self.c.assert_in('Registration successful', response,
51+
'Registration failed', status=status)
52+
53+
def login(self, r: remote, username: str, password: str, status: Status = Status.MUMBLE):
54+
r.sendline(f'login {username} {password}'.encode())
55+
response = r.recvline().decode()
56+
r.recvuntil(b'> ')
57+
58+
self.c.assert_in('Welcome,', response,
59+
'Login failed', status=status)
60+
61+
return r
62+
63+
def save_file(self, r: remote, path: str, content: str, status: Status = Status.MUMBLE):
64+
r.sendline(f'save {path} {content}'.encode())
65+
response = r.recvuntil(b'> ').decode()
66+
67+
self.c.assert_in('File saved successfully', response,
68+
'Save failed', status=status)
69+
70+
def cat_file(self, r: remote, path: str, status: Status = Status.MUMBLE) -> str:
71+
r.sendline(f'cat {path}'.encode())
72+
response = r.recvuntil(b'> ').decode()
73+
74+
# Extract content between the command and the next prompt
75+
lines = response.split('\n')
76+
self.c.assert_gt(len(lines), 1, 'Cat failed', status=status)
77+
return lines[0]
78+
79+
def suggest_users(self, r: remote, status: Status = Status.MUMBLE) -> list[SuggestedUser]:
80+
r.sendline(b'suggest_users')
81+
response = r.recvuntil(b'> ').decode()
82+
83+
self.c.assert_in('Suggested users:', response,
84+
'Suggest users failed', status=status)
85+
86+
users = []
87+
for line in response.split('\n'):
88+
if 'Username:' in line:
89+
# Username: kek, Created At: 2024-12-15 13:10:54.753883 UTC, File Count: 1, Total File Size: 4
90+
matches = re.match(
91+
r'Username: (\w+), .*, File Count: (\d+), Total File Size: (\d+)', line)
92+
if matches:
93+
users.append(SuggestedUser(
94+
matches.group(1), int(matches.group(2)), int(matches.group(3))))
95+
return users
96+
97+
def list_user_files(self, r: remote, username: str, status: Status = Status.MUMBLE) -> list[UserFile]:
98+
if username == '':
99+
r.sendline(b'list')
100+
else:
101+
r.sendline(f'list_files {username}'.encode())
102+
103+
response = r.recvuntil(b'> ').decode()
104+
105+
files = []
106+
for line in response.split('\n'):
107+
if 'ID' in line:
108+
# ID: 2, Path: /tmp/1, Size: 4 bytes
109+
matches = re.match(
110+
r'ID: (\d+), Path: (\S+), Size: (\d+) bytes', line)
111+
if matches:
112+
files.append(
113+
UserFile(matches.group(2), int(matches.group(3))))
114+
return files
115+
116+
def copy_file(self, r: remote, src_path: str, dst_path: str, status: Status = Status.MUMBLE):
117+
r.sendline(f'copy {src_path} {dst_path}'.encode())
118+
response = r.recvuntil(b'> ').decode()
119+
120+
self.c.assert_in('File copied successfully', response,
121+
'Copy failed', status=status)
122+
123+
def random_filename(self) -> str:
124+
d = random.choice(['tmp', 'files'])
125+
l = random.randint(10, 50)
126+
a = string.ascii_letters + string.digits + '.,-_=+:|()*[]&^'
127+
return f"/{d}/test_{rnd_string(l, a)}.txt"

checkers/ark/checker.py

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import random
5+
import sys
6+
7+
if True:
8+
saved_args = sys.argv.copy()
9+
10+
from ark_lib import *
11+
from checklib import * # type: ignore
12+
from pwn import PwnlibException
13+
14+
15+
class Checker(BaseChecker):
16+
vulns: int = 1
17+
timeout: int = 20
18+
uses_attack_data: bool = True
19+
20+
def __init__(self, *args, **kwargs):
21+
super(Checker, self).__init__(*args, **kwargs)
22+
self.mch = CheckMachine(self)
23+
24+
def check(self):
25+
# Register a user
26+
username, password = rnd_username(), rnd_password()
27+
with (
28+
self.mch.connect() as r1,
29+
self.mch.connect() as r2,
30+
self.mch.connect() as anon_r,
31+
):
32+
def gr():
33+
return random.choice([r1, r2])
34+
35+
def ar():
36+
return random.choice([r1, anon_r])
37+
38+
self.mch.register(gr(), username, password)
39+
40+
self.mch.login(r1, username, password)
41+
self.mch.login(r2, username, password)
42+
43+
files = [
44+
(self.mch.random_filename(), rnd_string(random.randint(10, 50)))
45+
for _ in range(random.randint(1, 10))
46+
]
47+
for file, content in files:
48+
self.mch.save_file(gr(), file, content)
49+
50+
random.shuffle(files)
51+
for file, content in files:
52+
got_content = self.mch.cat_file(gr(), file, Status.MUMBLE)
53+
self.assert_eq(got_content, content, "File content mismatch")
54+
55+
# Test list functionality
56+
list_output = self.mch.list_user_files(gr(), '')
57+
for file, _ in files:
58+
self.assert_in(file, [f.path for f in list_output],
59+
"File not found in list")
60+
61+
# Test suggest_users
62+
suggest_output = self.mch.suggest_users(gr())
63+
if len(suggest_output) < 30 and not any(u.username == username for u in suggest_output):
64+
self.cquit(Status.MUMBLE, "Suggest users failed")
65+
66+
# Test list_user_files
67+
user_files = self.mch.list_user_files(ar(), username)
68+
for file, content in files:
69+
self.assert_in(file, [f.path for f in user_files],
70+
"File not found in user files")
71+
self.assert_in(len(content), [f.size for f in user_files],
72+
"File size mismatch")
73+
74+
# Register a second user
75+
# Test second user can copy first user's files
76+
# Test first user can cat their own files from the second user's account
77+
78+
username2, password2 = rnd_username(), rnd_password()
79+
self.mch.register(ar(), username2, password2)
80+
self.mch.login(r2, username2, password2)
81+
82+
# Test list_user_files
83+
user_files = self.mch.list_user_files(ar(), username)
84+
for file, content in files:
85+
self.assert_in(file, [f.path for f in user_files],
86+
"File not found in user files")
87+
self.assert_in(len(content), [f.size for f in user_files],
88+
"File size mismatch")
89+
90+
file_to_copy = random.choice(files)
91+
dst_path = self.mch.random_filename()
92+
self.mch.copy_file(r2, file_to_copy[0], dst_path)
93+
94+
# Test second user can list their own files
95+
user_files = self.mch.list_user_files(r2, username2)
96+
self.assert_in(dst_path, [f.path for f in user_files],
97+
"File not found in user files")
98+
99+
# Test first user can cat their own files from the second user's quota
100+
content = self.mch.cat_file(r1, dst_path, Status.MUMBLE)
101+
self.assert_eq(content, file_to_copy[1], "File content mismatch")
102+
103+
self.cquit(Status.OK)
104+
105+
def put(self, flag_id: str, flag: str, vuln: str):
106+
username1, password1 = rnd_username(), rnd_password()
107+
username2, password2 = rnd_username(), rnd_password()
108+
109+
with (
110+
self.mch.connect() as r1,
111+
self.mch.connect() as r2,
112+
):
113+
self.mch.register(r1, username1, password1)
114+
self.mch.register(r2, username2, password2)
115+
116+
self.mch.login(r1, username1, password1)
117+
self.mch.login(r2, username2, password2)
118+
119+
flag_file1 = self.mch.random_filename()
120+
flag_file2 = self.mch.random_filename()
121+
122+
self.mch.save_file(r1, flag_file1, flag)
123+
self.mch.copy_file(r2, flag_file1, flag_file2)
124+
125+
public = f'u1={username1}:u2={username2}'
126+
127+
private = json.dumps({
128+
'username1': username1,
129+
'password1': password1,
130+
'username2': username2,
131+
'password2': password2,
132+
'flag_file1': flag_file1,
133+
'flag_file2': flag_file2,
134+
})
135+
136+
self.cquit(Status.OK, public=public, private=private)
137+
138+
def get(self, flag_id: str, flag: str, vuln: str):
139+
data = json.loads(flag_id)
140+
141+
with (
142+
self.mch.connect() as r1,
143+
self.mch.connect() as r2,
144+
self.mch.connect() as anon_r,
145+
):
146+
self.mch.login(r1, data['username1'],
147+
data['password1'], Status.CORRUPT)
148+
self.mch.login(r2, data['username2'],
149+
data['password2'], Status.CORRUPT)
150+
151+
for r in [r1, r2, anon_r]:
152+
file_list = self.mch.list_user_files(r, data['username1'])
153+
self.assert_in(data['flag_file1'], [f.path for f in file_list],
154+
"Flag file not found in user files", Status.CORRUPT)
155+
156+
file_list = self.mch.list_user_files(r, data['username2'])
157+
self.assert_in(data['flag_file2'], [f.path for f in file_list],
158+
"Flag file not found in user files", Status.CORRUPT)
159+
160+
content = self.mch.cat_file(r1, data['flag_file1'], Status.CORRUPT)
161+
self.assert_eq(content, flag, "Flag mismatch", Status.CORRUPT)
162+
163+
content = self.mch.cat_file(r1, data['flag_file2'], Status.CORRUPT)
164+
self.assert_eq(content, flag, "Flag mismatch", Status.CORRUPT)
165+
166+
self.cquit(Status.OK)
167+
168+
def action(self, action, *args, **kwargs):
169+
try:
170+
super(Checker, self).action(action, *args, **kwargs)
171+
except PwnlibException:
172+
self.cquit(Status.DOWN, 'Connection error', 'Got pwnlib exception')
173+
174+
175+
if __name__ == '__main__':
176+
c = Checker(saved_args[2])
177+
178+
try:
179+
c.action(saved_args[1], *saved_args[3:])
180+
except c.get_check_finished_exception():
181+
cquit(Status(c.status), c.public, c.private)

0 commit comments

Comments
 (0)