Skip to content

Commit

Permalink
Merge pull request #10 from anuraaga/token-string
Browse files Browse the repository at this point in the history
Use string type for current token and fingerprint
  • Loading branch information
jcchavezs authored Sep 13, 2022
2 parents 05da3bd + 39b01fb commit 6df74d7
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 91 deletions.
123 changes: 58 additions & 65 deletions sqli.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package libinjection

import (
"bytes"
"strings"
)

Expand All @@ -24,7 +23,7 @@ type sqliState struct {
current *sqliToken

// fingerprint pattern c-string, +1 form ending null
fingerprint []byte
fingerprint string

// |----------------------------------------|
// | |/**/ |--[start] |# |
Expand Down Expand Up @@ -84,7 +83,7 @@ func sqliInit(s *sqliState, input string, flags int) {
// single quote.
// ByteDouble ("), process pretending input started with a
// double quote.
func (s *sqliState) sqliFingerprint(flags int) []byte {
func (s *sqliState) sqliFingerprint(flags int) string {
s.reset(flags)
length := s.fold()

Expand All @@ -103,25 +102,28 @@ func (s *sqliState) sqliFingerprint(flags int) []byte {
s.tokenVec[length-1].category = sqliTokenTypeComment
}

fp := strings.Builder{}

for i := 0; i < length; i++ {
s.fingerprint = append(s.fingerprint, s.tokenVec[i].category)
}
c := s.tokenVec[i].category
// check for 'X' in pattern, and then
// clear out all tokens
//
// this means parsing could not be done
// accurately due to pgsql's double comments
// or other syntax that isn't consistent.
// Should be very rare false positive
if c == sqliTokenTypeEvil {
s.fingerprint = string(sqliTokenTypeEvil)
s.tokenVec[0].category = sqliTokenTypeEvil
s.tokenVec[0].val = string(sqliTokenTypeEvil)
return s.fingerprint
}

// check for 'X' in pattern, and then
// clear out all tokens
//
// this means parsing could not be done
// accurately due to pgsql's double comments
// or other syntax that isn't consistent.
// Should be very rare false positive
if bytes.ContainsAny(s.fingerprint, string(sqliTokenTypeEvil)) {
s.fingerprint = s.fingerprint[:0]
s.fingerprint = append(s.fingerprint, sqliTokenTypeEvil)

s.tokenVec[0].category = sqliTokenTypeEvil
s.tokenVec[0].val = [32]byte{sqliTokenTypeEvil}
fp.WriteByte(c)
}

s.fingerprint = fp.String()
return s.fingerprint
}

Expand Down Expand Up @@ -163,16 +165,10 @@ func (s *sqliState) merge(tokenA, tokenB *sqliToken) bool {
return false
}

// oddly annoying last.val + ' ' + current.val
var tmp [tokenSize]byte
copy(tmp[:], tokenA.val[:tokenA.len])
tmp[tokenA.len] = ' '
copy(tmp[tokenA.len+1:], tokenB.val[:tokenB.len])

length := tokenA.len + tokenB.len + 1
ch := s.lookupWord(sqliLookupWord, tmp[:length])
tmp := tokenA.val[:tokenA.len] + " " + tokenB.val[:tokenB.len]
ch := s.lookupWord(sqliLookupWord, tmp)
if ch != byteNull {
tokenA.assign(ch, tokenA.pos, length, string(tmp[:length]))
tokenA.assign(ch, tokenA.pos, len(tmp), tmp)
return true
}
return false
Expand Down Expand Up @@ -316,32 +312,32 @@ func (s *sqliState) fold() int {
case (s.tokenVec[left].category == sqliTokenTypeBareWord || s.tokenVec[left].category == sqliTokenTypeVariable) &&
s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis &&
( // TSQL functions but common enough to be column names
toUpperCmp("USER_ID", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("USER_NAME", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("USER_ID", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("USER_NAME", s.tokenVec[left].val[:s.tokenVec[left].len]) ||

// Function in MySQL
toUpperCmp("DATABASE", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("PASSWORD", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("USER", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("DATABASE", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("PASSWORD", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("USER", s.tokenVec[left].val[:s.tokenVec[left].len]) ||

// MySQL words that act as a variable and are a function

// TSQL current_users is fake_variable
// http://msdn.microsoft.com/en-us/library/ms176050.aspx
toUpperCmp("CURRENT_USER", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("CURRENT_DATE", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("CURRENT_TIME", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("CURRENT_TIMESTAMP", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("LOCALTIME", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("LOCALTIMESTAMP", string(s.tokenVec[left].val[:s.tokenVec[left].len]))):
toUpperCmp("CURRENT_USER", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("CURRENT_DATE", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("CURRENT_TIME", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("CURRENT_TIMESTAMP", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("LOCALTIME", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("LOCALTIMESTAMP", s.tokenVec[left].val[:s.tokenVec[left].len])):
// pos is the same
// other conversions need to go here... for instance
// password CAN be a function, coalesce CAN be a funtion
s.tokenVec[left].category = sqliTokenTypeFunction
continue
case s.tokenVec[left].category == sqliTokenTypeKeyword &&
(toUpperCmp("IN", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("NOT IN", string(s.tokenVec[left].val[:s.tokenVec[left].len]))):
(toUpperCmp("IN", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("NOT IN", s.tokenVec[left].val[:s.tokenVec[left].len])):
if s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis {
// got ... IN ( ... (or 'NOT IN')
// it's an operator
Expand All @@ -362,8 +358,8 @@ func (s *sqliState) fold() int {
// "foo" = LIKE(1,2)
continue
case s.tokenVec[left].category == sqliTokenTypeOperator &&
(toUpperCmp("LIKE", string(s.tokenVec[left].val[:s.tokenVec[left].len])) ||
toUpperCmp("NOT LIKE", string(s.tokenVec[left].val[:s.tokenVec[left].len]))):
(toUpperCmp("LIKE", s.tokenVec[left].val[:s.tokenVec[left].len]) ||
toUpperCmp("NOT LIKE", s.tokenVec[left].val[:s.tokenVec[left].len])):
if s.tokenVec[left+1].category == sqliTokenTypeLeftParenthesis {
// SELECT LIKE(...
// it's a function
Expand All @@ -385,7 +381,7 @@ func (s *sqliState) fold() int {
case s.tokenVec[left].category == sqliTokenTypeCollate && s.tokenVec[left+1].category == sqliTokenTypeBareWord:
// there are too many collation types.. so if the bareword has a "_"
// then it's TYPE_SQLTYPE
if bytes.ContainsRune(s.tokenVec[left+1].val[:], '_') {
if strings.ContainsRune(s.tokenVec[left+1].val[:], '_') {
s.tokenVec[left+1].category = sqliTokenTypeSQLType
left = 0
}
Expand Down Expand Up @@ -514,7 +510,7 @@ func (s *sqliState) fold() int {
s.tokenVec[left].category == sqliTokenTypeVariable ||
s.tokenVec[left].category == sqliTokenTypeString) &&
s.tokenVec[left+1].category == sqliTokenTypeOperator &&
string(s.tokenVec[left+1].val[:s.tokenVec[left+1].len]) == "::" &&
s.tokenVec[left+1].val[:s.tokenVec[left+1].len] == "::" &&
s.tokenVec[left+2].category == sqliTokenTypeSQLType:
pos -= 2
left = 0
Expand Down Expand Up @@ -607,7 +603,7 @@ func (s *sqliState) fold() int {
// if we get User(foo), then User is not a function
// This should be expanded since it eliminated a lot of false
// positives.
if toUpperCmp("USER", string(s.tokenVec[left].val[:s.tokenVec[left].len])) {
if toUpperCmp("USER", s.tokenVec[left].val[:s.tokenVec[left].len]) {
s.tokenVec[left].category = sqliTokenTypeBareWord
}
}
Expand Down Expand Up @@ -668,23 +664,25 @@ func (s *sqliState) tokenize() bool {
//
// return TRUE if SQLi, false otherwise
func (s *sqliState) blacklist() bool {
var fp []byte

length := len(s.fingerprint)
if length < 1 {
return false
}

fp = append(fp, '0')
fp := strings.Builder{}
fp.Grow(length + 1)

fp.WriteByte('0')
for i := 0; i < length; i++ {
ch := s.fingerprint[i]
if ch >= 'a' && ch <= 'z' {
ch -= 0x20
}
fp = append(fp, ch)
fp.WriteByte(ch)
}

return isKeyword(fp) == sqliTokenTypeFingerprint
return isKeyword(fp.String()) == sqliTokenTypeFingerprint
}

// Given a positive match for a pattern (i.e. pattern is SQLi), this function
Expand Down Expand Up @@ -789,8 +787,8 @@ func (s *sqliState) notWhitelist() bool {
// no opening quote, no closing quote
// and each string has data
// sos || s&s are string and operator || logic operator and string
switch {
case string(s.fingerprint) == "sos" || string(s.fingerprint) == "s&s":
switch s.fingerprint {
case "sos", "s&s":
if s.tokenVec[0].strOpen == byteNull &&
s.tokenVec[2].strClose == byteNull &&
s.tokenVec[0].strClose == s.tokenVec[2].strOpen {
Expand All @@ -803,22 +801,17 @@ func (s *sqliState) notWhitelist() bool {
}

return false
case string(s.fingerprint) == "s&n" ||
string(s.fingerprint) == "n&1" ||
string(s.fingerprint) == "1&1" ||
string(s.fingerprint) == "1&v" ||
string(s.fingerprint) == "1&s":
case "s&n", "n&1", "1&1", "1&v", "1&s":
// 'sexy and 17' not SQLi
// 'sexy and 17<18' SQLi
if s.statsTokens == 3 {
return false
}
case s.tokenVec[1].category == sqliTokenTypeKeyword:
if s.tokenVec[1].len < 5 || !toUpperCmp("INTO", string(s.tokenVec[1].val[:4])) {
// if it's not "INTO OUTFILE", or "INTO DUMPFILE" (MySQL)
// then treat as safe
return false
}
}
if s.tokenVec[1].category == sqliTokenTypeKeyword && (s.tokenVec[1].len < 5 || !toUpperCmp("INTO", s.tokenVec[1].val[:4])) {
// if it's not "INTO OUTFILE", or "INTO DUMPFILE" (MySQL)
// then treat as safe
return false
}
}

Expand All @@ -829,7 +822,7 @@ func (s *sqliState) checkFingerprint() bool {
return s.blacklist() && s.notWhitelist()
}

func (s *sqliState) lookupWord(lookupType int, word []byte) byte {
func (s *sqliState) lookupWord(lookupType int, word string) byte {
if lookupType == sqliLookupFingerprint {
if s.checkFingerprint() {
return 'X'
Expand Down Expand Up @@ -897,12 +890,12 @@ func (s *sqliState) check() bool {

// IsSQLi returns true if the input is SQLi
// It also returns the fingerprint of the SQL Injection as []byte
func IsSQLi(input string) (bool, []byte) {
func IsSQLi(input string) (bool, string) {
state := new(sqliState)
sqliInit(state, input, 0)
result := state.check()
if result {
return result, state.fingerprint
}
return result, []byte{}
return result, ""
}
6 changes: 3 additions & 3 deletions sqli_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ func toUpperCmp(a, b string) bool {
return a == strings.ToUpper(b)
}

func isKeyword(key []byte) byte {
func isKeyword(key string) byte {
return searchKeyword(key, sqlKeywords)
}

func searchKeyword(key []byte, keywords map[string]byte) byte {
upperKey := strings.ToUpper(string(key))
func searchKeyword(key string, keywords map[string]byte) byte {
upperKey := strings.ToUpper(key)

if val, ok := keywords[upperKey]; ok {
return val
Expand Down
22 changes: 11 additions & 11 deletions sqli_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func parseEolComment(s *sqliState) int {

func parseMoney(s *sqliState) int {
if s.pos+1 == s.length {
s.current.assignByte(sqliTokenTypeBareWord, s.pos, 1, '$')
s.current.assign(sqliTokenTypeBareWord, s.pos, 1, "$")
return s.length
}

Expand All @@ -48,14 +48,14 @@ func parseMoney(s *sqliState) int {
xlen := strLenSpn(s.input[s.pos+1:], s.length-s.pos-1, "abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
if xlen == 0 {
// hmm, it's "$" _something_ .. just add $ and keep going
s.current.assignByte(sqliTokenTypeBareWord, s.pos, 1, '$')
s.current.assign(sqliTokenTypeBareWord, s.pos, 1, "$")
return s.pos + 1
}

// we have $foobar?????
if s.pos+xlen+1 == s.length || s.input[s.pos+xlen+1] != '$' {
// not $foobar$, or fell off edge
s.current.assignByte(sqliTokenTypeBareWord, s.pos, 1, '$')
s.current.assign(sqliTokenTypeBareWord, s.pos, 1, "$")
return s.pos + 1
}

Expand All @@ -81,7 +81,7 @@ func parseMoney(s *sqliState) int {
}

func parseOther(s *sqliState) int {
s.current.assignByte(sqliTokenTypeUnknown, s.pos, 1, s.input[s.pos])
s.current.assign(sqliTokenTypeUnknown, s.pos, 1, s.input[s.pos:])
return s.pos + 1
}

Expand All @@ -90,12 +90,12 @@ func parseWhite(s *sqliState) int {
}

func parseOperator1(s *sqliState) int {
s.current.assignByte(sqliTokenTypeOperator, s.pos, 1, s.input[s.pos])
s.current.assign(sqliTokenTypeOperator, s.pos, 1, s.input[s.pos:])
return s.pos + 1
}

func parseByte(s *sqliState) int {
s.current.assignByte(s.input[s.pos], s.pos, 1, s.input[s.pos])
s.current.assign(s.input[s.pos], s.pos, 1, s.input[s.pos:])
return s.pos + 1
}

Expand All @@ -107,7 +107,7 @@ func parseHash(s *sqliState) int {
s.statsCommentHash++
return parseEolComment(s)
}
s.current.assignByte(sqliTokenTypeOperator, s.pos, 1, '#')
s.current.assign(sqliTokenTypeOperator, s.pos, 1, "#")
return s.pos + 1
}

Expand All @@ -128,7 +128,7 @@ func parseDash(s *sqliState) int {
s.statsCommentDDX++
return parseEolComment(s)
default:
s.current.assignByte(sqliTokenTypeOperator, s.pos, 1, '-')
s.current.assign(sqliTokenTypeOperator, s.pos, 1, "-")
return s.pos + 1
}
}
Expand Down Expand Up @@ -174,7 +174,7 @@ func parseBackSlash(s *sqliState) int {
s.current.assign(sqliTokenTypeNumber, s.pos, 2, s.input[s.pos:])
return s.pos + 2
}
s.current.assignByte(sqliTokenTypeBackslash, s.pos, 1, s.input[s.pos])
s.current.assign(sqliTokenTypeBackslash, s.pos, 1, s.input[s.pos:])
return s.pos + 1
}

Expand All @@ -189,7 +189,7 @@ func parseOperator2(s *sqliState) int {
return s.pos + 3
}

ch := s.lookupWord(sqliLookupOperator, []byte(s.input[s.pos:s.pos+2]))
ch := s.lookupWord(sqliLookupOperator, s.input[s.pos:s.pos+2])
if ch != byteNull {
s.current.assign(ch, s.pos, 2, s.input[s.pos:])
return s.pos + 2
Expand Down Expand Up @@ -321,7 +321,7 @@ func parseNumber(s *sqliState) int {

if pos-start == 1 {
// only one character read so far
s.current.assignByte(sqliTokenTypeDot, start, 1, '.')
s.current.assign(sqliTokenTypeDot, start, 1, ".")
return pos
}
}
Expand Down
Loading

0 comments on commit 6df74d7

Please sign in to comment.