2024年5月5日 星期日

PowerShell獲取檔案權限表格

PowerShell獲取檔案權限表格

原本的物件不是很好使,寫了個函式方便快速查詢

輸出範例

Identity                         FullControl Modify ReadAndExecute Read  Write
--------                         ----------- ------ -------------- ----  -----
NT AUTHORITY\SYSTEM              Allow
NT AUTHORITY\Authenticated Users             Allow
BUILTIN\Administrators           Allow
BUILTIN\Users                                       Allow          Allow Deny




函式代碼

Get-FilePermissions

function Get-FilePermissions {
     [CmdletBinding(DefaultParameterSetName = "")]
     param(
         [Parameter(Position = 0, ParameterSetName = "", Mandatory)]
         [string]$Path,
         [Parameter(Position = 1, ParameterSetName = "")]
         [string]$Identity
     )

     # 確認指定的路徑是否存在
     if (!(Test-Path $Path)) {
         Write-Error "Cannot find path '$Path' because it does not exist." -ErrorAction Stop
     }

     # 取得指定路徑的存取控制清單 (ACL)
     $acl = Get-Acl $Path

     # 定義需要檢查的權限列表
     $permissions = @('FullControl', 'Modify', 'ReadAndExecute', 'Read', 'Write')

     # 初始化一個字典來儲存每個使用者的權限狀態
     $permissionDict = @{}

     # 遍歷存取控制規則
     foreach ($accessRule in $acl.Access) {
         # 取得使用者名稱
         $user = $accessRule.IdentityReference.Value

         # 如果字典尚未包含目前用戶,則新增一個條目
         if (-not $permissionDict.ContainsKey($user)) {
             # 動態建立權限字段
             $userPermissions = [ordered]@{ Identity = $user }
             foreach ($permission in $permissions) {
                 $userPermissions[$permission] = ''
             }
             $permissionDict[$user] = $userPermissions
         }

         # 更新權限狀態,拒絕權限優先
         foreach ($permission in $permissions) {
             if ($accessRule.FileSystemRights -like "*$permission*") {
                 $state = $accessRule.AccessControlType.ToString()
                 if ($permissionDict[$user][$permission] -ne 'Deny') {
                     $permissionDict[$user][$permission] = $state
                 }
             }
         }
     }

     # 將字典中的值轉換為物件數組
     if ($Identity) {
         $permission = [PSCustomObject]($permissionDict[$Identity])
     } else {
         $permission = $permissionDict.Values | ForEach-Object { [PSCustomObject]$_ }
     }

     # 傳回產生的權限表格
     return $permission
}

# Get-FilePermissions "Z:\test.txt"|Format-Table|Out-String
# Get-FilePermissions "Z:\test.txt" -Identity 'BUILTIN\Administrators'


使用範例

# 輸出表格
Get-FilePermissions "Z:\test.txt"|Format-Table|Out-String

# 指定特定使用者或群組名稱
Get-FilePermissions "Z:\test.txt" -Identity 'BUILTIN\Administrators'



2024年5月1日 星期三

Linux Ubuntu 自動更新腳本

Linux Ubuntu 自動更新腳本

更新要打的指令其實就那麼幾個只是經常打覺得煩了,習慣上會弄成一個 update.sh 這樣來做比較快

#!/bin/bash

# 更新軟件包列表
sudo apt update

# 升級已安裝的軟件包
sudo apt upgrade -y

# 清理不再需要的軟件包
sudo apt autoremove -y

# 清理下載的安裝包緩存
sudo apt clean

複製之後使用下面的指令打開編輯器

nano ~/update.sh


然後貼上按下 CTRL+S 儲存,再按 CTRL+X 離開
接著在更改權限讓他變成可執行檔案

chmod +x ~/update.sh


到這邊就大功告成了可以使用下面快捷更新

./update.sh



執行之後還是有可能沒更完全

  1. 保留的軟件包:某些軟件包可能因為依賴關係的問題被保留下來,不進行自動升級。
  2. 需要手動干預:有時候,軟件包需要你手動確認升級,尤其是那些可能影響系統設定的重要軟件包。


查看哪些沒有被更新

sudo apt list --upgradable


1. 強制更新

sudo apt full-upgrade

2. 移除

sudo apt remove YOUR_APP_NAME

