TL;DR
To see the file names, use git ls-tree -r stash^3; to see their contents, use git show stash^3.
Long
How can I show the actual contents of the stash, including the untracked files?
The untracked files usually aren't in the stash. There is an exception—and you're using it, so I'll get to that in a moment. But showing those files is tricky. Using them is even harder. The git stash front end needs some work here, in my opinion.
First, though, it's important to understand how Git works internally. A Git repository is primarily a pair of databases. One—the biggest one, almost always—holds commits and other Git objects. A smaller database holds names: branch names like master, tag names like v2.1, remote-tracking names like origin/master, and so on. Branch names and tag names and the like are specific forms of the general purpose ref or reference. A ref holds a single Git hash ID, most commonly a commit hash ID.1
Each commit itself can also hold one or more previous-commit hash IDs. These are the parents of the commit. For normal (non-stash) commits, these usually form a nice simple chain: the last commit remembers, as its parent, the second-to-last. That second-to-last commit remembers its parent—the last commit's grandparent—and the grandparent remembers another parent and so on. A commit with two parents is a merge commit, by definition.
So, a branch name simply holds the hash ID of the commit we'd like to say is the last commit in / on / of that branch. Multiple names can select the same hash ID, and/or a hash ID can be reachable from some branch by starting at its tip commit and working backwards. So commits, in Git, are often on more than one branch.
The commits themselves contain snapshots of all of your (tracked) files. Git makes them by writing the tracked files into Git's index and then using git write-tree, which writes the files into internal tree objects, followed by git commit-tree, which writes a commit object using the tree(s) written by git write-tree. So all commits have their origin in the index. And, any file in the index is tracked, by definition. So this gets us to a bit of a puzzle.
1Branch names and remote-tracking names are required to hold only commit hash IDs; tag names are more flexible.
The stash
The special ref refs/stash, if it exists, points to one commit. That is the stash@{0} commit. (Its reflog entries in stash@{1} on up also point to one commit each.) So the stash, when it exists, consists of commits. These commits are on no branch:2 they're found through refs/stash instead.
A normal stash has the form of a merge commit, but not the same substance. What git stash does is to make two commits, using the very low-level git write-tree and git commit-tree method described above. The first such commit, based on whatever is in your index when you run git stash save or git stash push, is easy: git write-tree already just writes whatever is in the index, so the two commands, put together, make this index commit, which I call i (and the git stash documentation calls I).
The second commit is trickier, but essentially, what git stash does is run git add -u (though without actually using git add -u and thereby introducing bugs in various versions of git stash, some of which in some cases produce the wrong work-tree commit). This updates the index so that it holds all tracked files as of their state in the work-tree. Then git write-tree followed by git commit-tree makes a good snapshot of your work-tree, minus of course any untracked files.
Because git stash is using the low-level commands, it can make the work-tree commit—I call it w—with any set of parents it chooses. It makes this commit with both i and HEAD as its two parents:
...--o--o
|\
i-w
The i commit looks like any ordinary commit, and the w commit resembles a merge. It's not actually a merge—it's just a snapshot of your work-tree, as added to the index—but it has as its first parent the tip commit of your current branch, and as its second parent, commit i.
After making this stash, git stash save does a git reset --hard, so that your index and work-tree match HEAD. The HEAD itself never moves, and untracked files weren't saved and are unaffected.
When you do a git stash save -u or git stash save -a, however, Git makes a third commit, which I call u. This third commit uses an index that is cleared of all tracked files, and is then loaded with some or all of your untracked files, as if by git add --force on specific file names.3 The set of files that go into this third commit depends on whether you used -u or -a: -u enumerates untracked files that are not ignored, and -a enumerates all untracked files, even if they are ignored. Git copies those files into the (well, an) index and makes this u commit, with no parent at all, before it makes the final w commit. Then it makes the w commit with u as its third parent:
...--o--o
|\
i-w
/
u
Thus, w^1 is the commit at the tip of the branch, w^2 is the index commit i, and w^3 is the u commit. w continues to resemble a merge commit—this time, an octopus merge—but its snapshot is the same as for any two-commit stash.
Having made a u commit, git stash save or git stash push now removes from your work-tree all of the files that are in the u commit. If some files in u are also ignored, they're still removed.
Applying a three-commit stash fails if Git cannot extract the u commit into your current work-tree. So it's definitely useful to know what is in this third u commit. But there's no git stash ____ (fill in the blank with a verb) to show whether commit u even exists, much less what is in it. We must therefore fall back on lower-level Git commands.
In particular, since u is a root commit, git show will diff it against the empty tree. You can use git show --name-only or git ls-tree -r to get the list of files, if you don't want a full diff. To name commit u, we can name any stash commit—any of the w commit objects pointed to by refs/stash or one of the reflog entries for it—and add the ^3 suffix to mean third parent. If the stash has only w and i, the ^3 will fail: there is no third parent, and hence nothing to show.
2You can, if you like, actually get them onto a branch, but the result is ... ugly at best.
3Internally, git stash uses a temporary index instead of the real / main index, to make this easier. It does that for the w commit too. While there is one special index, the index, that tracks your work-tree, you can create a temporary index at any time, put its path name into GIT_INDEX_FILE, and use Git commands with that temporary index instead of using the distinguished index. That's handy for any command that needs to create a commit—which requires using the index—that doesn't want to disturb the index in the process.