Exploit-Exercises.com's Nebula, that is.  I just spent a good 8 hours or so working through the levels there, and I'm pretty sure I took much longer than I should have.  In any case, there were a couple of things I was disappointed by: running "getflag" to get a flag (or otherwise being delivered a token) didn't provide you with anything to really validate what you were doing.  You can actually jump directly to any level (which is good when you reset your VM) but not so interesting for "progression" or the sense of accomplishment -- at least for me.

I did, however, learn 3 completely new things from this set of challenges.

1. You can change register values within gdb.
Let's say you have a binary built from the following code:

int main(int argc,char **argv) {
    if(getuid() == 0) {
        printf("Yes, you have root.");
    } else {
        printf("Nope, no root.");
    }
}

Other than getting root, can you get it to print "Yes, you have root?" Let's assume you can't edit or rebuild the binary, only execute it.

GDB to the rescue! Let's see what the code looks like in gdb:

$ gdb -q ./g
Reading symbols from /home/david/tmp/getuid/g...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x400550
(gdb) run
Starting program: /home/david/tmp/getuid/g 

Breakpoint 1, 0x0000000000400550 in main ()
(gdb) disass
Dump of assembler code for function main:
   0x000000000040054c :	push   %rbp
   0x000000000040054d :	mov    %rsp,%rbp
=> 0x0000000000400550 :	sub    $0x10,%rsp
   0x0000000000400554 :	mov    %edi,-0x4(%rbp)
   0x0000000000400557 :	mov    %rsi,-0x10(%rbp)
   0x000000000040055b :	callq  0x400420 
   0x0000000000400560 :	test   %eax,%eax
   0x0000000000400562 :	jne    0x400570 
0x0000000000400564 : mov $0x40063c,%edi 0x0000000000400569 : callq 0x400410 0x000000000040056e : jmp 0x40057a
0x0000000000400570 : mov $0x400650,%edi 0x0000000000400575 : callq 0x400410 0x000000000040057a : mov $0x0,%eax 0x000000000040057f : leaveq 0x0000000000400580 : retq End of assembler dump.

We can see the call to getuid @0x40055b, so we know the comparison we're interested in is right after that. test, not to be confused with cmp, does a logical and on the arguments (which, when they are the same, returns the value itself) and then compares to 0. So we want that test to see it as 0. Let's set a breakpoint there and see what eax gives us.

(gdb) break *0x400560
Breakpoint 2 at 0x400560
(gdb) c
Continuing.

Breakpoint 2, 0x0000000000400560 in main ()
(gdb) disass
...
=> 0x0000000000400560 :	test   %eax,%eax
...

At this point, there are two approaches we can take. We can either change the value of %eax to be 0 (so 0 & 0 == 0) or we can alter %rip (on 64-bit, use %eip on 32-bit) to main+24 which will get us inside the code that would've been skipped by the branch. Personally, I think changing eax is the more obvious, so let's do that and continue execution:

(gdb) set $eax=0
(gdb) c
Continuing.
Yes, you have root.
[Inferior 1 (process 6242) exited normally]

And we're done! Changing rip would work similarly as we can see here:

(gdb) set $rip=0x400564
(gdb) c
Continuing.
Yes, you have root.
[Inferior 1 (process 6961) exited normally]

2. Bash Case Modification
Did you know that bash can modify case when evaluating a variable? Neither did I, until today...

$ FOO='foo Bar Baz'
$ echo ${FOO^^}
FOO BAR BAZ
$ echo ${FOO^}
Foo Bar Baz
$ echo ${FOO,,}
foo bar baz
$ echo ${FOO~~}
FOO bAR bAZ

WTF? I guess bash is trying to displace tr and sed. (Don't forget about ${VAR/.ext/} and similar constructs.)

3. Arbitrary Python Code Executed when de-pickling

import pickle
import subprocess
 
class PickleLS(object):
  def __reduce__(self):
    args = (('/bin/ls',),)
    return (subprocess.Popen, args)
 
 
pickle.loads(pickle.dumps(PickleLS()))

This is a strong reminder to never unpickle untrusted data. Please, go use JSON, YAML, or (if you want to be "enterprisey") use XML. Pickle is not a data interchange format!

So, I'm on to protostar to see what more tricks the guys at Exploit-Exercises have up their sleeves.