Overview
Noogle - web

Noogle - web

A Google-like search UI with a twist. Can you pull the flag?

January 5, 2026
3 min read

Challenge description: Last week I decided to create my own search engine. It was kinda hard so I piggybacked on another one. I also tried something on port 8000.

When I opened the challenge instance, I was greeted with a minimal search page that looked suspiciously familiar. Meet Noogle.

Noogle-Intro

It looks like a normal landing page, but submitting a query didn’t do anything.

So I inspected the page source.

Noogle-inspected

The UI was fine, so the next stop was the client-side script: search-query.js.

Javascript deobfuscation

Here’s the file as served (obfuscated):

const _0x43a57f = _0x22f9;
25 collapsed lines
(function(_0x3d7d57, _0x426e05) {
const _0x16c3fa = _0x22f9
, _0x318780 = _0x3d7d57();
while (!![]) {
try {
const _0x52f259 = -parseInt(_0x16c3fa(0x11d)) / 0x1 * (-parseInt(_0x16c3fa(0x10c)) / 0x2) + -parseInt(_0x16c3fa(0x11c)) / 0x3 + parseInt(_0x16c3fa(0x11b)) / 0x4 + parseInt(_0x16c3fa(0x120)) / 0x5 + -parseInt(_0x16c3fa(0x119)) / 0x6 + parseInt(_0x16c3fa(0x107)) / 0x7 + parseInt(_0x16c3fa(0x10e)) / 0x8;
if (_0x52f259 === _0x426e05)
break;
else
_0x318780['push'](_0x318780['shift']());
} catch (_0x409286) {
_0x318780['push'](_0x318780['shift']());
}
}
}(_0x1be0, 0x9799d),
document[_0x43a57f(0x116)](_0x43a57f(0x10d), function() {
const _0x55ef70 = _0x43a57f;
document[_0x55ef70(0x114)](_0x55ef70(0x11a))[_0x55ef70(0x116)](_0x55ef70(0x111), function(_0x3cfad3) {
const _0x4605e1 = _0x55ef70;
_0x3cfad3['preventDefault']();
const _0x44eac5 = _0x4605e1(0xff) + document['getElementById'](_0x4605e1(0x122))[_0x4605e1(0x115)];
document[_0x4605e1(0x114)](_0x4605e1(0x110))['innerHTML'] = '',
sendData(_0x44eac5);
});
}));
function sendData(_0x12b935) {
const _0x109fde = _0x43a57f;
fetch('/api/getLinks', {
'method': _0x109fde(0x10f),
'headers': {
'Content-Type': _0x109fde(0x103)
},
'body': JSON['stringify']({
'url': _0x12b935
})
})[_0x109fde(0x112)](_0x18bc0d => {
const _0x36b6b9 = _0x109fde;
if (!_0x18bc0d['ok'])
throw new Error(_0x36b6b9(0x113));
return _0x18bc0d[_0x36b6b9(0x106)]();
}
)[_0x109fde(0x112)](_0x7dd058 => {
const _0x1611a0 = _0x109fde
, _0x226a83 = new DOMParser()
, _0x3f7bbe = _0x226a83['parseFromString'](_0x7dd058, _0x1611a0(0x109))
, _0x5d877b = _0x3f7bbe['querySelectorAll']('a')
, _0x296887 = _0x3f7bbe[_0x1611a0(0x11e)]('h3')
, _0x2de64a = [];
_0x5d877b[_0x1611a0(0x108)](_0x197c31 => {
const _0x3b394d = _0x1611a0;
try {
const _0xdc782 = _0x197c31[_0x3b394d(0x105)]('href');
_0xdc782[_0x3b394d(0x100)](_0x3b394d(0x118)) && _0x197c31[_0x3b394d(0x105)](_0x3b394d(0x117))[_0x3b394d(0x100)]('2ahUKE') && _0x2de64a[_0x3b394d(0x104)](_0xdc782);
} catch (_0x448110) {}
}
),
Promise[_0x1611a0(0x102)]([_0x2de64a, _0x296887])['then']( ([_0x436123,_0x1d19c4]) => {
const _0x1a11e5 = _0x1611a0;
for (let _0x20c585 = 0x0; _0x20c585 < _0x5d877b[_0x1a11e5(0x121)] && _0x20c585 < _0x1d19c4[_0x1a11e5(0x121)]; _0x20c585++) {
const _0x250c42 = _0x436123[_0x20c585]
, _0x364137 = _0x1d19c4[_0x20c585]['textContent']['trim']();
document['getElementById'](_0x1a11e5(0x110))[_0x1a11e5(0x101)] += _0x1a11e5(0x11f) + _0x250c42 + '\x22>' + _0x364137 + _0x1a11e5(0x10a);
}
}
);
}
)['catch'](_0x47beb6 => {
const _0x35dd36 = _0x109fde;
console['error'](_0x35dd36(0x10b), _0x47beb6);
}
);
}
function _0x22f9(_0x4bc5ac, _0x2b5e49) {
const _0x1be057 = _0x1be0();
return _0x22f9 = function(_0x22f90e, _0x3b583c) {
_0x22f90e = _0x22f90e - 0xff;
let _0x1e0f27 = _0x1be057[_0x22f90e];
return _0x1e0f27;
}
,
_0x22f9(_0x4bc5ac, _0x2b5e49);
}
function _0x1be0() {
const _0x5e3c7c = ['</a><br>', 'Error:', '12bqpdPx', 'DOMContentLoaded', '523808pUNNLD', 'POST', 'response', 'submit', 'then', 'Network\x20response\x20was\x20not\x20ok', 'getElementById', 'value', 'addEventListener', 'data-ved', 'url', '5955222CsWYxv', 'search-form', '2372928EQZrlc', '2500197shtdfI', '138601QPPhqz', 'querySelectorAll', '<a\x20href=\x22https://www.google.com', '1699145xzyuVK', 'length', 'search-item', 'https://www.google.com/search?q=', 'includes', 'innerHTML', 'all', 'application/json', 'push', 'getAttribute', 'text', '4317250nbyqNF', 'forEach', 'text/html'];
_0x1be0 = function() {
return _0x5e3c7c;
}
;
return _0x1be0();
}

