Your understanding of setuid is correct. When you execute a program that has the suid bit, the process inherits the permissions of that program's owner. The piece of knowledge that you're missing is what the shell does after it's been invoked. Many popular implementations of sh drop privileges when they start up: they reset their effective UID to their real UID. This includes bash, dash, mksh and BusyBox sh, so on Linux you won't see anything else.
When the program runs a shell (e.g. by calling the system
function from the standard C library, or an equivalent in another language), the shell starts with elevated privileges but drops to ordinary privileges before it executes any user code. This mitigates exploits in setuid programs where the attacker only gets to run a shell command which wasn't intended to run with elevated privileges (e.g. because the shell command was buried in library code that the application programmer wasn't aware of).
This doesn't mitigate exploits where the attacker gets to run arbitrary code. It's just that to get a shell, you have to work a bit harder than calling system
. Arrange to call some different interpreter, e.g. run execve("/usr/bin/perl", "/usr/bin/perl")
(fork first if you don't want to terminate the parent program).