3. 移除(連同所有設定檔)

sudo apt purge YOUR_APP_NAME




PowerShell 如何接收外部程式返回的二進制值

PowerShell 如何接收外部程式返回的二進制值

這個超坑人阿 PowerShell 預設會自動把收到的數值編碼,導致二進制數值讀進來的瞬間就因為轉換的關係資料遺失了

舉個例子來說如果要用 OpenSSL 簽名,簽名有點複雜在簡化一點產生一個隨機數值並返回二進位數值

openssl rand -out - 8

拿到的值就是爛掉的因為轉換編碼的過程就發生不可逆的資料丟失了
查了一下還好是有解的

shell - How to get original binary data from external command output in PowerShell? - Stack Overflow

只能說真愛生命不要用 PowerShell 管道傳輸非字串的東西…




完成的函式

代碼我把他弄好成一個函式了

function Invoke-CommandAndGetBinaryOutput {
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [string]$CommandLine
    )
    Process {
        # 用空白分割命令行,並分配文件名與參數
        $parts = $CommandLine -split ' ', 2
        $fileName = $parts[0]
        $arguments = $parts[1]

        # 建立 ProcessStartInfo 對象並配置
        $processInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
            FileName = $fileName
            Arguments = $arguments
            RedirectStandardOutput = $true
            RedirectStandardError = $true
            UseShellExecute = $false
            CreateNoWindow = $true
        }

        # 開始進程
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $processInfo
        $process.Start() | Out-Null
        $process.WaitForExit()

        # 使用 MemoryStream 讀取二進制數據
        $memoryStream = New-Object System.IO.MemoryStream
        try {
            $process.StandardOutput.BaseStream.CopyTo($memoryStream)
            $binaryOutput = $memoryStream.ToArray()

            # 返回二進制數據
            return $binaryOutput
        } finally {
            $memoryStream.Dispose()
        }
    }
} # Write-Host (Invoke-CommandAndGetBinaryOutput "openssl rand -out - 8")

使用範例就是

Write-Host (Invoke-CommandAndGetBinaryOutput "openssl rand -out - 8")

拿到的是二進位數值,這個不要隨便亂成任意編碼(UTF-8)會丟失信息的。

想要安全的攜帶大概就是走base64的路子了,轉法下面有。




驗證

就能拿到正確的數值了,不過 OpenSSL 只有簽名的時候結果才是固定的這邊有點難驗證就是了,大概寫一下可驗證的辦法

走檔案路線的不會受到影響結果一定是對的

# 調用 OpenSSL 進行簽名 (走檔案路線)
$toSignData | Set-Content $toSignDataPath -NoNewline
openssl dgst -sha512 -sign $privatekeyPath -binary -out $signaturePath $toSignDataPath
$base64Signature = [System.Web.HttpServerUtility]::UrlTokenEncode([System.IO.File]::ReadAllBytes($signaturePath)) -replace('[012]$')

然後是副程式的用法

$toSignData | Set-Content $toSignDataPath -NoNewline
$bytes = Invoke-CommandAndGetBinaryOutput "OpenSSL dgst -sha512 -binary -sign `"$privatekeyPath`" `"$toSignDataPath`""
$base64Signature = [System.Web.HttpServerUtility]::UrlTokenEncode($bytes) -replace('[012]$')

寫個大概而已自己在琢磨一下 OpenSSL 怎麼使用




2024年4月27日 星期六

利用 Curl.exe 提交 Box 的 JWT 請求獲取 Access Token

利用 Curl.exe 提交 Box 的 JWT 請求獲取 Access Token

這邊比較難搞的是JWT,比較無腦一點就是直接載人家函式來弄,編碼之後會變成一長串文字,由Post送出去呼叫伺服器的API。


獲取 Box JSON File

請從BOX建立應用程式下載JWT認證Json私鑰檔案

$config 是直接從BOX上抓下來的Json檔案,他格式大概會長這樣子

{
  "boxAppSettings": {
    "clientID": "clientID",
    "clientSecret": "clientSecret",
    "appAuth": {
      "publicKeyID": "publicKeyID",
      "privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nSTRING\n-----END ENCRYPTED PRIVATE KEY-----",
      "passphrase": "secret_passphrase"
    }
  },
  "enterpriseID": "enterpriseID"
}


