There are a few things you need to be aware of, but it’s not all that difficult, even if you do it yourself instead of using a library as has been suggested.
The three main things I can think of are color order (RGB vs BGR), scanline padding, and top-to-bottom vs bottom-to-top data order.
The color order is easy. If the image comes out with the right picture, but the wrong colors, you probably have the red and the blue reversed, so just reverse those two to fix this.
I don’t remember seeing it in the documentation, but when you call glReadPixels, the data it creates always uses an even multiple of 4 bytes per line. So if you request GL_RGB format with GL_UNSIGNED_BYTE as your data type, each scanline in the returned buffer will have pad bytes added if they aren’t an even multiple of 4 bytes. So if your image is 99 pixels wide, the works out to 297 bytes per line, which rounds up to 300 bytes per line.
OpenGL treats the bottom left of your window as 0,0, so the first three bytes of the data returned from glReadPixels using the above format and data type will be the bottom left pixel. The BMP image format acutally uses this same order, so you shouldn’t have to do anything special there. In fact, I think the BMP format pads lines to the nearest 4 bytes also, so you can probably just use the buffer returned from glReadPixels unchanged (although you may have to adjust RGB vs BGR). It’s really easy to detect this, though. If your image turns out upside-down, then flip it.
Once you know what format you’re getting your data from OpenGL in, it’s just a matter of using it to create data in the appropriate image format. For BMP, this amounts to creating an appropriate header. For compressed formats, this probably involves using a library to perform the image compression. For a format like PPM or TGA, you just have to throw a header on the front of your data, and you’re done. This isn’t an appropriate place to be discussing the internals of individual image formats, though.