Intro
In my experience, .DS_Store
is an essential line in your personal scanning lists. These days, exposed .DS_Store
files can be rare, but it can be a useful string to use when trying to find hard-to-discover paths as well. Here’s a brief introduction to .DS_Store
, and a recount of a time when it really paid off for me.
What is a .DS_Store file?
The .DS_Store
file contains folder metadata for Mac users. These files are created automatically inside of archives, local folders, and even remotely mounted folders. More importantly, for us, these files can include file and folder names that we would have otherwise been unable to guess during normal directory bruteforcing techniques.
How can I read a .DS_Store file?
Luckily for us the format has been reversed engineered and you can use this simple one-liner or this Python-dsstore project to parse the file and read the contents. Additionally you can simply read the file with a text editor. You’ll just have to ignore the gibberish bits between the files/folders.
\showItemInfo_viewOptionsVersion_scrollPositionXYarrangeBy]labelOnBottomXiconSize_showIconPreview#?ð #@K #@( # Tnone #@P + A M V k z ‘ ¯ » È Ý ï ù"+4=?HIZ_`i j f i l e s 2vSrnlong f o n t sIlocblob ù .ÿÿÿÿÿÿ f o n t sbwspblob ¹bplist00Ö]ShowStatusBar[ShowToolbar[ShowTabView_ContainerShowSidebar\WindowBounds[ShowSidebar _{{2954, 825}, {880, 750}} #/;R_klmno‹
Œ f o n t slg1Scomp f o n t smoDDblob @ÀOµPìÃA f o n t smodDblob @ÀOµPìÃA f o n t sph1Scomp f o n t svSrnlong j q u e r y - 3 . 6 . 0 . m i n . j sIlocblob g .ÿÿÿÿÿÿ j q u e r y . m a s k . j sIlocblob A žÿÿÿÿÿÿ
How is .DS_Store helpful in an engagement?
A quick grep of SeclLists’ Web-Discovery
folder reveals that the file is included in all sizes of the file/word raft lists, quickhits.txt
, and dirsearch.txt
which are among the most popular pre-made bruteforce lists.
$ grep -i '.ds_store' *.txt
CGIs.txt:.DS_Store
dirsearch.txt:.ds_store
quickhits.txt:/.DS_Store
raft-large-files-lowercase.txt:.ds_store
raft-large-files.txt:.DS_Store
raft-large-files.txt:.ds_store
raft-large-words-lowercase.txt:.ds_store
raft-large-words.txt:.DS_Store
raft-large-words.txt:.ds_store
raft-medium-files-lowercase.txt:.ds_store
raft-medium-files.txt:.DS_Store
raft-medium-files.txt:.ds_store
raft-medium-words-lowercase.txt:.ds_store
raft-medium-words.txt:.DS_Store
raft-medium-words.txt:.ds_store
raft-small-files-lowercase.txt:.ds_store
raft-small-files.txt:.DS_Store
raft-small-words-lowercase.txt:.ds_store
raft-small-words.txt:.DS_Store
raft-small-words.txt:.ds_store
UnixDotfiles.fuzz.txt:/.DS_Store
However, this file doesn’t always make into the pentester’s personal shortlists, which I believe is a mistake.
Take at this payload position in Burp Intruder which can be used to potentially reveal directories that otherwise would return 404:
GET /§§/.DS_Store HTTP/1.1
Host: app.example.com
This might be appear to be useless unless a .DS_Store
file actually exists, but let’s take a closer look at our request/response behavior on this particular machine.
Advanced directory discovery.
Searching for the directory geoip
as normal returns a 404 error— so it must not exist, right?:
GET /geoip/ HTTP/1.1
Host: app.example.com
HTTP/1.1 404 Not Found
content-type: text/plain
content-length: 18
connection: close
404 page not found
Let’s try once more but look for a specific filename that surely doesn’t exist on the server:
GET /geoip/d035n073X157.txt HTTP/1.1
Host: app.example.com
HTTP/1.1 403 Forbidden
content-length: 0
connection: close
Now we hit a 403 Forbidden error. We know that sensitive files like .htaccess
or config files ending in .ini
will often return a 403, but this this file doesn’t exist so either the server settings forbid the requesting of .txt
files, or we have wandered into someplace we don’t belong and got a bit lucky that the server is setup to send 403s instead of 404s.
Let’s remove the extension and see:
GET /geoip/d035n073X157 HTTP/1.1
Host: app.example.com
HTTP/1.1 403 Forbidden
content-length: 0
connection: close
Yahtzee! It looks like we’ve really discovered a new directory. Let’s quickly double check our work before bombarding the client with 30,000 more requests for a false-positive URI!
GET /geoip1/d035n073X157 HTTP/1.1
Host: app.example.com
HTTP/1.1 404 Not Found
content-type: text/plain
content-length: 18
connection: close
404 page not found
Excellent! We’ve appended a character (1
) to the directory name and requested the same non-existent file and received a 404 Not Found error instead of the 403. Now I’m confident that geoip
is a valid directory. But first, let’s find some more interesting directories…
In search of a .DS_Store that doesn’t exist.
But what if we simply used the .DS_Store
file the entire time? Well, first we should do some due diligence checks.
Does the server return a 403 error for all dot files?
GET /.DS_Store HTTP/1.1
Host: app.example.com
HTTP/1.1 404 Not Found
content-type: text/plain
content-length: 18
connection: close
404 page not found
Our request returned at 404 instead of a 403 meaning that the server isn’t configured to automatically deny all requests to a file with a leading dot.
But what about the rules for our known good directory path specifically?
GET /geoip/.d035n073X157 HTTP/1.1
Host: app.example.com
HTTP/1.1 404 Not Found
content-type: text/plain
content-length: 18
connection: close
404 page not found
Another 404. It looks like there is no configuration or WAF preventing us from requesting dot files anywhere.
Let’s go ahead and scan for more directories using this technique. We’ll keep a lookout for 403 errors indicating that we’re on a valid path; and our fingers crossed for a 200! Our Intruder configuration will be:
GET /§§/.DS_Store HTTP/1.1
Host: app.example.com
And we quickly receive a hit. Requesting /banks/.DS_Store
gives us a 403. Unfortunately, scanning for sub directories gives us nothing but 404s, with and without using our .DS_Store trick.
Let’s go back to the basics and make a short list of well-known banks that serve our target’s geographical market and run it through Burp Intruder again.
HTTP/1.1 200 OK
Content-Length: 10244
Etag: "ta8b3p2bl"
Connection: close
1Bud1...[binarypsuedogibberish]...
Wow, we’ve actually found a .DS_Store
file in the wild! Next we should start a scan in the folder for other files and then take a look at our loot.
Our scan revealed the following files:
/main.css (Status: 200) [Size: 1022]
/print.css (Status: 200) [Size: 660]
/common.js (Status: 200) [Size: 10349]
/2.jpg (Status: 200) [Size: 20673]
/main.js (Status: 200) [Size: 962]
/1.jpg (Status: 200) [Size: 31282]
/lightbox.css (Status: 200) [Size: 6737]
/3.jpg (Status: 200) [Size: 17913]
/4.jpg (Status: 200) [Size: 9771]
/a.js (Status: 200) [Size: 137964]
/anchor.htm (Status: 200) [Size: 23290]
/ext.js (Status: 200) [Size: 4617]
And in .DS_Store
we find:
main.css
common.js
12.c74d23240ff553065b69.js
26.13554ebf550a57273b22.js
30.899dd121b5578d837ab8.js
bc77ddef.js
...
common.cfdce7ff8b2c43f86c772.js
gtm.js
I3RnZm6B
bc2fadef.js
anchor.htm
runtime.4b1d9a3261347e54.js
scripts.79bc7e3cacee9868.js
Hey, that’s a big help! We could have never bruteforced many of the additional files found. A lot of the files are minified versions and lazily-loaded parts, but because this app isn’t (supposed to be) a public facing interface we should thoroughly check these files for hardcoded API keys, API endpoints, and developer notes.
In essence, there is little reason why not to use .DS_Store (provided you’ve done your due diligence) as the target file for subdirectory scanning. Specifically, in environments where the server returns 403s for folders but not files or you are scanning and guessing multiple folders deep — places where developers don’t expect you to be — its a great tool to turn a shot-in-the-dark into something useful. If your environment permits it, you can save time waiting for scan results and potentially reveal files and folders that would not be otherwise discovered.
Happy hunting!