打包成JWT令牌

一個完整的JWT是由三個部分的base64url組成,分別是 "$heder.$payload.$Signature"

這部分請參考這份代碼獲取 $assertion

hunandy14/PsJwt (github.com)



向伺服器發送JWT請求

$assertion 是完整的 JWT 令牌,包含簽名後的字串

要快速驗證內容有工具網站可以看 JSON Web Tokens - jwt.io



利用 Invoke-RestMethod 請求 AccessToken

    # Generate BoxToken Request
    $irmParams = @{
        Uri = 'https://api.box.com/oauth2/token'
        Method = 'Post'
        Body = @{
            grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
            assertion = "$tokenData.$signature"
            client_id = $config.boxAppSettings.clientID
            client_secret = $config.boxAppSettings.clientSecret
        }
        ContentType = 'application/x-www-form-urlencoded'
    }; if ($env:HTTP_PROXY) { $irmParams['Proxy'] = $env:HTTP_PROXY }

    # Request AccessToken
    try {
        $response = Invoke-RestMethod @irmParams
    } catch {
        Write-Error $PSItem.Exception.Message -ea 1
    }

    # Check AccessToken
    $response



利用外部程序 curl.exe 請求 AccessToken 

下面是用 PowerShell 寫的 Curl 請求代碼。這邊要注意一個坑是如果沒有加上 .exe 的話預設會呼叫內建的指令,內建會直接回傳 PowerShell 物件而不是文字,然後參數用法有不同不能直接換過去。

function RequestBoxToken {
    param (
        [string]$Assertion,
        [string]$ConfigPath
    )
    # Read configuration from JSON file
    $configContent = Get-Content $ConfigPath -Raw
    $config = ConvertFrom-Json $configContent

    # Generate Request queryString
    $url = 'https://api.box.com/oauth2/token'
    $proxyUrl = $env:http_proxy
    $response = curl.exe `
        -x $proxyUrl `
        -X POST $url `
        -d 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' `
        -d "assertion=$Assertion" `
        -d "client_id=$($config.boxAppSettings.clientID)" `
        -d "client_secret=$($config.boxAppSettings.clientSecret)"
    # RequestBoxAccessToken
    $response = ConvertFrom-Json $response
    return $response
}



Python的請求方法 

import requests
import os
import json

def request_box_token(assertion, config_path):
    # 讀取設定檔
    with open(config_path, 'r') as file:
        config = json.load(file)

    url = 'https://api.box.com/oauth2/token'

    # 設置代理
    http_proxy = os.getenv('http_proxy')
    https_proxy = os.getenv('https_proxy')
    proxies = {}
    if http_proxy:
        proxies['http'] = http_proxy
    if https_proxy:
        proxies['https'] = https_proxy

    payload = {
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion': assertion,
        'client_id': config['boxAppSettings']['clientID'],
        'client_secret': config['boxAppSettings']['clientSecret']
    }
    response = requests.post(url, data=payload, proxies=proxies if proxies else None)
    return response.json()

# 使用方法
assertion = '你的assertion'
config_path = 'config.json'  # 設定檔路徑
response = request_box_token(assertion, config_path)
print(response)




官方SDK請求方法

import json
from boxsdk import Client, JWTAuth

# 載入官方 JSON 檔案
with open('config.json') as json_file:
    config = json.load(json_file)

# 創建 JWTAuth 認證對象
auth = JWTAuth(
    client_id=config['boxAppSettings']['clientID'],
    client_secret=config['boxAppSettings']['clientSecret'],
    enterprise_id=config['enterpriseID'],
    jwt_key_id=config['boxAppSettings']['appAuth']['publicKeyID'],
    rsa_private_key_data=config['boxAppSettings']['appAuth']['privateKey'].replace('\\n', '\n'),
    rsa_private_key_passphrase=config['boxAppSettings']['appAuth']['passphrase']
)

# 獲取 Access Token
access_token = auth.authenticate_instance()
print(f"access_token: {access_token}")

# 使用 Access Token 創建 Box Client
client = Client(auth)

# 驗證登錄並獲取當前用戶信息
current_user = client.user().get()
print('Authenticated as:', current_user.name)


