So we have a shell, but no way to control it. Time to use dup2.
I've simplified this challenge a lot by including a call to dup2() within the vulnerable binary, but normally you would leak libc via the GOT and then use libc's dup2() rather than the PLT; this walkthrough is about the basics, so I kept it as simple as possible.
Duplicating File Descriptors
As we know, we need to call dup2(newfd, oldfd). newfd will be 4 (our connection fd) and oldfd will be 0 and 1 (we need to call it twice to redirect bothstdin and stdout). Knowing what you do about calling conventions, have a go at doing this and then caling win(). The answer is below.
Using dup2()
Since we need two parameters, we'll need to find a gadget for RDI and RSI. I'll use ROPgadget to find these.
$ ROPgadget --binary vuln | grep "pop rdi"
0x000000000040150b : pop rdi ; ret
$ ROPgadget --binary vuln | grep "pop rsi"
0x0000000000401509 : pop rsi ; pop r15 ; ret