Exploiting XXE to retrieve data by repurposing a local DTD

4 min read Medium PortSwigger
XXE
Contents

On this page

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, trigger an error message containing the contents of the /etc/passwd file.

You'll need to reference an existing DTD file on the server and redefine an entity from it.

and the hint it gives

Systems using the GNOME desktop environment often have a DTD at /usr/share/yelp/dtd/docbookx.dtd containing an entity called ISOamso.

this follows the error message lab. there we hosted our own dtd on the exploit server and pointed the parser at it. this lab imagines a server that will not make outbound connections, so we cannot host a dtd anywhere it will fetch. the answer is to stop bringing our own file and instead hijack a dtd that is already sitting on the server.

The idea#

the reason we needed an external dtd in the first place is that the good trick, nesting parameter entities to build an exfiltration entity, is not allowed inside the doctype we write in the request itself. an external file lifts that restriction. so if the server will not fetch our file, we need some other dtd file that already exists locally, and we need a way to slip our own logic into it.

that is where redefining an entity comes in. xml lets you define an entity more than once, and the first definition wins. many linux boxes running gnome ship a dtd at /usr/share/yelp/dtd/docbookx.dtd, and inside it there is a parameter entity named ISOamso. our plan is to load that local dtd, but just before we load it, redefine ISOamso ourselves to hold the nested entity payload that reads /etc/passwd and leaks it through an error. because our definition comes first, when the local dtd runs and references ISOamso, it uses our version instead of its own, and our payload fires from inside a file the server was happy to read.

Step 1 - Find the XML being parsed#

as with the rest of the series, opening a product and clicking check stock sends an xml body the server parses

a
XML
<?xml version="1.0" encoding="UTF-8"?>
<stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>

Step 2 - Redefine a local dtd entity#

we insert this doctype between the xml declaration and the stockCheck element.

XML
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY &#x25; file SYSTEM "file:///etc/passwd">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>

it looks dense, but it is the error message attack from the last lab wrapped in one extra move. reading it top to bottom.

<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd"> declares a parameter entity pointing at the dtd that already exists on the server. we do not resolve it yet, we just name it.

<!ENTITY % ISOamso '...'> redefines the ISOamso entity. this is the heart of it. the local dtd defines ISOamso too, but ours is declared first, so ours is the one that counts. whatever we put inside these quotes becomes what ISOamso expands to when the local dtd later references it.

inside that redefinition sits the familiar nested payload. <!ENTITY % file SYSTEM "file:///etc/passwd"> reads the target file into file. the next line builds an error entity whose path is file:///nonexistent/ with the contents of file glued on, so resolving it later will try to open a path that does not exist and spill the file into the error. then %eval; and %error; run those two steps in order.

the escaping is the fiddly part, and it is only there so each % and ' stays dormant until the right moment. &#x25; is a %, written this way so the parameter references are not acted on while we are still defining ISOamso, and only wake up when ISOamso is expanded. &#x26;#x25; is one level deeper again, it is a % that must survive being written into the eval entity and only become live when eval itself runs. &#x27; is just an apostrophe, needed because the whole ISOamso value is already wrapped in apostrophes and the inner declaration needs its own quotes.

finally %local_dtd; loads the real gnome dtd. as it processes, it hits its own reference to ISOamso, gets our redefined version, and runs our payload.

How the whole thing fits together#

so the shape of the attack is, we never bring a file to the server, we borrow one it already has. we point at the local gnome dtd but do not run it until we have quietly overwritten one of the entities it uses. when we then let it load, the local dtd unwittingly executes our redefined ISOamso, which reads /etc/passwd, wedges it into a broken file path, and forces the parser to open that path. the open fails, the error message quotes the bad path back, and the file contents ride out inside that error. it is the same error based exfiltration as before, the only new idea is sourcing the required external dtd from the server's own disk instead of from a server we host.

send the request, and the response carries the parser error with the contents of /etc/passwd inside it.

aa

with this, we solved the lab!