After deobfuscating (not necessary), the interesting part boiled down to this:

function sendData(_0x12b935) {
const _0x109fde = _recursionfunction;
fetch('/api/getLinks', {
'method': 'POST',
'headers': {
'Content-Type': 'application/json'
},
'body': JSON['stringify']({
'url': _0x12b935
})
})['then'](res => {
const _0x36b6b9 = _0x109fde;
if (!res['ok'])
throw new Error(_0x36b6b9(0x113));
return res['text']();
}
)['then'](_0x7dd058 => {
const _0x1611a0 = _0x109fde
, _0x226a83 = new DOMParser()
, _0x3f7bbe = _0x226a83['parseFromString'](_0x7dd058, 'text/html')
, _all_a = _0x3f7bbe['querySelectorAll']('a')
, _all_h3 = _0x3f7bbe['querySelectorAll']('h3')
, _0x2de64a = [];
...

So the key endpoint is:

Terminal window
curl --path-as-is -i -s -k -X $'POST' \
-H $'Host: host' -H $'Content-Length: 45' -H $'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36' -H $'Content-Type: application/json' -H $'Accept: */*' -H $'Sec-GPC: 1' -H $'Accept-Language: en-US,en;q=0.5' -H $'Origin: http://34.141.76.145:31119' -H $'Referer: http://34.141.76.145:31119/' -H $'Accept-Encoding: gzip, deflate, br' -H $'Connection: keep-alive' \
--data-binary $'{\"url\":\"https://www.gogle.com/search?q=test\"}' \
$'http://host/api/getLinks'

The server fetches the provided URL and the client parses the returned HTML, pulling results out of Google’s response.

Playing around

Google-Response

When I started testing variations of the url parameter, I got a 400 Invalid URL with all type of variations. For example:

It looked like the backend was enforcing a strict prefix: "https[:]//www[dot]google[dot]com/<anything>". At this point I was stuck for a while, digging around for ways to stay within that constraint.

I already knew about a few Google open-redirect patterns, but many of them now land on a warning page:

google-redirection

Then I remembered something from email links: they’re often wrapped and include a data-saferedirecturl attribute (plus some extra parameters) pointing to the real destination.

Email-Inspect

I dropped that wrapped link into the url parameter, and that did it. The response contained the flag.

Flag

flag

Learnings

  • Strict URL allowlists are not a guarantee: if the server fetches URLs for you, the exact parsing rules matter a lot.
  • Look for legitimate redirect wrappers: sometimes the “allowed” domain already contains a safe-looking way to reference an external target.