View Javadoc
1   package com.jsql.model.suspendable;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.util.CookiesUtil;
5   import com.jsql.util.JsonUtil;
6   import com.jsql.view.subscriber.Seal;
7   import com.jsql.model.exception.JSqlException;
8   import com.jsql.model.exception.StoppedByUserSlidingException;
9   import com.jsql.model.injection.strategy.blind.InjectionCharInsertion;
10  import com.jsql.model.injection.engine.MediatorEngine;
11  import com.jsql.model.injection.engine.model.Engine;
12  import com.jsql.model.suspendable.callable.CallablePageSource;
13  import com.jsql.util.I18nUtil;
14  import com.jsql.util.LogLevelUtil;
15  import org.apache.commons.lang3.RandomStringUtils;
16  import org.apache.commons.lang3.StringUtils;
17  import org.apache.logging.log4j.LogManager;
18  import org.apache.logging.log4j.Logger;
19  
20  import java.util.*;
21  import java.util.concurrent.CompletionService;
22  import java.util.concurrent.ExecutionException;
23  import java.util.concurrent.ExecutorCompletionService;
24  import java.util.concurrent.ExecutorService;
25  import java.util.regex.Pattern;
26  import java.util.stream.Stream;
27  
28  /**
29   * Runnable class, define insertionCharacter to be used during injection,
30   * i.e -1 in "...php?id=-1 union select...", sometimes it's -1, 0', 0, etc.
31   * Find working insertion char when error message occurs in source.
32   * Force to 1 if no insertion char works and empty value from user,
33   * Force to user's value if no insertion char works,
34   * Force to insertion char otherwise.
35   */
36  public class SuspendableGetCharInsertion extends AbstractSuspendable {
37  
38      private static final Logger LOGGER = LogManager.getRootLogger();
39      private final String parameterOriginalValue;
40  
41      public SuspendableGetCharInsertion(InjectionModel injectionModel, String parameterOriginalValue) {
42          super(injectionModel);
43          this.parameterOriginalValue = parameterOriginalValue;
44      }
45  
46      @Override
47      public String run(Input input) throws JSqlException {
48          String characterInsertionByUser = input.payload();
49  
50          ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().threadUtil().getExecutor("CallableGetInsertionCharacter");
51          CompletionService<CallablePageSource> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
52  
53          var characterInsertionFoundOrByUser = new String[1];
54          characterInsertionFoundOrByUser[0] = characterInsertionByUser;  // either raw char or cookie char, with star
55          List<String> charactersInsertionForOrderBy = this.initCallables(taskCompletionService, characterInsertionFoundOrByUser);
56  
57          var mediatorEngine = this.injectionModel.getMediatorEngine();
58          LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "[Step 3] Fingerprinting database and prefix using ORDER BY...");
59  
60          String charFromOrderBy = null;
61  
62          int total = charactersInsertionForOrderBy.size();
63          while (0 < total) {
64              if (this.isSuspended()) {
65                  throw new StoppedByUserSlidingException();
66              }
67              try {
68                  CallablePageSource currentCallable = taskCompletionService.take().get();
69                  total--;
70                  String pageSource = currentCallable.getContent();
71  
72                  List<Engine> enginesOrderByMatches = this.getEnginesOrderByMatch(mediatorEngine, pageSource);
73                  if (!enginesOrderByMatches.isEmpty()) {
74                      if (this.injectionModel.getMediatorEngine().getEngineByUser() == this.injectionModel.getMediatorEngine().getAuto()) {
75                          this.setEngine(mediatorEngine, enginesOrderByMatches);
76                          this.injectionModel.sendToViews(new Seal.ActivateEngine(mediatorEngine.getEngine()));
77                      }
78  
79                      charFromOrderBy = currentCallable.getCharacterInsertion();
80                      String finalCharFromOrderBy = charFromOrderBy;
81                      LOGGER.log(
82                          LogLevelUtil.CONSOLE_SUCCESS,
83                          "Found prefix [{}] using ORDER BY and compatible with Error strategy",
84                          () -> SuspendableGetCharInsertion.format(finalCharFromOrderBy)
85                      );
86                      break;
87                  }
88              } catch (InterruptedException e) {
89                  LOGGER.log(LogLevelUtil.IGNORE, e, e);
90                  Thread.currentThread().interrupt();
91              } catch (ExecutionException e) {
92                  LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
93              }
94          }
95          this.injectionModel.getMediatorUtils().threadUtil().shutdown(taskExecutor);
96          if (charFromOrderBy == null && characterInsertionFoundOrByUser[0] != null) {
97              charFromOrderBy = characterInsertionFoundOrByUser[0];
98          }
99          return this.getCharacterInsertion(characterInsertionByUser, charFromOrderBy);
100     }
101 
102     private void setEngine(MediatorEngine mediatorEngine, List<Engine> enginesOrderByMatches) {
103         if (
104             enginesOrderByMatches.size() == 1
105             && enginesOrderByMatches.getFirst() != mediatorEngine.getEngine()
106         ) {
107             mediatorEngine.setEngine(enginesOrderByMatches.getFirst());
108         } else if (enginesOrderByMatches.size() > 1) {
109             if (enginesOrderByMatches.contains(mediatorEngine.getPostgres())) {
110                 mediatorEngine.setEngine(mediatorEngine.getPostgres());
111             } else if (enginesOrderByMatches.contains(mediatorEngine.getMysql())) {
112                 mediatorEngine.setEngine(mediatorEngine.getMysql());
113             } else {
114                 mediatorEngine.setEngine(enginesOrderByMatches.getFirst());
115             }
116         }
117     }
118 
119     private List<Engine> getEnginesOrderByMatch(MediatorEngine mediatorEngine, String pageSource) {
120         return mediatorEngine.getEnginesForFingerprint()
121             .stream()
122             .filter(engine -> engine != mediatorEngine.getAuto())
123             .filter(engine -> StringUtils.isNotEmpty(
124                 engine.instance().getModelYaml().getStrategy().getConfiguration().getFingerprint().getOrderByErrorMessage()
125             ))
126             .filter(engine -> {
127                 Optional<String> optionalOrderByErrorMatch = Stream.of(
128                     engine.instance().getModelYaml().getStrategy().getConfiguration().getFingerprint().getOrderByErrorMessage()
129                     .split("[\\r\\n]+")
130                 )
131                 .filter(errorMessage ->
132                     Pattern
133                     .compile(".*" + errorMessage + ".*", Pattern.DOTALL)
134                     .matcher(pageSource)
135                     .matches()
136                 )
137                 .findAny();
138                 if (optionalOrderByErrorMatch.isPresent()) {
139                     LOGGER.log(
140                         LogLevelUtil.CONSOLE_SUCCESS,
141                         "Found [{}] using ORDER BY",
142                         engine
143                     );
144                 }
145                 return optionalOrderByErrorMatch.isPresent();
146             })
147             .toList();
148     }
149 
150     private List<String> initCallables(CompletionService<CallablePageSource> completionService, String[] characterInsertionFoundOrByUser) throws JSqlException {
151         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "[Step 2] Fingerprinting prefix using boolean match...");
152 
153         List<String> prefixValues = List.of(
154             RandomStringUtils.secure().next(10, "012"),  // trigger probable failure
155             StringUtils.EMPTY,  // trigger matching, compatible with backtick
156             "1"  // trigger eventual success
157         );
158         List<String> prefixQuotes = List.of(
159             "'",
160             StringUtils.EMPTY,
161             "`",
162             "\"",
163             "%bf'"  // GBK slash encoding use case
164         );
165         List<String> prefixParentheses = List.of(
166             StringUtils.EMPTY,
167             ")",
168             "))"
169         );
170         List<String> charactersInsertionForOrderBy = this.findWorkingPrefix(
171             prefixValues,
172             prefixQuotes,
173             prefixParentheses,
174             characterInsertionFoundOrByUser
175         );
176         this.submitCallables(completionService, charactersInsertionForOrderBy);
177         return charactersInsertionForOrderBy;
178     }
179 
180     private List<String> findWorkingPrefix(
181         List<String> prefixValues,
182         List<String> prefixQuotes,
183         List<String> prefixParentheses,
184         String[] characterInsertionFoundOrByUser
185     ) throws JSqlException {
186         List<String> charactersInsertionForOrderBy = new ArrayList<>();
187         for (String value: prefixValues) {
188             for (String quote: prefixQuotes) {
189                 for (String parenthesis: prefixParentheses) {
190                     String prefix = this.buildPrefix(value, quote, parenthesis);
191                     if (this.checkInsertionChar(
192                         characterInsertionFoundOrByUser,
193                         charactersInsertionForOrderBy,
194                         prefix
195                     )) {
196                         return charactersInsertionForOrderBy;
197                     }
198                 }
199             }
200         }
201         return charactersInsertionForOrderBy;
202     }
203 
204     private void submitCallables(CompletionService<CallablePageSource> completionService, List<String> charactersInsertionForOrderBy) {
205         for (String characterInsertion: charactersInsertionForOrderBy) {
206             completionService.submit(
207                 new CallablePageSource(
208                     characterInsertion.replace(
209                         InjectionModel.STAR,
210                         StringUtils.SPACE  // covered by cleaning
211                         + this.injectionModel.getMediatorEngine().getEngine().instance().sqlOrderBy()
212                     ),
213                     characterInsertion,
214                     this.injectionModel,
215                     "prefix#orderby"
216                 )
217             );
218         }
219     }
220 
221     private String buildPrefix(String value, String quote, String parenthesis) {
222         var prefixValueAndQuote = value + quote;
223         var requiresSpace = prefixValueAndQuote.matches(".*\\w$") && parenthesis.isEmpty();
224         return prefixValueAndQuote + parenthesis + (requiresSpace ? "%20" : StringUtils.EMPTY);
225     }
226 
227     private boolean checkInsertionChar(
228         String[] characterInsertionFoundOrByUser,
229         List<String> charactersInsertionForOrderBy,
230         String prefixParenthesis
231     ) throws StoppedByUserSlidingException {  // requires prefix by user for cookie, else empty and failing
232         var isCookie = this.injectionModel.getMediatorMethod().getHeader() == this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection()
233             && this.injectionModel.getMediatorUtils().parameterUtil().getListHeader()
234             .stream()
235             .anyMatch(entry ->
236                 CookiesUtil.COOKIE.equalsIgnoreCase(entry.getKey())
237                 && entry.getValue().contains(InjectionModel.STAR)
238             );
239 
240         var isJson = false;
241         if (StringUtils.isNotBlank(this.parameterOriginalValue)) {  // can be null when path param
242             Object jsonEntity = JsonUtil.getJson(this.parameterOriginalValue);
243             isJson = !JsonUtil.createEntries(jsonEntity, "root", null).isEmpty();
244         }
245 
246         var isRawParamRequired = isJson || isCookie;
247 
248         if (isRawParamRequired) {
249             charactersInsertionForOrderBy.add(characterInsertionFoundOrByUser[0].replace(
250                 InjectionModel.STAR,
251                 prefixParenthesis
252                 + InjectionModel.STAR
253                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
254             ));
255         } else {
256             charactersInsertionForOrderBy.add(
257                 prefixParenthesis
258                 + InjectionModel.STAR
259                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
260             );
261         }
262 
263         InjectionCharInsertion injectionCharInsertion;
264         if (isRawParamRequired) {
265             injectionCharInsertion = new InjectionCharInsertion(
266                 this.injectionModel,
267                 characterInsertionFoundOrByUser[0].replace(InjectionModel.STAR, prefixParenthesis),
268                 characterInsertionFoundOrByUser[0].replace(InjectionModel.STAR, prefixParenthesis + InjectionModel.STAR)
269             );
270         } else {
271             injectionCharInsertion = new InjectionCharInsertion(
272                 this.injectionModel,
273                 prefixParenthesis,
274                 prefixParenthesis + InjectionModel.STAR
275                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
276             );
277         }
278 
279         if (this.isSuspended()) {
280             throw new StoppedByUserSlidingException();
281         }
282         if (injectionCharInsertion.isInjectable()) {
283             if (isRawParamRequired) {
284                 characterInsertionFoundOrByUser[0] = characterInsertionFoundOrByUser[0].replace(
285                     InjectionModel.STAR,
286                     prefixParenthesis + InjectionModel.STAR
287                     + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
288                 );
289             } else {
290                 characterInsertionFoundOrByUser[0] = prefixParenthesis
291                     + InjectionModel.STAR
292                     + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment();
293             }
294 
295             LOGGER.log(
296                 LogLevelUtil.CONSOLE_SUCCESS,
297                 "Found [{}] using boolean match",
298                 () -> SuspendableGetCharInsertion.format(characterInsertionFoundOrByUser[0])
299             );
300             return true;
301         }
302         return false;
303     }
304 
305     private String getCharacterInsertion(String characterInsertionByUser, String characterInsertionDetected) {
306         String characterInsertionDetectedFixed = characterInsertionDetected;
307         if (characterInsertionDetectedFixed == null) {
308             characterInsertionDetectedFixed = characterInsertionByUser;
309             String logCharacterInsertion = characterInsertionDetectedFixed;
310             LOGGER.log(
311                 LogLevelUtil.CONSOLE_ERROR,
312                 "No prefix found, forcing to [{}]",
313                 () -> SuspendableGetCharInsertion.format(logCharacterInsertion)
314             );
315         } else if (
316             !SuspendableGetCharInsertion.format(characterInsertionByUser).isBlank()
317             && !SuspendableGetCharInsertion.format(characterInsertionByUser).equals(
318                 SuspendableGetCharInsertion.format(characterInsertionDetectedFixed)
319             )
320         ) {
321             String finalCharacterInsertionDetectedFixed = characterInsertionDetectedFixed;
322             LOGGER.log(
323                 LogLevelUtil.CONSOLE_INFORM,
324                 "Found prefix [{}], disable auto search in Preferences to force [{}]",
325                 () -> SuspendableGetCharInsertion.format(finalCharacterInsertionDetectedFixed),
326                 () -> SuspendableGetCharInsertion.format(characterInsertionByUser)
327             );
328         } else {
329             LOGGER.log(
330                 LogLevelUtil.CONSOLE_INFORM,
331                 "{} [{}]",
332                 () -> I18nUtil.valueByKey("LOG_USING_INSERTION_CHARACTER"),
333                 () -> SuspendableGetCharInsertion.format(characterInsertionDetected)
334             );
335         }
336         return characterInsertionDetectedFixed;
337     }
338 
339     public static String format(String prefix) {  // trim space prefix in cookie
340         return prefix.trim().replaceAll("(%20)?"+ Pattern.quote(InjectionModel.STAR) +".*", StringUtils.EMPTY);
341     }
342 }