# [HCTF 2018]WarmUp # [HCTF 2018]WarmUp 初次访问,这个滑稽应该是代表了作者幸灾乐祸的表情 ![[HCTF 2018]WarmUp-1](https://pic.imgdb.cn/item/6533c62ec458853aef75af20.jpg) 一开始以为flag隐写在图片里,下载图片后查看里面的内容没有找到flag 查看网页源代码 ![[HCTF 2018]WarmUp-2](https://pic.imgdb.cn/item/6533c6b6c458853aef77d68e.jpg) 得到提示:``,访问`source.php` ![[HCTF 2018]WarmUp-3](https://pic.imgdb.cn/item/6533c6fcc458853aef78ed46.jpg) 代码审计 ```php "source.php","hint"=>"hint.php"]; if (! isset($page) || !is_string($page)) { echo "you can't see it"; return false; } if (in_array($page, $whitelist)) { return true; } $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } $_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } echo "you can't see it"; return false; } } if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file']) ) { include $_REQUEST['file']; exit; } else { echo "
"; } ?> ``` 初步审计,看到代码里出现了个`hint.php`,先访问看看 ![[HCTF 2018]WarmUp-4](https://pic.imgdb.cn/item/6533ca51c458853aef86d437.jpg) 告知flag在`ffffllllaaaagggg`,也就是要想办法看到`ffffllllaaaagggg`里的内容 先看看`emmm类`外的代码 ```php if (! empty($_REQUEST['file']) //'file'参数的值不为空 && is_string($_REQUEST['file']) //'file'参数的值为字符串 && emmm::checkFile($_REQUEST['file']) //将'file'参数的值传入emmm类的checkFile方法后返回真 ) { // 如果上述条件都满足,则执行以下代码块 include $_REQUEST['file']; // 包含'file'参数的值所指向的文件 exit; } else { // 如果上述条件不满足,执行以下代码块 echo "
"; } ``` **这题应该是想让我们用文件包含代码`include $_REQUEST['file']`去查看`ffffllllaaaagggg`里的内容** 假设`ffffllllaaaagggg`文件和`source.php`在相同目录下,构造payload:`?file=ffffllllaaaagggg`就可以包含到`ffffllllaaaagggg`文件 假设`ffffllllaaaagggg`文件在`source.php`所在的目录的父级目录(当前目录的上一级目录)下,则构造payload:`?file=../ffffllllaaaagggg`就可以包含到`ffffllllaaaagggg`文件 但是想要让代码执行到`include $_REQUEST['file']`,得满足`if`条件判断: - `'file'`参数的值不为空 (满足√) - `'file'`参数的值为字符串 (满足√) - 将`'file'`参数的值传入`emmm类`的`checkFile`函数后返回真 (暂且未知?) 所以接下来的重点是审计`emmm类`的`checkFile`函数: ```php public static function checkFile(&$page) //将$_REQUEST['file']接收到的值传给$page { $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; //创建$whitelist(白名单)数组 if (! isset($page) || !is_string($page)) { echo "you can't see it"; return false; //如果$page变量不存在或$page变量的值不是字符串则返回false } if (in_array($page, $whitelist)) { return true; //如果$page变量的值在$whitelist(白名单)数组中则返回true } $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); /* $page . '?'':给$page变量的值后面加个'?' mb_strpos($page . '?', '?'):输出$page . '?'这个字符串首次出现'?'的位置(输出一个数字) 之所以在$page变量的值后面加个'?',是因为mb_strpos()方法若是找不到首次出现'?'的位置会输出false mb_substr($page,0,mb_strpos($page . '?', '?')): 将$page变量的值(字符串)从首字符(第0)开始截取mb_strpos($page . '?', '?')个字符 换言之就是把$page中第一个'?'后面的字符串都去掉,只留下第一个'?'前的字符串 将截取后的值赋给$_page变量 */ if (in_array($_page, $whitelist)) { return true; } //如果$_page变量的值在$whitelist(白名单)数组中则返回true $_page = urldecode($page); //将$page变量的值通过URL解码后赋给$_page变量 $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') );//与上一个类似,就是截取URL解码后的$page变量的值的第一个'?'前的字符串 if (in_array($_page, $whitelist)) { return true; //如果$_page变量的值在$whitelist(白名单)数组中则返回true } echo "you can't see it"; return false; } ``` > 以上代码中出现的函数名: > > - [isset() 函数](https://www.runoob.com/php/php-isset-function.html) > - [is_string函数](https://www.runoob.com/php/php-is_string-function.html) > - [in_array函数](https://www.runoob.com/php/func-array-in-array.html) > - [mb_substr() 函数](https://www.runoob.com/php/func-string-mb_substr.html) > - [mb_strpos()函数](https://www.php.net/manual/zh/function.mb-strpos.php) 通过审计可以得知,`checkFile`函数里有三个地方可以返回true 若是要通过第一个地方来返回true,通过URL传进来的`file`参数的值只能为`source.php`或者`hint.php` 即:`source.php?file=hint.php`或`source.php?file=hint.php`,后面是不能跟其他的东西的,很显然是不可以通过第一个地方来返回true,毕竟我们是要去访问`ffffllllaaaagggg`文件 若是要通过第二个地方来返回true,通过URL传进来的`file`参数的值的格式必须为:`source.php?xxxx`或者`hint.php?xxxx` 当`$page`变量的值为`source.php?xxxx`或者`hint.php?xxxx`,代码会执行到: ```php $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } ``` 截取`$page`变量的值的第一个`?`前的字符串给`$_page`变量,`$_page`变量的值为`source.php`或者`hint.php`,在白名单内,返回true 所以payload为: - `?file=source.php?/../ffffllllaaaagggg` - `?file=source.php?/../../ffffllllaaaagggg` - `?file=source.php?/../../../ffffllllaaaagggg` - …… (把`source.php`替换成`hint.php`也可以) 在绕过白名单的情况下不断尝试往上级目录包含`ffffllllaaaagggg`文件 **最终确定payload为:`http://09b122ae-c857-4cde-b0c1-0619a97418a6.node4.buuoj.cn:81/source.php?file=source.php?/../../../../ffffllllaaaagggg`** > 可能会有疑惑:在路径前加个`source.php?`难道不会出错吗? > > 服务器会把`source.php?`看作是与**当前页面处于同级目录下的一个叫做"source.php?"的目录**,如果路径就单单是这个目录,因为目录不存在,就会无法确定而出错。 > > 如果路径是`source.php?/..`:相当于是当前目录下的一个"source.php?"目录下的上级目录,尽管"source.php?"目录不存在,但`source.php?/..`一定就是当前目录了嘛(有点绕) > > 所以`source.php?/../../../../ffffllllaaaagggg`和`../../../ffffllllaaaagggg`的效果是一样的 > > **对此我特意做了个实验来验证,放在本文的最下方** 当然,也可以从第三处入手来返回true,总的思路差不多 与第二处相比,第三处就多了条代码:`$_page = urldecode($page)`,也就是会先把`$page`的值通过URL解码后再截取第一个`?`前的字符串到白名单里比较 ```php $_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } ``` **因为使用超全局变量`$_REQUEST`已经被解码了**,所以要先把payload通过URL编码两次 **最终确定payload为:`http://09b122ae-c857-4cde-b0c1-0619a97418a6.node4.buuoj.cn:81/source.php?file=source.php%253F/../../../../ffffllllaaaagggg`** (也可以把`/`编码一次,但不可以编码两次) > [urldecode()函数](https://www.php.net/manual/zh/function.urldecode.php) > > 有的人可能有疑惑:为什么不把后面的`/`也编码编码两次? > > 全都编码两次的结果是:`source.php%253F%252F..%252F..%252F..%252F..%252Fffffllllaaaagggg` > > 执行`emmm::checkFile($_REQUEST['file'])`的`$_REQUEST['file']`接收参数时自动解码一次:`source.php%3F%2F..%2F..%2F..%2F..%2Fffffllllaaaagggg` > > 执行`$_page = urldecode($page)`时再解码一次:`source.php?/../../../../ffffllllaaaagggg` > > 这时返回true,开始执行`include $_REQUEST['file']`,但是执行`include $_REQUEST['file']`的`$_REQUEST['file']`接收到的参数是只解码过一次的:`source.php%3F%2F..%2F..%2F..%2F..%2Fffffllllaaaagggg`,服务器会把它直接当作一个文件名 ​ **验证实验:** - 服务器:CentOS 7 首先在网站的根目录`/www/admin/localhost_80/wwwroot`下有创建一个`index.php`作为网站首页,`wwwroot`目录下只有以下内容: ```bash [root@wzh wwwroot]# ls error index.php ``` `index.php`里的内容: ```php