0x00 前言
先献上flag,以表正义(我有一颗世界和平的心)
Congratulate you, flag is cb6afe419450c23f462159afb9976130
很高兴能够参加这次WooYun Puzzle,从P师傅出的题目中学到些许东西,下面分三步来突破安全盒子的秘密
0x01 引诱
题目伊始上来一个“安全盒子”,不清楚具体是什么鬼,先随便提交试试
提交后报错说权限拒绝,向这种摸不着头脑的猜测会有源码来告诉你具体逻辑,F12后发现访问源码方法<!-- ?x_show_source -->
:
随机访问http://0dac0a717c3cf340e.jie.sangebaimao.com:82/?x_show_source
后得到源码为:
所以重点在与最后的指定函数调用,分析如下:
- csrf_token校验:这里保证了提交的crsf_token的正确性,但小白我没有看出存在csrf的场景,token略显可疑
- hmac_md5校验:对于HMAC-MD5,自认为算法上不存在什么缺陷,又是
===
进行判断,就使得我们必须知道$_SESSION['SECRET_KEY']
的确切值,才能进入之后的if逻辑
- 函数存在性校验:先检验载调用函数,而这里具有利用价值的估计就是包含在flag.php中的函数了
- 调用指定函数:无参数传递,感觉会是个小坑哈
- 输出函数返回结果:两种输出方式二选其一,猜测作者这里也另有目的
在程序中,对于每一个session都会先生成6位SECRET_KEY保持不变,在每次提交后对16位的CSRF_TOKEN进行变化。要知道SECRET_KEY的确切值,我首先想到的是暴力破解,虽然62**6不是太大,可是后续的套具体还不知道,而且出题人也不会这么无聊,尝试了一下就放弃了这个想法。既然不能猜那就预测呗,我的痛苦经历让我想到了一次CTF的rand()预测题目,具体原理与题目可见:
有没感觉很像,CTF题目中是多次输出rand()的值,再结合其生成算法,根据其之前生成的值预测之后的值:
而我们这里呢,是每次提交后CSRF_TOKEN都会变化,而token中的字符根据对应关系也就是由rand()生成的值。但他们是向后预测,我们好像是向前推理,那该怎么办呢?加法会减法就不会啦?!
可是我们这里的rand()是有范围的那该如何呢,搜一搜翻出对应的源码即可找到答案:
对于n' = a + n(b-a+1)/(M+1)
,我们先令a=0,b=61化简成n' = 62n/(M+1)
,把运算用于整个公式,我再偷一下懒最后使用(o[31+i]+62-o[28+i])%62
来推算前6位的SECRET_KEY,初步代码(new-1.py)如下:
由于rand生成算法中有加1的随机情况存在,所以这里就需要多推测几次得到正确的SECRET_KEY,进入if逻辑调用函数,所以再写一个初步的代码(get-1.py)重复测试
运行得到如下输出:
显然我这样瞎猜函数不会是个头,那就必须知道flag.php中是否有函数可供我们利用呢,所以我们就得知道哪些函数可以查看当前脚本中定义的函数,变量,常量等信息,百度一下你就知道:PHP输出当前进程所有变量/常量/模块/函数/类的示例,正好还都是不需要参数的。我首先利用get_defined_vars()和get_defined_constants()看看能不能直接脱出flag变量或者常量,然而并没有我想象的那么简单。那就用get_defined_functions看看吧:
这里函数的结果是个数组,而strval()对数组的变换同一是”Array”。那就试试让其用json响应呗,根据stackoverflow上大神的回答,我们加个'X-Requested-With': 'XMLHttpRequest'
的header就好了,输出如下(有省略):
主要关注的是最后一个数组中用户定义的函数,哎呦,有个fd_show_source的函数,试试没准flag就出来了。
0x02 绕过
事情并没有我们想象的那么简单,看来是作者又给我们下了一个套,fd_show_source函数输出整理如下(已加个人注解):
首先在初始化_fd_init()中,要点分析如下:
- 再次hamc_md5校验:这里的校验好说,毕竟
SECRET_KEY
已经知晓,带上个h1
就好
- 防止h2中出现”admin”和”user”:这里使用了strpos来查看字符串中是否存在身份伪造,而且使用
!==
很规范,从源码基本也没看出什么破绽
- $s[‘role’]转为字符串:strval会将数组类型变得没有意义,也就想不出办法绕过对”admin”身份的检验
在fg_safebox()中第一关就是要判断其身份,要求其为”admin”或”user”。结合以上的分析,admin的两次检验我是绕不过去了我认了。所以就开始琢磨能不能使最终的$role
为”user”,而strpos约束我们在$h2中不能出现”user”,要不然我们编码试试?哈哈,这里的$s = json_decode($h2, true);
就是等着我们利用的,可以将”user”进行unicode编码成为\u0075\u0073\u0065\u0072
,这样在strpos中就不会检验出来,而且经过json_decode最终还原成”user”。
在(2)判读行为权限中,加入$box对象的read方法不在$config[‘role’][‘admin’]数组内,那么就不会判断$role != "admin"
,进而产生绕过。我们再修改一下原先的代码(new-2.py)如下:
相应的get-2.py也简单如下:
输出如下:
在简单尝试之后”user”只能调用$box对象的view、alist和view方法,唯独不能使用read方法,如果说一定要用read方法,这里估计也是无法绕过去了。不急,继续往下看看有没有思路,在(4)调用相应方法中,其会和PHP对象有关联,而就我知道的和搜索到的,大多都是PHP对象注入问题,而这里也没有魔术方法和序列化之类的东西,猜测也就不是这个考察点。那么关注的重心就移到了(3)判断对象方法是否存在,在使用method_exists的时候会不会出现什么问题呢,看源码之:
唉呀妈呀,其中一句lcname = zend_string_tolower(method_name);
,就猜测这里是先将方法名转成小写再进行判断和利用的。我们这里就可以大小写绕过,使”user”调用的方法为READ
,进入(4)中的函数调用,加上filename的POST就可以进行任意文件读取啦~
0x03 寻找
赶紧读读config.php里面有没有什么东西,结果:
看来还是要有点渗透思维去读读配置文件什么的,看看flag到底藏在哪,参考Linux渗透与提权:技巧总结篇与Linux提权后获取敏感信息的方法与途径,把里面cat的文件全部集中一起,写个脚本跑一遍及可发现flag,代码与之前的类似,详见github