Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Git
.git
.gitignore

# Documentation
README.md
LICENSE

# Docker
Dockerfile
.dockerignore
docker-compose.yml

# IDE
.vscode
.idea
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
*.log

# Runtime directories (will be created in container)
plots/
submissions/
injects/
temporary/
scoredfiles/
config/

# Development files
.env
*.conf
9 changes: 6 additions & 3 deletions .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
POSTGRES_PASSWORD=password
POSTGRES_DB=engine
POSTGRES_USER=engineuser
POSTGRES_HOST=localhost
POSTGRES_DB=engine
POSTGRES_PASSWORD=password
POSTGRES_HOST=db
POSTGRES_PORT=5432
export UID=1000
export GID=1000
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ plots/*
*.conf
!.gitkeep
quotient
.vscode
.vscode

data/
63 changes: 63 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Build stage
FROM golang:1.21-alpine AS builder

# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata

# Set working directory
WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o quotient .

# Final stage
FROM alpine:3.18

# Install runtime dependencies
RUN apk add --no-cache ca-certificates tzdata su-exec

# Create non-root user
RUN adduser -D -s /bin/sh quotient

# Set working directory
WORKDIR /app

# Copy binary from builder stage
COPY --from=builder /app/quotient .

# Copy required directories
COPY --from=builder /app/assets ./assets
COPY --from=builder /app/templates ./templates
COPY --from=builder /app/scripts ./scripts

# Copy entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Create necessary directories
RUN mkdir -p config data/plots data/submissions data/injects data/temporary data/scoredfiles data/keys

# Set ownership
RUN chown -R quotient:quotient /app

# Expose port
EXPOSE 80

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1

# Set entrypoint
ENTRYPOINT ["/entrypoint.sh"]

# Run the application
CMD ["./quotient", "-c", "config/event.conf"]
108 changes: 102 additions & 6 deletions authentication.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package main

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/http"
"strings"

"os"
"path/filepath"
"strings"
"time"

"github.com/gin-contrib/sessions"
Expand Down Expand Up @@ -74,25 +78,82 @@ func getClaimsFromToken(tokenString string) (jwt.MapClaims, error) {
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, err
return nil, fmt.Errorf("invalid token claims")
}

func readKeyFiles() ([]byte, []byte, error) {
// Try to read existing keys
prvKey, err := os.ReadFile(eventConf.JWTPrivateKey)
if err != nil {
fmt.Println(err)
// If private key doesn't exist, generate new keys
if os.IsNotExist(err) {
fmt.Println("JWT keys not found, generating new keys...")
return generateJWTKeys()
}
return nil, nil, err
}

pubKey, err := os.ReadFile(eventConf.JWTPublicKey)
if err != nil {
fmt.Println(err)
// If public key doesn't exist, generate new keys
if os.IsNotExist(err) {
fmt.Println("JWT keys not found, generating new keys...")
return generateJWTKeys()
}
return nil, nil, err
}

return prvKey, pubKey, nil
}

func generateJWTKeys() ([]byte, []byte, error) {
// Ensure the keys directory exists
keysDir := filepath.Dir(eventConf.JWTPrivateKey)
if err := os.MkdirAll(keysDir, 0755); err != nil {
return nil, nil, fmt.Errorf("failed to create keys directory: %w", err)
}

// Generate 2048-bit RSA key pair
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
}

// Encode private key to PEM
privateKeyPEM := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}

privateKeyBytes := pem.EncodeToMemory(privateKeyPEM)

// Encode public key to PEM
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal public key: %w", err)
}

publicKeyPEM := &pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
}

publicKeyBytes = pem.EncodeToMemory(publicKeyPEM)

// Write keys to files
if err := os.WriteFile(eventConf.JWTPrivateKey, privateKeyBytes, 0600); err != nil {
return nil, nil, fmt.Errorf("failed to write private key: %w", err)
}

if err := os.WriteFile(eventConf.JWTPublicKey, publicKeyBytes, 0644); err != nil {
return nil, nil, fmt.Errorf("failed to write public key: %w", err)
}

fmt.Printf("Generated JWT keys:\n Private: %s\n Public: %s\n", eventConf.JWTPrivateKey, eventConf.JWTPublicKey)

return privateKeyBytes, publicKeyBytes, nil
}

func initCookies(router *gin.Engine) {
router.Use(sessions.Sessions("quotient", cookie.NewStore([]byte("quotient"))))
}
Expand Down Expand Up @@ -276,10 +337,21 @@ func isLoggedIn(c *gin.Context) (bool, error) {
return true, nil
}

func clearAuthCookiesAndRedirect(c *gin.Context) {
// Clear the auth_token cookie
c.SetCookie("auth_token", "", -1, "/", "*", false, true)
// Clear session
session := sessions.Default(c)
session.Delete("id")
session.Save()
// Redirect to home page
c.Redirect(http.StatusSeeOther, "/")
}

func authRequired(c *gin.Context) {
status, err := isLoggedIn(c)
if status == false || err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
clearAuthCookiesAndRedirect(c)
return
}
c.Next()
Expand Down Expand Up @@ -313,6 +385,30 @@ func contextGetClaims(c *gin.Context) (UserJWTData, error) {
}

func adminAuthRequired(c *gin.Context) {
claims, err := contextGetClaims(c)
if err != nil {
clearAuthCookiesAndRedirect(c)
return
}

if claims.Admin == false {
clearAuthCookiesAndRedirect(c)
return
}

c.Next()
}

func authRequiredAPI(c *gin.Context) {
status, err := isLoggedIn(c)
if status == false || err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
c.Next()
}

func adminAuthRequiredAPI(c *gin.Context) {
claims, err := contextGetClaims(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
Expand Down
2 changes: 1 addition & 1 deletion checks/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func GetFile(fileName string) (string, error) {
// this isn't really an issue since if you can
// edit the config, you already have as shell,
// but whatever. and it's only reading/hashing
fileContent, err := os.ReadFile("./scoredfiles/" + fileName)
fileContent, err := os.ReadFile("./data/scoredfiles/" + fileName)
if err != nil {
return "", err
}
Expand Down
67 changes: 32 additions & 35 deletions checks/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package checks

import (
"fmt"
"os/exec"
"strconv"
"strings"
"time"

"github.com/go-ping/ping"
)

type Ping struct {
Expand All @@ -15,51 +16,47 @@ type Ping struct {
}

func (c Ping) Run(teamID uint, teamIdentifier string, target string, res chan Result) {
// Create pinger
pinger, err := ping.NewPinger(target)
if err != nil {
res <- Result{
Error: "ping creation failed",
Debug: err.Error(),
}
return
// Set default count if not specified
count := c.Count
if count == 0 {
count = 1
}

// Send ping
pinger.Count = 1
pinger.Timeout = 5 * time.Second
pinger.SetPrivileged(true)
err = pinger.Run()
if err != nil {
res <- Result{
Error: "ping failed",
Debug: err.Error(),
}
return
// Use configurable timeout from Service struct, with a minimum of 5 seconds
timeout := c.Timeout
if timeout == 0 {
timeout = 30 // Default from config
}
if timeout < 5 {
timeout = 5 // Minimum timeout for ping
}

stats := pinger.Statistics()
// Check packet loss instead of count
if c.AllowPacketLoss {
if stats.PacketLoss >= float64(c.Percent) {
res <- Result{
Error: "not enough pings suceeded",
Debug: "ping failed: packet loss of " + fmt.Sprintf("%.0f", stats.PacketLoss) + "% higher than limit of " + fmt.Sprintf("%d", c.Percent) + "%",
}
return
}
// Check for failure
} else if stats.PacketsRecv != c.Count {
// Use system ping command with configurable timeout and count
cmd := exec.Command("timeout", strconv.Itoa(timeout), "ping", "-c", strconv.Itoa(count), "-W", "5", target)

start := time.Now()
output, err := cmd.Output()
duration := time.Since(start)

outputStr := string(output)
fmt.Println(outputStr)
fmt.Println(err)
fmt.Println(duration)

// Check if ping was successful by looking for "1 received" in the output
// The timeout command may return non-zero exit code even when ping succeeds
if err != nil && !strings.Contains(outputStr, "1 received") {
res <- Result{
Error: "not all pings suceeded",
Debug: "packet loss of " + fmt.Sprintf("%f", stats.PacketLoss),
Error: fmt.Sprintf("ping failed: %v", err),
Debug: fmt.Sprintf("Target: %s, Count: %d, Timeout: %ds, Duration: %v, Output: %s", target, count, timeout, duration, outputStr),
}
return
}

res <- Result{
Status: true,
Points: c.Points,
Debug: fmt.Sprintf("Target: %s, Count: %d, Duration: %v", target, count, duration),
}
}

Expand Down
2 changes: 1 addition & 1 deletion checks/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (c Ssh) Run(teamID uint, teamIdentifier string, target string, res chan Res
config.SetDefaults()
config.Ciphers = append(config.Ciphers, "3des-cbc")
if c.PrivKey != "" {
key, err := os.ReadFile("./scoredfiles/" + c.PrivKey)
key, err := os.ReadFile("./data/scoredfiles/" + c.PrivKey)
if err != nil {
res <- Result{
Error: "error opening pubkey",
Expand Down
Loading