這個只要拿到 json 檔案並且審核通過後就可以直接拿來用了。至於如何通過 proxy 可以參考這一篇的說明 
CHG: 利用 Curl.exe 提交 Box 的 JWT 請求獲取 Access Token (charlottehong.blogspot.com)



PowerShell 如何生成時間戳記 相對於 1970-01-01 的秒數

PowerShell 如何生成時間戳記 相對於 1970-01-01 的秒數

在做JWT呼叫API相關的事情,被這時間戳記搞了一會

先說Python上的時間戳記,這東西不用太管直接出來就是正確的

import time
round(time.time())

但是如果要手動實現這個功能就會牽涉到相對時間跟時區的問題了

這裡關鍵就是 Get-Date '1970-01-01 00:00:00' 已經是絕對時間了就不能在加入時差運算了,原本兩邊都加了算出來奇怪怎麼對不上

# 獲取 PowerShell 的時間戳
$TimeStamp1 = [int][Math]::Round(((Get-Date).ToUniversalTime() - (Get-Date '1970-01-01 00:00:00')).TotalSeconds)
Write-Host "PowerShell: $TimeStamp1"

# 獲取 Python 的時間戳
$TimeStamp2 = & python -c "import time; print(round(time.time()))"
Write-Host "Python    : $TimeStamp2"

# 時差
($TimeStamp1-$TimeStamp2)/60/60

-

2024年4月19日 星期五

Pnp.PowerShell 如何安裝 與 基本用法

Pnp.PowerShell 如何安裝 與 基本用法

經常要測試,每次都從頭來有點煩了把常見的語法整合起來



安裝

安裝有分好幾個版本,比較要注意的是從2版開始必須要 pwsh7 才能動,環境不允許的話只能裝舊版的。

各版本可以從這裡點出來
powershell/README.md at dev · pnp/powershell (github.com)


夜間最新版

Install-Module -Name PnP.PowerShell -AllowPrerelease

最新穩定版 (PowerShell Gallery | PnP.PowerShell 2.4.0)

Install-Module -Name PnP.PowerShell -RequiredVersion 2.4.0

舊版

Install-Module PnP.PowerShell -RequiredVersion 1.12.0 -Force


安裝好之後可以通過這個代碼檢查

Get-Module -ListAvailable PnP.PowerShell


基於版本問題,建議在代碼中把這段放進去開頭,可以少走不少彎路

function GetDateString { $((Get-Date).Tostring("yyyy/MM/dd HH:mm:ss.fff")) }
$Module = (Get-Module -ListAvailable PnP.PowerShell)[0]
if (!$Module) { Write-Error "The module 'PnP.PowerShell' is not installed." -EA 1 }
$ver = "PowerShellVersion $($PSVersionTable.PSVersion), PnP.PowerShellVersion $($Module.Version)"
Write-Host "[$(GetDateString)] $ver" -ForegroundColor DarkGray

其中 Get-Module 有使用 [0] 取第一個,是因為有可能同時存在多個版本



登入

登入有好幾種方法,先介紹文字介面的

$SiteUrl = "https://chg.sharepoint.com/sites/Maple"
$User    = "UserName@chg.onmicrosoft.com"
$PWord   = "PassWord"
$PWord = $PWord | ConvertTo-SecureString -AsPlainText -Force
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $PWord
Connect-PnPOnline -Url $SiteUrl -Credentials $Credential

對於如何合理的儲存登入情報,可以參考這個專案https://github.com/hunandy14/ConvertFrom-Env 



這樣就登入了,再來可以用這個測試現在登入哪個網站

# 目前登入的用戶
(Get-PnPConnection).PSCredential

# 目前登入的網站
Get-PnPWeb


第二種方式是通過UI介面來登入

.Net登入窗口

$SiteUrl = "https://chg.sharepoint.com/sites/Maple"
Connect-PnPOnline -Url $SiteUrl -Interactive

外部瀏覽器登入窗口

$SiteUrl = "https://chg.sharepoint.com/sites/Maple"
Connect-PnPOnline -Url $SiteUrl -UseWebLogin




上傳檔案

這邊要注意一點是斜線的方向,一開始不知道弄了好一會兒。


先用預設的資料夾測試,這個是創建SharePoint時預設會建立的文件庫。

# 獲取預設資料夾
Get-PnPFolder -Url "/Shared Documents"

# 獲取預設資料夾中的子資料夾
Get-PnPFolder -Url "/Shared Documents/File"


