Introduction
This challenge asks us to connect to an webpage with a base64 encoded message. If we try to decode the message manually, the decoded message ends up either in a reversed ELF (Executable and Linkable Format) binary or more base64 to be decoded.
Trying this multiple times, it becomes apparent that the challenge reverses an ELF binary, encodes it one or more times in base64 and sends it to us.
If we run the executable, we are given a string which we must send to the challenge endpoint through the r
HTTP GET query parameter. The real challenge is to do this within the few seconds before the server resets the challenge.
Exploration
Let’s start by fetching the contents of the challenge URI.
resp, err := http.Get(uri)
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
To parse the webpage, we will use the goquery
package.
We create a new goquery document from the response body.
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatalln(err)
}
From our initial exploration, we know that the challenge
data can be extracted from the HTML div
element with the
“message” class.
We use a matcher that matches a single, that is, the first occurence of the aforementioned element and extract the text inside the element.
message := doc.FindMatcher(goquery.Single(".message")).Text()
The raw text has a few leading and trailing lines that are not useful for us. We will split the lines and take the one after the first two lines.
message = strings.Split(message, "\n")[2]
We will wipe any newlines, spaces and tabs before further processing. To do this, we will write a small helper function like so:
func WipeSet(s, set string) string {
for _, char := range set {
s = strings.Replace(s, string(char), "", -1)
}
return s
}
and utilize the function in the main function as:
challenge := WipeSet(message, "\n\t ")
Next, to decode the challenge base64 itself, we initalize a slice to store the raw decoded bytes.
var Bytes []byte
Since we don’t know how many levels the binary has been encoded in base64, we begin with an infinite loop.
for {
// until we break out of the loop
}
We use the base64 standard library to decode the string.
Bytes, err = base64.StdEncoding.DecodeString(challenge)
if err != nil {
log.Fatal(err)
}
If the decoded bytes end with the reversed ELF header, we can stop iterating and just reverse the bytes to yield the original executable file.
if bytes.HasSuffix(Bytes, []byte{0x46, 0x4c, 0x45, 0x7f}) {
for i, j := 0, len(Bytes)-1; i < j; i, j = i+1, j-1 {
Bytes[i], Bytes[j] = Bytes[j], Bytes[i]
}
break
}
Otherwise, we convert the bytes to string for the next round of decoding.
challenge = string(Bytes)
This marks the end of the repeated decoding in the loop.
We write the binary to a file called “exe” with the read, write and executable permissions for our user (0o700
).
if err := ioutil.WriteFile("exe", Bytes, 0o700); err != nil {
log.Fatal(err)
}
The proof of work is the output generated by running the binary.
secret, err := exec.Command("./exe").Output()
if err != nil {
log.Fatal(err)
}
After wiping any newlines and formatting the url with the r parameter as the secret, we set the request off.
flagPage, err := http.Get(
fmt.Sprintf("%s?r=%s", uri, WipeSet(string(secret), "\n")),
)
if err != nil {
log.Fatalln(err)
}
defer flagPage.Body.Close()
Now that we have the flag page we can create a new goquery document from this response’s body.
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
Finally, we can print the text in the “alert-info” div, which is the flag.
fmt.Println(doc.FindMatcher(goquery.Single(".alert-info")).Text())
Here is the code in all its entirety.
package main
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/PuerkitoBio/goquery"
"io/ioutil"
"log"
"net/http"
"os/exec"
"strings"
)
const uri = "http://challenges.ringzer0team.com:10015/"
func WipeSet(s, set string) string {
for _, char := range set {
s = strings.Replace(s, string(char), "", -1)
}
return s
}
func main() {
resp, err := http.Get(uri)
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatalln(err)
}
message := doc.FindMatcher(goquery.Single(".message")).Text()
message = strings.Split(message, "\n")[2]
challenge := WipeSet(message, "\n\t ")
var Bytes []byte
for {
Bytes, err = base64.StdEncoding.DecodeString(challenge)
if err != nil {
log.Fatal(err)
}
if bytes.HasSuffix(Bytes, []byte{0x46, 0x4c, 0x45, 0x7f}) {
for i, j := 0, len(Bytes)-1; i < j; i, j = i+1, j-1 {
Bytes[i], Bytes[j] = Bytes[j], Bytes[i]
}
break
}
challenge = string(Bytes)
}
if err := ioutil.WriteFile("exe", Bytes, 0o700); err != nil {
log.Fatal(err)
}
secret, err := exec.Command("./exe").Output()
if err != nil {
log.Fatal(err)
}
flagPage, err := http.Get(
fmt.Sprintf("%s?r=%s", uri, WipeSet(string(secret), "\n")),
)
if err != nil {
log.Fatalln(err)
}
defer flagPage.Body.Close()
doc, err = goquery.NewDocumentFromReader(flagPage.Body)
fmt.Println(doc.FindMatcher(goquery.Single(".alert-info")).Text())
}