Git Rekt #2 – Steghide

A while ago at our CTF meetup we were talking about the exploit exploit and got talking about other common CTF tools and what could be done with those. And since I particularly hate stego, I’m taking a look at steghide today.
Credit goes to eibwen who noticed that there’s no sanitization going on for the extraction filename.

Getting Started

Before anything else, it’s probably a good idea to check if the software is actually vulnerable. So let’s take a quick dive into the steghide source code.
What happens if we extract a file from it? Let’s just run it to see the output.

Normal usage output of steghide

Now we know that when a file is extracted something like “wrote extracted data to” is printed, so we can search for the string and find where it’s used in the source code.
This quickly points to Session.cc, where we find the output we were looking for on line 94.
Shortly before that the embdata objects data is written to a file with name fn.

BinaryIO io (fn, BinaryIO::WRITE) ;
std::vector<BYTE> data = embdata->getData() ;
for (std::vector<BYTE>::iterator i = data.begin() ; i != data.end() ; i++) {
	io.write8 (*i) ;
}
io.close() ;

if (printdone) {
	VerboseMessage vdone (_(" done")) ;
	vdone.printMessage() ;
}

if (Args.Verbosity.getValue() < VERBOSE) {
	if (fn != "") {
		Message m (_("wrote extracted data to \"%s\"."), fn.c_str()) ;
		m.printMessage() ;
	}
}

The data being written is probably just the file contents that have previously been embedded with steghide. But if you’ve used steghide before you know that it strips the path and just uses the filename, but there doesn’t seem to be any sanitization going on for fn at least in this section of code, so let’s see where it comes from.

else {
	// write extracted data to file with embedded file name
	myassert (Args.ExtFn.getValue() == "") ;
	fn = embdata->getFileName() ;
	if (fn.length() == 0) {
		throw SteghideError (_("please specify a file name for the extracted data (there is no name embedded in the stego file).")) ;
	}
}

The highlighted line from Session.cc shows the filename fn just being assigned from embdata->getFileName(). This object was also used for the file contents, so it’s probably worth checking out the EmbData type.
In EmbData.cc a few things jump out right away.

First there is a stripDir method; let’s see where it is called.
A quick search through the source code only yields one spot inside EmbData::getBitString in EmbData.cc. This method is only called when embedding an image, but isn’t used when extracting. So it looks like we’ve hit the JACKPOT.

	compr.append (hash.getHashBits()) ;
}
	
compr.append (stripDir (FileName)) ;
compr.append ((BYTE) 0, 8) ; // end of fileame

Secondly: The filename seems to just be read from the embedded data.
This in combination with the directory not being stripped when extracting means we are going to be able to embed arbitrary filenames that steghide will then happily (try to) extract to.

// read filename
char curchar = '\0' ;
FileName = "" ;
do {
	curchar = (char) plain.getValue (pos, 8) ;
	if (curchar != '\0') {
		FileName += curchar ;
	}
	pos += 8 ;
} while (curchar != '\0') ;

Simple removing the call to stripDir when building the file and recompiling steghide gives the desired results. But this is only the first step. The picture below shows embedding /etc/passwd with our modified steghide and extracting it with the original steghide, you can see that it tries to write to /etc/passwd.

steghide tries to extract to /etc/passwd

Improving The Payload

Looking at the current payload it’s rather unelegant. It’ll be very obvious that data was extracted to an unintended place and we’re being prompted if the file already exists.
There’s not much we can do about being prompted. But the output is just a printf so maybe there’s something that can be done with ANSI escape codes.

To test this I’ve made the following change in Embedder.cc

EmbData embdata (EmbData::EMBED, Args.Passphrase.getValue(), "/tmp/test\x1b[1000Dcould not extract any data with that passphrase!") ;

\x1b[1000D is an ANSI escape sequence that will move the cursor left a thousand times and then start overwriting whatever was there before. This should overwrite the message that was printed before the filename if it succeeds.

If it works it should create a file in /tmp with a terrible name, but the console should read as if nothing was extracted. Let’s recompile and give it a go.

Well, that looks a lot better, except for the trailing stuff that’s being printed as well.

Luckily there are more ANSI escape codes, and some of them let you set the text color as well. So by adding \x1b[30m to the end of our filename we can make the rest of the text black. Helpfully bash will reset the color when the command execution ends, so that the effect isn’t easily noticeable.

EmbData embdata (EmbData::EMBED, Args.Passphrase.getValue(), "/tmp/test\x1b[1000Dcould not extract any data with that passphrase!\x1b[30m") ;
Voila

When the file doesn’t exist we get what looks like exactly the same output on the command line as if extraction failed. If if does exist we get the second case where it looks like we get a few blank lines.
The second case looks weird and definitely a little suspicious, but much better than before and I’m not sure if it can be improved further easily.

Conclusion

Now if you can get somebody to run steghide on an image you control and they are running as root, you can just install a cron job or whatever to get code execution through steghide. And they may not even notice.

Good thing Kali doesn’t have people running as root by default anymore or script kiddies everywhere could be in danger!

Leave a Reply

Your email address will not be published. Required fields are marked *