My site still shows spam pages on addresses I do not recognise, while
WordPress is clean and the cache is cleared. Check whether real folders with
an index.html exist at those paths in the web root and list which ones they
are. Do not delete anything yet.
Here is the scenario: your site was hacked and cleaned up. The suspicious files are gone, the unknown administrator is removed, every password has been replaced, the database has been checked. You cleared the cache, twice.
And still: you open one of those spam addresses and the spam page is right there. Exactly the same, no difference.
At that moment nearly everyone thinks the same thing: it must still be cached. It almost never is.
What is really going on
WordPress works with a single entrance. Every address arrives at index.php, and WordPress works out which page you mean. That happens through a rule in .htaccess with an important condition in it: only send the request to WordPress if no real file and no real folder exists at that path.
That is the gap. If an attacker creates a real folder for every spam address with an index.html in it, that folder exists. The condition kicks in, the web server serves that file, and WordPress never gets a turn.
So you can scrub the entire WordPress installation, check the pages in the editor and clear the cache ten times. It does nothing, because you are looking at a layer that sits underneath all of that. It looks like stubborn caching and it behaves like stubborn caching, but it is simply a file on disk.
Why attackers do this: these pages are for search engines. They are full of links to something they want to sell and they borrow the good name of your domain. That is also why there are many of them straight away, not one.
How to see it in a single request
You do not need to dig through files for this. The difference is in the response headers.
Request the headers of such a spam address and compare them with a page you know for sure WordPress produces. A static file gives you two characteristics a WordPress page does not have: a last-modified header, and an etag built from the file size and the modification time. If that last-modified falls exactly inside the hack window, you know for certain.
A page produced by WordPress itself does not have those characteristics, not even when it comes from the cache.
Request only the response headers of these three addresses and put them side
by side in a table: status code, last-modified and etag. For each address,
say whether it is a static file or a page from WordPress.
The list the attacker made for you
Now you need to know how many there are. And there is a bitter joke in this: the attacker left that list behind.
This approach only works if search engines find the pages, so a separate sitemap is placed in the web root, often along with a modified robots.txt. That sitemap is the full inventory of everything that was created. Sometimes each folder holds its own sitemap as well.
So: look in the web root for sitemap files you do not recognise, and compare them with the sitemap your own SEO plugin generates. Whatever is in there and is not yours is your work list.
Cleaning up, and the pitfall in it
Two things often go wrong here.
Deleting only the index.html is not enough. If you remove the file and leave the folder, the folder still exists. The condition in .htaccess looks at the folder, not at the file. The result is that visitors get an error from then on instead of the spam. It is not solved, and nothing changes in the search results. Remove the whole folder.
You forget the files in the web root. The sitemap files and the modified robots.txt do not sit in a folder, they sit loose. Those have to go too, otherwise search engines keep requesting the old addresses.
Deleting is irreversible, so in the assistant you always get an approval for it. Ask for the list first, look at it, and only then give permission to delete. With us deleting goes through your hosting account's trash, so you still have a period in which you can pull something back if you took too much.
Checking that you are done
The check is a sweep across all the addresses. Two lists side by side: the addresses from the attacker's sitemap, and the addresses of your real pages.
From the first list you want to see an error code, because those pages should not exist. From the second list you want to see working pages, so you know you did not accidentally throw away something of your own. That is one instruction:
Request the status code of every address in this list and of every page of my
WordPress site. Put in a table which ones still return a page and which
return an error code. Flag anything that is not right.
After that there is one more thing: the search engines do not know yet. The addresses are indexed, and that does not go away because the files are gone. Resubmit your own sitemap and ask Search Console to remove the worst addresses. Count on weeks, not days.
And run the regular hack check once more afterwards, so you know the way in is closed too: what the hack check looks at.
Common mistakes
Clearing the cache one more time. If this is the problem, clearing changes nothing. Look at the response headers first, and you know within thirty seconds whether you are looking at a file or at a page.
Thinking it is back because the hack is back. These pages were left behind, not newly placed. That is an important difference, because it means your cleanup work was not wasted.
Removing only the index.html. Then you trade spam for an error message. Remove the whole folder.
Taking your own folders with it. Many sites have real folders in the web root that serve a purpose, for instance an old landing page or a folder with downloads. Go by the list and do not delete on instinct.
Stopping as soon as your site looks right. The search results lag behind. Leave the check in place for a few more weeks and see whether anything comes back. If it does, the way in is still open, and that is a different problem from this one.