A non-hardened private key is derived using the equations shown below. Here small case letter represents private keys and large case represents public keys. G is the generator point, c is the chain code and i is the index number of the key generated. Kpar and cpar together represent the extended public key. kpar and cpar together represents the extended privat key.
k(i) = kpar + hash(Kpar, cpar, i)
rearranging you get, kpar = k(i) - hash(Kpar, cpar, i)
Now, let us say the attacker gets his hands on k(i) and xpub. You can generate public keys without the need of private keys using the xpub with the following equation: K(i) = Kpar + hash(Kpar, cpar, i)*G (check why this equation holds below in Appendix). The attacker is going to increment the index (i) in a loop until it generates the public key associated with k(i). When K(i) = k(i) * G the attacker knows the index number.
Thus with the index in his hand, he can just calculate the kpar from the equation kpar = k(i) - hash(Kpar, cpar, i).
Hardened keys prevent this by using the equation: k(i) = kpar + hash(kpar, cpar, i). So, although you get your hands on the xpub and the k(i), you will not be able to reverse engineer kpar as that variable is in the hash function which is one-way.
Appendix:
we saw above that k(i) = kpar + hash(Kpar, cpar, i)
=> k(i) *G = kpar*g + hash(Kpar, cpar, i)*G
=> K(i) = Kpar + hash(Kpar, cpar, i)*G