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.

It looks like a normal landing page, but submitting a query didn’t do anything.
So I inspected the page source.

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:
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

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:

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.

I dropped that wrapped link into the url parameter, and that did it. The response contained the 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.