創建資料夾

try { # 檢查資料夾是否存在
    Get-PnPFolder -Url "/Shared Documents/File" -ErrorAction Stop
} catch { # 如果資料夾不存在,則建立它
    Add-PnPFolder -Name "File" -Folder "Shared Documents"
}


上傳文件

Add-PnPFile -Path ".\README.md" -Folder "Shared Documents/File"


查看文件

Get-PnPFile -Url "Shared Documents/File/README.md"



2024年3月3日 星期日

【已解決】安全性更新 KB5034441 無法安裝 0x80070643 錯誤

【已解決】安全性更新 KB5034441 無法安裝 0x80070643 錯誤

針對這次更新官方的說明,裡面明確的提到這次造成災難的主因:
KB5034441: 適用於 Windows 10 版本 21H2 和 22H2 的 Windows 修復環境更新

此更新需要修復磁碟分割區中有 250 MB 的可用空間才可成功安裝。 如果修復磁碟分割區沒有足夠的可用空間,此更新將會失敗。


回到正題,正常來說RE分區太小會自動生成新的變成兩個,但這次似乎並沒有自動生成而是直接跳錯了。

這次的問題不外乎就幾個三情況

  1. 不明原因RE分區被關掉 (單純重啟即可)
  2. 手癢把RE分區砍了 (要找其他電腦補檔案)
  3. 本次更新 KB5034441 包含900M的RE映像檔,RE分區容量不足導致的
    (有能力的話用分區軟體直接調大RE分區也可)

以下代碼請用管理員打開 PowerShell 後貼上執行 (命令提示字元不行)


執行過程可以參考這部影片




步驟1 重新啟用RE分區

先看一下啥狀況 (若已經是啟用狀態直接跳步驟3)

reagentc /info

如果是關閉狀態重啟看看 (某些情況卡BUG可能需要兩次)

reagentc /enable

出現 REAGENTC.EXE: 找不到 Windows RE 映像。 的話繼續步驟2補檔案
成功啟用或是本來就是啟用狀態的話,直接跳到步驟3就行了



步驟2 修復RE映像檔

無法重啟且這個路徑底下沒有 winre.wim 映像檔

C:\windows\system32\recovery

(這檔案可能是隱藏的記得打開檢視隱藏檔)

如果沒有檔案,參考這裡文末提供的檔案,放到上述的位置即可
https://charlottehong.blogspot.com/2018/02/windows-re.html



步驟3 重新生成修復分區

先把原本的刪除乾淨,參考的是這篇文章的工具

執行這段代碼,會自動刪除當前使用中的修復分區,並合併到前方分區

irm bit.ly/EditRecovery|iex; Remove-RecoveryPartition -Current -Merage -ForceEnable




然後接下來需要重新生成RE分區,參考這篇文章的工具

執行這段代碼,從C曹壓縮1G出來生成修復分區並重新啟用RE系統

irm bit.ly/EditRecovery|iex; New-RecoveryPartition -Size 1280MB -ReEnableRecovery


生成1G的修復分區之後就可以正常更新了。




錯誤應對

如果是舊系統一路升級上來的,似乎有一定概率會遇到一個問題是,C曹無法被壓縮



具體的原因是因為系統檔案正好被寫在C曹的結尾處,以至於磁碟管理內建的功能無法壓縮磁碟,這個只能用第三方的軟體處理,而且還必須要有第二個系統才能處理

遇到的話比較推薦的做法是刪除修復分區後(也就是執行第二個指令後看到上圖的錯誤時),直接啟用RE系統就好


啟用RE系統

reagentc /enable; reagentc /info


這時候由於沒有修復分區,系統會直接用C槽來頂替,RE系統的映像檔會展開到C槽底下。結果而言RE系統可以正常動作,更新也可以正常更新。




微軟後期處理

2024-03-30 更新微軟給出了解法,不過可能需要有一點底子才能知道怎麼做。

KB5034957:在已部署的裝置上更新 WinRE 磁碟分區,以解決 CVE-2024-20666 中的安全性弱點 - Microsoft 支援服務


另一個解法是就不解決了直接使用微軟的工具隱藏掉更新,不過漏洞還是依然存在謹慎使用,或是相對應的選擇關閉RE系統避開這個洞。